From 8679200f41430c68a784a3373dfe8f91f62eed1e Mon Sep 17 00:00:00 2001 From: root Date: Tue, 15 Sep 2026 13:57:10 +0800 Subject: [PATCH] Initial commit Co-Authored-By: Claude Opus 5 --- .gitignore | 63 + 91迭代后端技术方案.md | 231 ++ 91迭代配置改动点.md | 173 + AGENTS.md | 41 + Dockerfile | 5 + Makefile | 20 + README.md | 105 + ...IDEO_RECOMMENDATION_BACKEND_TECH_DESIGN.md | 441 ++ VIP卡片UI配置接口.md | 78 + app/api/active2023ctrl/lottery.go | 69 + app/api/active2023ctrl/user_info.go | 102 + app/api/activityctrl/activityctrl.go | 275 ++ app/api/actvctrl/actv.go | 52 + app/api/adsctrl/ads.go | 111 + app/api/advance_config_ctrl/advance_config.go | 34 + app/api/ai_changeface_ctrl/ai_changeface.go | 247 ++ .../ai_image_to_video.go | 114 + app/api/ai_mate_ctrl/ai_mate.go | 151 + app/api/ai_mate_v2_ctrl/ai_mate.go | 45 + .../ai_text_to_image_ctrl/aitexttoimage.go | 112 + .../ai_text_to_novel_ctrl/aitexttonovel.go | 138 + app/api/ai_undress_ctrl/ai_undress.go | 218 + app/api/aiplazactrl/aiplaza.go | 116 + .../aitemplatemodulectrl/aitemplatemodule.go | 88 + app/api/analyticsctrl/analytics.go | 30 + app/api/annouctrl/annouctrl.go | 64 + app/api/backpackctrl/backpack.go | 51 + app/api/checkinctrl/checkin.go | 61 + app/api/commentctrl/commentctrl.go | 307 ++ app/api/common.go | 5 + app/api/contentmarkerctrl/content_marker.go | 34 + app/api/couponctl/couponctl.go | 136 + app/api/customerCtrl/user.go | 119 + app/api/dramactrl/drama.go | 203 + app/api/exchcodectrl/exchcodectrl.go | 105 + app/api/followctrl/followctrl.go | 149 + app/api/goldextractrl/goldextractrl.go | 58 + app/api/health_check_ctrl/health.go | 38 + app/api/hotspotctr/hotspot.go | 157 + app/api/imctrl/ad.go | 71 + app/api/imctrl/imctrl.go | 435 ++ app/api/imgroupctrl/imgroup.go | 101 + app/api/imgroupmemberctrl/imgroupmember.go | 39 + app/api/immessagectrl/immessage.go | 74 + app/api/infmtctrl/infmtctrl.go | 90 + app/api/integeralctrl/integral.go | 75 + app/api/likectrl/likectrl.go | 116 + app/api/mediabookshelfctrl/mediabookshelf.go | 145 + app/api/mediacontentctrl/mediacontent.go | 94 + app/api/mediactrl/media.go | 295 ++ app/api/mediatagctrl/mediatag.go | 65 + app/api/messagectrl/messagectrl.go | 283 ++ app/api/minectrl/feedbackctrl.go | 62 + app/api/minectrl/followctrl.go | 209 + app/api/minectrl/reportctrl.go | 82 + app/api/modulectrl/module.go | 135 + app/api/nakedchatctrl/nakedchat.go | 66 + app/api/nakedchatorderctrl/nakedchatorder.go | 39 + app/api/newactivityctrl/gameactivity.go | 124 + app/api/newactivityctrl/newactivity.go | 296 ++ app/api/newactivityctrl/questionnre.go | 91 + app/api/notictrl/notificationctrl.go | 41 + app/api/officialWebsitectrl/album.go | 48 + app/api/officialWebsitectrl/basicData.go | 28 + app/api/officialWebsitectrl/business.go | 23 + app/api/officialWebsitectrl/hero.go | 44 + app/api/officialWebsitectrl/job.go | 23 + app/api/officialWebsitectrl/news.go | 44 + app/api/officialWebsitectrl/partner.go | 29 + app/api/officialWebsitectrl/recruit.go | 23 + app/api/officialWebsitectrl/tag.go | 23 + app/api/officialWebsitectrl/video.go | 70 + app/api/officialctrl/officialCtrl.go | 34 + app/api/paymentguidectrl/payment_guide.go | 50 + app/api/pingctrl/pingctrl.go | 911 +++++ app/api/productctrl/productctrl.go | 226 ++ app/api/publishctrl/publish.go | 122 + app/api/rankctrl/rankctrl.go | 27 + app/api/rechargectrl/DaBaiSha.go | 72 + app/api/rechargectrl/pay_center.go | 122 + app/api/rechargectrl/rechargectrl.go | 217 + app/api/rechargectrl/yinse.go | 64 + app/api/recommctrl/recommctrl.go | 145 + app/api/recreationctrl/recreationctrl.go | 56 + app/api/scenebannerctrl/banner.go | 50 + app/api/searchctrl/searchctrl.go | 301 ++ app/api/sharectrl/sharectrl.go | 138 + app/api/signrecordctrl/signrecord.go | 44 + app/api/smsctrl/smsctrl.go | 34 + app/api/statcenterctl/proto.go | 107 + app/api/statcenterctl/statcenterctr.go | 200 + app/api/statcenterctl/sync.go | 579 +++ app/api/statcenterctl/utils.go | 24 + app/api/staticctrl/staticctrl.go | 29 + app/api/tagctrl/tagctrl.go | 401 ++ app/api/taskctrl/sign.go | 86 + app/api/taskctrl/task.go | 238 ++ app/api/tonectrl/tonectrl.go | 50 + app/api/txnactctr/txnactctr.go | 215 + app/api/updownctrl/updownctrl.go | 667 +++ app/api/updownctrl/updownctrl_test.go | 58 + app/api/userctrl/jio.go | 46 + app/api/userctrl/privilege.go | 40 + app/api/userctrl/userctrl.go | 1048 +++++ app/api/userctrl/userwatchrecord.go | 40 + app/api/versionctrl/versionctrl.go | 54 + app/api/vidctrl/newsctrl.go | 143 + app/api/vidctrl/vidctrl.go | 1092 +++++ .../video_gold_coin_ctrl/video_gold_coin.go | 43 + app/api/videoactivityctrl/videoactivity.go | 67 + app/api/vipctrl/vipctrl.go | 223 + app/api/walletctrl/goldlog.go | 246 ++ app/api/walletctrl/store.go | 128 + app/api/walletctrl/wallectctrl.go | 48 + app/api/withdrawctrl/goldfish.go | 58 + app/api/withdrawctrl/withdrawctrl.go | 201 + app/api/withdrawctrl/yinse.go | 40 + app/api/wonderctrl/wonderctrl.go | 62 + app/appg/ai_callback.go | 14 + app/appg/appg.go | 229 ++ app/appg/elasticg.go | 24 + app/appg/mong.go | 31 + app/appg/redisg.go | 29 + app/appg/shortrecommend.go | 43 + app/appg/shortrecommend_test.go | 36 + app/appg/static.go | 30 + app/main.go | 177 + app/middleware/activityauth/activityauth.go | 55 + app/middleware/authuser/authuser.go | 265 ++ app/middleware/authuser/authuser_test.go | 30 + app/middleware/datacenter/datacenter.go | 123 + app/middleware/ip/ip.go | 13 + app/middleware/ipblock/ipblock.go | 103 + app/middleware/limitHandler/limit.go | 276 ++ app/middleware/requestEncrypt/decrypt.go | 92 + app/middleware/requestEncrypt/encrypt.go | 1 + app/middleware/requestEncrypt/private.pem | 16 + .../requestEncrypt/privatePKCs1.pem | 15 + app/middleware/requestEncrypt/public.pem | 6 + app/middleware/vercheck/vercheck.go | 66 + app/middleware/verfyparam/verfyparam.go | 194 + app/middleware/verfyparam/verfyparam_test.go | 32 + app/middleware/visitlog/visitlog.go | 134 + app/middleware/watcher/watch.go | 28 + app/proto/ping.go | 282 ++ app/proto/ping_short_drama_card_test.go | 27 + app/proto/product.go | 64 + app/proto/product_test.go | 55 + app/proto/pub.go | 4 + app/proto/recharge.go | 36 + app/proto/recomm.go | 9 + app/proto/search.go | 21 + app/proto/statsync.go | 54 + app/router/active_2023.go | 16 + app/router/activity.go | 24 + app/router/actv.go | 16 + app/router/ads.go | 16 + app/router/advance_config.go | 13 + app/router/ai_change_face.go | 23 + app/router/ai_image_to_video.go | 16 + app/router/ai_mate.go | 16 + app/router/ai_mate_test.go | 56 + app/router/ai_mate_v2.go | 12 + app/router/ai_mod.go | 15 + app/router/ai_text_to_image.go | 16 + app/router/ai_text_to_novel.go | 17 + app/router/ai_undress.go | 16 + app/router/aiplaza.go | 16 + app/router/aitemplatemodule.go | 16 + app/router/analytics.go | 14 + app/router/analytics_test.go | 18 + app/router/annou.go | 14 + app/router/backpack.go | 14 + app/router/base.go | 199 + app/router/checkin.go | 16 + app/router/comment.go | 23 + app/router/content_marker.go | 12 + app/router/coupon.go | 20 + app/router/customer.go | 19 + app/router/defray.go | 30 + app/router/drama.go | 20 + app/router/drama_test.go | 27 + app/router/exchcode.go | 16 + app/router/follow.go | 17 + app/router/goldextra.go | 14 + app/router/health.go | 13 + app/router/hotspot.go | 18 + app/router/im.go | 25 + app/router/imgroup.go | 16 + app/router/imgroupmember.go | 14 + app/router/immessage.go | 15 + app/router/infmt.go | 19 + app/router/integral.go | 16 + app/router/iteration_routes_test.go | 32 + app/router/like.go | 15 + app/router/media.go | 24 + app/router/mediabookshelf.go | 17 + app/router/mediacontent.go | 15 + app/router/mediatag.go | 15 + app/router/message.go | 21 + app/router/mine.go | 23 + app/router/module.go | 15 + app/router/nakedchat.go | 15 + app/router/nakedchatorder.go | 14 + app/router/newactivity.go | 31 + app/router/notification.go | 15 + app/router/official.go | 13 + app/router/officialWebsite.go | 62 + app/router/payment_guide.go | 15 + app/router/ping.go | 24 + app/router/product.go | 21 + app/router/proxy.go | 13 + app/router/publish.go | 15 + app/router/rank.go | 14 + app/router/rchg.go | 25 + app/router/recomm.go | 18 + app/router/recreation.go | 12 + app/router/scene_banner.go | 14 + app/router/search.go | 20 + app/router/share.go | 15 + app/router/signrecord.go | 15 + app/router/sms.go | 15 + app/router/static.go | 13 + app/router/store.go | 18 + app/router/tag.go | 26 + app/router/task.go | 25 + app/router/tone.go | 14 + app/router/txnact.go | 47 + app/router/updownLoad.go | 29 + app/router/user.go | 54 + app/router/userInvite.go | 23 + app/router/version.go | 14 + app/router/vid.go | 49 + app/router/vid_consume_route_test.go | 30 + app/router/video_gold_coin.go | 13 + app/router/videoactivity.go | 15 + app/router/vip.go | 20 + app/router/wallet.go | 25 + app/router/withdraw.go | 27 + app/router/wonder.go | 14 + app/service/active2023ser/lottery.go | 414 ++ app/service/active2023ser/type.go | 15 + app/service/activityclient/client.go | 7 + app/service/activityclient/deduct.go | 120 + app/service/activityclient/domain.go | 161 + app/service/activityclient/hongbao.go | 100 + .../activityclient/hongbao_countdown.go | 70 + app/service/activityclient/sign.go | 37 + app/service/activityclient/svcsign.go | 69 + app/service/activityser/activityser.go | 573 +++ app/service/activityser/deduct.go | 156 + app/service/activityser/reward.go | 378 ++ app/service/actvser/actv.go | 51 + app/service/actvser/type.go | 11 + app/service/adser/adser.go | 83 + app/service/adser/advertiseV2.go | 154 + app/service/adser/jtAdvertise.go | 262 ++ app/service/adser/recreationSer.go | 222 + app/service/adser/stat.go | 91 + .../advance_config_ser/advance_config.go | 134 + app/service/advance_ser/advance.go | 111 + .../ai_changeface_img.go | 329 ++ .../ai_changeface_ser/ai_changeface.go | 384 ++ .../ai_image_to_video.go | 359 ++ app/service/ai_mate_ser/ai_mate.go | 383 ++ app/service/ai_mate_ser/ai_mate_test.go | 164 + .../ai_text_to_image_ser/aitexttoimage.go | 372 ++ .../ai_text_to_novel_ser/aitexttonovel.go | 385 ++ app/service/ai_undress_server/ai_undress.go | 330 ++ app/service/aiautoser/automation.go | 329 ++ app/service/aiplazaser/aiplaza.go | 325 ++ app/service/aiser/ai.go | 244 ++ app/service/aiser/ai_test.go | 219 + .../aitemplatemoduleser/aitemplatemodule.go | 87 + app/service/annouser/annou.go | 63 + app/service/annouser/http.go | 56 + app/service/annouser/type.go | 55 + app/service/backpackser/backpack.go | 15 + app/service/checkinser/checkin.go | 644 +++ app/service/collectser/collectser.go | 240 ++ app/service/commentser/cmtchild.go | 71 + app/service/commentser/cmtcoverter.go | 328 ++ app/service/commentser/cmtlimitser.go | 32 + app/service/commentser/cmtparent.go | 251 ++ app/service/commentser/cmtsend.go | 276 ++ app/service/commentser/commentser.go | 355 ++ app/service/commentser/reply.go | 118 + app/service/common.go | 534 +++ .../contentmarkerser/content_marker.go | 182 + .../contentmarkerser/content_marker_test.go | 229 ++ app/service/couponser/couponser.go | 459 +++ app/service/currencyser/currency.go | 51 + app/service/customerser/customer.go | 284 ++ app/service/dramaser/activity.go | 174 + app/service/dramaser/download.go | 109 + app/service/dramaser/drama.go | 341 ++ app/service/dramaser/drama_test.go | 115 + app/service/dramaser/feed_cursor.go | 274 ++ app/service/dramaser/feed_cursor_test.go | 84 + app/service/dramaser/topic.go | 241 ++ app/service/dramaser/type.go | 85 + app/service/emailser/email.go | 54 + app/service/exchcodeser/exchcodeser.go | 203 + app/service/feedbackser/feedback.go | 39 + app/service/filterser/filter.go | 27 + app/service/followser/followser.go | 360 ++ app/service/health_check_ser/health.go | 546 +++ app/service/health_check_ser/type.go | 54 + app/service/imadser/advertise.go | 133 + app/service/imgroupmemberser/imgroupmember.go | 61 + app/service/imgroupser/imgroup.go | 63 + app/service/immessageser/immessage.go | 80 + app/service/imser/bridge.go | 312 ++ app/service/imser/imser.go | 89 + app/service/imser/sdk.go | 458 +++ app/service/infmtser/infmtser.go | 234 ++ .../integral_config_ser/integral_config.go | 524 +++ app/service/likeser/likeser.go | 279 ++ app/service/likeser/page.go | 140 + app/service/luckydrawser/base.go | 102 + app/service/luckydrawser/lucky_draw.go | 206 + app/service/luckydrawser/tool.go | 212 + app/service/luckydrawser/type.go | 47 + app/service/m3u8ticket/sign.go | 159 + app/service/m3u8ticket/ticket.go | 377 ++ app/service/m3u8ticket/ticket_test.go | 279 ++ .../mediabookshelfser/mediabookshelf.go | 235 ++ .../mediacontentser/drama_access_test.go | 52 + app/service/mediacontentser/mediacontent.go | 412 ++ .../mediacontentser/mediacontent_test.go | 68 + app/service/mediaser/help.go | 244 ++ app/service/mediaser/help_test.go | 38 + app/service/mediaser/media.go | 1302 ++++++ app/service/mediaser/media_sort_test.go | 64 + app/service/mediaser/type.go | 9 + app/service/mediatagser/mediatag.go | 204 + app/service/messageser/message.go | 861 ++++ app/service/mineser/mineser.go | 144 + app/service/moduleser/module.go | 127 + app/service/modulevideoser/modulevideo.go | 120 + .../modulevideoser/modulevideo_test.go | 86 + .../nakedchatorderser/nakedchatorder.go | 68 + app/service/nakedchatser/nakedchat.go | 70 + app/service/notiser/email.go | 33 + app/service/notiser/notificationser.go | 43 + app/service/officialWebsiteser/album.go | 161 + app/service/officialWebsiteser/basicData.go | 225 + app/service/officialWebsiteser/business.go | 66 + app/service/officialWebsiteser/common.go | 13 + app/service/officialWebsiteser/hero.go | 314 ++ app/service/officialWebsiteser/job.go | 47 + app/service/officialWebsiteser/news.go | 163 + app/service/officialWebsiteser/partner.go | 37 + app/service/officialWebsiteser/recruit.go | 97 + app/service/officialWebsiteser/tag.go | 48 + app/service/officialWebsiteser/video.go | 227 ++ app/service/officialser/official.go | 62 + app/service/paymentguideser/payment_guide.go | 349 ++ .../paymentguideser/payment_guide_test.go | 397 ++ app/service/productser/drama.go | 196 + app/service/productser/dto.go | 17 + app/service/productser/handler.go | 1401 +++++++ app/service/productser/productser.go | 3003 ++++++++++++++ app/service/productser/tool.go | 212 + .../productser/vip_experiment_attribution.go | 85 + .../vip_experiment_attribution_test.go | 78 + app/service/proxyser/error.go | 31 + app/service/proxyser/proxyser.go | 476 +++ app/service/publishser/publish.go | 144 + app/service/publishser/type.go | 79 + app/service/questionnreser/questionnreser.go | 98 + app/service/rankser/rankser.go | 241 ++ app/service/rankser/type.go | 36 + app/service/rechargeser/new_pay.go | 785 ++++ app/service/rechargeser/paytype_config.go | 59 + app/service/rechargeser/rechargeser.go | 984 +++++ app/service/rechargeser/type.go | 251 ++ .../rechargeser/type_attribution_test.go | 204 + app/service/rechargeser/vip_attribution.go | 93 + app/service/recommser/recommser.go | 423 ++ app/service/search/search.go | 203 + app/service/search/type.go | 12 + app/service/searchaccessser/search_access.go | 75 + .../searchaccessser/search_access_test.go | 51 + .../audiobooksearcher/audiobookSearcher.go | 76 + .../searcher/audiobooksearcher/result.go | 31 + .../fictionsearcher/fictionSearcher.go | 63 + .../searcher/fictionsearcher/result.go | 31 + app/service/searcher/keyword.go | 91 + app/service/searcher/location.go | 51 + app/service/searcher/loufeng.go | 83 + app/service/searcher/opt.go | 57 + app/service/searcher/result.go | 23 + app/service/searcher/search.go | 6 + app/service/searcher/tag.go | 86 + app/service/searcher/tagsearcher/result.go | 32 + .../searcher/tagsearcher/tagSearcher.go | 87 + app/service/searcher/tonesearcher/result.go | 33 + .../searcher/tonesearcher/toneSearcher.go | 74 + app/service/searcher/user.go | 37 + app/service/searcher/usersearcher/result.go | 26 + .../searcher/usersearcher/userSearcher.go | 105 + app/service/searcher/video.go | 562 +++ app/service/searcher/vidhkwsearcher/result.go | 37 + .../vidhkwsearcher/vidHotKeyWordSearcher.go | 112 + app/service/searcher/vidhotsearcher/result.go | 40 + .../searcher/vidhotsearcher/vidhotsearcher.go | 117 + .../searcher/vidpcountsearcher/result.go | 40 + .../vidpcountsearcher/vidPlayCountSearcher.go | 73 + .../searcher/vidrichsearcher/result.go | 28 + .../vidrichsearcher/vidRichSearcher.go | 76 + app/service/searcher/vidsearcher/result.go | 41 + .../searcher/vidsearcher/vidSearcher.go | 72 + app/service/searcher/vidtagsearcher/result.go | 34 + .../searcher/vidtagsearcher/vidTagSearcher.go | 63 + .../vidtagsearcher/vidTagSearcher_test.go | 78 + .../searcher/vidtonesearcher/freeArea.go | 55 + .../searcher/vidtonesearcher/goldCoinArea.go | 80 + .../searcher/vidtonesearcher/latestUpload.go | 58 + .../searcher/vidtonesearcher/mostComments.go | 57 + .../searcher/vidtonesearcher/mostLikes.go | 57 + .../searcher/vidtonesearcher/officialRecom.go | 56 + .../searcher/vidtonesearcher/playAtMost.go | 57 + .../searcher/vidtonesearcher/result.go | 51 + .../vidtonesearcher/vidToneSearcher.go | 64 + app/service/shareser/recommend_share.go | 130 + app/service/shareser/recommend_share_test.go | 250 ++ app/service/shareser/share.go | 238 ++ app/service/shareser/share_test.go | 45 + app/service/shortrecommendser/fetch.go | 373 ++ app/service/shortrecommendser/fetch_test.go | 523 +++ app/service/signrecordser/signrecord.go | 196 + app/service/smsser/smsser.go | 168 + app/service/sys_config/freewatch.go | 10 + app/service/sys_config/paymentpopup.go | 192 + app/service/systemser/config.go | 14 + app/service/tagser/list.go | 89 + app/service/tagser/tag.go | 313 ++ app/service/tagser/type.go | 35 + app/service/taskser/app.go | 102 + app/service/taskser/sign.go | 433 ++ app/service/taskser/task.go | 1162 ++++++ app/service/taskser/tool.go | 20 + app/service/updownloadser/updownloadser.go | 116 + app/service/userser/privilegeser.go | 42 + app/service/userser/statuser.go | 103 + app/service/userser/type.go | 25 + app/service/userser/userser.go | 2044 ++++++++++ .../userwatchrecordserver/userwatchrecord.go | 34 + app/service/versionser/versionV2.go | 172 + app/service/versionser/versionV2_test.go | 120 + app/service/versionser/versionser.go | 190 + .../video_gold_coin_ser/video_gold_coin.go | 63 + .../videoactivityserver/video_activity.go | 76 + app/service/vidhelpser/free_trial.go | 120 + app/service/vidhelpser/free_trial_test.go | 134 + app/service/vidhelpser/h265.go | 15 + app/service/vidhelpser/h265_test.go | 119 + app/service/vidhelpser/helpser.go | 1418 +++++++ .../vidhelpser/helpser_concurrency_test.go | 114 + app/service/vidhelpser/mongohelp.go | 88 + app/service/vidhelpser/redishelp.go | 317 ++ app/service/vidser/acg_sort_test.go | 29 + app/service/vidser/dedup.go | 50 + app/service/vidser/discountArea.go | 97 + app/service/vidser/free_trial.go | 15 + app/service/vidser/free_watch_consume.go | 250 ++ app/service/vidser/free_watch_consume_test.go | 205 + app/service/vidser/library.go | 409 ++ app/service/vidser/m3u8sign.go | 26 + app/service/vidser/m3u8sign_test.go | 29 + app/service/vidser/media_library.go | 154 + app/service/vidser/module_refresh.go | 100 + app/service/vidser/module_refresh_test.go | 55 + app/service/vidser/ranking.go | 236 ++ app/service/vidser/redis.go | 48 + app/service/vidser/type.go | 317 ++ app/service/vidser/vidser.go | 2885 +++++++++++++ app/service/vipcardexperimentser/service.go | 348 ++ .../vipcardexperimentser/service_test.go | 479 +++ app/service/walletser/inviteuser.go | 330 ++ app/service/walletser/store.go | 146 + app/service/walletser/type.go | 30 + app/service/walletser/walletser.go | 317 ++ app/service/withdrawser/withdrawser.go | 267 ++ app/templates/index.html | 109 + app/templates/sources/img/right.png | Bin 0 -> 718 bytes app/templates/sources/js/touch.js | 2 + app/templates/sources/style/animate.css | 3609 +++++++++++++++++ app/templates/sources/style/common.css | 77 + app/templates/sources/style/page.css | 44 + app/templates/sources/style/reset.css | 136 + app/templates/sources/util/rem.js | 39 + app/templates/sources/util/tools.js | 31 + common/PromotionCodeUtil.go | 22 + common/aiMate/aiMate.go | 258 ++ common/aiService/aiService.go | 396 ++ common/aiService/rediskey.go | 20 + common/aiService/type.go | 46 + common/bank/bank.go | 96 + common/cache/cache.go | 34 + common/cachev2/base.go | 445 ++ common/cachev2/proto.go | 14 + common/cachev2/util.go | 76 + common/checkWx/checkWx.go | 65 + common/conf/conf.go | 23 + common/constant/cacheconst/cacheconst.go | 3 + common/constant/constant.go | 375 ++ .../constant/imnotifyconst/imnotifyconst.go | 136 + .../redisconst/most_new_module_test.go | 10 + common/constant/redisconst/redisconst.go | 688 ++++ common/convUtil.go | 187 + common/crypt/checkSign.go | 137 + common/crypt/crypt.go | 755 ++++ common/crypt/crypt_ex_test.go | 49 + common/crypt/ecb/ecb.go | 63 + common/crypt/rsa.go | 140 + common/daichong/daichong.go | 163 + common/daichong/dcfunction.go | 149 + common/daichong/new-dcfunction.go | 53 + common/daichong/newdaichong.go | 9 + common/dataReport/handle.go | 184 + common/dataReport/report_test.go | 31 + common/dataReport/type.go | 242 ++ common/datacenter/handle.go | 65 + common/datacenter/proto.go | 127 + common/db/codec.go | 108 + common/db/helper.go | 7 + common/db/mongo.go | 392 ++ common/db/mongoClient.go | 307 ++ common/db/opt.go | 27 + common/deduction.go | 26 + common/dramatopic/topic.go | 89 + common/elastic/elastic.go | 110 + common/elastic/elasticClient.go | 267 ++ common/elastic/elasticInit.go | 39 + common/email/email.go | 74 + common/email/init.go | 37 + common/enum/imad/position.go | 15 + common/export/export.go | 79 + common/export/interface.go | 20 + common/file/file.go | 103 + common/file/path.go | 32 + common/file/s3.go | 107 + common/filter/filter.go | 291 ++ common/filter/filterClient.go | 148 + common/game/game.go | 130 + common/ginUtil.go | 529 +++ common/go.go | 108 + common/googauth/googauth.go | 32 + common/googauth/htop.go | 64 + common/handleCong.go | 25 + common/hevcpull/sign.go | 269 ++ common/hevcpull/sign_test.go | 176 + common/httputil/http_client.go | 101 + common/httputil/http_get.go | 175 + common/httputil/http_post.go | 198 + common/httputil/http_post_json.go | 135 + common/httputil/httputil.go | 282 ++ common/imclient/client.go | 879 ++++ common/imclient/defaults.go | 6 + common/imclient/errors.go | 41 + common/imclient/history_message.go | 318 ++ common/ip.go | 78 + common/kafka/kafka.go | 74 + common/laosiji/comics.go | 114 + common/laosiji/comics_define.go | 74 + common/laosiji/laosiji.go | 262 ++ common/laosiji/movie.go | 142 + common/laosiji/movie_define.go | 156 + common/laosiji/novel.go | 115 + common/laosiji/novel_define.go | 32 + common/laosiji/post.go | 115 + common/laosiji/post_define.go | 69 + common/laosiji/textToImage.go | 14 + common/laosiji/tools.go | 487 +++ common/laosiji/tools_test.go | 217 + common/laosiji/undress.go | 14 + common/laosiji_app/ai_mate.go | 100 + common/laosiji_app/laosiji.go | 169 + common/laosiji_app/laosiji_test.go | 109 + common/localcache/localcache.go | 16 + common/log/log.go | 113 + common/m3u8/m3u8.go | 503 +++ common/m3u8/m3u8_test.go | 281 ++ common/maths/correlation.go | 44 + common/maths/data.go | 23 + common/maths/decimal.go | 78 + common/maths/deviation.go | 34 + common/maths/errors.go | 31 + common/maths/legacy.go | 17 + common/maths/load.go | 181 + common/maths/maths.go | 31 + common/maths/max.go | 16 + common/maths/mean.go | 11 + common/maths/min.go | 17 + common/maths/mode.go | 41 + common/maths/round.go | 23 + common/maths/sum.go | 25 + common/maths/variance.go | 49 + common/mdrsdb/mediaresourcedatabase.go | 291 ++ common/mdrsdb/type.go | 128 + common/media/h265_test.go | 45 + common/media/sync.go | 111 + common/media/syncMedia.go | 202 + common/media/useMedia.go | 502 +++ common/media/useMedia_test.go | 33 + common/media/worker.go | 59 + common/mediaCenter/base.go | 66 + common/mediaCenter/media.go | 168 + common/mediaCenter/mediaType.go | 134 + common/mediaCenter/mediaType_test.go | 57 + common/mergemap/mergemap.go | 66 + common/modelUtil.go | 73 + common/money.go | 15 + common/pageopt/match.go | 465 +++ common/parseFilter.go | 61 + common/picture.go | 38 + common/qrcode.go | 30 + common/rand.go | 188 + common/rchgutil/asen.go | 188 + common/rchgutil/base.go | 384 ++ common/rchgutil/bill.go | 170 + common/rchgutil/billExchg.go | 131 + common/rchgutil/dabaisha.go | 349 ++ common/rchgutil/dx.go | 263 ++ common/rchgutil/goldfishExchange.go | 190 + common/rchgutil/goldfishplus.go | 206 + common/rchgutil/izhifu.go | 273 ++ common/rchgutil/leihuo.go | 238 ++ common/rchgutil/lucky.go | 252 ++ common/rchgutil/recharge.go | 460 +++ common/rchgutil/recharge_cache_test.go | 61 + common/rchgutil/shark.go | 205 + common/rchgutil/yilianfu.go | 260 ++ common/rchgutil/yongli.go | 214 + common/rchgutil/yongliwanshun.go | 207 + common/rchgutil/zhongfu.go | 264 ++ common/redis/redis.go | 1026 +++++ common/redis/redis_test.go | 97 + common/redis/redisutil.go | 11 + common/redis/redsync.go | 11 + common/services/follow/tip.go | 35 + common/services/message/tip.go | 35 + common/services/services.go | 10 + common/shortrecommend/shortrecommend.go | 1260 ++++++ common/shortrecommend/shortrecommend_test.go | 1098 +++++ common/slice/sli.go | 297 ++ common/sms/fengniao/fengniao.go | 74 + common/sms/sms.go | 61 + common/stderr/code.go | 421 ++ common/stderr/customerr.go | 31 + common/stderr/error.go | 32 + common/stderr/msg.go | 209 + common/stderr/stderr.go | 47 + common/stderr/tip.go | 192 + common/store/store.go | 65 + common/synclock/lock.go | 80 + common/tf/tf.go | 73 + common/tg/sendOpt.go | 27 + common/tg/tg.go | 76 + common/timeutil/timerange/timerange.go | 82 + common/timeutil/timeslice/timeSlice.go | 16 + common/timeutil/timeutil.go | 183 + common/top/asistant/asistant.go | 54 + common/top/dailytop/grow.go | 113 + common/top/monthtop/grow.go | 109 + common/top/top.go | 24 + common/top/weektop/grow.go | 102 + common/top/yeartop/grow.go | 106 + common/truthutil/truthutil.go | 359 ++ common/truthutil/truthutil_test.go | 38 + common/typeConv.go | 102 + common/usertruth/scode.jpg | Bin 0 -> 16075 bytes common/usertruth/userTruth.go | 178 + common/utils.go | 19 + common/uuid.go | 7 + common/v10/upgradev10.go | 47 + common/v10/validate.go | 168 + common/version/version.go | 99 + common/vid.go | 45 + common/worker/worker.go | 98 + common/ysinterface/disc/disc.go | 8 + common/ysphone/phone.go | 11 + common/ysqr/error.go | 15 + common/ysqr/qr.go | 60 + common/ysurl/yslinks.go | 47 + doc.go | 12 + generate/common/generate.go | 364 ++ generate/common/helper.go | 52 + generate/model/aiplaza/main.go | 32 + generate/model/hotsearch/main.go | 33 + generate/model/imgroup/main.go | 26 + generate/model/imgroupmember/main.go | 20 + generate/model/immessage/main.go | 22 + generate/model/nakechatorder/main.go | 27 + generate/model/nakedchat/main.go | 33 + generate/model/sysconf/main.go | 31 + generate/tpl/app/api/{{PackageTableName}}.tpl | 66 + .../tpl/app/router/{{PackageTableName}}.tpl | 15 + .../tpl/app/service/{{PackageTableName}}.tpl | 63 + generate/tpl/config.go | 44 + generate/tpl/models/cache/data.tpl | 34 + .../tpl/models/cache/{{PackageTableName}}.tpl | 89 + generate/tpl/models/v/app.tpl | 12 + generate/tpl/models/v/type.tpl | 44 + .../tpl/models/v/{{PackageTableName}}.tpl | 144 + generate/tpl/web/api/{{PackageTableName}}.tpl | 142 + .../tpl/web/router/{{PackageTableName}}.tpl | 17 + .../tpl/web/service/{{PackageTableName}}.tpl | 81 + go.mod | 134 + go.sum | 540 +++ main.go | 38 + middleware/cors/cors.go | 133 + middleware/ginzap/ginzap.go | 100 + middleware/ginzap/ginzap_test.go | 17 + middleware/ip/ip.go | 37 + middleware/requestid/requestid.go | 39 + middleware/requestid/requestid_test.go | 82 + middleware/requestid/type.go | 29 + middleware/ua/ua.go | 101 + middleware/ua/ua_test.go | 89 + models/Init/init.go | 423 ++ .../aiimagetovideodata/aiimagetovideo.go | 88 + models/cache/aiimagetovideodata/data.go | 43 + models/cache/aiplazadata/aiplaza.go | 89 + models/cache/aiplazadata/data.go | 153 + .../aitemplatemoduledata/aitemplatemodule.go | 89 + models/cache/aitemplatemoduledata/data.go | 36 + .../cache/aitexttoimagedata/aitexttoimage.go | 89 + models/cache/aitexttoimagedata/data.go | 49 + .../cache/aitexttonoveldata/aitexttonovel.go | 88 + models/cache/aitexttonoveldata/data.go | 49 + models/cache/bannerjumpdata/bannerjump.go | 73 + models/cache/imgroupdata/data.go | 43 + models/cache/imgroupdata/imgroup.go | 89 + models/cache/imgroupmemberdata/data.go | 37 + .../cache/imgroupmemberdata/imgroupmember.go | 89 + models/cache/immessagedata/data.go | 52 + models/cache/immessagedata/immessage.go | 89 + models/cache/mediabookshelfdata/data.go | 116 + .../mediabookshelfdata/mediabookshelf.go | 110 + models/cache/mediacontentdata/mediacontent.go | 37 + models/cache/mediadata/media.go | 37 + models/cache/mediatagdata/mediatag.go | 94 + .../mediatagdimension.go | 82 + models/cache/nakedchatdata/data.go | 74 + models/cache/nakedchatdata/nakedchat.go | 89 + models/cache/nakedchatorderdata/data.go | 44 + .../nakedchatorderdata/nakedchatorder.go | 89 + models/cache/quicksearchdata/data.go | 85 + models/cache/quicksearchdata/quicksearch.go | 89 + models/cache/signrecorddata/data.go | 89 + models/cache/signrecorddata/signrecord.go | 88 + models/cache/sysconfdata/sysconf.go | 161 + models/cache/sysconfdata/sysconf_test.go | 27 + models/cache/viddata/vid.go | 36 + models/commod/commod.go | 156 + models/commod/consts.go | 44 + models/commod/dedType.go | 52 + models/commod/disc.go | 93 + models/commod/ldy.go | 14 + models/commod/product.go | 34 + models/commod/sort.go | 65 + models/commod/stat-center.go | 165 + models/e/daichongmod/app.go | 18 + models/e/daichongmod/web.go | 34 + models/e/sharemod/app.go | 41 + models/e/sharemod/type.go | 9 + models/esTable.go | 15 + models/l/adsclicklogmod/type.go | 74 + models/l/exchlogmod/exchangecode.go | 121 + models/l/exchlogmod/type.go | 37 + models/l/exchlogmod/web.go | 25 + models/l/loginlgmod/login.go | 127 + models/l/loginlgmod/type.go | 43 + models/l/lotterylgmod/lottery.go | 108 + models/l/lotterylgmod/type.go | 20 + models/l/lotterylgmod/web.go | 50 + models/l/operatorlgmod/operator.go | 148 + models/l/operatorlgmod/type.go | 27 + models/l/operatorlgmod/web.go | 32 + models/l/payvidlgmod/app.go | 6 + models/l/payvidlgmod/discStat.go | 114 + models/l/payvidlgmod/list.go | 84 + models/l/payvidlgmod/pay.go | 602 +++ models/l/payvidlgmod/type.go | 78 + models/l/playlgmod/play.go | 426 ++ models/l/playlgmod/type.go | 73 + models/l/playlgmod/web.go | 37 + models/l/pullgmod/pull.go | 62 + models/l/pullgmod/type.go | 23 + models/l/registermod/register.go | 57 + models/l/registermod/type.go | 25 + models/l/searchlogmod/searchLog.go | 148 + models/l/searchlogmod/type.go | 54 + models/l/synccdnmod/synccdn.go | 57 + models/l/synccdnmod/type.go | 51 + models/l/userdailytasklogmod/type.go | 71 + models/l/visitlogmod/list.go | 44 + models/l/visitlogmod/type.go | 38 + models/l/visitlogmod/visitLog.go | 187 + models/l/welfarelgmod/pull.go | 54 + models/l/welfarelgmod/type.go | 31 + models/s/dailyretentionmod/dailyRetention.go | 121 + models/s/dailyretentionmod/type.go | 33 + models/s/hotstatmod/hotstat.go | 203 + models/s/hotstatmod/type.go | 27 + models/s/kwrankmod/keywordrank.go | 176 + models/s/kwrankmod/list.go | 78 + models/s/kwrankmod/type.go | 66 + models/s/kwstatmod/keywordStat.go | 234 ++ models/s/kwstatmod/type.go | 51 + models/s/sectionstatmod/stat.go | 43 + models/s/sectionstatmod/type.go | 63 + models/s/statordermod/list.go | 95 + models/s/statordermod/order.go | 123 + models/s/statordermod/type.go | 90 + models/s/statrecordmod/statRecord.go | 182 + models/s/statrecordmod/type.go | 84 + models/s/stattagplaymod/tagPlay.go | 90 + models/s/stattagplaymod/type.go | 30 + models/s/statusermod/list.go | 31 + models/s/statusermod/type.go | 47 + models/s/statusermod/userstat.go | 141 + models/s/statvidmod/list.go | 93 + models/s/statvidmod/page.go | 128 + models/s/statvidmod/stat.go | 134 + models/s/statvidmod/sum.go | 44 + models/s/statvidmod/type.go | 58 + models/s/statvidtotalmod/stat.go | 117 + models/s/statvidtotalmod/type.go | 48 + models/s/useradverstatmod/aggregate.go | 63 + models/s/useradverstatmod/type.go | 29 + models/s/useradverstatmod/useradverstat.go | 94 + models/table.go | 274 ++ models/v/active2023mod/free.go | 56 + models/v/active2023mod/lottery.go | 40 + models/v/active2023mod/prize.go | 52 + models/v/active2023mod/type.go | 62 + models/v/active2023mod/user.go | 106 + models/v/activitymod/activity.go | 138 + models/v/activitymod/prize_pool.go | 146 + models/v/activitymod/prize_pool_type.go | 28 + models/v/activitymod/type.go | 32 + models/v/activitymod/user_free.go | 117 + models/v/activitymod/user_free_type.go | 17 + models/v/activitymod/user_record.go | 93 + models/v/activitymod/user_record_type.go | 26 + models/v/activitymod/web.go | 171 + models/v/actmod/activity.go | 55 + models/v/actmod/type.go | 57 + models/v/actmod/web.go | 16 + models/v/adminmod/admin.go | 246 ++ models/v/adminmod/type.go | 64 + models/v/adreviewmod/adreview.go | 169 + models/v/adreviewmod/error.go | 9 + models/v/adreviewmod/list.go | 69 + models/v/adreviewmod/type.go | 105 + models/v/adsmod/ads.go | 141 + models/v/adsmod/error.go | 9 + models/v/adsmod/list.go | 183 + models/v/adsmod/type.go | 118 + models/v/advanceconfigmod/advanceconfig.go | 130 + models/v/advanceconfigmod/app.go | 51 + models/v/advanceconfigmod/type.go | 32 + models/v/advanceconfigmod/web.go | 126 + models/v/advanceordermod/advanceorder.go | 254 ++ models/v/advanceordermod/app.go | 56 + models/v/advanceordermod/type.go | 62 + models/v/advanceordermod/web.go | 49 + models/v/aiUnDressmod/aiUnDress.go | 198 + models/v/aiUnDressmod/app.go | 165 + models/v/aiUnDressmod/type.go | 64 + models/v/aiUnDressmod/web.go | 105 + .../v/aichangefaceimgmod/aichangefaceimg.go | 177 + models/v/aichangefaceimgmod/app.go | 99 + models/v/aichangefaceimgmod/type.go | 62 + models/v/aichangefaceimgmod/web.go | 79 + models/v/aichangefacemod/aichangeface.go | 94 + models/v/aichangefacemod/app.go | 11 + models/v/aichangefacemod/type.go | 39 + models/v/aichangefacemod/web.go | 115 + .../v/aichangefacevidmod/aichangefacevid.go | 119 + models/v/aichangefacevidmod/app.go | 60 + models/v/aichangefacevidmod/type.go | 44 + models/v/aichangefacevidmod/web.go | 91 + models/v/aiimagetovideomod/aiimagetovideo.go | 146 + models/v/aiimagetovideomod/app.go | 23 + models/v/aiimagetovideomod/type.go | 111 + models/v/aiplazamod/aiplaza.go | 221 + models/v/aiplazamod/app.go | 38 + models/v/aiplazamod/type.go | 80 + .../v/aitemplatemodulemod/aitemplatemodule.go | 142 + models/v/aitemplatemodulemod/app.go | 12 + models/v/aitemplatemodulemod/type.go | 55 + models/v/aitexttoimagemod/aitexttoimage.go | 147 + models/v/aitexttoimagemod/app.go | 28 + models/v/aitexttoimagemod/type.go | 78 + models/v/aitexttonovelmod/aitexttonovel.go | 146 + models/v/aitexttonovelmod/type.go | 77 + models/v/annoumod/annou.go | 172 + models/v/annoumod/announce.go | 109 + models/v/annoumod/app.go | 24 + models/v/annoumod/type.go | 42 + models/v/annoumod/web.go | 85 + models/v/audioanchormod/audioanchormod.go | 323 ++ models/v/audioanchormod/es.go | 105 + models/v/audioanchormod/type.go | 69 + models/v/audiobookmod/audiobook.go | 631 +++ models/v/audiobookmod/es.go | 69 + models/v/audiobookmod/type.go | 145 + models/v/authoritymod/authority.go | 131 + models/v/authoritymod/page.go | 40 + models/v/authoritymod/type.go | 48 + models/v/backpackmod/app.go | 9 + models/v/backpackmod/backpack.go | 221 + models/v/backpackmod/type.go | 76 + models/v/backpackmod/web.go | 41 + models/v/bannerjumpmod/app.go | 19 + models/v/bannerjumpmod/bannerjump.go | 130 + models/v/bannerjumpmod/type.go | 62 + models/v/chatrobotmod/chatrobot.go | 98 + models/v/chatrobotmod/type.go | 63 + models/v/chatrobotmod/web.go | 74 + models/v/checkinconfigmod/checkinconfig.go | 83 + models/v/checkinconfigmod/type.go | 33 + models/v/checkinprizemod/checkinprize.go | 116 + models/v/checkinprizemod/type.go | 47 + models/v/cmtmod/agent.go | 149 + models/v/cmtmod/app.go | 173 + models/v/cmtmod/comment.go | 1053 +++++ models/v/cmtmod/list.go | 63 + models/v/cmtmod/type.go | 82 + models/v/cmtmod/web.go | 82 + models/v/collectmod/app.go | 38 + models/v/collectmod/collect.go | 387 ++ .../v/collectmod/collect_video_status_test.go | 68 + models/v/collectmod/type.go | 40 + models/v/contentlibmod/contentlib.go | 80 + models/v/contentlibmod/type.go | 24 + models/v/contentlibmod/web.go | 31 + models/v/contentreviewmod/crud.go | 377 ++ models/v/contentreviewmod/type.go | 121 + models/v/coupon_record_mod/app.go | 73 + models/v/coupon_record_mod/coupon_record.go | 236 ++ models/v/coupon_record_mod/type.go | 39 + models/v/coupon_record_mod/web.go | 34 + models/v/currencymod/base.go | 15 + models/v/currencymod/currency.go | 112 + models/v/currencymod/type.go | 80 + models/v/dailytaskmod/dailytask.go | 128 + models/v/dailytaskmod/web.go | 41 + models/v/discount_area_mod/app.go | 1 + models/v/discount_area_mod/dIscountArea.go | 123 + models/v/discount_area_mod/type.go | 24 + models/v/discount_area_mod/web.go | 75 + .../discount_area_video.go | 115 + models/v/discount_area_video_mod/type.go | 21 + models/v/errfedbkmod/errorfeedback.go | 147 + models/v/errfedbkmod/type.go | 64 + models/v/eventmod/event.go | 86 + models/v/eventmod/type.go | 32 + models/v/exchcodemod/exchcode.go | 174 + models/v/exchcodemod/type.go | 80 + models/v/exchcodemod/web.go | 63 + models/v/export_task_mod/exportTask.go | 57 + models/v/export_task_mod/type.go | 33 + models/v/faqmod/faq.go | 144 + models/v/faqmod/type.go | 42 + models/v/feedbackmod/feedback.go | 127 + models/v/feedbackmod/type.go | 43 + models/v/feedbackmod/web.go | 18 + models/v/fictionmod/es.go | 69 + models/v/fictionmod/fiction.go | 478 +++ models/v/fictionmod/type.go | 97 + models/v/filtermod/filter.go | 107 + models/v/filtermod/type.go | 21 + models/v/filtermod/web.go | 28 + models/v/followmod/app.go | 67 + models/v/followmod/follow.go | 332 ++ models/v/followmod/list.go | 55 + models/v/followmod/type.go | 46 + models/v/freeVidmod/freeVidConfig.go | 184 + models/v/freeVidmod/type.go | 45 + models/v/freeVidmod/web.go | 17 + models/v/fsuidmod/fsuid.go | 70 + models/v/fsuidmod/type.go | 19 + models/v/fundtransferlogmod/crud.go | 74 + models/v/fundtransferlogmod/type.go | 44 + models/v/goldcfgmod/goldcfg.go | 176 + models/v/goldcfgmod/type.go | 46 + models/v/goldcfgmod/web.go | 20 + models/v/goldextramod/goldextramod.go | 118 + models/v/goldextramod/type.go | 20 + models/v/hevctaskmod/hevctask.go | 116 + models/v/hevctaskmod/type.go | 33 + models/v/idmod/id.go | 110 + models/v/idmod/type.go | 34 + models/v/imagetopmod/imagetop.go | 97 + models/v/imagetopmod/type.go | 107 + models/v/imgroupmembermod/app.go | 15 + models/v/imgroupmembermod/imgroupmember.go | 154 + models/v/imgroupmembermod/type.go | 53 + models/v/imgroupmod/app.go | 21 + models/v/imgroupmod/imgroup.go | 149 + models/v/imgroupmod/type.go | 58 + models/v/immessagemod/app.go | 20 + models/v/immessagemod/immessage.go | 142 + models/v/immessagemod/type.go | 53 + models/v/immod/app.go | 13 + models/v/immod/im.go | 152 + models/v/immod/type.go | 94 + models/v/immod/web.go | 7 + models/v/imusermod/im_user.go | 147 + models/v/imusermod/type.go | 23 + models/v/integralconfigmod/app.go | 33 + models/v/integralconfigmod/integralconfig.go | 168 + models/v/integralconfigmod/type.go | 39 + models/v/integralconfigmod/web.go | 118 + models/v/integralexcangemod/app.go | 1 + .../v/integralexcangemod/integralexchange.go | 101 + models/v/integralexcangemod/type.go | 39 + models/v/integralexcangemod/web.go | 77 + models/v/ipblockmod/ipblock.go | 109 + models/v/ipblockmod/type.go | 29 + models/v/ipblockmod/web.go | 69 + models/v/ipwhitemod/ipwhite.go | 106 + models/v/ipwhitemod/type.go | 23 + models/v/ipwhitemod/web.go | 76 + models/v/jingangmod/app.go | 15 + models/v/jingangmod/jingang.go | 124 + models/v/jingangmod/type.go | 21 + models/v/ldycfgmod/ldy.go | 40 + models/v/ldycfgmod/type.go | 23 + models/v/likemod/app.go | 19 + models/v/likemod/like.go | 304 ++ models/v/likemod/like_video_status_test.go | 68 + models/v/likemod/list.go | 64 + models/v/likemod/type.go | 42 + models/v/locmod/app.go | 16 + models/v/locmod/hotCity.go | 148 + models/v/locmod/location.go | 210 + models/v/locmod/type.go | 74 + models/v/locmod/web.go | 38 + models/v/marqueemod/method.go | 20 + models/v/marqueemod/type.go | 31 + .../v/media_buy_record_mod/mediaBuyRecord.go | 288 ++ models/v/media_buy_record_mod/type.go | 23 + models/v/mediabookshelfmod/app.go | 27 + models/v/mediabookshelfmod/mediabookshelf.go | 293 ++ models/v/mediabookshelfmod/type.go | 29 + models/v/mediacontentmod/mediacontent.go | 392 ++ models/v/mediacontentmod/type.go | 51 + models/v/mediamod/app.go | 213 + models/v/mediamod/es.go | 154 + models/v/mediamod/media.go | 608 +++ .../v/mediamod/media_latest_published_test.go | 25 + models/v/mediamod/type.go | 130 + models/v/mediamod/update_marker.go | 48 + models/v/mediamod/web.go | 200 + .../mediatagdimensionmod/mediatagdimension.go | 167 + models/v/mediatagdimensionmod/type.go | 30 + models/v/mediatagdimensionmod/web.go | 11 + models/v/mediatagmod/mediatag.go | 312 ++ models/v/mediatagmod/type.go | 27 + models/v/messagemod/app.go | 100 + models/v/messagemod/message.go | 262 ++ models/v/messagemod/type.go | 95 + models/v/messagemod/web.go | 63 + models/v/moduleconfmod/app.go | 39 + models/v/moduleconfmod/moduleconf.go | 325 ++ models/v/moduleconfmod/moduleconf_test.go | 101 + models/v/moduleconfmod/refresh_test.go | 37 + models/v/moduleconfmod/snapshot_cache.go | 334 ++ models/v/moduleconfmod/snapshot_cache_test.go | 322 ++ models/v/moduleconfmod/type.go | 186 + models/v/moduleconfmod/web.go | 18 + models/v/modulesectionmod/section.go | 309 ++ models/v/modulesectionmod/type.go | 144 + models/v/modulevidmod/modulevid.go | 238 ++ models/v/modulevidmod/type.go | 45 + models/v/nakedchatmod/app.go | 43 + models/v/nakedchatmod/nakedchat.go | 149 + models/v/nakedchatmod/type.go | 71 + models/v/nakedchatordermod/app.go | 22 + models/v/nakedchatordermod/nakedchatorder.go | 142 + models/v/nakedchatordermod/type.go | 60 + models/v/newactivity/activitymodel.go | 139 + models/v/newactivity/activitysold.go | 78 + models/v/newactivity/activitystock.go | 52 + models/v/newactivity/activitywin.go | 46 + models/v/newactivity/type.go | 164 + models/v/noticefmtmod/list.go | 98 + models/v/noticefmtmod/notice.go | 142 + models/v/noticefmtmod/noticeimpl.go | 169 + models/v/noticefmtmod/type.go | 149 + models/v/noticerecdmod/list.go | 46 + models/v/noticerecdmod/noticeRecord.go | 144 + models/v/noticerecdmod/trend.go | 42 + models/v/noticerecdmod/type.go | 31 + models/v/officialWebsitemod/album.go | 58 + models/v/officialWebsitemod/basic_data.go | 70 + models/v/officialWebsitemod/business.go | 54 + models/v/officialWebsitemod/common_methods.go | 125 + models/v/officialWebsitemod/common_types.go | 101 + models/v/officialWebsitemod/hero.go | 62 + models/v/officialWebsitemod/init.go | 57 + models/v/officialWebsitemod/job.go | 56 + models/v/officialWebsitemod/news.go | 58 + models/v/officialWebsitemod/partner.go | 53 + models/v/officialWebsitemod/photograph.go | 58 + models/v/officialWebsitemod/recruit_form.go | 57 + models/v/officialWebsitemod/tag.go | 60 + models/v/officialWebsitemod/video.go | 76 + models/v/officialmod/app.go | 6 + models/v/officialmod/official.go | 186 + models/v/officialmod/type.go | 64 + models/v/officialmod/web.go | 50 + models/v/oncetaskmod/oncetask.go | 104 + models/v/oncetaskmod/web.go | 43 + models/v/operationlogmod/operationlog.go | 70 + models/v/operationlogmod/type.go | 28 + models/v/operationlogmod/web.go | 17 + models/v/payaudiobookmod/pay.go | 118 + models/v/payaudiobookmod/type.go | 24 + models/v/paymentguidemod/model.go | 381 ++ models/v/paymentguidemod/type.go | 242 ++ models/v/paymentguidemod/type_test.go | 184 + models/v/prdcthsomod/app.go | 55 + models/v/prdcthsomod/paymentLog.go | 53 + models/v/prdcthsomod/producthistory.go | 213 + models/v/prdcthsomod/type.go | 93 + models/v/prdcthsomod/vip_experiment.go | 53 + models/v/prize_record_mod/app.go | 67 + models/v/prize_record_mod/hanlder.go | 404 ++ models/v/prize_record_mod/type.go | 25 + models/v/prizemod/hanlder.go | 443 ++ models/v/prizemod/prize.go | 205 + models/v/prizemod/type.go | 64 + models/v/prizemod/web.go | 127 + models/v/productbenefitmod/productBenefit.go | 136 + models/v/productbenefitmod/type.go | 29 + models/v/productbenefitmod/web.go | 46 + models/v/productmod/app.go | 16 + models/v/productmod/product.go | 322 ++ models/v/productmod/type.go | 196 + models/v/productmod/web.go | 162 + models/v/productposimod/productposi.go | 161 + models/v/productposimod/type.go | 32 + models/v/productposimod/web.go | 17 + models/v/proxyincomemod/proxyincome.go | 184 + models/v/proxyincomemod/type.go | 29 + models/v/proxymod/app.go | 19 + models/v/proxymod/h5.go | 24 + models/v/proxymod/proxy.go | 536 +++ models/v/proxymod/type.go | 43 + models/v/proxymod/web.go | 16 + models/v/proxyrecordmod/record.go | 51 + models/v/proxyrecordmod/type.go | 25 + models/v/proxyrecordmod/web.go | 19 + models/v/pushmod/type.go | 35 + models/v/pushmod/vid.go | 145 + models/v/pushmod/web.go | 31 + models/v/questionnremod/questionnre.go | 56 + models/v/questionnremod/type.go | 36 + models/v/quicksearchmod/app.go | 36 + models/v/quicksearchmod/quicksearch.go | 139 + models/v/quicksearchmod/type.go | 56 + models/v/rchgamegoldmod/gold.go | 156 + models/v/rchgamegoldmod/type.go | 51 + models/v/rchgamegoldmod/web.go | 33 + models/v/rchgamtmod/gold.go | 151 + models/v/rchgamtmod/type.go | 64 + models/v/rchgamtmod/web.go | 28 + models/v/rchgchanmod/paychannel.go | 190 + models/v/rchgchanmod/type.go | 77 + models/v/rchgchanmod/web.go | 51 + models/v/rchgordmod/app.go | 103 + models/v/rchgordmod/discStat.go | 162 + models/v/rchgordmod/list.go | 204 + models/v/rchgordmod/rchgorder.go | 775 ++++ models/v/rchgordmod/type.go | 193 + models/v/rchgordmod/vip_experiment.go | 69 + models/v/rchgordmod/web.go | 78 + models/v/recmdtag/app.go | 26 + models/v/recmdtag/error.go | 17 + models/v/recmdtag/list.go | 84 + models/v/recmdtag/m3u8sign.go | 10 + models/v/recmdtag/recmdtag.go | 128 + models/v/recmdtag/type.go | 50 + models/v/recommod/app.go | 32 + models/v/recommod/m3u8sign.go | 10 + models/v/recommod/reco.go | 201 + models/v/recommod/type.go | 24 + models/v/reptmod/list.go | 53 + models/v/reptmod/report.go | 123 + models/v/reptmod/type.go | 45 + models/v/repttypemod/repttype.go | 62 + models/v/repttypemod/type.go | 20 + models/v/rjctmpltmod/rjctmplt.go | 81 + models/v/rjctmpltmod/type.go | 30 + models/v/rjctmpltmod/web.go | 17 + models/v/rolemod/role.go | 132 + models/v/rolemod/type.go | 51 + models/v/rolemod/web.go | 33 + models/v/scenebannermod/model.go | 150 + models/v/scenebannermod/type.go | 85 + models/v/scenebannermod/type_test.go | 47 + models/v/sensitivewordmod/crud.go | 165 + models/v/sensitivewordmod/match.go | 64 + models/v/sensitivewordmod/type.go | 51 + models/v/sessionmod/app.go | 33 + models/v/sessionmod/session.go | 134 + models/v/sessionmod/type.go | 25 + models/v/settingmod/setting.go | 85 + models/v/settingmod/type.go | 32 + models/v/signrecordmod/app.go | 21 + models/v/signrecordmod/signrecord.go | 227 ++ models/v/signrecordmod/type.go | 64 + models/v/signrecordmod/web.go | 1 + models/v/signtaskmod/app.go | 11 + models/v/signtaskmod/config.go | 151 + models/v/signtaskmod/type.go | 26 + models/v/signtaskmod/web.go | 87 + models/v/sourcemod/source.go | 312 ++ models/v/sourcemod/source_cache_test.go | 33 + models/v/sourcemod/type.go | 101 + models/v/sysconfmod/free_trial_config_test.go | 16 + .../v/sysconfmod/payment_guide_config_test.go | 22 + models/v/sysconfmod/short_drama_card_test.go | 16 + models/v/sysconfmod/sysconf.go | 178 + models/v/sysconfmod/trans.go | 82 + models/v/sysconfmod/type.go | 121 + models/v/sysconfmod/var.go | 1395 +++++++ models/v/systemmod/base.go | 14 + models/v/systemmod/config.go | 79 + models/v/systemmod/type.go | 20 + models/v/tagconfmod/tagconf.go | 85 + models/v/tagconfmod/type.go | 34 + models/v/tagmod/app.go | 40 + models/v/tagmod/es.go | 63 + models/v/tagmod/tag.go | 583 +++ models/v/tagmod/type.go | 92 + models/v/tagmod/web.go | 80 + models/v/taskmod/app.go | 11 + models/v/taskmod/config.go | 151 + models/v/taskmod/type.go | 45 + models/v/taskmod/web.go | 92 + models/v/taskrecordmod/app.go | 21 + models/v/taskrecordmod/taskRecord.go | 452 +++ models/v/taskrecordmod/type.go | 74 + models/v/taskrecordmod/web.go | 1 + models/v/tonemod/list.go | 56 + models/v/tonemod/tone.go | 77 + models/v/tonemod/toneValue.go | 22 + models/v/tonemod/type.go | 54 + models/v/tonerecomod/toneRecom.go | 108 + models/v/tonerecomod/type.go | 50 + models/v/tonerecomod/web.go | 63 + models/v/txnactmod/app.go | 25 + models/v/txnactmod/txnact.go | 144 + models/v/txnactmod/type.go | 32 + models/v/txnactmod/web.go | 23 + models/v/txnmod/discStat.go | 190 + models/v/txnmod/fund_ledger.go | 214 + models/v/txnmod/list.go | 128 + models/v/txnmod/transaction.go | 654 +++ models/v/txnmod/type.go | 462 +++ models/v/txnmod/web.go | 59 + models/v/userResourcemod/type.go | 21 + models/v/userResourcemod/userResource.go | 175 + models/v/userResourcemod/web.go | 25 + models/v/useractmod/type.go | 31 + models/v/useractmod/useract.go | 135 + models/v/usercheckinmod/type.go | 26 + models/v/usercheckinmod/usercheckin.go | 115 + models/v/usermod/app.go | 319 ++ models/v/usermod/discStat.go | 163 + models/v/usermod/drama_expire_test.go | 27 + models/v/usermod/error.go | 13 + models/v/usermod/es.go | 63 + models/v/usermod/list.go | 218 + models/v/usermod/payment_status_popup_test.go | 39 + models/v/usermod/type.go | 396 ++ models/v/usermod/user.go | 1512 +++++++ models/v/usermod/vip_renew.go | 12 + models/v/usermod/vip_renew_test.go | 47 + models/v/usermod/web.go | 278 ++ models/v/userrecomod/reco.go | 154 + models/v/userrecomod/type.go | 24 + models/v/userrecomod/web.go | 34 + models/v/usertagmod/app.go | 12 + models/v/usertagmod/type.go | 22 + models/v/usertagmod/userTag.go | 127 + models/v/userwatchrecordmod/app.go | 27 + models/v/userwatchrecordmod/app_test.go | 18 + models/v/userwatchrecordmod/type.go | 15 + .../v/userwatchrecordmod/user_watch_record.go | 101 + models/v/verifyreportmod/type.go | 96 + models/v/verifyreportmod/verifyreport.go | 265 ++ models/v/versionmod/version.go | 363 ++ models/v/videoactivitymod/app.go | 71 + models/v/videoactivitymod/type.go | 24 + models/v/videoactivitymod/video_activity.go | 133 + models/v/videocoupon/init.go | 39 + models/v/videocoupon/type.go | 31 + models/v/videocoupon/videocoupon.go | 80 + models/v/videodiscountmod/type.go | 32 + models/v/videodiscountmod/videodiscount.go | 142 + models/v/videogoldcoinmod/app.go | 53 + models/v/videogoldcoinmod/gold_coin_video.go | 153 + models/v/videogoldcoinmod/type.go | 31 + models/v/videogoldcoinmod/web.go | 92 + models/v/vidmod/app.go | 524 +++ models/v/vidmod/app_video.go | 262 ++ models/v/vidmod/cache.go | 85 + models/v/vidmod/es.go | 161 + models/v/vidmod/free_trial_response_test.go | 54 + models/v/vidmod/hevc.go | 276 ++ models/v/vidmod/hevc_test.go | 33 + models/v/vidmod/liaoba_top_sort_test.go | 35 + models/v/vidmod/list.go | 182 + models/v/vidmod/m3u8sign.go | 52 + models/v/vidmod/m3u8sign_test.go | 35 + models/v/vidmod/payment_guide.go | 72 + models/v/vidmod/payment_guide_test.go | 31 + models/v/vidmod/ranking.go | 36 + models/v/vidmod/recommend_batch_test.go | 1491 +++++++ models/v/vidmod/recommend_test.go | 24 + models/v/vidmod/setDoc.go | 24 + models/v/vidmod/type.go | 307 ++ models/v/vidmod/update_marker.go | 59 + models/v/vidmod/vid.go | 3520 ++++++++++++++++ models/v/vidmod/watch_consume_test.go | 49 + models/v/vidmod/web.go | 555 +++ models/v/vidpopmod/type.go | 42 + models/v/vidpopmod/vidpop.go | 65 + models/v/vidtimeonlinemod/type.go | 32 + models/v/vidtimeonlinemod/vidtimeonline.go | 147 + models/v/vidtimeonlinemod/web.go | 55 + models/v/vipcardexperimentmod/model.go | 283 ++ models/v/vipcardexperimentmod/product_ids.go | 163 + models/v/vipcardexperimentmod/type.go | 294 ++ models/v/vipcardexperimentmod/type_test.go | 429 ++ models/v/vipconfigmod/type.go | 33 + models/v/vipconfigmod/vipconfig.go | 43 + models/v/walletmod/app.go | 7 + models/v/walletmod/type.go | 113 + models/v/walletmod/wallet.go | 943 +++++ models/v/wdchannmod/app.go | 17 + models/v/wdchannmod/channel.go | 134 + models/v/wdchannmod/list.go | 93 + models/v/wdchannmod/type.go | 36 + models/v/wdchannmod/web.go | 42 + models/v/wdordmod/app.go | 46 + models/v/wdordmod/discStat.go | 170 + models/v/wdordmod/error.go | 9 + models/v/wdordmod/list.go | 200 + models/v/wdordmod/type.go | 84 + models/v/wdordmod/web.go | 25 + models/v/wdordmod/withdraw.go | 470 +++ models/v/wdtaxmod/type.go | 22 + models/v/wdtaxmod/web.go | 10 + models/v/wdtaxmod/withdrawtariff.go | 36 + project.md | 124 + script/createAuthEnum.js | 123 + script/createAuthority.js | 82 + script/golang/gen_qr_code/main.go | 31 + script/golang/seed_adver_abtest/main.go | 410 ++ scripts/repair_test_drama_tags.py | 160 + scripts/seed_checkin/main.go | 161 + scripts/seed_checkin_setup/main.go | 404 ++ services/srv_im/ad_notify.go | 710 ++++ services/srv_im/im.go | 785 ++++ skd/job/cache/rankcache/run.go | 51 + skd/job/cache/tonecache/run.go | 51 + skd/job/check/daichongcallback.go | 149 + skd/job/checkin/checkin.go | 70 + skd/job/dailyAdverCalc/job.go | 205 + skd/job/dailyAdverCalc/job_test.go | 105 + skd/job/dataclean/dataclean.go | 29 + skd/job/dataclean/method.go | 94 + skd/job/elastic/esaudiobook/method.go | 27 + skd/job/elastic/esaudiobook/sync.go | 58 + skd/job/elastic/eskeyword/method.go | 26 + skd/job/elastic/eskeyword/sync.go | 58 + skd/job/elastic/esmedia/method.go | 65 + skd/job/elastic/esmedia/method_test.go | 25 + skd/job/elastic/esmedia/sync.go | 80 + skd/job/elastic/essearchlog/method.go | 26 + skd/job/elastic/essearchlog/sync.go | 58 + skd/job/elastic/estag/method.go | 44 + skd/job/elastic/estag/sync.go | 61 + skd/job/elastic/esuser/method.go | 38 + skd/job/elastic/esuser/sync.go | 72 + skd/job/elastic/esvideo/method.go | 108 + skd/job/elastic/esvideo/sync.go | 74 + skd/job/elastic/rdvideo/method.go | 27 + skd/job/elastic/rdvideo/sync.go | 77 + skd/job/hevc_transcode.go | 555 +++ skd/job/hevc_transcode_test.go | 330 ++ skd/job/hotvalue/video.go | 125 + skd/job/im_ad_notify.go | 150 + skd/job/job.go | 162 + skd/job/schedule.go | 34 + skd/job/shortrecommend/job.go | 231 ++ skd/job/shortrecommend/job_test.go | 18 + skd/job/sqlcollapser/shordvideo/method.go | 27 + skd/job/sqlcollapser/shordvideo/sync.go | 89 + skd/job/sqlcollapser/user/method.go | 44 + skd/job/sqlcollapser/user/runSync.go | 54 + skd/job/sqlcollapser/vid/method.go | 51 + skd/job/sqlcollapser/vid/runSync.go | 48 + skd/job/sqlcollasper.go | 50 + skd/job/stat.go | 104 + skd/job/stat/keyword/method.go | 59 + skd/job/stat/keyword/stat.go | 72 + skd/job/stat/kwrank/method.go | 29 + skd/job/stat/kwrank/stat.go | 52 + skd/job/stat/rank/stat.go | 53 + skd/job/stat/rechorder/method.go | 138 + skd/job/stat/rechorder/stat.go | 97 + skd/job/stat/statuser/method.go | 91 + skd/job/stat/statuser/stat.go | 143 + skd/job/stat/tagplay/method.go | 25 + skd/job/stat/tagplay/stat.go | 71 + skd/job/stat/video/method.go | 130 + skd/job/stat/video/stat.go | 97 + skd/job/stat/vidtotal/method.go | 46 + skd/job/stat/vidtotal/stat.go | 96 + skd/job/stat/withorder/method.go | 139 + skd/job/stat/withorder/stat.go | 97 + skd/job/sync.go | 74 + skd/job/sync_im_users.go | 176 + .../syncspiderdata/audiobookspider/method.go | 147 + .../syncspiderdata/audiobookspider/sync.go | 56 + .../syncspiderdata/fictionspider/method.go | 99 + skd/job/syncspiderdata/fictionspider/sync.go | 56 + skd/job/user/privilege.go | 16 + skd/main.go | 161 + skd/once/main.go | 77 + skd/once/syncAudioBook.go | 40 + skd/once/syncFiction.go | 40 + skd/once/syncVideoInfo.go | 40 + skd/service/chatrobot.go | 218 + skd/service/contentreviewser/acg.go | 68 + skd/service/contentreviewser/issue_builder.go | 92 + skd/service/contentreviewser/matcher.go | 114 + skd/service/contentreviewser/runner.go | 129 + skd/service/contentreviewser/scanner.go | 95 + skd/service/contentreviewser/video.go | 42 + skd/service/doregister.go | 119 + .../export_task/advance_order_export.go | 102 + skd/service/export_task/base.go | 282 ++ .../export_task/product_history_export.go | 115 + skd/service/export_task/user_export.go | 192 + skd/service/export_task/video_export.go | 219 + skd/service/fakeVideoInfo.go | 122 + skd/service/init.go | 5 + skd/service/promotionCode.go | 58 + skd/service/updownload.go | 102 + skd/service/versionser.go | 33 + skd/skdg/elastic.go | 24 + skd/skdg/redisg.go | 28 + skd/skdg/shortrecommend.go | 49 + skd/skdg/shortrecommend_test.go | 51 + skd/skdg/skdg.go | 105 + skd/skdg/static.go | 20 + sshpass.rb | 17 + swagger/main.go | 52 + tests/checkin/go.mod | 118 + tests/checkin/go.sum | 446 ++ tests/checkin/main.go | 35 + tests/checkin/reports/last_run.json | 5 + tests/checkin/reports/latest.html | 1137 ++++++ .../reports/report_20260507_212008.html | 962 +++++ .../reports/report_20260508_162923.html | 962 +++++ .../reports/report_20260508_163219.html | 1137 ++++++ .../reports/result_20260507_212008.json | 363 ++ .../reports/result_20260508_162923.json | 363 ++ .../reports/result_20260508_163219.json | 441 ++ tests/checkin/sandbox.go | 98 + tests/checkin/suite_checkin.go | 302 ++ web/api/acth5ctrl/lotnum.go | 52 + web/api/activityctrl/activity.go | 60 + web/api/activityctrl/prize.go | 65 + web/api/activityctrl/prize_pool.go | 55 + web/api/activityctrl/user_record.go | 24 + web/api/actvctrl/actv.go | 57 + web/api/addwlefarectrl/addwelfare.go | 40 + web/api/adminctrl/admin.go | 524 +++ web/api/adsctrl/ads.go | 263 ++ web/api/adv_group_ctrl/adv_group.go | 33 + web/api/advance_config_ctrl/advance_config.go | 158 + web/api/advance_order_ctrl/advance_order.go | 83 + web/api/ai_changeface_ctrl/ai_changeface.go | 417 ++ .../ai_image_to_video.go | 136 + web/api/ai_mate_ctr/ai_mate.go | 42 + web/api/ai_mate_v2_ctrl/ai_mate.go | 66 + .../aitemplatemodule.go | 167 + .../ai_text_to_image_ctrl/aitexttoimage.go | 139 + .../ai_text_to_novel_ctrl/aitexttonovel.go | 140 + web/api/ai_undress_ctrl/ai_undress.go | 213 + web/api/aiplazactrl/aiplaza.go | 178 + web/api/annouctrl/annou.go | 118 + web/api/annouctrl/announce.go | 115 + web/api/audiobookctrl/audiobook.go | 196 + web/api/authorctrl/author.go | 186 + web/api/backpack_ctrl/backpack.go | 40 + web/api/bannerjumpctrl/bannerjump.go | 142 + web/api/chat_message_ctrl/chat_message.go | 116 + web/api/chatrobotctrl/chatrobotctrl.go | 96 + web/api/checkinctrl/checkin.go | 281 ++ web/api/commentAgentctrl/commentAgent.go | 144 + web/api/commentctrl/commentCtrl.go | 503 +++ web/api/contentlibctrl/contentlibctrl.go | 85 + .../contentreviewctrl/contentreviewctrl.go | 450 ++ web/api/currencyctrl/currency.go | 55 + web/api/daichongctrl/daichong.go | 71 + web/api/daichongctrl/order.go | 84 + web/api/dramatopicctrl/topic.go | 139 + web/api/errfeedbackctrl/errfeedback.go | 105 + web/api/exchcodectrl/exchcodectrl.go | 129 + web/api/exchcodectrl/exchlogctrl.go | 39 + web/api/exportctrl/export.go | 558 +++ web/api/extctrl/extctrl.go | 91 + web/api/fictionctrl/fiction.go | 165 + web/api/filterctrl/filter.go | 88 + web/api/freevidcfgctrl/freevidcfg.go | 138 + web/api/goldcfgctrl/goldcfg.go | 163 + web/api/imagetopctrl/imagetop.go | 46 + web/api/imgroupctrl/imgroup.go | 106 + web/api/imgroupmemberctrl/imgroupmember.go | 106 + web/api/immessagectrl/immessage.go | 142 + web/api/infmtctrl/infmt.go | 459 +++ .../integral_config_ctrl/integral_config.go | 120 + .../integarl_exchange.go | 81 + web/api/ipblockctrl/ipblock.go | 169 + web/api/ipwhitectrl/ipwhite.go | 169 + web/api/jingangctrl/jingangcttrl.go | 158 + web/api/laosijictrl/comics.go | 83 + web/api/laosijictrl/movie.go | 97 + web/api/laosijictrl/novel.go | 83 + web/api/laosijictrl/post.go | 83 + web/api/ldyctrl/ldyctrl.go | 54 + web/api/locationctrl/city.go | 92 + web/api/locationctrl/hotCity.go | 186 + web/api/logctrl/log.go | 154 + web/api/media_resource/media_resource.go | 131 + web/api/mediacontentctrl/mediacontent.go | 236 ++ web/api/mediactrl/media.go | 313 ++ web/api/mediatagctrl/mediatag.go | 167 + .../mediatagdimension.go | 167 + web/api/modulectrl/moduleconf.go | 437 ++ web/api/modulevideoctrl/modulevideo.go | 219 + web/api/nakedchatctrl/nakedchat.go | 141 + web/api/nakedchatorderctrl/nakedchatorder.go | 71 + web/api/newactivityctrl/activity.go | 288 ++ web/api/officialWebsitectrl/album.go | 89 + web/api/officialWebsitectrl/basic.go | 84 + web/api/officialWebsitectrl/business.go | 89 + web/api/officialWebsitectrl/hero.go | 89 + web/api/officialWebsitectrl/job.go | 89 + web/api/officialWebsitectrl/news.go | 89 + web/api/officialWebsitectrl/partner.go | 89 + web/api/officialWebsitectrl/photograph.go | 103 + web/api/officialWebsitectrl/recruit_form.go | 89 + web/api/officialWebsitectrl/tag.go | 89 + web/api/officialWebsitectrl/video.go | 131 + web/api/officialWebsitectrl/video_ext.go | 184 + web/api/officialctrl/officialCtrl.go | 162 + web/api/operationlogctrl/operationlog.go | 31 + web/api/paymentguidectrl/payment_guide.go | 108 + web/api/productBenefitctrl/productbenefit.go | 107 + web/api/productctrl/productLog.go | 64 + web/api/productctrl/productposition.go | 118 + web/api/productctrl/vip.go | 248 ++ web/api/productctrl/vipConfig.go | 60 + web/api/promoteurlctrl/promoteurlctrl.go | 140 + web/api/proxyctrl/proxy.go | 133 + web/api/quicksearchctrl/quicksearch.go | 147 + web/api/rechargectrl/DaBaiSha.go | 70 + web/api/rechargectrl/dxzhifu.go | 53 + web/api/rechargectrl/gameGold.go | 176 + web/api/rechargectrl/gold.go | 167 + web/api/rechargectrl/goldfishplus.go | 68 + web/api/rechargectrl/izhifu.go | 46 + web/api/rechargectrl/leihuo.go | 70 + web/api/rechargectrl/lucky.go | 52 + web/api/rechargectrl/payChannel.go | 139 + web/api/rechargectrl/rchgorder.go | 84 + web/api/rechargectrl/shark.go | 69 + web/api/rechargectrl/yilianfu.go | 52 + web/api/rechargectrl/zhongfu.go | 52 + web/api/rolectrl/freevidcfg.go | 50 + web/api/scenebannerctrl/banner.go | 95 + .../sensitivewordctrl/sensitivewordctrl.go | 373 ++ web/api/signctrl/config.go | 38 + web/api/signctrl/daily.go | 61 + web/api/signctrl/once.go | 62 + web/api/signctrl/sign.go | 67 + web/api/sourcectrl/source.go | 138 + web/api/statcenterctl/statsync.go | 373 ++ web/api/statctrl/orderCtrl.go | 91 + web/api/statctrl/tarnsCtrl.go | 124 + web/api/statctrl/userCtrl.go | 62 + web/api/statctrl/videoCtrl.go | 159 + web/api/synccdnctrl/syncCtrl.go | 59 + web/api/sysconfctrl/sysconf.go | 178 + web/api/tagctrl/recmdCtrl.go | 222 + web/api/tagctrl/tagCtrl.go | 382 ++ web/api/tianyuctrl/http/http.go | 195 + web/api/tianyuctrl/http/http_test.go | 16 + web/api/tianyuctrl/media.go | 348 ++ web/api/tianyuctrl/proto.go | 14 + web/api/tianyuctrl/proxy/object.go | 382 ++ web/api/tianyuctrl/proxy/object_test.go | 1 + web/api/tianyuctrl/proxy/types.go | 351 ++ web/api/tianyuctrl/proxy/utils.go | 130 + web/api/tonectrl/tonectrl.go | 195 + web/api/updownloadctrl/updownloadCtrl.go | 697 ++++ web/api/userctrl/faq.go | 146 + web/api/userctrl/feedback.go | 234 ++ web/api/userctrl/login.go | 60 + web/api/userctrl/play.go | 74 + web/api/userctrl/reco.go | 130 + web/api/userctrl/report.go | 131 + web/api/userctrl/user.go | 1116 +++++ web/api/versionctrl/version.go | 177 + web/api/vidctrl/discount_area.go | 236 ++ web/api/vidctrl/mediaresource.go | 118 + web/api/vidctrl/rejecttemplate.go | 126 + web/api/vidctrl/vid.go | 840 ++++ web/api/video_activity_ctrl/video_activity.go | 67 + .../video_gold_coin_ctrl/video_gold_coin.go | 146 + web/api/vipcardexperimentctrl/experiment.go | 77 + web/api/web_im/im_sync.go | 136 + web/api/withdrawctrl/goldfish.go | 57 + web/api/withdrawctrl/txnact.go | 58 + web/api/withdrawctrl/withdraw.go | 244 ++ web/api/withdrawctrl/withdrawCfg.go | 198 + web/api/withdrawctrl/yinse.go | 34 + web/main.go | 173 + web/middleware/authweb/authweb.go | 350 ++ .../checkPermission/checkPermission.go | 51 + web/middleware/limitHandler/limit.go | 29 + web/middleware/limitHandler/login.go | 105 + web/proto/ai.go | 25 + web/router/activity.go | 35 + web/router/actv.go | 16 + web/router/addWare.go | 14 + web/router/adgroup.go | 14 + web/router/admin.go | 24 + web/router/ads.go | 17 + web/router/advance_config.go | 16 + web/router/advance_order.go | 16 + web/router/ai_changeface.go | 29 + web/router/ai_mate.go | 14 + web/router/ai_mate_v2.go | 12 + web/router/ai_mod.go | 18 + web/router/ai_text_to_novel.go | 17 + web/router/ai_undress.go | 18 + web/router/aiimagetovideo.go | 17 + web/router/aiplaza.go | 19 + web/router/aitemplatemodule.go | 18 + web/router/aitexttoimage.go | 18 + web/router/annou.go | 24 + web/router/author.go | 22 + web/router/backpack.go | 13 + web/router/bannerjump.go | 18 + web/router/base.go | 192 + web/router/chatrobot.go | 23 + web/router/checkin.go | 27 + web/router/comment.go | 23 + web/router/commentAgent.go | 22 + web/router/content_review.go | 21 + web/router/currency.go | 17 + web/router/daichong.go | 21 + web/router/defray.go | 37 + web/router/exchcode.go | 21 + web/router/export.go | 22 + web/router/extend.go | 15 + web/router/fiction.go | 34 + web/router/filter.go | 16 + web/router/freevidcfg.go | 17 + web/router/goldcfg.go | 17 + web/router/im.go | 24 + web/router/imagetop.go | 16 + web/router/imgroup.go | 16 + web/router/imgroupmember.go | 16 + web/router/immessage.go | 17 + web/router/infmt.go | 26 + web/router/integralConfig.go | 15 + web/router/integralExchange.go | 15 + web/router/ipblock.go | 17 + web/router/ipwhite.go | 17 + web/router/jingang.go | 15 + web/router/laosiji.go | 38 + web/router/ldycfg.go | 15 + web/router/location.go | 22 + web/router/log.go | 17 + web/router/media.go | 43 + web/router/mediacontent.go | 19 + web/router/mediatag.go | 18 + web/router/mediatagdimension.go | 18 + web/router/module.go | 41 + web/router/nakedchat.go | 17 + web/router/nakedchatorder.go | 15 + web/router/newActivity.go | 19 + web/router/official.go | 16 + web/router/officialWebsite.go | 107 + web/router/operationlog.go | 15 + web/router/payment_guide.go | 17 + web/router/product.go | 33 + web/router/productBenefit.go | 19 + web/router/promoteurl.go | 18 + web/router/proxy.go | 17 + web/router/quicksearch.go | 18 + web/router/recharge.go | 31 + web/router/rejectTemp.go | 17 + web/router/scene_banner.go | 17 + web/router/sensitive_word.go | 20 + web/router/sign.go | 37 + web/router/source.go | 17 + web/router/stat.go | 27 + web/router/sync.go | 32 + web/router/synccdn.go | 15 + web/router/sysconf.go | 19 + web/router/tag.go | 32 + web/router/tone.go | 21 + web/router/updownLoad.go | 31 + web/router/user.go | 72 + web/router/version.go | 16 + web/router/vid.go | 58 + web/router/video_activity.go | 17 + web/router/video_gold_coin.go | 16 + web/router/vip_card_experiment.go | 17 + web/router/withdraw.go | 35 + web/service/activityser/activity.go | 109 + web/service/activityser/cron.go | 131 + web/service/activityser/prize.go | 81 + web/service/activityser/prize_pool.go | 49 + web/service/activityser/user_record.go | 55 + web/service/actvser/actv.go | 99 + web/service/actvser/type.go | 35 + web/service/addwelfareser/addwelfare.go | 42 + web/service/adminser/web.go | 97 + web/service/adser/ads.go | 111 + web/service/adser/error.go | 29 + web/service/adser/review.go | 166 + .../advance_config_ser/advance_config.go | 50 + .../advance_order_ser/advance_order.go | 24 + web/service/advgroupser/advgroupser.go | 270 ++ web/service/advgroupser/advgroupser_test.go | 21 + .../ai_changeface_img_ser/ai_changface_img.go | 468 +++ .../ai_changeface_ser/ai_changeface.go | 540 +++ web/service/ai_changeface_ser/scene.go | 41 + .../ai_image_to_video.go | 311 ++ web/service/ai_mate_ser/ai_mate.go | 45 + web/service/ai_mate_v2_ser/ai_mate.go | 39 + .../aitemplatemodule.go | 155 + .../ai_text_to_image_ser/aitexttoimage.go | 325 ++ .../ai_text_to_novel_ser/aitexttonovel.go | 342 ++ web/service/ai_undress_service/ai_undress.go | 362 ++ web/service/aiplazaser/aiplaza.go | 223 + web/service/annouser/annou.go | 80 + web/service/annouser/announce.go | 88 + web/service/authorser/author.go | 57 + web/service/backpack_service/backpack.go | 22 + web/service/bannerjumpser/bannerjump.go | 129 + web/service/certificateser/certificate.go | 59 + web/service/chat_message_ser/chat_message.go | 219 + web/service/chatrobotser/chatrobot.go | 119 + web/service/commentser/comment.go | 424 ++ web/service/commentser/commentAgent.go | 417 ++ web/service/contentlibser/contentlib.go | 104 + web/service/contentreviewser/applier.go | 80 + web/service/currencyser/currency.go | 49 + web/service/currencyser/type.go | 135 + web/service/daichongser/daichong.go | 72 + web/service/deduction/deduction.go | 44 + web/service/deduction/type.go | 27 + web/service/discount_area_ser/discountArea.go | 236 ++ web/service/dramatopicser/topic.go | 492 +++ web/service/exchcodeser/exchcode.go | 258 ++ web/service/exchcodeser/exchlog.go | 49 + web/service/exportser/export.go | 431 ++ web/service/ffser/ff.go | 55 + web/service/filterser/filter.go | 67 + web/service/imagetopser/imagetop.go | 129 + web/service/imgroupmemberser/imgroupmember.go | 73 + web/service/imgroupser/imgroup.go | 106 + web/service/immessageser/immessage.go | 106 + web/service/infmtser/notice.go | 197 + .../integral_exchange.go | 52 + .../integralconfigser/integralconfig.go | 54 + web/service/jingangser/jingang.go | 76 + web/service/jingangser/type.go | 35 + web/service/laosijiser/comics.go | 266 ++ web/service/laosijiser/media.go | 207 + web/service/laosijiser/novel.go | 314 ++ web/service/laosijiser/novel_test.go | 34 + web/service/laosijiser/post.go | 229 ++ web/service/laosijiser/video.go | 338 ++ web/service/laosijiser/video_test.go | 53 + web/service/locationser/city.go | 43 + web/service/logser/log.go | 22 + web/service/mediacontentser/drama_web_test.go | 121 + web/service/mediacontentser/mediacontent.go | 723 ++++ web/service/mediaresourceser/media.go | 215 + web/service/mediaresourceser/media_test.go | 32 + web/service/mediaser/drama_web_test.go | 106 + web/service/mediaser/media.go | 1095 +++++ web/service/mediaser/sensitive_check.go | 100 + .../mediatagdimensionser/mediatagdimension.go | 201 + web/service/mediatagser/mediatag.go | 221 + web/service/moduleser/drama_module_test.go | 21 + web/service/moduleser/module.go | 355 ++ web/service/moduleser/type.go | 36 + .../nakedchatorderser/nakedchatorder.go | 68 + web/service/nakedchatser/nakedchat.go | 201 + web/service/officialWebsiteser/album.go | 284 ++ web/service/officialWebsiteser/backfill.go | 141 + web/service/officialWebsiteser/basic.go | 779 ++++ web/service/officialWebsiteser/business.go | 209 + web/service/officialWebsiteser/helper.go | 286 ++ web/service/officialWebsiteser/hero.go | 300 ++ web/service/officialWebsiteser/job.go | 220 + web/service/officialWebsiteser/news.go | 261 ++ web/service/officialWebsiteser/partner.go | 211 + web/service/officialWebsiteser/photograph.go | 331 ++ .../officialWebsiteser/recruit_form.go | 221 + web/service/officialWebsiteser/slug.go | 113 + web/service/officialWebsiteser/tag.go | 202 + web/service/officialWebsiteser/video.go | 652 +++ web/service/officialser/official.go | 45 + web/service/operationlogser/operationlog.go | 39 + web/service/operationlogser/type.go | 25 + web/service/paymentguideser/payment_guide.go | 168 + .../paymentguideser/payment_guide_test.go | 258 ++ web/service/payvidlgser/payvidlg.go | 63 + web/service/payvidlgser/type.go | 23 + web/service/prefetch/prefetch.go | 151 + .../productBenefitser/productBenefit.go | 71 + web/service/productser/productHistory.go | 19 + web/service/proxyser/proxy.go | 131 + web/service/quicksearchser/quicksearch.go | 139 + web/service/rechargeser/page.go | 125 + .../rechargeser/page_attribution_test.go | 33 + web/service/rechargeser/recharge.go | 217 + web/service/smsser/smsser.go | 57 + web/service/statser/daily.go | 111 + web/service/statser/order.go | 301 ++ .../sysconfser/short_drama_card_test.go | 26 + web/service/sysconfser/sysconf.go | 180 + web/service/tagser/recmd.go | 52 + web/service/tagser/tag.go | 147 + web/service/tagser/type.go | 32 + web/service/taskser/config.go | 52 + web/service/taskser/daily.go | 114 + web/service/taskser/once.go | 116 + web/service/taskser/sign.go | 55 + web/service/updownloadser/updownload.go | 714 ++++ web/service/userser/drama_expire_test.go | 31 + web/service/userser/faq.go | 19 + web/service/userser/login.go | 85 + web/service/userser/report.go | 210 + web/service/userser/user.go | 1201 ++++++ web/service/video_activity_service/type.go | 33 + .../video_activity_service/video_activity.go | 84 + .../video_gold_coin_ser/video_gold_coin.go | 90 + web/service/video_media_service/http.go | 84 + web/service/video_media_service/type.go | 189 + web/service/vidser/liaoba_top_sort_test.go | 55 + web/service/vidser/mediaresouce.go | 260 ++ web/service/vidser/scheduled_module_test.go | 27 + web/service/vidser/sensitive_check.go | 170 + web/service/vidser/tag_search_visibility.go | 27 + .../vidser/tag_search_visibility_test.go | 34 + web/service/vidser/type.go | 19 + web/service/vidser/vid.go | 1555 +++++++ web/service/vipcardexperimentser/service.go | 167 + .../vipcardexperimentser/service_test.go | 108 + web/service/withdrawser/bankcode.go | 37 + web/service/withdrawser/refund.go | 88 + web/service/withdrawser/wtihdraw.go | 471 +++ web/vidhelp/redis.go | 89 + web/vidhelp/vid.go | 417 ++ web/webg/ai_callback.go | 14 + web/webg/elastic.go | 24 + web/webg/redis.go | 28 + web/webg/webg.go | 155 + 亚模块红点AI女友接口.md | 156 + 付费引导Ping下发接口文档.md | 526 +++ 会员内容上新弹窗接口.md | 258 ++ 短视频推荐接口.md | 650 +++ 1897 files changed, 257900 insertions(+) create mode 100644 .gitignore create mode 100644 91迭代后端技术方案.md create mode 100644 91迭代配置改动点.md create mode 100644 AGENTS.md create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 SHORT_VIDEO_RECOMMENDATION_BACKEND_TECH_DESIGN.md create mode 100644 VIP卡片UI配置接口.md create mode 100644 app/api/active2023ctrl/lottery.go create mode 100644 app/api/active2023ctrl/user_info.go create mode 100644 app/api/activityctrl/activityctrl.go create mode 100644 app/api/actvctrl/actv.go create mode 100644 app/api/adsctrl/ads.go create mode 100644 app/api/advance_config_ctrl/advance_config.go create mode 100644 app/api/ai_changeface_ctrl/ai_changeface.go create mode 100755 app/api/ai_image_to_video_ctrl/ai_image_to_video.go create mode 100644 app/api/ai_mate_ctrl/ai_mate.go create mode 100644 app/api/ai_mate_v2_ctrl/ai_mate.go create mode 100755 app/api/ai_text_to_image_ctrl/aitexttoimage.go create mode 100755 app/api/ai_text_to_novel_ctrl/aitexttonovel.go create mode 100644 app/api/ai_undress_ctrl/ai_undress.go create mode 100755 app/api/aiplazactrl/aiplaza.go create mode 100755 app/api/aitemplatemodulectrl/aitemplatemodule.go create mode 100644 app/api/analyticsctrl/analytics.go create mode 100644 app/api/annouctrl/annouctrl.go create mode 100644 app/api/backpackctrl/backpack.go create mode 100644 app/api/checkinctrl/checkin.go create mode 100644 app/api/commentctrl/commentctrl.go create mode 100644 app/api/common.go create mode 100644 app/api/contentmarkerctrl/content_marker.go create mode 100644 app/api/couponctl/couponctl.go create mode 100644 app/api/customerCtrl/user.go create mode 100644 app/api/dramactrl/drama.go create mode 100644 app/api/exchcodectrl/exchcodectrl.go create mode 100644 app/api/followctrl/followctrl.go create mode 100644 app/api/goldextractrl/goldextractrl.go create mode 100644 app/api/health_check_ctrl/health.go create mode 100644 app/api/hotspotctr/hotspot.go create mode 100644 app/api/imctrl/ad.go create mode 100644 app/api/imctrl/imctrl.go create mode 100755 app/api/imgroupctrl/imgroup.go create mode 100755 app/api/imgroupmemberctrl/imgroupmember.go create mode 100755 app/api/immessagectrl/immessage.go create mode 100644 app/api/infmtctrl/infmtctrl.go create mode 100644 app/api/integeralctrl/integral.go create mode 100644 app/api/likectrl/likectrl.go create mode 100755 app/api/mediabookshelfctrl/mediabookshelf.go create mode 100755 app/api/mediacontentctrl/mediacontent.go create mode 100755 app/api/mediactrl/media.go create mode 100755 app/api/mediatagctrl/mediatag.go create mode 100644 app/api/messagectrl/messagectrl.go create mode 100644 app/api/minectrl/feedbackctrl.go create mode 100644 app/api/minectrl/followctrl.go create mode 100644 app/api/minectrl/reportctrl.go create mode 100644 app/api/modulectrl/module.go create mode 100755 app/api/nakedchatctrl/nakedchat.go create mode 100755 app/api/nakedchatorderctrl/nakedchatorder.go create mode 100644 app/api/newactivityctrl/gameactivity.go create mode 100644 app/api/newactivityctrl/newactivity.go create mode 100644 app/api/newactivityctrl/questionnre.go create mode 100644 app/api/notictrl/notificationctrl.go create mode 100644 app/api/officialWebsitectrl/album.go create mode 100644 app/api/officialWebsitectrl/basicData.go create mode 100644 app/api/officialWebsitectrl/business.go create mode 100644 app/api/officialWebsitectrl/hero.go create mode 100644 app/api/officialWebsitectrl/job.go create mode 100644 app/api/officialWebsitectrl/news.go create mode 100644 app/api/officialWebsitectrl/partner.go create mode 100644 app/api/officialWebsitectrl/recruit.go create mode 100644 app/api/officialWebsitectrl/tag.go create mode 100644 app/api/officialWebsitectrl/video.go create mode 100644 app/api/officialctrl/officialCtrl.go create mode 100644 app/api/paymentguidectrl/payment_guide.go create mode 100644 app/api/pingctrl/pingctrl.go create mode 100644 app/api/productctrl/productctrl.go create mode 100644 app/api/publishctrl/publish.go create mode 100644 app/api/rankctrl/rankctrl.go create mode 100644 app/api/rechargectrl/DaBaiSha.go create mode 100644 app/api/rechargectrl/pay_center.go create mode 100644 app/api/rechargectrl/rechargectrl.go create mode 100644 app/api/rechargectrl/yinse.go create mode 100644 app/api/recommctrl/recommctrl.go create mode 100644 app/api/recreationctrl/recreationctrl.go create mode 100644 app/api/scenebannerctrl/banner.go create mode 100644 app/api/searchctrl/searchctrl.go create mode 100644 app/api/sharectrl/sharectrl.go create mode 100755 app/api/signrecordctrl/signrecord.go create mode 100644 app/api/smsctrl/smsctrl.go create mode 100644 app/api/statcenterctl/proto.go create mode 100644 app/api/statcenterctl/statcenterctr.go create mode 100644 app/api/statcenterctl/sync.go create mode 100644 app/api/statcenterctl/utils.go create mode 100644 app/api/staticctrl/staticctrl.go create mode 100644 app/api/tagctrl/tagctrl.go create mode 100644 app/api/taskctrl/sign.go create mode 100644 app/api/taskctrl/task.go create mode 100644 app/api/tonectrl/tonectrl.go create mode 100644 app/api/txnactctr/txnactctr.go create mode 100644 app/api/updownctrl/updownctrl.go create mode 100644 app/api/updownctrl/updownctrl_test.go create mode 100644 app/api/userctrl/jio.go create mode 100644 app/api/userctrl/privilege.go create mode 100644 app/api/userctrl/userctrl.go create mode 100644 app/api/userctrl/userwatchrecord.go create mode 100644 app/api/versionctrl/versionctrl.go create mode 100644 app/api/vidctrl/newsctrl.go create mode 100644 app/api/vidctrl/vidctrl.go create mode 100644 app/api/video_gold_coin_ctrl/video_gold_coin.go create mode 100644 app/api/videoactivityctrl/videoactivity.go create mode 100644 app/api/vipctrl/vipctrl.go create mode 100644 app/api/walletctrl/goldlog.go create mode 100644 app/api/walletctrl/store.go create mode 100644 app/api/walletctrl/wallectctrl.go create mode 100644 app/api/withdrawctrl/goldfish.go create mode 100644 app/api/withdrawctrl/withdrawctrl.go create mode 100644 app/api/withdrawctrl/yinse.go create mode 100644 app/api/wonderctrl/wonderctrl.go create mode 100644 app/appg/ai_callback.go create mode 100644 app/appg/appg.go create mode 100644 app/appg/elasticg.go create mode 100644 app/appg/mong.go create mode 100644 app/appg/redisg.go create mode 100644 app/appg/shortrecommend.go create mode 100644 app/appg/shortrecommend_test.go create mode 100644 app/appg/static.go create mode 100644 app/main.go create mode 100644 app/middleware/activityauth/activityauth.go create mode 100644 app/middleware/authuser/authuser.go create mode 100644 app/middleware/authuser/authuser_test.go create mode 100644 app/middleware/datacenter/datacenter.go create mode 100644 app/middleware/ip/ip.go create mode 100644 app/middleware/ipblock/ipblock.go create mode 100644 app/middleware/limitHandler/limit.go create mode 100644 app/middleware/requestEncrypt/decrypt.go create mode 100644 app/middleware/requestEncrypt/encrypt.go create mode 100644 app/middleware/requestEncrypt/private.pem create mode 100644 app/middleware/requestEncrypt/privatePKCs1.pem create mode 100644 app/middleware/requestEncrypt/public.pem create mode 100644 app/middleware/vercheck/vercheck.go create mode 100644 app/middleware/verfyparam/verfyparam.go create mode 100644 app/middleware/verfyparam/verfyparam_test.go create mode 100644 app/middleware/visitlog/visitlog.go create mode 100644 app/middleware/watcher/watch.go create mode 100644 app/proto/ping.go create mode 100644 app/proto/ping_short_drama_card_test.go create mode 100644 app/proto/product.go create mode 100644 app/proto/product_test.go create mode 100644 app/proto/pub.go create mode 100644 app/proto/recharge.go create mode 100644 app/proto/recomm.go create mode 100644 app/proto/search.go create mode 100644 app/proto/statsync.go create mode 100644 app/router/active_2023.go create mode 100644 app/router/activity.go create mode 100644 app/router/actv.go create mode 100644 app/router/ads.go create mode 100644 app/router/advance_config.go create mode 100644 app/router/ai_change_face.go create mode 100755 app/router/ai_image_to_video.go create mode 100644 app/router/ai_mate.go create mode 100644 app/router/ai_mate_test.go create mode 100644 app/router/ai_mate_v2.go create mode 100644 app/router/ai_mod.go create mode 100755 app/router/ai_text_to_image.go create mode 100755 app/router/ai_text_to_novel.go create mode 100644 app/router/ai_undress.go create mode 100755 app/router/aiplaza.go create mode 100755 app/router/aitemplatemodule.go create mode 100644 app/router/analytics.go create mode 100644 app/router/analytics_test.go create mode 100644 app/router/annou.go create mode 100644 app/router/backpack.go create mode 100644 app/router/base.go create mode 100644 app/router/checkin.go create mode 100644 app/router/comment.go create mode 100644 app/router/content_marker.go create mode 100644 app/router/coupon.go create mode 100644 app/router/customer.go create mode 100644 app/router/defray.go create mode 100644 app/router/drama.go create mode 100644 app/router/drama_test.go create mode 100644 app/router/exchcode.go create mode 100644 app/router/follow.go create mode 100644 app/router/goldextra.go create mode 100644 app/router/health.go create mode 100644 app/router/hotspot.go create mode 100644 app/router/im.go create mode 100755 app/router/imgroup.go create mode 100755 app/router/imgroupmember.go create mode 100755 app/router/immessage.go create mode 100644 app/router/infmt.go create mode 100644 app/router/integral.go create mode 100644 app/router/iteration_routes_test.go create mode 100644 app/router/like.go create mode 100755 app/router/media.go create mode 100755 app/router/mediabookshelf.go create mode 100755 app/router/mediacontent.go create mode 100755 app/router/mediatag.go create mode 100644 app/router/message.go create mode 100644 app/router/mine.go create mode 100644 app/router/module.go create mode 100755 app/router/nakedchat.go create mode 100755 app/router/nakedchatorder.go create mode 100644 app/router/newactivity.go create mode 100644 app/router/notification.go create mode 100644 app/router/official.go create mode 100644 app/router/officialWebsite.go create mode 100644 app/router/payment_guide.go create mode 100644 app/router/ping.go create mode 100644 app/router/product.go create mode 100644 app/router/proxy.go create mode 100644 app/router/publish.go create mode 100644 app/router/rank.go create mode 100644 app/router/rchg.go create mode 100644 app/router/recomm.go create mode 100644 app/router/recreation.go create mode 100644 app/router/scene_banner.go create mode 100644 app/router/search.go create mode 100644 app/router/share.go create mode 100755 app/router/signrecord.go create mode 100644 app/router/sms.go create mode 100644 app/router/static.go create mode 100644 app/router/store.go create mode 100644 app/router/tag.go create mode 100644 app/router/task.go create mode 100644 app/router/tone.go create mode 100644 app/router/txnact.go create mode 100644 app/router/updownLoad.go create mode 100644 app/router/user.go create mode 100644 app/router/userInvite.go create mode 100644 app/router/version.go create mode 100644 app/router/vid.go create mode 100644 app/router/vid_consume_route_test.go create mode 100644 app/router/video_gold_coin.go create mode 100644 app/router/videoactivity.go create mode 100644 app/router/vip.go create mode 100644 app/router/wallet.go create mode 100644 app/router/withdraw.go create mode 100644 app/router/wonder.go create mode 100644 app/service/active2023ser/lottery.go create mode 100644 app/service/active2023ser/type.go create mode 100644 app/service/activityclient/client.go create mode 100644 app/service/activityclient/deduct.go create mode 100644 app/service/activityclient/domain.go create mode 100644 app/service/activityclient/hongbao.go create mode 100644 app/service/activityclient/hongbao_countdown.go create mode 100644 app/service/activityclient/sign.go create mode 100644 app/service/activityclient/svcsign.go create mode 100644 app/service/activityser/activityser.go create mode 100644 app/service/activityser/deduct.go create mode 100644 app/service/activityser/reward.go create mode 100644 app/service/actvser/actv.go create mode 100644 app/service/actvser/type.go create mode 100644 app/service/adser/adser.go create mode 100644 app/service/adser/advertiseV2.go create mode 100644 app/service/adser/jtAdvertise.go create mode 100644 app/service/adser/recreationSer.go create mode 100644 app/service/adser/stat.go create mode 100644 app/service/advance_config_ser/advance_config.go create mode 100644 app/service/advance_ser/advance.go create mode 100644 app/service/ai_changeface_img_ser/ai_changeface_img.go create mode 100644 app/service/ai_changeface_ser/ai_changeface.go create mode 100755 app/service/ai_image_to_video_ser/ai_image_to_video.go create mode 100644 app/service/ai_mate_ser/ai_mate.go create mode 100644 app/service/ai_mate_ser/ai_mate_test.go create mode 100755 app/service/ai_text_to_image_ser/aitexttoimage.go create mode 100755 app/service/ai_text_to_novel_ser/aitexttonovel.go create mode 100644 app/service/ai_undress_server/ai_undress.go create mode 100644 app/service/aiautoser/automation.go create mode 100755 app/service/aiplazaser/aiplaza.go create mode 100644 app/service/aiser/ai.go create mode 100644 app/service/aiser/ai_test.go create mode 100755 app/service/aitemplatemoduleser/aitemplatemodule.go create mode 100644 app/service/annouser/annou.go create mode 100644 app/service/annouser/http.go create mode 100644 app/service/annouser/type.go create mode 100644 app/service/backpackser/backpack.go create mode 100644 app/service/checkinser/checkin.go create mode 100644 app/service/collectser/collectser.go create mode 100644 app/service/commentser/cmtchild.go create mode 100644 app/service/commentser/cmtcoverter.go create mode 100644 app/service/commentser/cmtlimitser.go create mode 100644 app/service/commentser/cmtparent.go create mode 100644 app/service/commentser/cmtsend.go create mode 100644 app/service/commentser/commentser.go create mode 100644 app/service/commentser/reply.go create mode 100644 app/service/common.go create mode 100644 app/service/contentmarkerser/content_marker.go create mode 100644 app/service/contentmarkerser/content_marker_test.go create mode 100644 app/service/couponser/couponser.go create mode 100644 app/service/currencyser/currency.go create mode 100644 app/service/customerser/customer.go create mode 100644 app/service/dramaser/activity.go create mode 100644 app/service/dramaser/download.go create mode 100644 app/service/dramaser/drama.go create mode 100644 app/service/dramaser/drama_test.go create mode 100644 app/service/dramaser/feed_cursor.go create mode 100644 app/service/dramaser/feed_cursor_test.go create mode 100644 app/service/dramaser/topic.go create mode 100644 app/service/dramaser/type.go create mode 100644 app/service/emailser/email.go create mode 100644 app/service/exchcodeser/exchcodeser.go create mode 100644 app/service/feedbackser/feedback.go create mode 100644 app/service/filterser/filter.go create mode 100644 app/service/followser/followser.go create mode 100644 app/service/health_check_ser/health.go create mode 100644 app/service/health_check_ser/type.go create mode 100644 app/service/imadser/advertise.go create mode 100755 app/service/imgroupmemberser/imgroupmember.go create mode 100755 app/service/imgroupser/imgroup.go create mode 100755 app/service/immessageser/immessage.go create mode 100644 app/service/imser/bridge.go create mode 100644 app/service/imser/imser.go create mode 100644 app/service/imser/sdk.go create mode 100644 app/service/infmtser/infmtser.go create mode 100644 app/service/integral_config_ser/integral_config.go create mode 100644 app/service/likeser/likeser.go create mode 100644 app/service/likeser/page.go create mode 100644 app/service/luckydrawser/base.go create mode 100644 app/service/luckydrawser/lucky_draw.go create mode 100644 app/service/luckydrawser/tool.go create mode 100644 app/service/luckydrawser/type.go create mode 100644 app/service/m3u8ticket/sign.go create mode 100644 app/service/m3u8ticket/ticket.go create mode 100644 app/service/m3u8ticket/ticket_test.go create mode 100755 app/service/mediabookshelfser/mediabookshelf.go create mode 100644 app/service/mediacontentser/drama_access_test.go create mode 100755 app/service/mediacontentser/mediacontent.go create mode 100644 app/service/mediacontentser/mediacontent_test.go create mode 100644 app/service/mediaser/help.go create mode 100644 app/service/mediaser/help_test.go create mode 100755 app/service/mediaser/media.go create mode 100644 app/service/mediaser/media_sort_test.go create mode 100644 app/service/mediaser/type.go create mode 100755 app/service/mediatagser/mediatag.go create mode 100644 app/service/messageser/message.go create mode 100644 app/service/mineser/mineser.go create mode 100644 app/service/moduleser/module.go create mode 100644 app/service/modulevideoser/modulevideo.go create mode 100644 app/service/modulevideoser/modulevideo_test.go create mode 100755 app/service/nakedchatorderser/nakedchatorder.go create mode 100755 app/service/nakedchatser/nakedchat.go create mode 100644 app/service/notiser/email.go create mode 100644 app/service/notiser/notificationser.go create mode 100644 app/service/officialWebsiteser/album.go create mode 100644 app/service/officialWebsiteser/basicData.go create mode 100644 app/service/officialWebsiteser/business.go create mode 100644 app/service/officialWebsiteser/common.go create mode 100644 app/service/officialWebsiteser/hero.go create mode 100644 app/service/officialWebsiteser/job.go create mode 100644 app/service/officialWebsiteser/news.go create mode 100644 app/service/officialWebsiteser/partner.go create mode 100644 app/service/officialWebsiteser/recruit.go create mode 100644 app/service/officialWebsiteser/tag.go create mode 100644 app/service/officialWebsiteser/video.go create mode 100644 app/service/officialser/official.go create mode 100644 app/service/paymentguideser/payment_guide.go create mode 100644 app/service/paymentguideser/payment_guide_test.go create mode 100644 app/service/productser/drama.go create mode 100644 app/service/productser/dto.go create mode 100644 app/service/productser/handler.go create mode 100644 app/service/productser/productser.go create mode 100644 app/service/productser/tool.go create mode 100644 app/service/productser/vip_experiment_attribution.go create mode 100644 app/service/productser/vip_experiment_attribution_test.go create mode 100644 app/service/proxyser/error.go create mode 100644 app/service/proxyser/proxyser.go create mode 100644 app/service/publishser/publish.go create mode 100644 app/service/publishser/type.go create mode 100644 app/service/questionnreser/questionnreser.go create mode 100644 app/service/rankser/rankser.go create mode 100644 app/service/rankser/type.go create mode 100644 app/service/rechargeser/new_pay.go create mode 100644 app/service/rechargeser/paytype_config.go create mode 100644 app/service/rechargeser/rechargeser.go create mode 100644 app/service/rechargeser/type.go create mode 100644 app/service/rechargeser/type_attribution_test.go create mode 100644 app/service/rechargeser/vip_attribution.go create mode 100644 app/service/recommser/recommser.go create mode 100644 app/service/search/search.go create mode 100644 app/service/search/type.go create mode 100644 app/service/searchaccessser/search_access.go create mode 100644 app/service/searchaccessser/search_access_test.go create mode 100644 app/service/searcher/audiobooksearcher/audiobookSearcher.go create mode 100644 app/service/searcher/audiobooksearcher/result.go create mode 100644 app/service/searcher/fictionsearcher/fictionSearcher.go create mode 100644 app/service/searcher/fictionsearcher/result.go create mode 100644 app/service/searcher/keyword.go create mode 100644 app/service/searcher/location.go create mode 100644 app/service/searcher/loufeng.go create mode 100644 app/service/searcher/opt.go create mode 100644 app/service/searcher/result.go create mode 100644 app/service/searcher/search.go create mode 100644 app/service/searcher/tag.go create mode 100644 app/service/searcher/tagsearcher/result.go create mode 100644 app/service/searcher/tagsearcher/tagSearcher.go create mode 100644 app/service/searcher/tonesearcher/result.go create mode 100644 app/service/searcher/tonesearcher/toneSearcher.go create mode 100644 app/service/searcher/user.go create mode 100644 app/service/searcher/usersearcher/result.go create mode 100644 app/service/searcher/usersearcher/userSearcher.go create mode 100644 app/service/searcher/video.go create mode 100644 app/service/searcher/vidhkwsearcher/result.go create mode 100644 app/service/searcher/vidhkwsearcher/vidHotKeyWordSearcher.go create mode 100644 app/service/searcher/vidhotsearcher/result.go create mode 100644 app/service/searcher/vidhotsearcher/vidhotsearcher.go create mode 100644 app/service/searcher/vidpcountsearcher/result.go create mode 100644 app/service/searcher/vidpcountsearcher/vidPlayCountSearcher.go create mode 100644 app/service/searcher/vidrichsearcher/result.go create mode 100644 app/service/searcher/vidrichsearcher/vidRichSearcher.go create mode 100644 app/service/searcher/vidsearcher/result.go create mode 100644 app/service/searcher/vidsearcher/vidSearcher.go create mode 100644 app/service/searcher/vidtagsearcher/result.go create mode 100644 app/service/searcher/vidtagsearcher/vidTagSearcher.go create mode 100644 app/service/searcher/vidtagsearcher/vidTagSearcher_test.go create mode 100644 app/service/searcher/vidtonesearcher/freeArea.go create mode 100644 app/service/searcher/vidtonesearcher/goldCoinArea.go create mode 100644 app/service/searcher/vidtonesearcher/latestUpload.go create mode 100644 app/service/searcher/vidtonesearcher/mostComments.go create mode 100644 app/service/searcher/vidtonesearcher/mostLikes.go create mode 100644 app/service/searcher/vidtonesearcher/officialRecom.go create mode 100644 app/service/searcher/vidtonesearcher/playAtMost.go create mode 100644 app/service/searcher/vidtonesearcher/result.go create mode 100644 app/service/searcher/vidtonesearcher/vidToneSearcher.go create mode 100644 app/service/shareser/recommend_share.go create mode 100644 app/service/shareser/recommend_share_test.go create mode 100644 app/service/shareser/share.go create mode 100644 app/service/shareser/share_test.go create mode 100644 app/service/shortrecommendser/fetch.go create mode 100644 app/service/shortrecommendser/fetch_test.go create mode 100755 app/service/signrecordser/signrecord.go create mode 100644 app/service/smsser/smsser.go create mode 100644 app/service/sys_config/freewatch.go create mode 100644 app/service/sys_config/paymentpopup.go create mode 100644 app/service/systemser/config.go create mode 100644 app/service/tagser/list.go create mode 100644 app/service/tagser/tag.go create mode 100644 app/service/tagser/type.go create mode 100644 app/service/taskser/app.go create mode 100644 app/service/taskser/sign.go create mode 100644 app/service/taskser/task.go create mode 100644 app/service/taskser/tool.go create mode 100644 app/service/updownloadser/updownloadser.go create mode 100644 app/service/userser/privilegeser.go create mode 100644 app/service/userser/statuser.go create mode 100644 app/service/userser/type.go create mode 100644 app/service/userser/userser.go create mode 100644 app/service/userwatchrecordserver/userwatchrecord.go create mode 100644 app/service/versionser/versionV2.go create mode 100644 app/service/versionser/versionV2_test.go create mode 100644 app/service/versionser/versionser.go create mode 100644 app/service/video_gold_coin_ser/video_gold_coin.go create mode 100644 app/service/videoactivityserver/video_activity.go create mode 100644 app/service/vidhelpser/free_trial.go create mode 100644 app/service/vidhelpser/free_trial_test.go create mode 100644 app/service/vidhelpser/h265.go create mode 100644 app/service/vidhelpser/h265_test.go create mode 100644 app/service/vidhelpser/helpser.go create mode 100644 app/service/vidhelpser/helpser_concurrency_test.go create mode 100644 app/service/vidhelpser/mongohelp.go create mode 100644 app/service/vidhelpser/redishelp.go create mode 100644 app/service/vidser/acg_sort_test.go create mode 100644 app/service/vidser/dedup.go create mode 100644 app/service/vidser/discountArea.go create mode 100644 app/service/vidser/free_trial.go create mode 100644 app/service/vidser/free_watch_consume.go create mode 100644 app/service/vidser/free_watch_consume_test.go create mode 100644 app/service/vidser/library.go create mode 100644 app/service/vidser/m3u8sign.go create mode 100644 app/service/vidser/m3u8sign_test.go create mode 100644 app/service/vidser/media_library.go create mode 100644 app/service/vidser/module_refresh.go create mode 100644 app/service/vidser/module_refresh_test.go create mode 100644 app/service/vidser/ranking.go create mode 100644 app/service/vidser/redis.go create mode 100644 app/service/vidser/type.go create mode 100644 app/service/vidser/vidser.go create mode 100644 app/service/vipcardexperimentser/service.go create mode 100644 app/service/vipcardexperimentser/service_test.go create mode 100644 app/service/walletser/inviteuser.go create mode 100644 app/service/walletser/store.go create mode 100644 app/service/walletser/type.go create mode 100644 app/service/walletser/walletser.go create mode 100644 app/service/withdrawser/withdrawser.go create mode 100644 app/templates/index.html create mode 100644 app/templates/sources/img/right.png create mode 100644 app/templates/sources/js/touch.js create mode 100644 app/templates/sources/style/animate.css create mode 100644 app/templates/sources/style/common.css create mode 100644 app/templates/sources/style/page.css create mode 100644 app/templates/sources/style/reset.css create mode 100644 app/templates/sources/util/rem.js create mode 100644 app/templates/sources/util/tools.js create mode 100644 common/PromotionCodeUtil.go create mode 100644 common/aiMate/aiMate.go create mode 100644 common/aiService/aiService.go create mode 100644 common/aiService/rediskey.go create mode 100644 common/aiService/type.go create mode 100644 common/bank/bank.go create mode 100644 common/cache/cache.go create mode 100644 common/cachev2/base.go create mode 100644 common/cachev2/proto.go create mode 100644 common/cachev2/util.go create mode 100644 common/checkWx/checkWx.go create mode 100644 common/conf/conf.go create mode 100644 common/constant/cacheconst/cacheconst.go create mode 100644 common/constant/constant.go create mode 100644 common/constant/imnotifyconst/imnotifyconst.go create mode 100644 common/constant/redisconst/most_new_module_test.go create mode 100644 common/constant/redisconst/redisconst.go create mode 100644 common/convUtil.go create mode 100644 common/crypt/checkSign.go create mode 100644 common/crypt/crypt.go create mode 100644 common/crypt/crypt_ex_test.go create mode 100644 common/crypt/ecb/ecb.go create mode 100644 common/crypt/rsa.go create mode 100644 common/daichong/daichong.go create mode 100644 common/daichong/dcfunction.go create mode 100644 common/daichong/new-dcfunction.go create mode 100644 common/daichong/newdaichong.go create mode 100644 common/dataReport/handle.go create mode 100644 common/dataReport/report_test.go create mode 100644 common/dataReport/type.go create mode 100644 common/datacenter/handle.go create mode 100644 common/datacenter/proto.go create mode 100644 common/db/codec.go create mode 100644 common/db/helper.go create mode 100644 common/db/mongo.go create mode 100644 common/db/mongoClient.go create mode 100644 common/db/opt.go create mode 100644 common/deduction.go create mode 100644 common/dramatopic/topic.go create mode 100644 common/elastic/elastic.go create mode 100644 common/elastic/elasticClient.go create mode 100644 common/elastic/elasticInit.go create mode 100644 common/email/email.go create mode 100644 common/email/init.go create mode 100644 common/enum/imad/position.go create mode 100644 common/export/export.go create mode 100644 common/export/interface.go create mode 100644 common/file/file.go create mode 100644 common/file/path.go create mode 100644 common/file/s3.go create mode 100755 common/filter/filter.go create mode 100644 common/filter/filterClient.go create mode 100644 common/game/game.go create mode 100644 common/ginUtil.go create mode 100644 common/go.go create mode 100644 common/googauth/googauth.go create mode 100644 common/googauth/htop.go create mode 100644 common/handleCong.go create mode 100644 common/hevcpull/sign.go create mode 100644 common/hevcpull/sign_test.go create mode 100644 common/httputil/http_client.go create mode 100644 common/httputil/http_get.go create mode 100644 common/httputil/http_post.go create mode 100644 common/httputil/http_post_json.go create mode 100644 common/httputil/httputil.go create mode 100644 common/imclient/client.go create mode 100644 common/imclient/defaults.go create mode 100644 common/imclient/errors.go create mode 100644 common/imclient/history_message.go create mode 100644 common/ip.go create mode 100644 common/kafka/kafka.go create mode 100644 common/laosiji/comics.go create mode 100644 common/laosiji/comics_define.go create mode 100644 common/laosiji/laosiji.go create mode 100644 common/laosiji/movie.go create mode 100644 common/laosiji/movie_define.go create mode 100644 common/laosiji/novel.go create mode 100644 common/laosiji/novel_define.go create mode 100644 common/laosiji/post.go create mode 100644 common/laosiji/post_define.go create mode 100644 common/laosiji/textToImage.go create mode 100644 common/laosiji/tools.go create mode 100644 common/laosiji/tools_test.go create mode 100644 common/laosiji/undress.go create mode 100644 common/laosiji_app/ai_mate.go create mode 100644 common/laosiji_app/laosiji.go create mode 100644 common/laosiji_app/laosiji_test.go create mode 100644 common/localcache/localcache.go create mode 100644 common/log/log.go create mode 100644 common/m3u8/m3u8.go create mode 100644 common/m3u8/m3u8_test.go create mode 100755 common/maths/correlation.go create mode 100755 common/maths/data.go create mode 100644 common/maths/decimal.go create mode 100755 common/maths/deviation.go create mode 100755 common/maths/errors.go create mode 100755 common/maths/legacy.go create mode 100755 common/maths/load.go create mode 100755 common/maths/maths.go create mode 100755 common/maths/max.go create mode 100755 common/maths/mean.go create mode 100755 common/maths/min.go create mode 100755 common/maths/mode.go create mode 100755 common/maths/round.go create mode 100755 common/maths/sum.go create mode 100755 common/maths/variance.go create mode 100644 common/mdrsdb/mediaresourcedatabase.go create mode 100644 common/mdrsdb/type.go create mode 100644 common/media/h265_test.go create mode 100644 common/media/sync.go create mode 100644 common/media/syncMedia.go create mode 100644 common/media/useMedia.go create mode 100644 common/media/useMedia_test.go create mode 100644 common/media/worker.go create mode 100644 common/mediaCenter/base.go create mode 100644 common/mediaCenter/media.go create mode 100644 common/mediaCenter/mediaType.go create mode 100644 common/mediaCenter/mediaType_test.go create mode 100644 common/mergemap/mergemap.go create mode 100644 common/modelUtil.go create mode 100644 common/money.go create mode 100644 common/pageopt/match.go create mode 100644 common/parseFilter.go create mode 100644 common/picture.go create mode 100644 common/qrcode.go create mode 100644 common/rand.go create mode 100644 common/rchgutil/asen.go create mode 100644 common/rchgutil/base.go create mode 100644 common/rchgutil/bill.go create mode 100644 common/rchgutil/billExchg.go create mode 100644 common/rchgutil/dabaisha.go create mode 100644 common/rchgutil/dx.go create mode 100644 common/rchgutil/goldfishExchange.go create mode 100644 common/rchgutil/goldfishplus.go create mode 100644 common/rchgutil/izhifu.go create mode 100644 common/rchgutil/leihuo.go create mode 100644 common/rchgutil/lucky.go create mode 100644 common/rchgutil/recharge.go create mode 100644 common/rchgutil/recharge_cache_test.go create mode 100644 common/rchgutil/shark.go create mode 100644 common/rchgutil/yilianfu.go create mode 100644 common/rchgutil/yongli.go create mode 100644 common/rchgutil/yongliwanshun.go create mode 100644 common/rchgutil/zhongfu.go create mode 100644 common/redis/redis.go create mode 100644 common/redis/redis_test.go create mode 100644 common/redis/redisutil.go create mode 100644 common/redis/redsync.go create mode 100644 common/services/follow/tip.go create mode 100644 common/services/message/tip.go create mode 100644 common/services/services.go create mode 100644 common/shortrecommend/shortrecommend.go create mode 100644 common/shortrecommend/shortrecommend_test.go create mode 100644 common/slice/sli.go create mode 100644 common/sms/fengniao/fengniao.go create mode 100644 common/sms/sms.go create mode 100644 common/stderr/code.go create mode 100644 common/stderr/customerr.go create mode 100644 common/stderr/error.go create mode 100644 common/stderr/msg.go create mode 100644 common/stderr/stderr.go create mode 100644 common/stderr/tip.go create mode 100644 common/store/store.go create mode 100644 common/synclock/lock.go create mode 100644 common/tf/tf.go create mode 100644 common/tg/sendOpt.go create mode 100644 common/tg/tg.go create mode 100644 common/timeutil/timerange/timerange.go create mode 100644 common/timeutil/timeslice/timeSlice.go create mode 100644 common/timeutil/timeutil.go create mode 100644 common/top/asistant/asistant.go create mode 100644 common/top/dailytop/grow.go create mode 100644 common/top/monthtop/grow.go create mode 100644 common/top/top.go create mode 100644 common/top/weektop/grow.go create mode 100644 common/top/yeartop/grow.go create mode 100644 common/truthutil/truthutil.go create mode 100644 common/truthutil/truthutil_test.go create mode 100644 common/typeConv.go create mode 100644 common/usertruth/scode.jpg create mode 100644 common/usertruth/userTruth.go create mode 100644 common/utils.go create mode 100644 common/uuid.go create mode 100644 common/v10/upgradev10.go create mode 100644 common/v10/validate.go create mode 100644 common/version/version.go create mode 100644 common/vid.go create mode 100644 common/worker/worker.go create mode 100644 common/ysinterface/disc/disc.go create mode 100644 common/ysphone/phone.go create mode 100644 common/ysqr/error.go create mode 100644 common/ysqr/qr.go create mode 100644 common/ysurl/yslinks.go create mode 100644 doc.go create mode 100644 generate/common/generate.go create mode 100644 generate/common/helper.go create mode 100644 generate/model/aiplaza/main.go create mode 100644 generate/model/hotsearch/main.go create mode 100644 generate/model/imgroup/main.go create mode 100644 generate/model/imgroupmember/main.go create mode 100644 generate/model/immessage/main.go create mode 100644 generate/model/nakechatorder/main.go create mode 100644 generate/model/nakedchat/main.go create mode 100644 generate/model/sysconf/main.go create mode 100644 generate/tpl/app/api/{{PackageTableName}}.tpl create mode 100644 generate/tpl/app/router/{{PackageTableName}}.tpl create mode 100644 generate/tpl/app/service/{{PackageTableName}}.tpl create mode 100644 generate/tpl/config.go create mode 100644 generate/tpl/models/cache/data.tpl create mode 100644 generate/tpl/models/cache/{{PackageTableName}}.tpl create mode 100644 generate/tpl/models/v/app.tpl create mode 100644 generate/tpl/models/v/type.tpl create mode 100644 generate/tpl/models/v/{{PackageTableName}}.tpl create mode 100644 generate/tpl/web/api/{{PackageTableName}}.tpl create mode 100644 generate/tpl/web/router/{{PackageTableName}}.tpl create mode 100644 generate/tpl/web/service/{{PackageTableName}}.tpl create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 middleware/cors/cors.go create mode 100644 middleware/ginzap/ginzap.go create mode 100644 middleware/ginzap/ginzap_test.go create mode 100644 middleware/ip/ip.go create mode 100644 middleware/requestid/requestid.go create mode 100644 middleware/requestid/requestid_test.go create mode 100644 middleware/requestid/type.go create mode 100644 middleware/ua/ua.go create mode 100644 middleware/ua/ua_test.go create mode 100644 models/Init/init.go create mode 100755 models/cache/aiimagetovideodata/aiimagetovideo.go create mode 100755 models/cache/aiimagetovideodata/data.go create mode 100755 models/cache/aiplazadata/aiplaza.go create mode 100755 models/cache/aiplazadata/data.go create mode 100755 models/cache/aitemplatemoduledata/aitemplatemodule.go create mode 100755 models/cache/aitemplatemoduledata/data.go create mode 100755 models/cache/aitexttoimagedata/aitexttoimage.go create mode 100755 models/cache/aitexttoimagedata/data.go create mode 100755 models/cache/aitexttonoveldata/aitexttonovel.go create mode 100755 models/cache/aitexttonoveldata/data.go create mode 100755 models/cache/bannerjumpdata/bannerjump.go create mode 100755 models/cache/imgroupdata/data.go create mode 100755 models/cache/imgroupdata/imgroup.go create mode 100755 models/cache/imgroupmemberdata/data.go create mode 100755 models/cache/imgroupmemberdata/imgroupmember.go create mode 100755 models/cache/immessagedata/data.go create mode 100755 models/cache/immessagedata/immessage.go create mode 100755 models/cache/mediabookshelfdata/data.go create mode 100755 models/cache/mediabookshelfdata/mediabookshelf.go create mode 100755 models/cache/mediacontentdata/mediacontent.go create mode 100755 models/cache/mediadata/media.go create mode 100755 models/cache/mediatagdata/mediatag.go create mode 100755 models/cache/mediatagdimensiondata/mediatagdimension.go create mode 100755 models/cache/nakedchatdata/data.go create mode 100755 models/cache/nakedchatdata/nakedchat.go create mode 100755 models/cache/nakedchatorderdata/data.go create mode 100755 models/cache/nakedchatorderdata/nakedchatorder.go create mode 100755 models/cache/quicksearchdata/data.go create mode 100755 models/cache/quicksearchdata/quicksearch.go create mode 100755 models/cache/signrecorddata/data.go create mode 100755 models/cache/signrecorddata/signrecord.go create mode 100755 models/cache/sysconfdata/sysconf.go create mode 100644 models/cache/sysconfdata/sysconf_test.go create mode 100755 models/cache/viddata/vid.go create mode 100644 models/commod/commod.go create mode 100644 models/commod/consts.go create mode 100644 models/commod/dedType.go create mode 100644 models/commod/disc.go create mode 100644 models/commod/ldy.go create mode 100644 models/commod/product.go create mode 100644 models/commod/sort.go create mode 100644 models/commod/stat-center.go create mode 100644 models/e/daichongmod/app.go create mode 100644 models/e/daichongmod/web.go create mode 100644 models/e/sharemod/app.go create mode 100644 models/e/sharemod/type.go create mode 100644 models/esTable.go create mode 100644 models/l/adsclicklogmod/type.go create mode 100644 models/l/exchlogmod/exchangecode.go create mode 100644 models/l/exchlogmod/type.go create mode 100644 models/l/exchlogmod/web.go create mode 100644 models/l/loginlgmod/login.go create mode 100644 models/l/loginlgmod/type.go create mode 100644 models/l/lotterylgmod/lottery.go create mode 100644 models/l/lotterylgmod/type.go create mode 100644 models/l/lotterylgmod/web.go create mode 100644 models/l/operatorlgmod/operator.go create mode 100644 models/l/operatorlgmod/type.go create mode 100644 models/l/operatorlgmod/web.go create mode 100644 models/l/payvidlgmod/app.go create mode 100644 models/l/payvidlgmod/discStat.go create mode 100644 models/l/payvidlgmod/list.go create mode 100644 models/l/payvidlgmod/pay.go create mode 100644 models/l/payvidlgmod/type.go create mode 100644 models/l/playlgmod/play.go create mode 100644 models/l/playlgmod/type.go create mode 100644 models/l/playlgmod/web.go create mode 100644 models/l/pullgmod/pull.go create mode 100644 models/l/pullgmod/type.go create mode 100644 models/l/registermod/register.go create mode 100644 models/l/registermod/type.go create mode 100644 models/l/searchlogmod/searchLog.go create mode 100644 models/l/searchlogmod/type.go create mode 100644 models/l/synccdnmod/synccdn.go create mode 100644 models/l/synccdnmod/type.go create mode 100644 models/l/userdailytasklogmod/type.go create mode 100644 models/l/visitlogmod/list.go create mode 100644 models/l/visitlogmod/type.go create mode 100644 models/l/visitlogmod/visitLog.go create mode 100644 models/l/welfarelgmod/pull.go create mode 100644 models/l/welfarelgmod/type.go create mode 100644 models/s/dailyretentionmod/dailyRetention.go create mode 100644 models/s/dailyretentionmod/type.go create mode 100644 models/s/hotstatmod/hotstat.go create mode 100644 models/s/hotstatmod/type.go create mode 100644 models/s/kwrankmod/keywordrank.go create mode 100644 models/s/kwrankmod/list.go create mode 100644 models/s/kwrankmod/type.go create mode 100644 models/s/kwstatmod/keywordStat.go create mode 100644 models/s/kwstatmod/type.go create mode 100644 models/s/sectionstatmod/stat.go create mode 100644 models/s/sectionstatmod/type.go create mode 100644 models/s/statordermod/list.go create mode 100644 models/s/statordermod/order.go create mode 100644 models/s/statordermod/type.go create mode 100644 models/s/statrecordmod/statRecord.go create mode 100644 models/s/statrecordmod/type.go create mode 100644 models/s/stattagplaymod/tagPlay.go create mode 100644 models/s/stattagplaymod/type.go create mode 100644 models/s/statusermod/list.go create mode 100644 models/s/statusermod/type.go create mode 100644 models/s/statusermod/userstat.go create mode 100644 models/s/statvidmod/list.go create mode 100644 models/s/statvidmod/page.go create mode 100644 models/s/statvidmod/stat.go create mode 100644 models/s/statvidmod/sum.go create mode 100644 models/s/statvidmod/type.go create mode 100644 models/s/statvidtotalmod/stat.go create mode 100644 models/s/statvidtotalmod/type.go create mode 100644 models/s/useradverstatmod/aggregate.go create mode 100644 models/s/useradverstatmod/type.go create mode 100644 models/s/useradverstatmod/useradverstat.go create mode 100644 models/table.go create mode 100644 models/v/active2023mod/free.go create mode 100644 models/v/active2023mod/lottery.go create mode 100644 models/v/active2023mod/prize.go create mode 100644 models/v/active2023mod/type.go create mode 100644 models/v/active2023mod/user.go create mode 100644 models/v/activitymod/activity.go create mode 100644 models/v/activitymod/prize_pool.go create mode 100644 models/v/activitymod/prize_pool_type.go create mode 100644 models/v/activitymod/type.go create mode 100644 models/v/activitymod/user_free.go create mode 100644 models/v/activitymod/user_free_type.go create mode 100644 models/v/activitymod/user_record.go create mode 100644 models/v/activitymod/user_record_type.go create mode 100644 models/v/activitymod/web.go create mode 100644 models/v/actmod/activity.go create mode 100644 models/v/actmod/type.go create mode 100644 models/v/actmod/web.go create mode 100644 models/v/adminmod/admin.go create mode 100644 models/v/adminmod/type.go create mode 100644 models/v/adreviewmod/adreview.go create mode 100644 models/v/adreviewmod/error.go create mode 100644 models/v/adreviewmod/list.go create mode 100644 models/v/adreviewmod/type.go create mode 100644 models/v/adsmod/ads.go create mode 100644 models/v/adsmod/error.go create mode 100644 models/v/adsmod/list.go create mode 100644 models/v/adsmod/type.go create mode 100644 models/v/advanceconfigmod/advanceconfig.go create mode 100644 models/v/advanceconfigmod/app.go create mode 100644 models/v/advanceconfigmod/type.go create mode 100644 models/v/advanceconfigmod/web.go create mode 100644 models/v/advanceordermod/advanceorder.go create mode 100644 models/v/advanceordermod/app.go create mode 100644 models/v/advanceordermod/type.go create mode 100644 models/v/advanceordermod/web.go create mode 100644 models/v/aiUnDressmod/aiUnDress.go create mode 100644 models/v/aiUnDressmod/app.go create mode 100644 models/v/aiUnDressmod/type.go create mode 100644 models/v/aiUnDressmod/web.go create mode 100644 models/v/aichangefaceimgmod/aichangefaceimg.go create mode 100644 models/v/aichangefaceimgmod/app.go create mode 100644 models/v/aichangefaceimgmod/type.go create mode 100644 models/v/aichangefaceimgmod/web.go create mode 100644 models/v/aichangefacemod/aichangeface.go create mode 100644 models/v/aichangefacemod/app.go create mode 100644 models/v/aichangefacemod/type.go create mode 100644 models/v/aichangefacemod/web.go create mode 100644 models/v/aichangefacevidmod/aichangefacevid.go create mode 100644 models/v/aichangefacevidmod/app.go create mode 100644 models/v/aichangefacevidmod/type.go create mode 100644 models/v/aichangefacevidmod/web.go create mode 100755 models/v/aiimagetovideomod/aiimagetovideo.go create mode 100755 models/v/aiimagetovideomod/app.go create mode 100755 models/v/aiimagetovideomod/type.go create mode 100755 models/v/aiplazamod/aiplaza.go create mode 100755 models/v/aiplazamod/app.go create mode 100755 models/v/aiplazamod/type.go create mode 100755 models/v/aitemplatemodulemod/aitemplatemodule.go create mode 100755 models/v/aitemplatemodulemod/app.go create mode 100755 models/v/aitemplatemodulemod/type.go create mode 100755 models/v/aitexttoimagemod/aitexttoimage.go create mode 100755 models/v/aitexttoimagemod/app.go create mode 100755 models/v/aitexttoimagemod/type.go create mode 100755 models/v/aitexttonovelmod/aitexttonovel.go create mode 100755 models/v/aitexttonovelmod/type.go create mode 100644 models/v/annoumod/annou.go create mode 100644 models/v/annoumod/announce.go create mode 100644 models/v/annoumod/app.go create mode 100644 models/v/annoumod/type.go create mode 100644 models/v/annoumod/web.go create mode 100644 models/v/audioanchormod/audioanchormod.go create mode 100644 models/v/audioanchormod/es.go create mode 100644 models/v/audioanchormod/type.go create mode 100644 models/v/audiobookmod/audiobook.go create mode 100644 models/v/audiobookmod/es.go create mode 100644 models/v/audiobookmod/type.go create mode 100644 models/v/authoritymod/authority.go create mode 100644 models/v/authoritymod/page.go create mode 100644 models/v/authoritymod/type.go create mode 100644 models/v/backpackmod/app.go create mode 100644 models/v/backpackmod/backpack.go create mode 100644 models/v/backpackmod/type.go create mode 100644 models/v/backpackmod/web.go create mode 100755 models/v/bannerjumpmod/app.go create mode 100755 models/v/bannerjumpmod/bannerjump.go create mode 100755 models/v/bannerjumpmod/type.go create mode 100644 models/v/chatrobotmod/chatrobot.go create mode 100644 models/v/chatrobotmod/type.go create mode 100644 models/v/chatrobotmod/web.go create mode 100644 models/v/checkinconfigmod/checkinconfig.go create mode 100644 models/v/checkinconfigmod/type.go create mode 100644 models/v/checkinprizemod/checkinprize.go create mode 100644 models/v/checkinprizemod/type.go create mode 100644 models/v/cmtmod/agent.go create mode 100644 models/v/cmtmod/app.go create mode 100644 models/v/cmtmod/comment.go create mode 100644 models/v/cmtmod/list.go create mode 100644 models/v/cmtmod/type.go create mode 100644 models/v/cmtmod/web.go create mode 100644 models/v/collectmod/app.go create mode 100644 models/v/collectmod/collect.go create mode 100644 models/v/collectmod/collect_video_status_test.go create mode 100644 models/v/collectmod/type.go create mode 100644 models/v/contentlibmod/contentlib.go create mode 100644 models/v/contentlibmod/type.go create mode 100644 models/v/contentlibmod/web.go create mode 100644 models/v/contentreviewmod/crud.go create mode 100644 models/v/contentreviewmod/type.go create mode 100644 models/v/coupon_record_mod/app.go create mode 100644 models/v/coupon_record_mod/coupon_record.go create mode 100644 models/v/coupon_record_mod/type.go create mode 100644 models/v/coupon_record_mod/web.go create mode 100644 models/v/currencymod/base.go create mode 100644 models/v/currencymod/currency.go create mode 100644 models/v/currencymod/type.go create mode 100644 models/v/dailytaskmod/dailytask.go create mode 100644 models/v/dailytaskmod/web.go create mode 100644 models/v/discount_area_mod/app.go create mode 100644 models/v/discount_area_mod/dIscountArea.go create mode 100644 models/v/discount_area_mod/type.go create mode 100644 models/v/discount_area_mod/web.go create mode 100644 models/v/discount_area_video_mod/discount_area_video.go create mode 100644 models/v/discount_area_video_mod/type.go create mode 100644 models/v/errfedbkmod/errorfeedback.go create mode 100644 models/v/errfedbkmod/type.go create mode 100644 models/v/eventmod/event.go create mode 100644 models/v/eventmod/type.go create mode 100644 models/v/exchcodemod/exchcode.go create mode 100644 models/v/exchcodemod/type.go create mode 100644 models/v/exchcodemod/web.go create mode 100644 models/v/export_task_mod/exportTask.go create mode 100644 models/v/export_task_mod/type.go create mode 100644 models/v/faqmod/faq.go create mode 100644 models/v/faqmod/type.go create mode 100644 models/v/feedbackmod/feedback.go create mode 100644 models/v/feedbackmod/type.go create mode 100644 models/v/feedbackmod/web.go create mode 100644 models/v/fictionmod/es.go create mode 100644 models/v/fictionmod/fiction.go create mode 100644 models/v/fictionmod/type.go create mode 100644 models/v/filtermod/filter.go create mode 100644 models/v/filtermod/type.go create mode 100644 models/v/filtermod/web.go create mode 100644 models/v/followmod/app.go create mode 100644 models/v/followmod/follow.go create mode 100644 models/v/followmod/list.go create mode 100644 models/v/followmod/type.go create mode 100644 models/v/freeVidmod/freeVidConfig.go create mode 100644 models/v/freeVidmod/type.go create mode 100644 models/v/freeVidmod/web.go create mode 100644 models/v/fsuidmod/fsuid.go create mode 100644 models/v/fsuidmod/type.go create mode 100644 models/v/fundtransferlogmod/crud.go create mode 100644 models/v/fundtransferlogmod/type.go create mode 100644 models/v/goldcfgmod/goldcfg.go create mode 100644 models/v/goldcfgmod/type.go create mode 100644 models/v/goldcfgmod/web.go create mode 100644 models/v/goldextramod/goldextramod.go create mode 100644 models/v/goldextramod/type.go create mode 100644 models/v/hevctaskmod/hevctask.go create mode 100644 models/v/hevctaskmod/type.go create mode 100644 models/v/idmod/id.go create mode 100644 models/v/idmod/type.go create mode 100644 models/v/imagetopmod/imagetop.go create mode 100644 models/v/imagetopmod/type.go create mode 100755 models/v/imgroupmembermod/app.go create mode 100755 models/v/imgroupmembermod/imgroupmember.go create mode 100755 models/v/imgroupmembermod/type.go create mode 100755 models/v/imgroupmod/app.go create mode 100755 models/v/imgroupmod/imgroup.go create mode 100755 models/v/imgroupmod/type.go create mode 100755 models/v/immessagemod/app.go create mode 100755 models/v/immessagemod/immessage.go create mode 100755 models/v/immessagemod/type.go create mode 100644 models/v/immod/app.go create mode 100644 models/v/immod/im.go create mode 100644 models/v/immod/type.go create mode 100644 models/v/immod/web.go create mode 100644 models/v/imusermod/im_user.go create mode 100644 models/v/imusermod/type.go create mode 100644 models/v/integralconfigmod/app.go create mode 100644 models/v/integralconfigmod/integralconfig.go create mode 100644 models/v/integralconfigmod/type.go create mode 100644 models/v/integralconfigmod/web.go create mode 100644 models/v/integralexcangemod/app.go create mode 100644 models/v/integralexcangemod/integralexchange.go create mode 100644 models/v/integralexcangemod/type.go create mode 100644 models/v/integralexcangemod/web.go create mode 100644 models/v/ipblockmod/ipblock.go create mode 100644 models/v/ipblockmod/type.go create mode 100644 models/v/ipblockmod/web.go create mode 100644 models/v/ipwhitemod/ipwhite.go create mode 100644 models/v/ipwhitemod/type.go create mode 100644 models/v/ipwhitemod/web.go create mode 100644 models/v/jingangmod/app.go create mode 100644 models/v/jingangmod/jingang.go create mode 100644 models/v/jingangmod/type.go create mode 100644 models/v/ldycfgmod/ldy.go create mode 100644 models/v/ldycfgmod/type.go create mode 100644 models/v/likemod/app.go create mode 100644 models/v/likemod/like.go create mode 100644 models/v/likemod/like_video_status_test.go create mode 100644 models/v/likemod/list.go create mode 100644 models/v/likemod/type.go create mode 100644 models/v/locmod/app.go create mode 100644 models/v/locmod/hotCity.go create mode 100644 models/v/locmod/location.go create mode 100644 models/v/locmod/type.go create mode 100644 models/v/locmod/web.go create mode 100644 models/v/marqueemod/method.go create mode 100644 models/v/marqueemod/type.go create mode 100644 models/v/media_buy_record_mod/mediaBuyRecord.go create mode 100644 models/v/media_buy_record_mod/type.go create mode 100755 models/v/mediabookshelfmod/app.go create mode 100755 models/v/mediabookshelfmod/mediabookshelf.go create mode 100755 models/v/mediabookshelfmod/type.go create mode 100755 models/v/mediacontentmod/mediacontent.go create mode 100755 models/v/mediacontentmod/type.go create mode 100644 models/v/mediamod/app.go create mode 100644 models/v/mediamod/es.go create mode 100755 models/v/mediamod/media.go create mode 100644 models/v/mediamod/media_latest_published_test.go create mode 100755 models/v/mediamod/type.go create mode 100644 models/v/mediamod/update_marker.go create mode 100644 models/v/mediamod/web.go create mode 100755 models/v/mediatagdimensionmod/mediatagdimension.go create mode 100755 models/v/mediatagdimensionmod/type.go create mode 100644 models/v/mediatagdimensionmod/web.go create mode 100755 models/v/mediatagmod/mediatag.go create mode 100755 models/v/mediatagmod/type.go create mode 100644 models/v/messagemod/app.go create mode 100644 models/v/messagemod/message.go create mode 100644 models/v/messagemod/type.go create mode 100644 models/v/messagemod/web.go create mode 100644 models/v/moduleconfmod/app.go create mode 100644 models/v/moduleconfmod/moduleconf.go create mode 100644 models/v/moduleconfmod/moduleconf_test.go create mode 100644 models/v/moduleconfmod/refresh_test.go create mode 100644 models/v/moduleconfmod/snapshot_cache.go create mode 100644 models/v/moduleconfmod/snapshot_cache_test.go create mode 100644 models/v/moduleconfmod/type.go create mode 100644 models/v/moduleconfmod/web.go create mode 100644 models/v/modulesectionmod/section.go create mode 100644 models/v/modulesectionmod/type.go create mode 100644 models/v/modulevidmod/modulevid.go create mode 100644 models/v/modulevidmod/type.go create mode 100755 models/v/nakedchatmod/app.go create mode 100755 models/v/nakedchatmod/nakedchat.go create mode 100755 models/v/nakedchatmod/type.go create mode 100755 models/v/nakedchatordermod/app.go create mode 100755 models/v/nakedchatordermod/nakedchatorder.go create mode 100755 models/v/nakedchatordermod/type.go create mode 100644 models/v/newactivity/activitymodel.go create mode 100644 models/v/newactivity/activitysold.go create mode 100644 models/v/newactivity/activitystock.go create mode 100644 models/v/newactivity/activitywin.go create mode 100644 models/v/newactivity/type.go create mode 100644 models/v/noticefmtmod/list.go create mode 100644 models/v/noticefmtmod/notice.go create mode 100644 models/v/noticefmtmod/noticeimpl.go create mode 100644 models/v/noticefmtmod/type.go create mode 100644 models/v/noticerecdmod/list.go create mode 100644 models/v/noticerecdmod/noticeRecord.go create mode 100644 models/v/noticerecdmod/trend.go create mode 100644 models/v/noticerecdmod/type.go create mode 100644 models/v/officialWebsitemod/album.go create mode 100644 models/v/officialWebsitemod/basic_data.go create mode 100644 models/v/officialWebsitemod/business.go create mode 100644 models/v/officialWebsitemod/common_methods.go create mode 100644 models/v/officialWebsitemod/common_types.go create mode 100644 models/v/officialWebsitemod/hero.go create mode 100644 models/v/officialWebsitemod/init.go create mode 100644 models/v/officialWebsitemod/job.go create mode 100644 models/v/officialWebsitemod/news.go create mode 100644 models/v/officialWebsitemod/partner.go create mode 100644 models/v/officialWebsitemod/photograph.go create mode 100644 models/v/officialWebsitemod/recruit_form.go create mode 100644 models/v/officialWebsitemod/tag.go create mode 100644 models/v/officialWebsitemod/video.go create mode 100644 models/v/officialmod/app.go create mode 100644 models/v/officialmod/official.go create mode 100644 models/v/officialmod/type.go create mode 100644 models/v/officialmod/web.go create mode 100644 models/v/oncetaskmod/oncetask.go create mode 100644 models/v/oncetaskmod/web.go create mode 100644 models/v/operationlogmod/operationlog.go create mode 100644 models/v/operationlogmod/type.go create mode 100644 models/v/operationlogmod/web.go create mode 100644 models/v/payaudiobookmod/pay.go create mode 100644 models/v/payaudiobookmod/type.go create mode 100644 models/v/paymentguidemod/model.go create mode 100644 models/v/paymentguidemod/type.go create mode 100644 models/v/paymentguidemod/type_test.go create mode 100644 models/v/prdcthsomod/app.go create mode 100644 models/v/prdcthsomod/paymentLog.go create mode 100644 models/v/prdcthsomod/producthistory.go create mode 100644 models/v/prdcthsomod/type.go create mode 100644 models/v/prdcthsomod/vip_experiment.go create mode 100644 models/v/prize_record_mod/app.go create mode 100644 models/v/prize_record_mod/hanlder.go create mode 100644 models/v/prize_record_mod/type.go create mode 100644 models/v/prizemod/hanlder.go create mode 100644 models/v/prizemod/prize.go create mode 100644 models/v/prizemod/type.go create mode 100644 models/v/prizemod/web.go create mode 100644 models/v/productbenefitmod/productBenefit.go create mode 100644 models/v/productbenefitmod/type.go create mode 100644 models/v/productbenefitmod/web.go create mode 100644 models/v/productmod/app.go create mode 100644 models/v/productmod/product.go create mode 100644 models/v/productmod/type.go create mode 100644 models/v/productmod/web.go create mode 100644 models/v/productposimod/productposi.go create mode 100644 models/v/productposimod/type.go create mode 100644 models/v/productposimod/web.go create mode 100644 models/v/proxyincomemod/proxyincome.go create mode 100644 models/v/proxyincomemod/type.go create mode 100644 models/v/proxymod/app.go create mode 100644 models/v/proxymod/h5.go create mode 100644 models/v/proxymod/proxy.go create mode 100644 models/v/proxymod/type.go create mode 100644 models/v/proxymod/web.go create mode 100644 models/v/proxyrecordmod/record.go create mode 100644 models/v/proxyrecordmod/type.go create mode 100644 models/v/proxyrecordmod/web.go create mode 100644 models/v/pushmod/type.go create mode 100644 models/v/pushmod/vid.go create mode 100644 models/v/pushmod/web.go create mode 100644 models/v/questionnremod/questionnre.go create mode 100644 models/v/questionnremod/type.go create mode 100755 models/v/quicksearchmod/app.go create mode 100755 models/v/quicksearchmod/quicksearch.go create mode 100755 models/v/quicksearchmod/type.go create mode 100644 models/v/rchgamegoldmod/gold.go create mode 100644 models/v/rchgamegoldmod/type.go create mode 100644 models/v/rchgamegoldmod/web.go create mode 100644 models/v/rchgamtmod/gold.go create mode 100644 models/v/rchgamtmod/type.go create mode 100644 models/v/rchgamtmod/web.go create mode 100644 models/v/rchgchanmod/paychannel.go create mode 100644 models/v/rchgchanmod/type.go create mode 100644 models/v/rchgchanmod/web.go create mode 100644 models/v/rchgordmod/app.go create mode 100644 models/v/rchgordmod/discStat.go create mode 100644 models/v/rchgordmod/list.go create mode 100644 models/v/rchgordmod/rchgorder.go create mode 100644 models/v/rchgordmod/type.go create mode 100644 models/v/rchgordmod/vip_experiment.go create mode 100644 models/v/rchgordmod/web.go create mode 100644 models/v/recmdtag/app.go create mode 100644 models/v/recmdtag/error.go create mode 100644 models/v/recmdtag/list.go create mode 100644 models/v/recmdtag/m3u8sign.go create mode 100644 models/v/recmdtag/recmdtag.go create mode 100644 models/v/recmdtag/type.go create mode 100644 models/v/recommod/app.go create mode 100644 models/v/recommod/m3u8sign.go create mode 100644 models/v/recommod/reco.go create mode 100644 models/v/recommod/type.go create mode 100644 models/v/reptmod/list.go create mode 100644 models/v/reptmod/report.go create mode 100644 models/v/reptmod/type.go create mode 100644 models/v/repttypemod/repttype.go create mode 100644 models/v/repttypemod/type.go create mode 100644 models/v/rjctmpltmod/rjctmplt.go create mode 100644 models/v/rjctmpltmod/type.go create mode 100644 models/v/rjctmpltmod/web.go create mode 100644 models/v/rolemod/role.go create mode 100644 models/v/rolemod/type.go create mode 100644 models/v/rolemod/web.go create mode 100644 models/v/scenebannermod/model.go create mode 100644 models/v/scenebannermod/type.go create mode 100644 models/v/scenebannermod/type_test.go create mode 100644 models/v/sensitivewordmod/crud.go create mode 100644 models/v/sensitivewordmod/match.go create mode 100644 models/v/sensitivewordmod/type.go create mode 100644 models/v/sessionmod/app.go create mode 100644 models/v/sessionmod/session.go create mode 100644 models/v/sessionmod/type.go create mode 100644 models/v/settingmod/setting.go create mode 100644 models/v/settingmod/type.go create mode 100755 models/v/signrecordmod/app.go create mode 100755 models/v/signrecordmod/signrecord.go create mode 100755 models/v/signrecordmod/type.go create mode 100644 models/v/signrecordmod/web.go create mode 100644 models/v/signtaskmod/app.go create mode 100644 models/v/signtaskmod/config.go create mode 100644 models/v/signtaskmod/type.go create mode 100644 models/v/signtaskmod/web.go create mode 100644 models/v/sourcemod/source.go create mode 100644 models/v/sourcemod/source_cache_test.go create mode 100644 models/v/sourcemod/type.go create mode 100644 models/v/sysconfmod/free_trial_config_test.go create mode 100644 models/v/sysconfmod/payment_guide_config_test.go create mode 100644 models/v/sysconfmod/short_drama_card_test.go create mode 100755 models/v/sysconfmod/sysconf.go create mode 100644 models/v/sysconfmod/trans.go create mode 100755 models/v/sysconfmod/type.go create mode 100755 models/v/sysconfmod/var.go create mode 100644 models/v/systemmod/base.go create mode 100644 models/v/systemmod/config.go create mode 100644 models/v/systemmod/type.go create mode 100644 models/v/tagconfmod/tagconf.go create mode 100644 models/v/tagconfmod/type.go create mode 100644 models/v/tagmod/app.go create mode 100644 models/v/tagmod/es.go create mode 100644 models/v/tagmod/tag.go create mode 100644 models/v/tagmod/type.go create mode 100644 models/v/tagmod/web.go create mode 100644 models/v/taskmod/app.go create mode 100644 models/v/taskmod/config.go create mode 100644 models/v/taskmod/type.go create mode 100644 models/v/taskmod/web.go create mode 100644 models/v/taskrecordmod/app.go create mode 100644 models/v/taskrecordmod/taskRecord.go create mode 100644 models/v/taskrecordmod/type.go create mode 100644 models/v/taskrecordmod/web.go create mode 100644 models/v/tonemod/list.go create mode 100644 models/v/tonemod/tone.go create mode 100644 models/v/tonemod/toneValue.go create mode 100644 models/v/tonemod/type.go create mode 100644 models/v/tonerecomod/toneRecom.go create mode 100644 models/v/tonerecomod/type.go create mode 100644 models/v/tonerecomod/web.go create mode 100644 models/v/txnactmod/app.go create mode 100644 models/v/txnactmod/txnact.go create mode 100644 models/v/txnactmod/type.go create mode 100644 models/v/txnactmod/web.go create mode 100644 models/v/txnmod/discStat.go create mode 100644 models/v/txnmod/fund_ledger.go create mode 100644 models/v/txnmod/list.go create mode 100644 models/v/txnmod/transaction.go create mode 100644 models/v/txnmod/type.go create mode 100644 models/v/txnmod/web.go create mode 100644 models/v/userResourcemod/type.go create mode 100644 models/v/userResourcemod/userResource.go create mode 100644 models/v/userResourcemod/web.go create mode 100644 models/v/useractmod/type.go create mode 100644 models/v/useractmod/useract.go create mode 100644 models/v/usercheckinmod/type.go create mode 100644 models/v/usercheckinmod/usercheckin.go create mode 100644 models/v/usermod/app.go create mode 100644 models/v/usermod/discStat.go create mode 100644 models/v/usermod/drama_expire_test.go create mode 100644 models/v/usermod/error.go create mode 100644 models/v/usermod/es.go create mode 100644 models/v/usermod/list.go create mode 100644 models/v/usermod/payment_status_popup_test.go create mode 100644 models/v/usermod/type.go create mode 100644 models/v/usermod/user.go create mode 100644 models/v/usermod/vip_renew.go create mode 100644 models/v/usermod/vip_renew_test.go create mode 100644 models/v/usermod/web.go create mode 100644 models/v/userrecomod/reco.go create mode 100644 models/v/userrecomod/type.go create mode 100644 models/v/userrecomod/web.go create mode 100644 models/v/usertagmod/app.go create mode 100644 models/v/usertagmod/type.go create mode 100644 models/v/usertagmod/userTag.go create mode 100644 models/v/userwatchrecordmod/app.go create mode 100644 models/v/userwatchrecordmod/app_test.go create mode 100644 models/v/userwatchrecordmod/type.go create mode 100644 models/v/userwatchrecordmod/user_watch_record.go create mode 100644 models/v/verifyreportmod/type.go create mode 100644 models/v/verifyreportmod/verifyreport.go create mode 100644 models/v/versionmod/version.go create mode 100644 models/v/videoactivitymod/app.go create mode 100644 models/v/videoactivitymod/type.go create mode 100644 models/v/videoactivitymod/video_activity.go create mode 100644 models/v/videocoupon/init.go create mode 100644 models/v/videocoupon/type.go create mode 100644 models/v/videocoupon/videocoupon.go create mode 100644 models/v/videodiscountmod/type.go create mode 100644 models/v/videodiscountmod/videodiscount.go create mode 100644 models/v/videogoldcoinmod/app.go create mode 100644 models/v/videogoldcoinmod/gold_coin_video.go create mode 100644 models/v/videogoldcoinmod/type.go create mode 100644 models/v/videogoldcoinmod/web.go create mode 100644 models/v/vidmod/app.go create mode 100644 models/v/vidmod/app_video.go create mode 100644 models/v/vidmod/cache.go create mode 100644 models/v/vidmod/es.go create mode 100644 models/v/vidmod/free_trial_response_test.go create mode 100644 models/v/vidmod/hevc.go create mode 100644 models/v/vidmod/hevc_test.go create mode 100644 models/v/vidmod/liaoba_top_sort_test.go create mode 100644 models/v/vidmod/list.go create mode 100644 models/v/vidmod/m3u8sign.go create mode 100644 models/v/vidmod/m3u8sign_test.go create mode 100644 models/v/vidmod/payment_guide.go create mode 100644 models/v/vidmod/payment_guide_test.go create mode 100644 models/v/vidmod/ranking.go create mode 100644 models/v/vidmod/recommend_batch_test.go create mode 100644 models/v/vidmod/recommend_test.go create mode 100644 models/v/vidmod/setDoc.go create mode 100644 models/v/vidmod/type.go create mode 100644 models/v/vidmod/update_marker.go create mode 100644 models/v/vidmod/vid.go create mode 100644 models/v/vidmod/watch_consume_test.go create mode 100644 models/v/vidmod/web.go create mode 100644 models/v/vidpopmod/type.go create mode 100644 models/v/vidpopmod/vidpop.go create mode 100644 models/v/vidtimeonlinemod/type.go create mode 100644 models/v/vidtimeonlinemod/vidtimeonline.go create mode 100644 models/v/vidtimeonlinemod/web.go create mode 100644 models/v/vipcardexperimentmod/model.go create mode 100644 models/v/vipcardexperimentmod/product_ids.go create mode 100644 models/v/vipcardexperimentmod/type.go create mode 100644 models/v/vipcardexperimentmod/type_test.go create mode 100644 models/v/vipconfigmod/type.go create mode 100644 models/v/vipconfigmod/vipconfig.go create mode 100644 models/v/walletmod/app.go create mode 100644 models/v/walletmod/type.go create mode 100644 models/v/walletmod/wallet.go create mode 100644 models/v/wdchannmod/app.go create mode 100644 models/v/wdchannmod/channel.go create mode 100644 models/v/wdchannmod/list.go create mode 100644 models/v/wdchannmod/type.go create mode 100644 models/v/wdchannmod/web.go create mode 100644 models/v/wdordmod/app.go create mode 100644 models/v/wdordmod/discStat.go create mode 100644 models/v/wdordmod/error.go create mode 100644 models/v/wdordmod/list.go create mode 100644 models/v/wdordmod/type.go create mode 100644 models/v/wdordmod/web.go create mode 100644 models/v/wdordmod/withdraw.go create mode 100644 models/v/wdtaxmod/type.go create mode 100644 models/v/wdtaxmod/web.go create mode 100644 models/v/wdtaxmod/withdrawtariff.go create mode 100644 project.md create mode 100644 script/createAuthEnum.js create mode 100644 script/createAuthority.js create mode 100644 script/golang/gen_qr_code/main.go create mode 100644 script/golang/seed_adver_abtest/main.go create mode 100755 scripts/repair_test_drama_tags.py create mode 100644 scripts/seed_checkin/main.go create mode 100644 scripts/seed_checkin_setup/main.go create mode 100644 services/srv_im/ad_notify.go create mode 100644 services/srv_im/im.go create mode 100644 skd/job/cache/rankcache/run.go create mode 100644 skd/job/cache/tonecache/run.go create mode 100644 skd/job/check/daichongcallback.go create mode 100644 skd/job/checkin/checkin.go create mode 100644 skd/job/dailyAdverCalc/job.go create mode 100644 skd/job/dailyAdverCalc/job_test.go create mode 100644 skd/job/dataclean/dataclean.go create mode 100644 skd/job/dataclean/method.go create mode 100644 skd/job/elastic/esaudiobook/method.go create mode 100644 skd/job/elastic/esaudiobook/sync.go create mode 100644 skd/job/elastic/eskeyword/method.go create mode 100644 skd/job/elastic/eskeyword/sync.go create mode 100644 skd/job/elastic/esmedia/method.go create mode 100644 skd/job/elastic/esmedia/method_test.go create mode 100644 skd/job/elastic/esmedia/sync.go create mode 100644 skd/job/elastic/essearchlog/method.go create mode 100644 skd/job/elastic/essearchlog/sync.go create mode 100644 skd/job/elastic/estag/method.go create mode 100644 skd/job/elastic/estag/sync.go create mode 100644 skd/job/elastic/esuser/method.go create mode 100644 skd/job/elastic/esuser/sync.go create mode 100644 skd/job/elastic/esvideo/method.go create mode 100644 skd/job/elastic/esvideo/sync.go create mode 100644 skd/job/elastic/rdvideo/method.go create mode 100644 skd/job/elastic/rdvideo/sync.go create mode 100644 skd/job/hevc_transcode.go create mode 100644 skd/job/hevc_transcode_test.go create mode 100644 skd/job/hotvalue/video.go create mode 100644 skd/job/im_ad_notify.go create mode 100644 skd/job/job.go create mode 100644 skd/job/schedule.go create mode 100644 skd/job/shortrecommend/job.go create mode 100644 skd/job/shortrecommend/job_test.go create mode 100644 skd/job/sqlcollapser/shordvideo/method.go create mode 100644 skd/job/sqlcollapser/shordvideo/sync.go create mode 100644 skd/job/sqlcollapser/user/method.go create mode 100644 skd/job/sqlcollapser/user/runSync.go create mode 100644 skd/job/sqlcollapser/vid/method.go create mode 100644 skd/job/sqlcollapser/vid/runSync.go create mode 100644 skd/job/sqlcollasper.go create mode 100644 skd/job/stat.go create mode 100644 skd/job/stat/keyword/method.go create mode 100644 skd/job/stat/keyword/stat.go create mode 100644 skd/job/stat/kwrank/method.go create mode 100644 skd/job/stat/kwrank/stat.go create mode 100644 skd/job/stat/rank/stat.go create mode 100644 skd/job/stat/rechorder/method.go create mode 100644 skd/job/stat/rechorder/stat.go create mode 100644 skd/job/stat/statuser/method.go create mode 100644 skd/job/stat/statuser/stat.go create mode 100644 skd/job/stat/tagplay/method.go create mode 100644 skd/job/stat/tagplay/stat.go create mode 100644 skd/job/stat/video/method.go create mode 100644 skd/job/stat/video/stat.go create mode 100644 skd/job/stat/vidtotal/method.go create mode 100644 skd/job/stat/vidtotal/stat.go create mode 100644 skd/job/stat/withorder/method.go create mode 100644 skd/job/stat/withorder/stat.go create mode 100644 skd/job/sync.go create mode 100644 skd/job/sync_im_users.go create mode 100644 skd/job/syncspiderdata/audiobookspider/method.go create mode 100644 skd/job/syncspiderdata/audiobookspider/sync.go create mode 100644 skd/job/syncspiderdata/fictionspider/method.go create mode 100644 skd/job/syncspiderdata/fictionspider/sync.go create mode 100644 skd/job/user/privilege.go create mode 100644 skd/main.go create mode 100644 skd/once/main.go create mode 100644 skd/once/syncAudioBook.go create mode 100644 skd/once/syncFiction.go create mode 100644 skd/once/syncVideoInfo.go create mode 100644 skd/service/chatrobot.go create mode 100644 skd/service/contentreviewser/acg.go create mode 100644 skd/service/contentreviewser/issue_builder.go create mode 100644 skd/service/contentreviewser/matcher.go create mode 100644 skd/service/contentreviewser/runner.go create mode 100644 skd/service/contentreviewser/scanner.go create mode 100644 skd/service/contentreviewser/video.go create mode 100644 skd/service/doregister.go create mode 100644 skd/service/export_task/advance_order_export.go create mode 100644 skd/service/export_task/base.go create mode 100644 skd/service/export_task/product_history_export.go create mode 100644 skd/service/export_task/user_export.go create mode 100644 skd/service/export_task/video_export.go create mode 100644 skd/service/fakeVideoInfo.go create mode 100644 skd/service/init.go create mode 100644 skd/service/promotionCode.go create mode 100644 skd/service/updownload.go create mode 100644 skd/service/versionser.go create mode 100644 skd/skdg/elastic.go create mode 100644 skd/skdg/redisg.go create mode 100644 skd/skdg/shortrecommend.go create mode 100644 skd/skdg/shortrecommend_test.go create mode 100644 skd/skdg/skdg.go create mode 100644 skd/skdg/static.go create mode 100644 sshpass.rb create mode 100644 swagger/main.go create mode 100644 tests/checkin/go.mod create mode 100644 tests/checkin/go.sum create mode 100644 tests/checkin/main.go create mode 100644 tests/checkin/reports/last_run.json create mode 100644 tests/checkin/reports/latest.html create mode 100644 tests/checkin/reports/report_20260507_212008.html create mode 100644 tests/checkin/reports/report_20260508_162923.html create mode 100644 tests/checkin/reports/report_20260508_163219.html create mode 100644 tests/checkin/reports/result_20260507_212008.json create mode 100644 tests/checkin/reports/result_20260508_162923.json create mode 100644 tests/checkin/reports/result_20260508_163219.json create mode 100644 tests/checkin/sandbox.go create mode 100644 tests/checkin/suite_checkin.go create mode 100644 web/api/acth5ctrl/lotnum.go create mode 100644 web/api/activityctrl/activity.go create mode 100644 web/api/activityctrl/prize.go create mode 100644 web/api/activityctrl/prize_pool.go create mode 100644 web/api/activityctrl/user_record.go create mode 100644 web/api/actvctrl/actv.go create mode 100644 web/api/addwlefarectrl/addwelfare.go create mode 100644 web/api/adminctrl/admin.go create mode 100644 web/api/adsctrl/ads.go create mode 100644 web/api/adv_group_ctrl/adv_group.go create mode 100644 web/api/advance_config_ctrl/advance_config.go create mode 100644 web/api/advance_order_ctrl/advance_order.go create mode 100644 web/api/ai_changeface_ctrl/ai_changeface.go create mode 100755 web/api/ai_image_to_video_ctrl/ai_image_to_video.go create mode 100644 web/api/ai_mate_ctr/ai_mate.go create mode 100644 web/api/ai_mate_v2_ctrl/ai_mate.go create mode 100755 web/api/ai_template_module_ctrl/aitemplatemodule.go create mode 100755 web/api/ai_text_to_image_ctrl/aitexttoimage.go create mode 100755 web/api/ai_text_to_novel_ctrl/aitexttonovel.go create mode 100644 web/api/ai_undress_ctrl/ai_undress.go create mode 100755 web/api/aiplazactrl/aiplaza.go create mode 100644 web/api/annouctrl/annou.go create mode 100644 web/api/annouctrl/announce.go create mode 100644 web/api/audiobookctrl/audiobook.go create mode 100644 web/api/authorctrl/author.go create mode 100644 web/api/backpack_ctrl/backpack.go create mode 100755 web/api/bannerjumpctrl/bannerjump.go create mode 100644 web/api/chat_message_ctrl/chat_message.go create mode 100644 web/api/chatrobotctrl/chatrobotctrl.go create mode 100644 web/api/checkinctrl/checkin.go create mode 100644 web/api/commentAgentctrl/commentAgent.go create mode 100644 web/api/commentctrl/commentCtrl.go create mode 100644 web/api/contentlibctrl/contentlibctrl.go create mode 100644 web/api/contentreviewctrl/contentreviewctrl.go create mode 100644 web/api/currencyctrl/currency.go create mode 100644 web/api/daichongctrl/daichong.go create mode 100644 web/api/daichongctrl/order.go create mode 100644 web/api/dramatopicctrl/topic.go create mode 100644 web/api/errfeedbackctrl/errfeedback.go create mode 100644 web/api/exchcodectrl/exchcodectrl.go create mode 100644 web/api/exchcodectrl/exchlogctrl.go create mode 100644 web/api/exportctrl/export.go create mode 100644 web/api/extctrl/extctrl.go create mode 100644 web/api/fictionctrl/fiction.go create mode 100644 web/api/filterctrl/filter.go create mode 100644 web/api/freevidcfgctrl/freevidcfg.go create mode 100644 web/api/goldcfgctrl/goldcfg.go create mode 100644 web/api/imagetopctrl/imagetop.go create mode 100755 web/api/imgroupctrl/imgroup.go create mode 100755 web/api/imgroupmemberctrl/imgroupmember.go create mode 100755 web/api/immessagectrl/immessage.go create mode 100644 web/api/infmtctrl/infmt.go create mode 100644 web/api/integral_config_ctrl/integral_config.go create mode 100644 web/api/integral_exchange_ctrl/integarl_exchange.go create mode 100644 web/api/ipblockctrl/ipblock.go create mode 100644 web/api/ipwhitectrl/ipwhite.go create mode 100644 web/api/jingangctrl/jingangcttrl.go create mode 100644 web/api/laosijictrl/comics.go create mode 100644 web/api/laosijictrl/movie.go create mode 100644 web/api/laosijictrl/novel.go create mode 100644 web/api/laosijictrl/post.go create mode 100644 web/api/ldyctrl/ldyctrl.go create mode 100644 web/api/locationctrl/city.go create mode 100644 web/api/locationctrl/hotCity.go create mode 100644 web/api/logctrl/log.go create mode 100644 web/api/media_resource/media_resource.go create mode 100755 web/api/mediacontentctrl/mediacontent.go create mode 100755 web/api/mediactrl/media.go create mode 100755 web/api/mediatagctrl/mediatag.go create mode 100755 web/api/mediatagdimensionctrl/mediatagdimension.go create mode 100644 web/api/modulectrl/moduleconf.go create mode 100644 web/api/modulevideoctrl/modulevideo.go create mode 100755 web/api/nakedchatctrl/nakedchat.go create mode 100755 web/api/nakedchatorderctrl/nakedchatorder.go create mode 100644 web/api/newactivityctrl/activity.go create mode 100644 web/api/officialWebsitectrl/album.go create mode 100644 web/api/officialWebsitectrl/basic.go create mode 100644 web/api/officialWebsitectrl/business.go create mode 100644 web/api/officialWebsitectrl/hero.go create mode 100644 web/api/officialWebsitectrl/job.go create mode 100644 web/api/officialWebsitectrl/news.go create mode 100644 web/api/officialWebsitectrl/partner.go create mode 100644 web/api/officialWebsitectrl/photograph.go create mode 100644 web/api/officialWebsitectrl/recruit_form.go create mode 100644 web/api/officialWebsitectrl/tag.go create mode 100644 web/api/officialWebsitectrl/video.go create mode 100644 web/api/officialWebsitectrl/video_ext.go create mode 100644 web/api/officialctrl/officialCtrl.go create mode 100644 web/api/operationlogctrl/operationlog.go create mode 100644 web/api/paymentguidectrl/payment_guide.go create mode 100644 web/api/productBenefitctrl/productbenefit.go create mode 100644 web/api/productctrl/productLog.go create mode 100644 web/api/productctrl/productposition.go create mode 100644 web/api/productctrl/vip.go create mode 100644 web/api/productctrl/vipConfig.go create mode 100644 web/api/promoteurlctrl/promoteurlctrl.go create mode 100644 web/api/proxyctrl/proxy.go create mode 100755 web/api/quicksearchctrl/quicksearch.go create mode 100644 web/api/rechargectrl/DaBaiSha.go create mode 100644 web/api/rechargectrl/dxzhifu.go create mode 100644 web/api/rechargectrl/gameGold.go create mode 100644 web/api/rechargectrl/gold.go create mode 100644 web/api/rechargectrl/goldfishplus.go create mode 100644 web/api/rechargectrl/izhifu.go create mode 100644 web/api/rechargectrl/leihuo.go create mode 100644 web/api/rechargectrl/lucky.go create mode 100644 web/api/rechargectrl/payChannel.go create mode 100644 web/api/rechargectrl/rchgorder.go create mode 100644 web/api/rechargectrl/shark.go create mode 100644 web/api/rechargectrl/yilianfu.go create mode 100644 web/api/rechargectrl/zhongfu.go create mode 100644 web/api/rolectrl/freevidcfg.go create mode 100644 web/api/scenebannerctrl/banner.go create mode 100644 web/api/sensitivewordctrl/sensitivewordctrl.go create mode 100644 web/api/signctrl/config.go create mode 100644 web/api/signctrl/daily.go create mode 100644 web/api/signctrl/once.go create mode 100644 web/api/signctrl/sign.go create mode 100644 web/api/sourcectrl/source.go create mode 100644 web/api/statcenterctl/statsync.go create mode 100644 web/api/statctrl/orderCtrl.go create mode 100644 web/api/statctrl/tarnsCtrl.go create mode 100644 web/api/statctrl/userCtrl.go create mode 100644 web/api/statctrl/videoCtrl.go create mode 100644 web/api/synccdnctrl/syncCtrl.go create mode 100755 web/api/sysconfctrl/sysconf.go create mode 100644 web/api/tagctrl/recmdCtrl.go create mode 100644 web/api/tagctrl/tagCtrl.go create mode 100644 web/api/tianyuctrl/http/http.go create mode 100644 web/api/tianyuctrl/http/http_test.go create mode 100644 web/api/tianyuctrl/media.go create mode 100644 web/api/tianyuctrl/proto.go create mode 100644 web/api/tianyuctrl/proxy/object.go create mode 100644 web/api/tianyuctrl/proxy/object_test.go create mode 100644 web/api/tianyuctrl/proxy/types.go create mode 100644 web/api/tianyuctrl/proxy/utils.go create mode 100644 web/api/tonectrl/tonectrl.go create mode 100644 web/api/updownloadctrl/updownloadCtrl.go create mode 100644 web/api/userctrl/faq.go create mode 100644 web/api/userctrl/feedback.go create mode 100644 web/api/userctrl/login.go create mode 100644 web/api/userctrl/play.go create mode 100644 web/api/userctrl/reco.go create mode 100644 web/api/userctrl/report.go create mode 100644 web/api/userctrl/user.go create mode 100644 web/api/versionctrl/version.go create mode 100644 web/api/vidctrl/discount_area.go create mode 100644 web/api/vidctrl/mediaresource.go create mode 100644 web/api/vidctrl/rejecttemplate.go create mode 100644 web/api/vidctrl/vid.go create mode 100644 web/api/video_activity_ctrl/video_activity.go create mode 100644 web/api/video_gold_coin_ctrl/video_gold_coin.go create mode 100644 web/api/vipcardexperimentctrl/experiment.go create mode 100644 web/api/web_im/im_sync.go create mode 100644 web/api/withdrawctrl/goldfish.go create mode 100644 web/api/withdrawctrl/txnact.go create mode 100644 web/api/withdrawctrl/withdraw.go create mode 100644 web/api/withdrawctrl/withdrawCfg.go create mode 100644 web/api/withdrawctrl/yinse.go create mode 100644 web/main.go create mode 100644 web/middleware/authweb/authweb.go create mode 100644 web/middleware/checkPermission/checkPermission.go create mode 100644 web/middleware/limitHandler/limit.go create mode 100644 web/middleware/limitHandler/login.go create mode 100644 web/proto/ai.go create mode 100644 web/router/activity.go create mode 100644 web/router/actv.go create mode 100644 web/router/addWare.go create mode 100644 web/router/adgroup.go create mode 100644 web/router/admin.go create mode 100644 web/router/ads.go create mode 100644 web/router/advance_config.go create mode 100644 web/router/advance_order.go create mode 100644 web/router/ai_changeface.go create mode 100644 web/router/ai_mate.go create mode 100644 web/router/ai_mate_v2.go create mode 100644 web/router/ai_mod.go create mode 100755 web/router/ai_text_to_novel.go create mode 100644 web/router/ai_undress.go create mode 100755 web/router/aiimagetovideo.go create mode 100755 web/router/aiplaza.go create mode 100755 web/router/aitemplatemodule.go create mode 100755 web/router/aitexttoimage.go create mode 100644 web/router/annou.go create mode 100644 web/router/author.go create mode 100644 web/router/backpack.go create mode 100755 web/router/bannerjump.go create mode 100644 web/router/base.go create mode 100644 web/router/chatrobot.go create mode 100644 web/router/checkin.go create mode 100644 web/router/comment.go create mode 100644 web/router/commentAgent.go create mode 100644 web/router/content_review.go create mode 100644 web/router/currency.go create mode 100644 web/router/daichong.go create mode 100644 web/router/defray.go create mode 100644 web/router/exchcode.go create mode 100644 web/router/export.go create mode 100644 web/router/extend.go create mode 100644 web/router/fiction.go create mode 100644 web/router/filter.go create mode 100644 web/router/freevidcfg.go create mode 100644 web/router/goldcfg.go create mode 100644 web/router/im.go create mode 100644 web/router/imagetop.go create mode 100755 web/router/imgroup.go create mode 100755 web/router/imgroupmember.go create mode 100755 web/router/immessage.go create mode 100644 web/router/infmt.go create mode 100644 web/router/integralConfig.go create mode 100644 web/router/integralExchange.go create mode 100644 web/router/ipblock.go create mode 100644 web/router/ipwhite.go create mode 100644 web/router/jingang.go create mode 100644 web/router/laosiji.go create mode 100644 web/router/ldycfg.go create mode 100644 web/router/location.go create mode 100644 web/router/log.go create mode 100755 web/router/media.go create mode 100755 web/router/mediacontent.go create mode 100755 web/router/mediatag.go create mode 100755 web/router/mediatagdimension.go create mode 100644 web/router/module.go create mode 100755 web/router/nakedchat.go create mode 100755 web/router/nakedchatorder.go create mode 100644 web/router/newActivity.go create mode 100644 web/router/official.go create mode 100644 web/router/officialWebsite.go create mode 100644 web/router/operationlog.go create mode 100644 web/router/payment_guide.go create mode 100644 web/router/product.go create mode 100644 web/router/productBenefit.go create mode 100644 web/router/promoteurl.go create mode 100644 web/router/proxy.go create mode 100755 web/router/quicksearch.go create mode 100644 web/router/recharge.go create mode 100644 web/router/rejectTemp.go create mode 100644 web/router/scene_banner.go create mode 100644 web/router/sensitive_word.go create mode 100644 web/router/sign.go create mode 100644 web/router/source.go create mode 100644 web/router/stat.go create mode 100644 web/router/sync.go create mode 100644 web/router/synccdn.go create mode 100755 web/router/sysconf.go create mode 100644 web/router/tag.go create mode 100644 web/router/tone.go create mode 100644 web/router/updownLoad.go create mode 100644 web/router/user.go create mode 100644 web/router/version.go create mode 100644 web/router/vid.go create mode 100644 web/router/video_activity.go create mode 100644 web/router/video_gold_coin.go create mode 100644 web/router/vip_card_experiment.go create mode 100644 web/router/withdraw.go create mode 100644 web/service/activityser/activity.go create mode 100644 web/service/activityser/cron.go create mode 100644 web/service/activityser/prize.go create mode 100644 web/service/activityser/prize_pool.go create mode 100644 web/service/activityser/user_record.go create mode 100644 web/service/actvser/actv.go create mode 100644 web/service/actvser/type.go create mode 100644 web/service/addwelfareser/addwelfare.go create mode 100644 web/service/adminser/web.go create mode 100644 web/service/adser/ads.go create mode 100644 web/service/adser/error.go create mode 100644 web/service/adser/review.go create mode 100644 web/service/advance_config_ser/advance_config.go create mode 100644 web/service/advance_order_ser/advance_order.go create mode 100644 web/service/advgroupser/advgroupser.go create mode 100644 web/service/advgroupser/advgroupser_test.go create mode 100644 web/service/ai_changeface_img_ser/ai_changface_img.go create mode 100644 web/service/ai_changeface_ser/ai_changeface.go create mode 100644 web/service/ai_changeface_ser/scene.go create mode 100755 web/service/ai_image_to_video_ser/ai_image_to_video.go create mode 100644 web/service/ai_mate_ser/ai_mate.go create mode 100644 web/service/ai_mate_v2_ser/ai_mate.go create mode 100755 web/service/ai_template_module_ser/aitemplatemodule.go create mode 100755 web/service/ai_text_to_image_ser/aitexttoimage.go create mode 100755 web/service/ai_text_to_novel_ser/aitexttonovel.go create mode 100644 web/service/ai_undress_service/ai_undress.go create mode 100755 web/service/aiplazaser/aiplaza.go create mode 100644 web/service/annouser/annou.go create mode 100644 web/service/annouser/announce.go create mode 100644 web/service/authorser/author.go create mode 100644 web/service/backpack_service/backpack.go create mode 100755 web/service/bannerjumpser/bannerjump.go create mode 100644 web/service/certificateser/certificate.go create mode 100644 web/service/chat_message_ser/chat_message.go create mode 100644 web/service/chatrobotser/chatrobot.go create mode 100644 web/service/commentser/comment.go create mode 100644 web/service/commentser/commentAgent.go create mode 100644 web/service/contentlibser/contentlib.go create mode 100644 web/service/contentreviewser/applier.go create mode 100644 web/service/currencyser/currency.go create mode 100644 web/service/currencyser/type.go create mode 100644 web/service/daichongser/daichong.go create mode 100644 web/service/deduction/deduction.go create mode 100644 web/service/deduction/type.go create mode 100644 web/service/discount_area_ser/discountArea.go create mode 100644 web/service/dramatopicser/topic.go create mode 100644 web/service/exchcodeser/exchcode.go create mode 100644 web/service/exchcodeser/exchlog.go create mode 100644 web/service/exportser/export.go create mode 100644 web/service/ffser/ff.go create mode 100644 web/service/filterser/filter.go create mode 100644 web/service/imagetopser/imagetop.go create mode 100755 web/service/imgroupmemberser/imgroupmember.go create mode 100755 web/service/imgroupser/imgroup.go create mode 100755 web/service/immessageser/immessage.go create mode 100644 web/service/infmtser/notice.go create mode 100644 web/service/integarl_exchange_ser/integral_exchange.go create mode 100644 web/service/integralconfigser/integralconfig.go create mode 100644 web/service/jingangser/jingang.go create mode 100644 web/service/jingangser/type.go create mode 100644 web/service/laosijiser/comics.go create mode 100644 web/service/laosijiser/media.go create mode 100644 web/service/laosijiser/novel.go create mode 100644 web/service/laosijiser/novel_test.go create mode 100644 web/service/laosijiser/post.go create mode 100644 web/service/laosijiser/video.go create mode 100644 web/service/laosijiser/video_test.go create mode 100644 web/service/locationser/city.go create mode 100644 web/service/logser/log.go create mode 100644 web/service/mediacontentser/drama_web_test.go create mode 100644 web/service/mediacontentser/mediacontent.go create mode 100644 web/service/mediaresourceser/media.go create mode 100644 web/service/mediaresourceser/media_test.go create mode 100644 web/service/mediaser/drama_web_test.go create mode 100755 web/service/mediaser/media.go create mode 100644 web/service/mediaser/sensitive_check.go create mode 100755 web/service/mediatagdimensionser/mediatagdimension.go create mode 100755 web/service/mediatagser/mediatag.go create mode 100644 web/service/moduleser/drama_module_test.go create mode 100644 web/service/moduleser/module.go create mode 100644 web/service/moduleser/type.go create mode 100755 web/service/nakedchatorderser/nakedchatorder.go create mode 100755 web/service/nakedchatser/nakedchat.go create mode 100644 web/service/officialWebsiteser/album.go create mode 100644 web/service/officialWebsiteser/backfill.go create mode 100644 web/service/officialWebsiteser/basic.go create mode 100644 web/service/officialWebsiteser/business.go create mode 100644 web/service/officialWebsiteser/helper.go create mode 100644 web/service/officialWebsiteser/hero.go create mode 100644 web/service/officialWebsiteser/job.go create mode 100644 web/service/officialWebsiteser/news.go create mode 100644 web/service/officialWebsiteser/partner.go create mode 100644 web/service/officialWebsiteser/photograph.go create mode 100644 web/service/officialWebsiteser/recruit_form.go create mode 100644 web/service/officialWebsiteser/slug.go create mode 100644 web/service/officialWebsiteser/tag.go create mode 100644 web/service/officialWebsiteser/video.go create mode 100644 web/service/officialser/official.go create mode 100644 web/service/operationlogser/operationlog.go create mode 100644 web/service/operationlogser/type.go create mode 100644 web/service/paymentguideser/payment_guide.go create mode 100644 web/service/paymentguideser/payment_guide_test.go create mode 100644 web/service/payvidlgser/payvidlg.go create mode 100644 web/service/payvidlgser/type.go create mode 100644 web/service/prefetch/prefetch.go create mode 100644 web/service/productBenefitser/productBenefit.go create mode 100644 web/service/productser/productHistory.go create mode 100644 web/service/proxyser/proxy.go create mode 100755 web/service/quicksearchser/quicksearch.go create mode 100644 web/service/rechargeser/page.go create mode 100644 web/service/rechargeser/page_attribution_test.go create mode 100644 web/service/rechargeser/recharge.go create mode 100644 web/service/smsser/smsser.go create mode 100644 web/service/statser/daily.go create mode 100644 web/service/statser/order.go create mode 100644 web/service/sysconfser/short_drama_card_test.go create mode 100755 web/service/sysconfser/sysconf.go create mode 100644 web/service/tagser/recmd.go create mode 100644 web/service/tagser/tag.go create mode 100644 web/service/tagser/type.go create mode 100644 web/service/taskser/config.go create mode 100644 web/service/taskser/daily.go create mode 100644 web/service/taskser/once.go create mode 100644 web/service/taskser/sign.go create mode 100644 web/service/updownloadser/updownload.go create mode 100644 web/service/userser/drama_expire_test.go create mode 100644 web/service/userser/faq.go create mode 100644 web/service/userser/login.go create mode 100644 web/service/userser/report.go create mode 100644 web/service/userser/user.go create mode 100644 web/service/video_activity_service/type.go create mode 100644 web/service/video_activity_service/video_activity.go create mode 100644 web/service/video_gold_coin_ser/video_gold_coin.go create mode 100644 web/service/video_media_service/http.go create mode 100644 web/service/video_media_service/type.go create mode 100644 web/service/vidser/liaoba_top_sort_test.go create mode 100644 web/service/vidser/mediaresouce.go create mode 100644 web/service/vidser/scheduled_module_test.go create mode 100644 web/service/vidser/sensitive_check.go create mode 100644 web/service/vidser/tag_search_visibility.go create mode 100644 web/service/vidser/tag_search_visibility_test.go create mode 100644 web/service/vidser/type.go create mode 100644 web/service/vidser/vid.go create mode 100644 web/service/vipcardexperimentser/service.go create mode 100644 web/service/vipcardexperimentser/service_test.go create mode 100644 web/service/withdrawser/bankcode.go create mode 100644 web/service/withdrawser/refund.go create mode 100644 web/service/withdrawser/wtihdraw.go create mode 100644 web/vidhelp/redis.go create mode 100644 web/vidhelp/vid.go create mode 100644 web/webg/ai_callback.go create mode 100644 web/webg/elastic.go create mode 100644 web/webg/redis.go create mode 100644 web/webg/webg.go create mode 100644 亚模块红点AI女友接口.md create mode 100644 付费引导Ping下发接口文档.md create mode 100644 会员内容上新弹窗接口.md create mode 100644 短视频推荐接口.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae83f3e --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +logs +.DS_Store +.idea +.vscode +default.etcd + +/app/*.tar.gz +/app/91porn-app-* +/app/config/app.yaml +/app/main +/app/config/* +/web/91porn-web-* +.log +video-app-* +/skd/91porn-skd-* +/skd/*.tar.gz +/app/app +/web/web +/app/config/app.json +/web/config/web.json +/skd/skd +/skd/config/skd.json +/skd/once/once +/skd/once/91porn-once-server +/skd/once/91porn-once-server.tar.gz +/app/__debug_bin +/web/__debug_bin +/schedule/__debug_bin + +/web/*.tar.gz + +/docs +/web/docs +/app/docs +/app/*.txt +/web/*.txt +/skd/*.txt +/reco/*.txt +/reco/config/*.json +/reco/91porn-reco-* +/reco/*.tar.gz +/go-shell/video/main.go +/go-shell/channel/main.go +/go-shell/detail/main.go +/go-shell/channStat/main.go +/go-shell/userStat/main.go +/go-shell/loseUserStat/main.go +web_test.sh +/web/.air.toml +/app/.air.toml +/script/*.mongodb +/tmp +/web/tmp +/app/tmp +/vendor/ +/test/ +/skd/temp +/skd/config/ +/web/config/ +/app/config/ +*.sh +*/router/router_test +*.test \ No newline at end of file diff --git a/91迭代后端技术方案.md b/91迭代后端技术方案.md new file mode 100644 index 0000000..dbcdc42 --- /dev/null +++ b/91迭代后端技术方案.md @@ -0,0 +1,231 @@ +# 91迭代后端技术方案 + +适用分支:`feat/91-iteration` + +本文覆盖本次迭代的后端整体改造。短视频环形队列的底层细节另见 +`SHORT_VIDEO_RECOMMENDATION_BACKEND_TECH_DESIGN.md`,前端对接以 `短视频推荐接口.md` 为准。 + +## 1. 总体架构 + +本次不新增独立服务、Kafka Topic、MQ、Redis实例、端口或操作系统crontab,继续使用现有三个发布单元: + +- `app`:App/H5接口、推荐读取、观看次数、付费引导、AI女友等。 +- `web`:亚模块、付费引导、评论Banner、VIP A/B等管理接口。 +- `skd`:短视频推荐分刷新与全局队列构建。 + +数据存储沿用MongoDB与Redis。观看次数消费、AI女友上下分和VIP实验发布使用MongoDB事务,生产MongoDB必须支持事务。 + +## 2. 短视频全局推荐 + +### 2.1 推荐分 + +视频增加单调递增的真实互动计数和每日计算分: + +```text +recommendScore = recommendLikeCount + + recommendCollectCount * 2 + + recommendCommentCount * 3 + + recommendShareCount * 5 +``` + +真实互动成功新增时进入累计:点赞、收藏每次从未激活变为激活时增加, +评论每次成功发布时增加;取消或删除不回退,不计入 `fake*` 数据。 +分享按用户+视频+北京自然日去重,可选 `eventId` 用于跨日重试幂等。 + +SKD使用Mongo游标固定高水位分批扫描,有界worker并发计算和BulkWrite。历史数据不依赖一次性推荐分脚本:刷新时对每项真实计数取 +`max(推荐累计, 历史真实计数, 0)`,并写入 `recommendInitialized=true`。生产首次写入量受 `maxInitializationWrites` 保护。 + +### 2.2 队列构建 + +候选仅包含当前审核通过、`newsType=SHORT`、未删除、`recoWeight!=-1`、未被亚模块配置排除的视频。 + +- 新视频:按审核通过时间计算24小时,从高分池排除。 +- 每块:严格17条高分+3条新视频,新视频不足时用高分补足。 +- 去重:同一队列中视频不重复,尾块不足20条保留实际数量。 +- 定时:SKD启动时及默认每10分钟做健康检查;当天队列健康时不重复生成,缺失或不健康才重建。 + +新队列通过临时building key分批Pipeline写入,验证完整后原子切换 `current`。版本为 `YYYYMMDD-`,切版后新的offset Hash天然使全体用户从起点开始。 + +### 2.3 请求侧环形消费 + +推荐接口不使用pageNumber计算服务端位置。每个用户的下一读取位置存在当前版本的Redis Hash中,队尾按取模回绕。 + +读取使用 `Reserve -> Mongo分批校验/补位 -> Commit` 协议: + +- Reserve阶段不立即推进offset,同一用户并发请求由15秒租约串行化。 +- 只按实际扫描前缀Commit,查询失败或取消时Abort。 +- 只有客户端显式传入、Trim后非空且不超过128字节的 `X-Request-ID` + 才对应commit receipt,解决“Redis已提交但HTTP响应丢失”的重试幂等; + 中间件自动生成并回传的request ID不参与此业务幂等。 +- Mongo按有界批次取数,扫描量受队列长度、`scanMultiplier`和 `maxBatches` 共同限制,避免失效视频导致无界扫描。 + +主接口 `/api/app/recommend/vid/list` 和兼容接口 `/api/app/vid/module/short/all` +复用同一队列服务。只有尚未成功Reserve、返回的 `QueueVersion` 为空且非请求取消类故障时, +才使用原随机Set降级。Reserve成功后的Mongo校验或Commit失败会Abort并返回错误, +不再切到随机集;请求取消或超时在两个入口中保留返回空成功结果的兼容分支。 + +## 3. 首页随机刷新与亚模块治理 + +`module_conf` 增加稳定能力字段,前端不再根据可编辑标题判断: + +- `haiJiaoStyle.sortRules[].refreshMode=RANDOM_TOP_N` +- `randomCandidateN=30` +- `onlineAt/offlineAt` +- `excludeLatest/excludeRecommend` +- `searchOnlyWhenInactive` + +历史 `val=2` 在没有新字段时由后端兼容归一为随机刷新。新刷新接口从热门候选前30条中随机返回,`refreshToken` 保证同一次重试顺序稳定。 + +亚模块元数据使用15秒进程内不可变快照和singleflight。管理后台变更后会清理共享缓存和当前进程快照; +其他App进程最多延迟约15秒生效。`excludeLatest` 和 `excludeRecommend` +分别只控制“最新”和“推荐”场景。只有当亚模块当前失效且 +`searchOnlyWhenInactive=true` 时,才拦截搜索以外的普通入口、分享和直接详情; +搜索结果签发10分钟、绑定UID和视频ID的 `searchAccessToken`,详情接口校验后放行。 + +## 4. 更新红点 + +`GET /api/app/content/update-markers` 聚合首页最新、当日最新及各亚模块最后内容时间。视频取最近审核时间,动漫/漫画同时考虑子集更新时间;“当日”按UTC+8自然日计算。 + +返回结果在Redis缓存30秒,前端保存本地已读时间并自行判断红点,后端不保存每用户红点状态。 + +## 5. AI女友V2与钱包 + +`POST /api/app/aimatev2/url` 在用户级Redis锁内: + +1. 先回收上一次第三方剩余金额。 +2. 读取主钱包,按 `10金币=1元` 转换并请求第三方授权URL。 +3. 授权URL成功后,在Mongo事务内扣减主钱包并写入 `fund_transfer_log`。 + +`GET /api/app/mine/wallet` 返回钱包前尝试将第三方余额下分回主钱包,保留不足1金币的换算余数供下次结算。 +`fund_transfer_log` 记录上下分类型、金额、操作后余额和余数,用于审计和辅助状态排查; +它没有业务幂等唯一键,第三方上下分与本地Mongo事务也不是跨系统原子操作。 + +## 6. 付费引导与会员内容上新 + +`payment_guide` 按场景、用户分层、排序、启用状态和有效期选择配置。过期会员、已退款且权益回收的用户按未付费分层处理。注册不足24小时的未付费用户始终属于新用户分层;免费次数是否用完只控制 `HOME_NEW_USER_FREE_TRIAL` 的 `show`,不得影响其他场景的新老用户分层。 + +`VIP_CONTENT_UPDATE` 动态查询审核通过的最新VIP视频,排除免费区、金币视频、不可推荐和业务配置排除的亚模块,按 `reviewAt desc, _id desc` 返回。数量由 `videoLimit` 控制,默认4,最大20。 + +视频ID顺序生成 `contentVersion`。展示回执按“UID+配置+场景+内容版本”幂等记录到 `payment_guide_impression`;新内容产生新版本后可再次提示。 + +## 7. 评论区Banner + +`scene_banner` 支持 `COMMENT_TOP` 场景的多张图片/GIF、内链/外链/无跳转、排序、启用和上下架时间。App端只返回当前有效数据,按 `sort desc, updatedAt desc` 轮播。历史每场景单条唯一索引需在模型初始化时删除,再创建查询索引。 + +## 8. VIP卡片A/B与下单归因 + +`vip_card_experiment` 保存A/B流量、套餐集合、默认套餐、角标文案、皮肤标识和 `uiConfig`。UID通过稳定哈希命中A或B,发布新实验时在事务内停用旧实验,唯一active-slot索引保证全局只有一个生效实验。 + +App套餐接口仅返回当前UID命中分组的: + +- `experimentId/variant/skinKey/defaultProductId` +- `uiConfig.backgroundImage`(会员中心整页背景) +- `uiConfig.badgeStyles`(角标背景色和文字色) +- 每个套餐的 `badgeType/badgeText` + +`vip_card_analytics_event` 使用唯一 `eventId` 幂等保存卡皮展示、套餐展示和无购买关闭事件,统计同时返回去重人数和次数。 + +下单增加 `sourcePage/sourceRef/videoId/activityId/experimentId/experimentVariant/sessionId`。`sourcePage` 允许任意非空值,Trim后最长128个Unicode字符,空值归一为 `UNKNOWN`。参与实验时,后端校验UID分组、套餐归属和会话ID,并在创建订单时固化归因,支付回调不重新分组。 + +## 9. 免费观看与试看角标 + +`GET /api/app/vid/user/count` 增加 `totalWatchCount`,值改为后端系统配置 `sys_conf` 的 `totalWatchCount`(`gpCode=common`)。新客户端通过 `POST /api/app/vid/play/consume` 消费次数: + +- 在事务内写当日观看记录并扣减用户剩余次数。 +- `consumeKey=UID+视频+自然日` 的唯一索引保证并发只扣1次。 +- 遇到Mongo短暂事务冲突时最多有界重试5次,提交后再清理用户缓存。 + +当前实现边界:消费接口会校验审核、VIP/作者、免费区、金币和当日已看, +但没有限制 `newsType=SP/SHORT`。因此其他审核通过且同样非金币、非免费区的视频也可能消费次数; +若产品要求与角标资格完全一致,上线前需补充类型校验。 + +视频列表/详情增加 `showFreeTrialBadge/freeTrialRemaining/canUseFreeTrial`。后端在一次列表组装中复用用户上下文,不逐视频查用户或配置。只有同时满足以下条件时显示: + +- `freeTrialBadgeEnabled=true` +- 已登录、非VIP、剩余次数大于0 +- 普通/短视频VIP内容,非金币、非免费区、非用户自己发布 + +## 10. 动漫/漫画最新时间 + +`media` 增加 `latestPublishedAt`。媒体首次/重新上架、批量激活或成功新增子集时同步更新; +普通媒体元数据更新不改该时间。亚模块“最新”排序为: + +```text +latestPublishedAt desc, +contentUpdateTime desc, +createdAt desc, +_id desc +``` + +查询会追加旧时间字段作为后续排序键,但不等价于针对每条记录做 +`$ifNull` 回退,新旧文档混排仍可能失序。因此上线时必须单独交付并执行 +`script/mongo/backfill_media_latest_published_at.js`;该脚本不依赖服务启动,本次与文档包一起迁移。 + +## 11. 视频置顶 + +现有视频编辑接口的 `liaoBaTopSort` 取消0~99限制,允许重复值;保存时仅更新当前视频,不交换其他视频。置顶列表按 `liaoBaTopSort desc, reviewAt desc, _id desc` 稳定排序。 + +## 12. 数据模型、索引与缓存 + +新增Mongo集合: + +- `fund_transfer_log` +- `payment_guide` +- `payment_guide_impression` +- `scene_banner` +- `vip_card_experiment` +- `vip_card_analytics_event` + +关键索引: + +- 推荐扫描:`newsType, status, deleteAt, _id`。Mongo读取和写入按有界批次执行, + worker数有上限;但所有合格候选的精简快照会留在内存中再排序并组装17+3队列, + 内存随候选量增长,上线前需用生产规模数据压测。 +- 免费消费:`user_act.consumeKey` 部分唯一索引。 +- 付费引导曝光:`uid, configId, scene, contentVersion` 唯一。 +- VIP实验:`experimentId` 唯一、active-slot部分唯一、`eventId` 唯一。 +- 订单归因:`sourcePage, createdAt` 复合索引,以及 + `experimentId, experimentVariant, productID, status` 复合索引。 +- AI资金日志:`uid, category, createdAt desc`。 +- 媒体最新:模块、状态、删除标记与 `latestPublishedAt/contentUpdateTime/createdAt/_id` 复合索引。 + +关键Redis键: + +```text +recommend:short:current +recommend:short:queue:{version} +recommend:short:offset:{version} +recommend:short:meta:{version} +recommend:short:build-lock +recommend:short:reservation:{version}:{uid} +recommend:short:commit-receipt:{uid}:{receiptId} +content:update-markers:v1 +redsync:ai-fund:{uid} +``` + +App与SKD的 `keyTTLHours` 必须一致。`queue/meta` 默认按发布时间+72小时过期, +`offset` 使用meta记录的同一绝对过期时间;`current` 不设TTL, +`build-lock` 固定10分钟,`reservation` 固定15秒,`commit-receipt` 固定30秒。 + +## 13. 发布与数据迁移 + +1. 核对 `91迭代配置改动点.md`,完成App、SKD运行配置和各环境业务配置。 +2. 在低峰发布 `web`,确认新集合/索引初始化成功,再发布 `app`。 +3. 统计未初始化推荐字段数量,根据生产量调整 `maxInitializationWrites`。 +4. 发布 `skd`。可先使 `skd.shortRecommend.enabled=true` 生成队列,保持 `app.shortRecommend.enabled=false` 验证健康度,再开启App全局读取。 +5. 单独执行媒体历史回填脚本;执行前后统计缺失数并抽样验证。 +6. 逐项配置亚模块、付费引导、评论Banner、VIP A/B和免费试看开关;测试库数据不会自动迁移到生产。 + +推荐开关是服务实例级全局开关,不是按UID白名单灰度。影子验证应通过“SKD开、App关”完成。 + +## 14. 自测与验收 + +- 推荐:17+3比例、新视频不足、环形回绕、每日切版、同UID并发、超时重试、失效视频补位、降级。 +- 亚模块:上下架边界、各入口排除、搜索token的UID/视频/过期校验、随机刷新重试稳定。 +- 付费引导:全部用户分层、仅展示一次、内容版本变化、`videoLimit` 0/4/20/越界。 +- VIP A/B:UID稳定分流、UI配置下发、事件去重、人/次统计、创建/支付/退款口径、下单归因。 +- 免费观看:同UID+视频并发只扣1次,VIP/发布者/免费区/金币/当日已看分支,角标开关和剩余次数实时失效。 +- AI女友:重复进入、重复返回钱包、余数处理、第三方失败、Mongo事务回滚和流水一致。 +- 媒体:存量回填、新上架、子集更新、相同时间稳定排序与索引explain。 + +上线前应在接近生产数量级的数据上验证推荐全量扫描时间、BulkWrite批次、Redis发布耗时、接口P95/P99、Mongo连接池和Redis内存占用。 diff --git a/91迭代配置改动点.md b/91迭代配置改动点.md new file mode 100644 index 0000000..eae8b29 --- /dev/null +++ b/91迭代配置改动点.md @@ -0,0 +1,173 @@ +# 91迭代配置改动点 + +适用分支:`feat/91-iteration` + +本文只记录环境配置、后台业务配置和上线数据处理。账号、密钥及各环境真实配置值不得写入本文。 + +## 一、App运行配置 + +文件:`config/app.json` + +新增根节点: + +```json +{ + "shortRecommend": { + "enabled": true, + "requestTimeoutMs": 3000, + "maxBatches": 5, + "scanMultiplier": 5, + "keyTTLHours": 72 + }, + "laoSiJiAiMate": { + "appId": "<按环境填写>", + "apiKey": "<按环境填写,不得写入Git或文档>", + "apiUrl": "<按环境填写>" + } +} +``` + +字段说明: + +| 字段 | 默认值/要求 | 说明 | +|---|---|---| +| `shortRecommend.enabled` | 未配置默认 `true` | 是否使用全局短视频环形队列 | +| `requestTimeoutMs` | 默认 `3000`,范围 `100~10000` | App读取推荐队列超时 | +| `maxBatches` | 默认 `5`,范围 `1~20` | 过滤失效视频时最多补取批次 | +| `scanMultiplier` | 默认 `5`,范围 `1~20` | 单批候选扫描倍数 | +| `keyTTLHours` | 默认 `72`,有效范围 `24~720` | 推荐队列、偏移等Redis键TTL | +| `laoSiJiAiMate.*` | 三项必须完整 | AI女友V2第三方配置,仅App使用 | + +`base.totalWatch` 不再作为免费观看次数来源,请从 app 配置移除。非VIP免费观看总次数改为通过后端系统配置 `sys_conf`(`gpCode=common`)中的 `totalWatchCount` 控制。 + +## 二、SKD运行配置 + +文件:`config/skd.json` + +新增根节点: + +```json +{ + "shortRecommend": { + "enabled": true, + "cron": "CRON_TZ=Asia/Shanghai 0 5/10 * * * ?", + "keyTTLHours": 72, + "maxInitializationWrites": 50000 + } +} +``` + +字段说明: + +| 字段 | 默认值/要求 | 说明 | +|---|---|---| +| `enabled` | 未配置默认 `true` | 是否注册推荐队列任务并在启动时补建 | +| `cron` | 每10分钟健康检查 | 当天队列健康时不会重复生成 | +| `keyTTLHours` | 默认 `72` | 必须与App保持一致 | +| `maxInitializationWrites` | 生产未配置默认 `50000`;非生产默认 `0` | 首次推荐分初始化写入保护;显式 `0` 表示关闭限制 | + +正式发布前必须统计未初始化短视频数。若超过保护阈值,任务会在任何批量写入前终止;应选择提前初始化、提高阈值或经确认后显式关闭限制。 + +## 三、Web运行配置 + +`config/web.json` 没有新增字段。Web需要发布代码以提供管理后台配置接口,但不需要新增运行配置。 + +## 四、后台和数据库业务配置 + +这些数据不会随Jenkins代码发布从测试库迁移到生产库,各环境需要单独配置。 + +### 4.1 亚模块配置(`module_conf`) + +新增字段: + +- `onlineAt`、`offlineAt` +- `excludeLatest` +- `excludeRecommend` +- `searchOnlyWhenInactive` +- `haiJiaoStyle.sortRules[].refreshMode` +- `haiJiaoStyle.sortRules[].randomCandidateN` + +历史热门排序 `val=2` 未配置刷新字段时,会兼容为 `RANDOM_TOP_N`、候选数 `30`,不依赖可编辑标题。 + +### 4.2 付费引导(`payment_guide`) + +按环境配置场景、用户分层、样式、文案、套餐、跳转、启用状态和有效期。 +场景新增 `DISCOUNT_COUNTDOWN`;该场景只配置卡片内容,固定2小时倒计时由前端在每次App冷启动时本地重新创建,后端不使用 `durationSeconds` 计算倒计时。 +新增 `HOME_NEW_USER_FREE_TRIAL`,用于“首页新用户免费 X 次试看”,X 读取 `totalWatchCount`;旧 `HOME_NEW_USER` 保留兼容代码但不再向 App/Web 返回。`HOME_NEW_USER_FREE_TRIAL` 与 `HOME_OLD_USER` 共用系统配置 `paymentGuideHomeEnabled` 作为首页场景总开关。 +`VIP_CONTENT_UPDATE.videoLimit` 未配置或为 `0` 时默认返回4条,最大20条。 +曝光记录集合 `payment_guide_impression` 由程序自动写入,不需初始化。 + +### 4.3 评论区Banner(`scene_banner`) + +场景固定为 `COMMENT_TOP`,配置图片/GIF、内外链、排序、启用状态和上下架时间。 +没有默认Banner,测试环境数据不会自动进入生产。 + +### 4.4 VIP卡片A/B(`vip_card_experiment`) + +按环境使用真实套餐ID配置: + +- A/B流量比例 +- 套餐列表与默认套餐 +- `skinKey` +- `productBadges` +- `uiConfig.backgroundImage` +- `uiConfig.badgeStyles` +- 实验结束时间 + +默认没有启用实验;未发布实验时会员套餐接口保持原有返回。统计事件集合由程序自动写入。 + +### 4.5 系统配置(`sys_conf`) + +- 新增 `freeTrialBadgeEnabled=true`:服务启动时自动补充,上线前确认最终开关值。 +- 新增 `totalWatchCount`(int):非VIP用户单日免费观看上限,可编辑/可查看,默认值 `3`。 + - 示例:把 `totalWatchCount` 设置为 `3`,则 `totalWatchCount` 返回 `3`。 +- 新增 `paymentGuideHomeEnabled=true`(bool):首页付费引导共用开关,同时控制 `HOME_NEW_USER_FREE_TRIAL` 和 `HOME_OLD_USER`,可在 Web 系统通用配置中查看和编辑。 +- 复用 `aiGirlFriend=true`:AI女友V2启用条件之一。 + +## 五、历史数据处理 + +`media` 新增 `latestPublishedAt`,历史数据必须回填,否则动漫/漫画最新排序会受缺失值影响: + +```javascript +db.media.updateMany( + { + status: 1, + isDelete: false, + latestPublishedAt: { $exists: false } + }, + [ + { + $set: { + latestPublishedAt: { + $ifNull: [ + "$contentUpdateTime", + { + $ifNull: [ + "$updateTime", + "$createdAt" + ] + } + ] + } + } + } + ] +) +``` + +执行前后应分别统计缺失数量并抽样核对排序结果。该脚本需单独执行,代码发布不会自动回填。 + +## 六、基础设施与发布范围 + +- 不新增Kafka Topic、MQ、Redis实例、端口或操作系统crontab。 +- 继续使用现有MongoDB和Redis。 +- App/H5接口发布 `BUILD_ITEM=app`。 +- 管理后台接口发布 `BUILD_ITEM=web`。 +- 推荐队列生成任务发布 `BUILD_ITEM=skd`。 +- 服务启动会自动创建新增集合和索引,需确认MongoDB账号具有建索引权限。 +- Redis推荐队列、版本、用户偏移及红点缓存均由程序自动创建,无需手工初始化。 + +## 七、不属于本分支的配置 + +- 测试环境手工调整的 `imv2.baseUrl`、`imv2.dynamicConfigDomain`、`imv2.socketUrl` 是环境修复,不是 `feat/91-iteration` 新增配置。 +- H.265相关配置属于其他开发分支,不计入本迭代配置改动。 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..35b5556 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,41 @@ +# 91porn Project Instructions + +- The workspace instructions in `/home/zhufei/code/AGENTS.md` also apply here. +- Project-local credentials are stored in `.env`. Load them with `set -a; source .env; set +a` without printing values. +- Besides shared GitLab, Jenkins, and ZenTao settings, `.env` contains: + - `PORN91_TEST_H5_URL` + - `PORN91_TEST_ADMIN_URL` + - `PORN91_TEST_ADMIN_USERNAME` + - `PORN91_TEST_ADMIN_PASSWORD` + - `PORN91_TEST_ADMIN_VERIFICATION_CODE` +- Never print, log, commit, or copy credential values into user-visible output. +- Keep `.env` ignored by Git with file mode `600`. + +## Backend commit format + +- Use the following commit-message format for 91porn backend changes: + + ```text + [AI] type: concise description + ``` + +- Use the appropriate Conventional Commit type, such as `feat`, `fix`, `refactor`, `test`, or `docs`. +- Example: + + ```text + [AI] feat: 新增短视频全局推荐环形队列 + ``` + +## Jenkins deployment + +- Jenkins job: `91PORN` +- This is a parameterized job: + - `BUILD_BRANCHE`: Git branch to build. + - `BUILD_ITEM`: service/artifact to build and deploy. +- Supported `BUILD_ITEM` values: + - `app`: App/H5 backend API service. + - `web`: management backend API service. + - `skd`: scheduled-job service. + - `swagger`: Swagger artifact. +- Changes that affect App/H5 APIs and scheduled jobs, such as the short-video recommendation queue, require separate `app` and `skd` builds. +- Do not trigger a Jenkins build unless the user explicitly asks for deployment or build execution. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ce8bcd4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM alpine +ADD html /html +ADD web-web /web-web +WORKDIR / +ENTRYPOINT [ "/web-web" ] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..606e448 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ + +GOPATH:=$(shell go env GOPATH) + + +.PHONY: build +build: + go clean + env GOOS=linux go install -i -v + mv $(GOPATH)/bin/linux_amd64/91porn-server . + rm -rf $(GOPATH)/bin/linux_amd64 + zip -q -r 91porn-server.zip 91porn-server config/app.yaml config/app-release.yaml + rm 91porn-server + +.PHONY: test +test: + go test -v ./... -cover + +.PHONY: docker +docker: + docker build . -t web-web:latest diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e92001 --- /dev/null +++ b/README.md @@ -0,0 +1,105 @@ +# 91porn-server + +Go 语言后端服务项目,基于 Gin 框架 + +## 架构 + +单仓库多服务(Monorepo),包含 3 个独立可部署的服务: + +| 服务 | 入口 | 端口 | 说明 | +|------|------|------|------| +| **app** | `app/main.go` | 8182 | 面向客户端的 API 服务(移动端/APP) | +| **web** | `web/main.go` | - | 后台管理系统 API | +| **skd** | `skd/main.go` | - | 定时任务/后台 Job 调度服务 | + +根目录 `main.go` 仅用于 Swagger 文档生成(端口 20114)。 + +## 目录结构 + +``` +├── app/ # APP 端服务 +│ ├── api/ # 接口处理层 +│ ├── router/ # 路由定义 +│ ├── service/ # 业务逻辑层 +│ ├── middleware/ # APP 专用中间件 +│ ├── config/ # APP 配置 +│ └── proto/ # 请求/响应结构体 +├── web/ # 后台管理端服务 +│ ├── api/ # 接口处理层 +│ ├── router/ # 路由定义 +│ ├── service/ # 业务逻辑层 +│ ├── middleware/ # Web 专用中间件 +│ ├── config/ # Web 配置 +│ └── proto/ # 请求/响应结构体 +├── skd/ # 调度任务服务 +│ ├── job/ # 定时任务定义 +│ ├── service/ # 任务业务逻辑 +│ ├── once/ # 一次性脚本 +│ └── config/ # 调度配置 +├── models/ # 数据模型层(数据库表结构、缓存模型) +├── common/ # 公共模块 +│ ├── db/ # 数据库连接 +│ ├── redis/ # Redis 客户端 +│ ├── cache/ cachev2/ # 缓存层 +│ ├── kafka/ # Kafka 消息队列 +│ ├── elastic/ # Elasticsearch +│ ├── log/ # 日志 +│ ├── conf/ # 配置加载 +│ ├── tg/ # Telegram 机器人通知 +│ ├── email/ sms/ # 邮件/短信通知 +│ ├── filter/ # 内容过滤 +│ ├── dataReport/ # 数据上报 +│ ├── datacenter/ # 数据中心 +│ ├── aiMate/ aiService/# AI 功能 +│ ├── laosiji/ # 老司机模块 +│ ├── store/ # 对象存储 +│ └── ... # 其他工具模块 +├── middleware/ # 通用中间件(CORS、IP、UA、请求ID) +├── generate/ # 代码生成 +├── script/ # 脚本 +└── swagger/ # Swagger 文档 +``` + +## 技术栈 + +- **Web 框架**:Gin +- **ORM**:GORM +- **数据库**:MySQL +- **搜索引擎**:Elasticsearch +- **缓存**:Redis +- **消息队列**:Kafka (Sarama) +- **对象存储**:AWS S3 +- **API 文档**:Swagger (swaggo) +- **部署**:Docker + Shell 脚本 + +## 业务功能 + +- 用户体系(注册/登录/JWT 鉴权/VIP) +- 视频/媒体管理(上传/播放/标签/搜索) +- 支付系统(充值/提现/钱包/金币) +- 社区功能(评论/关注/点赞/排行榜) +- IM 即时通讯(群组/消息) +- AI 功能(换脸/脱衣/文生图/文生视频/文生小说) +- 运营工具(活动/公告/签到/任务/兑换码) +- 广告系统 +- 裸聊/代充 +- 后台管理(用户管理/内容审核/数据统计/配置管理) + +## 构建与部署 + +```bash +# 构建 +make build + +# 测试 +make test + +# Docker 构建 +make docker +``` + +生产部署脚本: +- `app_update_prod.sh` — 部署 APP 服务 +- `web_update_prod.sh` — 部署 Web 后台服务 +- `skd_update_prod.sh` — 部署调度服务 +- `skd_update_test.sh` — 部署调度服务(测试环境) diff --git a/SHORT_VIDEO_RECOMMENDATION_BACKEND_TECH_DESIGN.md b/SHORT_VIDEO_RECOMMENDATION_BACKEND_TECH_DESIGN.md new file mode 100644 index 0000000..0a6ee26 --- /dev/null +++ b/SHORT_VIDEO_RECOMMENDATION_BACKEND_TECH_DESIGN.md @@ -0,0 +1,441 @@ +# 短视频全局推荐环形队列——后端技术方案 + +## 1. 文档范围 + +本文仅覆盖《91porn迭代需求》第一项“短视频推荐逻辑修改”,以当前代码、测试H5和测试管理后台为准。 + +已确认的产品口径: + +- 只推荐审核通过的短视频; +- VIP、金币、广告和特殊视频均可参与推荐,其他状态和内容类型不参与; +- 互动分为 `点赞×1 + 收藏×2 + 评论×3 + 转发×5`; +- 使用全部累计真实互动,不使用运营假数据; +- 取消点赞、取消收藏、删除评论等行为不扣推荐累计分; +- 互动分和全局队列每日更新一次; +- 每批固定20条,严格按17条高分视频加3条新视频组织; +- 新视频按审核通过时间24小时计算,并从高分池排除; +- 新视频不足3条时用高分视频补齐; +- 用户偏移存Redis Hash; +- 每日新队列切换后所有用户从新队列起点重新开始; +- 队尾通过取模回到队首。 + +## 2. 现状核对 + +### 2.1 H5实际调用 + +测试H5短视频Swiper实际调用: + +```http +GET /api/app/recommend/vid/list?pageNumber={n}&pageSize={size} +``` + +当前响应: + +```json +{ + "vInfos": [], + "totalPages": 10 +} +``` + +H5还单独调用: + +```http +GET /api/app/recommend/vid/ad +GET /api/app/vid/user/count?vid={videoId} +``` + +广告插入和免费观看次数不并入本次推荐队列算法。 + +### 2.2 后端现状 + +主接口位于: + +- `app/api/recommctrl/recommctrl.go` +- `app/service/recommser/recommser.go` + +当前算法: + +```text +Redis Set(short-videos-all-ids-list) + -> SRANDMEMBER N + -> 查询视频详情 + -> 返回 +``` + +另有遗留接口: + +```http +GET /api/app/vid/module/short/all +``` + +该接口使用另一个Redis Set `shortVideosRecoCache`,并存在5分钟全局响应缓存。两条接口目前算法和Redis Key均不统一。 + +### 2.3 管理后台现状 + +后台已有“视频热度配置”: + +```http +GET /api/web/admin/vid/popularity/config +POST /api/web/admin/vid/popularity/config +``` + +该配置计算播放量、有效播放量、点赞量和审核时间衰减,是通用视频热度,不符合本次固定的互动分算法。因此: + +- 本次推荐不能复用现有 `VideoPopularityConfig`; +- 不修改现有热度配置页面; +- 一期不新增运营配置页面; +- 推荐权重按产品确认值作为后端常量; +- 灰度开关和任务时间使用服务配置。 + +## 3. 总体架构 + +```text +真实互动事件 + ↓ +视频推荐累计字段(只增不减) + ↓ +每日SKD任务计算recommendScore + ↓ +生成17+3全局有序队列 + ↓ +完整写入Redis版本Key + ↓ +原子切换current版本 + ↓ +推荐接口按用户Hash偏移环形读取 +``` + +不在请求时实时计算分数,不为每个用户生成独立队列。 + +## 4. 数据模型 + +在视频模型增加: + +```go +RecommendLikeCount int64 `json:"-" bson:"recommendLikeCount"` +RecommendCollectCount int64 `json:"-" bson:"recommendCollectCount"` +RecommendCommentCount int64 `json:"-" bson:"recommendCommentCount"` +RecommendShareCount int64 `json:"-" bson:"recommendShareCount"` +RecommendScore int64 `json:"-" bson:"recommendScore"` +RecommendScoreAt time.Time `json:"-" bson:"recommendScoreAt"` +``` + +计算公式: + +```text +recommendScore = + recommendLikeCount + + recommendCollectCount × 2 + + recommendCommentCount × 3 + + recommendShareCount × 5 +``` + +### 4.1 为什么不能直接使用现有字段 + +现有 `likeCount/collectCount/commentCount/shareCount` 会在取消或删除时下降,而产品已确认取消行为不扣推荐分。 + +现有 `fakeLikeCount/fakeCommentCount/fakeShareCount` 包含运营假数据,禁止参与推荐分。 + +因此需要独立的单调递增推荐累计字段。 + +### 4.2 互动写入规则 + +仅在真实行为首次成功时递增: + +- 点赞成功:`recommendLikeCount + 1` +- 收藏成功:`recommendCollectCount + 1` +- 评论发布成功:`recommendCommentCount + 1` +- 转发首次有效记录:`recommendShareCount + 1` + +现有 `POST /api/app/share/output` 增加可选参数 `videoID`。新客户端分享短视频时必须传入,服务端仅在ID合法且视频当前为审核通过的短视频时累计;旧客户端不传保持原分享能力,但无法归属到具体视频,因此不累计推荐分享分。 + +以下行为不修改推荐累计字段: + +- 取消点赞; +- 取消收藏; +- 删除评论; +- 运营修改假互动; +- 重复请求和重复事件。 + +每个互动入口必须先利用现有行为唯一性或幂等逻辑确认“首次成功”,再累计推荐字段。 + +## 5. 历史数据初始化 + +上线前执行一次幂等脚本: + +```text +recommendLikeCount = max(likeCount, 0) +recommendCollectCount = max(collectCount, 0) +recommendCommentCount = max(commentCount, 0) +recommendShareCount = max(shareCount, 0) +``` + +不读取任何 `fake*` 字段。 + +脚本增加初始化版本标记,重复执行不得覆盖已经由线上事件增长的新累计值。 + +## 6. 每日队列生成 + +任务放在 `skd/job`,使用现有Cron框架。默认按北京时间凌晨低峰运行,具体时间由部署配置确定。 + +### 6.1 候选条件 + +```text +status = 审核通过 +newsType = 短视频 +未删除 +``` + +VIP、金币、广告、特殊视频不作为排除条件。 + +### 6.2 新视频池 + +```text +reviewAt >= generatedAt - 24h +reviewAt <= generatedAt +``` + +排序: + +```text +reviewAt desc, _id desc +``` + +进入新视频池的视频必须从高分池排除,当天队列中只使用一次。 + +### 6.3 高分池 + +排序: + +```text +recommendScore desc, reviewAt desc, _id desc +``` + +排除当日新视频池全部ID。 + +### 6.4 组装算法 + +每个推荐块: + +```text +high = 最多17条 +new = 最多3条 +缺少的新视频名额由high补齐 +块内确定性洗牌 +``` + +要求: + +- 同一视频在当日全局队列只出现一次; +- 新视频消费完后,后续块全部由高分视频组成; +- 不复制视频凑足队列长度; +- 尾部不足20条时保留实际数量; +- 洗牌使用日期版本作为种子,便于问题复现。 + +## 7. Redis设计 + +```text +recommend:short:current +recommend:short:queue:{version} +recommend:short:offset:{version} +recommend:short:meta:{version} +recommend:short:build-lock:{version} +``` + +示例: + +```text +recommend:short:current -> 20260724 +recommend:short:queue:20260724 -> LIST(videoId...) +recommend:short:offset:20260724 -> HASH(uid => nextOffset) +recommend:short:meta:20260724 -> HASH(length, generatedAt, startOffset) +``` + +规则: + +- `queue` 使用Redis List,保持顺序; +- `offset` 使用一个Hash存放全部用户偏移; +- `startOffset` 第一期固定为0; +- 新版本先完整写入,再原子切换 `current`; +- 新版本Key设置72小时TTL; +- 版本切换天然实现每日偏移重置,不扫描删除旧Hash; +- 构建锁避免多个SKD实例同时生成同一版本。 + +## 8. 环形读取 + +`offset` 表示下一次读取位置: + +```text +newOffset = (offset + scannedCount) % queueLength +``` + +使用Lua脚本原子完成: + +1. 读取当前版本; +2. 读取队列长度; +3. `HGET`用户偏移; +4. 无偏移时使用0; +5. 计算本次候选索引; +6. 推进并保存新偏移; +7. 返回版本、旧偏移和候选视频ID。 + +接口层查询Mongo并再次校验视频状态。发现失效视频时继续向后扫描补位,最多扫描一整圈。 + +约束: + +- 同一响应内同一视频最多出现一次; +- 队列长度小于20时只返回一圈; +- Mongo返回结果必须按Redis ID顺序重新排列; +- 多设备和并发请求共享同一用户偏移。 + +## 9. App接口改造 + +### 9.1 H5主接口 + +保留现有地址: + +```http +GET /api/app/recommend/vid/list +``` + +修改 `recommser.GetVidList`,由随机Set切换到环形队列服务。 + +### 9.2 兼容接口 + +保留: + +```http +GET /api/app/vid/module/short/all +``` + +该接口删除5分钟全局响应缓存,并委托同一个环形队列服务读取,避免两个客户端获得不同推荐逻辑。 + +### 9.3 广告接口 + +保持不变: + +```http +GET /api/app/recommend/vid/ad +``` + +广告仍由H5在推荐内容之外按现有规则插入,不占17+3名额。 + +### 9.4 免费观看次数接口关联改动 + +现有接口: + +```http +GET /api/app/vid/user/count +``` + +传或不传 `vid` 的两个响应分支都增加: + +```json +{ + "totalWatchCount": 3 +} +``` + +该字段读取现有 `appg.Conf.Base.TotalWatch`,表示系统配置的免费观看总次数;现有 `watchCount` 继续表示用户剩余次数。不新增数据库字段,不修改扣减逻辑。此项已纳入接口设计,但代码尚未修改。 + +## 10. 响应兼容 + +H5主接口保留: + +```json +{ + "vInfos": [], + "totalPages": 100 +} +``` + +新增: + +```json +{ + "hasNext": true, + "queueVersion": "20260724" +} +``` + +说明: + +- `vInfos`结构不变; +- `totalPages`继续返回 `ceil(queueLength / 20)`,仅兼容旧客户端; +- 环形队列非空时 `hasNext=true`; +- 前端不得用 `pageNumber`计算服务端offset; +- `queueVersion`仅用于日志排查。 + +## 11. 灰度、降级和监控 + +服务配置: + +```text +shortRecommendV2Enabled +shortRecommendCron +shortRecommendKeyTTLHours = 72 +``` + +降级顺序: + +1. 当日队列不可用时使用上一有效版本; +2. 没有任何版本或Redis异常时,回退现有 `ShortVideosKey` 随机逻辑; +3. 降级必须记录日志和指标。 + +监控: + +- 生成耗时和任务状态; +- 候选、高分、新视频和最终队列数量; +- 每块17+3比例; +- 重复率; +- 接口耗时和空结果率; +- 失效视频跳过数; +- Redis/Lua失败数; +- 版本切换时间; +- 降级次数。 + +## 12. 索引 + +建议Mongo索引: + +```text +status + newsType + reviewAt +status + newsType + recommendScore + reviewAt +``` + +根据生产数据量通过 `explain` 确认最终字段顺序。 + +## 13. 测试 + +必须覆盖: + +- 正常17+3; +- 新视频0/1/2/3条; +- 新视频池耗尽; +- 高分池不足; +- 队列不足20条; +- 尾部跨界读取; +- 用户首次进入; +- 不同用户偏移隔离; +- 同用户并发; +- 每日版本重置; +- 队列中视频下架后的补位; +- 假数据不计分; +- 取消互动不扣分; +- 历史初始化幂等; +- SKD重复执行; +- 生成失败继续使用旧队列; +- H5主接口和兼容接口响应结构。 + +## 14. 发布步骤 + +1. 发布新增字段和索引; +2. 执行历史累计初始化; +3. 发布互动累计逻辑; +4. 发布SKD任务,影子生成队列但不切流; +5. 核对数量、分数、17+3比例和重复率; +6. 发布App接口兼容字段; +7. 小流量开启 `shortRecommendV2Enabled`; +8. 全量切换; +9. 保留旧随机Set作为短期降级路径。 diff --git a/VIP卡片UI配置接口.md b/VIP卡片UI配置接口.md new file mode 100644 index 0000000..7e125b4 --- /dev/null +++ b/VIP卡片UI配置接口.md @@ -0,0 +1,78 @@ +# VIP卡片 UI 配置接口 + +本次不新增接口,在现有接口中增加 `uiConfig`。 + +## 1. 发布配置 + +```http +POST /api/web/admin/vip-card-experiment/publish +``` + +`variantA`、`variantB` 增加: + +```json +{ + "uiConfig": { + "backgroundImage": "背景图地址", + "badgeStyles": [ + { + "badgeType": "MOST_POPULAR", + "backgroundColor": "#2B251A", + "textColor": "#F7D98C" + } + ] + } +} +``` + +说明: + +- `backgroundImage`:当前 A/B 分组的会员中心整体页面背景图,不是单个套餐卡片背景。 +- `badgeType`:与套餐返回的 `badgeType` 对应。 +- `backgroundColor`:角标背景色。 +- `textColor`:角标文字颜色。 +- 颜色支持 `#RRGGBB`、`#RRGGBBAA`。 + +## 2. 查询配置 + +```http +GET /api/web/admin/vip-card-experiment/current +``` + +增加返回: + +```text +data.variantA.uiConfig +data.variantB.uiConfig +``` + +## 3. App/H5 套餐接口 + +```http +GET /api/app/vip/product +``` + +增加返回: + +```text +data.uiConfig +``` + +App/H5 只会收到当前登录用户命中分组的 `uiConfig`。 + +## 4. 图片上传 + +继续使用现有接口: + +```http +POST /api/web/admin/vid/uploadStatic +``` + +将返回的 `data.coverImg` 填入 `uiConfig.backgroundImage`。 + +## 注意 + +- `uiConfig` 不传时,前端使用默认背景和默认配色。 +- `badgeStyles` 只控制颜色,角标文字仍使用套餐中的 `badgeText`。 +- `desc` 不参与新角标显示。 +- 发布接口是整份实验发布;修改配置时需要使用新的 `experimentId`。 diff --git a/app/api/active2023ctrl/lottery.go b/app/api/active2023ctrl/lottery.go new file mode 100644 index 0000000..65054ae --- /dev/null +++ b/app/api/active2023ctrl/lottery.go @@ -0,0 +1,69 @@ +package active2023ctrl + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/active2023ser" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/usermod" + + "github.com/gin-gonic/gin" +) + +// UserInfo doc +// @Summary 抽奖 +// @Description 抽奖 +// @Tags annou +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/active2023/lottery [post] +func Lottery(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil && err != common.ErrUserNotExist { + common.ServeJSON(ctx, stderr.UserIsNotExists, nil) + return + } + if uid == 0 { + common.ServeJSON(ctx, stderr.UserIsNotExists, nil) + return + } + var req active2023ser.LotteryReq + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if len(req.Prizes) == 0 { + common.ServeJSON(ctx, stderr.ErrParamError, "获奖为空") + return + } + userInfo, err := usermod.RefreshCacheAndGetUser(uid) // 因为抽奖可能会频繁刷新用户权益, 因此需要刷新用户缓存以保证获取用户最新信息 + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, "") + return + } + if userInfo == nil { + common.ServeJSON(ctx, stderr.UserIsNotExists, "用户不存在") + return + } + redisKey := redisconst.GetUserActive2023RedisKey(uid) + success, err := appg.Redis.Setnx_NewOK(redisKey, "1", redisconst.GetUserActive2023Expred()) + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, "") + return + } + if !success { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, "请求过于频繁, 请稍后再试") + return + } + defer func() { _, _ = appg.Redis.Del(redisKey) }() + if err = active2023ser.Lottery(userInfo, req); err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err.Error()) + return + } + log.Info("lottery success", log.Any("uid", uid), log.Any("count", req.Count), log.Any("gold cost", req.Gold), log.Any("prizes", req.Prizes)) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/app/api/active2023ctrl/user_info.go b/app/api/active2023ctrl/user_info.go new file mode 100644 index 0000000..1374c14 --- /dev/null +++ b/app/api/active2023ctrl/user_info.go @@ -0,0 +1,102 @@ +package active2023ctrl + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "time" + + "91porn-server/common" + "91porn-server/common/crypt" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/active2023mod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + + "github.com/gin-gonic/gin" +) + +// UserInfo doc +// @Summary 查询用户基本信息(抽奖维度) +// @Description 查询用户基本信息(抽奖维度) +// @Tags annou +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/active2023/user_info [get] +func UserInfo(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + userInfo, err := usermod.FindUserByUID(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, "") + return + } + if userInfo == nil { + common.ServeJSON(ctx, stderr.UserIsNotExists, "用户不存在") + return + } + w, err := walletmod.GetWallet(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err.Error()) + return + } + totalRecharge, balance := int64(0), int64(0) + if w != nil { + totalRecharge = w.Consumption / 10 + balance = w.Amount + w.Income + } + active2023userInfo, err := active2023mod.GetActive2023UserInfoByID(nil, int64(uid)) + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err.Error()) + return + } + var lotteryTimes int64 + if active2023userInfo != nil { + lotteryTimes = active2023userInfo.LotteryRemain + } + token := ctx.Request.Header.Get("Authorization") + st := struct { + UID uint64 `json:"uid"` + UserName string `json:"user_name"` + AppID int32 `json:"app_id"` + TotalRecharge int64 `json:"total_recharge"` // 用户累计充值金额 + Balance int64 `json:"balance"` // 金币余额 + Token string `json:"token"` + }{ + UID: userInfo.UID, + UserName: userInfo.Name, + AppID: commod.KFK_APPID, + TotalRecharge: totalRecharge, + Balance: balance, + Token: token, + } + ct, _ := json.Marshal(st) + ctx.JSON(http.StatusOK, gin.H{ + "code": stderr.Success, + "hash": false, + "msg": "success", + "tip": "", + "data": struct { + UID uint64 `json:"uid"` + Data string `json:"data"` + LotteryTimes int64 `json:"lottery_times"` + }{ + UID: userInfo.UID, + Data: encrypt(ct), + LotteryTimes: lotteryTimes, + }, + "time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), + }) +} + +// 字段加密. 加密协议: AES-CBC-PCK5 +func encrypt(bts []byte) string { + xpass, _ := crypt.AESCBCPck5Encrypt(bts, []byte("nU7cLOX7t3yJHq8yeIMCfO9emiOWtdlN")) + return base64.StdEncoding.EncodeToString(xpass) +} diff --git a/app/api/activityctrl/activityctrl.go b/app/api/activityctrl/activityctrl.go new file mode 100644 index 0000000..2916400 --- /dev/null +++ b/app/api/activityctrl/activityctrl.go @@ -0,0 +1,275 @@ +package activityctrl + +import ( + "strconv" + + "91porn-server/app/service/activityser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +type CurrencyListReq struct { + UserID string `json:"userId" binding:"required"` +} + +func CurrencyList(c *gin.Context) { + var req CurrencyListReq + if err := c.ShouldBindJSON(&req); err != nil { + common.ServeJSONNoEncrypt(c, stderr.ErrParamError, nil) + return + } + + data, code := activityser.GetCurrencyList(c) + if code != stderr.Success { + common.ServeJSONNoEncrypt(c, code, nil) + return + } + + common.ServeJSONNoEncrypt(c, stderr.Success, gin.H{ + "list": data, + }) +} + +// VipDeductReq 会员卡当前可用抵扣(按抵扣后金额匹配支付通道) +type VipDeductReq struct { + ProductID string `json:"productId"` // 会员卡ID + DeductAmount int64 `json:"deductAmount"` // 券面额(分) +} + +type ProductListReq struct { + UserID string `json:"userId" binding:"required"` + Deducts []VipDeductReq `json:"deducts"` // 各会员卡可用抵扣;按抵扣后有效金额匹配支付通道,可选 +} + +func ProductList(c *gin.Context) { + var req ProductListReq + if err := c.ShouldBindJSON(&req); err != nil { + common.ServeJSONNoEncrypt(c, stderr.ErrParamError, nil) + return + } + + uid, err := strconv.ParseUint(req.UserID, 10, 64) + if err != nil { + common.ServeJSONNoEncrypt(c, stderr.ErrParamError, "invalid userId") + return + } + + deducts := make([]activityser.VipDeduct, 0, len(req.Deducts)) + for _, d := range req.Deducts { + deducts = append(deducts, activityser.VipDeduct{ProductID: d.ProductID, DeductAmount: d.DeductAmount}) + } + + res, err := activityser.GetProductList(c, uid, deducts) + if err != nil { + log.ErrorX(c, "活动服-获取会员卡列表异常", log.Any("uid", req.UserID), log.E(err)) + common.ServeJSONNoEncrypt(c, stderr.ErrNetWorkBusy, nil) + return + } + + common.ServeJSONNoEncrypt(c, stderr.Success, gin.H{ + "list": res, + }) +} + +type RechargeOrderReq struct { + UserID string `json:"userId" binding:"required"` + RechargeType string `json:"rechargeType" binding:"required"` + ProductID string `json:"productId" binding:"required"` + BuyType int `json:"buyType" binding:"required"` + ActivityID string `json:"activityId"` + ExperimentID string `json:"experimentId"` + ExperimentVariant string `json:"experimentVariant"` + SessionID string `json:"sessionId"` + CouponID string `json:"couponId"` // 会员抵扣券ID(可选,购买会员卡时自动抵扣) + DeductAmount int64 `json:"deductAmount"` // 活动服建议抵扣金额(分,可选),本服按券自行校验上限后折价 +} + +func RechargeOrder(c *gin.Context) { + var req RechargeOrderReq + if err := c.ShouldBindJSON(&req); err != nil { + common.ServeJSONNoEncrypt(c, stderr.ErrParamError, nil) + return + } + + uid, err := strconv.ParseUint(req.UserID, 10, 64) + if err != nil { + common.ServeJSONNoEncrypt(c, stderr.ErrParamError, "invalid userId") + return + } + + payUrl, mode, err := activityser.CreateRechargeOrder( + c, + uid, + req.RechargeType, + req.ProductID, + req.BuyType, + c.ClientIP(), + activityser.RechargeAttribution{ + ActivityID: req.ActivityID, + ExperimentID: req.ExperimentID, + ExperimentVariant: req.ExperimentVariant, + SessionID: req.SessionID, + }, + req.CouponID, + req.DeductAmount, + ) + if err != nil { + log.ErrorX(c, "活动服-充值下单异常", log.Any("uid", req.UserID), log.E(err)) + common.ServeJSONNoEncrypt(c, stderr.RechargeFaile, err.Error()) + return + } + + common.ServeJSONNoEncrypt(c, stderr.Success, gin.H{ + "payUrl": payUrl, + "mode": mode, + }) +} + +type BuyCoinProductReq struct { + UserID string `json:"userId" binding:"required"` + ProductID string `json:"productId" binding:"required"` +} + +func BuyCoinProduct(c *gin.Context) { + var req BuyCoinProductReq + if err := c.ShouldBindJSON(&req); err != nil { + common.ServeJSONNoEncrypt(c, stderr.ErrParamError, nil) + return + } + + uid, err := strconv.ParseUint(req.UserID, 10, 64) + if err != nil { + common.ServeJSONNoEncrypt(c, stderr.ErrParamError, "invalid userId") + return + } + + code, err := activityser.BuyCoinProduct(c, uid, req.ProductID, "") + if err != nil { + log.ErrorX(c, "活动服-金币购买异常", log.Any("uid", req.UserID), log.Any("productId", req.ProductID), log.E(err)) + if code == stderr.InsufficientBalance { + common.ServeJSONNoEncrypt(c, 201, "余额不足") + return + } + common.ServeJSONNoEncrypt(c, code, err.Error()) + return + } + + common.ServeJSONNoEncrypt(c, stderr.Success, nil) +} + +type UserBalanceReq struct { + UserID string `json:"userId" binding:"required"` +} + +func UserBalance(c *gin.Context) { + var req UserBalanceReq + if err := c.ShouldBindJSON(&req); err != nil { + common.ServeJSONNoEncrypt(c, stderr.ErrParamError, nil) + return + } + + uid, err := strconv.ParseUint(req.UserID, 10, 64) + if err != nil { + common.ServeJSONNoEncrypt(c, stderr.ErrParamError, "invalid userId") + return + } + + balance, err := activityser.GetUserBalance(uid) + if err != nil { + log.ErrorX(c, "活动服-查询余额异常", log.Any("uid", req.UserID), log.E(err)) + common.ServeJSONNoEncrypt(c, stderr.ErrNetWorkBusy, nil) + return + } + + common.ServeJSONNoEncrypt(c, stderr.Success, balance) +} + +type AppInfoReq struct { + UserID string `json:"userId" binding:"required"` +} + +func AppInfo(c *gin.Context) { + var req AppInfoReq + if err := c.ShouldBindJSON(&req); err != nil { + common.ServeJSONNoEncrypt(c, stderr.ErrParamError, nil) + return + } + + uid, err := strconv.ParseUint(req.UserID, 10, 64) + if err != nil { + common.ServeJSONNoEncrypt(c, stderr.ErrParamError, "invalid userId") + return + } + + data, err := activityser.GetAppInfo(c, uid) + if err != nil { + log.ErrorX(c, "活动服-获取应用信息异常", log.Any("uid", req.UserID), log.E(err)) + common.ServeJSONNoEncrypt(c, stderr.ErrNetWorkBusy, nil) + return + } + + common.ServeJSONNoEncrypt(c, stderr.Success, data) +} + +func Reward(c *gin.Context) { + var req activityser.RewardReq + if err := c.ShouldBindJSON(&req); err != nil { + common.ServeJSONNoEncrypt(c, stderr.ErrParamError, nil) + return + } + + if err := activityser.GrantReward(c, &req); err != nil { + log.ErrorX(c, "活动服-发奖异常", + log.Any("uid", req.UserId), + log.Any("rewardType", req.RewardType), + log.Any("amount", req.Amount), + log.E(err)) + common.ServeJSONNoEncrypt(c, stderr.Failure, err.Error()) + return + } + + common.ServeJSONNoEncrypt(c, stderr.Success, nil) +} + +func BatchReward(c *gin.Context) { + var req activityser.BatchRewardReq + if err := c.ShouldBindJSON(&req); err != nil { + common.ServeJSONNoEncrypt(c, stderr.ErrParamError, nil) + return + } + + if err := activityser.GrantBatchReward(c, &req); err != nil { + log.ErrorX(c, "活动服-批量发奖异常", + log.Any("uid", req.UserId), + log.Any("rewardCount", len(req.Rewards)), + log.E(err)) + common.ServeJSONNoEncrypt(c, stderr.Failure, err.Error()) + return + } + + common.ServeJSONNoEncrypt(c, stderr.Success, nil) +} + +func Deduct(c *gin.Context) { + var req activityser.DeductReq + if err := c.ShouldBindJSON(&req); err != nil { + common.ServeJSONNoEncrypt(c, stderr.ErrParamError, nil) + return + } + + code, err := activityser.Deduct(c, &req) + if err != nil { + log.ErrorX(c, "活动服-扣款异常", + log.Any("uid", req.UserId), + log.Any("deductType", req.DeductType), + log.Any("amount", req.Amount), + log.E(err)) + common.ServeJSONNoEncrypt(c, code, err.Error()) + return + } + + common.ServeJSONNoEncrypt(c, stderr.Success, nil) +} diff --git a/app/api/actvctrl/actv.go b/app/api/actvctrl/actv.go new file mode 100644 index 0000000..8f3584f --- /dev/null +++ b/app/api/actvctrl/actv.go @@ -0,0 +1,52 @@ +package actvctrl + +import ( + "91porn-server/app/service/actvser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func GetActivities(c *gin.Context) { + var r actvser.GetActitiesRequest + if err := c.Bind(&r); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + activities, hasNext, err := actvser.GetActivities(r) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(c, stderr.Success, gin.H{ + "list": activities, + "hasNext": hasNext, + }) +} + +func GetActiveByID(c *gin.Context) { + var r actvser.GetActiveByIDRequest + if err := c.Bind(&r); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + id, err := primitive.ObjectIDFromHex(r.ID) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, "invalid id") + return + } + if id.IsZero() { + common.ServeJSON(c, stderr.ErrParamError, "empty id") + return + } + activity, err := actvser.GetActiveByID(id) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(c, stderr.Success, gin.H{ + "act": activity, + }) +} diff --git a/app/api/adsctrl/ads.go b/app/api/adsctrl/ads.go new file mode 100644 index 0000000..10f5c17 --- /dev/null +++ b/app/api/adsctrl/ads.go @@ -0,0 +1,111 @@ +package adsctrl + +import ( + "91porn-server/app/service/adser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/adsmod" + "91porn-server/models/v/usermod" + "context" + "fmt" + "time" + + "github.com/gin-gonic/gin" +) + +// AdsClick doc +// @Summary 广告点击日志 +// @Description 广告点击日志 +// @Tags annou +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ads/click [post] +func AdsClick(ctx *gin.Context) { + _, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrTokenIsNotExist, "") + return + } + var arg struct { + ID string `form:"id" json:"id" binding:"required"` //广告id + Type int `form:"type" json:"type"` // 0 普通广告; 1 金主楼凤广告 + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// list doc +// @Summary 广告列表 +// @Description 广告点击日志 +// @Tags annou +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ads/list [post] +func List(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + ads := adser.AdList(adsmod.SysAds, "") + common.ServeJSON(ctx, stderr.Success, gin.H{"ads": ads}) + return + } + user, _ := usermod.FindUserByUID(uid) + if user == nil { + ads := adser.AdList(adsmod.SysAds, "") + common.ServeJSON(ctx, stderr.Success, gin.H{"ads": ads}) + return + } + if user.DistrictCode == "" { + ads := adser.AdList(adsmod.SysAds, "") + common.ServeJSON(ctx, stderr.Success, gin.H{"ads": ads}) + return + } + ads := adser.AdList(adsmod.DiscAds, user.DistrictCode) + common.ServeJSON(ctx, stderr.Success, gin.H{"ads": ads}) +} + +// AdsClickStat doc +// @Summary 广告点击统计 +// @Description 广告点击统计 +// @Tags annou +// @Accept mpfd,json +// @Produce json,html +// @Param type query int false "广告类型 0:应用 1:广告" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ads/click/stat [post] +func AdsClickStat(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrTokenIsNotExist, "") + return + } + var arg struct { + Type int32 `form:"type" json:"type"` // 广告类型 0:应用 1:广告 + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + ua, _ := common.GetUA(ctx) + ip := common.GetIP(ctx) + common.Go(func() { + userInfo, _ := usermod.FindUserByUID(uid) + if userInfo != nil { + log.Info(fmt.Sprintf("ApplicationAdClick-param-%s:", userInfo.AdGroup), log.Any("uid", uid), log.Any("Type", arg.Type), log.Any("ua", ua), log.Any("ip", ip)) + if arg.Type == 1 { + _ = adser.UpsertAdStat(context.Background(), userInfo, time.Now(), 0, 1, 0) + } else { + _ = adser.UpsertAdStat(context.Background(), userInfo, time.Now(), 1, 0, 0) + } + } + }) + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/app/api/advance_config_ctrl/advance_config.go b/app/api/advance_config_ctrl/advance_config.go new file mode 100644 index 0000000..ce319ea --- /dev/null +++ b/app/api/advance_config_ctrl/advance_config.go @@ -0,0 +1,34 @@ +package advance_config_ctrl + +import ( + "91porn-server/app/service/advance_config_ser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "fmt" + "github.com/gin-gonic/gin" +) + +// GetAdvanceConfig doc +// @Summary 预售配置列表 +// @Description 预售配置列表 +// @Tags 预售配置信息 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} advanceconfigmod.AppAdvanceRes +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/advance_config/list [get] +func GetAdvanceConfig(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + data, err := advance_config_ser.GainAdvanceConfig(uid) + if err != nil { + log.Error(fmt.Sprintf("advance_config_ser GainAdvanceConfig error:%v,uid:%v", err, uid)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/ai_changeface_ctrl/ai_changeface.go b/app/api/ai_changeface_ctrl/ai_changeface.go new file mode 100644 index 0000000..3e39d05 --- /dev/null +++ b/app/api/ai_changeface_ctrl/ai_changeface.go @@ -0,0 +1,247 @@ +package ai_changeface_ctrl + +import ( + "91porn-server/app/service/ai_changeface_ser" + "91porn-server/app/service/m3u8ticket" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/aichangefacemod" + "encoding/json" + "errors" + "fmt" + "strconv" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// List doc +// @Summary AI换脸列表 +// @Description AI换脸列表 +// @Tags AI视频换脸 +// @Accept json +// @Produce json +// @Param status query int false "记录状态" +// @Param pageNumber query int true "第几页" +// @Param pageSize query int true "每页数量" +// @Success 200 object interface{} "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ai/changeface/list [get] +func List(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + var req struct { + Status *aichangefacemod.AiChangeFaceStatus `form:"status" json:"status"` // 0 未完成; 1 已完成; -1 已退款 + commod.Page + } + if err = c.ShouldBindQuery(&req); err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + list, hasNext, err := ai_changeface_ser.List(uid, req.Status, int(req.Skip()), int(req.Limit())) + if err != nil { + log.Error(fmt.Sprintf("ai_changeface_ser List error%v, uid%v", err.Error(), uid)) + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + for i := range list { + m3u8ticket.SignURL(c, uid, &list[i].ModVideo, true, false) + m3u8ticket.SignURL(c, uid, &list[i].Url, true, false) + } + common.ServeJSON(c, stderr.Success, gin.H{ + "list": list, + "hasNext": hasNext, + }) +} + +// Generate doc +// @Summary 生成AI换脸记录 +// @Description 生成AI换脸记录 +// @Tags AI视频换脸 +// @Accept json +// @Produce json +// @Param q body aichangefacemod.GenerateRequest false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ai/changeface/generate [post] +func Generate(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + var in aichangefacemod.GenerateRequest + if err = ctx.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("undress Generate param err%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if len(in.Pic) == 0 || in.VidModID.IsZero() { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + ua, _ := common.GetUA(ctx) + ip := common.GetIP(ctx) + code := ai_changeface_ser.Generate(uid, in.Pic, in.VidModID, in.Discount, in.ShareTitle, in.ShareStatus, ua, ip) + if code != stderr.Success { + log.Error(fmt.Sprintf("undress Generate err%v", code)) + common.ServeJSON(ctx, code, code.Error()) + return + } + updateLog, _ := json.Marshal(in) + _ = operatorlgmod.RecordOperation(strconv.FormatUint(uid, 10), constant.AiChangeface, constant.Add, string(updateLog), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// Hide doc +// @Summary 删除AI换脸记录 +// @Description 删除AI换脸记录 +// @Tags AI视频换脸 +// @Accept json +// @Produce json +// @Param id formData string false "AI订单ID" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ai/changeface/hide [post] +func Hide(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req struct { + ID primitive.ObjectID `json:"id"` + } + if err = ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("changeface Generate param err%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if req.ID.IsZero() { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + acf, err := aichangefacemod.FindByID(nil, req.ID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + if acf.ID.IsZero() { + common.ServeJSON(ctx, stderr.Failure, errors.New("ai换脸订单未找到")) + return + } + if acf.Uid != uid { + common.ServeJSON(ctx, stderr.Failure, errors.New("只能删除自己的订单")) + return + } + if acf.Status == aichangefacemod.StatusGenning || acf.Status == aichangefacemod.StatusSubmit { + common.ServeJSON(ctx, stderr.AiGenningDelForbidden, errors.New("不能删除排队中的订单")) + return + } + if err = aichangefacemod.Hide(uid, req.ID); err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + updateLog, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(strconv.FormatUint(uid, 10), constant.AiChangeface, constant.Delete, string(updateLog), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// ModList doc +// @Summary AI模版列表 +// @Description AI模版列表 +// @Tags AI模版 +// @Accept json +// @Produce json +// @Success 200 object aichangefacevidmod.AppResponse "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/ai/mod/list [get] +func ModList(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + data, err := ai_changeface_ser.ModList(uid) + if err != nil { + log.Error(fmt.Sprintf("ai_changeface_ser ModList error%v,uid%v", err.Error(), uid)) + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + if data != nil { + for i := range data.AiChangeFaceVideoMod { + m3u8ticket.SignURL(c, uid, &data.AiChangeFaceVideoMod[i].SourceURL, true, false) + } + for i := range data.AiImgToVideoMod { + m3u8ticket.SignURL(c, uid, &data.AiImgToVideoMod[i].NewUrl, true, false) + } + } + common.ServeJSON(c, stderr.Success, data) +} + +// ModListV2 doc +// @Summary AI模版列表 +// @Description AI模版列表 +// @Tags AI模版 +// @Accept json +// @Produce json +// @Param q query ai_changeface_ser.ModListV2Req false "请求参数" +// @Success 200 object ai_changeface_ser.ModListV2Resp "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ai/mod/v2/list [get] +func ModListV2(ctx *gin.Context) { + var req ai_changeface_ser.ModListV2Req + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, err := ai_changeface_ser.ModListV2(req) + if err != nil { + log.Error("ai_changeface_ser.ModListV2 fail", log.Any("req", req), log.E(err)) + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + uid := common.TryGetUID(ctx) + for i := range data.TemplateList { + if data.TemplateList[i] == nil { + continue + } + m3u8ticket.SignURL(ctx, uid, &data.TemplateList[i].M3u8Url, true, false) + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// ModInfo doc +// @Summary AI模版详情 +// @Description AI模版详情 +// @Tags AI模版 +// @Accept json +// @Produce json +// @Param q query ai_changeface_ser.ModInfoReq false "请求参数" +// @Success 200 object ai_changeface_ser.ModInfoResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/ai/mod/info [get] +func ModInfo(ctx *gin.Context) { + var req ai_changeface_ser.ModInfoReq + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, err := req.GetInfo() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + uid := common.TryGetUID(ctx) + m3u8ticket.SignURL(ctx, uid, &data.AiChangeFaceMod.M3u8Url, true, false) + m3u8ticket.SignURL(ctx, uid, &data.AiImgToVideoMod.NewUrl, true, false) + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/ai_image_to_video_ctrl/ai_image_to_video.go b/app/api/ai_image_to_video_ctrl/ai_image_to_video.go new file mode 100755 index 0000000..da1b723 --- /dev/null +++ b/app/api/ai_image_to_video_ctrl/ai_image_to_video.go @@ -0,0 +1,114 @@ +package ai_image_to_video_ctrl + +import ( + "91porn-server/app/service/ai_image_to_video_ser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "fmt" + + "github.com/gin-gonic/gin" +) + +// List doc +// +// @Summary 获取AI图生视频列表列表接口 +// @Description 获取AI图生视频列表列表 +// @Tags 移动端-AI图生视频列表 +// @Accept mpfd,json +// @Produce json +// @Param q query ai_image_to_video_ser.AppQueryListReq false "请求参数" +// @Success 200 object ai_image_to_video_ser.AppListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ai/imagetovideo/list [get] +func List(ctx *gin.Context) { + p := &ai_image_to_video_ser.AppQueryListReq{} + if err := ctx.ShouldBind(&p); err != nil { + log.Error(fmt.Sprintf("imagetovideo param err:%v", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 获取用户当前配置 + var err error + p.UID, err = common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + list, err := p.GetList() + if err != nil { + log.Error(fmt.Sprintf("imagetovideo get list err:%v, uid:%v", err, p.UID)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, list) +} + +// Generate doc +// @Summary 生成AI换脸记录 +// @Description 生成AI换脸记录 +// @Tags 移动端-AI图生视频列表 +// @Accept json +// @Produce json +// @Param q body ai_image_to_video_ser.GenerateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ai/imagetovideo/generate [post] +func Generate(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + in := ai_image_to_video_ser.GenerateReq{} + if err = ctx.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("imagetovideo Generate param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + in.UID = uid + ua, _ := common.GetUA(ctx) + ip := common.GetIP(ctx) + code, err := in.Generate(ua, ip) + if err != nil { + log.Error(fmt.Sprintf("imagetovideo Generate err:%v\n", err)) + common.ServeJSON(ctx, code, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// Hide doc +// @Summary 删除AI换脸记录 +// @Description 删除AI换脸记录 +// @Tags 移动端-AI图生视频列表 +// @Accept json +// @Produce json +// @Param q body ai_image_to_video_ser.HideReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ai/imagetovideo/hide [post] +func Hide(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + in := ai_image_to_video_ser.HideReq{} + if err = ctx.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("imagetovideo hide param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + in.UID = uid + err = in.Hide() + if err != nil { + log.Error(fmt.Sprintf("imagetovideo hide err:%v\n", err)) + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} diff --git a/app/api/ai_mate_ctrl/ai_mate.go b/app/api/ai_mate_ctrl/ai_mate.go new file mode 100644 index 0000000..f12fa3d --- /dev/null +++ b/app/api/ai_mate_ctrl/ai_mate.go @@ -0,0 +1,151 @@ +package ai_mate_ctrl + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/ai_mate_ser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" +) + +// GetCurrencys doc +// @Summary AI伴侣 +// @Description 获取AI伴侣币列表 +// @Tags mine +// @Accept json +// @Produce json +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/aimate/currencys [get] +func GetCurrencys(ctx *gin.Context) { + code, data := ai_mate_ser.GetCurrencyList() + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, code, gin.H{"list": data}) +} + +// Exchange doc +// @Summary AI伴侣 +// @Description 兑换AI伴侣货币 +// @Tags mine +// @Accept json +// @Produce json +// @Param q query ai_mate_ser.ExchangeReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/aimate/exchange [post] +func Exchange(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req ai_mate_ser.ExchangeReq + if err = ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + ua, _ := common.GetUA(ctx) + ip := common.GetIP(ctx) + code := ai_mate_ser.Exchange(uid, req, ua, ip) + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, code, "操作成功") +} + +// Login doc +// @Summary AI伴侣 +// @Description 获取当前用户的AI女友登录地址 +// @Tags mine +// @Accept json +// @Produce json +// @Success 200 {object} ai_mate_ser.LoginResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/aimate/login [get] +func Login(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + if appg.Conf.URL.AIMateH5 == "" { + common.ServeJSON(ctx, stderr.FunctionNotEnabled, nil) + return + } + ret, err := ai_mate_ser.Login(uid) + if err != nil { + log.Error("ai_mate_ser Login failed", log.Any("uid", uid), log.E(err)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeJSON(ctx, stderr.Success, ret) +} + +// GetBalance doc +// @Summary AI伴侣 +// @Description 获取用户AI伴侣余额 +// @Tags mine +// @Accept json +// @Produce json +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/aimate/getBalance [get] +func GetBalance(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + if appg.Conf.URL.AIMateH5 == "" { + common.ServeJSON(ctx, stderr.FunctionNotEnabled, nil) + return + } + + ret, err := ai_mate_ser.GetNewBalance(uid) + if err != nil { + log.Error(fmt.Sprintf("uid:%d, ai_mate_ser GetNewBalance err:%v", uid, err)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeJSON(ctx, stderr.Success, ret) +} + +// GianBalance 保留历史拼写错误的接口,兼容旧客户端。 +func GianBalance(ctx *gin.Context) { + GetBalance(ctx) +} + +// SyncInfo doc +// @Summary 同步聊天信息 +// @Description 同步聊天信息 +// @Tags AI伴侣模块 +// @Accept mpfd,json +// @Produce json,html +// @Param param body ai_mate_ser.SyncInfoRes true "参数列表" +// @Success 200 {string} string "成功" +// @Failure 400 {string} string "获取失败的返回结果" +// @Router /api/app/aimate/sync [post] +func SyncInfo(ctx *gin.Context) { + var in ai_mate_ser.SyncInfoRes + if err := ctx.ShouldBindJSON(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + err := in.Sync() + if err != nil { + log.Error(fmt.Sprintf("uid:%v,aimate sync err:%v", in.UID, err)) + common.ServeJSON(ctx, stderr.Failure, err) + return + } + ctx.JSON(http.StatusOK, stderr.Success.Msg()) +} diff --git a/app/api/ai_mate_v2_ctrl/ai_mate.go b/app/api/ai_mate_v2_ctrl/ai_mate.go new file mode 100644 index 0000000..68a0a14 --- /dev/null +++ b/app/api/ai_mate_v2_ctrl/ai_mate.go @@ -0,0 +1,45 @@ +package ai_mate_v2_ctrl + +import ( + "91porn-server/app/service/aiser" + "91porn-server/common" + "91porn-server/common/laosiji_app" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/v/sysconfmod" + + "github.com/gin-gonic/gin" +) + +// URL doc +// @Summary AI女友V2 +// @Description 主钱包金币上分并获取AI女友授权链接 +// @Tags AI女友V2 +// @Produce json +// @Success 200 {object} aiser.GetAuthURLResp +// @Router /api/app/aimatev2/url [post] +func URL(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + config, err := sysconfdata.GetAllFromCache() + if err != nil { + log.Warn("get AI girlfriend switch failed", log.Any("uid", uid), log.E(err)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + if !config.GetBool(sysconfmod.VCodeAiGirlFriend) || !laosiji_app.Configured() { + common.ServeJSON(ctx, stderr.FunctionNotEnabled, nil) + return + } + resp, err := aiser.GetAuthURL(ctx.Request.Context(), uid) + if err != nil { + log.Error("get AI girlfriend V2 URL failed", log.Any("uid", uid), log.E(err)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} diff --git a/app/api/ai_text_to_image_ctrl/aitexttoimage.go b/app/api/ai_text_to_image_ctrl/aitexttoimage.go new file mode 100755 index 0000000..6b63918 --- /dev/null +++ b/app/api/ai_text_to_image_ctrl/aitexttoimage.go @@ -0,0 +1,112 @@ +package ai_text_to_image_ctrl + +import ( + "91porn-server/app/service/ai_text_to_image_ser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "fmt" + + "github.com/gin-gonic/gin" +) + +// List doc +// +// @Summary 获取AI绘图列表接口 +// @Description 获取AI绘图列表 +// @Tags 移动端-AI绘图列表 +// @Accept mpfd,json +// @Produce json +// @Param q query ai_text_to_image_ser.AppQueryListReq false "请求参数" +// @Success 200 object ai_text_to_image_ser.AppListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ai/text_to_image/list [get] +func List(ctx *gin.Context) { + p := &ai_text_to_image_ser.AppQueryListReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + // 获取用户当前配置 + var err error + p.UID, err = common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + list := p.GetList() + common.ServeJSON(ctx, stderr.Success, list) +} + +// Generate doc +// @Summary 生成AI绘图订单记录 +// @Description 生成AI绘图订单记录 +// @Tags 移动端-AI绘图列表 +// @Accept json +// @Produce json +// @Param q body ai_text_to_image_ser.GenerateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ai/text_to_image/generate [post] +func Generate(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + in := ai_text_to_image_ser.GenerateReq{} + if err = ctx.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("ai_text_to_image Generate param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + in.UID = uid + ua, _ := common.GetUA(ctx) + ip := common.GetIP(ctx) + code, err := in.Generate(ua, ip) + if err != nil { + log.Error(fmt.Sprintf("ai_text_to_image Generate err:%v\n", err)) + common.ServeJSON(ctx, code, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// Hide doc +// @Summary 删除AI绘图记录 +// @Description 删除AI绘图记录 +// @Tags 移动端-AI绘图列表 +// @Accept json +// @Produce json +// @Param q body ai_text_to_image_ser.HideReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ai/text_to_image/hide [post] +func Hide(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + in := ai_text_to_image_ser.HideReq{} + if err = ctx.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("ai_text_to_image hide param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + in.UID = uid + err = in.Hide() + if err != nil { + log.Error(fmt.Sprintf("ai_text_to_image hide err:%v\n", err)) + if code, ok := err.(stderr.Code); ok { + common.ServeJSON(ctx, code, code.Error()) + return + } + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} diff --git a/app/api/ai_text_to_novel_ctrl/aitexttonovel.go b/app/api/ai_text_to_novel_ctrl/aitexttonovel.go new file mode 100755 index 0000000..73dd8e4 --- /dev/null +++ b/app/api/ai_text_to_novel_ctrl/aitexttonovel.go @@ -0,0 +1,138 @@ +package ai_text_to_novel_ctrl + +import ( + "91porn-server/app/service/ai_text_to_novel_ser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "fmt" + + "github.com/gin-gonic/gin" +) + +// List doc +// +// @Summary 获取AI小说列表列表接口 +// @Description 获取AI小说列表列表 +// @Tags 移动端-AI小说列表 +// @Accept mpfd,json +// @Produce json +// @Param q query ai_text_to_novel_ser.AppQueryListReq false "请求参数" +// @Success 200 object ai_text_to_novel_ser.AppListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ai_text_to_novel/list [get] +func List(ctx *gin.Context) { + p := &ai_text_to_novel_ser.AppQueryListReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + // 获取用户当前配置 + var err error + p.UID, err = common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + list := p.GetList() + common.ServeJSON(ctx, stderr.Success, list) +} + +// Info doc +// @Summary 获取AI小说列表详情接口 +// @Description 获取AI小说列表详情 +// @Tags 移动端-AI小说列表 +// @Accept mpfd,json +// @Produce json +// @Param q query ai_text_to_novel_ser.AppQueryInfoReq false "请求参数" +// @Success 200 object ai_text_to_novel_ser.AppQueryInfoRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ai_text_to_novel/info [get] +func Info(ctx *gin.Context) { + p := &ai_text_to_novel_ser.AppQueryInfoReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + data, err := p.GetInfo() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + common.ServeJSON(ctx, stderr.Success, data) +} + +// Generate doc +// @Summary 生成AI小说订单记录 +// @Description 生成AI小说订单记录 +// @Tags 移动端-AI小说列表 +// @Accept json +// @Produce json +// @Param q body ai_text_to_novel_ser.GenerateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ai_text_to_novel/generate [post] +func Generate(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + in := ai_text_to_novel_ser.GenerateReq{} + if err = ctx.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("ai_text_to_novel Generate param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + in.UID = uid + ua, _ := common.GetUA(ctx) + ip := common.GetIP(ctx) + code, err := in.Generate(ua, ip) + if err != nil { + log.Error(fmt.Sprintf("ai_text_to_novel Generate err:%v\n", err)) + common.ServeJSON(ctx, code, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// Hide doc +// @Summary 删除AI小说记录 +// @Description 删除AI小说记录 +// @Tags 移动端-AI小说列表 +// @Accept json +// @Produce json +// @Param q body ai_text_to_novel_ser.HideReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ai_text_to_novel/hide [post] +func Hide(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + in := ai_text_to_novel_ser.HideReq{} + if err = ctx.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("ai_text_to_novel hide param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + in.UID = uid + err = in.Hide() + if err != nil { + log.Error(fmt.Sprintf("ai_text_to_novel hide err:%v\n", err)) + if code, ok := err.(stderr.Code); ok { + common.ServeJSON(ctx, code, code.Error()) + return + } + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} diff --git a/app/api/ai_undress_ctrl/ai_undress.go b/app/api/ai_undress_ctrl/ai_undress.go new file mode 100644 index 0000000..4f84766 --- /dev/null +++ b/app/api/ai_undress_ctrl/ai_undress.go @@ -0,0 +1,218 @@ +package ai_undress_ctrl + +import ( + "91porn-server/app/service/ai_changeface_img_ser" + "91porn-server/app/service/ai_undress_server" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/aiUnDressmod" + "91porn-server/models/v/aichangefaceimgmod" + "encoding/json" + "fmt" + "strconv" + + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary AI脱衣列表 +// @Description AI脱衣列表 +// @Tags AI脱衣 +// @Accept json +// @Produce json +// @Param status query int false "记录状态" +// @Param pageNumber query int true "第几页" +// @Param pageSize query int true "每页数量" +// @Success 200 object interface{} "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ai/undress/list [get] +func List(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + var req aiUnDressmod.ListRequest + if err = c.ShouldBindQuery(&req); err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + data, code := ai_undress_server.List(uid, &req) + if code != stderr.Success { + log.Error(fmt.Sprintf("ai_undress_service List error%v,uid%v", code.Error(), uid)) + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, data) +} + +// Generate doc +// @Summary 生成AI脱衣记录 +// @Description 生成AI脱衣记录 +// @Tags AI脱衣 +// @Accept json +// @Produce json +// @Param q body aiUnDressmod.GenerateRequest false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ai/undress/generate [post] +func Generate(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req aiUnDressmod.GenerateRequest + if err = ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("undress Generate param err%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + ua, _ := common.GetUA(ctx) + ip := common.GetIP(ctx) + code := ai_undress_server.Generate(uid, &req, ua, ip) + if code != stderr.Success { + log.Error(fmt.Sprintf("undress Generate err%v", code)) + common.ServeJSON(ctx, code, code.Error()) + return + } + updateLog, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(strconv.FormatUint(uid, 10), constant.AiUndressList, constant.Add, string(updateLog), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// AiImgList doc +// @Summary AI换脸列表 +// @Description AI换脸列表 +// @Tags AI图片换脸 +// @Accept json +// @Produce json +// @Param status query int false "记录状态" +// @Param pageNumber query int true "第几页" +// @Param pageSize query int true "每页数量" +// @Success 200 object aichangefaceimgmod.AiChangeFaceImg "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/ai/img/list [get] +func AiImgList(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + var req aichangefaceimgmod.ListRequest + if err = c.ShouldBindQuery(&req); err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + data, code := ai_changeface_img_ser.List(uid, &req) + if code != stderr.Success { + log.Error(fmt.Sprintf("ai_change_face_service List error%v,uid%v", code.Error(), uid)) + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, data) +} + +// AiImgGenerate doc +// @Summary 生成AI换脸记录 +// @Description 生成AI换脸记录 +// @Tags AI图片换脸 +// @Accept json +// @Produce json +// @Param q body aichangefaceimgmod.GenerateRequest false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/ai/img/generate [post] +func AiImgGenerate(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req aichangefaceimgmod.GenerateRequest + if err = ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("ai_change_face_img Generate param err%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + ua, _ := common.GetUA(ctx) + ip := common.GetIP(ctx) + code := ai_changeface_img_ser.Generate(uid, &req, ua, ip) + if code != stderr.Success { + log.Error(fmt.Sprintf("ai_change_face_img Generate err%v", code)) + common.ServeJSON(ctx, code, code.Error()) + return + } + updateLog, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(strconv.FormatUint(uid, 10), constant.AiChangeFaceImgList, constant.Add, string(updateLog), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// AiChangeFaceImgHide doc +// @Summary 删除AI换脸记录 +// @Description 删除AI换脸记录 +// @Tags AI图片换脸 +// @Accept json +// @Produce json +// @Param id formData string false "AI订单ID" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/ai/img/hide [post] +func AiChangeFaceImgHide(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req aichangefaceimgmod.DelRequest + if err = ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("ai_change_face_img del param err%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + code := ai_changeface_img_ser.AiChangeFaceImgHide(uid, &req) + if code != stderr.Success { + common.ServeJSON(ctx, code, code.Error()) + return + } + updateLog, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(strconv.FormatUint(uid, 10), constant.AiChangeFaceImgList, constant.Delete, string(updateLog), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// AiUndressHide doc +// @Summary 删除AI换脸记录 +// @Description 删除AI换脸记录 +// @Tags AI脱衣 +// @Accept json +// @Produce json +// @Param id formData string false "AI订单ID" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/ai/undress/hide [post] +func AiUndressHide(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req aiUnDressmod.DelRequest + if err = ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("undress del param err%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + code := ai_undress_server.AiUndressHide(uid, &req) + if code != stderr.Success { + common.ServeJSON(ctx, code, code.Error()) + return + } + updateLog, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(strconv.FormatUint(uid, 10), constant.AiUndressList, constant.Delete, string(updateLog), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} diff --git a/app/api/aiplazactrl/aiplaza.go b/app/api/aiplazactrl/aiplaza.go new file mode 100755 index 0000000..46f83a3 --- /dev/null +++ b/app/api/aiplazactrl/aiplaza.go @@ -0,0 +1,116 @@ +package aiplazactrl + +import ( + "91porn-server/app/service/aiplazaser" + "91porn-server/app/service/m3u8ticket" + "91porn-server/common" + "91porn-server/common/stderr" + "github.com/gin-gonic/gin" +) + +// List doc +// +// @Summary 获取ai广场帖子列表接口 +// @Description 获取ai广场帖子列表 +// @Tags 移动端-ai广场帖子 +// @Accept mpfd,json +// @Produce json +// @Param q query aiplazaser.AppQueryListReq false "请求参数" +// @Success 200 object aiplazaser.AppListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/aiplaza/list [get] +func List(ctx *gin.Context) { + p := &aiplazaser.AppQueryListReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + // 获取用户当前配置 + // var err error + // p.Uid, err = common.GetUID(ctx) + // if err != nil { + // common.ServeJSON(ctx, stderr.ErrNoToken, err) + // return + // } + + list := p.GetList() + uid := common.TryGetUID(ctx) + for i := range list.List { + if list.List[i] == nil { + continue + } + m3u8ticket.SignURL(ctx, uid, &list.List[i].OriginalVideo, true, false) + m3u8ticket.SignURL(ctx, uid, &list.List[i].GenerateVideo, true, false) + } + common.ServeJSON(ctx, stderr.Success, list) +} + +// Info doc +// +// @Summary 获取ai广场帖子详情接口 +// @Description 获取ai广场帖子详情 +// @Tags 移动端-ai广场帖子 +// @Accept mpfd,json +// @Produce json +// @Param q query aiplazaser.AppQueryInfoReq false "请求参数" +// @Success 200 object aiplazaser.AppQueryInfoResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/aiplaza/info [get] +func Info(ctx *gin.Context) { + p := &aiplazaser.AppQueryInfoReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + // 获取用户当前配置 + var err error + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + data, err := p.GetInfo(uid) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + if data.Detail != nil { + m3u8ticket.SignURL(ctx, uid, &data.Detail.OriginalVideo, true, false) + m3u8ticket.SignURL(ctx, uid, &data.Detail.GenerateVideo, true, false) + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// Share doc +// @Summary 分享ai记录到ai广场 +// @Description 分享ai记录到ai广场 +// @Tags 移动端-ai广场帖子 +// @Accept mpfd,json +// @Produce json +// @Param q body aiplazaser.ShareReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/aiplaza/share [post] +func Share(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + p := &aiplazaser.ShareReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 创建 + err = p.Create(uid) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/app/api/aitemplatemodulectrl/aitemplatemodule.go b/app/api/aitemplatemodulectrl/aitemplatemodule.go new file mode 100755 index 0000000..51fc8fd --- /dev/null +++ b/app/api/aitemplatemodulectrl/aitemplatemodule.go @@ -0,0 +1,88 @@ +package aitemplatemodulectrl + +import ( + "91porn-server/app/service/aitemplatemoduleser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// List doc +// +// @Summary 获取AI模版模块列表列表接口 +// @Description 获取AI模版模块列表列表 +// @Tags 移动端-AI模版模块列表 +// @Accept mpfd,json +// @Produce json +// @Param q query aitemplatemoduleser.AppQueryListReq false "请求参数" +// @Success 200 object aitemplatemoduleser.AppListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ai_template_module/list [get] +func List(ctx *gin.Context) { + p := &aitemplatemoduleser.AppQueryListReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + // 获取用户当前配置 + // var err error + // p.Uid, err = common.GetUID(ctx) + // if err != nil { + // common.ServeJSON(ctx, stderr.ErrNoToken, err) + // return + // } + + list := p.GetList() + common.ServeJSON(ctx, stderr.Success, list) +} + +// AllList doc +// +// @Summary 获取AI模版模块列表列表接口 +// @Description 获取AI模版模块列表列表 +// @Tags 移动端-AI模版模块列表 +// @Accept mpfd,json +// @Produce json +// @Param q query aitemplatemoduleser.AppQueryAllListReq false "请求参数" +// @Success 200 object aitemplatemoduleser.AppAllListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ai_template_module/all [get] +func AllList(ctx *gin.Context) { + p := &aitemplatemoduleser.AppQueryAllListReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + list := p.GetList() + common.ServeJSON(ctx, stderr.Success, list) +} + +// Info doc +// +// @Summary 获取AI模版模块列表详情接口 +// @Description 获取AI模版模块列表详情 +// @Tags 移动端-AI模版模块列表 +// @Accept mpfd,json +// @Produce json +// @Param q query aitemplatemoduleser.AppQueryInfoReq false "请求参数" +// @Success 200 object aitemplatemoduleser.AppQueryInfoRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ai_template_module/info [get] +func Info(ctx *gin.Context) { + p := &aitemplatemoduleser.AppQueryInfoReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + data, err := p.GetInfo() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/analyticsctrl/analytics.go b/app/api/analyticsctrl/analytics.go new file mode 100644 index 0000000..50eb880 --- /dev/null +++ b/app/api/analyticsctrl/analytics.go @@ -0,0 +1,30 @@ +package analyticsctrl + +import ( + "time" + + "91porn-server/app/service/vipcardexperimentser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +func Events(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + request := vipcardexperimentser.EventsRequest{} + if err = ctx.ShouldBindJSON(&request); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + response, err := vipcardexperimentser.RecordEvents(uid, request, time.Now()) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, response) +} diff --git a/app/api/annouctrl/annouctrl.go b/app/api/annouctrl/annouctrl.go new file mode 100644 index 0000000..3790374 --- /dev/null +++ b/app/api/annouctrl/annouctrl.go @@ -0,0 +1,64 @@ +package annouctrl + +import ( + "91porn-server/app/service/annouser" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/annoumod" + + "github.com/gin-gonic/gin" +) + +// GetAnnou doc +// @Summary 获取公告 +// @Description 获取公告 +// @Tags annou +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/annou/list [get] +func GetAnnou(ctx *gin.Context) { + if _, err := common.GetUID(ctx); err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + req := annoumod.PopReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + info, _ := annouser.GetAnnouList(req.Type) + infos := []*annouser.Annou{info} + common.ServeJSON(ctx, stderr.Success, infos) +} + +// GetAnnous doc +// @Summary 获取公告列表 +// @Description 获取消息模块公告列表 +// @Tags annou +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/annou/msg/list [get] +func GetAnnous(ctx *gin.Context) { + _, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + req := annoumod.MsgListReq{} + err1 := ctx.ShouldBind(&req) + if err1 != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + info, err := annouser.MsgAnnouList(req) + common.ServeJSON(ctx, stderr.Success, info) +} diff --git a/app/api/backpackctrl/backpack.go b/app/api/backpackctrl/backpack.go new file mode 100644 index 0000000..3059225 --- /dev/null +++ b/app/api/backpackctrl/backpack.go @@ -0,0 +1,51 @@ +package backpackctrl + +import ( + "91porn-server/app/service/backpackser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// GetCouponList doc +// +// @Summary 获取优惠券 +// @Description 获取优惠券 +// @Tags 移动端-优惠券 +// @Accept mpfd,json +// @Produce json +// +// @Param status formData integer false "物品状态 1-已使用 2-未使用 3-过期" +// @Param page formData integer false "当前页" +// @Param limit formData integer false "每页条数" +// @Param type formData integer false "1-楼风解锁折扣卷 2-会员折扣卷 3-AI换脸折扣券" +// +// @Success 200 object backpackmod.Backpack "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/backpack [get] +func GetCouponList(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + var params struct { + Status int `form:"status" binding:"required,min=1,max=3"` // 物品状态 + Page int64 `form:"page" binding:"required,min=1"` // 当前页 + Limit int64 `form:"limit" binding:"required,min=10,max=50"` // 每页条数 + Type int `form:"type" ` // 类型 + } + if err := c.ShouldBindQuery(¶ms); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err.Error()) + return + } + log.Info("GetJewelBoxPrize start", log.Any("uid", uid), log.Any("params", params)) + data, code := backpackser.GetCouponList(uid, params.Type, params.Status, params.Limit, params.Page) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, stderr.Success, data) +} diff --git a/app/api/checkinctrl/checkin.go b/app/api/checkinctrl/checkin.go new file mode 100644 index 0000000..9fc2451 --- /dev/null +++ b/app/api/checkinctrl/checkin.go @@ -0,0 +1,61 @@ +package checkinctrl + +import ( + "91porn-server/app/service/checkinser" + "91porn-server/app/service/m3u8ticket" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// UserCheckin 用户签到 +func UserCheckin(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + resp, code := checkinser.AddCheckin(uid) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + if resp != nil { + m3u8ticket.SignURL(c, uid, &resp.PrizeVideo, true, false) + } + common.ServeJSON(c, stderr.Success, resp) +} + +// GetCheckinPrize 获取签到奖品 +func GetCheckinPrize(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + resp, code := checkinser.GetCheckinPrizes(uid) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, stderr.Success, resp) +} + +// ClaimVipCheckinPrize 补领VIP签到奖励 +func ClaimVipCheckinPrize(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + resp, code := checkinser.ClaimVipCheckinPrizes(uid) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + if resp != nil { + m3u8ticket.SignURL(c, uid, &resp.PrizeVideo, true, false) + } + common.ServeJSON(c, stderr.Success, resp) +} diff --git a/app/api/commentctrl/commentctrl.go b/app/api/commentctrl/commentctrl.go new file mode 100644 index 0000000..5a801ff --- /dev/null +++ b/app/api/commentctrl/commentctrl.go @@ -0,0 +1,307 @@ +package commentctrl + +import ( + "91porn-server/app/service/commentser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/aiplazamod" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/noticerecdmod" + "time" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// List doc +// @Summary 评论模块 - 获取评论列表 +// @Description 用户评论 +// @Tags Comment +// @Accept mpfd,json +// @Produce json,html +// @Param objID query string true "评论对象的ID" +// @Param curTime query string true "打开评论列表的时间" +// @Param objType query string true "评论对象类型 video/cartoon/drama/AiPlaza" +// @Param pageNumber query integer true "当前页" mininum(1) +// @Param pageSize query integer true "每页条数" mininum(1) +// @Success 200 {object} cmtmod.ParentRespList "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/comment/list [get] +func List(ctx *gin.Context) { + uid, _ := common.GetUID(ctx) + type Info struct { + ObjID string `form:"objID" json:"objID" binding:"required"` //评论对象的ID + CurTime time.Time `form:"curTime" json:"curTime" binding:"required"` //打开评论列表的时间 + ObjType string `form:"objType" json:"objType"` // 评论对象类型 video:视频(默认) cartoon:动漫 AiPlaza:ai广场 + commod.Page + } + param := Info{} + err := ctx.ShouldBind(¶m) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + objID, err := primitive.ObjectIDFromHex(param.ObjID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + var ( + data = make(map[string]interface{}) + vCnt int64 = 0 + fCnt int64 = 0 + hasNext bool + code stderr.Code + ) + // 获取第一层评论总数,在获取第一页评论时返回总评论数 + if param.Page.PageNumber == 1 { + if param.ObjType == "" || param.ObjType == "video" { + code, vCnt, err := commentser.GetVideoTotalComments(objID) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + data["total"] = vCnt // 总评论条数 + } else if param.ObjType == cmtmod.OTypeCartoon || param.ObjType == cmtmod.OTypeDrama { + media, _ := mediamod.GetInfo(objID) + data["total"] = media.CountComment // 总评论条数 + } else if param.ObjType == "AiPlaza" { + // ai广场 + aiplaza, _ := aiplazamod.GetInfo(objID) + data["total"] = aiplaza.CommentCount // 总评论条数 + } + } else { + code, fCnt, err = commentser.GetTotalComments(objID, param.ObjType) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + } + code, list, hasNext, err := commentser.GetParentCmtList(uid, objID, param.ObjType, param.CurTime, param.Page) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + // 获取置顶快捷搜索 + if param.PageNumber == 1 { + list = commentser.QuickSearch(objID.Hex(), list) + if len(list) == 1 && vCnt == 0 { + data["total"] = 1 + fCnt = 1 + } + } + data["lfCount"] = fCnt // 一级评论条数 + data["hasNext"] = hasNext + data["list"] = list + common.ServeJSON(ctx, code, data) +} + +// Send doc +// @Summary 评论模块 - 发表评论 +// @Description 用户评论 +// @Tags Comment +// @Accept mpfd,json +// @Produce json,html +// @Param objType formData string true "评论对象类型 video:视频(默认) cartoon:动漫 AiPlaza:ai广场" +// @Param objID formData string true "评论对象的ID 帖子的ID" +// @Param cid formData string false "此评论是对某条评论的评论或回复 一级评论的ID,如果为空,则为对该视频的评论" +// @Param rid formData string false "被回复的评论id" +// @Param level formData integer false "评论层级 1:一级评论 2:二级评论" +// @Param toUserID formData integer false "对某用户回复评论 用户ID" +// @Param content formData string true "评论内容" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/comment/send [post] +func Send(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + param := cmtmod.PublishReqInfo{} + err = ctx.ShouldBind(¶m) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if param.Level != 1 && param.Level != 2 { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if param.Content == "" && param.Image == "" { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + ua, err := common.GetUA(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + ip := common.GetIP(ctx) + + code, data, err := commentser.PublishComment(uid, ua, ip, param) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + if code == stderr.CommentUserNotBind { + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, code, data) +} + +// NoVidSend doc +// @Summary 评论模块 - 发表评论,非视频帖子评论 +// @Description 用户评论 +// @Tags Comment +// @Accept mpfd,json +// @Produce json,html +// @Param objID formData string true "评论对象的ID 帖子的ID" +// @Param cid formData string false "此评论是对某条评论的评论或回复 一级评论的ID,如果为空,则为对该视频的评论" +// @Param rid formData string false "被回复的评论id" +// @Param level formData integer false "评论层级 1:一级评论 2:二级评论" +// @Param toUserID formData integer false "对某用户回复评论 用户ID" +// @Param content formData string true "评论内容" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /comment/sendV2 [post] +func NoVidSend(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + type Info struct { + CmtType string `form:"cmtType" json:"cmtType"` //评论类型,desire:愿望工单 + cmtmod.PublishReqInfo + } + param := Info{} + err = ctx.ShouldBind(¶m) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, data, err := commentser.NoVidPublishComment(uid, common.GetIP(ctx), param.CmtType, param.PublishReqInfo) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + if code == stderr.CommentUserNotBind { + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, code, data) +} + +// Info doc +// @Summary 评论模块 - 获取评论详情(获取二级评论) +// @Description 用户评论 +// @Tags Comment +// @Accept mpfd,json +// @Produce json,html +// @Param objID query string true "评论对象的ID" +// @Param cmtId query string true "评论id" +// @Param curTime query string true "打开评论列表的时间" +// @Param fstID query string true "默认展示的第一条二级评论的id" +// @Param pageNumber query integer true "当前页" mininum(1) +// @Param pageSize query integer true "每页条数" mininum(1) +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /comment/info [get] +func Info(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + type Info struct { + ObjID string `form:"objID" json:"objID" binding:"required"` //评论对象的ID + CmtID string `form:"cmtId" json:"cmtId" binding:"required"` //某条评论id,用于获取该评论下的评论 + FstID string `form:"fstID" json:"fstID" binding:"required"` //默认展示的第一条二级评论的id + CurTime time.Time `form:"curTime" json:"curTime" binding:"required"` //打开评论列表的时间 + commod.Page + } + param := Info{} + err = ctx.ShouldBind(¶m) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + objID, err := primitive.ObjectIDFromHex(param.ObjID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + cmtID, err := primitive.ObjectIDFromHex(param.CmtID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + fstID, err := primitive.ObjectIDFromHex(param.FstID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + var code stderr.Code + var data []cmtmod.ChildRespList + code, data, err = commentser.GetChildCmtList(uid, objID, cmtID, fstID, param.CurTime, param.Page) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + var hasNext bool + if len(data) > int(param.PageSize) { + hasNext = true + data = data[:param.PageSize] + } + common.ServeJSON(ctx, code, commod.ListResp{HasNext: hasNext, List: data}) +} + +// ReplyList doc +// @Summary 评论模块 - 回复列表 +// @Description 回复列表 +// @Tags Comment +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query integer true "当前页" mininum(1) +// @Param pageSize query integer true "每页条数" mininum(1) +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /comment/reply/list [get] +func ReplyList(c *gin.Context) { + var arg struct { + commod.Page + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "comment RecordList arg error "+err.Error()) + return + } + // 检查uid是否存在 + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.UserIsNotExists, "comment RecordList Context USER_ID is not exist ") + return + } + skip := (arg.PageNumber - 1) * arg.PageSize + limit := arg.PageSize + replyPage, err := commentser.ReplyPages(uid, int64(skip), int64(limit)) + if err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, err) + return + } + err = noticerecdmod.UpdateTrendReadTime(noticerecdmod.Cmet, uid, time.Now()) + if err != nil { + log.Error("commentctrl ReplyList UpdateTrendReadTime faild", log.E(err)) + } + common.ServeJSON(c, stderr.Success, replyPage) +} diff --git a/app/api/common.go b/app/api/common.go new file mode 100644 index 0000000..5d3fd85 --- /dev/null +++ b/app/api/common.go @@ -0,0 +1,5 @@ +package api + +func GetHasNext(pageSize int, pageNumber int, total int64) bool { + return pageSize*pageNumber < int(total) +} diff --git a/app/api/contentmarkerctrl/content_marker.go b/app/api/contentmarkerctrl/content_marker.go new file mode 100644 index 0000000..96e4e00 --- /dev/null +++ b/app/api/contentmarkerctrl/content_marker.go @@ -0,0 +1,34 @@ +package contentmarkerctrl + +import ( + "time" + + "91porn-server/app/appg" + "91porn-server/app/service/contentmarkerser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// Get doc +// @Summary 获取首页及亚模块内容更新时间 +// @Description 客户端根据更新时间与本地已读时间判断是否展示红点 +// @Tags 内容更新 +// @Produce json +// @Success 200 {object} contentmarkerser.Response +// @Router /api/app/content/update-markers [get] +func Get(ctx *gin.Context) { + var cache contentmarkerser.MarkerCache + if appg.Redis != nil { + cache = appg.Redis + } + data, err := contentmarkerser.GetCached(time.Now(), cache) + if err != nil { + log.Error("get content update markers failed", log.E(err)) + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/couponctl/couponctl.go b/app/api/couponctl/couponctl.go new file mode 100644 index 0000000..a26aacc --- /dev/null +++ b/app/api/couponctl/couponctl.go @@ -0,0 +1,136 @@ +package couponctl + +import ( + "91porn-server/app/service/couponser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/coupon_record_mod" + "91porn-server/models/v/prize_record_mod" + "fmt" + + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取优惠券 +// @Description 获取优惠券 +// @Tags +// @Accept mpfd,json +// @Produce json,html +// @Param type query integer true "优惠券类型" +// @Param pageNumber query integer true "查询页码" +// @Param pageSize query integer true "页码大小" +// @Success 200 {object} coupon_record_mod.QueryAllRes +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/coupon/list [get] +func List(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + var req coupon_record_mod.AppListReq + err = c.ShouldBindQuery(&req) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + data, err := couponser.List(uid, &req) + if err != nil { + common.ServeJSON(c, stderr.Failure, nil) + return + } + common.ServeJSON(c, stderr.Success, data) +} + +// Gain doc +// @Summary 上传用户信息 +// @Description 上传用户信息 +// @Tags +// @Accept mpfd,json +// @Produce json,html +// @Param type query integer true "优惠券类型" +// @Param pageNumber query integer true "查询页码" +// @Param pageSize query integer true "页码大小" +// @Success 200 {object} coupon_record_mod.QueryAllRes +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/luckyDraw/gain [get] +func Gain(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + + data, err := couponser.Gain(ctx, uid) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeToJSON(ctx, stderr.Success, data) +} + +// Upload doc +// @Summary 上传用户优惠券 +// @Description 上传用户优惠券 +// @Tags +// @Accept mpfd,json +// @Produce json,html +// @Param type query integer true "优惠券类型" +// @Param pageNumber query integer true "查询页码" +// @Param pageSize query integer true "页码大小" +// @Success 200 {object} coupon_record_mod.QueryAllRes +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/coupon/Upload [post] +func Upload(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + var req prize_record_mod.AppUploadReq + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + if err = couponser.Upload(uid, &req); err != nil { + log.Error(fmt.Sprintf("App couponser Upload err:%v", err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// Delete doc +// @Summary 删除用户优惠券 +// @Description 删除用户优惠券 +// @Tags +// @Accept mpfd,json +// @Produce json,html +// @Param type query integer true "优惠券类型" +// @Param pageNumber query integer true "查询页码" +// @Param pageSize query integer true "页码大小" +// @Success 200 {object} coupon_record_mod.QueryAllRes +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/coupon [delete] +func Delete(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + var req prize_record_mod.AppDeleteReq + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + if err = couponser.Delete(uid, &req); err != nil { + log.Error(fmt.Sprintf("App couponser Upload err:%v", err)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/app/api/customerCtrl/user.go b/app/api/customerCtrl/user.go new file mode 100644 index 0000000..11315e3 --- /dev/null +++ b/app/api/customerCtrl/user.go @@ -0,0 +1,119 @@ +package customerCtrl + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/customerser" + "91porn-server/common" + "91porn-server/common/crypt" + "91porn-server/common/log" + "91porn-server/common/stderr" + "encoding/hex" + "github.com/gin-gonic/gin" + "github.com/go-playground/form" + "net/url" +) + +// Url doc +// +// @Summary 获取客服链接 +// @Description 获取客服链接 +// @Tags 移动端-客服 +// @Accept mpfd,json +// @Produce json +// @Success 200 object string "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/customer/url [get] +func Url(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + ua, _ := common.GetUA(ctx) + resp, err := customerser.GetUrl(uid, ua) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, resp.Data.Url) +} + +func parseRequest(ctx *gin.Context, data interface{}) (err error) { + // 获取请求参数sign + sign := ctx.Query("sign") + decodeByte, err := hex.DecodeString(sign) + if err != nil { + log.Warn("非法请求1", log.Any("sign", sign), log.E(err)) + return + } + str, err := crypt.AesDecrypt(string(decodeByte), appg.Conf.Customer.Secret) + if err != nil { + log.Warn("非法请求2", log.Any("sign", sign), log.E(err)) + return + } + values, err := url.ParseQuery(str) + if err != nil { + log.Warn("非法请求3", log.Any("sign", sign), log.Any("str", str), log.E(err)) + return + } + // 解码到结构体 + decoder := form.NewDecoder() + err = decoder.Decode(data, values) + if err != nil { + log.Warn("参数解析错误", log.Any("values", values), log.E(err)) + return + } + return nil +} + +// Backpack doc +// +// @Summary 获取背包 +// @Description 获取背包 +// @Tags 移动端-客服 +// @Accept mpfd,json +// @Produce json +// @Param q query customerser.BackpackReq false "请求参数" +// @Success 200 object customerser.BackpackResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /customer/user/backpack [get] +func Backpack(ctx *gin.Context) { + p := &customerser.BackpackReq{} + if err := parseRequest(ctx, p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + resp, err := p.GetData(ctx, uint64(p.UserID), p.Account, p.Phone, p.InviteCode) + if err != nil { + common.ServeJSONNoEncrypt(ctx, stderr.Failure, err) + return + } + common.ServeJSONNoEncrypt(ctx, stderr.Success, resp) +} + +// Recharge doc +// +// @Summary 获取充值订单 +// @Description 获取充值订单 +// @Tags 移动端-客服 +// @Accept mpfd,json +// @Produce json +// @Param q query customerser.RechargeReq false "请求参数" +// @Success 200 object customerser.RechargeResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /customer/user/recharge [get] +func Recharge(ctx *gin.Context) { + p := &customerser.RechargeReq{} + if err := parseRequest(ctx, p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + resp, err := p.GetData(ctx, uint64(p.UserID)) + if err != nil { + common.ServeJSONNoEncrypt(ctx, stderr.Failure, err) + return + } + common.ServeJSONNoEncrypt(ctx, stderr.Success, resp) +} diff --git a/app/api/dramactrl/drama.go b/app/api/dramactrl/drama.go new file mode 100644 index 0000000..a5ba0a9 --- /dev/null +++ b/app/api/dramactrl/drama.go @@ -0,0 +1,203 @@ +package dramactrl + +import ( + "errors" + "time" + + "91porn-server/app/service/dramaser" + "91porn-server/app/service/m3u8ticket" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/walletmod" + + "github.com/gin-gonic/gin" +) + +// ChannelConfig doc +// @Summary 短剧频道配置 +// @Tags 移动端-短剧 +// @Success 200 {object} dramaser.ChannelConfig +// @Router /api/app/media/drama/channel/config [get] +func ChannelConfig(ctx *gin.Context) { + if _, err := common.GetUID(ctx); err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + data, err := dramaser.GetChannelConfig(time.Now()) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// Feed doc +// @Summary AI短剧沉浸式推荐Feed +// @Tags 移动端-短剧 +// @Param q query dramaser.FeedRequest true "请求参数" +// @Success 200 {object} dramaser.FeedResponse +// @Router /api/app/media/drama/feed [get] +func Feed(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req dramaser.FeedRequest + if err = ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, err := dramaser.GetFeed(ctx.Request.Context(), uid, req.PageSize, time.Now()) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + for i := range data.List { + m3u8ticket.SignURL(ctx, uid, &data.List[i].Content.VideoUrl, true, false) + m3u8ticket.SignURL(ctx, uid, &data.List[i].Content.H265Url, true, false) + m3u8ticket.SignURL(ctx, uid, &data.List[i].Content.AudioUrl, true, false) + m3u8ticket.SignURL(ctx, uid, &data.List[i].Content.PreviewVideoUrl, false, true) + m3u8ticket.SignURL(ctx, uid, &data.List[i].Content.PreviewH265Url, false, true) + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// List doc +// @Summary 热门短剧双列列表 +// @Tags 移动端-短剧 +// @Param q query dramaser.ListRequest true "请求参数" +// @Success 200 {object} dramaser.ListResponse +// @Router /api/app/media/drama/list [get] +func List(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req dramaser.ListRequest + if err = ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, err := dramaser.GetList(uid, req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// Topics doc +// @Summary 短剧专题列表 +// @Tags 移动端-短剧 +// @Success 200 {object} dramaser.TopicListResponse +// @Router /api/app/media/drama/topics [get] +func Topics(ctx *gin.Context) { + if _, err := common.GetUID(ctx); err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + data, err := dramaser.GetTopics(time.Now()) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// TopicWorks doc +// @Summary 短剧专题作品 +// @Tags 移动端-短剧 +// @Param q query dramaser.TopicWorksRequest true "请求参数" +// @Success 200 {object} dramaser.TopicWorksResponse +// @Router /api/app/media/drama/topic/works [get] +func TopicWorks(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req dramaser.TopicWorksRequest + if err = ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, err := dramaser.GetTopicWorks(uid, req, time.Now()) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// DownloadAuthorize doc +// @Summary 申请短剧单集下载并扣除下载次数 +// @Tags 移动端-短剧 +// @Param X-Request-ID header string true "幂等请求ID" +// @Param body body dramaser.DownloadAuthorizeRequest true "请求参数" +// @Success 200 {object} dramaser.DownloadAuthorizeResponse +// @Router /api/app/media/drama/download/authorize [post] +func DownloadAuthorize(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req dramaser.DownloadAuthorizeRequest + if err = ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, err := dramaser.AuthorizeDownload(ctx.Request.Context(), uid, ctx.GetHeader("X-Request-ID"), req, time.Now()) + if err != nil { + serveDownloadAuthorizeError(ctx, err) + return + } + m3u8ticket.SignURL(ctx, uid, &data.DownloadURL, true, false) + m3u8ticket.SignURL(ctx, uid, &data.H265DownloadURL, true, false) + common.ServeJSON(ctx, stderr.Success, data) +} + +func serveDownloadAuthorizeError(ctx *gin.Context, err error) { + switch { + case errors.Is(err, dramaser.ErrDramaEntitlementRequired): + common.ServeJsonWithExtra(ctx, stderr.ErrAccessForbid, + gin.H{"reason": "DRAMA_ENTITLEMENT_REQUIRED"}, gin.H{"msg": "", "tip": ""}) + case errors.Is(err, walletmod.ErrDownloadCountNotEnough): + common.ServeJsonWithExtra(ctx, stderr.DownloadCountIsNotEnough, + gin.H{"reason": "DOWNLOAD_COUNT_NOT_ENOUGH", "remainingDownloadCount": 0}, gin.H{"msg": "", "tip": ""}) + case errors.Is(err, walletmod.ErrDownloadRequestConflict): + common.ServeJSON(ctx, stderr.ErrInvalidRequest, err) + case errors.Is(err, dramaser.ErrDownloadResourceInvalid): + common.ServeJSON(ctx, stderr.ErrParamError, err) + default: + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err) + } +} + +// SaveEvents doc +// @Summary 批量上报短剧一期埋点 +// @Tags 移动端-短剧 +// @Param X-Request-ID header string true "幂等请求ID" +// @Param body body dramaser.EventsRequest true "请求参数" +// @Success 200 {object} dramaser.EventsResponse +// @Router /api/app/media/drama/events [post] +func SaveEvents(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req dramaser.EventsRequest + if err = ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, err := dramaser.SaveEvents(uid, ctx.GetHeader("X-Request-ID"), req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/exchcodectrl/exchcodectrl.go b/app/api/exchcodectrl/exchcodectrl.go new file mode 100644 index 0000000..0141f6f --- /dev/null +++ b/app/api/exchcodectrl/exchcodectrl.go @@ -0,0 +1,105 @@ +package exchcodectrl + +import ( + "fmt" + "net/http" + + "91porn-server/app/middleware/authuser" + "91porn-server/app/service/exchcodeser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + + "github.com/gin-gonic/gin" +) + +// CodeExchange doc +// @Summary 兑换码兑换 +// @Description 兑换码 +// @Tags ExchangeCode +// @Accept mpfd,json +// @Produce json,html +// @Param code formData string true "兑换码" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/code/exchange [post] +func CodeExchange(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + var param struct { + Code string `json:"code" binding:"required"` + } + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, data, err := exchcodeser.CodeExchange(uid, param.Code) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, data) +} + +// UserRecord doc +// @Summary 查询用户兑换记录 +// @Description 查询用户兑换记录 +// @Tags UserRecord +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData int true "页码" +// @Param pageSize formData int true "每页数据量" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/code/userRecord [get] +func UserRecord(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + var req struct { + commod.Page + } + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, data, err := exchcodeser.UserExchageRecord(uid, req.Page) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, data) +} + +func WebCodeExchange(ctx *gin.Context) { + token := ctx.Request.Header.Get("X-Authorization") //为兼容H5带APP token + if token == "" { + ctx.AbortWithStatusJSON(http.StatusOK, stderr.ErrNoToken.Struct()) + return + } + claims, err := authuser.ParseWebClaims(token) + if err != nil { + log.Error("activity CodeExchange ParseWebClaims error", log.Any("token", fmt.Sprintf("%+v", token)), log.E(err)) + ctx.JSON(http.StatusBadRequest, stderr.InvalidToken.Struct()) + return + } + var param struct { + Code string `json:"code" binding:"required"` + } + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, _, err := exchcodeser.CodeExchange(claims.UID, param.Code) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, gin.H{}) +} diff --git a/app/api/followctrl/followctrl.go b/app/api/followctrl/followctrl.go new file mode 100644 index 0000000..62acbfd --- /dev/null +++ b/app/api/followctrl/followctrl.go @@ -0,0 +1,149 @@ +package followctrl + +import ( + "91porn-server/app/service/followser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/followmod" + "91porn-server/models/v/noticerecdmod" + "github.com/gin-gonic/gin" + "time" +) + +// GetFollowList doc +// @Summary 获取关注列表 - 获取自己关注的用户 +// @Description 获取关注列表 +// @Tags 关注 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Param newsType formData string true "类型: SHORT:短视频博主 其他为空字符" +// @Success 200 {object} followmod.ListResp "{"list": [],"hasNext":false}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/follow/list [get] +func GetFollowList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + param := followmod.ListReq{} + err1 := ctx.ShouldBind(¶m) + if err1 != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if param.PageSize < 1 { + param.PageSize = 10 + } + if param.UID != 0 { + code, data := followser.GetHisFollowList(uid, param.UID, param.PageNumber, param.PageSize) + common.ServeJSON(ctx, code, data) + return + } + // 固定给20个下去不翻页(运营已经确定) + param.PageSize = 20 + param.PageNumber = 1 + + code, data := followser.GetFollowList(uid, param.PageNumber, param.PageSize, param.IsShort) + common.ServeJSON(ctx, code, data) +} + +// GetFansList doc +// @Summary 获取粉丝列表 - 获取我的粉丝 +// @Description 获取粉丝列表 +// @Tags 关注 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {object} followmod.ListResp "{"list": [],"hasNext":false}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/follow/fans/list [get] +func GetFansList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + param := followmod.ListReq{} + err1 := ctx.ShouldBind(¶m) + if err1 != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if param.PageSize < 1 { + param.PageSize = 10 + } + if param.UID != 0 { + code, data := followser.GetHisFansList(uid, param.UID, param.PageNumber, param.PageSize) + common.ServeJSON(ctx, code, data) + return + } + code, data := followser.GetFansList(uid, param.PageNumber, param.PageSize) + if code == stderr.Success { + err = noticerecdmod.UpdateTrendReadTime(noticerecdmod.Fans, uid, time.Now()) + if err != nil { + log.Error("followctrl GetFansList UpdateTrendReadTime faild", log.E(err)) + } + } + common.ServeJSON(ctx, code, data) +} + +// DynamicsList doc +// @Summary 获取关注用户发布的视频 +// @Description 获取关注用户发布的视频 +// @Tags 关注 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {object} followmod.DynamicsResp "{"list": [],"hasNext":false}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/follow/dynamics/list [get] +func DynamicsList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + param := followmod.AppDynamicsListReq{} + err1 := ctx.ShouldBind(¶m) + if err1 != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if param.PageSize < 1 { + param.PageSize = 10 + } + data, code := followser.GetDynamicsList(uid, param) + common.ServeJSON(ctx, code, data) +} + +// GetFollowUpUsersWithShort doc +// @Summary 获取关注用户发布的短视频 +// @Description 获取关注用户发布的短视频 (没有关注用户则返回推荐UP主) +// @Tags 关注 +// @Accept mpfd,json +// @Produce json,html +// @Param q query followser.GetFollowUpUsersWithShortReq true "请求参数" +// @Success 200 {object} followser.GetFollowUpUsersWithShortRep "success" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/follow/list/short [get] +func GetFollowUpUsersWithShort(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + param := followser.GetFollowUpUsersWithShortReq{} + err1 := ctx.ShouldBind(¶m) + if err1 != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + data, code := param.GetFollowUpUserListWithShort(uid) + common.ServeJSON(ctx, code, data) +} diff --git a/app/api/goldextractrl/goldextractrl.go b/app/api/goldextractrl/goldextractrl.go new file mode 100644 index 0000000..30cf5b4 --- /dev/null +++ b/app/api/goldextractrl/goldextractrl.go @@ -0,0 +1,58 @@ +package goldextractrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/goldextramod" + + "github.com/gin-gonic/gin" +) + +func UserGoldExtra(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + var goldExtraReq struct { + Type uint `form:"type" json:"type"` + commod.Page + } + if err := ctx.ShouldBind(&goldExtraReq); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + skip := goldExtraReq.Skip() + limit := goldExtraReq.Limit() + var userExtras []goldextramod.GoldExtra + switch goldExtraReq.Type { + case 0: // 返回所有 + userExtras, err = goldextramod.GetUserGoldExtra(nil, uid, skip, limit+1) + case 1: // 只返回有效 + userExtras, err = goldextramod.GetUserGoldExtraValid(nil, uid, skip, limit+1) + case 2: // 已过期 + userExtras, err = goldextramod.GetUserGoldExtraExpired(nil, uid, skip, limit+1) + case 3: // 已使用 + userExtras, err = goldextramod.GetUserGoldExtraUsed(nil, uid, skip, limit+1) + default: + common.ServeJSON(ctx, stderr.ErrParamError, "无效的type值") + return + } + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, "") + return + } + hasNext := false + if uint64(len(userExtras)) > limit { + userExtras = userExtras[:limit] + hasNext = true + } + common.ServeJSON(ctx, stderr.Success, struct { + List []goldextramod.GoldExtra `json:"list"` + HasNext bool `json:"hasNext"` + }{ + List: userExtras, + HasNext: hasNext, + }) +} diff --git a/app/api/health_check_ctrl/health.go b/app/api/health_check_ctrl/health.go new file mode 100644 index 0000000..fb316ab --- /dev/null +++ b/app/api/health_check_ctrl/health.go @@ -0,0 +1,38 @@ +package health_check_ctrl + +import ( + "91porn-server/app/service/health_check_ser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "fmt" + + "github.com/gin-gonic/gin" +) + +// Ping doc +// @Summary 查询服务健康检测记录 +// @Description 查询服务健康检测记录 +// @Tags 移动端-服务健康检测 +// @Accept json +// @Produce json +// @Param q body health_check_ser.PingReq false "请求参数" +// @Success 200 object health_check_ser.SystemStatus "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/health/ping [post] +func Ping(ctx *gin.Context) { + p := &health_check_ser.PingReq{} + if err := ctx.ShouldBind(&p); err != nil { + log.Error(fmt.Sprintf("health ping param err:%v", err)) + common.ServeJSONNoEncrypt(ctx, stderr.ErrParamError, nil) + return + } + + list, err := p.Ping() + if err != nil { + log.Error(fmt.Sprintf("health ping err:%v", err)) + common.ServeJSONNoEncrypt(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSONNoEncrypt(ctx, stderr.Success, list) +} diff --git a/app/api/hotspotctr/hotspot.go b/app/api/hotspotctr/hotspot.go new file mode 100644 index 0000000..0fe41e6 --- /dev/null +++ b/app/api/hotspotctr/hotspot.go @@ -0,0 +1,157 @@ +package hotspotctr + +import ( + "91porn-server/app/service/rankser" + "91porn-server/app/service/search" + "91porn-server/app/service/searcher" + "91porn-server/app/service/searcher/tonesearcher" + "91porn-server/app/service/searcher/vidhotsearcher" + "91porn-server/app/service/tagser" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + + "github.com/gin-gonic/gin" +) + +// HotTag +// @Summary 获取用户喜欢标签及标签下对应的视频列表(默认3个视频) +// @Description 热点,获取用户喜欢标签及标签下对应的视频列表(默认3个视频) +// @Tags 热点 +// @Accept json +// @Produce json +// @Param pageNumber formData integer true "页数" +// @Param pageSize formData integer true "每页条数" +// @Success 200 {object} tagser.TagGroupResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/hotspot/htag [get] +func HotTag(ctx *gin.Context) { + var arg struct { + commod.Page + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "hotspotctr HotTag arg error "+err.Error()) + return + } + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + param := commod.Page{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + resp := tagser.GetTagsList(uid, param) + common.ServeJSON(ctx, stderr.Success, resp) +} + +// Rank doc +// @Summary 热点 - rank +// @Description 获取排行和音色热点 +// @Tags 热点 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功" "data":{}}" +// @Failure 400 {string} json "{"msg": "操作失败" "data":{}}" +// @Router /api/app/hotspot/rank [get] +func Rank(ctx *gin.Context) { + rankMap, err := rankser.GetRankMap() + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + common.ServeJSON(ctx, stderr.Success, rankMap) +} + +// Tone doc +// @Summary 热点 - tone +// @Description 获取排行和音色热点 +// @Tags 热点 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功" "data":{}}" +// @Failure 400 {string} json "{"msg": "操作失败" "data":{}}" +// @Router /api/app/hotspot/area [get] +func Area(ctx *gin.Context) { + res, err := tonesearcher.NewToneSearcher().Search(nil, nil) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, err) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": res.Data(), + "hasNext": res.HasNext(), + }) +} + +// WonderTagList doc +// @Summary 热点 - 发现精彩 +// @Description 获取发现精彩标签列表 +// @Tags 热点 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query integer true "页码" +// @Param pageSize query integer true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/hotspot/wonder/list [get] +func WonderTagList(ctx *gin.Context) { + var arg struct { + commod.Page + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "hotspotctr WonderTagList arg error "+err.Error()) + return + } + skip := int64((arg.PageNumber - 1) * arg.PageSize) + limit := int64(arg.PageSize) + tags, hasNext, err := search.GetWonderTagList(skip, limit) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, "hotspotctr WonderTags error: "+err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": tags, + "hasNext": hasNext, + }) +} + +// HotVidList doc +// @Summary 热点 - 今日最热视屏 +// @Description 今日最热视屏 +// @Tags 热点 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query integer true "页码" +// @Param pageSize query integer true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "data":{}}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/hotspot/hotvid/list [get] +func HotVidList(ctx *gin.Context) { + var arg struct { + commod.Page + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "hotspotctr HotVidList arg error "+err.Error()) + return + } + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + opt := (&searcher.Option{}). + SetSkip(int64((arg.PageNumber - 1) * arg.PageSize)). + SetLimit(int64(arg.PageSize)) //最热视屏 + res, err := vidhotsearcher.NewVidHotSearcher(uid).Search(nil, opt) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": res.Data(), + "hasNext": res.HasNext(), + }) +} diff --git a/app/api/imctrl/ad.go b/app/api/imctrl/ad.go new file mode 100644 index 0000000..46aeaec --- /dev/null +++ b/app/api/imctrl/ad.go @@ -0,0 +1,71 @@ +package imctrl + +import ( + "strings" + + "91porn-server/app/service/imadser" + "91porn-server/common" + "91porn-server/common/enum/imad" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +type IMAdReq struct { + Position string `json:"position"` + Positions []string `json:"positions"` +} + +type IMAdResp struct { + Groups []imadser.AdPositionGroup `json:"groups"` +} + +func GetIMAd(ctx *gin.Context) { + if _, err := common.GetUID(ctx); err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + var req IMAdReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + positions := normalizeIMAdPositions(req) + if len(positions) == 0 { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + for _, position := range positions { + if !imad.IsPositionCode(position) { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + } + + common.ServeJSON(ctx, stderr.Success, &IMAdResp{ + Groups: imadser.GetAdsByPositionCodes(positions), + }) +} + +func normalizeIMAdPositions(req IMAdReq) []string { + positions := make([]string, 0, len(req.Positions)+1) + if position := strings.TrimSpace(req.Position); position != "" { + positions = append(positions, position) + } + positions = append(positions, req.Positions...) + seen := make(map[string]struct{}, len(positions)) + normalized := make([]string, 0, len(positions)) + for _, position := range positions { + position = strings.TrimSpace(position) + if position == "" { + continue + } + if _, ok := seen[position]; ok { + continue + } + seen[position] = struct{}{} + normalized = append(normalized, position) + } + return normalized +} diff --git a/app/api/imctrl/imctrl.go b/app/api/imctrl/imctrl.go new file mode 100644 index 0000000..5c92545 --- /dev/null +++ b/app/api/imctrl/imctrl.go @@ -0,0 +1,435 @@ +package imctrl + +import ( + "fmt" + "91porn-server/app/service/customerser" + "net/http" + + "91porn-server/app/middleware/authuser" + "91porn-server/app/service/imser" + "91porn-server/app/service/messageser" + "91porn-server/common" + "91porn-server/common/imclient" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/truthutil" + "91porn-server/models/v/messagemod" + "91porn-server/models/v/sourcemod" + + "github.com/gin-gonic/gin" +) + +var accepts = []int32{1000, 1001, 1002, 1003} + +const ( + live_default = "ys-01" + faqURL = "/kefu/api/faq/queryByAppId" + checkURL = "/kefu/api/play/unread" +) + +// GetImSign doc +// @Summary 获取Im签名 +// @Description 获取Im签名 +// @Tags IM +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/im/sign [get] +func GetImSign(ctx *gin.Context) { + token := ctx.Request.Header.Get("Authorization") //为兼容H5带APP token + if token == "" { + ctx.AbortWithStatusJSON(http.StatusOK, stderr.ErrNoToken.Struct()) + return + } + claims, err := authuser.ParseToken(token) + if err != nil { + log.Error("GetImSign ParseToken error", log.Any("token", fmt.Sprintf("%+v", token)), log.E(err)) + ctx.JSON(http.StatusBadRequest, stderr.InvalidToken.Struct()) + return + } + ua, _ := common.GetUA(ctx) + if !sourcemod.GetCustomerStat() { + common.ServeJSON(ctx, stderr.ErrCustomerBanned, nil) + return + } + sign := imser.GetSign(claims.UID, ua) + common.ServeJSON(ctx, stderr.Success, sign) +} + +// GetImSign doc +// @Summary 获取Im签名 +// @Description 获取Im签名 b 不需要token校验 +// @Tags IM +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/im/whiteSign [get] +func GetWhiteImSign(ctx *gin.Context) { + if !sourcemod.GetCustomerStat() { + common.ServeJSON(ctx, stderr.ErrCustomerBanned, nil) + return + } + var uid uint64 + var sign string + ua, _ := common.GetUA(ctx) + token := ctx.Request.Header.Get("Authorization") //token + if token != "" { + claims, err := authuser.ParseToken(token) + if err != nil { + log.Error("GetImSign ParseToken error", log.Any("token", fmt.Sprintf("%+v", token)), log.E(err)) + } + uid = claims.UID + } + if uid != 0 { + //sign = imser.GetSign(uid, ua) + res, err := customerser.GetUrl(uid, ua) + if err != nil { + log.Error("GetImSign GetUrl error", log.Any("uid", uid), log.E(err)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + + sign = "/app/newkefu?" + res.Data.Params + + } else if sign == "" { + sign = imser.GetWhiteSign() + } + common.ServeJSON(ctx, stderr.Success, sign) +} + +// ImSign doc +// @Summary 获取Im签名 +// @Description 获取Im签名 b 不需要token校验 +// @Tags IM +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/im/newSign [get] +func ImSign(ctx *gin.Context) { + if !sourcemod.GetCustomerStat() { + common.ServeJSON(ctx, stderr.ErrCustomerBanned, nil) + return + } + var uid uint64 + var sign string + ua, _ := common.GetUA(ctx) + token := ctx.Request.Header.Get("Authorization") //token + if token != "" { + claims, err := authuser.ParseToken(token) + if err != nil { + log.Error("GetImSign ParseToken error", log.Any("token", fmt.Sprintf("%+v", token)), log.E(err)) + } else { + uid = claims.UID + } + } + if uid != 0 { + sign = imser.GetSignNew(uid, ua) + } else if sign == "" { + sign = imser.GetWhiteSignNew() + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "sign": sign, + "faq": faqURL, + "check": checkURL, + "isVoiceActive": true, + }) +} + +func Token(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + if !imser.SDKEnabled() { + common.ServeJSON(ctx, stderr.Success, &imser.SDKAuthInfo{ + Enabled: false, + UserID: uid, + }) + return + } + data, err := imser.GetSDKAuth(uid) + if err != nil { + log.Warn("Get IM SDK token error", log.Any("uid", uid), log.E(err)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + if ua, uaErr := common.GetUA(ctx); uaErr == nil { + data.SysType = ua.SysType + } + common.ServeJSON(ctx, stderr.Success, data) +} + +type UserIDReq struct { + UserID uint64 `json:"userId"` + ImUserID int64 `json:"imUserId"` +} + +type UserIDResp struct { + Enabled bool `json:"enabled"` + UserID uint64 `json:"userId"` + ImUserID int64 `json:"imUserId"` +} + +func UserID(ctx *gin.Context) { + if _, err := common.GetUID(ctx); err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req UserIDReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if (req.UserID > 0) == (req.ImUserID > 0) { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if !imser.SDKEnabled() { + common.ServeJSON(ctx, stderr.Success, &UserIDResp{ + Enabled: false, + UserID: req.UserID, + ImUserID: req.ImUserID, + }) + return + } + resp := &UserIDResp{Enabled: true, UserID: req.UserID, ImUserID: req.ImUserID} + if req.UserID > 0 { + imUserID, err := imser.ResolveStoredIMUserID(req.UserID) + if err != nil { + log.Warn("resolve IM user id failed", log.Any("uid", req.UserID), log.E(err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + resp.ImUserID = imUserID + } else { + uid, err := imser.ResolveUIDByIMUserID(req.ImUserID) + if err != nil { + log.Warn("resolve user id failed", log.Any("imUserId", req.ImUserID), log.E(err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + resp.UserID = uid + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +type EnsureFriendReq struct { + PeerID uint64 `json:"peerId" binding:"required"` +} + +type EnsureFriendResp struct { + Enabled bool `json:"enabled"` + UserID uint64 `json:"userId"` + ImUserID int64 `json:"imUserId"` + PeerID uint64 `json:"peerId"` + PeerImUserID int64 `json:"peerImUserId"` + FriendAdded bool `json:"friendAdded"` +} + +func EnsureFriend(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req EnsureFriendReq + if err = ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if req.PeerID == 0 || req.PeerID == uid { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if !imser.SDKEnabled() { + common.ServeJSON(ctx, stderr.Success, &EnsureFriendResp{ + Enabled: false, + UserID: uid, + PeerID: req.PeerID, + }) + return + } + selfIMUserID, peerIMUserID, added, err := imser.EnsureFriendsBidirectional(uid, req.PeerID) + if err != nil { + log.Warn("ensure IM friend failed", log.Any("uid", uid), log.Any("peerId", req.PeerID), log.E(err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, &EnsureFriendResp{ + Enabled: true, + UserID: uid, + ImUserID: selfIMUserID, + PeerID: req.PeerID, + PeerImUserID: peerIMUserID, + FriendAdded: added, + }) +} + +type FriendListReq struct { + Now *int64 `json:"now,omitempty"` +} + +type FriendListResp struct { + Enabled bool `json:"enabled"` + UserID uint64 `json:"userId"` + ImUserID int64 `json:"imUserId"` + Now int64 `json:"now"` + NextNow int64 `json:"nextNow"` + Friends []imser.IMFriendItem `json:"friends"` +} + +func FriendList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req FriendListReq + _ = ctx.ShouldBindJSON(&req) + if !imser.SDKEnabled() { + common.ServeJSON(ctx, stderr.Success, &FriendListResp{ + Enabled: false, + UserID: uid, + Friends: []imser.IMFriendItem{}, + }) + return + } + var now int64 + if req.Now != nil { + now = *req.Now + } + imUserID, usedNow, nextNow, friends, err := imser.FriendList(uid, now) + if err != nil { + log.Warn("get IM friend list failed", log.Any("uid", uid), log.E(err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, &FriendListResp{ + Enabled: true, + UserID: uid, + ImUserID: imUserID, + Now: usedNow, + NextNow: nextNow, + Friends: friends, + }) +} + +type MessageHistoryReq struct { + PeerID uint64 `json:"peerId" binding:"required"` + StartTime *int64 `json:"startTime,omitempty"` + EndTime *int64 `json:"endTime,omitempty"` + StartSeq *int64 `json:"startSeq,omitempty"` + EndSeq *int64 `json:"endSeq,omitempty"` + Direction string `json:"direction,omitempty"` + Size int `json:"size,omitempty"` +} + +type MessageHistoryResp struct { + Enabled bool `json:"enabled"` + UserID uint64 `json:"userId"` + ImUserID int64 `json:"imUserId"` + PeerID uint64 `json:"peerId"` + PeerImUserID int64 `json:"peerImUserId"` + Messages []imser.IMHistoryMessage `json:"messages"` +} + +func MessageHistory(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req MessageHistoryReq + if err = ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if req.PeerID == 0 || req.PeerID == uid { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if !imser.SDKEnabled() { + common.ServeJSON(ctx, stderr.Success, &MessageHistoryResp{ + Enabled: false, + UserID: uid, + PeerID: req.PeerID, + Messages: []imser.IMHistoryMessage{}, + }) + return + } + historyReq := imclient.HistoryMessageRequest{ + Direction: req.Direction, + Size: req.Size, + } + if req.StartTime != nil { + historyReq.StartTime = *req.StartTime + } + if req.EndTime != nil { + historyReq.EndTime = *req.EndTime + } + if req.StartSeq != nil { + historyReq.StartSeq = *req.StartSeq + } + if req.EndSeq != nil { + historyReq.EndSeq = *req.EndSeq + } + selfIMUserID, peerIMUserID, messages, err := imser.HistoryMessages(uid, req.PeerID, historyReq) + if err != nil { + log.Warn("get IM message history failed", log.Any("uid", uid), log.Any("peerId", req.PeerID), log.E(err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, &MessageHistoryResp{ + Enabled: true, + UserID: uid, + ImUserID: selfIMUserID, + PeerID: req.PeerID, + PeerImUserID: peerIMUserID, + Messages: messages, + }) +} + +// SendMessage doc +// @Summary IM 发送私信 +// @Description 扣费 + 内容校验通过后,通过第三方 IM 平台投递;参数与 /app/message/priLetter/add 一致 +// @Tags IM +// @Accept json +// @Produce json +// @Param body body messagemod.AddMsgReqInfo true "私信参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/im/message/send [post] +func SendMessage(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var req messagemod.AddMsgReqInfo + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if len(req.ImgUrl) <= 0 && req.Content == "" { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + // 内容合规校验:失败时静默成功(与 priLetter/add 行为一致,避免泄露规则) + if req.Content != "" && !truthutil.CheckIsValid(req.Content, 1) { + log.Error("IM 私信内容校验不通过", log.Any("uid", uid), log.Any("content", req.Content)) + common.ServeJSON(ctx, stderr.Success, nil) + return + } + code := messageser.SendIMPrivateLetter(uid, req) + if code != stderr.Success { + log.Warn("imctrl SendMessage fail", + log.Any("uid", uid), log.Any("takeUid", req.TakeUid), log.Any("code", code)) + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/app/api/imgroupctrl/imgroup.go b/app/api/imgroupctrl/imgroup.go new file mode 100755 index 0000000..4984908 --- /dev/null +++ b/app/api/imgroupctrl/imgroup.go @@ -0,0 +1,101 @@ +package imgroupctrl + +import ( + "91porn-server/app/service/imgroupmemberser" + "91porn-server/app/service/imgroupser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// List doc +// +// @Summary 获取im群组列表接口 +// @Description 获取im群组列表 +// @Tags 移动端-im群组 +// @Accept mpfd,json +// @Produce json +// @Param q query imgroupser.AppQueryListReq false "请求参数" +// @Success 200 object imgroupser.AppListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/imgroup/list [get] +func List(ctx *gin.Context) { + p := &imgroupser.AppQueryListReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + // 获取用户当前配置 + // var err error + // p.Uid, err = common.GetUID(ctx) + // if err != nil { + // common.ServeJSON(ctx, stderr.ErrNoToken, err) + // return + // } + + list := p.GetList() + common.ServeJSON(ctx, stderr.Success, list) +} + +// Info doc +// +// @Summary 获取im群组详情接口 +// @Description 获取im群组详情 +// @Tags 移动端-im群组 +// @Accept mpfd,json +// @Produce json +// @Param q query imgroupser.AppQueryInfoReq false "请求参数" +// @Success 200 object imgroupser.AppQueryInfoRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/imgroup/info [get] +func Info(ctx *gin.Context) { + p := &imgroupser.AppQueryInfoReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + data, err := p.GetInfo() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + common.ServeJSON(ctx, stderr.Success, data) +} + +// HasJoin doc +// +// @Summary 获取是否加入群组接口 +// @Description 获取是否加入群组详情 +// @Tags 移动端-im群组 +// @Accept mpfd,json +// @Produce json +// @Param q query imgroupmemberser.HasJoinReq false "请求参数" +// @Success 200 object imgroupmemberser.HasJoinRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/imgroup/hasjoin [get] +func HasJoin(ctx *gin.Context) { + p := &imgroupmemberser.HasJoinReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + // 获取用户当前配置 + var err error + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + data, err := p.GetInfo(uid) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/imgroupmemberctrl/imgroupmember.go b/app/api/imgroupmemberctrl/imgroupmember.go new file mode 100755 index 0000000..7af87f8 --- /dev/null +++ b/app/api/imgroupmemberctrl/imgroupmember.go @@ -0,0 +1,39 @@ +package imgroupmemberctrl + +import ( + "91porn-server/app/service/imgroupmemberser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// List doc +// +// @Summary 获取im群组成员列表接口 +// @Description 获取im群组成员列表 +// @Tags 移动端-im群组成员 +// @Accept mpfd,json +// @Produce json +// @Param q query imgroupmemberser.AppQueryListReq false "请求参数" +// @Success 200 object imgroupmemberser.AppListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/imgroupmember/list [get] +func List(ctx *gin.Context) { + p := &imgroupmemberser.AppQueryListReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + // 获取用户当前配置 + // var err error + // p.Uid, err = common.GetUID(ctx) + // if err != nil { + // common.ServeJSON(ctx, stderr.ErrNoToken, err) + // return + // } + + list := p.GetList() + common.ServeJSON(ctx, stderr.Success, list) +} diff --git a/app/api/immessagectrl/immessage.go b/app/api/immessagectrl/immessage.go new file mode 100755 index 0000000..e29bd4c --- /dev/null +++ b/app/api/immessagectrl/immessage.go @@ -0,0 +1,74 @@ +package immessagectrl + +import ( + "91porn-server/app/service/immessageser" + "91porn-server/common" + "91porn-server/common/stderr" + "github.com/gin-gonic/gin" +) + +// List doc +// +// @Summary 获取im消息列表接口 +// @Description 获取im消息列表 +// @Tags 移动端-im消息 +// @Accept mpfd,json +// @Produce json +// @Param q query immessageser.AppQueryListReq false "请求参数" +// @Success 200 object immessageser.AppListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/immessage/list [get] +func List(ctx *gin.Context) { + p := &immessageser.AppQueryListReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + // 获取用户当前配置 + // var err error + // p.Uid, err = common.GetUID(ctx) + // if err != nil { + // common.ServeJSON(ctx, stderr.ErrNoToken, err) + // return + // } + + list := p.GetList() + common.ServeJSON(ctx, stderr.Success, list) +} + +// Send doc +// @Summary 发送im消息 +// @Description 发送im消息 +// @Tags 移动端-im消息 +// @Accept mpfd,json +// @Produce json +// @Param q body immessageser.SendReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/immessage/send [post] +func Send(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + p := &immessageser.SendReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if p.Content == "" && p.Image == "" { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + // 创建 + err = p.Create(uid) + if err != nil { + common.ServeError(ctx, err) + return + } + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/app/api/infmtctrl/infmtctrl.go b/app/api/infmtctrl/infmtctrl.go new file mode 100644 index 0000000..26e1af8 --- /dev/null +++ b/app/api/infmtctrl/infmtctrl.go @@ -0,0 +1,90 @@ +package infmtctrl + +import ( + "time" + + "91porn-server/app/service/infmtser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/noticefmtmod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ObjectID +type ObjectID = primitive.ObjectID + +// NoticeList doc +// @Summary 消息模块 - 预览 +// @Description 动态预览和通知预览 +// @Tags Information +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功" "data": infmtser.NoticePage }" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/inform/preview [get] +func Preview(c *gin.Context) { + // 检查uid是否存在 + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.UserIsNotExists, "Notice List Context USER_ID is not exist ") + return + } + noticePreviewList, hasNewNotice, err := infmtser.UpdateNoticePreviewList(uid, time.Now()) + if err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, err) + return + } + trendPreMap, hasNewTrend := infmtser.TrendPreviewMap(uid) + common.ServeJSON(c, stderr.Success, gin.H{ + "noticePreList": noticePreviewList, + "trendPreMap": trendPreMap, + "hasNew": hasNewNotice || hasNewTrend, + "updatedAt": time.Now(), + }) +} + +// NoticeList doc +// @Summary 消息模块 - 通知队列 +// @Description 获取通知并同步通知状态 +// @Tags Information +// @Accept mpfd,json +// @Produce json,html +// @Param sender formData string true "消息发送者" +// @Param pageNumber formData int true "当前页" +// @Param pageSize formData int true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功" "data": infmtser.MailPage }" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/inform/notice/list [get] +func NoticeList(c *gin.Context) { + var arg struct { + Sender noticefmtmod.Sender `form:"sender" json:"sender" binding:"required"` //消息发送者 活动助手、系统消息 + commod.Page + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "Notice List arg error "+err.Error()) + return + } + // 检查uid是否存在 + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.UserIsNotExists, "Notice List Context USER_ID is not exist ") + return + } + skip := (arg.PageNumber - 1) * arg.PageSize + limit := arg.PageSize + noticePage, err := infmtser.NoticePages(arg.Sender, uid, int64(skip), int64(limit)) + if err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, err) + return + } + common.Go(func() { + if err = infmtser.UpdateNoticeReadTime(arg.Sender, uid, time.Now()); err != nil { + log.Error("UpdateNoticeReadTime faild", log.E(err)) + } + }) + common.ServeJSON(c, stderr.Success, noticePage) +} diff --git a/app/api/integeralctrl/integral.go b/app/api/integeralctrl/integral.go new file mode 100644 index 0000000..dffc166 --- /dev/null +++ b/app/api/integeralctrl/integral.go @@ -0,0 +1,75 @@ +package integeralctrl + +import ( + "91porn-server/app/service/integral_config_ser" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/integralconfigmod" + + "github.com/gin-gonic/gin" +) + +// ExchangeIntegral doc +// @Summary 积分兑换 +// @Description 积分兑换 +// @Tags 积分配置 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "积分兑换配置ID" +// @Param name formData string false "兑换用户姓名" +// @Param tel formData string false "积分兑换用户电话" +// @Param address formData string false "兑换用户地址" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/integral/exchangeIntegral [post] +func ExchangeIntegral(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var in integralconfigmod.ExchangeIntegralReq + if err = ctx.ShouldBind(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code := integral_config_ser.ExchangeIntegral(uid, &in) + if code != stderr.Success { + common.ServeJSON(ctx, code, code.Error()) + return + } + common.ServeJSON(ctx, code, stderr.Success.Msg()) +} + +// GetList doc +// @Summary 积分兑换列表 +// @Description 积分兑换列表 +// @Tags 积分配置 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/integral/list [get] +func GetList(c *gin.Context) { + data := integral_config_ser.GetAllConfig() + common.ServeJSON(c, stderr.Success, data) +} + +// GetRecordList doc +// @Summary 积分兑换记录列表 +// @Description 积分兑换记录列表 +// @Tags 积分配置 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 object integralconfigmod.AppIntegralRecord "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/integral/record/list [get] +func GetRecordList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + data := integral_config_ser.GetRecordConfig(uid) + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/likectrl/likectrl.go b/app/api/likectrl/likectrl.go new file mode 100644 index 0000000..1797470 --- /dev/null +++ b/app/api/likectrl/likectrl.go @@ -0,0 +1,116 @@ +package likectrl + +import ( + "time" + + "91porn-server/app/service/likeser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/likemod" + "91porn-server/models/v/noticerecdmod" + + "github.com/gin-gonic/gin" +) + +// ThumbsUp doc +// @Summary 点赞 - 视频/评论点赞 +// @Description 用户操作 +// @Tags 点赞 +// @Accept mpfd,json +// @Produce json,html +// @Param q body likemod.ReqInfo true "参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /thumbsUp [post] +func ThumbsUp(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + param := likemod.ReqInfo{} + err = ctx.ShouldBind(¶m) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + code, err := likeser.ThumbsUp(ctx, uid, param.Type, param.ObjID, param.TagID) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, nil) +} + +// ThumbsDown doc +// @Summary 取消点赞 - 视频/评论取消点赞 +// @Description 用户操作 +// @Tags 点赞 +// @Accept mpfd,json +// @Produce json,html +// @Param q query likemod.DesLikeReq true "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /thumbsDown [post] +func ThumbsDown(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + param := likemod.DesLikeReq{} + err = ctx.ShouldBind(¶m) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + code, err := likeser.ThumbsDown(ctx, uid, param.Type, param.ObjIDs...) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, nil) +} + +// Record doc +// @Summary 被赞列表 +// @Description 被赞列表 +// @Tags 点赞 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query integer true "当前页" mininum(1) +// @Param pageSize query integer true "每页条数" mininum(1) +// @Success 200 {object} likeser.RecordPage "{"hasNext": true,"list":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/like/record/list [get] +func RecordList(c *gin.Context) { + var arg struct { + commod.Page + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "likectrl RecordList arg error "+err.Error()) + return + } + // 检查uid是否存在 + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.UserIsNotExists, "likectrl RecordList Context USER_ID is not exist ") + return + } + skip := (arg.PageNumber - 1) * arg.PageSize + limit := arg.PageSize + recordPage, err := likeser.RecordPages(uid, int64(skip), int64(limit)) + if err != nil { + common.ServeJSON(c, stderr.Failure, err) + return + } + err = noticerecdmod.UpdateTrendReadTime(noticerecdmod.Like, uid, time.Now()) + if err != nil { + log.Error("likectrl RecordList UpdateTrendReadTime faild", log.E(err)) + } + common.ServeJSON(c, stderr.Success, recordPage) +} diff --git a/app/api/mediabookshelfctrl/mediabookshelf.go b/app/api/mediabookshelfctrl/mediabookshelf.go new file mode 100755 index 0000000..5fc86bb --- /dev/null +++ b/app/api/mediabookshelfctrl/mediabookshelf.go @@ -0,0 +1,145 @@ +package mediabookshelfctrl + +import ( + "91porn-server/app/service/mediabookshelfser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取媒体书架列表接口 +// @Description 获取媒体书架列表 +// @Tags 移动端-媒体书架列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediabookshelfser.AppQueryListReq false "请求参数" +// @Success 200 object mediabookshelfser.AppListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media_bookshelf/list [get] +func List(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + p := &mediabookshelfser.AppQueryListReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + list, err := p.GetList(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, list) +} + +// Add doc +// @Summary 添加媒体进入书架接口 +// @Description 添加媒体进入书架 +// @Tags 移动端-媒体书架列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediabookshelfser.AppAddBookshelfReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media_bookshelf/add [post] +func Add(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + p := &mediabookshelfser.AppAddBookshelfReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + ua, err := common.GetUA(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + ip := common.GetIP(ctx) + + err = p.Add(uid, ua, ip) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// Del doc +// @Summary 删除书架中媒体接口 +// @Description 删除书架中媒体 +// @Tags 移动端-媒体书架列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediabookshelfser.AppDelBookshelfReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media_bookshelf/del [post] +func Del(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + p := &mediabookshelfser.AppDelBookshelfReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + ua, err := common.GetUA(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + ip := common.GetIP(ctx) + + err = p.Del(uid, ua, ip) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// DelBatch doc +// @Summary 批量删除书架中媒体接口 +// @Description 批量删除书架中媒体 +// @Tags 移动端-媒体书架列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediabookshelfser.AppDelBatchBookshelfReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media_bookshelf/del/batch [post] +func DelBatch(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + p := &mediabookshelfser.AppDelBatchBookshelfReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + err = p.Del(uid) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} diff --git a/app/api/mediacontentctrl/mediacontent.go b/app/api/mediacontentctrl/mediacontent.go new file mode 100755 index 0000000..dbb40b5 --- /dev/null +++ b/app/api/mediacontentctrl/mediacontent.go @@ -0,0 +1,94 @@ +package mediacontentctrl + +import ( + "91porn-server/app/service/m3u8ticket" + "91porn-server/app/service/mediacontentser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取动漫内容列表列表接口 +// @Description 获取动漫内容列表列表 +// @Tags 移动端-动漫内容列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediacontentser.AppQueryListReq false "请求参数" +// @Success 200 object mediacontentser.AppListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media_content/list [get] +func List(ctx *gin.Context) { + p := &mediacontentser.AppQueryListReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + // 获取用户当前配置 + // var err error + // p.Uid, err = common.GetUID(ctx) + // if err != nil { + // common.ServeJSON(ctx, stderr.ErrNoToken, err) + // return + // } + // 获取用户当前配置 + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + list, err := p.GetList(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + // H5 防盗链:逐条为动漫/有声内容的 m3u8 地址签票(非 H5/未开启时零副作用)。 + for i := range list.List { + m3u8ticket.SignURL(ctx, uid, &list.List[i].VideoUrl, true, false) + m3u8ticket.SignURL(ctx, uid, &list.List[i].H265Url, true, false) + m3u8ticket.SignURL(ctx, uid, &list.List[i].AudioUrl, true, false) + m3u8ticket.SignURL(ctx, uid, &list.List[i].PreviewVideoUrl, false, true) + m3u8ticket.SignURL(ctx, uid, &list.List[i].PreviewH265Url, false, true) + } + common.ServeJSON(ctx, stderr.Success, list) +} + +// Info doc +// +// @Summary 获取动漫内容列表详情接口 +// @Description 获取动漫内容列表详情 +// @Tags 移动端-动漫内容列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediacontentser.AppQueryInfoReq false "请求参数" +// @Success 200 object mediacontentser.MediaContentInfo "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media_content/info [get] +func Info(ctx *gin.Context) { + p := &mediacontentser.AppQueryInfoReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + // 获取用户当前配置 + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + data, err := p.GetInfo(uid) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + // H5 防盗链:为动漫/有声详情的 m3u8 地址签票(非 H5/未开启时零副作用)。 + m3u8ticket.SignURL(ctx, uid, &data.VideoUrl, true, false) + m3u8ticket.SignURL(ctx, uid, &data.H265Url, true, false) + m3u8ticket.SignURL(ctx, uid, &data.AudioUrl, true, false) + m3u8ticket.SignURL(ctx, uid, &data.PreviewVideoUrl, false, true) + m3u8ticket.SignURL(ctx, uid, &data.PreviewH265Url, false, true) + + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/mediactrl/media.go b/app/api/mediactrl/media.go new file mode 100755 index 0000000..d071f7f --- /dev/null +++ b/app/api/mediactrl/media.go @@ -0,0 +1,295 @@ +package mediactrl + +import ( + "91porn-server/app/service/mediaser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/mediamod" + "fmt" + "github.com/gin-gonic/gin" +) + +// List doc +// +// @Summary 获取动漫列表列表接口 +// @Description 获取动漫列表列表 +// @Tags 移动端-动漫列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaser.AppQueryListReq false "请求参数" +// @Success 200 object mediaser.AppListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media/list [get] +func List(ctx *gin.Context) { + p := &mediaser.AppQueryListReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + // 获取用户当前配置 + // var err error + // p.Uid, err = common.GetUID(ctx) + // if err != nil { + // common.ServeJSON(ctx, stderr.ErrNoToken, err) + // return + // } + + list := p.GetList() + common.ServeJSON(ctx, stderr.Success, list) +} + +// Info doc +// @Summary 获取动漫列表详情接口 +// @Description 获取动漫列表详情 +// @Tags 移动端-动漫列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaser.AppQueryInfoReq false "请求参数" +// @Success 200 object mediamod.AppMediaBase "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media/info [get] +func Info(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + p := &mediaser.AppQueryInfoReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + data, err := p.GetInfo(uid) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + common.ServeJSON(ctx, stderr.Success, data) +} + +// Library doc +// @Summary 获取动漫片库接口 +// @Description 获取动漫片库详情 +// @Tags 移动端-动漫列表 +// @Accept mpfd,json +// @Produce json +// @Success 200 object mediamod.AppLibrary "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media/library [get] +func Library(ctx *gin.Context) { + data, err := mediaser.GetLibrary() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// LibrarySearch doc +// @Summary 动漫片库搜索接口 +// @Description 动漫片库搜索详情 +// @Tags 移动端-动漫列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediamod.AppLibraryReq false "请求参数" +// @Success 200 object mediamod.AppElasticSearchLibraryResponse "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media/library/search [post] +func LibrarySearch(ctx *gin.Context) { + req := mediamod.AppLibraryReq{} + if err := ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("media librarySearch param err:%v", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, err := mediaser.LibrarySearch(req) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// Search doc +// @Summary 动漫片库搜索接口 +// @Description 动漫片库搜索详情 +// @Tags 移动端-动漫列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediamod.AppSearchReq false "请求参数" +// @Success 200 object mediamod.AppElasticSearchResponse "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media/search [get] +func Search(ctx *gin.Context) { + req := mediamod.AppSearchReq{} + if err := ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("media Search param err:%v", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + uid, _ := common.GetUID(ctx) + data, err := mediaser.Search(uid, req) + if err != nil { + log.Error(fmt.Sprintf("media Search err:%v", err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// GetTopicList doc +// @Summary 更多-动漫专题列表 +// @Description 动漫专题列表 +// @Tags 移动端-动漫专题 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaser.AppGetTopicReq false "请求参数" +// @Success 200 object mediaser.AppGetTopicRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media/topic [get] +func GetTopicList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + p := &mediaser.AppGetTopicReq{} + if err := ctx.ShouldBind(&p); err != nil { + log.Error(fmt.Sprintf("GetTopicList param err:%v", err)) + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + list, err := p.GetTopicList(uid) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + + common.ServeJSON(ctx, stderr.Success, list) +} + +// Recommend doc +// @Summary 动漫详情-推荐列表 +// @Description 动漫详情-推荐列表 +// @Tags 移动端-动漫推荐 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaser.AppRecommendReq false "请求参数" +// @Success 200 object mediaser.AppRecommendRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media/recommend [get] +func Recommend(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + p := &mediaser.AppRecommendReq{} + if err := ctx.ShouldBind(&p); err != nil { + log.Error(fmt.Sprintf("Recommend param err:%v", err)) + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + list, err := p.GetList(uid) + if err != nil { + log.Error(fmt.Sprintf("Recommend GetList err:%v", err)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + + common.ServeJSON(ctx, stderr.Success, list) +} + +// Ranking doc +// @Summary 动漫排行榜 +// @Description 动漫排行榜 +// @Tags 移动端-动漫排行榜 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaser.RankingReq false "请求参数" +// @Success 200 object mediaser.RankingResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media/ranking [get] +func Ranking(ctx *gin.Context) { + p := &mediaser.RankingReq{} + if err := ctx.ShouldBind(&p); err != nil { + log.Error(fmt.Sprintf("Rank param err:%v", err)) + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + res, err := p.List() + if err != nil { + log.Error(fmt.Sprintf("Get Ranking List err:%v", err)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Hot doc +// @Summary 热门动漫 +// @Description 热门动漫 +// @Tags 移动端-动漫 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaser.HotReq false "请求参数" +// @Success 200 object mediaser.HotResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media/hot [get] +func Hot(ctx *gin.Context) { + p := &mediaser.HotReq{} + if err := ctx.ShouldBind(&p); err != nil { + log.Error(fmt.Sprintf("Hot param err:%v", err)) + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + resp, err := p.List() + if err != nil { + log.Error(fmt.Sprintf("Get Hot Media List err:%v", err)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + + common.ServeJSON(ctx, stderr.Success, resp) +} + +// MyBuy doc +// @Summary 我的购买 +// @Description 我的购买 +// @Tags 移动端-我的购买 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaser.MyBuyReq false "请求参数" +// @Success 200 object mediaser.MyBuyResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media/my_buy [get] +func MyBuy(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + p := &mediaser.MyBuyReq{} + if err := ctx.ShouldBind(&p); err != nil { + log.Error(fmt.Sprintf("MyBuy param err:%v", err)) + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + resp, err := p.List(uid) + if err != nil { + log.Error(fmt.Sprintf("Get MyBuy Media List err:%v", err)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} diff --git a/app/api/mediatagctrl/mediatag.go b/app/api/mediatagctrl/mediatag.go new file mode 100755 index 0000000..f1ff1fa --- /dev/null +++ b/app/api/mediatagctrl/mediatag.go @@ -0,0 +1,65 @@ +package mediatagctrl + +import ( + "91porn-server/app/service/mediatagser" + "91porn-server/common" + "91porn-server/common/stderr" + "github.com/gin-gonic/gin" +) + +// List doc +// +// @Summary 获取动漫标签列表接口 +// @Description 获取动漫标签列表 +// @Tags 移动端-动漫标签 +// @Accept mpfd,json +// @Produce json +// @Param q query mediatagser.AppQueryListReq false "请求参数" +// @Success 200 object mediatagser.AppListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media_tag/list [get] +func List(ctx *gin.Context) { + p := &mediatagser.AppQueryListReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + // 获取用户当前配置 + // var err error + // p.Uid, err = common.GetUID(ctx) + // if err != nil { + // common.ServeJSON(ctx, stderr.ErrNoToken, err) + // return + // } + + list := p.GetList() + common.ServeJSON(ctx, stderr.Success, list) +} + +// Info doc +// +// @Summary 获取动漫标签详情接口 +// @Description 获取动漫标签详情 +// @Tags 移动端-动漫标签 +// @Accept mpfd,json +// @Produce json +// @Param q query mediatagser.AppQueryInfoReq false "请求参数" +// @Success 200 object mediatagmod.MediaTag "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/media_tag/info [get] +func Info(ctx *gin.Context) { + p := &mediatagser.AppQueryInfoReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + data, err := p.GetInfo() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/messagectrl/messagectrl.go b/app/api/messagectrl/messagectrl.go new file mode 100644 index 0000000..1f45738 --- /dev/null +++ b/app/api/messagectrl/messagectrl.go @@ -0,0 +1,283 @@ +package messagectrl + +import ( + "91porn-server/app/service/messageser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/truthutil" + "91porn-server/models/v/messagemod" + "91porn-server/models/v/sessionmod" + "fmt" + + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 消息 +// @Description 动态列表 +// @Tags 私信模块 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Param msgType formData string true "消息类型:comment_msg 评论" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/msg/dynamic/list [get] +func List(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var in *messagemod.QueryCond + if err := ctx.ShouldBind(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, has, code := messageser.QueryDynamics(uid, in) + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + resp := make(map[string]interface{}) + resp["list"] = data + resp["hasNext"] = has + common.ServeJSON(ctx, stderr.Success, resp) +} + +// NoRedNum doc +// @Summary 消息 +// @Description 动态列表 +// @Tags 私信模块 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/msg/dynamic/noRedNum [get] +func NoRedNum(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + data, code := messageser.QueryNoRedDynamicNum(uid) + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// SessionList doc +// @Summary 消息 +// @Description 会话列表 +// @Tags 私信模块 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/msg/session/list [get] +func SessionList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var in *sessionmod.QueryCond + if err := ctx.ShouldBind(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, has, code := messageser.QueryPrivateLetterSession(uid, in) + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + resp := make(map[string]interface{}) + resp["list"] = data + resp["hasNext"] = has + common.ServeJSON(ctx, stderr.Success, resp) +} + +// DelSession doc +// @Summary 消息 +// @Description 删除会话 +// @Tags 私信模块 +// @Accept mpfd,json +// @Produce json,html +// @Param sessionId formData string true "会话ID" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/msg/session/del [post] +func DelSession(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + type req struct { + SessionId string `json:"sessionId" form:"sessionId"` // 会话id + } + var in req + if err := ctx.ShouldBind(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code := messageser.DelSession(uid, in.SessionId) + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// GetSessionId doc +// @Summary 消息 +// @Description 获取sessionId +// @Tags 私信模块 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/msg/session/get [get] +func GetSessionId(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var in *sessionmod.QuerySessionIdCond + if err := ctx.ShouldBind(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if in.TakeUid <= 0 { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + sessionId := messageser.GetSessionId(uid, in.TakeUid) + if sessionId == "" { + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeJSON(ctx, stderr.Success, sessionId) +} + +// MessageList doc +// @Summary 消息 +// @Description 会话消息列表 +// @Tags 私信模块 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/msg/message/list [get] +func MessageList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var in *messagemod.QueryMsgCond + if err := ctx.ShouldBind(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + data, has, code := messageser.QueryPrivateLetterMsg(uid, in) + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + resp := make(map[string]interface{}) + resp["list"] = data + resp["hasNext"] = has + common.ServeJSON(ctx, stderr.Success, resp) +} + +// PrivateLetter doc +// @Summary 消息 +// @Description 发消息(私信) +// @Tags 私信模块 +// @Accept mpfd,json +// @Produce json,html +// @Param takeUid formData string true "接受用户uid" +// @Param imgUrl formData []string false "图片内容" +// @Param content formData integer false "消息内容" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/message/priLetter/add [post] +func PrivateLetter(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + param := messagemod.AddMsgReqInfo{} + err = ctx.ShouldBind(¶m) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if len(param.ImgUrl) <= 0 && param.Content == "" { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + // 检查发送内容的合法性 + if param.Content != "" && !truthutil.CheckIsValid(param.Content, 1) { + log.Error("私信检查不通过", log.Any("uid", uid), log.Any("content", param.Content)) + common.ServeJSON(ctx, stderr.Success, nil) + return + } + + code := messageser.AddPrivateLetter(uid, param) + if code != stderr.Success { + log.Warn(fmt.Sprintf("messagectrl Add messageser.AddPrivateLetter error:%+v:", code.Msg()), log.Any("uid", uid)) + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Read doc +// @Summary 消息 +// @Description 消息已读 +// @Tags 私信模块 +// @Accept mpfd,json +// @Produce json,html +// @Param question formData string true "问题" +// @Param images formData []string true "图片" +// @Param bountyGold formData integer true "悬赏金额" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/message/read [post] +func Read(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + param := messagemod.ReadMsgReqInfo{} + err = ctx.ShouldBind(¶m) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code := messageser.ReadMsg(uid, param) + if code != stderr.Success { + log.Warn(fmt.Sprintf("messagectrl Adoption messageser.ReadMsg error:%+v:", code.Tip()), log.Any("uid", uid), log.Any("MsgIds", param.MsgIds)) + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/app/api/minectrl/feedbackctrl.go b/app/api/minectrl/feedbackctrl.go new file mode 100644 index 0000000..c13a27c --- /dev/null +++ b/app/api/minectrl/feedbackctrl.go @@ -0,0 +1,62 @@ +package minectrl + +import ( + "91porn-server/app/service/feedbackser" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "github.com/gin-gonic/gin" +) + +// FeedBack doc +// @Summary 用户 - 用户反馈 +// @Description 用户反馈 +// @Tags mine +// @Accept mpfd,json +// @Produce json,html +// @Param q body feedbackser.FeedbackReq false "请求参数" +// @Success 200 {string} json "{"msg": "成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/feedback [post] +func FeedBack(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + var req = feedbackser.FeedbackReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + ua, _ := common.GetUA(ctx) + code, err := req.Submit(ua, uid) + common.ServeJSON(ctx, code, err) +} + +// FeedBackList doc +// @Summary 用户 - 用户反馈 +// @Description 用户反馈 +// @Tags mine +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": []}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/feedback/list [get] +func FeedBackList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + arg := commod.Page{} + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + result, _ := feedbackser.GetFeedBackList(arg, uid) + common.ServeJSON(ctx, stderr.Success, result) +} diff --git a/app/api/minectrl/followctrl.go b/app/api/minectrl/followctrl.go new file mode 100644 index 0000000..95c01c2 --- /dev/null +++ b/app/api/minectrl/followctrl.go @@ -0,0 +1,209 @@ +package minectrl + +import ( + "91porn-server/app/service/collectser" + "91porn-server/app/service/mineser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/collectmod" + "91porn-server/models/v/followmod" + "91porn-server/models/v/userResourcemod" + "fmt" + "github.com/gin-gonic/gin" +) + +// Follow doc +// @Summary 关注或者取消关注 +// @Description 关注或者取消关注 +// @Tags 关注 +// @Accept mpfd,json +// @Produce json,html +// @Param followUID formData number true "关注的用户uid" +// @Param isShort formData bool true "是否是短视频用户" +// @Param isFollow formData bool true "true则是关注,false则是取消关注" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/mine/follow [post] +func Follow(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + param := followmod.UserFollowReq{} + err = ctx.ShouldBind(¶m) + if err != nil { + log.Error(fmt.Sprintf("mine follow param error:%v,uid:%v", err, uid)) + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if param.FollowUID <= 0 { + log.Error(fmt.Sprintf("mine follow param followUid err param:%+v,uid:%v", param, uid)) + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := mineser.Follow(uid, param.FollowUID, param.IsFollow, param.IsShort) + if code != stderr.Success { + log.Error(fmt.Sprintf("mine follow mineser Follow err:%+v,uid:%v", data, uid)) + } + common.ServeJSON(ctx, code, data) +} + +// UserDownload doc +// @Summary 使用下载次数 +// @Description 使用下载次数 +// @Tags 我的 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/mine/download/use [post] +func UserDownload(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + + code, data := mineser.UseDownloadCount(uid) + if code != stderr.Success { + common.ServeJSON(ctx, code, data) + return + } + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// Collect doc +// @Summary 用户模块 - 用户收藏信息 +// @Description 保存用户一条收藏信息 +// @Tags mine +// @Accept json +// @Produce json +// @Param q body collectmod.DoCollectReqInfo true "参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/mine/collect [post] +func Collect(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + param := collectmod.DoCollectReqInfo{} + err = ctx.ShouldBind(¶m) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + ua, _ := common.GetUA(ctx) + ip := common.GetIP(ctx) + code, err := collectser.DoCollect(uid, param.Type, param.ObjID, param.IsCollect, ua, ip) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, nil) +} + +// BatchCancelCollect doc +// @Summary 用户模块 - 批量取消收藏 +// @Description 用户模块 - 批量取消收藏 +// @Tags mine +// @Accept json +// @Produce json +// @Param objIds formData []string true "数组-收藏对象ID合集" +// @Param type formData string true "收藏类型 SP-长视频 SHORT-短视频 COVER-图文帖子 PIC-图集帖子 SEED_LINK-种子/黄油帖子" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/mine/collect/batch/cancel [post] +func BatchCancelCollect(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + param := collectmod.DoBatchCancelCollectReq{} + err = ctx.ShouldBind(¶m) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + ua, _ := common.GetUA(ctx) + ip := common.GetIP(ctx) + code, err := collectser.DoBatchCancelCollect(uid, param.Type, param.ObjIDs, ua, ip) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, nil) +} + +// InfoList doc +// @Summary 用户模块 - 用户收藏详情列表 +// @Description 视频/地点/话题 列表 +// @Tags mine +// @Accept json +// @Produce json +// @Param type formData string true "收藏类型 video:视频 tag:专题 location:地点" +// @Param pageNumber formData integer true "页数" +// @Param pageSize formData integer true "每页条数" +// @Param uid formData integer true "用户uid" +// @Success 200 {object} commod.ListResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/mine/collect/infoList [get] +func InfoList(ctx *gin.Context) { + type Info struct { + Type string `form:"type" json:"type" binding:"required"` + UID uint64 `form:"uid" json:"uid" binding:"required"` + Page commod.Page + } + param := Info{} + err := ctx.ShouldBind(¶m) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "api mine collect InfoList ShouldBind err"+err.Error()) + return + } + code, hasNext, data, err := collectser.GetInfoList(param.UID, param.Type, param.Page) + if err != nil { + common.ServeJSON(ctx, code, "api mine collect InfoList GetInfoList err"+err.Error()) + return + } + common.ServeJSON(ctx, code, commod.ListResp{HasNext: hasNext, List: data}) +} + +// UserResourceList doc +// @Summary 用户资源列表 +// @Description 用户资源列表 +// @Tags mine +// @Accept json +// @Produce json +// @Param type formData string true "收藏类型 video:视频 tag:专题 location:地点" +// @Param pageNumber formData integer true "页数" +// @Param pageSize formData integer true "每页条数" +// @Param uid formData integer true "用户uid" +// @Success 200 {object} commod.ListResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/mine/userResource/list [get] +func UserResourceList(ctx *gin.Context) { + type Info struct { + Type string `form:"type" json:"type" binding:"required"` + Page commod.Page + } + param := Info{} + err := ctx.ShouldBind(¶m) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "api mine collect InfoList ShouldBind err"+err.Error()) + return + } + data, total, err := userResourcemod.GetUserResource(int(param.Page.PageNumber), int(param.Page.PageSize), param.Type) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": data, + "total": total, + }) +} diff --git a/app/api/minectrl/reportctrl.go b/app/api/minectrl/reportctrl.go new file mode 100644 index 0000000..97fe424 --- /dev/null +++ b/app/api/minectrl/reportctrl.go @@ -0,0 +1,82 @@ +package minectrl + +import ( + "time" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/reptmod" + "91porn-server/models/v/repttypemod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Report doc +// @Summary 举报 - 用户举报 +// @Description 用户操作 +// @Tags mine +// @Accept mpfd,json +// @Produce json,html +// @Param uid query integer true "举报者uid" +// @Param objType query string true "举报对象类型,video、comment、user" +// @Param types query string true "类型:内容违规/账号违规/侵权/其他" +// @Param objID query string false "举报对象ID,videoID、commentId" +// @Param objUID query integer false "被举报用户UID" +// @Success 200 {string} json "{"msg": {"hasReported":true}} 已经举报则hasReported为true,提示用户已经举报" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/report [post] +func Report(ctx *gin.Context) { + var arg struct { + UID uint64 `form:"uid" json:"uid" binding:"required"` + ObjType reptmod.ReportObjType `form:"objType" json:"objType" binding:"required"` + Types string `form:"types" json:"types" binding:"required"` + ObjID *primitive.ObjectID `form:"objID" json:"objID" binding:""` + ObjUID *uint64 `form:"objUID" json:"objUID" binding:""` + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "mine Report arg error "+err.Error()) + return + } + if arg.ObjType == reptmod.User && arg.ObjUID == nil { + common.ServeJSON(ctx, stderr.ErrParamError, "mine Report arg error: no objUID ") + return + } + if arg.ObjType != reptmod.User && arg.ObjID == nil { + common.ServeJSON(ctx, stderr.ErrParamError, "mine Report arg error: no objID ") + return + } + //举报有效期一个月 + ok, err := reptmod.Do(arg.UID, arg.ObjType, arg.ObjID, arg.ObjUID, arg.Types, time.Hour*24*30) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, err) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "hasReported": ok, + }) +} + +// Report doc +// @Summary 举报 - 举报种类 +// @Description 举报种类 +// @Tags mine +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": []}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/report/types/list [get] +func ReportTypesList(c *gin.Context) { + list, err := repttypemod.List() + if err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, "mine ReportTypesList error: "+err.Error()) + return + } + typesList := make([]string, 0, len(list)) + for _, report := range list { + if report.Name != nil { + typesList = append(typesList, *report.Name) + } + } + common.ServeJSON(c, stderr.Success, typesList) +} diff --git a/app/api/modulectrl/module.go b/app/api/modulectrl/module.go new file mode 100644 index 0000000..8ef2393 --- /dev/null +++ b/app/api/modulectrl/module.go @@ -0,0 +1,135 @@ +package modulectrl + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/moduleser" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/moduleconfmod" + "fmt" + "time" + + "github.com/vmihailenco/msgpack/v5" + + "github.com/gin-gonic/gin" +) + +// List ... +// @Summary 获取系统的所有后台配置模块 +// @Description 获取系统的所有后台配置模块 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {array} []moduleconfmod.AppModuleConf +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/modules/list [get] +func List(c *gin.Context) { + ip := c.ClientIP() + str, err := appg.Redis.Get(redisconst.ModulesCache) + if err != nil { + log.Warn(fmt.Sprintf("IP:%s;缓存获取广告列表信息异常:%v", ip, err)) + } + var modules []moduleconfmod.ModuleConf + if str != nil { + if err = msgpack.Unmarshal([]byte(*str), &modules); err != nil { + log.Warn(fmt.Sprintf("IP:%s;解析缓存数据异常:%v", ip, err)) + modules = nil + } + } + if modules == nil { + modules, err = moduleconfmod.GetAllModule() + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, err) + return + } + common.Go(func() { + bytes, _ := msgpack.Marshal(modules) + if setErr := appg.Redis.Set(redisconst.ModulesCache, bytes, 600*time.Second); setErr != nil { + log.Warn(fmt.Sprintf("IP:%s;保存缓存数据异常:%v", ip, setErr)) + } + }) + } + + var data moduleconfmod.AppModuleConf + if len(modules) <= 0 { + common.ServeJSON(c, stderr.Success, data) + return + } + + now := time.Now() + for _, m := range modules { + if !m.IsActiveAt(now) { + continue + } + m.HaiJiaoStyle.EnsureSortRules() + for k, v := range m.HaiJiaoStyle.SortRules { + v.Name = v.Val.Name() + m.HaiJiaoStyle.SortRules[k] = v + } + + subConf := moduleconfmod.APPModuleConf{ + ID: m.ID, + ModuleName: m.ModuleName, + Cover: m.Cover, + Type: m.Type, + ShowType: m.ShowType, + ShowJG: m.ShowJG, + HaiJiaoStyle: m.HaiJiaoStyle, + AiPlazaStyle: m.AiPlazaStyle, + PureVersion: m.PureVersion, + OnlineAt: m.OnlineAt, + OfflineAt: m.OfflineAt, + ExcludeLatest: m.ExcludeLatest, + ExcludeRecommend: m.ExcludeRecommend, + ExcludeSearch: m.ExcludeSearch, + SearchOnlyWhenInactive: m.SearchOnlyWhenInactive, + } + if !m.DefaultTagId.IsZero() { + subConf.DefaultTagId = m.DefaultTagId.Hex() + } + switch m.Type { + case moduleconfmod.HomePage, moduleconfmod.Cartoon, moduleconfmod.Comics: + data.HomePage = append(data.HomePage, subConf) + case moduleconfmod.Pics: + data.Pics = append(data.Pics, subConf) + case moduleconfmod.Novel: + data.Novel = append(data.Novel, subConf) + case moduleconfmod.Community: + data.Community = append(data.Community, subConf) + //case moduleconfmod.PrivateCircle: + //data.PrivateCircle = append(data.PrivateCircle, subConf) + case moduleconfmod.DeepWeb: + data.DeepWeb = append(data.DeepWeb, subConf) + case moduleconfmod.ShortPage: + data.ShortPage = append(data.ShortPage, subConf) + case moduleconfmod.Drama: + // 短剧模块仅下发给独立短剧频道,避免混入首页顶部模块。 + data.DramaPage = append(data.DramaPage, subConf) + + //case moduleconfmod.AiPlaza: + // data.AiPlaza = append(data.AiPlaza, subConf) + } + } + + common.ServeJSON(c, stderr.Success, data) +} + +// Announcements ... +// @Summary 获取跑马灯 +// @Description 获取跑马灯 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} moduleser.AnnouncementResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/modules/announce [get] +func Announcements(c *gin.Context) { + resp, err := moduleser.GetModuleAnnouncements() + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, err) + return + } + common.ServeJSON(c, stderr.Success, resp) +} diff --git a/app/api/nakedchatctrl/nakedchat.go b/app/api/nakedchatctrl/nakedchat.go new file mode 100755 index 0000000..5fa8b66 --- /dev/null +++ b/app/api/nakedchatctrl/nakedchat.go @@ -0,0 +1,66 @@ +package nakedchatctrl + +import ( + "91porn-server/app/service/nakedchatser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// List doc +// +// @Summary 获取裸聊列表接口 +// @Description 获取裸聊列表 +// @Tags 移动端-裸聊 +// @Accept mpfd,json +// @Produce json +// @Param q query nakedchatser.AppQueryListReq false "请求参数" +// @Success 200 object nakedchatser.AppListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/nakedchat/list [get] +func List(ctx *gin.Context) { + p := &nakedchatser.AppQueryListReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + // 获取用户当前配置 + // var err error + // p.Uid, err = common.GetUID(ctx) + // if err != nil { + // common.ServeJSON(ctx, stderr.ErrNoToken, err) + // return + // } + + list := p.GetList() + common.ServeJSON(ctx, stderr.Success, list) +} + +// Info doc +// +// @Summary 获取裸聊详情接口 +// @Description 获取裸聊详情 +// @Tags 移动端-裸聊 +// @Accept mpfd,json +// @Produce json +// @Param q query nakedchatser.AppQueryInfoReq false "请求参数" +// @Success 200 object nakedchatser.AppQueryInfoRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/nakedchat/info [get] +func Info(ctx *gin.Context) { + p := &nakedchatser.AppQueryInfoReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + data, err := p.GetInfo() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/nakedchatorderctrl/nakedchatorder.go b/app/api/nakedchatorderctrl/nakedchatorder.go new file mode 100755 index 0000000..dba5eaf --- /dev/null +++ b/app/api/nakedchatorderctrl/nakedchatorder.go @@ -0,0 +1,39 @@ +package nakedchatorderctrl + +import ( + "91porn-server/app/service/nakedchatorderser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// List doc +// +// @Summary 获取裸聊订单列表接口 +// @Description 获取裸聊订单列表 +// @Tags 移动端-裸聊订单 +// @Accept mpfd,json +// @Produce json +// @Param q query nakedchatorderser.AppQueryListReq false "请求参数" +// @Success 200 object nakedchatorderser.AppListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/nakedchatorder/list [get] +func List(ctx *gin.Context) { + p := &nakedchatorderser.AppQueryListReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + // 获取用户当前配置 + var err error + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + list := p.GetList(uid) + common.ServeJSON(ctx, stderr.Success, list) +} diff --git a/app/api/newactivityctrl/gameactivity.go b/app/api/newactivityctrl/gameactivity.go new file mode 100644 index 0000000..83ce594 --- /dev/null +++ b/app/api/newactivityctrl/gameactivity.go @@ -0,0 +1,124 @@ +package newactivityctrl + +import ( + "fmt" + "math/rand" + "net/http" + + "91porn-server/app/middleware/authuser" + "91porn-server/app/service/productser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/lotterylgmod" + "91porn-server/models/v/walletmod" + + "github.com/gin-gonic/gin" +) + +// 常量配置 +const ( + MaxTimesOneDay = 3 + Zero = 0 + + ChanceLimit = 100 + WinChance = 20 + + CodeBegin = 1 + CodeEnd = 1000 + + Title = "每局游戏有20%机率获得抽奖号" + SubTilte = "中奖后找到在线客服领取会员卡、金币、楼凤信息等福利哦!" +) + +// UserInfo 用户信息 +func UserInfo(ctx *gin.Context) { + token := ctx.Request.Header.Get("X-Authorization") //为兼容H5带APP token + if token == "" { + ctx.AbortWithStatusJSON(http.StatusOK, stderr.ErrNoToken.Struct()) + return + } + claims, err := authuser.ParseWebClaims(token) + if err != nil { + log.Error("gameactivity UserInfo ParseWebClaims error", log.Any("token", fmt.Sprintf("%+v", token)), log.E(err)) + ctx.JSON(http.StatusBadRequest, stderr.InvalidToken.Struct()) + return + } + uid := claims.UID + wal, err := walletmod.GetWallet(uid) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "data": err.Error(), "code": stderr.ErrServerUnavailable, "msg": stderr.ErrServerUnavailable.Msg()}) + return + } + nums, err := lotterylgmod.UserTodayNum(uid) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "data": err.Error(), "code": stderr.ErrServerUnavailable, "msg": stderr.ErrServerUnavailable.Msg()}) + return + } + coins := wal.Amount + wal.Income + dt := gin.H{"coins": coins, "nums": nums, "title": Title, "subTitle": SubTilte} + ctx.JSON(int(stderr.Success), gin.H{"data": dt, "code": stderr.Success, "msg": stderr.Success.Msg()}) +} + +// Deduct2Coins 固定每次接口调用扣除两金币 +func Deduct2Coins(ctx *gin.Context) { + token := ctx.Request.Header.Get("X-Authorization") //为兼容H5带APP token + if token == "" { + ctx.AbortWithStatusJSON(http.StatusOK, stderr.ErrNoToken.Struct()) + return + } + claims, err := authuser.ParseWebClaims(token) + if err != nil { + log.Error("activity UserBalance ParseWebClaims error", log.Any("token", fmt.Sprintf("%+v", token)), log.E(err)) + ctx.JSON(http.StatusBadRequest, stderr.InvalidToken.Struct()) + return + } + code := productser.DeductGameCoins(claims.UID, 2) + ctx.JSON(int(stderr.Success), gin.H{"data": nil, "code": code, "msg": code.Msg()}) +} + +func genLotteryCode() int { + if rand.Intn(ChanceLimit) >= WinChance { + return Zero + } + return 1 + rand.Intn(1000) +} + +// RecordRewardCode 记录中奖号码 +func RecordRewardCode(ctx *gin.Context) { + token := ctx.Request.Header.Get("X-Authorization") //为兼容H5带APP token + if token == "" { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + claims, err := authuser.ParseWebClaims(token) + if err != nil { + log.Error("activity UserBalance ParseWebClaims error", log.Any("token", fmt.Sprintf("%+v", token)), log.E(err)) + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + uid := claims.UID + var arg struct { + GateName string `form:"gateName" json:"gateName"` //关卡 + } + if err = ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + times, err := lotterylgmod.UserTodayChances(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + if times >= MaxTimesOneDay { + common.ServeJSON(ctx, stderr.ActLotteryNoTimes, "") + return + } + code := genLotteryCode() + if code > Zero { + _ = lotterylgmod.InsertLog(uid, code, arg.GateName) + } + common.ServeJSON(ctx, stderr.Success, gin.H{"number": code}) +} diff --git a/app/api/newactivityctrl/newactivity.go b/app/api/newactivityctrl/newactivity.go new file mode 100644 index 0000000..b83bbb8 --- /dev/null +++ b/app/api/newactivityctrl/newactivity.go @@ -0,0 +1,296 @@ +package newactivityctrl + +import ( + "fmt" + "math/rand" + "net/http" + "strconv" + "time" + + "91porn-server/app/appg" + "91porn-server/app/middleware/authuser" + "91porn-server/app/service/productser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/synclock" + "91porn-server/common/timeutil" + "91porn-server/models/v/newactivity" + "91porn-server/models/v/walletmod" + + "github.com/gin-gonic/gin" +) + +// @Tags 特制H5活动 +// @Summary 获取用户余额 +// @Description 获取用户余额 +// @Security ApiKeyAuth +// @Tags app-H5嫩模活动 +// @Accept json +// @Produce json +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/newactivity/userBanlance [post] +func UserBalance(ctx *gin.Context) { + token := ctx.Request.Header.Get("X-Authorization") //为兼容H5带APP token + if token == "" { + ctx.AbortWithStatusJSON(http.StatusOK, stderr.ErrNoToken.Struct()) + return + } + claims, err := authuser.ParseWebClaims(token) + if err != nil { + log.Error("activity UserBalance ParseWebClaims error", log.Any("token", fmt.Sprintf("%+v", token)), log.E(err)) + ctx.JSON(http.StatusBadRequest, stderr.InvalidToken.Struct()) + return + } + w, err := walletmod.GetWallet(claims.UID) + if err != nil { + ctx.JSON(http.StatusInternalServerError, + gin.H{"data": err.Error(), + "code": stderr.ErrServerUnavailable, + "msg": stderr.ErrServerUnavailable.Msg()}) + return + } + ctx.JSON(int(stderr.Success), gin.H{"data": w.Amount + w.Income, "code": stderr.Success, "msg": stderr.Success.Msg()}) +} + +// @Tags 特制H5活动 +// @Summary 获取所有嫩模list +// @Description 获取所有嫩模list +// @Security ApiKeyAuth +// @Tags app-H5嫩模活动 +// @Accept json +// @Produce json +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/newactivity/modelList [post] +func ModelList(ctx *gin.Context) { + token := ctx.Request.Header.Get("X-Authorization") //为兼容H5带APP token + if token == "" { + ctx.AbortWithStatusJSON(http.StatusOK, stderr.ErrNoToken.Struct()) + return + } + if _, err := authuser.ParseWebClaims(token); err != nil { + log.Error("activity ModelList ParseWebClaims error", log.Any("token", fmt.Sprintf("%+v", token)), log.E(err)) + ctx.JSON(http.StatusBadRequest, stderr.InvalidToken.Struct()) + return + } + rs, err := newactivity.ModelList() + if err != nil { + ctx.JSON(http.StatusInternalServerError, + gin.H{"data": err.Error(), + "code": stderr.ErrServerUnavailable, + "msg": stderr.ErrServerUnavailable.Msg()}) + return + } + ctx.JSON(int(stderr.Success), gin.H{"data": rs, "code": stderr.Success, "msg": stderr.Success.Msg()}) +} + +// @Tags 特制H5活动 +// @Summary 查询指定模特数据 +// @Description 查询指定模特数据 +// @Security ApiKeyAuth +// @Tags app-H5嫩模活动 +// @Accept json +// @Produce json +// @Param modelId formData integer false "模特ID" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/newactivity/findOneModel [post] +func FindOneModel(ctx *gin.Context) { + token := ctx.Request.Header.Get("X-Authorization") //为兼容H5带APP token + if token == "" { + ctx.AbortWithStatusJSON(http.StatusOK, stderr.ErrNoToken.Struct()) + return + } + if _, err := authuser.ParseWebClaims(token); err != nil { + log.Error("activity FindOneModel ParseWebClaims error", log.Any("token", fmt.Sprintf("%+v", token)), log.E(err)) + ctx.JSON(http.StatusBadRequest, stderr.InvalidToken.Struct()) + return + } + var req struct { + ModelId uint32 `json:"modelId"` + } + if err := ctx.ShouldBind(&req); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"data": err.Error(), "code": stderr.ErrParamError, "msg": stderr.ErrParamError.Msg()}) + return + } + rs, err := newactivity.FindOne(req.ModelId) + if err != nil { + ctx.JSON(http.StatusInternalServerError, + gin.H{"data": err.Error(), + "code": stderr.ErrServerUnavailable, + "msg": stderr.ErrServerUnavailable.Msg()}) + return + } + ctx.JSON(http.StatusOK, gin.H{"data": rs, "code": stderr.Success, "msg": stderr.Success.Msg()}) +} + +// @Tags 特制H5活动 +// @Summary 购买礼物 +// @Description 购买礼物 +// @Security ApiKeyAuth +// @Tags app-H5嫩模活动 +// @Accept json +// @Produce json +// @Param modelId formData integer false "模特id" +// @Param quantity formData integer false "礼物数目" +// @Param buyOut formData boolean false "是否买断" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/newactivity/buyGifts [post] +func BuyGifts(ctx *gin.Context) { + token := ctx.Request.Header.Get("X-Authorization") //为兼容H5带APP token + if token == "" { + ctx.AbortWithStatusJSON(http.StatusOK, stderr.ErrNoToken.Struct()) + return + } + claims, err := authuser.ParseWebClaims(token) + if err != nil { + log.Error("activity BuyGifts ParseWebClaims error", log.Any("token", fmt.Sprintf("%+v", token)), log.E(err)) + ctx.JSON(http.StatusBadRequest, stderr.InvalidToken.Struct()) + return + } + var req newactivity.BuyReq + if err := ctx.ShouldBind(&req); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"data": err.Error(), "code": stderr.ErrParamError, "msg": stderr.ErrParamError.Msg()}) + return + } + if req.Quantity <= 0 || req.Quantity > 300 || (req.BuyOut && req.Quantity != 300) { + ctx.JSON(http.StatusBadRequest, gin.H{"msg": "请重新指定礼物数目", "code": http.StatusBadRequest}) + return + } + //上锁(用户账户锁) + lock := synclock.Lock{Lock: appg.Redis} + if _, err = lock.UserAccountSpinLock(uint32(claims.UID), synclock.SpinLockExpire); err != nil { + return + } + defer lock.UserAccountUnlock(uint32(claims.UID)) + code := productser.BuyModel(claims.UID, req) + if code != stderr.Success { + ctx.JSON(http.StatusBadRequest, code.Struct()) + return + } + ctx.JSON(http.StatusOK, code.Struct()) +} + +// @Tags 特制H5活动 +// @Summary 购买礼物 +// @Description 购买礼物 +// @Security ApiKeyAuth +// @Tags app-H5嫩模活动 +// @Accept json +// @Produce json +// @Param modelId formData integer false "模特id" +// @Param quantity formData integer false "礼物数目" +// @Param buyOut formData boolean false "是否买断" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/newactivity/buyGiftsSchedule [post] +func BuyGiftsSchedule(ctx *gin.Context) { + var req struct { + UIDs []uint64 `json:"uids" form:"uids"` + Token string `json:"token" form:"token" binding:"required"` + Active bool `json:"active" form:"active"` + NumPercent int `json:"numPercent" form:"numPercent"` //每次对于一个嫩模购买的份数 + Interval int `json:"interval" form:"interval"` //时间间隔 以秒为单位 + } + if err := ctx.ShouldBind(&req); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"data": err.Error(), "code": stderr.ErrParamError, "msg": stderr.ErrParamError.Msg()}) + return + } + if req.Token != "iNrKZ7vIm98ImFYmOKxXytf1ANJiZGB2" { + ctx.AbortWithStatusJSON(http.StatusOK, stderr.ErrNoToken.Struct()) + return + } + if req.NumPercent >= 300 { + ctx.JSON(http.StatusBadRequest, gin.H{"msg": "请重新指定礼物数目", "code": http.StatusBadRequest}) + return + } + if req.Interval == 0 { + req.Interval = 4 + } + if req.NumPercent == 0 { + req.NumPercent = 3 + } + key := "NengModel-Schedule" + timeutil.BeginningOfDay(time.Now()).Format("YYYY-MM-DD") + active := strconv.FormatBool(req.Active) + //每次调用相当于就是一个任务 + _, _ = appg.Redis.Del(key) + time.Sleep(2 * time.Second) + if err := appg.Redis.Set(key, active, 0); err != nil { + ctx.JSON(http.StatusOK, gin.H{"data": err.Error(), "code": stderr.ErrParamError, "msg": stderr.ErrParamError.Msg()}) + return + } + common.Go(func() { + for { + result, err := appg.Redis.Get(key) + if err != nil || result == nil { + return + } + if *result == "false" { + return + } + buyReq := newactivity.BuyReq{ + ModelId: uint32(rand.Intn(178) + 1), + Quantity: int32(rand.Intn(req.NumPercent) + 1), + BuyOut: false, + } + uid := req.UIDs[rand.Intn(len(req.UIDs)-1)] + //上锁(用户账户锁) + lock := synclock.Lock{Lock: appg.Redis} + if _, err = lock.UserAccountSpinLock(uint32(uid), synclock.SpinLockExpire); err != nil { + continue + } + defer lock.UserAccountUnlock(uint32(uid)) + code := productser.BuyModelFakeUser(uid, buyReq) + log.Info("[METHOD-BuyGiftsSchedule] run fake buy model run ========>", log.Any("code", code), log.Any("uid", uid), log.Any("param", fmt.Sprintf("%+v", buyReq))) + + } + }) + if req.Active { + ctx.JSON(http.StatusOK, gin.H{"msg": "任务开始启动执行.....", "code": http.StatusOK}) + return + } + ctx.JSON(http.StatusOK, gin.H{"msg": "结束任务.....", "code": http.StatusOK}) +} + +// @Tags 特制H5活动 +// @Summary 获奖记录 +// @Description 获奖记录 +// @Security ApiKeyAuth +// @Tags app-H5嫩模活动 +// @Accept json +// @Produce json +// @Param date formData string false "日期" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/newactivity/winRecords [post] +func WinRecords(ctx *gin.Context) { + token := ctx.Request.Header.Get("X-Authorization") //为兼容H5带APP token + if token == "" { + ctx.AbortWithStatusJSON(http.StatusOK, stderr.ErrNoToken.Struct()) + return + } + if _, err := authuser.ParseWebClaims(token); err != nil { + log.Error("activity WinRecords ParseWebClaims error", log.Any("token", fmt.Sprintf("%+v", token)), log.E(err)) + ctx.JSON(http.StatusBadRequest, stderr.InvalidToken.Struct()) + return + } + var req struct { + Date time.Time `json:"date"` + } + if err := ctx.ShouldBind(&req); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"data": err.Error(), "code": stderr.ErrParamError, "msg": stderr.ErrParamError.Msg()}) + return + } + rs, err := newactivity.WinRecordsByDate(req.Date) + if err != nil { + ctx.JSON(http.StatusInternalServerError, + gin.H{"data": err.Error(), + "code": stderr.ErrServerUnavailable, + "msg": stderr.ErrServerUnavailable.Msg()}) + return + } + ctx.JSON(http.StatusOK, gin.H{"data": rs, "code": stderr.Success, "msg": stderr.Success.Msg()}) +} diff --git a/app/api/newactivityctrl/questionnre.go b/app/api/newactivityctrl/questionnre.go new file mode 100644 index 0000000..8c44c01 --- /dev/null +++ b/app/api/newactivityctrl/questionnre.go @@ -0,0 +1,91 @@ +package newactivityctrl + +import ( + "fmt" + "net/http" + + "91porn-server/app/middleware/authuser" + "91porn-server/app/service/questionnreser" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/questionnremod" + + "github.com/gin-gonic/gin" +) + +// @Tags 问卷调查 +// @Summary 问卷提交 +// @Description 问卷提交 +// @Security ApiKeyAuth +// @Accept json +// @Produce json +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/newactivity/questionnaire/submit [post] +func Submit(ctx *gin.Context) { + token := ctx.Request.Header.Get("X-Authorization") //为兼容H5带APP token + if token == "" { + ctx.AbortWithStatusJSON(http.StatusOK, stderr.ErrNoToken.Struct()) + return + } + claims, err := authuser.ParseWebClaims(token) + if err != nil { + log.Error("activity QuestionnaireSubmit ParseWebClaims error", log.Any("token", fmt.Sprintf("%+v", token)), log.E(err)) + ctx.JSON(http.StatusBadRequest, stderr.InvalidToken.Struct()) + return + } + var req questionnremod.Questionnaire + if err := ctx.ShouldBind(&req); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"data": err.Error(), "code": stderr.ErrParamError, "msg": stderr.ErrParamError.Msg()}) + return + } + req.UID = claims.UID + code, res := questionnreser.Submit(req) + if code != stderr.Success { + ctx.JSON(http.StatusBadRequest, code.Struct()) + return + } + ctx.JSON(http.StatusOK, res) +} + +// @Tags 问卷调查 +// @Summary 根据用户ID查询问卷信息接口 +// @Description 根据用户ID查询问卷信息接口 +// @Security ApiKeyAuth +// @Accept json +// @Produce json +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/newactivity/questionnaire/getQuestionnaireByUser [GET] +func GetQuestionnaireByUsers(ctx *gin.Context) { + token := ctx.Request.Header.Get("X-Authorization") //为兼容H5带APP token + if token == "" { + ctx.AbortWithStatusJSON(http.StatusOK, stderr.ErrNoToken.Struct()) + return + } + claims, err := authuser.ParseWebClaims(token) + if err != nil { + log.Error("activity QuestionnaireSubmit ParseWebClaims error", log.Any("token", fmt.Sprintf("%+v", token)), log.E(err)) + ctx.JSON(http.StatusBadRequest, stderr.InvalidToken.Struct()) + return + } + var req struct { + UID uint64 `form:"uid" json:"uid"` + } + if err = ctx.ShouldBind(&req); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"data": err.Error(), "code": stderr.ErrParamError, "msg": stderr.ErrParamError.Msg()}) + return + } + if req.UID == 0 { + req.UID = claims.UID + } + vipLevel, list, err := questionnreser.GetByUID(req.UID) + if err != nil { + code := stderr.ErrNetWorkBusy + ctx.JSON(http.StatusBadRequest, code.Struct()) + } + ctx.JSON(http.StatusOK, gin.H{ + "vipLevel": vipLevel, + "list": list, + }) +} diff --git a/app/api/notictrl/notificationctrl.go b/app/api/notictrl/notificationctrl.go new file mode 100644 index 0000000..1ad3de4 --- /dev/null +++ b/app/api/notictrl/notificationctrl.go @@ -0,0 +1,41 @@ +package notictrl + +import ( + "91porn-server/app/service/notiser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// SendCaptcha doc +// @Summary 发送验证码 +// @Description 发送用户注册、登录的验证码 +// @Tags captcha +// @Accept json +// @Produce json +// @Param mobile formData string false "手机号码信息" +// @Param email formData string false "邮箱地址" +// @Param type formData integer false "发送验证码的用途 1-绑定手机号 2-手机号登陆 3-email" +// @Success 200 {string} string "操作成功" +// @Router /api/app/notification/captcha [post] +func SendCaptcha(ctx *gin.Context) { + var args struct { + Mobile string `form:"mobile" json:"mobile"` + Email string `form:"email" json:"email"` + Type int `form:"type" json:"type"` + } + if err := ctx.ShouldBind(&args); err != nil { + log.Warn("SendCaptcha bind args", log.E(err)) + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if args.Email == "" && args.Mobile == "" { + log.ErrorX(ctx, "empty phone number and email", log.Any("args", args)) + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + errcode := notiser.SendCaptcha(ctx, args.Mobile, args.Email, args.Type) + common.ServeJSON(ctx, errcode, nil) +} diff --git a/app/api/officialWebsitectrl/album.go b/app/api/officialWebsitectrl/album.go new file mode 100644 index 0000000..950773a --- /dev/null +++ b/app/api/officialWebsitectrl/album.go @@ -0,0 +1,48 @@ +package officialWebsitectrl + +import ( + "91porn-server/app/service/officialWebsiteser" + "91porn-server/common" + "91porn-server/common/cachev2" + "91porn-server/common/constant/redisconst" + "91porn-server/common/stderr" + "fmt" + + "github.com/gin-gonic/gin" +) + +func AlbumList(ctx *gin.Context) { + req := &officialWebsiteser.AlbumListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + var data officialWebsiteser.AlbumListResp + _, err := cachev2.Classes(). + CacheTime(redisconst.OfficialWebsiteAlbumListCacheExpire). + AutoListKey(fmt.Sprintf(redisconst.OfficialWebsiteAlbumListCacheKey, req.PageNumber, req.PageSize)). + ResBind(&data).Cache(officialWebsiteser.AlbumList, req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, &data) +} + +func AlbumDetail(ctx *gin.Context) { + var req = &officialWebsiteser.AlbumDetailReq{} + if err := ctx.ShouldBindUri(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialWebsiteser.AlbumDetail(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/officialWebsitectrl/basicData.go b/app/api/officialWebsitectrl/basicData.go new file mode 100644 index 0000000..0c9c9d8 --- /dev/null +++ b/app/api/officialWebsitectrl/basicData.go @@ -0,0 +1,28 @@ +package officialWebsitectrl + +import ( + "91porn-server/app/service/officialWebsiteser" + "91porn-server/common" + "91porn-server/common/cachev2" + "91porn-server/common/constant/redisconst" + "91porn-server/common/stderr" + "91porn-server/models/v/sourcemod" + + "github.com/gin-gonic/gin" +) + +func GetBasicData(ctx *gin.Context) { + var data = officialWebsiteser.GetBasicDataResp{} + _, err := cachev2.Classes(). + CacheTime(redisconst.OfficialWebsiteBasicDataCacheExpire). + AutoListKey(redisconst.OfficialWebsiteBasicDataCacheKey). + ResBind(&data). + Cache(officialWebsiteser.GetBasicData) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + // 域名信息实时获取,不随基础数据大缓存(12h),避免下发已失效的域名 + data.Domain, data.SourceList = sourcemod.PingList() + common.ServeJSON(ctx, stderr.Success, &data) +} diff --git a/app/api/officialWebsitectrl/business.go b/app/api/officialWebsitectrl/business.go new file mode 100644 index 0000000..824afa5 --- /dev/null +++ b/app/api/officialWebsitectrl/business.go @@ -0,0 +1,23 @@ +package officialWebsitectrl + +import ( + "91porn-server/app/service/officialWebsiteser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +func BusinessList(ctx *gin.Context) { + req := &officialWebsiteser.BusinessListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialWebsiteser.BusinessList(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/officialWebsitectrl/hero.go b/app/api/officialWebsitectrl/hero.go new file mode 100644 index 0000000..1b5fea5 --- /dev/null +++ b/app/api/officialWebsitectrl/hero.go @@ -0,0 +1,44 @@ +package officialWebsitectrl + +import ( + "91porn-server/app/service/officialWebsiteser" + "91porn-server/common" + "91porn-server/common/cachev2" + "91porn-server/common/constant/redisconst" + "91porn-server/common/stderr" + "fmt" + + "github.com/gin-gonic/gin" +) + +func HeroList(ctx *gin.Context) { + req := &officialWebsiteser.HeroListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + var data officialWebsiteser.HeroListResp + _, err := cachev2.Classes(). + CacheTime(redisconst.OfficialWebsiteHeroListCacheExpire). + AutoListKey(fmt.Sprintf(redisconst.OfficialWebsiteHeroListCacheKey, req.SortType, req.PageNumber, req.PageSize)). + ResBind(&data).Cache(officialWebsiteser.HeroList, req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +func HeroDetail(ctx *gin.Context) { + req := &officialWebsiteser.HeroDetailReq{} + if err := ctx.ShouldBindUri(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialWebsiteser.HeroDetail(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/officialWebsitectrl/job.go b/app/api/officialWebsitectrl/job.go new file mode 100644 index 0000000..82d4994 --- /dev/null +++ b/app/api/officialWebsitectrl/job.go @@ -0,0 +1,23 @@ +package officialWebsitectrl + +import ( + "91porn-server/app/service/officialWebsiteser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +func JobList(ctx *gin.Context) { + req := &officialWebsiteser.JobListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialWebsiteser.JobList(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/officialWebsitectrl/news.go b/app/api/officialWebsitectrl/news.go new file mode 100644 index 0000000..1e1056a --- /dev/null +++ b/app/api/officialWebsitectrl/news.go @@ -0,0 +1,44 @@ +package officialWebsitectrl + +import ( + "91porn-server/app/service/officialWebsiteser" + "91porn-server/common" + "91porn-server/common/cachev2" + "91porn-server/common/constant/redisconst" + "91porn-server/common/stderr" + "fmt" + + "github.com/gin-gonic/gin" +) + +func NewsList(ctx *gin.Context) { + req := &officialWebsiteser.NewsListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + var data officialWebsiteser.NewsListResp + _, err := cachev2.Classes(). + CacheTime(redisconst.OfficialWebsiteNewsListCacheExpire). + AutoListKey(fmt.Sprintf(redisconst.OfficialWebsiteNewsListCacheKey, req.PageNumber, req.PageSize)). + ResBind(&data).Cache(officialWebsiteser.NewsList, req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"hasNext": data.HasNext, "list": data.List}) +} + +func NewsDetail(ctx *gin.Context) { + req := &officialWebsiteser.NewsDetailReq{} + if err := ctx.ShouldBindUri(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialWebsiteser.NewsDetail(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/officialWebsitectrl/partner.go b/app/api/officialWebsitectrl/partner.go new file mode 100644 index 0000000..d0eece1 --- /dev/null +++ b/app/api/officialWebsitectrl/partner.go @@ -0,0 +1,29 @@ +package officialWebsitectrl + +import ( + "91porn-server/app/service/officialWebsiteser" + "91porn-server/common" + "91porn-server/common/cachev2" + "91porn-server/common/constant/redisconst" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +func PartnerList(ctx *gin.Context) { + req := &officialWebsiteser.PartnerListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + var data officialWebsiteser.PartnerListResp + _, err := cachev2.Classes(). + CacheTime(redisconst.OfficialWebsitePartnerListCacheExpire). + AutoListKey(redisconst.OfficialWebsitePartnerListCacheKey). + ResBind(&data).Cache(officialWebsiteser.PartnerList, req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/officialWebsitectrl/recruit.go b/app/api/officialWebsitectrl/recruit.go new file mode 100644 index 0000000..39efe28 --- /dev/null +++ b/app/api/officialWebsitectrl/recruit.go @@ -0,0 +1,23 @@ +package officialWebsitectrl + +import ( + "91porn-server/app/service/officialWebsiteser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +func RecruitForm(ctx *gin.Context) { + req := &officialWebsiteser.RecruitFormReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialWebsiteser.RecruitForm(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/officialWebsitectrl/tag.go b/app/api/officialWebsitectrl/tag.go new file mode 100644 index 0000000..10976dc --- /dev/null +++ b/app/api/officialWebsitectrl/tag.go @@ -0,0 +1,23 @@ +package officialWebsitectrl + +import ( + "91porn-server/app/service/officialWebsiteser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +func TagList(ctx *gin.Context) { + req := &officialWebsiteser.TagListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialWebsiteser.TagList(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/officialWebsitectrl/video.go b/app/api/officialWebsitectrl/video.go new file mode 100644 index 0000000..7192e71 --- /dev/null +++ b/app/api/officialWebsitectrl/video.go @@ -0,0 +1,70 @@ +package officialWebsitectrl + +import ( + "91porn-server/app/service/officialWebsiteser" + "91porn-server/common" + "91porn-server/common/cachev2" + "91porn-server/common/constant/redisconst" + "91porn-server/common/stderr" + "fmt" + "strings" + + "github.com/gin-gonic/gin" +) + +func VideoList(ctx *gin.Context) { + req := &officialWebsiteser.VideoListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + var data officialWebsiteser.VideoListResp + _, err := cachev2.Classes(). + CacheTime(redisconst.OfficialWebsiteVideoListCacheExpire). + AutoListKey(fmt.Sprintf(redisconst.OfficialWebsiteVideoListCacheKey, req.Type, req.ID, req.Sort, req.PageNumber, req.PageSize)). + ResBind(&data).Cache(officialWebsiteser.VideoList, req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +func VideoDetail(ctx *gin.Context) { + req := &officialWebsiteser.VideoDetailReq{} + if err := ctx.ShouldBindUri(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialWebsiteser.VideoDetail(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +func VideoCheck(ctx *gin.Context) { + source := ctx.Param("source") + if source == "" { + common.ServeJSON(ctx, stderr.ErrParamError, "") + ctx.Abort() + } + + id := ctx.Param("id") + var req = &officialWebsiteser.VideoDetailReq{ID: id} + data, err := officialWebsiteser.VideoDetail(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + ctx.Abort() + } + if data.ID == "" { + common.ServeJSON(ctx, stderr.ErrParamError, "") + ctx.Abort() + } + if !strings.Contains(source, data.Url) { + common.ServeJSON(ctx, stderr.ErrParamError, "") + ctx.Abort() + } + ctx.Next() +} diff --git a/app/api/officialctrl/officialCtrl.go b/app/api/officialctrl/officialCtrl.go new file mode 100644 index 0000000..d698751 --- /dev/null +++ b/app/api/officialctrl/officialCtrl.go @@ -0,0 +1,34 @@ +package officialctrl + +import ( + "91porn-server/app/service/officialser" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/officialmod" + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 官方列表 +// @Description APP-官方列表 +// @Tags 官方列表 +// @Accept mpfd,json +// @Produce json,html +// @Param type query integer true "类型,1:下载 2:社区" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/official/list [get] +func List(ctx *gin.Context) { + var in *officialmod.QueryCond + if err := ctx.ShouldBind(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + ua, _ := common.GetUA(ctx) + data, code := officialser.QueryAll(in, ua.Ver, ua.SysType) + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/paymentguidectrl/payment_guide.go b/app/api/paymentguidectrl/payment_guide.go new file mode 100644 index 0000000..a9ed348 --- /dev/null +++ b/app/api/paymentguidectrl/payment_guide.go @@ -0,0 +1,50 @@ +package paymentguidectrl + +import ( + "strings" + + "91porn-server/app/service/paymentguideser" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/paymentguidemod" + + "github.com/gin-gonic/gin" +) + +func Get(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + scene := strings.ToUpper(strings.TrimSpace(ctx.Query("scene"))) + if !paymentguidemod.ValidScene(scene) { + common.ServeJSON(ctx, stderr.ErrParamError, "invalid scene") + return + } + resp, err := paymentguideser.GetGuide(uid, scene) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +func Impression(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + req := paymentguideser.ImpressionReq{} + if err = ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + req.Scene = strings.ToUpper(strings.TrimSpace(req.Scene)) + if err = paymentguideser.RecordImpression(uid, req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/app/api/pingctrl/pingctrl.go b/app/api/pingctrl/pingctrl.go new file mode 100644 index 0000000..8288ae2 --- /dev/null +++ b/app/api/pingctrl/pingctrl.go @@ -0,0 +1,911 @@ +package pingctrl + +import ( + "91porn-server/app/service/activityclient" + "91porn-server/app/service/adser" + "91porn-server/app/service/advance_ser" + "91porn-server/app/service/ai_mate_ser" + "91porn-server/app/service/messageser" + "91porn-server/app/service/paymentguideser" + "91porn-server/app/service/sys_config" + "91porn-server/common/constant" + "91porn-server/common/services/message" + "91porn-server/common/store" + "91porn-server/models/cache/bannerjumpdata" + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/v/bannerjumpmod" + "91porn-server/models/v/jingangmod" + "91porn-server/models/v/sysconfmod" + "91porn-server/models/v/walletmod" + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "91porn-server/app/appg" + "91porn-server/app/middleware/requestEncrypt" + "91porn-server/app/proto" + "91porn-server/app/service/versionser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/version" + "91porn-server/models/commod" + "91porn-server/models/v/sourcemod" + "91porn-server/models/v/systemmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/versionmod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// newUserAdFree 判断当前用户是否处于新人免广告期内。 +// 当【新人广告开关】(VCodeNewUserAdFreeSwitch) 开启,且用户已登录、注册时间仍在 +// 【新人免广告时限】(VCodeNewUserAdFreeHours) 内时返回 true,此时 +// /ping/domain 与 /ping/domain/h5 不返回广告信息。 +func newUserAdFree(configure sysconfmod.ConfMap, user *usermod.User) bool { + if !configure.GetBool(sysconfmod.VCodeNewUserAdFreeSwitch) { + return false + } + if user == nil { + return false + } + hours := configure.GetInt(sysconfmod.VCodeNewUserAdFreeHours) + if hours <= 0 { + return false + } + return user.CreatedAt.Add(time.Duration(hours) * time.Hour).After(time.Now()) +} + +func shortDramaEntryPopupEnabled(configure sysconfmod.ConfMap) bool { + if _, exists := configure[string(sysconfmod.VCodeShortDramaEntryPopup)]; !exists { + return true + } + return configure.GetBool(sysconfmod.VCodeShortDramaEntryPopup) +} + +const ( + defaultEntryPageHome = "home" + defaultEntryPageDrama = "drama" + defaultEntryAudienceNew = "new_user" + defaultEntryAudienceAll = "all_users" +) + +func normalizeDefaultEntryPage(page string) string { + switch strings.TrimSpace(page) { + case defaultEntryPageDrama: + return defaultEntryPageDrama + case defaultEntryPageHome: + return defaultEntryPageHome + default: + return defaultEntryPageHome + } +} + +// defaultEntryAudienceMatch 判断当前用户是否属于默认入口配置的生效对象。 +// 注册未满24小时视为新用户;已经处理过旧版本的老用户升级后也命中一次。 +// 历史用户首次接入版本标记时,以最近登录版本兼容判断是否刚升级。 +func defaultEntryAudienceMatch(audience, currentVer string, user *usermod.User, now time.Time) bool { + switch strings.TrimSpace(audience) { + case defaultEntryAudienceAll: + return true + case defaultEntryAudienceNew: + if user == nil || currentVer == "" { + return false + } + if user.IsNewUser(now) { + return true + } + if user.DefaultEntryHandledVer == currentVer { + return false + } + if user.DefaultEntryHandledVer != "" { + return true + } + return user.LastVer != "" && user.LastVer != currentVer + default: + return false + } +} + +// defaultEntryClaimAccepted 判断版本标记的原子抢占结果是否允许本次进入配置页。 +// 注册未满24小时的用户持续命中新用户规则,不受版本标记及其缓存状态影响; +// 超过24小时的升级老用户仍只允许首次抢占成功的请求命中。 +func defaultEntryClaimAccepted(audience string, user *usermod.User, now time.Time, claimed bool, claimErr error) bool { + if strings.TrimSpace(audience) != defaultEntryAudienceNew || user.IsNewUser(now) { + return true + } + return claimErr == nil && claimed +} + +// resolveDefaultEntryPage 返回客户端本次应直接进入的最终页面。 +// defaultEntryAudience 仅作为后台规则保留,客户端无需再次组合判断。 +func resolveDefaultEntryPage(configure sysconfmod.ConfMap, user *usermod.User, currentVer string) string { + page := normalizeDefaultEntryPage(configure.GetString(sysconfmod.VCodeDefaultEntryPage)) + audience := strings.TrimSpace(configure.GetString(sysconfmod.VCodeDefaultEntryAudience)) + now := time.Now() + matched := defaultEntryAudienceMatch(audience, currentVer, user, now) + + if user != nil && currentVer != "" && user.DefaultEntryHandledVer != currentVer { + claimed, err := usermod.ClaimDefaultEntryVersion(user.UID, currentVer) + if !defaultEntryClaimAccepted(audience, user, now, claimed, err) { + return defaultEntryPageHome + } + } + if matched { + return page + } + return defaultEntryPageHome +} + +// newUserAdFreePosSet 返回【新人免广告-广告位列表】(VCodeNewUserAdFreePositions) 配置的广告位集合。 +// 命中新人免广告的用户,集合内的广告位(pos)不返回广告。 +func newUserAdFreePosSet(configure sysconfmod.ConfMap) map[int]struct{} { + codes := configure.GetStrSlice(sysconfmod.VCodeNewUserAdFreePositions) + set := make(map[int]struct{}, len(codes)) + for _, c := range codes { + pos, err := strconv.Atoi(c) + if err != nil { + log.Error("新人免广告广告位配置错误,必须为数字", log.Any("code", c)) + continue + } + set[pos] = struct{}{} + } + return set +} + +// DomainList doc +// @Summary 获取资源信息 +// @Description 获取域名/广告/版本等信息 +// @Tags PING +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} proto.SysInfo "{"msg": "操作成功"}" +// @Router /api/app/ping/domain [get] +func DomainList(ctx *gin.Context) { + wg := sync.WaitGroup{} + ua, _ := common.GetUA(ctx) + configure, _ := sysconfdata.GetAllFromCache() + uid, _ := common.GetUID(ctx) + + var user *usermod.User + var wallet *walletmod.Wallet + if uid > 0 { + user, _ = usermod.FindUserByUID(uid) + if user != nil { + wallet, _ = walletmod.GetWallet(user.UID) + } + } + + adFree := newUserAdFree(configure, user) + + wg.Add(8) + sysInfo := new(proto.SysInfo) + var ads proto.AdsRes + common.Go(func() { + defer wg.Done() + advSource := proto.AdvanceSource{ + PageBackground: configure.GetString(sysconfmod.VCodeAdvancePageBackground), + PageVidBackground: configure.GetString(sysconfmod.VCodeAdvancePageVidBackground), + ButtonBackground: configure.GetString(sysconfmod.VCodeAdvanceButtonBackground), + ButtonWaitBackground: configure.GetString(sysconfmod.VCodeAdvanceButtonWaitBackground), + ButtonProcBackground: configure.GetString(sysconfmod.VCodeAdvanceButtonProcBackground), + EnterBgWait: configure.GetString(sysconfmod.VCodeAdvanceEnterBgWait), + EnterBgProc: configure.GetString(sysconfmod.VCodeAdvanceEnterBgProc), + PopBgWait: configure.GetString(sysconfmod.VCodeAdvancePopBgWait), + PopBgProc: configure.GetString(sysconfmod.VCodeAdvancePopBgProc), + Banner: configure.GetString(sysconfmod.VCodeAdvanceBanner), + BannerWait: configure.GetString(sysconfmod.VCodeAdvanceBannerWait), + BannerProc: configure.GetString(sysconfmod.VCodeAdvanceBannerProc), + } + sysInfo.AdvancePage = advSource + + // 获取banner活动 + banner := make(map[int]bannerjumpmod.BannerJumpInfo) + bj, err := bannerjumpdata.GetAllFromCache() + if err != nil { + log.Error("获取banner活动发生错误", log.E(err)) + return + } + for _, b := range bj { + item, ok := buildBannerJumpInfo(&b, user, wallet) + if !ok { + continue + } + sysInfo.BannerJumpList = append(sysInfo.BannerJumpList, item) + if _, exists := banner[b.Position]; exists { + continue + } + banner[b.Position] = item + } + + sysInfo.BannerJump = banner + }) + common.Go(func() { + defer wg.Done() + + sysInfo.AiBubble = configure.GetStrSlice(sysconfmod.VCodeAiBubble) + sysInfo.AiCharacterImg = configure.GetString(sysconfmod.VCodeAiCharacterImg) + }) + common.Go(func() { + defer wg.Done() + sysInfo.Domain, sysInfo.SourceList = sourcemod.PingList() + sysInfo.JGArea, _ = jingangmod.GetJGListValid(nil) + for _, v := range sysInfo.JGArea { + v.LinkUrl = activityclient.ReplaceActivityDomain(v.LinkUrl, user, wallet) + } + }) + common.Go(func() { + defer wg.Done() + if user != nil && !user.ID.IsZero() { + sysInfo.SendMsgPrice = messageser.CheckChatPrice(user) + } + sysInfo.AdvanceStatus = advance_ser.GainAdvanceStatus(uid) + }) + common.Go(func() { + defer wg.Done() + //版本、广告、公告 + verResp, _, annouResp, _, _, _, err := versionser.AdvVersionAnnounThreeServer(ua.Ver, ua.SysType) + if err != nil { + log.Error("VersionThreeServer", log.E(err)) + } + for k, v := range annouResp { + v.Href = activityclient.ReplaceActivityDomain(v.Href, user, wallet) + annouResp[k] = v + } + //版本业务 + //安卓不做限制;iOS 端版本 <= 1.11.2 不下发版本信息 + skipVer := false + if ua.SysType == constant.SysTypeIOS { + if cur, err := version.New(ua.Ver); err == nil && cur.LTE(version.MustNew("1.11.2")) { + skipVer = true + } + } + if len(verResp.DownloadLink) > 0 && !skipVer { + versionBody := []*versionmod.VersionBody{ + &versionmod.VersionBody{ + VersionName: verResp.ServerVersion, + Platform: ua.SysType, + Description: verResp.Description, + ForcedUpdate: verResp.IsForceUpdate, + URL: verResp.DownloadLink[0], + IosUrl: verResp.DownloadLink[0], + }} + sysInfo.Ver = versionBody + } + //公告 + sysInfo.Ads.AnnounList = annouResp + }) + common.Go(func() { + defer wg.Done() + jtAds, err := adser.JtAdvertiseThreeServer() + if err != nil { + log.Error("JtAdvertiseThreeServer error occur", log.E(err)) + return + } + // 新人免广告:命中开关时,构建需要屏蔽的广告位集合 + freePosSet := make(map[int]struct{}) + if adFree { + freePosSet = newUserAdFreePosSet(configure) + } + // 默认空列表,避免序列化为 null + adsList := []proto.AdsInfo{} + for _, loc := range jtAds { + pos, err := strconv.Atoi(loc.AdvertiseLocationCode) + if err != nil { + log.Error("广告位置代码错误,必须为数字", log.Any("code", loc.AdvertiseLocationCode)) + continue + } + // 100000 以上保留为娱乐广告 + if pos > 100000 { + continue + } + // 新人免广告:命中配置的广告位则跳过,不返回该广告位的广告 + if _, ok := freePosSet[pos]; ok { + continue + } + for _, ad := range loc.AdDetailInfoList { + extra := ad.GetExtraData() + adsinfo := proto.AdsInfo{ + ID: ad.AdvertiseCode, + Title: ad.AdvertiseName, + Cover: ad.GetCoverLsj(), + Href: ad.GetRealLink(user, wallet), + Position: pos, + PositionName: loc.AdvertiseLocationName, + SortCode: ad.Sort, + CoverImgSize: extra.CoverImgSize, + WatchTime: extra.WatchTime, + } + adsList = append(adsList, adsinfo) + } + } + sysInfo.Ads.AdsList = adsList + }) + common.Go(func() { + defer wg.Done() + var e error + sysInfo.PaymentStatusPopupConfig, sysInfo.PaymentStatusPopup, e = sys_config.SysConfUserPaymentStatusPopup(user) + if e != nil { + // 分层弹窗配置失败不阻断整个接口,降级为空配置 + log.Error("SysConfUserPaymentStatusPopup error", log.E(e)) + } + }) + common.Go(func() { + defer wg.Done() + var e error + sysInfo.PaymentGuide, e = paymentguideser.GetPingGuide(user) + if e != nil { + // 新版付费引导失败不阻断 Ping,降级为不展示。 + log.Error("GetPingGuide error", log.E(e)) + } + }) + wg.Wait() + item, _ := json.Marshal(ads) + log.Info(fmt.Sprintf("home:%s", string(item))) + sysInfo.SystemConfigList = []*systemmod.Config{} + sysInfo.TotalWatch = sys_config.GetTotalWatchCount() + sysInfo.RandomBanner = appg.Conf.RandomBanner + //sysInfo.Active2023URL = appg.Conf.URL.Active2023 + "?appId=" + strconv.FormatInt(int64(commod.KFK_APPID), 10) + sysInfo.AdsTimeLongVideo = commod.AdsTimeLongVideo + sysInfo.HlH5URL = appg.Conf.URL.HlH5Url + if configure.GetBool(sysconfmod.VCodeLotteryEnable) { + sysInfo.LuckyDrawIcon = configure.GetString(sysconfmod.VCodeLotteryIcon) + sysInfo.LuckyDrawH5 = appg.Conf.URL.LuckyDrawH5 + luckyDrawUrl := configure.GetString(sysconfmod.VCodeLotteryUrl) + if luckyDrawUrl != "" { + sysInfo.LuckyDrawH5 = activityclient.ReplaceActivityDomain(luckyDrawUrl, user, wallet) + } + } + sysInfo.AiUndressPrice = configure.GetInt(sysconfmod.VCodeAiUndressPrice) + sysInfo.AiImageToVideoPrice = configure.GetInt(sysconfmod.VCodeAiImageToVideoPrice) + sysInfo.AiTextToImagePrice = configure.GetInt(sysconfmod.VCodeAiTextToImagePrice) + sysInfo.Broadcast = configure.GetBool(sysconfmod.VCodeBroadcast) + sysInfo.StoreIsOpen = configure.GetBool(sysconfmod.VCodeStoreOpen) + sysInfo.BackgroundTheme = constant.ThemeDefault + sysInfo.HotSearchTerms = configure.GetStrSlice(sysconfmod.VCodeHotSearchTerms) + sysInfo.SearchHintWord = configure.GetStrSlice(sysconfmod.VCodeSearchHintWord) + sysInfo.FestivalUi = configure.GetString(sysconfmod.VCodeFestivalUi) + sysInfo.AiGirlFriend = configure.GetBool(sysconfmod.VCodeAiGirlFriend) + sysInfo.AiUndress = configure.GetBool(sysconfmod.VCodeAiUndress) + sysInfo.AiImageChangeFace = configure.GetBool(sysconfmod.VCodeAiImageChangeFace) + sysInfo.AiVideoChangeFace = configure.GetBool(sysconfmod.VCodeAiVideoChangeFace) + sysInfo.AiTextToNovelPrice = configure.GetInt(sysconfmod.VCodeAiTextToNovelPrice) + sysInfo.QmdlUrl = configure.GetString(sysconfmod.VCodeQMDL) + sysInfo.DarkWebVipName = configure.GetString(sysconfmod.VCodeDarkWebVipName) + sysInfo.DarkWebVipId = configure.GetString(sysconfmod.VCodeDarkWebVipId) + sysInfo.RecommendVipIds = configure.GetStrSlice(sysconfmod.VCodeRecommendVipId) + sysInfo.ShortDramaCardID = configure.GetString(sysconfmod.VCodeShortDramaCardID) + sysInfo.ShortDramaEntryPopupEnabled = shortDramaEntryPopupEnabled(configure) + sysInfo.DefaultEntryPage = resolveDefaultEntryPage(configure, user, ua.Ver) + sysInfo.DefaultEntryAudience = configure.GetString(sysconfmod.VCodeDefaultEntryAudience) + sysInfo.NewbieSaleTime = configure.GetInt(sysconfmod.VCodeNewbieSaleTime) + sysInfo.PrivateZoneVipName = configure.GetString(sysconfmod.VCodePrivateZoneVipName) + sysInfo.PrivateZoneVipId = configure.GetString(sysconfmod.VCodePrivateZoneVipId) + sysInfo.ReturnSaleVipIds = configure.GetStrSlice(sysconfmod.VCodeReturnSaleVipIds) + sysInfo.OldReturnSaleTime = configure.GetInt(sysconfmod.VCodeOldReturnSaleTime) + sysInfo.Video1 = configure.GetString(sysconfmod.VCodeVideo1) + sysInfo.Video2 = configure.GetString(sysconfmod.VCodeVideo2) + sysInfo.PersonalCenterBackground = configure.GetString(sysconfmod.VCodePersonalCenterBackground) + sysInfo.AIMateH5 = ai_mate_ser.GetApiUrl() + sysInfo.ReportUrl = appg.Conf.DataReport.AppUrl + sysInfo.FreeMark = configure.GetBool(sysconfmod.VCodeFreeMark) + sysInfo.VipMark = configure.GetBool(sysconfmod.VCodeVipMark) + sysInfo.CoinMark = configure.GetBool(sysconfmod.VCodeCoinMark) + sysInfo.AiSwitchConf = doAiSwitchConf(configure.GetObject(sysconfmod.VCodeAiSort), configure.GetObject(sysconfmod.VCodeAiSwitch)) + sysInfo.SignIcon = configure.GetString(sysconfmod.VCodeSignIcon) + sysInfo.DarkWebEnable = configure.GetBool(sysconfmod.VCodeDarkWebEnable) + sysInfo.DarkWebImg = configure.GetString(sysconfmod.VCodeDarkWebImg) + sysInfo.DarkWebIcon = configure.GetString(sysconfmod.VCodeDarkWebIcon) + sysInfo.DarkWebIconName = configure.GetString(sysconfmod.VCodeDarkWebIconName) + + common.ServeJSON(ctx, stderr.Success, sysInfo) +} + +// Ping doc +// @Summary 域名测试 +// @Description 测试域名是否正常 +// @Tags PING +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} proto.GinH{response=string} "response:返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/ping/check [get] +func Ping(ctx *gin.Context) { + common.ServeJSON(ctx, stderr.Success, gin.H{"response": "pong"}) +} + +// Ping doc +// @Summary 域名 +// @Description 域名 +// @Tags PING +// @Accept mpfd,json +// @Produce json,html +// @Param ver query string true "版本号" +// @Param buildId query string true "安装包ID" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/pass [get] +func Pass(ctx *gin.Context) { + ver := ctx.Param("ver") + buildId := ctx.Param("buildId") + if ver == "" || buildId == "" { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + pass, err := versionmod.CheckPass(ver, buildId) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, nil) + return + } + ctx.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "msg": "success", "data": gin.H{"pass": pass}}) +} + +// GetSysDate doc +// @Summary 获取服务器时间 +// @Description 获取服务器时间 +// @Tags PING +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ping/sysDate [get] +func GetSysDate(ctx *gin.Context) { + common.ServeJSON(ctx, http.StatusOK, gin.H{"sysDate": time.Now()}) +} + +func M(ctx *gin.Context) { + ctx.String(200, "%d", 0) +} + +// Domain doc +// @Summary 获取资源信息(web) +// @Description 获取域名/广告/版本等信息(web) +// @Tags PING +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} proto.SysInfos "{"msg": "操作成功"}" +// @Router /api/app/ping/domain/h5 [get] +func Domain(ctx *gin.Context) { + wg := sync.WaitGroup{} + ua, _ := common.GetUA(ctx) + configure, _ := sysconfdata.GetAllFromCache() + uid, _ := common.GetUID(ctx) + + var user *usermod.User + var wallet *walletmod.Wallet + if uid > 0 { + user, _ = usermod.FindUserByUID(uid) + if user != nil { + wallet, _ = walletmod.GetWallet(user.UID) + } + } + + adFree := newUserAdFree(configure, user) + + wg.Add(8) + sysInfo := new(proto.SysInfos) + common.Go(func() { + defer wg.Done() + advSource := proto.AdvanceSource{ + PageBackground: configure.GetString(sysconfmod.VCodeAdvancePageBackground), + PageVidBackground: configure.GetString(sysconfmod.VCodeAdvancePageVidBackground), + ButtonBackground: configure.GetString(sysconfmod.VCodeAdvanceButtonBackground), + ButtonWaitBackground: configure.GetString(sysconfmod.VCodeAdvanceButtonWaitBackground), + ButtonProcBackground: configure.GetString(sysconfmod.VCodeAdvanceButtonProcBackground), + EnterBgWait: configure.GetString(sysconfmod.VCodeAdvanceEnterBgWait), + EnterBgProc: configure.GetString(sysconfmod.VCodeAdvanceEnterBgProc), + PopBgWait: configure.GetString(sysconfmod.VCodeAdvancePopBgWait), + PopBgProc: configure.GetString(sysconfmod.VCodeAdvancePopBgProc), + Banner: configure.GetString(sysconfmod.VCodeAdvanceBanner), + BannerWait: configure.GetString(sysconfmod.VCodeAdvanceBannerWait), + BannerProc: configure.GetString(sysconfmod.VCodeAdvanceBannerProc), + } + sysInfo.AdvancePage = advSource + + // 获取banner活动 + banner := make(map[int]bannerjumpmod.BannerJumpInfo) + bj, err := bannerjumpdata.GetAllFromCache() + if err != nil { + log.Error("获取banner活动发生错误", log.E(err)) + return + } + for _, b := range bj { + item, ok := buildBannerJumpInfo(&b, user, wallet) + if !ok { + continue + } + sysInfo.BannerJumpList = append(sysInfo.BannerJumpList, item) + if _, exists := banner[b.Position]; exists { + continue + } + banner[b.Position] = item + } + + sysInfo.BannerJump = banner + }) + common.Go(func() { + defer wg.Done() + + sysInfo.AiBubble = configure.GetStrSlice(sysconfmod.VCodeAiBubble) + sysInfo.AiCharacterImg = configure.GetString(sysconfmod.VCodeAiCharacterImg) + }) + common.Go(func() { + defer wg.Done() + sysInfo.Domain, sysInfo.SourceList = sourcemod.PingList() + sysInfo.JGArea, _ = jingangmod.GetJGListValid(nil) + for _, v := range sysInfo.JGArea { + v.LinkUrl = activityclient.ReplaceActivityDomain(v.LinkUrl, user, wallet) + } + }) + common.Go(func() { + defer wg.Done() + if user != nil && !user.ID.IsZero() { + sysInfo.SendMsgPrice = messageser.CheckChatPrice(user) + } + sysInfo.AdvanceStatus = advance_ser.GainAdvanceStatus(uid) + }) + common.Go(func() { + defer wg.Done() + //版本、广告、公告 + verResp, _, annouResp, iosUrl, androidUrl, shopIosLink, err := versionser.AdvVersionAnnounThreeServer(ua.Ver, "ios") + if err != nil { + log.Error("VersionThreeServer", log.E(err)) + return + } + //版本业务 + if len(verResp.DownloadLink) > 0 { + versionBody := []*versionmod.VersionBody{ + &versionmod.VersionBody{ + VersionName: verResp.ServerVersion, + Platform: ua.SysType, + Description: verResp.Description, + ForcedUpdate: verResp.IsForceUpdate, + URL: verResp.DownloadLink[0], + }} + sysInfo.Ver = versionBody + } + for k, v := range annouResp { + v.Href = activityclient.ReplaceActivityDomain(v.Href, user, wallet) + annouResp[k] = v + } + //公告 + sysInfo.AnnounList = annouResp + + sysInfo.IosLink = iosUrl + sysInfo.AndLink = androidUrl + sysInfo.ShopIosLink = shopIosLink + }) + common.Go(func() { + defer wg.Done() + jtAds, err := adser.JtAdvertiseThreeServer() + if err != nil { + log.Error("JtAdvertiseThreeServer error occur", log.E(err)) + return + } + // 新人免广告:命中开关时,构建需要屏蔽的广告位集合 + freePosSet := make(map[int]struct{}) + if adFree { + freePosSet = newUserAdFreePosSet(configure) + } + // 默认空列表,避免序列化为 null + adsList := []proto.AdsInfo{} + for _, loc := range jtAds { + pos, err := strconv.Atoi(loc.AdvertiseLocationCode) + if err != nil { + log.Error("广告位置代码错误,必须为数字", log.Any("code", loc.AdvertiseLocationCode)) + continue + } + // 100000 以上保留为娱乐广告 + if pos > 100000 { + continue + } + // 新人免广告:命中配置的广告位则跳过,不返回该广告位的广告 + if _, ok := freePosSet[pos]; ok { + continue + } + for _, ad := range loc.AdDetailInfoList { + extra := ad.GetExtraData() + adsinfo := proto.AdsInfo{ + ID: ad.AdvertiseCode, + Title: ad.AdvertiseName, + Cover: ad.GetCoverLsj(), + Href: ad.GetRealLink(user, wallet), + Position: pos, + PositionName: loc.AdvertiseLocationName, + SortCode: ad.Sort, + CoverImgSize: extra.CoverImgSize, + WatchTime: extra.WatchTime, + } + adsList = append(adsList, adsinfo) + } + } + sysInfo.AdsList = adsList + }) + common.Go(func() { + defer wg.Done() + var e error + sysInfo.PaymentStatusPopupConfig, sysInfo.PaymentStatusPopup, e = sys_config.SysConfUserPaymentStatusPopup(user) + if e != nil { + // 分层弹窗配置失败不阻断整个接口,降级为空配置 + log.Error("SysConfUserPaymentStatusPopup error", log.E(e)) + } + }) + common.Go(func() { + defer wg.Done() + var e error + sysInfo.PaymentGuide, e = paymentguideser.GetPingGuide(user) + if e != nil { + // 新版付费引导失败不阻断 Ping,降级为不展示。 + log.Error("GetPingGuide error", log.E(e)) + } + }) + wg.Wait() + sysInfo.SystemConfigList = []*systemmod.Config{} + sysInfo.TotalWatch = sys_config.GetTotalWatchCount() + sysInfo.EKey = requestEncrypt.PubKey + sysInfo.RandomBanner = appg.Conf.RandomBanner + //sysInfo.Active2023URL = appg.Conf.URL.Active2023 + "?appId=" + strconv.FormatInt(int64(commod.KFK_APPID), 10) + sysInfo.AdsTimeLongVideo = commod.AdsTimeLongVideo + sysInfo.HlH5URL = appg.Conf.URL.HlH5Url + if configure.GetBool(sysconfmod.VCodeLotteryEnable) { + sysInfo.LuckyDrawIcon = configure.GetString(sysconfmod.VCodeLotteryIcon) + sysInfo.LuckyDrawH5 = appg.Conf.URL.LuckyDrawH5 + luckyDrawUrl := configure.GetString(sysconfmod.VCodeLotteryUrl) + if luckyDrawUrl != "" { + sysInfo.LuckyDrawH5 = activityclient.ReplaceActivityDomain(luckyDrawUrl, user, wallet) + } + } + sysInfo.AiUndressPrice = configure.GetInt(sysconfmod.VCodeAiUndressPrice) + sysInfo.AiImageToVideoPrice = configure.GetInt(sysconfmod.VCodeAiImageToVideoPrice) + sysInfo.AiTextToImagePrice = configure.GetInt(sysconfmod.VCodeAiTextToImagePrice) + sysInfo.Broadcast = configure.GetBool(sysconfmod.VCodeBroadcast) + sysInfo.StoreIsOpen = configure.GetBool(sysconfmod.VCodeStoreOpen) + sysInfo.BackgroundTheme = constant.ThemeDefault + sysInfo.HotSearchTerms = configure.GetStrSlice(sysconfmod.VCodeHotSearchTerms) + sysInfo.SearchHintWord = configure.GetStrSlice(sysconfmod.VCodeSearchHintWord) + sysInfo.FestivalUi = configure.GetString(sysconfmod.VCodeFestivalUi) + sysInfo.AiGirlFriend = configure.GetBool(sysconfmod.VCodeAiGirlFriend) + sysInfo.AiUndress = configure.GetBool(sysconfmod.VCodeAiUndress) + sysInfo.AiImageChangeFace = configure.GetBool(sysconfmod.VCodeAiImageChangeFace) + sysInfo.AiVideoChangeFace = configure.GetBool(sysconfmod.VCodeAiVideoChangeFace) + sysInfo.AiTextToNovelPrice = configure.GetInt(sysconfmod.VCodeAiTextToNovelPrice) + sysInfo.QmdlUrl = configure.GetString(sysconfmod.VCodeQMDL) + sysInfo.DarkWebVipName = configure.GetString(sysconfmod.VCodeDarkWebVipName) + sysInfo.DarkWebVipId = configure.GetString(sysconfmod.VCodeDarkWebVipId) + sysInfo.RecommendVipIds = configure.GetStrSlice(sysconfmod.VCodeRecommendVipId) + sysInfo.ShortDramaCardID = configure.GetString(sysconfmod.VCodeShortDramaCardID) + sysInfo.ShortDramaEntryPopupEnabled = shortDramaEntryPopupEnabled(configure) + sysInfo.DefaultEntryPage = resolveDefaultEntryPage(configure, user, ua.Ver) + sysInfo.DefaultEntryAudience = configure.GetString(sysconfmod.VCodeDefaultEntryAudience) + sysInfo.PrivateZoneVipName = configure.GetString(sysconfmod.VCodePrivateZoneVipName) + sysInfo.PrivateZoneVipId = configure.GetString(sysconfmod.VCodePrivateZoneVipId) + sysInfo.ReturnSaleVipIds = configure.GetStrSlice(sysconfmod.VCodeReturnSaleVipIds) + sysInfo.OldReturnSaleTime = configure.GetInt(sysconfmod.VCodeOldReturnSaleTime) + sysInfo.NewbieSaleTime = configure.GetInt(sysconfmod.VCodeNewbieSaleTime) + sysInfo.AIMateH5 = ai_mate_ser.GetApiUrl() + sysInfo.Video1 = configure.GetString(sysconfmod.VCodeVideo1) + sysInfo.Video2 = configure.GetString(sysconfmod.VCodeVideo2) + sysInfo.PersonalCenterBackground = configure.GetString(sysconfmod.VCodePersonalCenterBackground) + sysInfo.ReportUrl = appg.Conf.DataReport.AppUrl + sysInfo.FreeMark = configure.GetBool(sysconfmod.VCodeFreeMark) + sysInfo.VipMark = configure.GetBool(sysconfmod.VCodeVipMark) + sysInfo.CoinMark = configure.GetBool(sysconfmod.VCodeCoinMark) + sysInfo.AiSwitchConf = doAiSwitchConf(configure.GetObject(sysconfmod.VCodeAiSort), configure.GetObject(sysconfmod.VCodeAiSwitch)) + sysInfo.SignIcon = configure.GetString(sysconfmod.VCodeSignIcon) + sysInfo.DarkWebEnable = configure.GetBool(sysconfmod.VCodeDarkWebEnable) + sysInfo.DarkWebImg = configure.GetString(sysconfmod.VCodeDarkWebImg) + sysInfo.DarkWebIcon = configure.GetString(sysconfmod.VCodeDarkWebIcon) + sysInfo.DarkWebIconName = configure.GetString(sysconfmod.VCodeDarkWebIconName) + + common.ServeJSON(ctx, stderr.Success, sysInfo) +} + +// CheckMessageTip doc +// @Summary 检查消息小红点 +// @Description 检查消息小红点 +// @Tags PING +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ping/checkMessageTip [get] +func CheckMessageTip(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + tip := message.CheckTip(uid) + + common.ServeJSON(ctx, http.StatusOK, gin.H{"newsTip": tip}) + return +} + +func StoreUrl(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil || uid == 0 { + common.ServeJSON(ctx, stderr.Success, nil) + return + } + user, err := usermod.FindUserByUID(uid) + if err != nil { + return + } + if user == nil { + return + } + var balance int64 + w, _ := walletmod.GetWallet(uid) + if w != nil { + balance = w.Income + w.Amount + } + shopUrl := store.GetStoreLink(&store.UserData{ + AppUid: user.UID, + AppId: int(commod.KFK_APPID), + Name: user.Name, + Portrait: user.Portrait, + ExpireTime: time.Now().Add(time.Hour * 24 * 2).Unix(), + Balance: balance, + }) + common.ServeJSON(ctx, stderr.Success, shopUrl) +} + +// buildBannerJumpInfo 根据 banner DB 数据构造下发 DTO。 +// 倒计时类型按 Url 中的 type 参数推导(与 task/list 保持一致的判断方式)。 +// 返回 ok=false 表示该 banner 当前不应下发: +// - Url 含 type=hongbaoRain 但活动服无可用红包雨场次 +// +// countdownType=1 时,StartAt/EndAt 用场次起止时间覆盖;否则保留 banner 自身时间。 +func buildBannerJumpInfo(b *bannerjumpmod.BannerJump, user *usermod.User, wallet *walletmod.Wallet) (bannerjumpmod.BannerJumpInfo, bool) { + cdStart, cdEnd, cdType, ok := activityclient.ResolveCountdownByLink(b.Url) + if !ok { + return bannerjumpmod.BannerJumpInfo{}, false + } + startAt, endAt := b.StartAt, b.EndAt + if cdType == 1 { + startAt, endAt = cdStart, cdEnd + } + return bannerjumpmod.BannerJumpInfo{ + ID: b.ID, + Position: b.Position, + Banner: b.Banner, + Title: b.Title, + Url: activityclient.ReplaceActivityDomain(b.Url, user, wallet), + StartAt: startAt, + EndAt: endAt, + CountdownType: cdType, + }, true +} + +// GetBannerJump doc +// @Summary 通过浮窗ID获取浮窗信息 +// @Description 按浮窗ID返回单个浮窗的最新信息,等同于 /ping/domain 中对应 banner 的状态。countdownType=1 但当前无可用红包雨场次时返回空数据 +// @Tags PING +// @Accept json +// @Produce json +// @Param id path string true "浮窗ID" +// @Success 200 {object} bannerjumpmod.BannerJumpInfo "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "参数错误"}" +// @Router /api/app/ping/banner/{id} [get] +func GetBannerJump(ctx *gin.Context) { + id, err := primitive.ObjectIDFromHex(ctx.Param("id")) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + uid, _ := common.GetUID(ctx) + var user *usermod.User + var wallet *walletmod.Wallet + if uid != 0 { + user, _ = usermod.FindUserByUID(uid) + wallet, _ = walletmod.GetWallet(uid) + } + + bj, err := bannerjumpdata.GetAllFromCache() + if err != nil { + log.Error("获取banner活动发生错误", log.E(err)) + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, nil) + return + } + + for i := range bj { + if bj[i].ID != id { + continue + } + info, ok := buildBannerJumpInfo(&bj[i], user, wallet) + if !ok { + common.ServeJSON(ctx, stderr.CodeEmptyData, nil) + return + } + common.ServeJSON(ctx, stderr.Success, info) + return + } + common.ServeJSON(ctx, stderr.CodeEmptyData, nil) +} + +func doAiSwitchConf(aiSort map[string]string, aiSwitch map[string]string) []proto.AISwitchConf { + list := make([]proto.AISwitchConf, 0) + for i := 0; i < 7; i++ { + conf := proto.AISwitchConf{ + Type: i + 1, // 类型从1(脱衣)开始 + Sort: i + 7, // 默认排后面 + IsOpen: true, // 默认开启状态 + } + key := strconv.Itoa(conf.Type) + if _, ok := aiSort[key]; ok { + sortInt, _ := strconv.Atoi(aiSort[key]) + conf.Sort = sortInt + } + if _, ok := aiSwitch[key]; ok { + isOpen, _ := strconv.Atoi(aiSwitch[key]) + conf.IsOpen = isOpen == 1 + } + list = append(list, conf) + } + return list +} + +// DomainRefresh doc +// @Summary 按需刷新资源信息 +// @Description 按 keys 增量刷新部分资源(当前支持 paymentPopup/paymentGuide) +// @Tags PING +// @Accept mpfd,json +// @Produce json,html +// @Param keys query []string true "刷新项,如 paymentPopup/paymentGuide" +// @Success 200 {object} proto.SysInfoRefresh "{"msg": "操作成功"}" +// @Router /api/app/ping/domain/refresh [get] +func DomainRefresh(ctx *gin.Context) { + var p = &struct { + Keys []string `json:"keys" form:"keys" binding:"required"` + }{} + if err := ctx.ShouldBind(p); err != nil { + log.Error(fmt.Sprintf("DomainRefresh param err:%v", err)) + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + var user *usermod.User + if uid, err := common.GetUID(ctx); err == nil && uid > 0 { + user, _ = usermod.FindUserByUID(uid) + } + + var resp = proto.SysInfoRefresh{} + for _, key := range p.Keys { + switch key { + case "paymentPopup": + if user == nil { + continue + } + paymentStatusPopupConfig, paymentStatusPopup, err := sys_config.SysConfUserPaymentStatusPopup(user) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, "SysConfUserPaymentStatusPopup Error: "+err.Error()) + return + } + resp.PaymentPopup = proto.SysInfoRefreshPaymentPopup{ + PaymentStatusPopup: paymentStatusPopup, + Homepage: paymentStatusPopupConfig.Homepage, + HomepageFlot: paymentStatusPopupConfig.HomepageFlot, + PlayPage: paymentStatusPopupConfig.PlayPage, + MeTab: paymentStatusPopupConfig.MeTab, + VipCard: paymentStatusPopupConfig.VipCard, + LastDiscountTime: paymentStatusPopupConfig.LastDiscountTime, + } + case "paymentGuide": + paymentGuide, err := paymentguideser.GetPingGuide(user) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, "GetPingGuide Error: "+err.Error()) + return + } + resp.PaymentGuide = paymentGuide + default: + log.Error(fmt.Sprintf("DomainRefresh unknown key:%v", key)) + } + } + common.ServeJSON(ctx, stderr.Success, resp) +} diff --git a/app/api/productctrl/productctrl.go b/app/api/productctrl/productctrl.go new file mode 100644 index 0000000..6fa48fe --- /dev/null +++ b/app/api/productctrl/productctrl.go @@ -0,0 +1,226 @@ +package productctrl + +import ( + "91porn-server/app/service/advance_ser" + "91porn-server/app/service/integral_config_ser" + "91porn-server/models/v/integralconfigmod" + "net/http" + + "91porn-server/app/service/productser" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type BuyProductRequest struct { + ProductType commod.ProductType `form:"productType" json:"productType" bson:"productType"` + ProductID primitive.ObjectID `form:"productID" json:"productID" binding:"required" bson:"productID"` + ContentID primitive.ObjectID `form:"contentID" json:"contentID" bson:"contentID"` + CheckoutContextID string `form:"checkoutContextId" json:"checkoutContextId"` + ChapterID string `form:"chapterID" json:"chapterID"` + CouponID primitive.ObjectID `form:"couponId" json:"couponId"` + ServiceID primitive.ObjectID `form:"serviceId" json:"serviceId"` + GoldVideoCouponNum int `form:"goldVideoCouponNum" json:"goldVideoCouponNum"` + IsH5 bool `form:"isH5" json:"isH5"` + Num uint64 `json:"num" form:"num"` + UserContact string `json:"userContact" form:"userContact"` + ExperimentID string `json:"experimentId" form:"experimentId"` + ExperimentVariant string `json:"experimentVariant" form:"experimentVariant"` + SessionID string `json:"sessionId" form:"sessionId"` +} + +// BuyProduct doc +// @Summary 商品购买 +// @Description 商品购买 +// @Tags product +// @Accept json,mpfd +// @Produce json,html +// @Param request body productctrl.BuyProductRequest true "购买参数;短剧单集解锁时productType=19且contentID、checkoutContextId必填" +// @Param X-Request-ID header string false "短剧单集购买幂等ID" +// @Success 200 {object} productser.BuyDramaEpisodeResponse "短剧单集购买成功时的数据结构" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/product/buy [post] +func BuyProduct(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + ua, err := common.GetUA(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + ip := common.GetIP(ctx) + args := BuyProductRequest{} + if err = ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if args.ProductType == commod.Media && !args.ContentID.IsZero() { + data, code := productser.BuyDramaEpisode( + uid, args.ProductID, args.ContentID, args.CheckoutContextID, + ctx.GetHeader("X-Request-ID"), ua, ip, + ) + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, code, data) + return + } + code := productser.Buy(uid, args.ProductType, args.ProductID, args.CouponID, args.ServiceID, args.Num, args.UserContact, ua.SysType, + args.ChapterID, args.GoldVideoCouponNum, args.IsH5, ua, ip, productser.VIPExperimentAttribution{ + ExperimentID: args.ExperimentID, + ExperimentVariant: args.ExperimentVariant, + SessionID: args.SessionID, + }) + common.ServeJSON(ctx, code, nil) +} + +// DelBroughtProductHistory doc +// @Summary 删除购买视频 +// @Description 删除购买视频 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Param productID formData string true "产品id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /product/delBrought [post] +func DelBroughtProductHistory(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + args := struct { + ProductID primitive.ObjectID `form:"productID" json:"productID" binding:"required" bson:"productID"` //产品id + }{} + if err = ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = productser.DelBroughtHistory(args.ProductID, uid); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + common.ServeJSON(ctx, http.StatusOK, nil) +} + +// 获取优惠卷详情 +func GetCouponDetail(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + ua, err := common.GetUA(c) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, err.Error()) + return + } + var params struct { + ProductType commod.ProductType `form:"productType"` //产品类型 + ProductID string `form:"productId" binding:"required"` //产品id + } + if err = c.ShouldBindQuery(¶ms); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + data, code := productser.GetCouponDetail(uid, params.ProductID, params.ProductType, ua.SysType) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, data) +} + +// 金币月卡获取金币 +func GetCoinMonthCoin(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + data, code := productser.GetCoin(uid) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, data) +} + +// GetAwVip doc +// @Summary 获取暗网VIP会员卡 +// @Description 获取暗网VIP会员卡 +// @Tags 产品配置 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/product/getAwVip [get] +func GetAwVip(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + data, code := productser.GetAwVipInfo(uid) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, data) +} + +// ExchangeIntegral doc +// @Summary 积分兑换 +// @Description 积分兑换 +// @Tags 产品配置 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "积分兑换配置ID" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/product/exchangeIntegral [post] +func ExchangeIntegral(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var in integralconfigmod.ExchangeIntegralReq + if err = ctx.ShouldBind(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code := integral_config_ser.ExchangeIntegral(uid, &in) + if code != stderr.Success { + common.ServeJSON(ctx, code, code.Error()) + return + } + common.ServeJSON(ctx, code, stderr.Success.Msg()) +} + +// AdvanceStatus doc +// @Summary 获取预售状态 +// @Description 获取预售状态 +// @Tags 产品配置 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 object advanceordermod.AdvanceStatus "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/product/advanceStatus [get] +func AdvanceStatus(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + advanceStatus := advance_ser.GainAdvanceStatus(uid) + common.ServeJSON(c, stderr.Success, advanceStatus) +} diff --git a/app/api/publishctrl/publish.go b/app/api/publishctrl/publish.go new file mode 100644 index 0000000..8520de0 --- /dev/null +++ b/app/api/publishctrl/publish.go @@ -0,0 +1,122 @@ +package publishctrl + +import ( + "sync" + + "91porn-server/app/service/publishser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/vidmod" + + "github.com/gin-gonic/gin" +) + +// Details doc +// @Summary 创作视频 +// @Description 数据详情 +// @Tags 发布 +// @Accept json +// @Produce json +// @Success 200 object publishser.DetailsResponse "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /publish/details [get] +func Details(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err.Error()) + return + } + var ( + res publishser.DetailsResponse + wg sync.WaitGroup + ) + wg.Add(7) + common.Go(func() { + defer wg.Done() + res.IsFirst, err = vidmod.IsSubmitByPublisherID(uid) + }) + common.Go(func() { + defer wg.Done() + var weekIncomeLeaderboard []publishser.User + weekIncomeLeaderboard, err = publishser.GetWeekIncomeLeaderboard(3) + if err != nil { + return + } + res.Leaderboards = append(res.Leaderboards, publishser.Leaderboard{ + Type: publishser.WeekIncomeLeaderboard, + Members: weekIncomeLeaderboard, + }) + }) + common.Go(func() { + defer wg.Done() + var weekWorkLeaderboard []publishser.User + weekWorkLeaderboard, err = publishser.GetWeekWorkLeaderboard(3) + if err != nil { + return + } + res.Leaderboards = append(res.Leaderboards, publishser.Leaderboard{ + Type: publishser.WeekWorkLeaderboard, + Members: weekWorkLeaderboard, + }) + }) + common.Go(func() { + defer wg.Done() + res.PendingReviewWorkCount, err = publishser.GetPendingReviewWorkCount(uid) + }) + common.Go(func() { + defer wg.Done() + res.WorkTotal, err = publishser.GetWorkTotal(uid) + }) + common.Go(func() { + defer wg.Done() + res.ActivityDetails, err = publishser.GetActivityDetails() + }) + common.Go(func() { + defer wg.Done() + res.WorkCreateCount, err = publishser.GetCreatorNumber() + }) + wg.Wait() + res.PassWorkCount = res.WorkTotal - res.PendingReviewWorkCount + if err != nil { + log.Error(err.Error()) + common.ServeJSON(c, stderr.ErrNetWorkBusy, err.Error()) + return + } + common.ServeJSON(c, stderr.Success, res) +} + +// WeekLeaderboard doc +// @Summary 周榜详情 +// @Description 周榜-查看更多 +// @Tags 发布 +// @Accept json +// @Produce json +// @Param type query int true "榜单类型" +// @Success 200 object publishser.WeekLeaderboardResp "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /publish/leaderboard [get] +func WeekLeaderboard(c *gin.Context) { + var ( + req publishser.WeekLeaderboardReq + leaderboard []publishser.User + err error + ) + if err = c.ShouldBindQuery(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err.Error()) + return + } + switch publishser.ListType(req.Type) { + case publishser.WeekIncomeLeaderboard: + if leaderboard, err = publishser.GetWeekIncomeLeaderboard(10); err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, err.Error()) + return + } + case publishser.WeekWorkLeaderboard: + if leaderboard, err = publishser.GetWeekWorkLeaderboard(10); err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, err.Error()) + return + } + } + common.ServeJSON(c, stderr.Success, publishser.Leaderboard{Type: publishser.ListType(req.Type), Members: leaderboard}) +} diff --git a/app/api/rankctrl/rankctrl.go b/app/api/rankctrl/rankctrl.go new file mode 100644 index 0000000..62e7f04 --- /dev/null +++ b/app/api/rankctrl/rankctrl.go @@ -0,0 +1,27 @@ +package rankctrl + +import ( + "91porn-server/app/service/rankser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// HotSearchList doc +// @Summary 排行榜 - 热搜排行榜列表 +// @Description 获取热搜视频 +// @Tags rank +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /rank/hotsearch/list [get] +func HotSearchList(ctx *gin.Context) { + data, err := rankser.GetHotSearchList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, err) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/rechargectrl/DaBaiSha.go b/app/api/rechargectrl/DaBaiSha.go new file mode 100644 index 0000000..45b44ee --- /dev/null +++ b/app/api/rechargectrl/DaBaiSha.go @@ -0,0 +1,72 @@ +package rechargectrl + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "91porn-server/app/service/rechargeser" + "91porn-server/common/log" + "91porn-server/common/rchgutil" + + "github.com/gin-gonic/gin" +) + +// DaBaiShaCallBack 金鱼结构回调函数 +func DaBaiShaCallBack(ctx *gin.Context) { + if err := func() error { + rchg := rchgutil.DaBaiShaRes{} + g := rchgutil.DaBaiSha{} + if err := ctx.ShouldBindJSON(&rchg); err != nil { + log.Error(fmt.Sprintf("DaBaiSha callback parameter bind fail error:%+v:", err)) + return err + } + bs, _ := json.Marshal(rchg) + log.Info(fmt.Sprintf("DaBaiSha callback parameter data:%+v:", string(bs))) + buf := bytes.Buffer{} + buf.WriteString(strconv.Itoa(rchg.Code)) + buf.WriteString(rchg.MercID) + buf.WriteString(rchg.OID) + buf.WriteString(rchg.PayMoney) + buf.WriteString(rchg.TradeNo) + buf.WriteString(g.GetAppSecret()) + if !rchgutil.VerifySign(rchg.Sign, buf.String()) { + log.Error("DaBaiSha callback sign verify fail") + return errors.New("check sign fail") + } + payMoneyf, err := strconv.ParseFloat(rchg.PayMoney, 64) + if err != nil { + log.Error(fmt.Sprintf("DaBaiSha callback ParseFloat fail error:%+v:", err)) + return fmt.Errorf("invalid payMoney %s", rchg.PayMoney) + } + payMoneyf = payMoneyf * 100 + payMoney := int64(payMoneyf) + g.TradeNo = rchg.TradeNo + msg, err := g.QueryOrder() + if err != nil { + return err + } + if msg.PayTime == "" { + return errors.New("querry order err,no payTime") + } + loc, _ := time.LoadLocation("Local") + paymentAt, err := time.ParseInLocation("2006-01-02T15:04:05Z07:00", msg.PayTime, loc) + if err != nil { + return err + } + rchg.TradeNo = rchgutil.RChgIDDisassemble(rchg.TradeNo) + if err = rechargeser.RechargeCallBack(ctx, rchg.OID, payMoney, rchg.TradeNo, rchg.Code, paymentAt, time.Now()); err != nil { + log.Error(fmt.Sprintf("DaBaiSha RechargeCallBack fail error:%+v:", err)) + return err + } + return nil + }(); err != nil { + ctx.String(http.StatusBadRequest, "fail") + return + } + ctx.String(http.StatusOK, "success") +} diff --git a/app/api/rechargectrl/pay_center.go b/app/api/rechargectrl/pay_center.go new file mode 100644 index 0000000..1345d4c --- /dev/null +++ b/app/api/rechargectrl/pay_center.go @@ -0,0 +1,122 @@ +package rechargectrl + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + + "91porn-server/app/service/rechargeser" + "91porn-server/common/log" + "91porn-server/common/rchgutil" +) + +// PayCenterCallBack 支付中心结构回调函数 +func PayCenterCallBack(ctx *gin.Context) { + if err := func() error { + rchg := rchgutil.RechargeCallbackResp{} + g := rchgutil.Recharge{} + if err := ctx.ShouldBindJSON(&rchg); err != nil { + log.Error(fmt.Sprintf("PayCenterCallBack parameter bind fail error:%+v:", err)) + return err + } + bs, _ := json.Marshal(rchg) + log.Info(fmt.Sprintf("PayCenterCallBack parameter data:%+v:", string(bs))) + buf := bytes.Buffer{} + buf.WriteString(strconv.Itoa(rchg.Code)) + buf.WriteString(rchg.MercID) + buf.WriteString(rchg.OID) + buf.WriteString(rchg.PayMoney) + buf.WriteString(rchg.TradeNo) + buf.WriteString(g.GetAppSecret()) + if !rchgutil.VerifySign(rchg.Sign, buf.String()) { + log.Error("PayCenterCallBack sign verify fail") + return errors.New("check sign fail") + } + payMoneyf, err := strconv.ParseFloat(rchg.PayMoney, 64) + if err != nil { + log.Error(fmt.Sprintf("PayCenterCallBack ParseFloat fail error:%+v:", err)) + return fmt.Errorf("pay center invalid payMoney %s", rchg.PayMoney) + } + payMoneyf = payMoneyf * 100 + payMoney := int64(payMoneyf) + g.TradeNo = rchg.TradeNo + msg, err := g.QueryOrder() + if err != nil { + log.Error(fmt.Sprintf("PayCenterCallBack QueryOrder error: %+v, data: %+v", err, msg)) + return err + } + if msg.PayTime == "" { + log.Error(fmt.Sprintf("PayCenterCallBack PayTime error: %+v", msg)) + return errors.New("query order err,no payTime") + } + loc, _ := time.LoadLocation("Local") + paymentAt, err := time.ParseInLocation("2006-01-02T15:04:05Z07:00", msg.PayTime, loc) + if err != nil { + log.Error(fmt.Sprintf("PayCenterCallBack paymentAt error: %+v, data: %+v", err, msg)) + return err + } + rchg.TradeNo = rchgutil.RChgIDDisassemble(rchg.TradeNo) + if err = rechargeser.RechargeCallBack(ctx, rchg.OID, payMoney, rchg.TradeNo, rchg.Code, paymentAt, time.Now()); err != nil { + log.Error(fmt.Sprintf("PayCenterCallBack RechargeCallBack error:%+v:", err)) + return err + } + return nil + }(); err != nil { + ctx.String(http.StatusBadRequest, "fail") + return + } + ctx.String(http.StatusOK, "success") +} + +// RefundCallBack 支付中心结构退款函数 +func RefundCallBack(ctx *gin.Context) { + if err := func() error { + rchg := rchgutil.RefundCallbackResp{} + g := rchgutil.Recharge{} + if err := ctx.ShouldBindJSON(&rchg); err != nil { + log.Error(fmt.Sprintf("payCenter RefundCallBack parameter bind fail error:%+v:", err)) + return err + } + bs, _ := json.Marshal(rchg) + log.Info(fmt.Sprintf("payCenter RefundCallBack parameter data:%+v:", string(bs))) + buf := bytes.Buffer{} + buf.WriteString(strconv.Itoa(rchg.Code)) + buf.WriteString(rchg.MercID) + buf.WriteString(rchg.OID) + buf.WriteString(rchg.TradeNo) + buf.WriteString(g.GetAppSecret()) + if !rchgutil.VerifySign(rchg.Sign, buf.String()) { + log.Error("payCenter RefundCallBack sign verify fail") + return errors.New("check sign fail") + } + + //g.TradeNo = rchg.TradeNo + //msg, err := g.QueryOrder() + //if err != nil { + // log.Error(fmt.Sprintf("payCenter RefundCallBack QueryOrder error: %+v, data: %+v", err, msg)) + // return err + //} + // + //if msg.PayStatus != "未知" { + // log.Error(fmt.Sprintf("payCenter RefundCallBack PayTime error: %+v", msg)) + // return errors.New("refund querry order payStatus err") + //} + + rchg.TradeNo = rchgutil.RChgIDDisassemble(rchg.TradeNo) + if err := rechargeser.RefundCallBack(ctx, rchg.OID, rchg.TradeNo); err != nil { + log.Error(fmt.Sprintf("payCenter RefundCallBack RechargeCallBack error:%+v:", err)) + return err + } + return nil + }(); err != nil { + ctx.String(http.StatusBadRequest, "fail") + return + } + ctx.String(http.StatusOK, "success") +} diff --git a/app/api/rechargectrl/rechargectrl.go b/app/api/rechargectrl/rechargectrl.go new file mode 100644 index 0000000..96d3d40 --- /dev/null +++ b/app/api/rechargectrl/rechargectrl.go @@ -0,0 +1,217 @@ +package rechargectrl + +import ( + "sync" + + "91porn-server/app/proto" + "91porn-server/app/service/rechargeser" + "91porn-server/common" + "91porn-server/common/daichong" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/rchgamtmod" + "91porn-server/models/v/rchgordmod" + + "github.com/gin-gonic/gin" +) + +// NewRecharge doc +// @Summary 充值接口 充值完成后会产生一条充值流水 +// @Description 充值接口 充值完成后会产生一条充值流水 +// @Tags 钱包 +// @Accept json +// @Produce json +// @Param request formData rechargeser.RechargeRequest true "request" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/topay [post] +func NewRecharge(c *gin.Context) { + var ( + err error + in = new(rechargeser.RechargeRequest) + payUrl, mode string + ) + in.UID, err = common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + in.IP = common.GetIP(c) + if err = c.ShouldBindJSON(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + ua, _ := common.GetUA(c) + // 客户端下单不参与活动抵扣券(deduct=nil),couponId/deductAmount 仅活动服 HMAC 入口可注入 + if payUrl, mode, err = rechargeser.Recharge(c, in, ua, nil); err != nil { + common.ServeJSON(c, stderr.RechargeFaile, err.Error()) + return + } + common.ServeJSON(c, stderr.Success, gin.H{ + "payUrl": payUrl, + "mode": mode, + }) +} + +// GetRecHistory doc +// @Summary 获取充值记录 +// @Description 根据条件查询充值记录 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData int true "页码" +// @Param pageSize formData int true "条数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/rchg/order [get] +func GetRecHistory(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var arg struct { + commod.Page + } + if err = ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + total, data, hasNext, err := rchgordmod.FindMyOrders(uid, arg.PageSize, arg.PageNumber) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "total": total, + "orders": data, + "list": data, + "hasNext": hasNext, + }) +} + +// GetRechargeType doc +// @Summary 获取充值类型 +// @Description 获取充值类型 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param typeID query string false "结束时间" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/rechargeTypeList [get] +func GetRechargeType(ctx *gin.Context) { + ua, _ := common.GetUA(ctx) + var ( + goldRes []*rchgamtmod.GoldRes + dai daichong.ChatResp + goldResErrpr error + ) + wg := sync.WaitGroup{} + wg.Add(1) + common.Go(func() { + defer wg.Done() + goldRes, goldResErrpr = rechargeser.GetPayChannel_new(ctx, ua.SysType, 0) + + }) + wg.Wait() + if goldResErrpr != nil { + common.ServeJSON(ctx, stderr.PayBusy, goldResErrpr.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": goldRes, + "daichong": dai.Data, + }) +} + +// CurrencyList doc +// @Summary 获取充值金额列表 +// @Description 获取充值金额列表 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param request query rechargeser.CurrencyListRequest true "类型1-金币 2-游戏币 3-果币" +// @Success 200 {object} proto.CurryenyResp "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/currencys [get] +func CurrencyList(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + ua, err := common.GetUA(c) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + var ( + args rechargeser.CurrencyListRequest + wg sync.WaitGroup + dcChat daichong.ChatResp + code stderr.Code + data []*proto.CurrencyListResponse + ) + if err := c.ShouldBindQuery(&args); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + wg.Add(2) + common.Go(func() { + defer wg.Done() + data, code = rechargeser.New_CurrencyList(c, uid, ua.SysType, commod.CurrencyType(args.Type)) + }) + common.Go(func() { + defer wg.Done() + //var productType int + //if commod.CurrencyType(args.Type) == commod.GameCoin { + // productType = 1 + //} + //dcChat, _ = daichongser.NewTakeChat(c, uid, productType) + dcChat = daichong.ChatResp{} + }) + wg.Wait() + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, stderr.Success, proto.CurryenyResp{ + Chat: dcChat.Data, + List: data, + }) +} + +// GetUserTransactions doc +// @Summary 获取充值记录 +// @Description 根据条件查询充值记录 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData int true "页码" +// @Param pageSize formData int true "条数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/mine/transaction [get] +func GetUserTransactions(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + var arg commod.Page + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + total, data, hasNext, err := rechargeser.GetUserTransactionDetails(uid, arg.PageNumber, arg.PageSize) + if err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, err.Error()) + return + } + common.ServeJSON(c, stderr.Success, gin.H{ + "total": total, + "list": data, + "hasNext": hasNext, + }) +} diff --git a/app/api/rechargectrl/yinse.go b/app/api/rechargectrl/yinse.go new file mode 100644 index 0000000..044bdd0 --- /dev/null +++ b/app/api/rechargectrl/yinse.go @@ -0,0 +1,64 @@ +package rechargectrl + +import ( + "91porn-server/app/service/proxyser" + "91porn-server/common" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "91porn-server/app/service/rechargeser" + "91porn-server/common/log" + "91porn-server/common/rchgutil" + + "github.com/gin-gonic/gin" +) + +// CallBack 支付回调回调函数 +func CallBack(ctx *gin.Context) { + var transNo string = "" + var payMoney int64 = 0 + success, err := func() (string, error) { + var err error + name := ctx.Param("name") + rchg := rchgutil.GetNotifyBack(name) + if rchg == nil { + log.Error(fmt.Sprintf("%s callback request path invalid.", name)) + return "", errors.New(" request name invalid") + } + ata, _ := ctx.GetRawData() + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(ata)) + bodystr := string(ata) + log.Info(fmt.Sprintf("%s callback request url +%s, body:%s,contenttype:%s", name, ctx.Request.URL, bodystr, ctx.ContentType())) + if err = ctx.ShouldBind(rchg); err != nil { + log.Error(fmt.Sprintf("%s callback parameter bind fail error:%+v:", name, err)) + return "", err + } + bs, _ := json.Marshal(rchg) + log.Info(fmt.Sprintf("%s callback parameter data:%+v:", name, string(bs))) + rb, err := rchg.Notify() + if err != nil { + return "", err + } + rb.TransNo = rchgutil.RChgIDDisassemble(rb.TransNo) + if err = rechargeser.RechargeCallBack(ctx, rb.OID, rb.PayMoney, rb.TransNo, rb.Code, rb.PaymentAt, rb.SuccessAt); err != nil { + log.Error(fmt.Sprintf("%s RechargeCallBack fail error:%+v:", name, err)) + } + transNo = rb.TransNo + payMoney = rb.PayMoney + return rchg.Success(), err + }() + if err != nil { + ctx.String(http.StatusBadRequest, "fail") + return + } else { + //异步处理全民代理分成 + common.Go(func() { + proxyser.HandelProxyRechargeCommission(nil, transNo, payMoney) + }) + ctx.String(http.StatusOK, success) + } +} diff --git a/app/api/recommctrl/recommctrl.go b/app/api/recommctrl/recommctrl.go new file mode 100644 index 0000000..6a7fe4f --- /dev/null +++ b/app/api/recommctrl/recommctrl.go @@ -0,0 +1,145 @@ +package recommctrl + +import ( + "91porn-server/app/service/m3u8ticket" + "91porn-server/app/service/recommser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/middleware/requestid" + "91porn-server/models/commod" + "91porn-server/models/v/recmdtag" + + "github.com/gin-gonic/gin" +) + +// GetVidList doc +// @Summary 获取短视频推荐列表 +// @Description 获取推荐视频 +// @Tags recommend +// @Accept mpfd,json +// @Produce json,html +// @Param pageSize formData integer true "页码大小" +// @Param X-Request-ID header string false "重试幂等ID;同一次请求重试保持不变" +// @Success 200 {object} recommod.VideoListResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/recommend/vid/list [get] +func GetVidList(ctx *gin.Context) { + uid, _ := common.GetUID(ctx) + + type Param struct { + PageSize uint64 `json:"pageSize" form:"pageSize" binding:"required,min=1,max=100"` // 每页条数 + } + param := Param{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if param.PageSize < 1 { + param.PageSize = 10 + } + + requestID, _ := requestid.FromClient(ctx) + code, data := recommser.GetVidListContext( + ctx.Request.Context(), + uid, + param.PageSize, + requestID, + ) + // 推荐视频列表:对 m3u8 播放地址签票 + m3u8ticket.Sign(ctx, data) + common.ServeJSON(ctx, code, data) +} + +// GetUserList doc +// @Summary 获取主播推荐列表 +// @Description 获取主播荐视频 +// @Tags recommend +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/recommend/user/list [get] +func GetUserList(ctx *gin.Context) { + //uid用来做用户行为分析,暂时没用 + uid, _ := common.GetUID(ctx) + param := commod.Page{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if param.PageSize < 1 { + param.PageSize = 10 + } + code, data := recommser.GetUserList(uid, param.PageNumber, param.PageSize) + common.ServeJSON(ctx, code, data) +} + +// GetLightVidList doc +// @Summary 获取轻量视频推荐列表 +// @Description 获取轻量推荐视频 +// @Tags recommend +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} proto.LightVideoRes +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/recommend/vid/lightlist [get] +func GetLightVidList(ctx *gin.Context) { + param := commod.Page{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if param.PageSize < 1 { + param.PageSize = 10 + } + code, data := recommser.GetLightVidList(ctx.ClientIP()) + common.ServeJSON(ctx, code, data) +} + +// GetVidAd doc +// @Summary 获取视频插播广告 +// @Description 获取视频插播广告 +// @Tags recommend +// @Accept mpfd,json +// @Produce json,html +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/recommend/GetVidAd/list [get] +func GetVidAd(ctx *gin.Context) { + uid, _ := common.GetUID(ctx) + data := recommser.GetAd(uid) + common.ServeJSON(ctx, stderr.Success, data) +} + +// GetShortDiscoverList doc +// @Summary 获取抖音短视频视频列表 +// @Description 获取抖音短视频列表(第一页存在发现tag列表) +// @Tags recommend +// @Accept json +// @Produce json,html +// @Param type query recmdtag.AppGetShortDiscoverListReq true "请求参数" +// @Success 200 {object} recmdtag.AppGetShortDiscoverListRep "{}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/recommend/vid/list/discover [get] +func GetShortDiscoverList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + param := &recmdtag.AppGetShortDiscoverListReq{} + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + resp, code, err := recommser.GetShortDiscoverList(uid, param) + if err != nil { + log.Error("GetShortVideoList error:", log.E(err)) + } + m3u8ticket.Sign(ctx, resp) + common.ServeJSON(ctx, code, resp) +} diff --git a/app/api/recreationctrl/recreationctrl.go b/app/api/recreationctrl/recreationctrl.go new file mode 100644 index 0000000..6e26a18 --- /dev/null +++ b/app/api/recreationctrl/recreationctrl.go @@ -0,0 +1,56 @@ +package recreationctrl + +import ( + "91porn-server/app/service/adser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取娱乐模块列表 +// @Description 获取金主广告列表 +// @Tags PING +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {array} adser.RecreationListRes +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/recreation/list [get] +func List(ctx *gin.Context) { + list, err := adser.RecreationFromJt() + if err != nil { + log.Error("RecreationFromJt", log.E(err)) + common.ServeJSON(ctx, stderr.Success, nil) // 加载娱乐广告失败可忽略 + return + } + common.ServeJSON(ctx, stderr.Success, list) +} + +// Click doc +// @Summary 娱乐模块广告点击 +// @Description 娱乐模块广告点击 +// @Tags PING +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "广告数据id" +// @Param type formData string true "用户点击数据类型,app or adv" +// @Param sysType formData string true "用户设备类型. ios or android" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/recreation/click [post] +func Click(ctx *gin.Context) { + var p adser.RecreationClickInfo + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + _, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrTokenIsNotExist, "") + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/app/api/scenebannerctrl/banner.go b/app/api/scenebannerctrl/banner.go new file mode 100644 index 0000000..d9c0ff5 --- /dev/null +++ b/app/api/scenebannerctrl/banner.go @@ -0,0 +1,50 @@ +package scenebannerctrl + +import ( + "strings" + "time" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/scenebannermod" + + "github.com/gin-gonic/gin" +) + +type listResp struct { + List []bannerItem `json:"list"` +} + +type bannerItem struct { + ID string `json:"id"` + ImageURL string `json:"imageUrl"` + MediaType string `json:"mediaType"` + LinkType string `json:"linkType"` + LinkValue string `json:"linkValue"` + Sort int `json:"sort"` +} + +func List(ctx *gin.Context) { + scene := strings.ToUpper(strings.TrimSpace(ctx.Query("scene"))) + if !scenebannermod.ValidScene(scene) { + common.ServeJSON(ctx, stderr.ErrParamError, "invalid scene") + return + } + list, err := scenebannermod.FindActive(scene, time.Now()) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + items := make([]bannerItem, 0, len(list)) + for _, banner := range list { + items = append(items, bannerItem{ + ID: banner.ID.Hex(), + ImageURL: banner.ImageURL, + MediaType: banner.MediaType, + LinkType: banner.LinkType, + LinkValue: banner.LinkValue, + Sort: banner.Sort, + }) + } + common.ServeJSON(ctx, stderr.Success, listResp{List: items}) +} diff --git a/app/api/searchctrl/searchctrl.go b/app/api/searchctrl/searchctrl.go new file mode 100644 index 0000000..bf43c2a --- /dev/null +++ b/app/api/searchctrl/searchctrl.go @@ -0,0 +1,301 @@ +package searchctrl + +import ( + "91porn-server/app/service/moduleser" + "91porn-server/app/service/search" + "91porn-server/app/service/searcher" + "91porn-server/app/service/vidser" + "91porn-server/common" + "91porn-server/common/filter" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/searchlogmod" + "fmt" + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 搜索模块 - 搜索keyWords和realm指定的相关资源 +// @Description 获取FILE域 Token +// @Tags Search +// @Accept mpfd,json +// @Produce json,html +// @Param q body search.NewsKeywordSearchReq true "参数" +// @Success 200 object search.NewsKeywordSearchRep "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/search/list [post] +func List(c *gin.Context) { + + var req search.NewsKeywordSearchReq + if err := c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "search List arg error "+err.Error()) + return + } + // 检查uid是否存在 + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrAccessForbid, "search List Context USER_ID is not exist ") + return + } + + req.KeyWords, err = filterKeyWord(req.KeyWords) + if err != nil { + common.ServeJSON(c, stderr.TagAddTagNameInvalidErr, err) + return + } + //录入搜索日志 + common.Go(func() { + _ = searchlogmod.InsertMany(uid, req.Realm, req.KeyWords) + }) + + data, err := req.Search(uid) + if err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, err) + return + } + common.ServeJSON(c, stderr.Success, data) + + //if arg.Realm == constant.Video { + // if items, ok := result.Data().([]searcher.VideoRes); ok { + // // 查询用户信息 + // u, err := usermod.FindUserByUID(uid) + // if err != nil { + // common.ServeJSON(c, stderr.ErrNetWorkBusy, err) + // return + // } + // if u.VipExpireDate.After(time.Now()) { + // for k, item := range items { + // if item.Coins != nil && *item.Coins < 10 { + // var zero int64 = 0 + // item.Coins = &zero + // } + // items[k] = item + // } + // } + // common.ServeJSON(c, stderr.Success, gin.H{ + // "list": items, + // "hasNext": result.HasNext(), + // }) + // return + // } + //} + // + + //if req.Realm == constant.SearchSP || req.Realm == constant.SearchShort { + // // 额外获取TAG相关信息 + // tagId, err := tagmod.GetTagIDByName(req.KeyWords[0]) + // if tagId.IsZero() || err != nil { + // common.ServeJSON(c, stderr.Success, gin.H{ + // "list": result.Data(), + // "tagID": "", + // "tagVidList": nil, + // "hasNext": result.HasNext(), + // }) + // return + // } + // // 6-最多收藏 + // vmodList, err := vidser.GetVideosByTagID(tagId, req.Realm, 6, 0, 4) + // if err != nil { + // common.ServeJSON(c, stderr.Success, gin.H{ + // "list": result.Data(), + // "tagID": "", + // "tagVidList": nil, + // "hasNext": result.HasNext(), + // }) + // return + // } + // oids := make([]primitive.ObjectID, len(vmodList)) + // for i, v := range vmodList { + // oids[i] = v.ID + // } + // if len(oids) > int(req.PageSize) { + // oids = oids[:req.PageSize] + // } + // vidList := vidhelpser.GetVideosByIDs(0, oids) + // common.ServeJSON(c, stderr.Success, gin.H{ + // "list": result.Data(), + // "tagID": tagId.Hex(), + // "tagVidList": vidList, + // "hasNext": result.HasNext(), + // }) + // return + //} + //common.ServeJSON(c, stderr.Success, gin.H{ + // "list": result.Data(), + // "hasNext": result.HasNext(), + //}) +} + +// 如返回为空数组 则表示所输入关键字 全为违规词汇 +func filterKeyWord(KeyWords []string) ([]string, error) { + //过滤掉违规词汇 + pureKw := make([]string, 0, len(KeyWords)) + for _, v := range KeyWords { + s, e := filter.TagFilter.Filter(v) + if len(s) == 0 && e == nil { + pureKw = append(pureKw, v) + } + } + if len(pureKw) == 0 { + return pureKw, fmt.Errorf("Invalid tag name: %v", KeyWords) + } + return pureKw, nil +} + +// IndexList doc +// @Summary 搜索 - 搜索首页 +// @Description 获取搜索首页列表 +// @Tags Search +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功" "data":{ "hotTagList":[] "hotVidList":[] "themeList":[] }" +// @Failure 400 {string} json "{"msg": "操作失败" "data":{}}" +// @Router /search/index [get] +func IndexList(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, "") + return + } + //热点视屏Opting + hVidOpt := (&searcher.Option{}).SetLimit(6) + //今日最热视屏Opting + hsVidopt := (&searcher.Option{}).SetLimit(20) //前端希望给20个 文东确认 + home := search.GetHome(uid, hVidOpt, hsVidopt) + common.ServeJSON(c, stderr.Success, home) +} + +// WonderTagList doc +// @Summary 搜索 - 搜索首页 +// @Description 获取发现精彩标签列表 +// @Tags Search +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query integer true "页码" +// @Param pageSize query integer true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /search/wonder/list [get] +func WonderTagList(c *gin.Context) { + var arg struct { + commod.Page + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "searchCtrl WonderTagList arg error "+err.Error()) + return + } + skip := int64((arg.PageNumber - 1) * arg.PageSize) + limit := int64(arg.PageSize) + tags, hasNext, err := search.GetWonderTagList(skip, limit) + if err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, "searchCtrl WonderTags error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, gin.H{ + "list": tags, + "hasNext": hasNext, + }) +} + +// HotTagSearch doc +// @Summary 搜索 +// @Description 猜你想要 +// @Tags Search +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} proto.Tag +// @Failure 400 {string} json "{"msg": "操作失败" "data":{}}" +// @Router /search/hotTag [get] +func HotTagSearch(c *gin.Context) { + data, err := search.GetHotTag() + if err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, err.Error()) + return + } + common.ServeJSON(c, stderr.Success, gin.H{ + "list": data, + }) +} + +// HotVid doc +// @Summary 热门视频列表 +// @Description 热门视频列表 +// @Tags Search +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query int true "当前页" +// @Param pageSize query int true "每页条数" +// @Param type query int true "0最新热播 1本月最热 2上月最热" +// @Success 200 {object} vidmod.VideoModel "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/search/hotVid/list [get] +func HotVid(c *gin.Context) { + var arg struct { + commod.Page + T int `json:"type" form:"type"` // 0 最新热播 1本月最热 2上月最热 + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "searchCtrl WonderTagList arg error "+err.Error()) + return + } + + res, err := vidser.GetHotVideo(int64(arg.PageNumber), int64(arg.PageSize), arg.T) + if err != nil { + common.ServeJSON(c, stderr.Failure, err) + return + } + + common.ServeJSON(c, stderr.Success, res) +} + +// HotPublisher doc +// @Summary 热门博主 +// @Description 热门博主 +// @Tags Search +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query int true "当前页" +// @Param pageSize query int true "每页条数" +// @Success 200 {object} proto.HotPublisher "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /search/hotPublisher/list [get] +func HotPublisher(c *gin.Context) { + // 检查uid是否存在 + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrAccessForbid, "search HotPublisher Context USER_ID is not exist ") + return + } + list, err := vidser.GetHotPublisher(uid) + if err != nil { + common.ServeJSON(c, stderr.Failure, err) + return + } + common.ServeJSON(c, stderr.Success, gin.H{ + "list": list, + }) +} + +// PublisherList doc +// @Summary 热门板块 +// @Description 热门板块 +// @Tags 发布 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} tagmod.TagInfoRes "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/search/publisher/list [get] +func PublisherList(c *gin.Context) { + // 检查uid是否存在 + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrAccessForbid, "search HotPublisher Context USER_ID is not exist ") + return + } + list, err := moduleser.GetPublishTag(uid) + if err != nil { + common.ServeJSON(c, stderr.Failure, err) + return + } + common.ServeJSON(c, stderr.Success, list) +} diff --git a/app/api/sharectrl/sharectrl.go b/app/api/sharectrl/sharectrl.go new file mode 100644 index 0000000..eed7ac6 --- /dev/null +++ b/app/api/sharectrl/sharectrl.go @@ -0,0 +1,138 @@ +package sharectrl + +import ( + "91porn-server/app/service/shareser" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/e/sharemod" + "net/http" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + + "github.com/gin-gonic/gin" +) + +// GeneratorQrCode doc +// @Summary 获取视频的分享次数 +// @Description 获取视频的分享次数 +// @Tags share +// @Accept mpfd,json +// @Produce json,html +// @Param content formData string true "分享的url" +// @Param videoID formData string false "视频ID;同一用户、视频、自然日最多累计一次真实分享推荐分" +// @Param eventId formData string false "分享事件ID;同一次事件重试时保持不变,最长128字符" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/share/output [post] +func GeneratorQrCode(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + param := sharemod.VShareReq{} + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if len(param.Content) == 0 { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if param.ObjType == "drama" { + requestID := ctx.GetHeader("X-Request-ID") + if param.MediaID == "" || param.EventID == "" || requestID == "" || len(requestID) > 128 || len(param.EventID) > 128 { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := shareser.GeneratorDramaQrCodeContext( + ctx.Request.Context(), uid, param.Content, param.MediaID, param.ContentID, param.EventID, + ) + common.ServeJSON(ctx, code, data) + return + } + code, data := shareser.GeneratorQrCodeContext( + ctx.Request.Context(), + uid, + param.Content, + param.VideoID, + param.EventID, + ) + common.ServeJSON(ctx, code, data) +} + +// GetShareCnt doc +// @Summary 获取视频的分享次数 +// @Description 获取视频的分享次数 +// @Tags share +// @Accept mpfd,json +// @Produce json,html +// @Param videoID formData string true "视频id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/share/count [get] +func GetShareCnt(ctx *gin.Context) { + param := sharemod.VShareCntReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if len(param.VideoID) == 0 { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := shareser.GetShareCnt(param.VideoID) + common.ServeJSON(ctx, code, data) +} + +// Info doc +// @Summary 获取分享信息 +// @Description 获取分享信息 +// @Tags share +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "视频id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/share/info [get] +func Info(ctx *gin.Context) { + var param struct { + ID primitive.ObjectID `json:"id"` + } + h := gin.H{ + "hash": false, + "data": "", + "time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), + } + if err := ctx.ShouldBind(¶m); err != nil { + h["code"] = stderr.ErrParamError + h["msg"] = stderr.ErrParamError.Msg() + h["tip"] = stderr.ErrParamError.Tip() + ctx.JSON(http.StatusOK, h) + return + } + if param.ID.IsZero() { + h["code"] = stderr.ErrParamError + h["msg"] = stderr.ErrParamError.Msg() + h["tip"] = stderr.ErrParamError.Tip() + ctx.JSON(http.StatusOK, h) + return + } + + ua, _ := common.GetUA(ctx) + data, sErr := shareser.Info(param.ID, ua.SysType) + if sErr != nil && sErr.Code != stderr.Success { + h["code"] = sErr.Code + h["msg"] = sErr.Msg + h["tip"] = sErr.Tips + ctx.JSON(http.StatusOK, h) + return + } + + h["code"] = stderr.Success + h["msg"] = stderr.Success.Msg() + h["tip"] = stderr.Success.Tip() + h["data"] = data + ctx.JSON(http.StatusOK, h) +} diff --git a/app/api/signrecordctrl/signrecord.go b/app/api/signrecordctrl/signrecord.go new file mode 100755 index 0000000..b731638 --- /dev/null +++ b/app/api/signrecordctrl/signrecord.go @@ -0,0 +1,44 @@ +package signrecordctrl + +import ( + "91porn-server/app/service/signrecordser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "fmt" + + "github.com/gin-gonic/gin" +) + +// AgainSign doc +// @Summary 补签打卡接口 +// @Description 补签打卡 +// @Tags 移动端-补签打卡 +// @Accept mpfd,json +// @Produce json +// @Param q query signrecordser.AppReSignReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/sign_record/resign [post] +func AgainSign(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + p := &signrecordser.AppReSignReq{} + if err := ctx.ShouldBind(&p); err != nil { + log.Error(fmt.Sprintf("uid:%v,resign param is err:%v", uid, err)) + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + code, err := p.ReSign(uid) + if code != stderr.Success { + log.Error(fmt.Sprintf("uid:%v,resign is err:%v", uid, err)) + common.ServeJSON(ctx, code, err.Error()) + return + } + + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} diff --git a/app/api/smsctrl/smsctrl.go b/app/api/smsctrl/smsctrl.go new file mode 100644 index 0000000..9575907 --- /dev/null +++ b/app/api/smsctrl/smsctrl.go @@ -0,0 +1,34 @@ +package smsctrl + +import ( + "91porn-server/app/service/smsser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// Captcha doc +// @Summary 短信验证码 +// @Description 发送短信验证码,安卓客户端在用 +// @Tags captcha +// @Accept json +// @Produce json +// @Param mobile formData string true "手机号码信息" +// @Param type formData integer false "发送验证码的用途 1-绑定手机号 2-手机号登陆" +// @Success 200 {string} json "{"msg": "操作成功","code":200,"data","验证码Id"}" +// @Router /sms/captcha [post] +func SendCaptcha(ctx *gin.Context) { + var args struct { + Mobile string `form:"mobile" json:"mobile" binding:"required"` + Type int64 `form:"type" json:"type"` + } + if err := ctx.ShouldBind(&args); err != nil { + log.WarnX(ctx, "SendCaptcha bind args", log.E(err)) + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + errcode := smsser.SendSmsCode(ctx, args.Mobile, int(args.Type)) + common.ServeJSON(ctx, errcode, nil) +} diff --git a/app/api/statcenterctl/proto.go b/app/api/statcenterctl/proto.go new file mode 100644 index 0000000..060bcb9 --- /dev/null +++ b/app/api/statcenterctl/proto.go @@ -0,0 +1,107 @@ +package statcenterctl + +import ( + "time" + + "91porn-server/models/commod" +) + +// 全民代理查询 +type UserInviteUserListReq struct { + UserId uint64 `form:"userId" json:"userId"` // 用户ID + Appid int32 `form:"appId" json:"appId"` // APPID + commod.Page +} + +type UserInviteUserListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []UserInviteUserInfo `json:"list"` +} + +type UserInviteUserInfo struct { + UserId uint64 `json:"userId"` // 邀请用户ID + Name string `json:"name"` // 邀请用户名称 + Portrait string `json:"portrait"` // 被邀请人头像 + BindPhone string `json:"bindPhone"` // 绑定 + CreateAt time.Time `json:"createAt"` // 注册时间 +} + +type UserInviteIncomeListReq struct { + UserId uint64 `form:"userId" json:"userId" ` // 用户ID + Appid int32 `form:"appId" json:"appId"` // APPID + commod.Page +} + +type UserInviteIncomeListRes struct { + //总邀请数 + TotalInvites int64 `json:"totalInvites"` + //今日邀请 + TodayInvites int64 `json:"todayInvites"` + //总邀请充值 + TotalInviteAmount int64 `json:"totalInviteAmount"` + //今日充值 + TodayInviteAmount int64 `json:"todayInviteAmount"` + //列表总数 + Total int64 `json:"total"` + //是否还有下一页 + HasNext bool `json:"hasNext"` + //列表 + List []UserInviteIncomeInfo `json:"list"` +} + +type UserInviteIncomeInfo struct { + // 充值用户 + UserId uint64 `json:"userId"` + // 充值用户 + UserName string `json:"userName"` + // 收入金币 + IncomeAmount int64 `json:"incomeAmount" bson:"incomeAmount"` + // 分成比例 + IncomeRate float64 `json:"incomeRate" bson:"incomeRate"` + // 充值时间 + RechargeAt time.Time `json:"rechargeAt"` +} + +type VideoIncomeListReq struct { + commod.Page +} + +type StatcenterSyncReq struct { + Job string `json:"job"` // user_access/user_register 大于用户Id: + UserId uint64 `json:"userId"` // 用户ID + PlatformId string `json:"platformId"` // 原始平台Id + MaxSize int64 `json:"maxSize"` // 最大条数 + SuccessTime time.Time `json:"successTime"` // 成功时间 +} + +type StatcenterSyncResp struct { + Code int `json:"code"` // 200正常 其他异常 + Msg string `json:"msg"` // 错误消息 + Job string `json:"job" bson:"job" binding:"required"` // 类型 + AccessList []commod.UserAccessMsg `json:"accessList"` // 日活记录 + RegisterList []commod.UserRegisterMsg `json:"registerList"` // 提现记录 + BindingList []commod.UserBindingMsg `json:"bindingList"` // 用户绑定记录 + InviteList []commod.UserInviteBindMsg `json:"inviteList"` // 邀请记录 + ConsumeList []commod.ConsumeRecordMsg `json:"consumeList"` // 消费流水 + RechargeList []commod.UserRechargeMsg `json:"rechargeList"` // 充值流水 + AllRechargeList []commod.UserRechargeMsg `json:"allRechargeList"` // 全部支付订单 + CardSellList []commod.CardSellMsg `json:"cardSellList"` // 会员卡特权卡销售流水 + AiSellList []commod.AiSellMsg `json:"aiSellList"` // AI销售流水 +} + +type UserInviteIncomeListResWaLi struct { + TotalInvites int64 `json:"totalInvites"` //总推广人数 + TodayInvites int64 `json:"todayInvites"` //今日推广 + TotalInviteAmount int64 `json:"totalInviteAmount"` //总收益 + YesterdaylInviteAmount int64 `json:"yesterdaylInviteAmount"` //昨日收益 + Total int64 `json:"total"` //总数 + List []UserInviteIncomeInfoWaLi `json:"list"` //收益记录 + HasNext bool `json:"hasNext"` +} + +type UserInviteIncomeInfoWaLi struct { + Desc string `json:"desc"` // 收益描述名称 + IncomeAmount int64 `json:"incomeAmount"` // 收入金币 + SetDate time.Time `json:"setDate"` // 结算时间 +} diff --git a/app/api/statcenterctl/statcenterctr.go b/app/api/statcenterctl/statcenterctr.go new file mode 100644 index 0000000..de9f251 --- /dev/null +++ b/app/api/statcenterctl/statcenterctr.go @@ -0,0 +1,200 @@ +package statcenterctl + +import ( + "91porn-server/app/service/proxyser" + "91porn-server/app/service/walletser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// UserInviteInfo +// @Tags 全名代理 +// @Summary 获取账户信息 +// @Description 获取账户信息 +// @Accept json +// @Produce json +// @Success 200 {object} walletser.UserInviteAmountInfo +// @Success 400 {string} string "失败" +// @Router /userInvite/info [POST] +func UserInviteInfo(ctx *gin.Context) { + uid, err := common.GetUID(ctx) //当前用户uid + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + resp, err := walletser.GetUserAmount(uid) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// @Tags 全名代理 +// @Summary 获取视频收益详情 +// @Description +// @Accept json +// @Produce json +// @Param param body VideoIncomeListReq true "参数" +// @Success 200 {string} string "成功" +// @Success 400 {string} string "失败" +// @Router /userInvite/videolist [POST] +func UserVideoList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) //当前用户uid + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + var request VideoIncomeListReq + if err = ctx.ShouldBind(&request); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, err := walletser.GetVideoIncomelist(uid, request.PageNumber, request.PageSize) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// UserInviteList +// @Tags 全名代理 +// @Summary 邀请列表 +// @Description 返回全民代理被邀请人列表 +// @Accept json +// @Produce json +// @Param param body UserInviteUserListReq true "参数" +// @Success 200 {string} string "成功" +// @Success 400 {string} string "失败" +// @Router /userInvite/userlist [POST] +func UserInviteList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) //当前用户uid + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + var request UserInviteUserListReq + if err = ctx.ShouldBind(&request); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + //查询 + list, total, err := proxyser.GetInveUserList(uid, request.PageNumber, request.PageSize) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + var resp UserInviteUserListRes + if uint64(total) > request.PageNumber*request.PageSize { + resp.HasNext = true + } + resp.Total = total + resp.List = make([]UserInviteUserInfo, len(list)) + for i, l := range list { + resp.List[i] = UserInviteUserInfo{ + UserId: l.Invitee, + Name: l.InviteeName, + Portrait: l.InviteePortrait, + CreateAt: l.CreatedAt, + } + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// NewUserInviteList +// @Tags 全名代理 +// @Summary 新邀请列表 +// @Description 返回全民代理被邀请人列表 +// @Accept json +// @Produce json +// @Param param body UserInviteUserListReq true "参数" +// @Success 200 {string} string "成功" +// @Success 400 {string} string "失败" +// @Router /userInvite/userlist [POST] +func NewUserInviteList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) //当前用户uid + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + var request UserInviteUserListReq + if err = ctx.ShouldBind(&request); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + //查询 + list, total, err := proxyser.GetInveUserList(uid, request.PageNumber, request.PageSize) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + var resp UserInviteUserListRes + if uint64(total) > request.PageNumber*request.PageSize { + resp.HasNext = true + } + resp.Total = total + resp.List = make([]UserInviteUserInfo, len(list)) + for i, l := range list { + resp.List[i] = UserInviteUserInfo{ + UserId: l.Invitee, + Name: l.InviteeName, + Portrait: l.InviteePortrait, + CreateAt: l.CreatedAt, + } + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// UserInviteIncomeList +// @Tags 全名代理 +// @Summary 收益详情 +// @Description 返回用户收益详情 +// @Accept json +// @Produce json +// @Param param body UserInviteIncomeListReq true "参数" +// @Success 200 {object} UserInviteIncomeListRes "成功" +// @Success 400 {string} string "失败" +// @Router /api/app/userinvite/incomelist [POST] +func UserInviteIncomeList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) //当前用户uid + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + var request UserInviteIncomeListReq + if err = ctx.ShouldBind(&request); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, err := walletser.GetInviteIncomelist(uid, request.PageNumber, request.PageSize) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// StatcenterRechargeCallBack 充值回调 +func StatcenterRechargeCallBack(ctx *gin.Context) { + var request struct { + UserId uint64 `form:"userId" json:"userId" binding:"required"` + InvitedUserId uint64 `form:"invitedUserId" json:"invitedUserId" binding:"required"` + IncomeAmount int64 `form:"incomeAmount" json:"incomeAmount" binding:"required"` + OrderId string `form:"orderId" json:"orderId" binding:"required"` + } + var resp struct { + OrderId string `json:"orderId"` //第三方平台id + Code int `json:"code"` //200正常 其他异常 + Err string `json:"err"` //内部错误信息 + Msg string `json:"msg"` //错误消息 + } + if err := ctx.ShouldBind(&request); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp.Code = 200 + common.ServeJSON(ctx, stderr.Success, resp) +} diff --git a/app/api/statcenterctl/sync.go b/app/api/statcenterctl/sync.go new file mode 100644 index 0000000..e0a6285 --- /dev/null +++ b/app/api/statcenterctl/sync.go @@ -0,0 +1,579 @@ +package statcenterctl + +import ( + "fmt" + "math" + "net/http" + "strings" + + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/visitlogmod" + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/productposimod" + "91porn-server/models/v/proxymod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + + "github.com/gin-gonic/gin" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// @Tags 数据通过 +// @Summary 拉去数据同步信息 +// @Description 返回用户收益详情 +// @Accept json +// @Produce json +// @Param param body StatcenterSyncReq true "参数" +// @Success 200 {string} string "成功" +// @Success 400 {string} string "失败" +// @Router /statcenter/sync [POST] +func StatcenterSyncList(ctx *gin.Context) { + var request StatcenterSyncReq + var err error + if err = ctx.ShouldBind(&request); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + var resp StatcenterSyncResp + resp.Job = request.Job + resp.Code = 200 + // 根据记录查询数据库 + switch request.Job { + case string(commod.USER_ACCE): + fmt.Println(request.Job) + // 查询 + resp.AccessList, err = UserAccessList(request) + if err != nil { + resp.Code = 502 + resp.Msg = err.Error() + } + case string(commod.USER_REG): + fmt.Println(request.Job) + resp.RegisterList, err = UserRegisterList(request) + if err != nil { + resp.Code = 502 + resp.Msg = err.Error() + } + case string(commod.USER_BINDING): + fmt.Println(request.Job) + resp.BindingList, err = UserBindingList(request) + if err != nil { + resp.Code = 502 + resp.Msg = err.Error() + } + case string(commod.USER_INVITE): + fmt.Println(request.Job) + resp.InviteList, err = UserInviterList(request) + if err != nil { + resp.Code = 502 + resp.Msg = err.Error() + } + case string(commod.ConsumeRecordJob): + fmt.Println(request.Job) + resp.ConsumeList, err = ConsumeRecordList(request) + if err != nil { + resp.Code = 502 + resp.Msg = err.Error() + } + case string(commod.USER_RECH): + fmt.Println(request.Job) + resp.RechargeList, err = UserRechargeList(request) + if err != nil { + resp.Code = 502 + resp.Msg = err.Error() + } + case string(commod.USER_RECH_ALL): + fmt.Println(request.Job) + resp.AllRechargeList, err = UserAllOrderList(request) + if err != nil { + resp.Code = 502 + resp.Msg = err.Error() + } + case string(commod.CardSellJob): + fmt.Println(request.Job) + resp.CardSellList, err = CardSellList(request) + if err != nil { + resp.Code = 502 + resp.Msg = err.Error() + } + case string(commod.AiSellJob): + fmt.Println(request.Job) + resp.AiSellList, err = AiSellList(request) + if err != nil { + resp.Code = 502 + resp.Msg = err.Error() + } + default: + resp.Code = 501 + resp.Msg = "job is error" + } + ctx.JSON(http.StatusOK, resp) +} + +// AiSellList AI销售流水同步 +func AiSellList(req StatcenterSyncReq) (list []commod.AiSellMsg, err error) { + log.Debug("AiSellList job start running") + typeInts := []txnmod.TransType{txnmod.AiChangeFaceImgDebitGold, txnmod.AiImageToVideoDebitGold, txnmod.AiChangefaceDebitGold, txnmod.AiUndressDebitGold, txnmod.AiTextToImageDebitGold, txnmod.AiMateChat} + startID, err := primitive.ObjectIDFromHex(req.PlatformId) + if err != nil { + return + } + filter := bson.M{"tranTypeInt": bson.M{"$in": typeInts}, "_id": bson.M{"$gt": startID}} + opts := options.Find().SetLimit(req.MaxSize).SetSort(bson.D{{Key: "_id", Value: 1}}) + _, logs, err := txnmod.FindTransactionLogs(filter, opts) + if err != nil { + return + } + list = make([]commod.AiSellMsg, len(logs)) + if len(list) == 0 { + log.Debug("AiSellList job len(list) == 0") + return + } + for i, l := range logs { + amount := l.Amount + if amount < 0 { + amount = -amount + } + msg := commod.AiSellMsg{ + AppID: commod.KFK_APPID, + UID: l.UID, + UniqID: l.ID.Hex(), + Amount: amount, + SysType: l.SysType, + CurrencyType: "pay", + TranCreatedAt: l.CreatedAt, + IsRepurchase: l.IsRepurchase, + } + switch txnmod.TransType(l.TranTypeInt) { + case txnmod.AiChangeFaceImgDebitGold: + msg.TranType = "img_faceswap" + case txnmod.AiImageToVideoDebitGold: + msg.TranType = "img_to_vid" + case txnmod.AiChangefaceDebitGold: + msg.TranType = "vid_faceswap" + case txnmod.AiUndressDebitGold: + msg.TranType = "strip" + case txnmod.AiTextToImageDebitGold: + msg.TranType = "text_to_img" + case txnmod.AiMateChat: + msg.TranType = "mate" + aiMatePoint := l.AiMatePoint + if aiMatePoint < 0 { + msg.Amount = int64(math.Round(math.Abs(aiMatePoint))) + } + } + list[i] = msg + } + log.Debug("AiSellList job start finished") + return +} + +// UserAccessList 日活数据同步 +func UserAccessList(request StatcenterSyncReq) ([]commod.UserAccessMsg, error) { + visitList, err := visitlogmod.AccessSyncById(request.PlatformId, request.MaxSize) + if err != nil { + log.Error("UserAccessList AccessSyncById ", log.E(err)) + return nil, err + } + accessList := make([]commod.UserAccessMsg, len(visitList)) + for i, visitInfo := range visitList { + // iOS 开头的 devType(如 "iOS:25.3.0")统一规范化为 "ios" + if strings.HasPrefix(strings.ToLower(visitInfo.DevType), "ios") { + visitInfo.DevType = "ios" + } + accessList[i] = commod.UserAccessMsg{ + UserId: visitInfo.UID, + AppId: commod.KFK_APPID, + SysType: visitInfo.SysType, + DevType: visitInfo.DevType, + IP: visitInfo.IP, + Version: visitInfo.Ver, + DevID: visitInfo.DevID, + VisitAt: visitInfo.CreatedAt, + PlatformId: visitInfo.ID.Hex(), + IsDirect: visitInfo.IsDirect, + DistrictCode: visitInfo.DistrictCode, + RegisterTime: visitInfo.RegisterTime, + IsDeduction: visitInfo.IsDeduction, + } + } + return accessList, nil +} + +// UserRegisterList 注册数据同步 +func UserRegisterList(request StatcenterSyncReq) ([]commod.UserRegisterMsg, error) { + userList, err := usermod.StatcenterSyncList(request.UserId, request.MaxSize) + if err != nil { + log.Error("UserRegisterList AccessSyncById ", log.E(err)) + return nil, err + } + registerList := make([]commod.UserRegisterMsg, len(userList)) + for i, userInfo := range userList { + registerList[i] = commod.UserRegisterMsg{ + UserId: userInfo.UID, + AppId: commod.KFK_APPID, + SysType: userInfo.SysType, + DevType: userInfo.DevType, + Mobile: userInfo.Mobile, + Name: userInfo.Name, + IP: userInfo.RegisterIP, + IsDirect: userInfo.IsDirect, + DistrictCode: userInfo.DistrictCode, + PromSeqe: userInfo.PromSeqe, + PUC: userInfo.PUC, + PromCode: userInfo.PromCode, + RegisterTime: userInfo.CreatedAt, + PlatformId: userInfo.ID.Hex(), + } + } + return registerList, nil +} + +// UserRegisterList 注册数据同步 +func UserInviterList(request StatcenterSyncReq) ([]commod.UserInviteBindMsg, error) { + data, err := proxymod.StatCenterSyncInviteList(request.SuccessTime, request.MaxSize) + if err != nil { + log.Error("UserInviteList InviteSyncByInviteTime ", log.E(err)) + return nil, err + } + res := make([]commod.UserInviteBindMsg, len(data)) + for i, v := range data { + res[i] = commod.UserInviteBindMsg{ + UserId: v.UID, + AppId: commod.KFK_APPID, + ParentPromCode: v.InviteCode, + InviteTime: v.InviteTime, + } + } + return res, nil +} + +// UserBindingList 绑定数据同步 +func UserBindingList(request StatcenterSyncReq) ([]commod.UserBindingMsg, error) { + userList, err := usermod.StatcenterSyncBindUserList(request.SuccessTime, request.MaxSize) + if err != nil { + log.Error("UserBindingList AccessSyncById ", log.E(err)) + return nil, err + } + bindingList := make([]commod.UserBindingMsg, len(userList)) + for i, userInfo := range userList { + bindingList[i] = commod.UserBindingMsg{ + UserId: userInfo.UID, + AppId: commod.KFK_APPID, + SysType: userInfo.SysType, + DevType: userInfo.DevType, + Mobile: userInfo.Mobile, + PlatformId: userInfo.ID.Hex(), + BindingTime: userInfo.MobileBindAt, + } + } + return bindingList, nil +} + +// ConsumeRecordList 消费流水同步 +func ConsumeRecordList(request StatcenterSyncReq) ([]commod.ConsumeRecordMsg, error) { + var typeInts = []txnmod.TransType{txnmod.PayVIP, txnmod.MeetingCard, + txnmod.BuyVIP, txnmod.VideoFreeCard, txnmod.VideoDiscount, + txnmod.Other, txnmod.LouFeng, txnmod.LouFengMianFei, txnmod.BookLoufeng, txnmod.CoinMonthCard} + objId, err := primitive.ObjectIDFromHex(request.PlatformId) + if err != nil { + return nil, err + } + f := bson.M{"tranTypeInt": bson.M{"$in": typeInts}, "_id": bson.M{"$gt": objId}} + opt := options.Find().SetLimit(request.MaxSize).SetSort(bson.D{{Key: "_id", Value: 1}}) + _, txns, err := txnmod.FindTransactionLogs(f, opt) + if err != nil { + log.Error("UserBindingList AccessSyncById ", log.E(err)) + return nil, err + } + list := make([]commod.ConsumeRecordMsg, len(txns)) + for i, v := range txns { + temp := commod.ConsumeRecordMsg{ + AppID: commod.KFK_APPID, + UID: v.UID, + CurrencyType: v.CurrencyType, + Amount: decimal.NewFromFloat(v.ActualAmount), + Uniq: v.ID.Hex(), + CreatedAt: v.CreatedAt, + } + switch txnmod.TransType(v.TranTypeInt) { + case txnmod.PayVIP: + temp.Type = commod.StatVipCard + case txnmod.LouFeng, txnmod.LouFengMianFei, txnmod.BookLoufeng: + temp.Type = commod.StatLouFeng + case txnmod.Other, txnmod.MeetingCard: + temp.Type = commod.StatValueAddSer + } + if v.CurrencyType == commod.CurrencyTypeCash { + temp.Money = decimal.NewFromFloat(v.ActualAmount).Shift(-1) + } + list[i] = temp + } + return list, nil +} + +// UserRechargeList 充值数据同步 +func UserRechargeList(request StatcenterSyncReq) ([]commod.UserRechargeMsg, error) { + rechargeOrders, err := rchgordmod.StatCenterSyncRecharge(request.SuccessTime, request.MaxSize) + if err != nil { + log.Error("UserRechargeList StatCenterSyncRecharge ", log.E(err)) + return nil, err + } + data := make([]commod.UserRechargeMsg, len(rechargeOrders)) + for i, v := range rechargeOrders { + data[i] = commod.UserRechargeMsg{ + UserId: v.UID, + AppId: commod.KFK_APPID, + PlatformId: v.ID.Hex(), + SysType: v.DevType, + DevType: v.DevType, + ChannelName: v.Channel, + CID: v.Channel, + Type: v.RechargeType, + OrderId: v.ID.Hex(), + OID: v.OID, + Money: v.Money, + PayMoney: v.PayMoney, + Status: v.Status, + Rate: "12", + SuccessAt: v.SuccessAt, + ProductType: v.ProductType, + ChanShareMod: v.ChanShareMod, + } + } + return data, nil +} + +// UserAllOrderList 用户订单同步 +func UserAllOrderList(request StatcenterSyncReq) ([]commod.UserRechargeMsg, error) { + rechargeOrders, err := rchgordmod.StatCenterSyncOrder(request.SuccessTime, request.MaxSize) + if err != nil { + log.Error("UserRechargeList StatCenterSyncRecharge ", log.E(err)) + return nil, err + } + data := make([]commod.UserRechargeMsg, len(rechargeOrders)) + for i, v := range rechargeOrders { + data[i] = commod.UserRechargeMsg{ + UserId: v.UID, + AppId: commod.KFK_APPID, + PlatformId: v.ID.Hex(), + SysType: v.DevType, + DevType: v.DevType, + ChannelName: v.Channel, + CID: v.Channel, + Type: v.RechargeType, + OrderId: v.ID.Hex(), + OID: v.OID, + Money: v.Money, + PayMoney: v.PayMoney, + Status: v.Status, + Rate: "12", + SuccessAt: v.CreatedAt, + ProductType: v.ProductType, + ChanShareMod: v.ChanShareMod, + } + } + return data, nil +} + +// CardSellList 会员卡特权卡销售流水同步 +func CardSellList(req StatcenterSyncReq) (list []commod.CardSellMsg, err error) { + log.Debug("CardSellList job start running") + typeInts := []txnmod.TransType{txnmod.MeetingCard, txnmod.LouFengDiscount, txnmod.LouFengMianFei, + txnmod.BuyVIP, txnmod.VideoDiscount, txnmod.VideoFreeCard, txnmod.PayVIP, txnmod.BuyAdvanceVIP, + txnmod.BuyBalanceVIP, txnmod.BuyGameAdvanceVIP, txnmod.BuyWhoringCard, + } + startID, err := primitive.ObjectIDFromHex(req.PlatformId) + if err != nil { + return + } + filter := bson.M{"tranTypeInt": bson.M{"$in": typeInts}, "_id": bson.M{"$gt": startID}} + opts := options.Find().SetLimit(req.MaxSize).SetSort(bson.D{{Key: "_id", Value: 1}}) + _, logs, err := txnmod.FindTransactionLogs(filter, opts) + if err != nil { + return + } + logsLen := len(logs) + productIDs := make([]primitive.ObjectID, 0, logsLen) + historyIDs := make([]primitive.ObjectID, 0, logsLen) + for _, l := range logs { + if l.ProductID != nil && *l.ProductID != "" { + productID, err := primitive.ObjectIDFromHex(*l.ProductID) + if err != nil { + log.Error("primitive.ObjectIDFromHex", log.Any("productID", *l.ProductID), log.E(err)) + continue + } + productIDs = append(productIDs, productID) + } else if !l.TransNo.IsZero() { + historyIDs = append(historyIDs, l.TransNo) + } + } + historyMap, err := getProductsByHistories(historyIDs) + if err != nil { + return + } + productMap, err := getProductPositions(productIDs) + if err != nil { + return + } + list = make([]commod.CardSellMsg, len(logs)) + for i, l := range logs { + amount := l.Amount + if amount < 0 { + amount = -amount + } + msg := commod.CardSellMsg{ + AppID: commod.KFK_APPID, + UID: l.UID, + UniqID: l.ID.Hex(), + Amount: amount, + TranTypeInt: l.TranTypeInt, + TranType: l.TranType, + SysType: l.SysType, + CurrencyType: l.CurrencyType, + TranCreatedAt: l.CreatedAt, + } + if !l.TransNo.IsZero() { + msg.Product = historyMap[l.TransNo] + } else if l.ProductID != nil && *l.ProductID != "" { + msg.Product = productMap[*l.ProductID] + } else { + msg.Product = commod.Product{ + ProductType: txnmod.TranType2ProductType[txnmod.TransType(l.TranTypeInt)], + Position: detectProductPosition(l), + } + } + list[i] = msg + } + log.Debug("CardSellList job start finished") + return +} + +func getProductsByHistories(historyIDs []primitive.ObjectID) (products map[primitive.ObjectID]commod.Product, err error) { + _, histories, err := prdcthsomod.FindProductHistorys(bson.M{"_id": bson.M{"$in": historyIDs}}, options.Find()) + if err != nil { + return + } + historyMap := make(map[primitive.ObjectID]prdcthsomod.ProductHistory) + for _, history := range histories { + historyMap[history.ID] = *history + } + productIDs := make([]primitive.ObjectID, len(histories)) + for i, h := range histories { + productIDs[i] = h.ProductID + } + productMap, err := productmod.ListByIDsMap(productIDs) + if err != nil { + return + } + posIDs := make([]primitive.ObjectID, 0, len(productMap)) + for _, p := range productMap { + if p.Position != "" { + posID, err := primitive.ObjectIDFromHex(p.Position) + if err != nil { + log.Error("primitive.ObjectIDFromHex", log.Any("productID", p.ID.Hex()), + log.Any("position", p.Position), log.E(err)) + continue + } + posIDs = append(posIDs, posID) + } + } + positions, err := productposimod.FindByIDs(posIDs) + if err != nil { + log.Error("productposimod.FindByIDs", log.Any("positionIDs", posIDs), log.E(err)) + } + positionMap := make(map[string]productposimod.ProductPosition) + for _, p := range positions { + positionMap[p.ID.Hex()] = p + } + products = make(map[primitive.ObjectID]commod.Product) + for hID, h := range historyMap { + product := commod.Product{Position: commod.Position{}} + pro, ok := productMap[h.ProductID] + if ok { + product.ID = pro.ID.Hex() + product.Name = pro.Name + product.DiscountedPrice = pro.DiscountedPrice + product.ProductType = pro.ProductType + pos, ok := positionMap[pro.Position] + if ok { + product.Position.ID = pos.ID + product.Position.Name = pos.Name + } + } + products[hID] = product + } + return +} + +func getProductPositions(productIDs []primitive.ObjectID) (products map[string]commod.Product, err error) { + productList, err := productmod.ListToIDs(productIDs, "") + if err != nil { + return + } + positionIDs := make([]primitive.ObjectID, 0, len(productList)) + for _, p := range productList { + if p.Position != "" { + positionID, err := primitive.ObjectIDFromHex(p.Position) + if err != nil { + log.Error("primitive.ObjectIDFromHex", log.Any("position", p.Position), log.E(err)) + continue + } + positionIDs = append(positionIDs, positionID) + } + } + positionList, err := productposimod.FindByIDs(positionIDs) + if err != nil { + return + } + positionMap := make(map[string]productposimod.ProductPosition) + for _, p := range positionList { + positionMap[p.ID.Hex()] = p + } + products = make(map[string]commod.Product) + for _, p := range productList { + pos := positionMap[p.Position] + position := commod.Position{ + ID: pos.ID, + Name: pos.Name, + } + products[p.ID.Hex()] = commod.Product{ + ID: p.ID.Hex(), + Name: p.Name, + DiscountedPrice: p.DiscountedPrice, + ProductType: p.ProductType, + Position: position, + } + } + return +} + +func detectProductPosition(txnLog *txnmod.TransactionLog) (position commod.Position) { + positionMap, err := productposimod.FindAllNameMap() + if err != nil { + return + } + pos := productposimod.ProductPosition{} + if txnLog != nil { + switch txnLog.TranTypeInt { + case int64(txnmod.BuyVIP), int64(txnmod.PayVIP): + pos = positionMap["会员卡"] + case int64(txnmod.MeetingCard), int64(txnmod.LouFengDiscount), int64(txnmod.LouFengMianFei), + int64(txnmod.Other), int64(txnmod.VideoDiscount), int64(txnmod.VideoFreeCard): + pos = positionMap["特权卡"] + default: // 默认特权卡 + pos = positionMap["特权卡"] + } + } + position.ID = pos.ID + position.Name = pos.Name + return +} diff --git a/app/api/statcenterctl/utils.go b/app/api/statcenterctl/utils.go new file mode 100644 index 0000000..c8ed830 --- /dev/null +++ b/app/api/statcenterctl/utils.go @@ -0,0 +1,24 @@ +package statcenterctl + +import ( + "context" + "fmt" + "net/http" + + "91porn-server/common/httputil" + "91porn-server/common/log" +) + +// HttpRequest HttpRequest +func HttpRequest(ctx context.Context, url string, request interface{}, resp interface{}) error { + code, err := httputil.DefaultClientPostJsonWithResp(resp, url, nil, &request) + if err != nil { + log.Error("HttpRequest err", log.Any("Url", url), log.E(err)) + return err + } + if code != http.StatusOK { + log.Error("HttpRequest err", log.Any("code", code)) + return fmt.Errorf("code err, %d", code) + } + return nil +} diff --git a/app/api/staticctrl/staticctrl.go b/app/api/staticctrl/staticctrl.go new file mode 100644 index 0000000..2fad587 --- /dev/null +++ b/app/api/staticctrl/staticctrl.go @@ -0,0 +1,29 @@ +package staticctrl + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// FaqPage doc +// @Summary 用户常见问题 H5页面 +// @Description 用户常见问题列表 +// @Tags 用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Router /api/app/static/faq/html/index [get] +func HtmlFaqPage(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", "") +} + +// FaqPage doc +// @Summary 用户常见问题 H5页面 +// @Description 用户常见问题列表 +// @Tags 用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Router /api/app/static/faq/tmpl/index [get] +func TmplFaqPage(c *gin.Context) { + c.HTML(http.StatusOK, "index.tmpl", "") +} diff --git a/app/api/tagctrl/tagctrl.go b/app/api/tagctrl/tagctrl.go new file mode 100644 index 0000000..6f56000 --- /dev/null +++ b/app/api/tagctrl/tagctrl.go @@ -0,0 +1,401 @@ +package tagctrl + +import ( + "91porn-server/app/service/mediaser" + "91porn-server/web/service/vidser" + "fmt" + + "91porn-server/app/service/tagser" + "91porn-server/common" + "91porn-server/common/filter" + "91porn-server/common/stderr" + v10 "91porn-server/common/v10" + "91porn-server/models/commod" + "91porn-server/models/v/usertagmod" + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// VidList doc +// @Summary 专题 - 视频列表 +// @Description 根据标签获取视频列表 +// @Tags 圈子 +// @Accept json +// @Produce json +// @Param q query vidser.GetVideoListByTagReq true "参数" +// @Success 200 {object} vidser.GetVideoListByTagReq "success" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/tag/vid/list [get] +func VidList(ctx *gin.Context) { + var req *vidser.GetVideoListByTagReq + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "tagCtrl VidList arg error "+err.Error()) + return + } + rep, err := req.GetVideoListByTag() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeJSON(ctx, stderr.Success, rep) + +} + +// UserTagList 用户的标签列表 +// @Summary 专题 - 用户的标签列表 +// @Description 获取用户标签列表 +// @Tags Special Topic +// @Accept json +// @Produce json +// @Param pageNumber query integer true "页码" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /tag/user/list [get] +func UserTagList(ctx *gin.Context) { + var arg struct { + PageNumber uint `form:"pageNumber" json:"pageNumber" binding:"required"` + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "tagCtrl Usertag arg error "+err.Error()) + return + } + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, "tagCtrl Usertag USER_ID is not exist") + return + } + data, err := usertagmod.UserTagList(uid, arg.PageNumber) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, "tagCtrl UserTagList faild "+err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// Group 专题列表,获取用户喜欢标签及标签下对应的视频列表(默认3个视频) +// @Summary 专题模块 - 标签视频列表 +// @Description 查询所有的标签和对应的视频 +// @Tags Special Topic +// @Accept json +// @Produce json +// @Param pageNumber formData integer true "页数" +// @Param pageSize formData integer true "每页条数" +// @Success 200 {object} tagser.TagGroupResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/tag/group [get] +func Group(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + param := commod.Page{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + resp := tagser.GetTagsList(uid, param) + common.ServeJSON(ctx, stderr.Success, resp) +} + +// AddUserTag 话题列表 点击红心按钮添加标签到用户标签列表中 +// @Summary 专题模块 - 给用户添加标签 +// @Description 给用户添加一个标签信息 +// @Tags Special Topic +// @Accept json +// @Produce json +// @Param tagID formData string true "标签id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/tag/add [post] +func AddUserTag(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + type Info struct { + TagID primitive.ObjectID `form:"tagID" json:"tagID" binding:"required"` + } + param := Info{} + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if err = tagser.AddToUserTag(uid, param.TagID); err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + code := stderr.Success + common.ServeJSON(ctx, code, nil) +} + +// DeleteUserTag 删除用户标签 +// @Summary 专题模块 - 删除标签 +// @Description 给用户删除一个标签 +// @Tags Special Topic +// @Accept json +// @Produce json +// @Param tagId formData string true "标签id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/tag/deleteUserTag [delete] +func DeleteUserTag(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + type Info struct { + TagID primitive.ObjectID `form:"tagId" json:"tagId" binding:"required"` + } + param := Info{} + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if err = tagser.DeleteUserTag(uid, param.TagID); err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + code := stderr.Success + common.ServeJSON(ctx, code, nil) +} + +// TagList 标签列表 +// @Summary 专题模块 - 标签列表 +// @Description 获取标签列表 +// @Tags Special Topic +// @Accept json +// @Produce json +// @Param pageNumber query integer true "页码" +// @Param pageSize query integer true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "data":tagser.Tag}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /tag/list [get] +func TagList(ctx *gin.Context) { + wordGroup := tagser.GetTagGroup(16) + common.ServeJSON(ctx, stderr.Success, wordGroup) +} + +func V2TagList(ctx *gin.Context) { + var param struct { + Content string `form:"content" json:"content"` + commod.Page + } + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data := tagser.GetCommonUsedTagList(param.Content, param.PageNumber, param.PageSize) + common.ServeJSON(ctx, stderr.Success, data) +} + +// TagListMostPlayed 标签列表-根据播放量由高到低排序 +// @Summary 专题模块 - 标签列表 +// @Description 获取标签列表,根据播放量由高到低排序 +// @Tags Special Topic +// @Accept json +// @Produce json +// @Param pageNumber query integer true "页码" +// @Param pageSize query integer true "每页条数" +// @Success 200 {object} tagser.MostPlayedTagListResponse +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /tag/list/most-played [get] +func TagListMostPlayed(c *gin.Context) { + req := tagser.MostPlayedTagListRequest{} + if err := c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + resp, err := tagser.GetMostPlayedTagList(req) + if err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, err) + return + } + common.ServeJSON(c, stderr.Success, resp) +} + +// TagConfList 标签列表 +// @Summary 专题模块 - 标签列表,获取后台配置标签列表 +// @Description 获取标签列表,由后台配置 +// @Tags Special Topic +// @Accept json +// @Produce json +// @Success 200 {object} tagser.AllTagConfResponse +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /tag/conf/list [get] +func TagConfList(c *gin.Context) { + resp, err := tagser.GetCommonUsedRecmdTags() + if err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, err) + return + } + common.ServeJSON(c, stderr.Success, resp) +} + +// RelatedTagList doc +// @Summary 专题模块 - 根据用户输入的内容获取关联标签列表(模糊查询) +// @Description 相关标签列表 +// @Tags Special Topic +// @Accept json +// @Produce json +// @Param content query string true "标签名字" +// @Param pageNumber query integer true "页数" +// @Param pageSize query integer true "条数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/tag/related/list [get] +func RelatedTagList(ctx *gin.Context) { + type Info struct { + Content string `form:"content" json:"content" binding:"required"` + commod.Page + } + param := Info{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + s, _ := filter.TagFilter.Filter(param.Content) + if len(s) > 0 { + common.ServeJSON(ctx, stderr.TagAddTagNameInvalidErr, fmt.Errorf("Invalid tag name: %s, invaild tag: %v", param.Content, s)) + return + } + // 获取总条数 + resp := make(map[string]interface{}) + if param.PageNumber == 1 { + count, err := tagser.GetTagsCountByRegexName(param.Content) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + resp["count"] = count + } + code, data, err := tagser.GetRelatedTagsList(param.Content, param.Page) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + resp["list"] = data + common.ServeJSON(ctx, code, resp) +} + +// AddNewTag doc +// @Summary 用户模块 - 新增标签 +// @Description 新增一个标签信息 +// @Tags Special Topic +// @Accept json +// @Produce json +// @Param tagName formData string true "标签名字tagName" +// @Param coverImg formData string false "封面图片" +// @Param description formData string false "说明" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/tag/add/new [post] +func AddNewTag(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + type Info struct { + TagName string `form:"tagName" json:"tagName" binding:"required"` // 标签名字 + CoverImg string `form:"coverImg" json:"coverImg" binding:"omitempty"` // 封面图片 + Description string `form:"description" json:"description" binding:"omitempty"` // 文字说明 + } + param := Info{} + err = ctx.ShouldBind(¶m) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if !v10.IsPureChar(param.TagName) { + common.ServeJSON(ctx, stderr.TagAddTagNameInvalidErr, fmt.Errorf("Invalid tag name: %s", param.TagName)) + return + } + tagName := v10.ExtractPureChar(param.TagName) + if tagName == "" { + common.ServeJSON(ctx, stderr.TagAddTagNameEmptyErr, fmt.Errorf("tag name can't empty: %s", param.TagName)) + return + } + s, _ := filter.TagFilter.Filter(tagName) + if len(s) > 0 { + common.ServeJSON(ctx, stderr.TagAddTagNameInvalidErr, fmt.Errorf("Invalid tag name: %s, invaild tag: %v", param.TagName, s)) + return + } + data, err := tagser.UserAddNewTag(uid, tagName, param.CoverImg, param.Description) + if err != nil { + if stderr.IsEqual(err, stderr.InsertExistError) { + common.ServeJSON(ctx, stderr.TagAddTagNameExistedErr, err) + return + } + common.ServeJSON(ctx, stderr.Failure, err) + return + } + code := stderr.Success + common.ServeJSON(ctx, code, data) +} + +// GetTagInfo doc +// @Summary 用户模块 - 标签详情 +// @Description 获取标签详细信息(标签对应的视频列表) +// @Tags Special Topic +// @Accept json +// @Produce json +// @Param tagID formData string true "标签id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/tag/info [get] +func GetTagInfo(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + // 获取标签详情 + type Info struct { + TagID string `form:"tagID" json:"tagID" binding:"required"` + } + param := Info{} + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + tagID, err := primitive.ObjectIDFromHex(param.TagID) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + code, data, err := tagser.GetTagInfo(uid, tagID) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, data) +} + +// MediaList doc +// @Summary 标签-ACG列表 +// @Description 根据标签获取ACG列表 +// @Tags 圈子 +// @Accept json +// @Produce json +// @Param q query mediaser.TagMediaListReq true "请求参数" +// @Success 200 object mediaser.TagMediaListResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/api/tag/media/list [get] +func MediaList(ctx *gin.Context) { + var req mediaser.TagMediaListReq + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "tagCtrl MediaList arg error "+err.Error()) + return + } + + resp, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + + common.ServeJSON(ctx, stderr.Success, resp) +} diff --git a/app/api/taskctrl/sign.go b/app/api/taskctrl/sign.go new file mode 100644 index 0000000..04014d9 --- /dev/null +++ b/app/api/taskctrl/sign.go @@ -0,0 +1,86 @@ +package taskctrl + +import ( + "91porn-server/app/service/taskser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// 签到 +func Sign(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + log.Info("Sign start", log.Any("uid", uid)) + var params struct { + ID string `json:"id" binding:"required"` + } + if err = c.ShouldBindJSON(¶ms); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err.Error()) + return + } + log.Info("Sign start", log.Any("uid", uid), log.Any("params", params)) + if code := taskser.Sign(uid, params.ID); code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, stderr.Success, "success") +} + +// 补签 +func ReSign(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + log.Info("ReSign start", log.Any("uid", uid)) + var params struct { + ID string `json:"id" binding:"required"` + } + if err = c.ShouldBindJSON(¶ms); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err.Error()) + return + } + log.Info("ReSign start", log.Any("uid", uid), log.Any("params", params)) + if code := taskser.ReSign(uid, params.ID); code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, stderr.Success, "success") +} + +// 获取签到的额外奖励 +func SignExtraPrizes(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + data, code := taskser.SignExtraPrizes(uid) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, stderr.Success, data) +} + +// 获取签到信息 +func GetSignDetails(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + data, code := taskser.GetSignDetails(uid) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, stderr.Success, data) +} diff --git a/app/api/taskctrl/task.go b/app/api/taskctrl/task.go new file mode 100644 index 0000000..41551ee --- /dev/null +++ b/app/api/taskctrl/task.go @@ -0,0 +1,238 @@ +package taskctrl + +import ( + "91porn-server/models/v/taskmod" + "fmt" + "sync" + + "91porn-server/app/service/activityclient" + "91porn-server/app/service/taskser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + + "github.com/gin-gonic/gin" +) + +/* +// 获取任务列表 +func GetTaskList(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + data, code := taskser.GetTaskList(uid) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, stderr.Success, data) +} + +// 获取任务详情 +func GetTaskDetails(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + log.Info("GetTaskDetails start", log.Any("uid", uid)) + var params struct { + Type int `form:"type" binding:"required,min=3"` + } + if err = c.ShouldBindQuery(¶ms); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err.Error()) + return + } + log.Info("GetTaskDetails start", log.Any("uid", uid), log.Any("params", params)) + data, code := taskser.GetTaskDetails(uid, params.Type) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, stderr.Success, data) +} + +// 领取宝箱奖励 +func GetBoon(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + log.InfoX(c, "GetJewelBoxPrize start", log.Any("uid", uid)) + var params struct { + ID string `json:"id"` + Type int `json:"type"` + } + if err = c.ShouldBindJSON(¶ms); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err.Error()) + return + } + log.InfoX(c, "GetJewelBoxPrize start", log.Any("uid", uid), log.Any("params", params)) + if code := taskser.GetBoon(c, uid, params.ID, params.Type); code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, stderr.Success, "success") +} +*/ + +// GetNewTask doc +// @Summary 任务列表 +// @Description 任务列表 +// @Tags 福利任务 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} taskser.NewTaskResponse "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/task/list [post] +func GetNewTask(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + + var user *usermod.User + var wallet *walletmod.Wallet + user, _ = usermod.FindUserByUID(uid) + if user != nil { + wallet, _ = walletmod.GetWallet(uid) + } + + var dailyTasks []taskser.DailyTaskResponse + var onceTasks []taskser.OnceTaskResponse + var growthTasks []*taskser.GrowthTaskResponse + var dailyTaskCode, onceTaskCode, growthCode stderr.Code + var dailyTaskMsg, onceTaskMsg, growthTaskMsg string + wg := sync.WaitGroup{} + wg.Add(3) + common.Go(func() { + defer wg.Done() + dailyTasks, dailyTaskCode, dailyTaskMsg = taskser.GetDailyTask(uid) + }) + common.Go(func() { + defer wg.Done() + onceTasks, onceTaskCode, onceTaskMsg = taskser.GetOnceTask(uid) + }) + common.Go(func() { + defer wg.Done() + growthTasks, growthCode, growthTaskMsg = taskser.GetGrowthTask(uid) + }) + wg.Wait() + if dailyTaskCode != stderr.Success { + common.ServeJSON(c, dailyTaskCode, dailyTaskMsg) + return + } + if onceTaskCode != stderr.Success { + common.ServeJSON(c, onceTaskCode, onceTaskMsg) + return + } + if growthCode != stderr.Success { + common.ServeJSON(c, growthCode, growthTaskMsg) + return + } + + // 按 Link 中的 type 推导倒计时类型。CountdownType=1 (红包雨) 但活动服无可用场次时,过滤该任务。 + filteredDailyTasks := make([]taskser.DailyTaskResponse, 0, len(dailyTasks)) + for i := range dailyTasks { + start, end, ctype, ok := activityclient.ResolveCountdownByLink(dailyTasks[i].Link) + if !ok { + continue + } + dailyTasks[i].StartAt = start + dailyTasks[i].EndAt = end + dailyTasks[i].CountdownType = ctype + dailyTasks[i].Link = activityclient.ReplaceActivityDomain(dailyTasks[i].Link, user, wallet) + filteredDailyTasks = append(filteredDailyTasks, dailyTasks[i]) + } + dailyTasks = filteredDailyTasks + + filteredOnceTasks := make([]taskser.OnceTaskResponse, 0, len(onceTasks)) + for i := range onceTasks { + start, end, ctype, ok := activityclient.ResolveCountdownByLink(onceTasks[i].Link) + if !ok { + continue + } + onceTasks[i].StartAt = start + onceTasks[i].EndAt = end + onceTasks[i].CountdownType = ctype + onceTasks[i].Link = activityclient.ReplaceActivityDomain(onceTasks[i].Link, user, wallet) + filteredOnceTasks = append(filteredOnceTasks, onceTasks[i]) + } + onceTasks = filteredOnceTasks + + common.ServeJSON(c, stderr.Success, taskser.NewTaskResponse{ + DailyTasks: dailyTasks, + OnceTasks: onceTasks, + GrowthTasks: growthTasks, + }) +} + +// Receive doc +// @Summary 我的任务 - 领取积分 +// @Description 领取积分 +// @Tags 福利任务 +// @Accept mpfd,json +// @Produce json,html +// @Param taskId formData string true "任务ID" +// @Param type formData int true "任务类型 1、每日任务 2、一次行任务" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/task/receive [post] +func Receive(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + var in taskmod.ReceiveTaskReq + err = c.ShouldBindJSON(&in) + if err != nil { + log.Error(fmt.Sprintf("task Receive task param err:%v", err)) + common.ServeJSON(c, stderr.ErrParamError, err.Error()) + return + } + code := taskser.ReceiveTask(uid, &in) + if code != stderr.Success { + common.ServeJSON(c, code, code.Error()) + return + } + common.ServeJSON(c, stderr.Success, stderr.Success.Msg()) +} + +// Do doc +// @Summary 我的任务-做任务 +// @Description 做任务 +// @Tags 福利任务 +// @Accept mpfd,json +// @Produce json,html +// @Param taskId formData string true "任务ID" +// @Param type formData int true "任务类型 1、每日任务 2、一次行任务" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/task/do [post] +func Do(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + var in taskmod.DoTaskReq + err = c.ShouldBindJSON(&in) + if err != nil { + log.Error(fmt.Sprintf("task do task param err:%v", err)) + common.ServeJSON(c, stderr.ErrParamError, err.Error()) + return + } + code := taskser.DoTask(uid, &in) + if code != stderr.Success { + common.ServeJSON(c, code, code.Error()) + return + } + common.ServeJSON(c, stderr.Success, stderr.Success.Msg()) +} diff --git a/app/api/tonectrl/tonectrl.go b/app/api/tonectrl/tonectrl.go new file mode 100644 index 0000000..bb133be --- /dev/null +++ b/app/api/tonectrl/tonectrl.go @@ -0,0 +1,50 @@ +package tonectrl + +import ( + "91porn-server/app/service/searcher" + "91porn-server/app/service/searcher/vidtonesearcher" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/tonerecomod" + + "github.com/gin-gonic/gin" +) + +// VidList doc +// @Summary 获取音色最热视屏列表 +// @Description 获取音色最热视屏列表 +// @Tags Tone +// @Accept mpfd,json +// @Produce json,html +// @Param theme formData string true "主题"" +// @Param pageNumber formData int true "当前页" +// @Param pageSize formData int true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "data": vidtonesearcher.Result}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /tone/vid/list [post] +func VidList(c *gin.Context) { + var arg struct { + Theme tonerecomod.ThemeType `form:"theme" json:"theme" binding:"required"` + commod.Page + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "theme VidList arg error "+err.Error()) + return + } + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrAccessForbid, "theme VidList USER_ID is not exist ") + return + } + headOpt := &searcher.Option{} + headOpt.SetSkip(int64((arg.PageNumber - 1) * (arg.PageSize))) + headOpt.SetLimit(int64(arg.PageSize)) + vidToneSearcher := vidtonesearcher.NewVidToneSearcher(arg.Theme, uid) + result, err := vidToneSearcher.Search(nil, headOpt) + if err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, err) + return + } + common.ServeJSON(c, stderr.Success, result.Data()) +} diff --git a/app/api/txnactctr/txnactctr.go b/app/api/txnactctr/txnactctr.go new file mode 100644 index 0000000..2b879ea --- /dev/null +++ b/app/api/txnactctr/txnactctr.go @@ -0,0 +1,215 @@ +package txnactctr + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/txnactmod" + "91porn-server/models/v/usermod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetBanks doc +// @Summary 获取绑定的银行卡列表 +// @Description 银行卡绑定 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/txnact/banks [get] +func GetBanks(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + data, err := txnactmod.FindManyByActType(uid, txnactmod.Bank) + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": data, + }) +} + +// AddBank doc +// @Summary 绑定银行卡 +// @Description 银行卡绑定 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param actName body string true "账户姓名" +// @Param act body string true "账户号" +// @Param bankCode body string true "银行代号" +// @Param cardType body string true "卡类型" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/txnact/bank [post] +func AddBank(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + t := txnactmod.TransactionAct{AType: txnactmod.Bank} + if err = ctx.ShouldBind(&t); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + t.UID = uid + user, err := usermod.FindUserByUID(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, err.Error()) + return + } + if user.BankActName != "" && user.BankActName != t.ActName { + common.ServeJSON(ctx, stderr.DifferentBankActName, "") + return + } + if err = txnactmod.Insert(&t); err != nil { + if stderr.IsEqual(err, stderr.InsertExistError) { + common.ServeJSON(ctx, stderr.ErrWithDrawAccountHasBind, err.Error()) + return + } + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// EditTransactionAct doc +// @Summary 修改提现账户 +// @Description 修改提现账户 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param id body string true "id" +// @Param actName body string true "账户名字" +// @Param act body string true "账户号" +// @Param bankCode body string true "银行卡代码" +// @Param cardType body string true "卡类型" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/txnact/bank/update [post] +func EditTransactionAct(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + var p struct { + ID primitive.ObjectID `json:"id"` + txnactmod.TransactionActSelector + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if p.ActName != nil { + var txa txnactmod.TransactionAct + txa, err = txnactmod.FindOneByID(p.ID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, err.Error()) + return + } + if txa.AType == txnactmod.Bank { + user, err := usermod.FindUserByUID(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, err.Error()) + return + } + if user.BankActName != "" && user.BankActName != *p.ActName { + common.ServeJSON(ctx, stderr.DifferentBankActName, "") + return + } + } + } + if _, err = txnactmod.Update(p.ID, &p.TransactionActSelector); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// GetAlipays doc +// @Summary 获取支付宝列表 +// @Description 银行卡绑定 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/txnact/alipays [get] +func GetAlipays(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + data, err := txnactmod.FindManyByActType(uid, txnactmod.Alipay) + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": data, + }) +} + +// AddAliPay doc +// @Summary 绑定支付宝 +// @Description 绑定支付宝 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param actName body string true "账户姓名" +// @Param act body string true "账户号" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/txnact/alipay [post] +func AddAliPay(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + t := txnactmod.TransactionAct{AType: txnactmod.Alipay} + if err = ctx.ShouldBind(&t); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + t.UID = uid + if err = txnactmod.Insert(&t); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// DelAlipay doc +// @Summary 删除除提现账户 +// @Description 删除除提现账户 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param id body string true "id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/txnact/del [delete] +func DelTxAccount(ctx *gin.Context) { + var p struct { + ID primitive.ObjectID `json:"id"` + } + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if _, err := txnactmod.DeleteByID(p.ID); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/app/api/updownctrl/updownctrl.go b/app/api/updownctrl/updownctrl.go new file mode 100644 index 0000000..a152e0d --- /dev/null +++ b/app/api/updownctrl/updownctrl.go @@ -0,0 +1,667 @@ +package updownctrl + +import ( + "encoding/base64" + "fmt" + "io" + "net/http" + "net/url" + "path/filepath" + "strconv" + "strings" + "sync" + "time" + + "91porn-server/app/appg" + "91porn-server/app/service/m3u8ticket" + "91porn-server/app/service/updownloadser" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/hevcpull" + "91porn-server/common/log" + "91porn-server/common/m3u8" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/sourcemod" + "91porn-server/models/v/usermod" + + "github.com/gin-gonic/gin" +) + +const ( + Retries = 3 //重试3次 + transcodeM3u8RoutePrefix = "/api/app/vid/transcode/m3u8/" + transcodeSigningOriginHost = "hevc-pull.invalid" + // ctxM3u8TicketRequired 标记当前 m3u8 路由需要做 H5 防盗链票据校验。 + // 仅 App H5 播放路由挂载 RequireM3u8Ticket;官网/分享等自有鉴权路由不挂载,避免误伤。 + ctxM3u8TicketRequired = "m3u8_ticket_required" +) + +// RequireM3u8Ticket 是一个标记中间件:挂到某条 m3u8 路由后,DownloadM3u8H5 会对其启用票据校验。 +// 未挂载的路由保持旧逻辑(不验票),从而把防盗链范围精确限定在 App H5 播放地址上。 +func RequireM3u8Ticket(c *gin.Context) { + c.Set(ctxM3u8TicketRequired, true) +} + +var interval = []int64{5, 5, 10, 15} //通知时间间隔 + +// Upload doc +// @Summary 文件管理 - 表单上传文件 +// @Description 表单上传文件 +// @Tags uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param upload formData file true "文件" +// @Param id formData string true "文件ID" +// @Success 200 {string} json "{"msg": "success" "data":{"coverImg":"xxxxxxxxxx.ext"}}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /vid/upload [post] +func Upload(c *gin.Context) { + headers, err := c.FormFile("upload") + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + id := c.PostForm("id") + if id == "" { + common.ServeJSON(c, stderr.ErrUploadError, "id is required") + return + } + f, err := headers.Open() + if err != nil { + log.Warn("headers Open file wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + byteData, err := io.ReadAll(f) + f.Close() + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + fileData := base64.StdEncoding.EncodeToString(byteData) + ext := strings.TrimLeft(filepath.Ext(headers.Filename), ".") + resp, err := updownloadser.SendVidCover2FS(id, ext, fileData) + if err != nil { + log.Warn("file upload wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + if resp.Code == http.StatusOK { + common.ServeJSON(c, stderr.Success, resp.Data) + return + } + common.ServeJSON(c, stderr.ErrUploadError, "") +} + +// UploadStatic doc +// @Summary 文件管理 - 表单上传文件 +// @Description 表单上传文件,上传静态文件到AWS 上传独立文件 +// @Tags uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param upload formData file true "文件" +// @Success 200 {string} json "{"msg": "success" "data":{"coverImg":"xxxxxxxxxx.ext"}}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /vid/uploadStatic [post] +func UploadStatic(c *gin.Context) { + headers, err := c.FormFile("upload") + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + f, err := headers.Open() + if err != nil { + log.Warn("headers Open file wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + byteData, err := io.ReadAll(f) + f.Close() + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + fileData := base64.StdEncoding.EncodeToString(byteData) + resp, err := updownloadser.SendImageToFS(headers.Filename, fileData) + if err != nil { + log.Warn("file upload wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + if resp.Code == http.StatusOK { + common.ServeJSON(c, stderr.Success, gin.H{"coverImg": resp.Data.FileName}) + return + } + common.ServeJSON(c, stderr.ErrUploadError, "") +} + +// UploadDotStream doc +// @Summary 文件管理 - 流式断点续传文件 +// @Description 流式断点续传 +// @Tags uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param ID header string true "文件ID 文件MD5做ID" +// @Param POS header string true "第几片视频" +// @Param TotalPos header string true "视频总片数" +// @Success 200 {string} json "{"msg": "success" "data":{"id":"5d8a2af58747044ca077f358","videoUri":"xxxxxxx.m3u8"} }" +// @Failure 400 {string} json "{"msg": "fail"}" +// @Router /vid/uploadDotStream [post] +func UploadDotStream(c *gin.Context) { + var ( + id, pos, totalPos string + ) + var cnt int + var resp commod.Resp + var httpErr error + data := c.Request.Body + id = c.GetHeader("ID") + pos = c.GetHeader("POS") + totalPos = c.GetHeader("TotalPos") + if id == "" || pos == "" || totalPos == "" { + common.ServeJSON(c, stderr.ErrParamError, "upload args error") + return + } + posint, _ := strconv.ParseInt(pos, 10, 32) + total, _ := strconv.ParseInt(totalPos, 10, 32) + byteData, err := io.ReadAll(data) + data.Close() + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + fileData := base64.StdEncoding.EncodeToString(byteData) + for cnt < Retries { + resp, httpErr = updownloadser.SendFile2FS(id, fileData, posint, total) + if httpErr == nil { + break + } + time.Sleep(time.Duration(interval[cnt]) * time.Second) + log.Warn("retry to upload file to file-server", log.Any("重试次数", cnt), log.E(httpErr)) + cnt++ + } + if httpErr != nil { + log.Warn("file upload wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, nil) + return + } + fmt.Println(resp) + if resp.Code == http.StatusOK { + common.ServeJSON(c, stderr.Success, resp.Data) + return + } + common.ServeJSON(c, stderr.ErrUploadError, "") +} + +// UploadDotJson doc +// @Summary 文件管理 - API断点续传文件 +// @Description API断点续传 +// @Tags uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "taskId 文件MD5做ID" +// @Param pos formData integer true "第几片视频" +// @Param totalPos formData integer true "视频总片数" +// @Param data formData string true "数据内容" +// @Success 200 {string} json "{"msg": "success" "data":{"id":"5d8a2af58747044ca077f358","videoUri":"xxxxxxx.m3u8"} }" +// @Failure 400 {string} json "{"msg": "fail"}" +// @Router /vid/uploadDotJson [post] +func UploadDotJson(c *gin.Context) { + uid, err := common.GetUID(c) + if err == nil && uid > 0 { + //判断该用户是否被禁止上传视频 + user, err := usermod.FindUserByUID(uid) + if err != nil || (user != nil && user.ForbidUpload) { + common.ServeJSON(c, stderr.ForbidUploadVideo, "") + return + } + } + var cnt int + var resp commod.Resp + var httpErr error + var args struct { + ID string `form:"id" json:"id" binding:"required"` //taskId + POS int64 `form:"pos" json:"pos" binding:"required"` //分片序号 + TotalPos int64 `form:"totalPos" json:"totalPos" binding:"required"` //总分片数 + Data string `form:"data" json:"data" binding:"required"` //分片内容 + } + if err = c.ShouldBind(&args); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + for cnt < Retries { + resp, httpErr = updownloadser.SendFile2FS(args.ID, args.Data, args.POS, args.TotalPos) + if httpErr == nil { + break + } + time.Sleep(time.Duration(interval[cnt]) * time.Second) + log.Warn("retry to upload file to file-server", log.Any("重试次数", cnt), log.E(httpErr)) + cnt++ + } + if httpErr != nil { + log.Warn("file upload wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, nil) + return + } + if resp.Code == http.StatusOK { + common.ServeJSON(c, stderr.Success, resp.Data) + return + } + common.ServeJSON(c, stderr.ErrUploadError, nil) +} + +// Download doc +// @Summary 文件管理 - 下载文件接口 +// @Description 下载文件 +// @Tags 正式 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /vid/m3u8/:source [get] +func DownloadM3u8(c *gin.Context) { + source := c.Param("source") + if source == "" { + common.ServeJSON(c, stderr.ErrParamError, "") + return + } + //cdn有值,则代表前端选线使用 + cdn := c.Query("c") + // 该接口不做严格校验:老明文链接原样、新带票链接解密还原真实 path,都能播放。 + source = m3u8ticket.StripTicket(source) + ext := filepath.Ext(source) + if ext != ".m3u8" { + common.ServeJSON(c, stderr.ErrMimeType, "") + return + } + fileName := filepath.Base(source) + + c.Writer.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName)) + cdns := sourcemod.GetCdnURL() + if len(cdns) <= 0 { + common.ServeJSON(c, stderr.Failure, "") + return + } + if cdn == "" { + //cdn为空时,则前端为老版本,没有选线 + //切记,后台配置第一个域名为当前系统常用cdn域名(eg:松鼠云) + cdn = cdns[0].Url + //去掉首尾反斜杠(/)、空格 + cdn = strings.Trim(cdn, "/ ") + } + byteBuff, err := m3u8.GetAPPM3u8(source, fileName, ext, cdn, updownloadser.FsIO) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"data": "", "msg": err.Error()}) + return + } + if byteBuff == nil { + log.Warn("can't create m3u8 file", log.Any("source", source)) + c.JSON(http.StatusBadRequest, gin.H{"data": "", "msg": stderr.CodeEmptyData.Msg()}) + return + } + c.Writer.Header().Add("Content-Length", strconv.Itoa(byteBuff.Len())) + c.Data(200, "application/octet-stream", byteBuff.Bytes()) +} + +// UploadStaticBatch doc +// @Summary 文件管理 - 表单上传文件 批量上传 +// @Description 表单上传文件,上传静态文件到文件服务器 用于独立文件上传 +// @Tags uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param upload[] formData file true "文件" +// @Success 200 {string} json "{"msg": "success" "data":{"coverImg":"xxxxxxxxxx.ext"}}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /vid/uploadStatic/batch [post] +func UploadStaticBatch(c *gin.Context) { + form, err := c.MultipartForm() + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + files := form.File["upload[]"] + batch := make([]*updownloadser.FileInfo, len(files)) + for i, f := range files { + fi, err := f.Open() + if err != nil { + log.Warn("headers Open file wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + byteData, err := io.ReadAll(fi) + fi.Close() + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + fileData := base64.StdEncoding.EncodeToString(byteData) + batch[i] = &updownloadser.FileInfo{ + FileName: &f.Filename, + FileData: &fileData, + } + } + resp, err := updownloadser.SendImageToFSBatch(updownloadser.InfoBatch{Batch: batch}) + if err != nil { + log.Warn("file upload wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + common.ServeJSON(c, stderr.Success, gin.H{"filePath": resp.Data.GetFileNames(), "success": resp.Data.Count()}) +} + +// DownloadM3u8H5 doc +// @Summary 文件管理 - 下载文件接口H5 +// @Description 下载文件 +// @Tags 正式 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /vid//h5/m3u8/:source [get] +func DownloadM3u8H5(c *gin.Context) { + // 统计请求来源(Referer/Origin):进程内无锁累加,后台定时批量刷回 Redis ZSet 计数。 + collectM3u8H5Referer(c) + source := c.Param("source") + if source == "" { + common.ServeJSON(c, stderr.ErrParamError, "") + return + } + //cdn有值,则代表前端选线使用 + cdn := c.Query("c") + // 防盗链:开启票据后校验,校验失败改下发广告兜底 m3u8,阻断盗链。 + // 带票地址是加密单段 token(无 .m3u8 后缀),故先验票解出真实 path,再判断后缀与取文件名。 + source = verifyH5M3u8Ticket(c, source) + ext := filepath.Ext(source) + if ext != ".m3u8" { + common.ServeJSON(c, stderr.ErrMimeType, "") + return + } + fileName := filepath.Base(source) + + c.Writer.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName)) + cdns := sourcemod.GetCdnURL() + if len(cdns) <= 0 { + common.ServeJSON(c, stderr.Failure, "") + return + } + if cdn == "" { + //cdn为空时,则前端为老版本,没有选线 + //切记,后台配置第一个域名为当前系统常用cdn域名(eg:松鼠云) + cdn = cdns[0].Url + //去掉首尾反斜杠(/)、空格 + cdn = strings.Trim(cdn, "/ ") + } + byteBuff, err := m3u8.GetAPPM3u8(source, fileName, ext, cdn, updownloadser.FsIO) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"data": "", "msg": err.Error()}) + return + } + if byteBuff == nil { + c.JSON(http.StatusBadRequest, gin.H{"data": "", "msg": stderr.CodeEmptyData.Msg()}) + return + } + c.Writer.Header().Add("Content-Length", strconv.Itoa(byteBuff.Len())) + c.Data(200, "application/octet-stream", byteBuff.Bytes()) +} + +// m3u8 请求来源计数(按 origin):进程内用 map[origin]次数 聚合,再由后台定时批量刷回 Redis ZSet。 +// 每个请求都计入(不去重,去重会丢失次数),把高频接口"每请求一次 Redis 写"降为"每周期每来源一次"。 +// ZSet:member=origin,score=该来源累计请求次数 —— ZScore 查单个来源,ZRevRangeWithScores 看 Top。 +var ( + m3u8RefererMu sync.Mutex + m3u8RefererCounts = make(map[string]int64) // key=origin,value=该来源累计请求次数 + m3u8RefererFlushOnce sync.Once +) + +// m3u8RefererFlushInterval 为聚合计数刷回 Redis 的周期;越短则崩溃丢失窗口越小、Redis 写越频繁。 +const m3u8RefererFlushInterval = 5 * time.Second + +// m3u8RefererResetHour 为来源累计计数每日清零的整点(time.Local,已在启动时设为 Asia/Shanghai): +// 每天该点该 ZSet 过期失效、从零重新累计。改此值即可调整清零时刻。 +const m3u8RefererResetHour = 5 + +// collectM3u8H5Referer 给当前请求来源(origin)的计数 +1:每个 origin 在 map 里各占一个计数。 +// 加锁只护一次 map 自增,不碰 Redis;计数由后台定时批量刷回 Redis(见 startM3u8RefererFlusher)。 +func collectM3u8H5Referer(c *gin.Context) { + origin := refererOrigin(c) + if origin == "" { + return + } + m3u8RefererFlushOnce.Do(startM3u8RefererFlusher) // 首个请求到来时惰性启动后台刷新协程 + m3u8RefererMu.Lock() + m3u8RefererCounts[origin]++ // 每个 origin 各自累加 + m3u8RefererMu.Unlock() +} + +// startM3u8RefererFlusher 启动后台协程,按固定周期把聚合计数批量刷回 Redis。 +func startM3u8RefererFlusher() { + common.Go(func() { + ticker := time.NewTicker(m3u8RefererFlushInterval) + defer ticker.Stop() + for range ticker.C { + FlushM3u8RefererStats() + } + }) +} + +// FlushM3u8RefererStats 换出当前按 origin 聚合的计数,逐个 ZIncrBy 刷回 Redis,供定时器与进程退出兜底调用。 +// 锁内只换出快照(不含 Redis IO),换出后本地表即清空(空闲来源自然淘汰);刷回失败的计数并回本地表、下个周期重试。 +func FlushM3u8RefererStats() { + if appg.Redis == nil { + return + } + m3u8RefererMu.Lock() + if len(m3u8RefererCounts) == 0 { + m3u8RefererMu.Unlock() + return + } + snapshot := m3u8RefererCounts + m3u8RefererCounts = make(map[string]int64) + m3u8RefererMu.Unlock() + + var failed map[string]int64 + for origin, cnt := range snapshot { + if _, err := appg.Redis.ZIncrBy(redisconst.M3u8H5RefererSet, float64(cnt), origin); err != nil { + if failed == nil { + failed = make(map[string]int64) + } + failed[origin] += cnt + } + } + // 每天凌晨 m3u8RefererResetHour 点整体清零:每次刷回都把过期续到下一个清零点,到点 Redis 删除该 key, + // 下次刷回自然重建、从零累计。用 EXPIREAT(绝对时间点)而非相对 TTL,故进程重启/无请求空窗期也照常按点失效。 + _, _ = appg.Redis.ExpireKeAt(redisconst.M3u8H5RefererSet, nextM3u8RefererResetAt()) + if len(failed) > 0 { + m3u8RefererMu.Lock() + for origin, cnt := range failed { + m3u8RefererCounts[origin] += cnt + } + m3u8RefererMu.Unlock() + log.Warn("flush m3u8 referer stats partially failed", log.Any("failedSources", len(failed))) + } +} + +// nextM3u8RefererResetAt 返回下一个每日清零时刻(今天 m3u8RefererResetHour 点未过则用今天,已过则用明天), +// 供刷回时给累计 ZSet 设 EXPIREAT。基于 time.Now()(time.Local=Asia/Shanghai),即北京时间。 +func nextM3u8RefererResetAt() time.Time { + now := time.Now() + reset := time.Date(now.Year(), now.Month(), now.Day(), m3u8RefererResetHour, 0, 0, 0, now.Location()) + if !now.Before(reset) { // 已到/过今天清零点,则顺延到明天 + reset = reset.AddDate(0, 0, 1) + } + return reset +} + +// refererOrigin 提取请求来源站点并归一到 scheme://host:优先 Referer,缺省或解析失败时回退 Origin。 +// 解析不出 host(非法 URL / Origin 为 "null" 等)返回空串丢弃,避免任意串塞进永不过期的 ZSet 撑爆内存。 +func refererOrigin(c *gin.Context) string { + if o := normalizeOrigin(c.GetHeader("Referer")); o != "" { + return o + } + return normalizeOrigin(c.GetHeader("Origin")) +} + +// normalizeOrigin 把来源头归一到 scheme://host;空串或解析不出 host 一律返回空串。 +func normalizeOrigin(raw string) string { + if raw == "" { + return "" + } + if u, err := url.Parse(raw); err == nil && u.Host != "" { + return u.Scheme + "://" + u.Host + } + return "" +} + +// verifyH5M3u8Ticket 校验 H5 m3u8 播放防盗链票据。 +// 未配置密钥时原样返回 source(保持旧逻辑);开启后票据非法/过期/IP 不符则返回广告兜底 source, +// 并打上 no-store 避免中间层把兜底 playlist 当正片缓存。 +func verifyH5M3u8Ticket(c *gin.Context, source string) string { + if !m3u8ticket.Enabled() { + return source + } + // 仅 h5/m3u8 这类显式标记 RequireM3u8Ticket 的路由做严格校验;其余路由(h5 light/官网/分享等)只把 + // 带票地址解密还原成真实 path,不校验,保证新老链接都能播放。 + if !c.GetBool(ctxM3u8TicketRequired) { + return m3u8ticket.StripTicket(source) + } + ip := common.GetIP(c) + ua := "" + if u, uaErr := common.GetUA(c); uaErr == nil { + ua = u.UserAgent + } + // 带票地址形如 /{version}/{token}.m3u8,真实 path 加密在 token 里。 + realSource, info, ok := m3u8ticket.VerifyPath(source, ip, ua) + if ok { + // 若上游 Auth 已解析出登录用户,则要求与票据签发用户一致,进一步绑定到本人。 + if uid := common.TryGetUID(c); uid > 0 && uid != info.UserID { + ok = false + } + } + if !ok { + log.Warn("DownloadM3u8H5 ticket invalid", + log.Any("source", source), + log.Any("ip", ip), + ) + c.Header("Cache-Control", "private, no-store") + return m3u8ticket.FallbackPath + } + return realSource +} + +// DownloadTranscodeM3u8 仅供 H.265 云转码服务拉取源播放列表。 +// URL 必须由 SKD 使用共享密钥签名,签名同时绑定资源路径和过期时间。 +func DownloadTranscodeM3u8(c *gin.Context) { + c.Header("Cache-Control", "no-store") + secret := "" + if appg.Conf != nil { + secret = appg.Conf.Hevc.PullSecret + } + if err := hevcpull.VerifyURL(c.Request.URL, secret, time.Now()); err != nil { + log.Warn("DownloadTranscodeM3u8 rejected", + log.Any("path", c.Request.URL.Path), + log.E(err), + ) + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ + "code": http.StatusUnauthorized, + "msg": "invalid or expired transcode pull signature", + }) + return + } + expiresUnix, err := strconv.ParseInt(c.Query(hevcpull.ExpiresParam), 10, 64) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ + "code": http.StatusUnauthorized, + "msg": "invalid or expired transcode pull signature", + }) + return + } + expiresAt := time.Unix(expiresUnix, 0).UTC() + // Downstream playback helpers and generic error logs do not need the + // bearer query after verification; remove it before any further handling. + c.Request.URL.RawQuery = "" + c.Request.RequestURI = c.Request.URL.RequestURI() + source := strings.TrimLeft(c.Param("source"), "/") + normalizedSource, err := hevcpull.NormalizeSource(source) + if err != nil || normalizedSource != source { + log.Warn("DownloadTranscodeM3u8 rejected non-canonical source", + log.Any("path", c.Request.URL.Path), + ) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ + "code": http.StatusBadRequest, + "msg": "invalid transcode pull source", + }) + return + } + + fileName := filepath.Base(normalizedSource) + if filepath.Ext(normalizedSource) != ".m3u8" { + common.ServeJSON(c, stderr.ErrMimeType, "") + return + } + cdns := sourcemod.GetCdnURL() + if len(cdns) <= 0 { + common.ServeJSON(c, stderr.Failure, "") + return + } + cdn := strings.Trim(cdns[0].Url, "/ ") + byteBuff, err := m3u8.GetAPPM3u8( + transcodePlaybackSource(normalizedSource), + fileName, + ".m3u8", + cdn, + updownloadser.FsIO, + ) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"data": "", "msg": err.Error()}) + return + } + byteBuff, err = m3u8.RewriteMasterPlaylist(byteBuff.Bytes(), func(childURI string) (string, error) { + return signedTranscodeChildPlaylistURI(normalizedSource, childURI, secret, expiresAt) + }) + if err != nil { + log.Warn("DownloadTranscodeM3u8 rewrite master failed", + log.Any("source", normalizedSource), + log.E(err), + ) + c.JSON(http.StatusBadRequest, gin.H{"data": "", "msg": "invalid transcode master playlist"}) + return + } + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName)) + c.Header("Content-Length", strconv.Itoa(byteBuff.Len())) + c.Data(http.StatusOK, "application/octet-stream", byteBuff.Bytes()) +} + +func transcodePlaybackSource(normalizedSource string) string { + return "/" + strings.TrimLeft(normalizedSource, "/") +} + +func signedTranscodeChildPlaylistURI(parentSource, childURI, secret string, expiresAt time.Time) (string, error) { + childSource, err := hevcpull.ResolveChildSource(parentSource, childURI) + if err != nil { + return "", err + } + unsigned := (&url.URL{ + Scheme: "https", + Host: transcodeSigningOriginHost, + Path: transcodeM3u8RoutePrefix + childSource, + }).String() + signed, err := hevcpull.SignURL(unsigned, secret, expiresAt) + if err != nil { + return "", err + } + parsed, err := url.Parse(signed) + if err != nil { + return "", err + } + // Root-relative output keeps the trusted host of the originally signed + // master URL and cannot be influenced by a forwarded Host header. + return parsed.RequestURI(), nil +} diff --git a/app/api/updownctrl/updownctrl_test.go b/app/api/updownctrl/updownctrl_test.go new file mode 100644 index 0000000..1695cd6 --- /dev/null +++ b/app/api/updownctrl/updownctrl_test.go @@ -0,0 +1,58 @@ +package updownctrl + +import ( + "net/url" + "strings" + "testing" + "time" + + "91porn-server/common/hevcpull" +) + +func TestSignedTranscodeChildPlaylistURI(t *testing.T) { + now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC) + expiresAt := now.Add(time.Hour) + secret := "test-pull-secret-strong-32-bytes!!" + + childURI, err := signedTranscodeChildPlaylistURI( + "sp/movie/master.m3u8", + "720/index.m3u8", + secret, + expiresAt, + ) + if err != nil { + t.Fatalf("signedTranscodeChildPlaylistURI failed: %v", err) + } + if !strings.HasPrefix(childURI, transcodeM3u8RoutePrefix+"sp/movie/720/index.m3u8?") { + t.Fatalf("unexpected child URI: %s", childURI) + } + parsed, err := url.Parse("https://app.example.com" + childURI) + if err != nil { + t.Fatalf("parse child URI: %v", err) + } + if err = hevcpull.VerifyURL(parsed, secret, now); err != nil { + t.Fatalf("child URI signature failed: %v", err) + } + + if _, err = signedTranscodeChildPlaylistURI( + "sp/movie/master.m3u8", + "https://external.example.com/index.m3u8", + secret, + expiresAt, + ); err == nil { + t.Fatal("absolute child playlist was accepted") + } +} + +func TestTranscodePlaybackSourcePreservesExistingM3u8Semantics(t *testing.T) { + for _, source := range []string{ + "sp/movie/index.m3u8", + "pms/movie/index.m3u8", + "laosiji/m3m/movie/index.m3u8", + } { + got := transcodePlaybackSource(source) + if got != "/"+source { + t.Fatalf("transcodePlaybackSource(%q) = %q", source, got) + } + } +} diff --git a/app/api/userctrl/jio.go b/app/api/userctrl/jio.go new file mode 100644 index 0000000..5ce64d3 --- /dev/null +++ b/app/api/userctrl/jio.go @@ -0,0 +1,46 @@ +package userctrl + +import "91porn-server/models/commod" + +// PageRule 通用分页验证结构体 +type PageRule struct { + UID uint64 `json:"uid" form:"uid"` + //hot 热度值排序;watch 最多播放;like 最多点赞(收藏);new 最新视频 + Type string `json:"type" form:"type"` + // 点赞类型: SP:长视频 SHORT:短视频 COVER:图文帖子 PIC:图集帖子 SEED_LINK:种子/黄油帖子 TAG:标签 COMMENT:评论 video:动漫 image:漫画 text:小说" + LikeType string `json:"likeType" form:"likeType"` + //0 默认全部 1 长视频 2 短视频 3-帖子 4-图集 + PlayTimeType int `json:"playTimeType" form:"playTimeType"` + commod.Page +} + +var portrait = []string{ + "image/gy/8j/hw/la/aa283b9a2c6c443883da2c6dec05c19a.jpeg", + "image/e5/oq/hd/hi/2a120f6ebb44468b92a1f0c55b4a475c.jpeg", + "image/tl/gc/lk/5p/96586678db814f579e6eaf8ef55165bc.jpeg", + "image/hm/21/19/57/e5c625d1a903453e8fc8e3e3575c860b.jpeg", + "image/0v/h7/g1/tb/f3d02445d42e49b7915b843aecfb7c7c.jpeg", + "image/en/fl/su/8w/d7e168beed5c4742be13cf998613d112.jpeg", + "image/25/2q/gh/fd/a122e79c556c40c1a61e4ad55219134f.jpeg", + "image/6h/2k/gs/5x/6fe68e9d41ae4cbda2a5e34e64ba4af9.jpeg", + "image/kz/2y/xv/a9/ad29c0e2fb7e43c282a9c1d4d70e1185.jpeg", + "image/1s/h3/hi/ht/1de64b74f1c34545ae8013553b12c6c1.jpeg", + "image/6n/os/7a/24/7e5031e18874496bb7481fbe27a85d34.jpeg", + "image/vo/g9/nj/l2/f288707eeb9041b8b471f0ac5f25fcdd.jpeg", + "image/of/cg/cb/zm/90280c2e0c2c4198b677c0e0430c3166.jpeg", + "image/ex/yb/so/l0/5011e1a5415c400783ab5d29d1d68129.jpeg", + "image/0b/4n/ct/hs/9d84d100aba340558f68e638e45c6203.jpeg", + "image/2b/bz/6w/g5/bbb4d6d1c0064491a5131a75a0d23546.jpeg", + "image/qf/n1/oq/m6/a29791ca041541c192205e1c82073398.jpeg", + "image/oa/u8/w3/f6/d149a55b4e894fbba332fa06889a091a.jpeg", + "image/ar/8x/9u/xw/e69e7be1a7174e6d96049b9b84888989.jpeg", + "image/g0/uu/64/mu/3bdcdd6c86a64b589bfcd0c9fac2d7da.jpeg", + "image/xa/a2/gs/dz/406b619f79684992837334ec2c21cffb.jpeg", + "image/eq/wo/4y/8e/749252631f5c40bda89d19150bfc661c.jpeg", + "image/ox/z3/3u/tr/227819454ccf4851b0905c2c74331394.jpeg", + "image/75/fj/s5/b4/afeeaada5460465982cc8539d7c1ff03.jpeg", + "image/jr/8v/e2/84/4e75fdfcf8404700b3f1e3b352ed45e0.jpeg", + "image/pd/1s/ld/mp/779ce7c27d71446188b40cdf11774df0.jpeg", + "image/nb/92/0s/gc/f158a9166e3648c8be5de39259556caa.jpeg", + "image/kh/bj/aa/zp/a5088e99962049a398f92033838c97ad.jpeg", +} diff --git a/app/api/userctrl/privilege.go b/app/api/userctrl/privilege.go new file mode 100644 index 0000000..9524102 --- /dev/null +++ b/app/api/userctrl/privilege.go @@ -0,0 +1,40 @@ +package userctrl + +import ( + "91porn-server/app/service/userser" + "91porn-server/common" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// ConsumePrivilege doc +// @Summary 消耗预售卡预付权益 +// @Description 消耗预售卡预付权益 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param q body userser.ConsumeReq false "请求参数" +// @Success 200 {string} string "操作成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/privilege/consume [post] +func ConsumePrivilege(ctx *gin.Context) { + // 获取用户当前配置 + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + + var args userser.ConsumeReq + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + err = userser.Consume(uid, args.PrivilegeType, args.Count) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, nil) + } + + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/app/api/userctrl/userctrl.go b/app/api/userctrl/userctrl.go new file mode 100644 index 0000000..343f909 --- /dev/null +++ b/app/api/userctrl/userctrl.go @@ -0,0 +1,1048 @@ +package userctrl + +import ( + "91porn-server/app/appg" + "91porn-server/app/middleware/authuser" + "91porn-server/app/service/commentser" + "91porn-server/app/service/filterser" + "91porn-server/app/service/notiser" + "91porn-server/app/service/proxyser" + "91porn-server/app/service/smsser" + "91porn-server/app/service/userser" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/truthutil" + v10 "91porn-server/common/v10" + "91porn-server/common/ysphone" + "91porn-server/common/ysqr" + "91porn-server/models/commod" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/faqmod" + "91porn-server/models/v/settingmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "91porn-server/web/service/annouser" + "strconv" + "strings" + + "github.com/gin-gonic/gin" +) + +var devIDNilValue = "00000000-0000-0000-0000-000000000000" +var maxNameRune = 12 + +// MobileBind doc +// @Summary 用户模块 - 绑定手机号 +// @Description 绑定手机号 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param mobile formData string true "电话号码" +// @Param code formData string true "验证码" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/mobileBind [post] +func MobileBind(ctx *gin.Context) { + var args struct { + Mobile string `json:"mobile" form:"mobile" binding:"required"` + Code string `json:"code" form:"code" binding:"required"` + PassWord string `json:"passWord"` + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + args.Mobile = strings.TrimSpace(args.Mobile) + if !v10.IsGlobalizationPhoneNumber(args.Mobile) { + common.ServeJSON(ctx, stderr.ErrMobileInvalid, nil) + return + } + args.Mobile = ysphone.FormatPhoneNumber(args.Mobile) + verifyCode := smsser.VerifySmsCode(args.Mobile, args.Code) + if verifyCode != stderr.Success { + common.ServeJSON(ctx, verifyCode, nil) + return + } + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + code := userser.MobileBind(uid, args.Mobile, args.PassWord) + common.ServeJSON(ctx, code, nil) +} + +// MobileLogin doc +// @Summary 用户模块 - 使用机号登陆 +// @Description 使用手机号登陆 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param mobile formData string true "电话号码" +// @Param code formData string true "验证码" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/mobileLoginOnly [post] +func MobileLogin(ctx *gin.Context) { + var args struct { + Mobile string `json:"mobile" form:"mobile" binding:"required"` + Code string `json:"code" form:"code" binding:"required"` + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + args.Mobile = ysphone.FormatPhoneNumber(strings.TrimSpace(args.Mobile)) + fakeMobilePrefixLen := len(constant.FakeMobilePrefix) + if len(args.Mobile) < fakeMobilePrefixLen { + common.ServeJSON(ctx, stderr.ErrMobileInvalid, nil) + return + } + //非马甲手机号 需要校验 + if args.Mobile[:fakeMobilePrefixLen] != constant.FakeMobilePrefix { + if !v10.IsGlobalizationPhoneNumber(args.Mobile) { + common.ServeJSON(ctx, stderr.ErrMobileInvalid, nil) + return + } + verifyCode := smsser.VerifySmsCode(args.Mobile, args.Code) + if verifyCode != stderr.Success { + common.ServeJSON(ctx, verifyCode, "") + return + } + } else { + //马甲账号验证1 验证码 + if args.Code != appg.Static.VerifyCodeForFake { + common.ServeJSON(ctx, stderr.ErrCaptchaInvalid, "") + return + } + } + ip := common.GetIP(ctx) + ua, _ := common.GetUA(ctx) + userInfo, code := userser.MobileLoginOnly(ip, args.Mobile, ua) + if code == stderr.Success { + userInfo.LoginType = usermod.MobileLogin + common.ServeJSON(ctx, code, gin.H{"token": userInfo.Token, "userInfo": userInfo}) + return + } + if code == stderr.ErrAccessForbid { + common.ServeJSON(ctx, code, gin.H{"uid": userInfo.UID, "reason": userInfo.LockReason}) + return + } + common.ServeJSON(ctx, code, nil) +} + +// Modify doc +// @Summary 用户模块 - 修改信息 +// @Description 用户修改信息 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param gender formData string false "性别" +// @Param name formData string false "用户名称" +// @Param age formData integer false "年龄" +// @Param mobile formData string false "手机号" +// @Param password formData integer false "密码" +// @Param portrait formData string false "头像" +// @Param background formData string false "背景图" +// @Param status formData integer false "状态" +// @Param role formData string false "角色" +// @Param summary formData string false "简介" +// @Param region formData string false "地区" +// @Param promotionCode formData string false "推广码" +// @Param qRCode formData string false "二维码" +// @Param birthday formData string false "生日" +// @Param promotionCode formData string false "生日" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/info [post] +func Modify(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + set := usermod.UserModifyReq{} + if err = ctx.ShouldBind(&set); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if set.AppLock != nil { + if len(*set.AppLock) != 4 { + common.ServeJSON(ctx, stderr.ErrParamError, "app_lock length invalid") + return + } + if _, err = strconv.ParseInt(*set.AppLock, 10, 64); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "app_lock is not number string") + return + } + } + if set.Portrait != nil || set.Name != nil || set.Background != nil || set.Summary != nil { + u, err := usermod.FindUserByUID(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, err.Error()) + return + } + if u == nil { + common.ServeJSON(ctx, stderr.UserIsNotExists, "") + return + } + //if set.Portrait != nil && u.CreatedAt.After(time.Now().Add(-7*24*time.Hour)) { + // common.ServeJSON(ctx, stderr.UserActiveNotEnough, "") + // return + //} + if !u.IsPaidVIP() { + common.ServeJSON(ctx, stderr.NoVipPrivilege, "") + return + } + if set.Name != nil { + if len([]rune(*set.Name)) > maxNameRune { + common.ServeJSON(ctx, stderr.ErrUserNameTooLong, nil) + return + } + newName, err := filterser.TextFilter(*set.Name) + if err == nil { + // 检查合法性 + if !truthutil.CheckIsValid(newName, 0) { + msg := map[string]interface{}{"msg": "昵称设置错误"} + common.ServeJsonWithExtra(ctx, stderr.Failure, nil, msg) + return + } + + set.Name = &newName + } + name := strings.TrimSpace(*set.Name) + set.Name = &name + } + if set.Summary != nil { + newSummary, err := filterser.TextFilter(*set.Summary) + if err == nil { + // 检查合法性 + if !truthutil.CheckIsValid(newSummary, 0) { + msg := map[string]interface{}{"msg": "个性签名设置错误"} + common.ServeJsonWithExtra(ctx, stderr.Failure, nil, msg) + return + } + + set.Summary = &newSummary + } + } + } + if _, err = usermod.UpdateSelf(uid, set); err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, err) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// ModifySetting doc +// @Summary 用户模块 - 修改配置信息 +// @Description 用户修改配置信息 +// @Tags user +// @Param privateLetters formData bool false "私信" +// @Param followWroks formData bool false "关注人的作品" +// @Param followInteraction formData bool false "关注人的随拍" +// @Param recommend formData bool false "作品推荐" +// @Param liveRemind formData bool false "开播提醒" +// @Param dynamicCover formData bool false "动态封面" +// @Param preUpload formData bool false "提前上传" +// @Param blackGround formData string false "背景图" +// @Param watch formData integer false "谁可以看" +// @Param reply formData integer false "谁可以回复" +// @Param watchMultiFlash formData bool false "允许好友在多闪内查看我的随拍" +// @Param privateAccount formData bool false "私密账号" +// @Param selfRecommended formData bool false "允许将我推荐给好友" +// @Param message formData integer false "谁可以发消息给我" +// @Param watchILike formData integer false "谁可以看我的喜欢列表" +// @Param blackGround formData string false "背景图" +// @Param watch formData integer false "谁可以看" +// @Param reply formData integer false "谁可以回复" +// @Param watchMultiFlash formData bool false "允许好友在多闪内查看我的随拍" +// @Param privateAccount formData bool false "私密账号" +// @Param selfRecommended formData bool false "允许将我推荐给好友" +// @Param message formData integer false "谁可以发消息给我" +// @Param watchILike formData integer false "谁可以看我的喜欢列表" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/setting [post] +func ModifySetting(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + set := settingmod.SettingSelector{} + if err = ctx.ShouldBind(&set); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = settingmod.UpdateSetting(uid, set); err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// LoginWithDevice doc +// @Summary 用户模块 - 用户登陆 +// @Description 登陆 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param devID formData string true "设备ID" +// @Param devType formData string true "设备型号" +// @Param sysType formData string true "系统类型" Enums(Android-10,IOS-13) +// @Param ver formData string true "app 应用版本号" +// @Param buildID formData string true "app 构建ID(包ID)" +// @Param channel formData string false "渠道" +// @Param promotionCode formData string false "推广码" +// @Param devToken formData string false "设备token" +// @Param cutInfos formData string false "剪切板信息" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/login [post] +func LoginWithDevice(ctx *gin.Context) { + token := ctx.Request.Header.Get("Authorization") + p := usermod.DevLoginP{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if p.DevID == devIDNilValue { + common.ServeJSON(ctx, stderr.ErrParamError, "devID is nil") + return + } + if !common.IsNormalDevId(p.DevID) { + common.ServeJSON(ctx, stderr.ErrParamError, "devID is invalid") + return + } + ua, _ := common.GetUA(ctx) + ip := common.GetIP(ctx) + userInfo, code, loginType, err := userser.DeviceLogin(ip, p, token, ua) + if err != nil { + log.Warn("LoginWithDevice", log.E(err)) + if code != 0 { + if code == stderr.ErrAccessForbid { + common.ServeJSON(ctx, code, gin.H{"uid": userInfo.UID, "reason": userInfo.LockReason}) + return + } + common.ServeJSON(ctx, code, err.Error()) + return + } + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, err.Error()) + return + } + userInfo.LoginType = loginType + common.ServeJSON(ctx, stderr.Success, userInfo) +} + +// MyRights doc +// @Summary 用户模块 - 用户权益查询 +// @Description 用户权益,包括观影券 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} usermod.UserRights +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/rights [get] +func MyRights(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + userRights, err := userser.MyRights(uid) + if err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, "") + return + } + log.Info("userRights info", log.Any("res", userRights)) + common.ServeJSON(c, stderr.Success, userRights) +} + +// Info doc +// @Summary 用户模块 - 基本信息查询 +// @Description 用户基本信息查询 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer false "用户id(如果是查看自己页面则不传)" +// @Success 200 {object} usermod.UserRes "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/info [get] +func Info(ctx *gin.Context) { + uid, _ := common.GetUID(ctx) + var user struct { + UID uint64 `form:"uid" json:"uid"` + } + if err := ctx.ShouldBind(&user); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if user.UID != 0 { + info, err := userser.OtherInfo(uid, user.UID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, "") + return + } + common.ServeJSON(ctx, stderr.Success, info) + return + } + info, err := userser.Info(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, "") + return + } + log.Debug("info", log.Any("info", info)) + common.ServeJSON(ctx, stderr.Success, info) +} + +// ISetting doc +// @Summary 用户模块 - 配置信息查询 +// @Description 用户设置信息查询 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/setting [get] +func ISetting(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + setting, err := settingmod.FindSetting(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err) + return + } + common.ServeJSON(ctx, stderr.Success, setting) +} + +// Collection doc +// @Summary 我的模块 - 我的帖子 +// @Description 作品,传uid是获取所传uid的作品,不传是获取自己的作品 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer false "用户id" +// @Param pageSize formData int true "每页显示条数" +// @Param pageNumber formData int true "当前第几页" +// @Param uid formData uint64 false "用户id(查询其他用户使用,非必传)" +// @Param type formData string false "hot 热度值排序;new 最新视频" +// @Success 200 {object} userser.VideoListResp 操作成功 +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/collection [get] +func Collection(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + pgR := PageRule{} + if err = ctx.ShouldBind(&pgR); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + var collection []*vidmod.VideoInfoResp + var hasNext bool + var total int64 + if pgR.UID == 0 { + collection, total, hasNext, err = userser.IVList(uid, pgR.Type, pgR.PageSize, pgR.PageNumber, pgR.PlayTimeType) + } else { + collection, total, hasNext, err = userser.HisVList(uid, pgR.UID, pgR.PageSize, pgR.PageNumber, pgR.Type, pgR.PlayTimeType) + } + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, err) + return + } + resp := userser.VideoListResp{ + List: collection, + Total: total, + HasNext: hasNext, + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// PublishNum doc +// @Summary 我的模块 - 用户发布的帖子数量 +// @Description 作品-用户发布的帖子数量 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param q body userser.PublishNumReq true "参数" +// @Success 200 {object} userser.PublishNumRep 操作成功 +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/publish/num [get] +func PublishNum(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + req := userser.PublishNumReq{} + rep, err := req.GetUserPublishNum(uid) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, rep) +} + +// PublishList doc +// @Summary 我的模块 - 用户发布的帖子列表 +// @Description 作品-用户发布的帖子列表 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param q body userser.PublishWorkReq true "参数" +// @Success 200 {object} userser.PublishWorkRep 操作成功 +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/publish [get] +func PublishList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + req := userser.PublishWorkReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + rep, err := req.GetUserPublishWorkList(uid) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, rep) +} + +// PublishWorkDelete doc +// @Summary 我的模块 - 删除我发布但未通过审核的作品 +// @Description 作品-删除我发布但未通过审核的作品 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param q body userser.PublishWorkDeleteReq true "参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/publish/delete [post] +func PublishWorkDelete(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + req := userser.PublishWorkDeleteReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + err = req.UserPublishWorkDelete(uid) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// BuyVidList doc +// @Summary 我的模块 - 购买的视频 +// @Description 我购买的视频 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer false "用户id" +// @Param pageSize formData int true "每页显示条数" +// @Param pageNumber formData int true "当前第几页" +// @Param newsType formData string true "购买类型 SP,SHORT,COVER,PIC,SEED_LINK" +// @Success 200 {object} userser.VideoListResp 操作成功 +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/buyVid [get] +func BuyVidList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var p struct { + UID uint64 `form:"uid" json:"uid"` + PageNumber int64 `form:"pageNumber" json:"pageNumber" binding:"required"` + PageSize int64 `form:"pageSize" json:"pageSize" binding:"required"` + NewsType string `form:"newsType" json:"newsType"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + vList, total, hasNext, err := userser.BuyVidList(uid, p.UID, p.NewsType, p.PageSize, p.PageNumber) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, err) + return + } + resp := userser.VideoListResp{ + List: vList, + Total: total, + HasNext: hasNext, + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// Like doc +// @Summary 我的模块 - 我的喜欢 +// @Description 喜欢,传uid是获取所传uid的喜欢,不传是获取自己的喜欢 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param q query userser.GetUserLikeReq true "每页显示条数" +// @Success 200 {object} userser.ILikeListRep 操作成功 +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/like [get] +func Like(ctx *gin.Context) { + + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + req := userser.GetUserLikeReq{} + err = ctx.ShouldBind(&req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + vList, hasNext, err := userser.UserLikesVidList(uid, &req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, nil) + return + } + + resp := userser.ILikeListRep{ + List: vList, + HasNext: hasNext, + } + common.ServeJSON(ctx, stderr.Success, resp) + return +} + +// Comment doc +// @Summary 我的模块 - 我的评论 +// @Description 我的评论 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param pageSize formData int true "每页显示条数" +// @Param pageNumber formData int true "当前第几页" +// @Success 200 {object} userser.VideoListResp 操作成功 +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/comment [get] +func Comment(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + pgR := PageRule{} + err = ctx.ShouldBind(&pgR) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + list, hasNext, err := commentser.GetMyCmtList(uid, pgR.Page) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + common.ServeJSON(ctx, stderr.Success, &cmtmod.MyCmtResp{ + List: list, + HasNext: hasNext, + }) +} + +// FaqList doc +// @Summary 用户常见问题 +// @Description 用户常见问题列表 +// @Tags 用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param category query string false "问题类型" +// @Param pageNumber query integer true "当前页" +// @Param pageSize query integer true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "date": {}}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/faq/list [get] +func FaqList(c *gin.Context) { + var args struct { + Category string `form:"cate" json:"cate" binding:"required"` + commod.Page + } + if err := c.ShouldBind(&args); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user FaqList arg error "+err.Error()) + return + } + stdQuery := commod.StdQuery{Page: &commod.PageBy{Num: args.PageNumber, Size: args.PageSize}} + page, err := userser.FaqList(args.Category, stdQuery) + if err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, "web user PlayRecord error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} + +// AutoLocate doc +// @Summary 自动定位 +// @Description 根据IP自动定位 +// @Tags 用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功", "date": {}}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mime/autoLocate [get] +func AutoLocate(c *gin.Context) { + ip := common.GetIP(c) + city, province := common.GetLocationByIP(ip) + common.ServeJSON(c, stderr.Success, gin.H{"city": city, "province": province}) +} + +// FaqCateList doc +// @Summary 用户常见问题 +// @Description 用户常见问题分类列表 +// @Tags 用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功", "date": {}}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/faq/cate/list [get] +func FaqCateList(c *gin.Context) { + page, err := faqmod.FaqCateList() + if err != nil { + common.ServeJSON(c, stderr.ErrNetWorkBusy, "web user FaqCateList error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} + +// QrCode doc +// @Summary 二维码 +// @Description 二维码 +// @Tags 用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param Authorization header string true "user token" +// @Success 200 {string} json "{"msg": "操作成功", "content": ""}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/certificate/qr [get] +func QrCode(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + user, _ := usermod.FindUserByUID(uid) + if user == nil { + common.ServeJSON(ctx, stderr.AdminIsNotExist, "") + return + } + t := ysqr.Login + if actType, b := ctx.Get("actType"); b { + at, ok := actType.(ysqr.ActType) + if ok { + t = at + } + } + content := ysqr.Content{ + UID: user.UID, + T: t, + LoginClaims: ysqr.LoginClaims{ + DevID: user.DevID, + }, + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "content": content.String(authuser.GetTokenSecret()), + }) +} + +// Invite doc +// @Summary 邀请 +// @Description 邀请绑定 +// @Tags 用户 +// @Accept mpfd,json +// @Produce json,html +// @Param promotionCode formData string true "邀请码" +// @Success 200 {string} json "{"msg": "操作成功", "content": ""}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/inviteBind [post] +func Invite(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var p struct { + PromCode string `json:"promotionCode" bson:"promotionCode"` //推广码 + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code := proxyser.InviteBind(uid, p.PromCode, true) + common.ServeJSON(ctx, code, code.Msg()) +} + +// Inviter doc +// @Summary 获取邀请者 +// @Description 邀请绑定 +// @Tags 用户 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功", "content": ""}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/inviter [post] +func Inviter(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + inviter, err := proxyser.GetInv(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, " user Get Inviter fail error: "+err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"inviter": inviter}) +} + +// Announce doc +// @Summary 会员中心跑马灯 +// @Description 会员中心跑马灯 +// @Tags 用户 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功", "content": ""}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/announce [get] +func Announce(ctx *gin.Context) { + code, data := annouser.GetAnnounceListApi() + common.ServeJSON(ctx, code, data) +} + +// AnnounceList doc +// @Summary 跑马灯列表 +// @Description 跑马灯列表 +// @Tags 用户 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} []annoumod.Announce "success" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/announce/list [get] +func AnnounceList(ctx *gin.Context) { + code, data := annouser.GetAnnounceList() + common.ServeJSON(ctx, code, data) +} + +// LoginWithH5 doc +// @Summary 用户模块 - 注册用户接口 +// @Description 手机注册登陆 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param request usermod.LoginWithH5 body object true "request" +// @Success 200 {object} usermod.UserRes "success" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/login/h5 [post] +func LoginWithH5(ctx *gin.Context) { + p := usermod.LoginWithH5{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + // 仅允许 h5/ios 端登录注册,其余 sysType 一律拒绝 + if p.SysType != "h5" && p.SysType != "ios" { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + // devID 以 web 开头的为攻击流量,直接拒绝(忽略大小写) + if strings.HasPrefix(strings.ToLower(p.DevID), "web") { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + ip := common.GetIP(ctx) + ua, err := common.GetUA(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + log.Info("login info", log.Any("ip", ip), log.Any("p", p), log.Any("ua", ua)) + user, code, err := userser.H5Login(ctx, ip, p, ua) + if err != nil || code != stderr.Success { + log.ErrorX(ctx, "login h5 failed", log.Any("req", p), log.Any("ip", ip), log.E(err)) + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, code, user) +} + +// LoginByQRCode doc +// @Summary 用户模块 - 登录 +// @Description H5用户通过二维码登录 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param request body usermod.LoginH5ByQRCode true "request" +// @Success 200 {object} usermod.UserRes "success" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/login/h5/qr [post] +func LoginByQRCode(ctx *gin.Context) { + req := usermod.LoginH5ByQRCode{} + if err := ctx.ShouldBind(&req); err != nil { + log.Error("bind request parameter failed") + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + user, err := userser.H5LoginByQRCode(ctx, req.Content, common.GetIP(ctx)) + if err != stderr.Success { + common.ServeJSON(ctx, err, nil) + return + } + common.ServeJSON(ctx, stderr.Success, user) +} + +// MobileVerify doc +// @Summary 手机校验 +// @Description 手机校验 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param mobile formData string true "电话号码" +// @Param code formData string true "验证码" +// @Success 200 {string} json "{"msg": "操作成功","data":"加密串"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/resetpassword/mobileverify [post] +func MobileVerify(ctx *gin.Context) { + var args struct { + Mobile string `json:"mobile" form:"mobile"` + Email string `json:"email" form:"email"` + Code string `json:"code" form:"code" binding:"required"` + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if args.Email == "" && args.Mobile == "" { + common.ServeJSON(ctx, stderr.ErrInvalidRequest, nil) + return + } + if code := notiser.VerifyCaptchaCode(ctx, args.Mobile, args.Email, args.Code); code != stderr.Success { + common.ServeJSON(ctx, stderr.ErrCaptchaInvalid, nil) + return + } + code, str := userser.GenResetWorld(ctx, args.Mobile, args.Email) + common.ServeJSON(ctx, code, str) +} + +// ResetPassWord doc +// @Summary 找回密码 +// @Description 找回密码。邮箱、手机号 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param token body string true "加密字符串" +// @Param string body string true "密码" +// @Success 200 {string} string "操作成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/resetpassword [post] +func ResetPassWord(ctx *gin.Context) { + var args struct { + Token string `json:"token" form:"token" binding:"required"` + Password string `json:"passWord" form:"passWord" binding:"required"` + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code := userser.ResetPassword(ctx, args.Token, args.Password) + common.ServeJSON(ctx, code, nil) +} + +// Register doc +// @Summary 注册 +// @Description 注册 支持手机号、邮箱、账号 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param request body userser.RegisterRequestH5 true "request" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/register [post] +func Register(ctx *gin.Context) { + ua, err := common.GetUA(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + var args = userser.RegisterRequestH5{} + if err = ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if args.Email == "" && args.Mobile == "" && args.Account == "" { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + u, code := userser.RegisterH5(ctx, uid, ua.SysType, args) + common.ServeJSON(ctx, code, u) +} + +// ExtractQRCode doc +// @Summary 解析二维码 +// @Description 解析二维码 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} string "success" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/qrcode/extract [post] +func ExtractQRCode(ctx *gin.Context) { + file, err := ctx.FormFile("qrcode") + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + f, err := file.Open() + if err != nil { + common.ServeJSON(ctx, stderr.ErrInterServerError, err) + return + } + content, err := common.DecodeQRCode(f) + f.Close() + if err != nil { + common.ServeJSON(ctx, stderr.ErrInterServerError, err) + return + } + common.ServeJSON(ctx, stderr.Success, content) +} + +// Portrait 头像列表 +// +// @Summary 头像列表 +// @Description 头像列表 +// @Tags user +// @Accept mpfd,json +// @Produce json,html +// @Param Authorization header string true "user token" +// @Success 200 {string} json "{"msg": "操作成功", "content": ""}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/portrait [get] +func Portrait(ctx *gin.Context) { + common.ServeJSON(ctx, stderr.Success, gin.H{"data": portrait}) +} diff --git a/app/api/userctrl/userwatchrecord.go b/app/api/userctrl/userwatchrecord.go new file mode 100644 index 0000000..e837589 --- /dev/null +++ b/app/api/userctrl/userwatchrecord.go @@ -0,0 +1,40 @@ +package userctrl + +import ( + "91porn-server/app/service/userwatchrecordserver" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/userwatchrecordmod" + + "github.com/gin-gonic/gin" +) + +// WatchRecord doc +// @Summary 用户观看记录列表 +// @Description 观看记录列表 +// @Tags user +// @Accept json +// @Produce json +// @Param pageNumber query int true "第几页" +// @Param pageSize query int true "每页数量" +// @Success 200 object userwatchrecordmod.ListResponse "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/watch_record/list [get] +func WatchRecord(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + var req userwatchrecordmod.ListRequest + if err = c.ShouldBindQuery(&req); err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + data, code := userwatchrecordserver.List(uid, &req) + if code != stderr.Success { + common.ServeJSON(c, code, err) + return + } + common.ServeJSON(c, code, data) +} diff --git a/app/api/versionctrl/versionctrl.go b/app/api/versionctrl/versionctrl.go new file mode 100644 index 0000000..26f6d84 --- /dev/null +++ b/app/api/versionctrl/versionctrl.go @@ -0,0 +1,54 @@ +/* + * @Description: In User Settings Edit + * @Author: your name + * @Date: 2019-08-28 19:44:56 + * @LastEditTime: 2019-08-29 15:24:34 + * @LastEditors: Please set LastEditors + */ +package versionctrl + +import ( + "91porn-server/app/service/versionser" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/versionmod" + + "github.com/gin-gonic/gin" +) + +// GetVersion doc +// @Summary 平台信息 获取最新版本号 +// @Description 平台信息 获取最新版本号 +// @Tags 平台信息 +// @Accept mpfd,json +// @Produce json,html +// @Param platform query string false "选择平台:Android/Ios" Enums(android,ios) +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/ver [get] +func GetVersion(ctx *gin.Context) { + p := ctx.Query("platform") + ua, err := common.GetUA(ctx) + var ver versionmod.VersionBody + var returnVerList []*versionmod.VersionBody + if p != "" && err == nil && ua.BuildID != "" { + returnVerList = versionser.CheckVersionBaseOnBuildId(ua.Ver, p, ua.BuildID) + if len(returnVerList) > 0 { + ver = *returnVerList[0] + common.ServeJSON(ctx, stderr.Success, ver) + return + } + } + version, err := versionmod.FindVersion(p) + if err != nil { + common.ServeJSON(ctx, stderr.Success, nil) + return + } + ver.Code = version.Code + ver.URL = version.URL + ver.VersionName = version.VersionName + ver.Platform = version.Platform + ver.Description = version.Description + ver.ForcedUpdate = version.ForcedUpdate + common.ServeJSON(ctx, stderr.Success, ver) +} diff --git a/app/api/vidctrl/newsctrl.go b/app/api/vidctrl/newsctrl.go new file mode 100644 index 0000000..9106f03 --- /dev/null +++ b/app/api/vidctrl/newsctrl.go @@ -0,0 +1,143 @@ +package vidctrl + +import ( + "fmt" + "time" + + "91porn-server/app/service/vidser" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/vidmod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetNewsList doc +// @Summary 获取帖子 +// @Description 获取帖子 +// @Tags +// @Accept mpfd,json +// @Produce json,html +// @Param type formData integer true "类型" +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {object} vidmod.NewsListResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/vid/news/list [get] +func GetNewsList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + req := vidmod.NewsListReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + //如果第一页,追加置顶、力荐、加精 + head := []*vidmod.VideoModel{} + if req.PageNumber == vidmod.TypeHotest && !(req.Type == vidmod.TypeNewst && req.SubType == 1) { + isMadou := false + if req.Type == vidmod.TypePay && req.SubType == vidmod.MadouUp { + isMadou = true + } + head, err = vidser.GetHeadNews(req.Type, primitive.NilObjectID, isMadou) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + } + var code stderr.Code + var data interface{} + switch req.Type { + case vidmod.TypeNewst: //最新 + if req.ReqTime != "" { + reqTime, err := time.Parse(time.RFC3339, req.ReqTime) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, fmt.Sprintf("reqTime: %s", req.ReqTime)) + return + } + code, data = vidser.GetNewestNewsList(uid, req.PageNumber, req.PageSize, head, reqTime) + } else { //当没有传reqTime参数时, 返回默认最新 + code, data = vidser.GetNewestNewsList_old(uid, req.PageNumber, req.PageSize, head, req.SubType) + } + case vidmod.TypeHotest: //最热 + code, data = vidser.GetHotestNewsList(uid, req.SubType, req.PageNumber, req.PageSize) + case vidmod.TypeSameCity: //同城 + ip := common.GetIP(ctx) + code, data = vidser.GetLocationList(uid, ip, req.City, req.PageNumber, req.PageSize, vidmod.COVER) + case vidmod.TypePay: //金币专区 + if req.Version == vidmod.Version { + /** TODO: + 返回model为二维数组 + */ + code, data = vidser.GetOriginalList(uid, req.SubType, req.PageNumber, req.PageSize) + } else { + code, data = vidser.GetNewsCoinsList(uid, req.SubType, req.PageNumber, req.PageSize, head) + } + case vidmod.TypeVip: //会员专区 + code, data = vidser.GetVIPVideo(uid, req.PageNumber, req.PageSize) + } + common.ServeJSON(ctx, code, data) +} + +// DoUnlike doc +// @Summary 不感兴趣 +// @Description 不感兴趣 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param type formData integer true "类型" +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/vid/news/unlike [post] +func DoUnlike(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + req := vidmod.UnlikeReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := vidser.DoUnlikeVideo(uid, req.VideoID) + common.ServeJSON(ctx, code, data) +} + +// List doc +// @Summary 获取帖子 +// @Description 获取帖子 +// @Tags +// @Accept mpfd,json +// @Produce json,html +// @Param type query integer true "类型" +// @Param model query integer false "模块" +// @Param time query time.Time false "请求页面时间" +// @Param tag query string false "标签ID" +// @Param paymentType query integer false "付费类型" +// @Param city query string false "城市" +// @Param pageNumber query integer true "查询页码" +// @Param pageSize query integer true "页码大小" +// @Success 200 {object} vidmod.NewsListResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/vid/list [get] +func List(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + var req vidmod.AppListReq + if err = c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + data, code := vidser.List(uid, &req) + common.ServeJSON(c, code, data) +} diff --git a/app/api/vidctrl/vidctrl.go b/app/api/vidctrl/vidctrl.go new file mode 100644 index 0000000..8796b6c --- /dev/null +++ b/app/api/vidctrl/vidctrl.go @@ -0,0 +1,1092 @@ +package vidctrl + +import ( + "91porn-server/models/v/discount_area_mod" + "errors" + "fmt" + "strings" + "time" + + "91porn-server/app/service/m3u8ticket" + "91porn-server/app/service/vidser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/middleware/requestid" + "91porn-server/models/commod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetLocationList doc +// @Summary 获取同城视频 +// @Description 获取同城视频 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param ip formData string true "用户ip地址" +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/vid/location/list [get] +func GetLocationList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + req := vidmod.LocVideoReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if req.PageSize < 1 { + req.PageSize = 10 + } + code, data := vidser.GetLocationList(uid, ctx.ClientIP(), req.City, req.PageNumber, req.PageSize, vidmod.SP) + common.ServeJSON(ctx, code, data) +} + +// GetLocationHot doc +// @Summary 获取热门城市 +// @Description 获取热门城市 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/vid/location/hot [get] +func GetLocationHot(ctx *gin.Context) { + code, data := vidser.GetLocationHot() + common.ServeJSON(ctx, code, data) +} + +// GetVidInfo doc +// @Summary 获取视频详情 +// @Description 获取视频详情 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param videoID formData string true "视频id" +// @Param searchAccessToken query string false "搜索结果返回的短时效访问凭证" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/vid/info [get] +func GetVidInfo(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + param := vidmod.VideoReq{} + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if len(param.VideoID) == 0 { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := vidser.GetInfoWithSearchAccess(uid, param.VideoID, param.SearchAccessToken) + // 视频详情:对 m3u8 播放地址签发 H5 防盗链票据 + m3u8ticket.Sign(ctx, data) + common.ServeJSON(ctx, code, data) +} + +// Submit doc +// @Summary 提交视频的基本信息 +// @Description 视频基本信息提交 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer false "用户ID" +// @Param title formData string false "视频标题" +// @Param content formData string false "视频内容" +// @Param newsType formData string false "上传类型 COVER-图集 SP-视频" +// @Param tags formData array true "视频标签数组" +// @Param playTime formData integer false "视频时长" +// @Param cover formData string false "封面图" +// @Param coverThumb formData string false "小图" +// @Param seriesCover formData array false "图集图片地址" +// @Param via formData string false "视频来源" +// @Param coins formData integer false "观看金币" +// @Param size formData integer false "文件大小" +// @Param mimeType formData string false "影片类型" +// @Param actor formData string false "演员名字" +// @Param sourceURL formData string false "资源url 为SP时 必传" +// @Param sourceID formData string false "上传视频成功后 返回的ID 为SP时必传" +// @Param filename formData string false "文件名" +// @Param resolution formData string false "分辨率" +// @Param ratio formData number false "视频比列" +// @Param md5 formData string false "文件摘要" +// @Param freeTime formData integer false "免费观影时长" +// @Param location formData string false "地理位置信息" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /vid/submit [post] +func Submit(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + param := vidmod.SubmitReq{} + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + //判断该用户是否被禁止上传视频 + user, err := usermod.FindUserByUID(uid) + if err != nil || (user != nil && user.ForbidUpload) { + common.ServeJSON(ctx, stderr.ForbidUploadVideo, err) + return + } + if param.NewsType == vidmod.COVER || param.NewsType == vidmod.PIC { + if user.CoverUploadCount > 3 && !user.IsPaidVIP() { + common.ServeJSON(ctx, stderr.NoVipNoUpload, err) + return + } + } else if param.NewsType == vidmod.SP || param.NewsType == vidmod.SHORT { + if user.VidUploadCount >= 3 && !user.IsPaidVIP() { + common.ServeJSON(ctx, stderr.NoVipNoUpload, err) + return + } + } + //默认newsType + if len(param.NewsType) == 0 { + param.NewsType = vidmod.SP + } + //图集的第一张放到封面中 + if (param.NewsType == vidmod.COVER || param.NewsType == vidmod.PIC) && len(param.SeriesCover) != 0 { + param.Cover = param.SeriesCover[0] + param.CoverThumb = param.SeriesCover[0] + } + //免费时长设置,收费视频 + if param.Coins > 0 { + param.FreeTime = vidmod.GetFreeTime(param.PlayTime) + } + ip := common.GetIP(ctx) + code, vid := vidser.SubmitBase(uid, param, ip) + if code != stderr.Success { + _, _ = vidser.DeleteManyByTTL(uid, vid) + common.ServeJSON(ctx, code, vid) + return + } + n := int64(1) + if param.NewsType == vidmod.COVER || param.NewsType == vidmod.PIC { + // code = vidser.SendCoverInfo2FS(param) + // if code != stderr.Success { + // vidser.DeleteManyByTTL(uid, vid) + // } + _ = usermod.IncUploadCount(uid, usermod.UploadCountInc{ + TotalWorks: &n, + CoverUploadCount: &n, + }) + common.ServeJSON(ctx, code, vid) + return + } + if param.NewsType == vidmod.SP || param.NewsType == vidmod.SHORT { + _ = usermod.IncUploadCount(uid, usermod.UploadCountInc{ + TotalWorks: &n, + VidUploadCount: &n, + }) + } + param.SourceID = vid.Hex() + code = vidser.SendBaseInfo2FS(param) + if code != stderr.Success { + _, _ = vidser.DeleteManyByTTL(uid, vid) + } + common.ServeJSON(ctx, code, vid) +} + +// PlayRecord doc +// @Summary 今日热门视频列表 +// @Description 今日热门视频列表 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param videoID formData string true "视频id" +// @Param beginTime formData string true "开始播放的时间" +// @Param endTime formData string true "退出或者跳转到下个视频的时间" +// @Param playWay formData integer true "通过什么途径获取视频的 0,免费 1付费 2.试看 " +// @Param via formData integer true "视频来源,1.推荐 2.附近 3专题 4搜索 5分享 6热搜 7收藏 8热搜" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/vid/play [post] +func PlayRecord(ctx *gin.Context) { + uid := common.TryGetUID(ctx) + param := vidmod.PlayReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := vidser.PlayRecord(uid, param) + common.ServeJSON(ctx, code, data) +} + +// HotList doc +// @Summary 今日最热视频列表 +// @Description 今日最热视频列表 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query int true "当前页" +// @Param pageSize query int true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /vid/hot/list [get] +func HotList(c *gin.Context) { + var arg struct { + commod.Page + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "vid HotList arg error "+err.Error()) + return + } + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrAccessForbid, err) + return + } + skip := int64((arg.PageNumber - 1) * arg.PageSize) + limit := int64(arg.PageSize) + list, hasNext, err := vidser.GetHotList(uid, skip, limit) + if err != nil { + common.ServeJSON(c, stderr.Failure, err) + return + } + common.ServeJSON(c, stderr.Success, gin.H{ + "list": list, + "hasNext": hasNext, + "updatedAt": time.Now().Format("2006-01-02 15:04"), + }) +} + +// HotKWList doc +// @Summary 热点视频列表 +// @Description 热点视频列表 通过搜索量排名高的多个关键字,查询视屏列表 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query int true "当前页" +// @Param pageSize query int true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /vid/hotkw/list [get] +func HotKWList(c *gin.Context) { + var arg struct { + commod.Page + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "vid HotKWList arg error "+err.Error()) + return + } + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrAccessForbid, err) + return + } + //热搜视屏代替 + skip := int64((arg.PageNumber - 1) * arg.PageSize) + limit := int64(arg.PageSize) + list, hasNext, err := vidser.GetHotKWList(uid, skip, limit) + if err != nil { + common.ServeJSON(c, stderr.Failure, err) + return + } + common.ServeJSON(c, stderr.Success, gin.H{ + "list": list, + "hasNext": hasNext, + }) +} + +// GetLocation doc +// @Summary 根据id获取location信息 +// @Description 根据id获取location信息 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "地址信息id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/vid/location/info [get] +func GetLocation(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + param := vidmod.LocReq{} + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := vidser.GetLocation(uid, param.ID) + common.ServeJSON(ctx, code, data) +} + +// GetWatchCount doc +// @Summary 根据id获取用户观看次数 +// @Description 根据id获取用户观看次数 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param vid formData string false "视频ID" +// @Success 200 {object} vidmod.WatchCountResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vid/user/count [get] +func GetWatchCount(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + var p struct { + Vid string `form:"vid"` + First string `form:"first"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if p.Vid == "" { + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "FindUserByUID error") + return + } + watchCount := vidser.FreshCount(u.UID) + watch := watchCount > 0 + totalWatchCount := vidser.TotalFreeWatchCount() + log.Info("GetWatchCount", log.Any("uid", uid), log.Any("isCan", watch), log.Any("watchCount", watchCount), + log.Any("totalWatchCount", totalWatchCount)) + common.ServeJSON(ctx, stderr.Success, vidmod.WatchCountResp{ + IsCan: watch, + WatchCount: watchCount, + TotalWatchCount: totalWatchCount, + }) + return + } + oid, err := primitive.ObjectIDFromHex(p.Vid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "get watch count arg error "+err.Error()) + return + } + watch, watchCount := vidser.GetWatchCount(uid, oid, p.First) + log.Info("GetWatchCount", log.Any("uid", uid), log.Any("vid", p.Vid), log.Any("isCan", watch)) + common.ServeJSON(ctx, stderr.Success, vidmod.WatchCountResp{ + IsCan: watch, + WatchCount: watchCount, + TotalWatchCount: vidser.TotalFreeWatchCount(), + }) +} + +// ConsumeWatchCount doc +// @Summary 消费用户免费观看视频次数 +// @Description 同一用户同一视频每天最多消费一次 +// @Tags vid +// @Accept json +// @Produce json +// @Param request body vidmod.WatchConsumeReq true "视频ID" +// @Success 200 {object} vidmod.WatchConsumeResp +// @Failure 400 {string} json +// @Router /api/app/vid/play/consume [post] +func ConsumeWatchCount(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + var req vidmod.WatchConsumeReq + if err = ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + videoID := req.GetVideoID() + oid, err := primitive.ObjectIDFromHex(videoID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + resp, err := vidser.ConsumeFreeWatch(uid, oid) + if err != nil { + log.Error("ConsumeWatchCount failed", log.Any("uid", uid), log.Any("videoID", videoID), log.E(err)) + common.ServeJSON(ctx, stderr.ErrDbUpdateError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// DeleteManyByTTL doc +// @Summary 删除自己的视频 +// @Description 删除自己的视频 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "地址信息id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/vid/remove/ [delete] +func RemoveMyVideos(ctx *gin.Context) { + //20220222接口屏蔽start + common.ServeJSON(ctx, stderr.ErrParamError, nil) +} + +// ModuleVideoList doc +// @Summary 获取模块下各个专题视频 +// @Description 获取模块下各个专题所有视频 +// @Tags 首页模块 +// @Accept mpfd,json +// @Produce json,html +// @Param subModuleID path string true "亚模块" +// @Param tagid query string true "标签id" +// @Param pageNumber query integer true "模块下视频页码" +// @Param pageSize query integer true "模式下视频数量" +// @Param moduleSort query integer true " 视频排序 1、最新,2、最热/最多喜欢,3、最多播放,4、十分钟以上视频, 5、精华/精选,6、视频 7-最多收藏 8、解锁次数" +// @Success 200 {object} vidser.VideoUnderSubModuleResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vid/module/:subModuleID [get] +func ModuleVideoList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil && !errors.Is(err, common.ErrUserNotExist) { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + subModuleIDStr := ctx.Param("subModuleID") + if subModuleIDStr == "" { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + subModuleID, err := primitive.ObjectIDFromHex(subModuleIDStr) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + req := vidser.VideoUnderSubModuleReq{} + if err := ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + res, err := vidser.GetVideosUnderSubModuleNew(subModuleID, uid, req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + // 首页模块下各专题视频:对 m3u8 播放地址签发 H5 防盗链票据 + m3u8ticket.Sign(ctx, res) + common.ServeJSON(ctx, stderr.Success, res) +} + +// RefreshModuleVideos 从亚模块热门候选池中随机返回视频。 +func RefreshModuleVideos(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil && !errors.Is(err, common.ErrUserNotExist) { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + subModuleID, err := primitive.ObjectIDFromHex(ctx.Param("subModuleID")) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + req := vidser.RefreshModuleVideosReq{} + if err = ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + req.RefreshToken = strings.TrimSpace(req.RefreshToken) + if req.RefreshToken == "" { + common.ServeJSON(ctx, stderr.ErrParamError, "refreshToken is required") + return + } + res, err := vidser.RefreshModuleVideos(uid, subModuleID, req) + if err != nil { + if errors.Is(err, vidser.ErrRandomRefreshUnsupported) { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + // 随机刷新列表:对 sourceURL、h265Url 签发播放防盗链票据。 + m3u8ticket.Sign(ctx, res.AllVideoInfo) + common.ServeJSON(ctx, stderr.Success, res) +} + +// SectionVideoList doc +// @Summary 获取一个专题下的视频列表 +// @Description 获取一个专题下的视频列表,如“撩吧--推荐”模块下“XX”专题下的视频列表 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param sectionID path string true "专题ID" +// @Param pageSize query integer true "页码大小" +// @Param pageNumber query integer true "专题页码" +// @Param sortType query string true "watch:最多播放,new:最新,collect: 收藏" +// @Success 200 {object} vidser.VideoUnderSectionResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vid/section/:sectionID [get] +func SectionVideoList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil && !errors.Is(err, common.ErrUserNotExist) { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + req := vidser.VideoUnderSectionReq{} + if err = ctx.ShouldBindUri(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + var sectionID primitive.ObjectID + //var bloggerID int + sectionID, err = primitive.ObjectIDFromHex(req.SectionID) + //bloggerID, err2 := strconv.Atoi(req.SectionID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + page := commod.Page{} + if err = ctx.ShouldBind(&page); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + resp := vidser.VideoUnderSectionResp{} + if !sectionID.IsZero() { + resp, err = vidser.GetSectionVideos(uid, sectionID, page, req.SortType, req.PlayTimeType) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + } + //if bloggerID != 0 { + // resp, err = vidser.GetBloggerVideos(uid, uint64(bloggerID), page) + // if err != nil { + // serveVid(ctx, stderr.ErrDbQueryError, nil) + // return + // } + //} + common.Go(func() { + if err := vidser.IncSectionHits(ctx, sectionID); err != nil { + log.ErrorX(ctx, "vidser.IncSectionHits failed", log.Any("sectionID", sectionID), log.E(err)) + } + }) + common.ServeJSON(ctx, stderr.Success, resp) +} + +// AllVideosOfModule doc +// @Summary 获取模块下所有视频 +// @Description 获取模块下所有视频 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param subModuleID path string true "亚模块" +// @Param pageSize query integer true "页码大小,一个专题下视频数量" +// @Param sectionSize query integer true "专题数量" +// @Param sectionPage query integer true "专题页码" +// @Success 200 {object} vidser.VideoUnderSubModuleResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vid/module/moduleVideo [get] +func AllVideosOfModule(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil && !errors.Is(err, common.ErrUserNotExist) { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + req := vidser.AllVideosOfModuleReq{} + if err := ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + subModuleID, err := primitive.ObjectIDFromHex(req.SubModuleID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + res, err := vidser.AllVideosOfModule(subModuleID, uid, req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, res) +} + +func Sections(ctx *gin.Context) { + if _, err := common.GetUID(ctx); err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + sections, err := vidser.GetValidSections() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, struct { + Sections []vidser.SectionResp `json:"sections"` + }{ + Sections: sections, + }) +} + +// 视频播放页的推荐视频列表 +func VidPlayRecommand(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + var vrr vidser.VidRecommandReq + if err := ctx.ShouldBind(&vrr); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + vid, err := primitive.ObjectIDFromHex(vrr.ID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if vid.IsZero() { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + resp, err := vidser.GetVidPlayRecommand(uid, vid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// 搜索页面的视频播放列表 +func SearchRecommand(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + resp, err := vidser.GetSearchRecommand(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// DiscountVidList doc +// @Summary 获取折扣专区下视频 +// @Description 获取折扣专区下视频 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param discountId query string true "折扣专区id" +// @Param pageSize query integer true "页码大小,一个专题下视频数量" +// @Param pageNumber query integer true "页码数" +// @Param sortType query integer true "排序类型 0-默认 1-最新 2-最热" +// @Success 200 {object} vidser.DiscountVideoResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vid/discount/list [get] +func DiscountVidList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil && !errors.Is(err, common.ErrUserNotExist) { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + req := vidser.DiscountVideoReq{} + if err := ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + resp, err := vidser.GetDiscountAreaVideos(uid, &req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + // 通过折扣专区id获取折扣视频 + common.ServeJSON(ctx, stderr.Success, resp) +} + +// DiscountArea doc +// @Summary 获取vip折扣专区 +// @Description 获取vip折扣专区 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} vidser.DiscountAreaResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vid/discount/area [get] +func DiscountArea(ctx *gin.Context) { + list, err := vidser.GetDiscountArea() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + if len(list) == 0 { + list = []discount_area_mod.DiscountArea{} + } + common.ServeJSON(ctx, stderr.Success, &vidser.DiscountAreaResp{ + List: list, + }) +} + +// CommunityRecommend doc +// @Summary 获取社区下黄油和图集的推荐 +// @Description 获取社区下黄油和图集的推荐 +// @Tags 社区模块 +// @Accept mpfd,json +// @Produce json,html +// @Param q query vidser.CommunityRecommendReq false "请求参数" +// @Success 200 {object} vidser.VideoUnderSubModuleResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vid/community/recommend [get] +func CommunityRecommend(ctx *gin.Context) { + req := vidser.CommunityRecommendReq{} + if err := ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + res, err := vidser.GetCommunityRecommend(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, res) +} + +// Ranking doc +// @Summary 视频/帖子/图集排行榜 +// @Description 视频/帖子/图集排行榜 +// @Tags 移动端-视频/帖子/图集排行榜 +// @Accept mpfd,json +// @Produce json +// @Param q query vidser.RankingReq false "请求参数" +// @Success 200 object vidser.RankingResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vid/ranking [get] +func Ranking(ctx *gin.Context) { + p := &vidser.RankingReq{} + if err := ctx.ShouldBind(&p); err != nil { + log.Error(fmt.Sprintf("Rank param err:%v", err)) + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + p.UID, _ = common.GetUID(ctx) + + res, err := p.List() + if err != nil { + log.Error(fmt.Sprintf("Get Ranking List err:%v", err)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// SectionList doc +// @Summary 获取一个模块下的专题 +// @Description 获取一个模块下的专题 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param mid path string true "模块id" +// @Param pageSize query integer true "页码大小" +// @Param pageNumber query integer true "专题页码" +// @Success 200 object vidser.SectionListResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vid/section/list [get] +func SectionList(ctx *gin.Context) { + req := vidser.SectionListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + resp, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// RecommendList doc +// @Summary 推荐视频列表 +// @Description 推荐视频列表 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param tagId query string true "标签id" +// @Param newsType query string true "帖子类型 SP-视频 SHORT-短视频 COVER-帖子 PIC-图集 SEED_LINK-种子帖" +// @Param pageNumber query int true "当前页" +// @Param pageSize query int true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vid/recommend/list [get] +func RecommendList(c *gin.Context) { + var arg struct { + commod.Page + TagId string `json:"tagId" form:"tagId"` // 标签 + NewsType string `json:"newsType" form:"newsType"` // 视频/帖子类型 + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "vid HotList arg error "+err.Error()) + return + } + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrAccessForbid, err) + return + } + list, hasNext, err := vidser.GetRecommendList(uid, arg.TagId, arg.NewsType, arg.Page) + if err != nil { + common.ServeJSON(c, stderr.Failure, err) + return + } + common.ServeJSON(c, stderr.Success, gin.H{ + "list": list, + "hasNext": hasNext, + "updatedAt": time.Now().Format("2006-01-02 15:04"), + }) +} + +// SubModuleAllVideoList doc +// @Summary 获取模块下视频列表(猜你喜欢) +// @Description 获取模块下视频列表,不区分专题(猜你喜欢) +// @Tags 首页模块 +// @Accept mpfd,json +// @Produce json,html +// @Param subModuleID path string true "亚模块" +// @Param q query vidser.SubModuleAllVideoListReq false "请求参数" +// @Success 200 {object} vidser.VideoUnderSubModuleResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vid/module/all/:subModuleID [get] +func SubModuleAllVideoList(ctx *gin.Context) { + uid, _ := common.GetUID(ctx) + subModuleIDStr := ctx.Param("subModuleID") + if subModuleIDStr == "" { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + subModuleID, err := primitive.ObjectIDFromHex(subModuleIDStr) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + req := vidser.SubModuleAllVideoListReq{} + if err := ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + res, err := vidser.SubModuleAllVideoList(uid, subModuleID, req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, res) +} + +// Library doc +// +// @Summary 获取首页片库接口 +// @Description 获取首页片库详情 +// @Tags 移动端-主页模块 +// @Accept mpfd,json +// @Produce json +// @Success 200 object vidmod.LibraryData "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vid/library [get] +func Library(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, nil) + return + } + data, err := vidser.GetLibrary(uid) + if err != nil { + common.ServeJSON(c, stderr.Failure, nil) + return + } + common.ServeJSON(c, stderr.Success, data) +} + +// LibrarySearch doc +// +// @Summary 片库-搜索视频 +// @Description 片库-搜索视频 +// @Tags 移动端-主页模块 +// @Accept mpfd,json +// @Produce json,html +// @Param q query vidser.LibraryElasticSearchRequest false "请求参数" +// @Success 200 object vidser.AppElasticSearchLibraryResponse "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vid/library/search [post] +func LibrarySearch(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + in := vidser.LibraryElasticSearchRequest{} + if err := ctx.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("video librarySearch param err:%v", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + in.UID = uid + data, err := in.CheckSearchType() + if err != nil { + log.Error(fmt.Sprintf("video librarySearch err:%v", err)) + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// RecommendShortVideos doc +// @Summary 获取模块下随机短视频 +// @Description 获取模块下随机视频 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query integer true "每页数量" +// @Param pageSize query integer true "页码大小" +// @Param X-Request-ID header string false "重试幂等ID;同一次请求重试保持不变" +// @Success 200 {object} vidser.VideoUnderSubModuleResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vid/module/short/all [get] +func RecommendShortVideos(ctx *gin.Context) { + uid, _ := common.GetUID(ctx) + //if err != nil && !errors.Is(err, common.ErrUserNotExist) { + // serveVid(ctx, stderr.ErrNoToken, nil) + // return + //} + req := vidser.HandleShortVideoUnderSubModuleReq{} + req.UID = uid + req.Context = ctx.Request.Context() + req.RequestID, _ = requestid.FromClient(ctx) + if err := ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + res := req.Do() + if req.Err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + //if err != nil { + // serveVid(ctx, stderr.ErrDbQueryError, nil) + // return + //} + common.ServeJSON(ctx, stderr.Success, res) +} + +// ChangeSectionVideoList doc +// @Summary 获取专题下的换一批视频列表 +// @Description 专题下的视频列表-换一批 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param sectionID path string true "专题ID" +// @Success 200 {object} vidser.VideoUnderSectionResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/vid/section/changeVideo/:sectionID [get] +func ChangeSectionVideoList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil && !errors.Is(err, common.ErrUserNotExist) { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + in := vidmod.ChangeVideoSectionReq{} + if err = ctx.ShouldBindUri(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err = ctx.ShouldBind(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + data, err := vidser.GetChangeVideos(&in, uid) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// ChangeSectionMediaList doc +// @Summary 获取专题下的换一批动漫/漫画列表 +// @Description 专题下的acg列表-换一批 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param sectionID path string true "专题ID" +// @Success 200 {object} vidser.VideoUnderSectionResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/vid/section/changeMedia/:sectionID [get] +func ChangeSectionMediaList(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil && !errors.Is(err, common.ErrUserNotExist) { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + in := vidmod.ChangeVideoSectionReq{} + if err = ctx.ShouldBindUri(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err = ctx.ShouldBind(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + data, err := vidser.GetChangeMedias(&in, uid) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// HomeMostNewModuleVideoList doc +// @Summary 首页固定的最新模块下视频数据列表 +// @Description 首页固定的最新模块下视频数据列表 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param q query vidmod.HomeMostNewModuleVideoListReq true "req" +// @Success 200 {object} vidser.VideoUnderSectionResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vid/home/new/list [get] +func HomeMostNewModuleVideoList(ctx *gin.Context) { + + in := vidmod.HomeMostNewModuleVideoListReq{} + err := ctx.ShouldBindQuery(&in) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + err = ctx.ShouldBind(&in) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + ua, _ := common.GetUA(ctx) + // 兼容线上参数错误 + if ua.SysType == "android" && in.SortType == 4 { + in.SortType = 3 + } else if in.SortType == 5 { + in.SortType = 4 + } + uid, _ := common.GetUID(ctx) + data, err := vidser.MostNewModuleVideoList(uid, in) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/video_gold_coin_ctrl/video_gold_coin.go b/app/api/video_gold_coin_ctrl/video_gold_coin.go new file mode 100644 index 0000000..b9d5639 --- /dev/null +++ b/app/api/video_gold_coin_ctrl/video_gold_coin.go @@ -0,0 +1,43 @@ +package video_gold_coin_ctrl + +import ( + "91porn-server/app/service/video_gold_coin_ser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/videogoldcoinmod" + "fmt" + "github.com/gin-gonic/gin" +) + +// GetVideoGoldCoin doc +// @Summary 金币视频列表 +// @Description 金币视频列表 +// @Tags 预售金币视频配置 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "每页条数" +// @Success 200 {object} videogoldcoinmod.QueryVideoGoldCoinRes +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/video_gold_coin/list [get] +func GetVideoGoldCoin(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var in videogoldcoinmod.QueryVideoGoldCoinCond + if err = ctx.ShouldBindQuery(&in); err != nil { + log.Error(fmt.Sprintf("video_gold_coin_ser param error:%v,uid:%v", err, uid)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, err := video_gold_coin_ser.GainVideoGoldCoin(uid, &in) + if err != nil { + log.Error(fmt.Sprintf("video_gold_coin_ser GainVideoGoldCoin error:%v,uid:%v", err, uid)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/videoactivityctrl/videoactivity.go b/app/api/videoactivityctrl/videoactivity.go new file mode 100644 index 0000000..b55ef33 --- /dev/null +++ b/app/api/videoactivityctrl/videoactivity.go @@ -0,0 +1,67 @@ +package videoactivityctrl + +import ( + "91porn-server/app/service/videoactivityserver" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/videoactivitymod" + + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 活动视频列表 +// @Description 活动详情/作品列表 +// @Tags 参赛作品 +// @Accept json +// @Produce json +// @Param activityId query string false "活动ID" +// @Param type query int false "榜单类型" +// @Param pageNumber query int true "第几页" +// @Param pageSize query int true "每页数量" +// @Success 200 object videoactivitymod.ListResponse "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /video_activity/list [get] +func List(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + var req videoactivitymod.ListRequest + if err = c.ShouldBindQuery(&req); err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + data, code := videoactivityserver.List(uid, &req) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, data) +} + +// HistoryRecord doc +// @Summary 活动列表 +// @Description 活动详情 +// @Tags 参赛作品 +// @Accept json +// @Produce json +// @Param pageNumber query int true "第几页" +// @Param pageSize query int true "每页数量" +// @Success 200 object videoactivitymod.HistoryRecordRequest "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /video_activity/history_record [get] +func HistoryRecord(c *gin.Context) { + var req videoactivitymod.HistoryRecordRequest + if err := c.ShouldBindQuery(&req); err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + data, code := videoactivityserver.HistoryRecord(&req) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, data) +} diff --git a/app/api/vipctrl/vipctrl.go b/app/api/vipctrl/vipctrl.go new file mode 100644 index 0000000..b606f2a --- /dev/null +++ b/app/api/vipctrl/vipctrl.go @@ -0,0 +1,223 @@ +package vipctrl + +import ( + "91porn-server/app/service/integral_config_ser" + "91porn-server/common/daichong" + "sync" + "time" + + "91porn-server/app/proto" + "91porn-server/app/service/productser" + "91porn-server/app/service/rechargeser" + "91porn-server/app/service/vipcardexperimentser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/annoumod" + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vipconfigmod" + + "github.com/gin-gonic/gin" +) + +// PurchaseHistory doc +// @Summary vip购买历史 +// @Description vip购买历史 +// @Tags vipzz +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData int true "页码" +// @Param pageSize formData int true "条数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/vip/history [get] +func PurchaseHistory(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var arg struct { + commod.Page + } + if err = ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + total, data, hasNext, err := prdcthsomod.FindProductHistorysByUID(uid, arg.PageNumber, arg.PageSize) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, map[string]interface{}{ + "total": total, + "history": data, + "list": data, + "hasNext": hasNext, + }) +} + +// Product doc +// @Summary vip种类 新 +// @Description 可购买的vip种类 +// @Tags vip +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} proto.ProductRes "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/vip/product [get] +func Product(ctx *gin.Context) { + uid, _ := common.GetUID(ctx) + ua, _ := common.GetUA(ctx) + var ( + data = proto.ProductRes{ + IsNewUser: false, + } + goldResError error + ) + var arg struct { + IsNewUser bool `json:"isNewUser" form:"isNewUser"` // 是否新手 + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + wg := sync.WaitGroup{} + wg.Add(3) + common.Go(func() { + defer wg.Done() + countdownSec, _ := productser.CountdownTiroCard(uid) + if countdownSec > 0 { + data.IsNewUser = true + } + if arg.IsNewUser == true { + data.IsNewUser = arg.IsNewUser + } + data.List, goldResError = rechargeser.New_ProductList(uid, ua.SysType, data.IsNewUser, 0) + }) + common.Go(func() { + defer wg.Done() + //d, _ := daichongser.NewTakeChat(ctx, uid, 0) + d := daichong.ChatResp{} + data.Daichong = d.Data + }) + common.Go(func() { + defer wg.Done() + data.IntegralList = integral_config_ser.GetAllConfig() + }) + wg.Wait() + if goldResError != nil { + common.ServeJSON(ctx, stderr.PayBusy, goldResError.Error()) + return + } + if err := vipcardexperimentser.ApplyToProductResponse(uid, &data, time.Now()); err != nil { + // 实验是增强能力,查询异常时降级到原套餐列表,不能阻断购买主链路。 + log.Warn("vip card experiment fallback to legacy product response", log.Any("uid", uid), log.E(err)) + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// 获取新手卡倒计时 +func CountdownTiroCard(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + countdownSec, err := productser.CountdownTiroCard(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"countdownSec": countdownSec}) +} + +// 获取新手卡倒计时 +func VIPUpAnnouns(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + var announs []annoumod.AnnounInfo + if u.IsPaidVIP() && u.VipExpireDate.Before(time.Now().AddDate(20, 0, 0)) { + announs, _ = annoumod.GetAnnouForDomain(annoumod.VIPUP) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + } + common.ServeJSON(ctx, stderr.Success, gin.H{"list": announs}) +} + +func VIPUpInfo(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + var cfg vipconfigmod.VIPConfig + cfg, _ = vipconfigmod.FindOne() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + var vipUpCheapPrice int64 + var vipUpPrice int64 + now := time.Now() + if u.VipExpireDate.After(now) && u.VipExpireDate.Before(now.AddDate(20, 0, 0)) { + switch u.VipLevel { + case 1: + vipUpPrice = cfg.VipUpPrice + vipUpCheapPrice = cfg.VipUpCheapPrice + case 2: + vipUpPrice = cfg.SVipUpPrice + vipUpCheapPrice = cfg.SVipUpCheapPrice + default: + } + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "vipUpPrice": vipUpPrice, + "vipUpCheapPrice": vipUpCheapPrice, + }) +} + +func VIPUp(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + code := productser.VIPUp(uid) + common.ServeJSON(ctx, code, gin.H{}) +} + +//// VipRecommend doc +//// @Summary vip推荐展示 +//// @Description vip推荐展示 +//// @Tags vip +//// @Accept mpfd,json +//// @Produce json,html +//// @Success 200 {object} productmod.Product +//// @Failure 400 {string} json "{"msg": "操作失败"}" +//// @Router /api/app/vip/recommend [get] +//func VipRecommend(ctx *gin.Context) { +// resp, err := productser.GetRecommendVip() +// if err != nil { +// common.ServeJSON(ctx, stderr.ErrDbQueryError, err) +// return +// } +// common.ServeJSON(ctx, stderr.Success, resp) +//} diff --git a/app/api/walletctrl/goldlog.go b/app/api/walletctrl/goldlog.go new file mode 100644 index 0000000..2ca9198 --- /dev/null +++ b/app/api/walletctrl/goldlog.go @@ -0,0 +1,246 @@ +package walletctrl + +import ( + "91porn-server/app/service/walletser" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/payvidlgmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "github.com/gin-gonic/gin" +) + +// GetBill doc +// @Summary --我的账单 +// @Description 我的账单 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData int true "页码" +// @Param pageSize formData int true "条数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/bills [get] +func GetBill(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var arg struct { + Timestamp int64 `form:"timeline"` + } + if err = ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, hasNext, nextTimeline, err := walletser.GetBills(uid, arg.Timestamp) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "hasNext": hasNext, + "list": data, + "timeline": nextTimeline, + }) +} + +// GetNewBill doc +// @Summary --我的账单新 +// @Description 我的账单 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param year query string true "年" +// @Param month query string true "月" +// @Param pageNumber query int true "页码" +// @Param pageSize query int true "条数" +// @Success 200 {object} txnmod.Bills1Res "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/zhangdan [get] +func GetNewBill(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var arg struct { + Month int `form:"month"` + Year int `form:"year"` + commod.Page + } + if err = ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, err := walletser.GetBills1(uid, arg.Year, arg.Month, arg.PageNumber, arg.PageSize) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// GetWorksIncome doc +// @Summary 我的作品--帖子收入记录详情 +// @Description 查询收入记录详情 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData int true "页码" +// @Param pageSize formData int true "条数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/income/works [get] +func GetWorksIncome(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var page commod.Page + if err = ctx.ShouldBind(&page); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + //total, totalAmount, data, hasNext, err := payvidlgmod.FindThreeMonthVideoPayRecord(uid, arg.PageNumber, arg.PageSize) + //if err != nil { + // common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + // return + //} + //common.ServeJSON(ctx, stderr.Success, gin.H{ + // "total": total, + // "totalIncome": totalAmount, + // "history": data, + // "list": data, + // "hasNext": hasNext, + //}) + + list, next, err := payvidlgmod.GetWorksIncomeList(uid, page) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": list, + "hasNext": next, + }) +} + +// GetIncome doc +// @Summary --收益明细 +// @Description 收益明细 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param startTime query string false "开始时间" +// @Param endTime query string false "结束时间" +// @Param pageNumber formData int true "页码" +// @Param pageSize formData int true "条数" +// @Success 200 {object} txnmod.TransactionLog "{"list": [],"hasNext":false}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/mine/iIncomes [get] +func GetIncome(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + var arg struct { + commod.Page + } + if err = ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, hasNext, err := txnmod.FindIncome(uid, arg.PageNumber, arg.PageSize) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + var rechargeIds []uint64 + for _, d := range data { + rechargeIds = append(rechargeIds, d.RechargeId) + } + + if len(rechargeIds) > 0 { + // 获取用户信息 + users, err := usermod.FindUsersMapByUID(rechargeIds) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + mUser := make(map[uint64]*txnmod.RechargeUserInfo) + if len(users) > 0 { + for _, u := range users { + ru := txnmod.RechargeUserInfo{ + UID: u.UID, + Name: u.Name, + Portrait: u.Portrait, + } + mUser[u.UID] = &ru + } + } + + for _, d := range data { + if d.RechargeId != 0 { + d.RechargeUser = *mUser[d.RechargeId] + } + } + } + + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": data, + "hasNext": hasNext, + }) +} + +// GetAllIncome doc +// @Summary --收益明细 +// @Description 收益明细 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param startTime query string false "开始时间" +// @Param endTime query string false "结束时间" +// @Param pageNumber formData int true "页码" +// @Param pageSize formData int true "条数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/mine/getAllIncome [get] +func GetAllIncome(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + data, err := walletser.GetIncome(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +func FruitCoinBill(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + common.ServeJSON(c, stderr.ErrNoToken, err) + return + } + var in = new(walletser.FruitCoinBillCond) + if err = c.ShouldBindQuery(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + data, code := walletser.FruitCoinBill(uid, in) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, data) +} diff --git a/app/api/walletctrl/store.go b/app/api/walletctrl/store.go new file mode 100644 index 0000000..6725b50 --- /dev/null +++ b/app/api/walletctrl/store.go @@ -0,0 +1,128 @@ +package walletctrl + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/walletser" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/common/store" + "91porn-server/models/v/walletmod" + "encoding/json" + "errors" + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" + "net/http" + "time" +) + +func WalletBalance(ctx *gin.Context) { + req := struct { + Uid uint64 `json:"uid" form:"uid"` + }{} + err := ctx.ShouldBind(&req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + w, err := walletmod.GetWallet(req.Uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err.Error()) + return + } + resp := struct { + Balance int64 `json:"balance"` + }{} + resp.Balance = w.Amount + w.Income + ctx.JSON(http.StatusOK, gin.H{ + "code": stderr.Success, + "msg": stderr.Success.Msg(), + "tip": stderr.Success.Tip(), + "data": resp, + "time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), + }) +} + +func WalletDeduct(ctx *gin.Context) { + req := struct { + Uid uint64 `json:"uid" form:"uid"` + OrderId primitive.ObjectID `json:"orderId" form:"orderId"` + Source int `json:"source" form:"source"` + Amount int64 `json:"amount" form:"amount"` // 扣除金额 + Desc string `json:"desc" form:"desc"` + }{} + err := ctx.ShouldBind(&req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if req.Uid <= 0 || req.OrderId.IsZero() || req.Amount <= 0 { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + err = Check(ctx, req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + resp := struct { + Balance int64 `json:"balance"` + }{} + balance, code := walletser.StoreDeductBalance(req.Uid, req.Amount, req.OrderId, req.Source, req.Desc) + resp.Balance = balance + ctx.JSON(http.StatusOK, gin.H{ + "code": code, + "msg": code.Msg(), + "tip": code.Tip(), + "data": resp, + "time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), + }) +} + +func Check(ctx *gin.Context, param interface{}) (err error) { + b, _ := json.Marshal(param) + sign, err := store.Encrypt(b, appg.Conf.Base.StoreEncryptKey) + if err != nil { + return + } + headerSign := ctx.Request.Header.Get("sign") + if sign != headerSign { + return errors.New("校验失败") + } + return nil +} + +func WalletRefund(ctx *gin.Context) { + req := struct { + Uid uint64 `json:"uid"` + OrderId primitive.ObjectID `json:"orderId"` + Source int `json:"source"` + Amount int64 `json:"amount"` + Desc string `json:"desc"` + }{} + err := ctx.ShouldBind(&req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if req.Uid <= 0 || req.OrderId.IsZero() || req.Amount <= 0 { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + err = Check(ctx, req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + resp := struct { + Balance int64 `json:"balance"` + }{} + balance, code := walletser.StoreRefund(req.Uid, req.Amount, req.OrderId, req.Source, req.Desc) + resp.Balance = balance + ctx.JSON(http.StatusOK, gin.H{ + "code": code, + "msg": code.Msg(), + "tip": code.Tip(), + "data": resp, + "time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), + }) +} diff --git a/app/api/walletctrl/wallectctrl.go b/app/api/walletctrl/wallectctrl.go new file mode 100644 index 0000000..5677ce3 --- /dev/null +++ b/app/api/walletctrl/wallectctrl.go @@ -0,0 +1,48 @@ +package walletctrl + +import ( + "91porn-server/app/service/ai_undress_server" + "91porn-server/app/service/aiser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/walletmod" + + "github.com/gin-gonic/gin" +) + +// GetWalletAmount doc +// @Summary 获取钱包信息 +// @Description 获取钱包信息 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} walletmod.Wallet "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /mine/wallet [get] +func GetWalletAmount(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + // 从AI女友页面返回后,钱包查询会先尝试把第三方剩余余额下分回主钱包。 + // 下分失败只记录告警,钱包接口仍返回当前本地余额。 + if err = aiser.TrySettleDown(ctx.Request.Context(), uid); err != nil { + log.Warn("wallet AI girlfriend settlement failed", log.Any("uid", uid), log.E(err)) + } + w, err := walletmod.GetWallet(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err.Error()) + return + } + resp := &walletmod.WalletResp{} + + // 当日剩余ai免费脱衣次数 + if w != nil { + resp.Wallet = *w + resp.TodayAiFreeTimes = ai_undress_server.GetTodayRemainingAiFreeUndressTimes(*w) + } + + common.ServeJSON(ctx, stderr.Success, resp) +} diff --git a/app/api/withdrawctrl/goldfish.go b/app/api/withdrawctrl/goldfish.go new file mode 100644 index 0000000..2083c49 --- /dev/null +++ b/app/api/withdrawctrl/goldfish.go @@ -0,0 +1,58 @@ +package withdrawctrl + +import ( + "bytes" + "errors" + "fmt" + "net/http" + "strconv" + + "91porn-server/app/service/withdrawser" + "91porn-server/common/log" + "91porn-server/common/rchgutil" + + "github.com/gin-gonic/gin" +) + +// GoldFishCallBack 金鱼结构回调函数 +func GoldFishCallBack(ctx *gin.Context) { + if err := func() error { + exchg := rchgutil.GoldFishExchangeRes{} + if err := ctx.ShouldBindJSON(&exchg); err != nil { + log.Error(fmt.Sprintf("GoldFish callback parameter bind fail error:%+v:", err)) + return err + } + e := rchgutil.GoldFishExchange{} + log.Info(fmt.Sprintf("GoldFish callback parameter data:%+v:", exchg)) + buf := bytes.Buffer{} + buf.WriteString(strconv.Itoa(exchg.Code)) + buf.WriteString(exchg.Mark) + buf.WriteString(exchg.MercID) + buf.WriteString(exchg.Money) + buf.WriteString(exchg.OID) + buf.WriteString(exchg.Status) + buf.WriteString(exchg.TradeNo) + buf.WriteString(e.GetAppSecret()) + if !rchgutil.VerifySign(exchg.Sign, buf.String()) { + log.Error("GoldFish callback sign verify fail") + return errors.New("check sign fail") + } + moneyf, err := strconv.ParseFloat(exchg.Money, 64) + if err != nil { + log.Error(fmt.Sprintf("GoldFish callback ParseFloat fail error:%+v:", err)) + return err + } + moneyf = moneyf * 100 + money := int64(moneyf) + exchg.TradeNo = rchgutil.EChgIDDisassemble(exchg.TradeNo) + if err = withdrawser.ExchgCallBack(exchg.TradeNo, exchg.OID, money, exchg.Status, exchg.Mark); err != nil { + log.Error(fmt.Sprintf("GoldFish RechargeCallBack fail error:%+v:", err)) + return err + } + return nil + }(); err != nil { + ctx.String(http.StatusBadRequest, "fail") + return + } + ctx.String(http.StatusOK, "success") +} diff --git a/app/api/withdrawctrl/withdrawctrl.go b/app/api/withdrawctrl/withdrawctrl.go new file mode 100644 index 0000000..dac1a7e --- /dev/null +++ b/app/api/withdrawctrl/withdrawctrl.go @@ -0,0 +1,201 @@ +package withdrawctrl + +import ( + "91porn-server/app/service/withdrawser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/txnactmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/wdordmod" + + "github.com/gin-gonic/gin" +) + +// Withdraw doc +// @Summary 提现 +// @Description 提现 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param money formData int true "提现金额" +// @Param payType formData string true "提现方式(alipay,bankcard)" +// @Param withdrawType formData int true "提现类型(0代理提现/1金币提现)" +// @Param actName formData string true "交易账户持有人" +// @Param act formData string true "交易账户" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /withdraw [post] +func Withdraw(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + p := wdordmod.WithdrawReq{} + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + //类型校验 + if p.PayType != wdordmod.TypeBankCard && p.PayType != wdordmod.TypeAlipay && p.PayType != wdordmod.TypeUsdt { + common.ServeJSON(ctx, stderr.ExchangeCodeInvalid, err.Error()) + return + } + w := wdordmod.WithdrawOrder{ + UID: uid, + UserIP: common.GetIP(ctx), + Money: p.Money, + PayType: p.PayType, + ActName: p.ActName, + Act: p.Act, + ProductType: p.ProductType, + } + if p.PayType == wdordmod.TypeBankCard { + user, err := usermod.FindUserByUID(uid) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, err) + return + } + if user.BankActName != "" && user.BankActName != p.ActName { + common.ServeJSON(ctx, stderr.DifferentBankActName, err) + return + } + typeAct := txnactmod.Bank + act, err := txnactmod.FindOne(txnactmod.Query{ + Act: &(p.Act), + AType: &typeAct, + }) + if err != nil { + log.Error("txnactmod.FindOne", log.Any("req", p), log.E(err)) + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, err) + return + } + if act.ID.IsZero() { + log.Warn("zero act id", log.Any("req", p), log.Any("act", act)) + common.ServeJSON(ctx, stderr.ErrNetWorkBusy, "") + return + } + w.BankCode = act.BankCode + } + if p.Name != nil { + w.Name = *p.Name + } + if p.DevID != nil { + w.DevID = *p.DevID + } + if p.WithdrawType != nil { + w.WithdrawType = *p.WithdrawType + } else { + w.WithdrawType = 1 + } + if err = withdrawser.WithDraw(&w); err != nil { + switch err.Error() { + case "Insufficient balance": + log.Error("WithDraw Insufficient balance", log.Any("error==>", err.Error())) + common.ServeJSON(ctx, stderr.InsufficientBalance, err.Error()) + return + case "PayType Insufficient balance": + log.Error("WithDraw PayType Insufficient balance", log.Any("error==>", err.Error())) + common.ServeJSON(ctx, stderr.InsufficientBalance, err.Error()) + return + case "illegal withdraw amount": + log.Error("Withdraw illegal withdraw amount", log.Any("order", w), log.E(err)) + common.ServeJSON(ctx, stderr.WithDrawIllegalAmount, err.Error()) + return + default: + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// WithdrawOrder doc +// @Summary 查看提现订单 +// @Description 查看提现订单 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData int true "页码" +// @Param pageSize formData int true "条数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /withdraw/order [get] +func WithdrawOrder(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + var arg struct { + commod.Page + } + if err = ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + total, data, hasNext, err := wdordmod.FindOrdersByUID(uid, arg.PageNumber, arg.PageSize) + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, map[string]interface{}{ + "total": total, + "result": data, + "list": data, + "hasNext": hasNext, + }) +} + +// GetType doc +// @Summary 提现方式获取 +// @Description +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Param payType formData string true "提现方式(银行卡/支付宝)" +// @Param pageSize formData int true "条数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /app/withdraw/type [get] +func GetType(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err.Error()) + return + } + var arg struct { + Type string `json:"payType" form:"payType"` + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := withdrawser.WithdrawType(u.SnapVip, arg.Type) + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// GetWithdrawCfg doc +// @Summary 提现配置获取 +// @Description +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /withdraw/cfg [get] +func GetWithdrawCfg(ctx *gin.Context) { + data := withdrawser.GetWithdrawCfg() + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/app/api/withdrawctrl/yinse.go b/app/api/withdrawctrl/yinse.go new file mode 100644 index 0000000..f2dc0ef --- /dev/null +++ b/app/api/withdrawctrl/yinse.go @@ -0,0 +1,40 @@ +package withdrawctrl + +import ( + "fmt" + "net/http" + + "91porn-server/app/service/withdrawser" + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +// YinseCallBack +func YinseCallBack(ctx *gin.Context) { + name := ctx.Param("name") + if name == "goldfish" { + GoldFishCallBack(ctx) + return + } + ctx.JSON(http.StatusOK, func() (msg rchgutil.ExchgBackMsg) { + exchg := rchgutil.ExchgBack{} + msg.Code = int(stderr.Failure) + if err := ctx.ShouldBindJSON(&exchg); err != nil { + msg.Err = fmt.Sprintf("GoldFish callback parameter bind fail error:%+v:", err) + log.Error(msg.Err) + return + } + log.Info(fmt.Sprintf("GoldFish callback parameter data:%+v:", exchg)) + exchg.TradeNo = rchgutil.EChgIDDisassemble(exchg.TradeNo) + if err := withdrawser.ExchgCallBack(exchg.TradeNo, exchg.OID, exchg.Money, exchg.Status, exchg.Mark); err != nil { + msg.Err = fmt.Sprintf("GoldFish RechargeCallBack fail error:%+v:", err) + log.Error(msg.Err) + return + } + msg.Code = int(stderr.Success) + return + }()) +} diff --git a/app/api/wonderctrl/wonderctrl.go b/app/api/wonderctrl/wonderctrl.go new file mode 100644 index 0000000..369b826 --- /dev/null +++ b/app/api/wonderctrl/wonderctrl.go @@ -0,0 +1,62 @@ +package wonderctrl + +import ( + "91porn-server/app/service/searcher" + "91porn-server/app/service/searcher/vidtagsearcher" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// VidList doc +// @Summary 精彩标签视屏 - 精彩标签视频 +// @Description 获取精彩标签视屏列表 +// @Tags Wonder +// @Accept mpfd,json +// @Produce json,html +// @Param tagID formData string true "标签ID" +// @Param pageNumber formData int true "当前页" +// @Param pageSize formData int true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "data":[{"name":"xx"},{"name":"yy"}]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /wonder/vid/list [post] +func VidList(ctx *gin.Context) { + var arg struct { + TagID string `form:"tagID" json:"tagID" binding:"required"` + commod.Page + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "search ThemeVids arg error "+err.Error()) + return + } + tagID, err := primitive.ObjectIDFromHex(arg.TagID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "search ThemeVids arg.TagId error: "+err.Error()) + return + } + // 检查uid是否存在 + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrAccessForbid, "search List Context USER_ID is not exist ") + ctx.Abort() + return + } + var keyword = &searcher.ObjID{ID: tagID} + opt := &searcher.Option{} + opt.SetSkip(int64((arg.PageNumber - 1) * (arg.PageSize))) + opt.SetLimit(int64(arg.PageSize)) + opt.SetSort("wonderSortCode", -1) + opt.SetSort("tPlayCount", -1) + result, err := vidtagsearcher.NewVidTagSearcher(uid).Search(keyword, opt) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": result.Data(), + "hasNext": result.HasNext(), + }) +} diff --git a/app/appg/ai_callback.go b/app/appg/ai_callback.go new file mode 100644 index 0000000..4858d15 --- /dev/null +++ b/app/appg/ai_callback.go @@ -0,0 +1,14 @@ +package appg + +import ( + "errors" + "strings" +) + +// AICallbackURL returns the externally reachable Web callback URL for AI jobs. +func AICallbackURL(path string) (string, error) { + if Conf == nil || strings.TrimSpace(Conf.URL.AiImageToVideoCallbackUrl) == "" { + return "", errors.New("ai callback url is not configured") + } + return strings.TrimRight(strings.TrimSpace(Conf.URL.AiImageToVideoCallbackUrl), "/") + "/" + strings.TrimLeft(path, "/"), nil +} diff --git a/app/appg/appg.go b/app/appg/appg.go new file mode 100644 index 0000000..7170c84 --- /dev/null +++ b/app/appg/appg.go @@ -0,0 +1,229 @@ +package appg + +import ( + "91porn-server/common/constant" + "91porn-server/common/db" + "91porn-server/common/elastic" + "91porn-server/common/email" + "91porn-server/common/httputil" + "91porn-server/common/laosiji_app" + "91porn-server/common/redis" + "91porn-server/common/sms" + "91porn-server/common/tg" +) + +var ( + Conf *GlobalConfig + Static *StaticSource + Name *NameSource + Redis *redis.Client + VideoDB *db.MongoDB + LogDB *db.MongoDB + StatDB *db.MongoDB + Bot *tg.Bot + VideoES *elastic.Client // ES搜索 + ProxyCfg *httputil.ProxyCfg + ImAuth string //im token +) + +// ShouldEnforceIPRateLimit reports whether IP-based request restrictions should +// be applied. The shared test environment intentionally skips these checks so +// NAT traffic from testers cannot block login, registration, SMS, or comments. +func ShouldEnforceIPRateLimit() bool { + return Conf == nil || Conf.Base.Env != constant.TestRunmod +} + +// H265URLForApp 提供统一的 App H.265 熔断开关。 +// 未配置或配置为 true 时返回原地址;显式为 false 时所有 App 接口隐藏地址。 +func H265URLForApp(rawURL string) string { + if Conf != nil && Conf.Hevc.EnableApp != nil && !*Conf.Hevc.EnableApp { + return "" + } + return rawURL +} + +type StaticSource struct { + Portrait []string `json:"portrait"` + SysUser []uint64 `json:"sysUser"` + TagFilter []string `json:"tagFilter"` + VerifyCodeForFake string `json:"verifyCodeForFake"` //马甲号验证码 +} + +type NameSource struct { + FamilyNames []string `json:"familyNames"` + MiddleNames []string `json:"middleNames"` + AfterNames []string `json:"afterNames"` +} + +type ReplayAttackConfig struct { + Enable bool `json:"enable"` + Key string `json:"key"` + WindowDurationSeconds int64 `json:"windowDurationSeconds"` +} + +type GlobalConfig struct { + Base struct { + Port int64 `json:"port"` + PProf bool `json:"pprof"` + Env string `json:"env"` + JwtKey string `json:"jwtKey"` + ResKey string `json:"resKey"` + ResKey360 string `json:"resKey360"` + DevIDSignKey string `json:"devIDSignKey"` + WebJwtKey string `json:"webJwtKey"` + ReplayAttack ReplayAttackConfig `json:"replayAttack"` + DemarcatAmout uint64 `json:"demarcatAmout"` + TotalWatch uint64 `json:"totalWatch"` + StoreEncryptKey string `json:"storeEncryptKey"` + // H5M3u8TicketKey H5 m3u8 播放防盗链票据的服务端密钥(建议 openssl rand -base64 32)。 + // 留空时保持旧逻辑(不签票、不验票),便于先发布代码再由运维配置密钥开启严格模式。 + H5M3u8TicketKey string `json:"h5M3u8TicketKey"` + // 媒体分片(TS)鉴权签名密钥对象(含 keyVersion/key);未配置时回退内置默认(版本 default + constant.MediaSourceAuthKey) + TsAuth constant.TsAuthKeyConfig `json:"tsAuth"` + } `json:"base"` + Log struct { + Level string `json:"level"` + DisableStacktrace bool `json:"disableStacktrace"` + } `json:"log"` + MonitorReport struct { + GroupId string `json:"groupId"` + SecretKey string `json:"secretKey"` + } `json:"monitorReport"` + Kefu struct { + AppID string `json:"appId"` + AppSecret string `json:"appSecret"` + PlatName string `json:"platName"` + Theme string `json:"theme"` + } `json:"kefu"` + DataReport struct { + Url string `json:"url"` + AppUrl string `json:"appUrl"` + } `json:"dataReport"` + DaiChong struct { + AppID string `json:"appId"` + AppSecret string `json:"appSecret"` + PlatName string `json:"platName"` + Domain string `json:"domain"` + UploadUrl string `json:"uploadUrl"` + H5domain string `json:"h5domain"` + H5domainQP string `json:"h5domainQP"` + } `json:"daichong"` + Customer struct { + Url string `json:"url"` + AppId string `json:"appId"` // 客服中心给的appid + Secret string `json:"secret"` + } `json:"customer"` + PayCenter struct { + AppName string `json:"appName"` // APP名称 + CallbackUrl string `json:"callbackUrl"` // 回调地址(APP服务地址) + ApiUrl string `json:"apiUrl"` // 新支付中心地址 + MercID string `json:"mercID"` // 商户ID + MercSecret string `json:"mercSecret"` // 商户密钥 + } `json:"payCenter"` + Im struct { + AppID string `json:"appId"` + AppName string `json:"appName"` + AppSecret string `json:"appSecret"` + AppJwtKey string `json:"appJwtKey"` + } `json:"im"` + ImV2 struct { + BaseURL string `json:"baseUrl"` + DynamicConfigDomain string `json:"dynamicConfigDomain"` + SocketURL string `json:"socketUrl"` + MerchantCode string `json:"merchantCode"` + TenantCode string `json:"tenantCode"` + AppKey string `json:"appKey"` + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + } `json:"imv2"` + Elastic struct { + VideoUrl string `json:"videoUrl"` + IsActive bool `json:"isActive"` + UserName string `json:"userName"` + PassWord string `json:"passWord"` + } `json:"elastic"` + Mongo struct { + VideoDbUrl string `json:"videoDbUrl"` //主数据库 存放强业务相关的数据 + StatDbUrl string `json:"statDbUrl"` //统计数据库 存放统计 日志 渠道相关的数据 + LogDbUrl string `json:"logDbUrl"` //日志数据库 + LogReadDBUrl string `json:"logDbUrlRead"` //读日志数据库 + } `json:"mongo"` + Redis struct { + URL string `json:"url"` + } `json:"redis"` + ShortRecommend ShortRecommendConfig `json:"shortRecommend"` + Game struct { + URL string `json:"url"` + MercId int `json:"mercId"` + } `json:"game"` + URL struct { + ImCenterUrl string `json:"imCenterUrl"` + OriginUrl string `json:"originUrl"` + JH1BOriginUrl string `json:"jH1BOriginUrl"` + LaoSiJiOriginUrl string `json:"laoSiJiOriginUrl"` + LaoSiJiTSCdn string `json:"laoSiJiTSCdn"` + UploadUrl string `json:"uploadUrl"` + UploadImgUrl string `json:"uploadImgUrl"` + SendSingleFile string `json:"sendSingleFile"` + SendBatchFile string `json:"sendBatchFile"` + FileInfoUrl string `json:"fileInfoUrl"` + CoverInfoUrl string `json:"coverInfoUrl"` + LocalWebUrl string `json:"localWebUrl"` + CdnUrl string `json:"cdnUrl"` + HttpProxy string `json:"httpProxy"` + TransactionUrl string `json:"transactionUrl"` // 支付下单地址 + DataCentreURL string `json:"dataCentreUrl"` // 数据中心 + ApiUrls []string `json:"apiUrls"` // api域名 + ProductUrl string `json:"productUrl"` // 产品中心 + //Active2023 string `json:"active2023"` // 2023新春抽奖地址 + NavigateUrl string `json:"navigateUrl"` // 导航站地址 + HlH5Url string `json:"hlH5Url"` // 黑料h5url + LuckyDrawH5 string `json:"luckyDrawH5"` // 抽奖H5链接 + StoreUrl string `json:"storeUrl"` // 原味商城url + AiServer string `json:"aiServer"` //ai订单请求地址 + AiUndressServer string `json:"aiUndressServer"` // AI图片脱衣服务地址 + AiImageToVideoUrl string `json:"aiImageToVideoUrl"` // AI图生视频请求地址 + AiImageToVideoCallbackUrl string `json:"aiImageToVideoCallbackUrl"` // AI图生视频回调地址 + AIMateH5 string `json:"aiMateH5"` // AI伴侣链接 + } `json:"url"` + Limit struct { + GlobalLimit int64 `json:"globalLimit"` // 全局接口访问频率限制 + CommentLimit int64 `json:"commentLimit"` // 评论接口限制 + RechargeLimit int64 `json:"rechargeLimit"` // 充值接口限制 + LoginLimit int64 `json:"loginLimit"` // 登陆注册接口限制 + SMSIPLimit int64 `json:"smsIPLimit"` // 短信验证码 IP 频率限制(次/自然日) + } `json:"limit"` + Hevc struct { + EnableApp *bool `json:"enableApp"` // false 时隐藏 App 响应里的 H265 播放地址;未配置时默认返回 + PullSecret string `json:"pullSecret"` // SKD 与 App 共享的 H265 拉流签名密钥,至少 32 个随机字节 + } `json:"hevc"` + CmtLimit struct { + CmtLimitExpire int64 `json:"cmtLimitExpire"` // 发表评论限制过期时间(秒) + CommentCount int64 `json:"commentCount"` // 发表评论次数限制 + } `json:"cmtLimit"` + HttpOptions struct { + DcCtxTimeOut uint64 `json:"dcCtxTimeOut"` //代充http请求 超时ctx取消时间 + } `json:"httpOptions"` + AWS struct { + SES email.Config `json:"ses"` + } `json:"aws"` + Kafka struct { + Url string `yaml:"url"` + Group string `yaml:"group"` + } `yaml:"kafka"` + AdCenter struct { + ApiDomain string `json:"apiDomain"` // api地址 如:https://adopenapi.adcs01.top + MerchantCode string `json:"merchantCode"` // 商户 code + AppCode string `json:"appCode"` // 应用 code + AesKey string `json:"aesKey"` // 数据解密KEY + } `json:"adCenter"` + RandomBanner string `json:"randomBanner"` // 随机banner + AiFreeUnDressDailyLimit int `json:"aiFreeUnDressDailyLimit"` // AI免费脱衣每日限额 + SMS sms.Config `json:"sms"` + ActivityServer struct { + AppId string `json:"appId"` // 活动服应用ID + SecretKey string `json:"secretKey"` // 活动服回调签名密钥(Base64编码) + ApiUrl string `json:"apiUrl"` // 活动服API地址,用于拉取域名等 + } `json:"activityServer"` + LaoSiJiAiMate laosiji_app.Config `json:"laoSiJiAiMate"` // 老司机AI女友V2第三方配置 +} diff --git a/app/appg/elasticg.go b/app/appg/elasticg.go new file mode 100644 index 0000000..30ab34f --- /dev/null +++ b/app/appg/elasticg.go @@ -0,0 +1,24 @@ +package appg + +import ( + "os" + + "91porn-server/common/elastic" + "91porn-server/common/log" +) + +func InitElastic() { + opts := elastic.Options{ + Address: []string{Conf.Elastic.VideoUrl}, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 30, + UserName: Conf.Elastic.UserName, + PassWord: Conf.Elastic.PassWord, + } + c, err := elastic.InitElastic(opts) + if err != nil { + log.Error("startUp VideoES InitElastic error", log.E(err)) + os.Exit(1) + } + VideoES = c +} diff --git a/app/appg/mong.go b/app/appg/mong.go new file mode 100644 index 0000000..db3c1cc --- /dev/null +++ b/app/appg/mong.go @@ -0,0 +1,31 @@ +package appg + +import ( + "91porn-server/common/db" + "91porn-server/models" +) + +func InitMongodbParam(registerPool []models.Register) ([]db.DBmap, []db.Register) { + pri := db.DBmap{ + Key: models.VideoDb, + URL: Conf.Mongo.VideoDbUrl, + } + sta := db.DBmap{ + Key: models.StatDb, + URL: Conf.Mongo.StatDbUrl, + } + log := db.DBmap{ + Key: models.LogDb, + URL: Conf.Mongo.LogDbUrl, + } + readLog := db.DBmap{ + Key: models.LogReadDb, + URL: Conf.Mongo.LogReadDBUrl, + } + dbgister := make([]db.Register, len(registerPool)) + for i, v := range registerPool { + dbgister[i] = db.Register{Key: v.Key, Table: v.Table} + } + dbmaps := []db.DBmap{pri, sta, log, readLog} + return dbmaps, dbgister +} diff --git a/app/appg/redisg.go b/app/appg/redisg.go new file mode 100644 index 0000000..0faedf2 --- /dev/null +++ b/app/appg/redisg.go @@ -0,0 +1,29 @@ +package appg + +import ( + "os" + + "91porn-server/common/log" + "91porn-server/common/redis" +) + +func InitRedis() { + r, err := redis.WithURL(Conf.Redis.URL) + if err != nil { + log.Error("startUp vdb redis error", log.E(err)) + os.Exit(1) + } + Redis = r +} + +func RedisClose() error { + if Redis != nil { + err := Redis.Close() + if err != nil { + log.Error("RUdb Redis Close Error", log.E(err)) + return err + } + log.Info("RUdb Redis Close OK") + } + return nil +} diff --git a/app/appg/shortrecommend.go b/app/appg/shortrecommend.go new file mode 100644 index 0000000..c953af5 --- /dev/null +++ b/app/appg/shortrecommend.go @@ -0,0 +1,43 @@ +package appg + +import "time" + +const ( + defaultShortRecommendRequestTimeout = 3 * time.Second + defaultShortRecommendKeyTTLHours = 72 +) + +// ShortRecommendConfig 控制App侧短视频环形队列读取行为。 +// Enabled 使用指针是为了兼容旧配置:未配置时默认启用。 +type ShortRecommendConfig struct { + Enabled *bool `json:"enabled"` + RequestTimeoutMs int `json:"requestTimeoutMs"` + MaxBatches int `json:"maxBatches"` + ScanMultiplier int `json:"scanMultiplier"` + KeyTTLHours int `json:"keyTTLHours"` +} + +func (c *GlobalConfig) ShortRecommendEnabled() bool { + return c == nil || c.ShortRecommend.Enabled == nil || *c.ShortRecommend.Enabled +} + +func (c *GlobalConfig) ShortRecommendRequestTimeout() time.Duration { + if c == nil || c.ShortRecommend.RequestTimeoutMs <= 0 { + return defaultShortRecommendRequestTimeout + } + timeout := time.Duration(c.ShortRecommend.RequestTimeoutMs) * time.Millisecond + if timeout < 100*time.Millisecond { + return 100 * time.Millisecond + } + if timeout > 10*time.Second { + return 10 * time.Second + } + return timeout +} + +func (c *GlobalConfig) ShortRecommendKeyTTL() int { + if c == nil || c.ShortRecommend.KeyTTLHours <= 0 { + return defaultShortRecommendKeyTTLHours + } + return c.ShortRecommend.KeyTTLHours +} diff --git a/app/appg/shortrecommend_test.go b/app/appg/shortrecommend_test.go new file mode 100644 index 0000000..56f61a3 --- /dev/null +++ b/app/appg/shortrecommend_test.go @@ -0,0 +1,36 @@ +package appg + +import ( + "testing" + "time" +) + +func TestShortRecommendDefaultsPreserveLegacyDeployment(t *testing.T) { + var nilConfig *GlobalConfig + if !nilConfig.ShortRecommendEnabled() { + t.Fatal("nil config should default to enabled") + } + if got := nilConfig.ShortRecommendRequestTimeout(); got != 3*time.Second { + t.Fatalf("timeout = %v, want 3s", got) + } + if got := nilConfig.ShortRecommendKeyTTL(); got != 72 { + t.Fatalf("TTL hours = %d, want 72", got) + } +} + +func TestShortRecommendConfigBoundsTimeout(t *testing.T) { + disabled := false + cfg := &GlobalConfig{} + cfg.ShortRecommend.Enabled = &disabled + cfg.ShortRecommend.RequestTimeoutMs = 1 + if cfg.ShortRecommendEnabled() { + t.Fatal("configured disabled should be honored") + } + if got := cfg.ShortRecommendRequestTimeout(); got != 100*time.Millisecond { + t.Fatalf("minimum timeout = %v, want 100ms", got) + } + cfg.ShortRecommend.RequestTimeoutMs = 20_000 + if got := cfg.ShortRecommendRequestTimeout(); got != 10*time.Second { + t.Fatalf("maximum timeout = %v, want 10s", got) + } +} diff --git a/app/appg/static.go b/app/appg/static.go new file mode 100644 index 0000000..9a3b010 --- /dev/null +++ b/app/appg/static.go @@ -0,0 +1,30 @@ +package appg + +import ( + "fmt" + + "91porn-server/common/conf" +) + +func InitStatic() { + initName() + initStatic() +} + +func initName() { + name := NameSource{} + err := conf.LoadJSON("config/names.json", &name) + if err != nil { + fmt.Printf("startUp load names error: %v\n", err) + } + Name = &name +} + +func initStatic() { + static := StaticSource{} + err := conf.LoadJSON("config/static.json", &static) + if err != nil { + fmt.Printf("startUp load static error: %v\n", err) + } + Static = &static +} diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..7be5541 --- /dev/null +++ b/app/main.go @@ -0,0 +1,177 @@ +package main + +import ( + "91porn-server/common/cachev2" + "91porn-server/common/dataReport" + "91porn-server/common/services" + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "91porn-server/app/api/updownctrl" + "91porn-server/app/appg" + "91porn-server/app/router" + "91porn-server/app/service/activityclient" + "91porn-server/app/service/imser" + "91porn-server/common" + "91porn-server/common/conf" + "91porn-server/common/constant" + _ "91porn-server/common/daichong" + "91porn-server/common/db" + "91porn-server/common/email" + "91porn-server/common/filter" + "91porn-server/common/httputil" + "91porn-server/common/laosiji_app" + "91porn-server/common/log" + recommendqueue "91porn-server/common/shortrecommend" + "91porn-server/common/sms" + "91porn-server/common/tg" + "91porn-server/models" + "91porn-server/models/Init" + _ "91porn-server/models/commod" + _ "91porn-server/models/v/moduleconfmod" // swagger依赖 + _ "91porn-server/models/v/productmod" + _ "91porn-server/models/v/rchgamtmod" // swagger依赖 + _ "91porn-server/models/v/recommod" // swagger依赖 + _ "91porn-server/models/v/sourcemod" + _ "91porn-server/models/v/systemmod" + _ "91porn-server/models/v/txnmod" // swagger + _ "91porn-server/models/v/usermod" // swagger依赖 + _ "91porn-server/models/v/versionmod" + _ "91porn-server/models/v/videocoupon" // swagger依赖 + _ "91porn-server/models/v/vidmod" // swagger + _ "91porn-server/models/v/walletmod" + + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +var httpServer *http.Server + +// @title app文档 +// @version 1.0 +// @description 短视频服务后端API接口文档 + +// @contact.name API Support +// @contact.url http://www.swagger.io/support + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +func init() { + cfg := appg.GlobalConfig{} + if err := conf.LoadJSON("config/app.json", &cfg); err != nil { + fmt.Printf("startUp get config error: %v\n", err) + os.Exit(1) + } + fmt.Println("use local config successfully") + //加载配置 + appg.Conf = &cfg + log.Init(log.Options{Level: appg.Conf.Log.Level, DisableStack: appg.Conf.Log.DisableStacktrace}) + sms.Init(appg.Conf.SMS) + laosiji_app.Init(appg.Conf.LaoSiJiAiMate) + common.InitGinSecret(appg.Conf.Base.ResKey, appg.Conf.Base.ResKey360, appg.Conf.Base.Env) + if cfg.Base.Env == constant.ProdEnv { + appg.Bot = tg.New(tg.TianTian_Bot, fmt.Sprintf("短视频[env:%s]", appg.Conf.Base.Env)) + common.SetPanicHandler(func(s string) { _ = appg.Bot.Send(tg.Msg{ChatId: tg.ChatIdServerTeam, Text: "[App-Server]" + s}) }) + } +} + +// @host 127.0.0.1:9009 +// @BasePath +func main() { + time.Local, _ = time.LoadLocation("Asia/Shanghai") + common.Go(startUp) + waitQuit() +} + +func startUp() { + //初始化数据库 + ds := db.InitDS(appg.InitMongodbParam(models.RegisterPool)) + if len(ds) > 0 { + for k, v := range ds { + switch k { + case models.VideoDb: + appg.VideoDB = v + case models.LogDb: + appg.LogDB = v + case models.StatDb: + appg.StatDB = v + } + } + } + appg.ProxyCfg = &httputil.ProxyCfg{ + Host: appg.Conf.URL.HttpProxy, + IsActive: false, + } + dataReport.Init(dataReport.Config{ + ApiUrl: appg.Conf.DataReport.Url, + }) + //初始化静态配置 + appg.InitStatic() + // 初始化Redis + appg.InitRedis() + if err := recommendqueue.SetKeyTTLHours(appg.Conf.ShortRecommendKeyTTL()); err != nil { + log.Error("short recommend key TTL config invalid, use default", + log.E(err), log.Any("configuredHours", appg.Conf.ShortRecommendKeyTTL())) + _ = recommendqueue.SetKeyTTLHours(72) + } + services.Init(appg.Redis) + // 初始化缓存操作中间件 + cachev2.Init(appg.Redis, log.ZapLog) + // 初始化ElasticSearch + appg.InitElastic() + Init.InitMongo() + Init.ESInit() + Init.InitCache() + // init email service + _ = email.MustInit(context.TODO(), appg.Conf.AWS.SES) + // 初始化文本过滤器 + filter.Start() + + // 启动时异步取一次 IM appToken + 每小时刷新(不阻断启动) + imser.InitAppToken() + // 启动活动服定时同步 + activityclient.Start() + //开始http + ginRouter := router.SetupGin() + ginRouter.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + port := appg.Conf.Base.Port + httpServer = &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: ginRouter, + } + log.Info("Start Listen ...", log.Any("port", port)) + if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatal("server listen err", log.E(err)) + } + // Block Here + log.Info("httpServer quit...") +} + +func waitQuit() { + //graceful exit + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT) + <-ch + log.Info("Recv Signal, Shutdown Server Start...") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if httpServer != nil { + if err := httpServer.Shutdown(ctx); err != nil { + log.Error("Server Shutdown error", log.E(err)) + } + } + db.CloseDS() + + updownctrl.FlushM3u8RefererStats() // 退出前把进程内聚合的来源计数刷回 Redis,减少丢失 + if err := appg.RedisClose(); err != nil { + log.Error("redis close error", log.E(err)) + return + } + log.Warn("Shutdown Server OK...") +} diff --git a/app/middleware/activityauth/activityauth.go b/app/middleware/activityauth/activityauth.go new file mode 100644 index 0000000..cef1e76 --- /dev/null +++ b/app/middleware/activityauth/activityauth.go @@ -0,0 +1,55 @@ +package activityauth + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "io" + "net/http" + + "91porn-server/app/appg" + "91porn-server/common/log" + + "github.com/gin-gonic/gin" +) + +// HmacAuth 活动服回调 HMAC-SHA256 签名校验中间件 +func HmacAuth(c *gin.Context) { + signature := c.GetHeader("X-Signature") + if signature == "" { + c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "missing signature"}) + c.Abort() + return + } + + bodyBytes, err := io.ReadAll(c.Request.Body) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "read body failed"}) + c.Abort() + return + } + c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + secretKey := appg.Conf.ActivityServer.SecretKey + keyBytes, err := base64.StdEncoding.DecodeString(secretKey) + if err != nil { + log.Error("activityauth: decode secretKey failed", log.E(err)) + c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "server config error"}) + c.Abort() + return + } + + h := hmac.New(sha256.New, keyBytes) + h.Write(bodyBytes) + expected := hex.EncodeToString(h.Sum(nil)) + + if !hmac.Equal([]byte(expected), []byte(signature)) { + c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "invalid signature"}) + c.Abort() + return + } + + c.Next() +} diff --git a/app/middleware/authuser/authuser.go b/app/middleware/authuser/authuser.go new file mode 100644 index 0000000..f95d6a7 --- /dev/null +++ b/app/middleware/authuser/authuser.go @@ -0,0 +1,265 @@ +package authuser + +import ( + "net/http" + "strings" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/crypt" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/usermod" + + "github.com/gin-gonic/gin" +) + +var ( + // 白名单,不需要验证token的api + // 在新的Auth中间件逻辑下,配置了白名单后,用户未登录,context中的uid会被设为0;否则,会绑定用户真实uid。 + // 需要白名单中API的handler自己对用户登录状态进行判断。 + whitelist = map[string]bool{ + "/swagger": true, + "/api/app/mine/login": true, + "/api/app/im/whiteSign": true, + "/api/app/im/sign": true, + "/api/app/mine/mobileLoginOnly": true, + //"/api/app/ping/domain": true, + "/api/app/mine/ver": true, + "/api/app/ping/check": true, + "/api/app/static/faq/html/index": true, + "/api/app/static/faq/tmpl/index": true, + //"/api/app/vid/m3u8": true, //前端测试 下载m3u8 添加到白名单 ,生产环境中需要从白名单中剔除 + "/api/app/vid/h5/light/m3u8": true, // 现有 H5 轻量播放接口 + // H.265 云转码使用 Handler 自己的路径绑定、限时 HMAC 鉴权。 + "/api/app/vid/transcode/m3u8": true, + "/sources": true, + "/defray/callback/shark": true, + "/defray/callback/goldfish": true, + "/api/app/vid/sec": true, + "/api/app/vid/pms/sec": true, + "/api/app/vid/pms/mt_sec": true, + "/api/app/vid/lsjsec": true, + "/api/app/vid/m3u8sec": true, + //"/api/app/notification/captcha": true, + "/api/app/newactivity": true, + "/api/app/sge": true, + "/api/app/code/webexchange": true, + "/api/app/game/userInfo": true, + "/api/app/game/deduct": true, + "/api/app/game/code": true, + "/api/app/userinvite/callback/recharge": true, + "/api/app/statcenter/sync": true, + "/api/app/avcomment/info": true, + "/api/app/ping/v": true, + //"/api/app/recommend": true, + // "/api/app/modules": true, + // "/api/app/tag/conf/list": true, + "/api/app/avcomment/list": true, + // 兼容登录、未登录状态的API + // "/api/app/vid/module/:subModuleID": true, + // "/api/app/vid/section/:sectionID": true, + "/api/app/mine/resetpassword": true, + "/api/app/mine/resetpassword/mobileverify": true, + "/api/app/mine/login/h5": true, + //"/api/app/comment/list": true, + //"/api/app/mine/info": true, + "/api/app/recommend/light/vids": true, + "/api/app/store_wallet": true, + "/api/app/aimate/sync": true, + "/api/app/health/ping": true, + } +) + +func GetTokenSecret() string { + return appg.Conf.Base.JwtKey +} + +type Claims struct { + UID uint64 `json:"uid"` + Timestamp int64 `json:"timestamp"` + Type uint8 `json:"type"` +} + +func GenToken(claims *Claims) (string, error) { + secret := GetTokenSecret() + args, _ := common.JSONStruct2Map(claims) + token, err := crypt.CreateToken(secret, args) + if err != nil { + log.Error("genUserToken error", log.Any("claims", claims), log.E(err)) + } + return token, err +} + +func ParseToken(token string) (*Claims, error) { + secret := GetTokenSecret() + claims, err := crypt.ParseToken(secret, token) + if err != nil { + return nil, err + } + c := Claims{} + return &c, common.Map2JSONStruct(&c, claims) +} + +func tokenRedisKey(uid uint64) string { + return redisconst.UserTokenKey(uid) +} + +func cacheToken(uid uint64, token string) { + key := tokenRedisKey(uid) + _ = appg.Redis.Set(key, token, redisconst.UserTokenExpire) +} + +func RevokeTokenCache(uids ...uint64) { + keys := make([]string, len(uids)) + for i, uid := range uids { + keys[i] = tokenRedisKey(uid) + } + _, _ = appg.Redis.Del(keys...) +} + +func auth(token string) (uint64, stderr.Code) { + claims, err := ParseToken(token) + if err != nil { + return 0, stderr.InvalidToken + } + uid := claims.UID + redisKey := tokenRedisKey(uid) + redisToken, err := appg.Redis.Get(redisKey) + if redisToken == nil || err != nil { //redis没有获取到token + u, err := usermod.FindUserByUID(uid) + if err != nil { + log.Warn("[InvalidToken] cause by find user by uid error", log.Any("uid", uid), log.E(err)) + return 0, stderr.ErrNetWorkBusy + } + if u == nil { + log.Warn("[InvalidToken] cause by not find user info", log.Any("uid", uid)) + return 0, stderr.UserIsNotExists + } + if u.Token != token { + log.Warn("[InvalidToken] cause by current token not equal user's token", log.Any("uid", uid), log.Any("user's token", u.Token), log.Any("current token", token)) + return 0, stderr.InvalidToken + } + if u.HasLocked { + return uid, stderr.ErrAccessForbid + } + cacheToken(uid, u.Token) + } else { + if *redisToken != token { + log.Warn("[InvalidToken] cause by current token not equal redis's token", log.Any("uid", uid), log.Any("current token", token), log.Any("redis token", *redisToken)) + return 0, stderr.InvalidToken + } + } + return uid, stderr.Success +} + +func Auth(ctx *gin.Context) { + inWhiteList := false + for url, ok := range whitelist { + if ok && (strings.HasPrefix(ctx.Request.URL.Path, url) || strings.HasPrefix(ctx.FullPath(), url)) { + inWhiteList = true + break + } + } + var token string + var uid uint64 + var code stderr.Code + t1 := ctx.Request.Header.Get("Authorization") + t2 := ctx.Query("token") //为兼容m3u8 + if t1 != "" { + token = t1 + } + if t2 != "" { + token = t2 + } + if token == "" && !inWhiteList { // 请求的api未进入白名单且请求未带token,则视为非法请求,直接返回错误 + ctx.AbortWithStatusJSON(http.StatusOK, stderr.ErrNoToken.Struct()) + return + } + if token != "" { // 带token说明用户已登录,则解析并校验其token + uid, code = auth(token) + if code != stderr.Success { + // 白名单接口允许客户端携带失效token继续按未登录态访问,例如用户被删除后重新登录。 + if inWhiteList { + ctx.Set(constant.CtxUserID, 0) + return + } + //兼容用户封禁时调用客服接口 + if code == stderr.ErrAccessForbid && ctx.Request.URL.Path == "/api/app/im/newSign" { + ctx.Set(constant.CtxUserID, uid) + return + } + handleErrCode(ctx, uid, code) + return + } + ctx.Set(constant.CtxUserID, uid) + return + } + ctx.Set(constant.CtxUserID, 0) +} + +// 处理返回的错误码 +func handleErrCode(ctx *gin.Context, uid uint64, code stderr.Code) { + var data interface{} + switch code { + case stderr.ErrAccessForbid: + var reason string + u, err := usermod.FindUserByUID(uid) + if err == nil && u != nil { + reason = u.LockReason + } + data = gin.H{"uid": uid, "reason": reason} + } + common.ServeJSON(ctx, code, data) + ctx.Abort() +} + +func getWebTokenSecret() string { + return appg.Conf.Base.WebJwtKey +} + +// https://tools.ietf.org/html/rfc7519#section-4.1 +// See examples for how to use this with your own claim types +type WebClaims struct { + //用户UID + UID uint64 `json:"uid,omitempty"` + + //The "exp" (expiration time) claim identifies the expiration time on + //or after which the JWT MUST NOT be accepted for processing. The + //processing of the "exp" claim requires that the current date/time + //MUST be before the expiration date/time listed in the "exp" claim. + //Implementers MAY provide for some small leeway, usually no more than + //a few minutes, to account for clock skew. Its value MUST be a number + //containing a NumericDate value. Use of this claim is OPTIONAL. + ExpiresAt int64 `json:"exp,omitempty"` + + //The "iat" (issued at) claim identifies the time at which the JWT was + //issued. This claim can be used to determine the age of the JWT. Its + //value MUST be a number containing a NumericDate value. Use of this + //claim is OPTIONAL. + IssuedAt int64 `json:"iat,omitempty"` +} + +func GenWebToken(claims WebClaims) (string, error) { + secret := getWebTokenSecret() + m, _ := common.JSONStruct2Map(claims) + token, err := crypt.CreateToken(secret, m) + if err != nil { + log.Error("GenWebToken error", log.Any("claims", claims), log.E(err)) + } + return token, err +} + +// ParseWebClaims +// 如果token过期会返回error +func ParseWebClaims(token string) (WebClaims, error) { + secret := getWebTokenSecret() + claims, err := crypt.ParseToken(secret, token) + if err != nil { + return WebClaims{}, err + } + webClaims := WebClaims{} + return webClaims, common.Map2JSONStruct(&webClaims, claims) +} diff --git a/app/middleware/authuser/authuser_test.go b/app/middleware/authuser/authuser_test.go new file mode 100644 index 0000000..6ed320e --- /dev/null +++ b/app/middleware/authuser/authuser_test.go @@ -0,0 +1,30 @@ +package authuser + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestSignedTranscodePullEndpointDoesNotRequireUserToken(t *testing.T) { + gin.SetMode(gin.TestMode) + router := gin.New() + router.Use(Auth) + router.GET("/api/app/vid/transcode/m3u8/*source", func(ctx *gin.Context) { + ctx.Status(http.StatusNoContent) + }) + + req := httptest.NewRequest( + http.MethodGet, + "/api/app/vid/transcode/m3u8/laosiji/m3m/source.m3u8", + nil, + ) + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, req) + + if recorder.Code != http.StatusNoContent { + t.Fatalf("transcode pull endpoint required an App token: status=%d body=%s", recorder.Code, recorder.Body.String()) + } +} diff --git a/app/middleware/datacenter/datacenter.go b/app/middleware/datacenter/datacenter.go new file mode 100644 index 0000000..8c1c5fe --- /dev/null +++ b/app/middleware/datacenter/datacenter.go @@ -0,0 +1,123 @@ +package datacenter + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/models/v/usermod" + + "github.com/gin-gonic/gin" +) + +type OnlineStatData struct { + UserId int64 `bson:"userId" json:"userId"` // 用户Id + DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码 + WatchCount int64 `json:"watchCount,omitempty" bson:"watchCount"` // 观看次数 + WatchTime int64 `json:"watchTime,omitempty" bson:"watchTime"` // 观看时长 + AdClick int64 `json:"adClick,omitempty" bson:"adClick"` // 广告点击 + AppClick int64 `json:"appClick,omitempty" bson:"appClick"` // APP点击 + RequestCount int64 `json:"requestCount,omitempty" bson:"requestCount"` // 请求次数 + RegisterAt time.Time `json:"registerAt,omitempty" bson:"registerAt"` // 注册时间(只统计最近一个月注册的渠道用户?) + StatIndex int64 `json:"statIndex" bson:"statIndex"` // 统计数据更新时钟 + SendIndex int64 `json:"sendIndex" bson:"sendIndex"` // 上次发送时钟 + SendAt time.Time `json:"sendAt" bson:"sendAt"` // 上次发送时间 +} + +// OnlineStat 统计用户在线时间和接口访问次数 +func OnlineStat(ctx *gin.Context) { + now := time.Now() + var statInfo OnlineStatData + + // 查询用户 + userId, err := common.GetUID(ctx) + if userId <= 0 || err != nil { + return + } + + userOnlineKey := fmt.Sprintf("mdsmOnline:%s:%d", now.Format("20060102"), userId) + onlineValue, err := appg.Redis.Get(userOnlineKey) + if err != nil { + log.Error("OnlineStat", log.E(err)) + return + } + + if onlineValue != nil && *onlineValue != "" { + err = json.Unmarshal([]byte(*onlineValue), &statInfo) + if err != nil { + log.Error("OnlineStat", log.E(err)) + return + } + + } else { + user, serr := usermod.FindUserByUID(userId) + if serr != nil { + return + } + + statInfo = OnlineStatData{ + UserId: int64(userId), + DistrictCode: user.DistrictCode, + RegisterAt: user.CreatedAt, + WatchCount: 0, + WatchTime: 0, + AdClick: 0, + AppClick: 0, + RequestCount: 0, + StatIndex: 0, + SendIndex: 0, + SendAt: now, + } + } + + // 统计数据 + StatApiHandle(ctx, &statInfo) + + // 更新数据库埋点规则 () 后期只统计渠道用户 + if (statInfo.StatIndex < 2) || //前1次直接发送 + (statInfo.StatIndex-statInfo.SendIndex) > 50 || //每30次合并发送 + statInfo.SendAt.Before(time.Now().Add(-time.Second*40)) { // 超过40秒 + + // 清理数据 + statInfo.WatchCount = 0 + statInfo.AdClick = 0 + statInfo.AppClick = 0 + statInfo.RequestCount = 0 + statInfo.SendAt = now + statInfo.SendIndex = statInfo.StatIndex + } + + marshalStr, err := json.Marshal(statInfo) + if err != nil { + log.Error("OnlineStat", log.E(err)) + return + } + _ = appg.Redis.Set(userOnlineKey, marshalStr, 24*time.Hour) +} + +// StatApiHandle 统计Api和关键接口次数 +func StatApiHandle(ctx *gin.Context, statInfo *OnlineStatData) { + statInfo.StatIndex += 1 + statInfo.RequestCount += 1 + + // 播放接口 + if strings.HasPrefix(ctx.Request.URL.Path, "/api/app/vid/h5/m3u8/") || + strings.HasPrefix(ctx.Request.URL.Path, "/api/app/vid/m3u8/") || + strings.HasPrefix(ctx.Request.URL.Path, "/api/app/vid/h5/light/m3u8/") { + statInfo.WatchCount += 1 + statInfo.StatIndex += 1 + } + + // 广告点击 + if strings.Contains(ctx.Request.URL.Path, "/api/app/ads/click") || + strings.Contains(ctx.Request.URL.Path, "/api/app/recreation/click") { + statInfo.AdClick += 1 + statInfo.StatIndex += 1 + } + + // 其他关键监控指标 +} diff --git a/app/middleware/ip/ip.go b/app/middleware/ip/ip.go new file mode 100644 index 0000000..85880d1 --- /dev/null +++ b/app/middleware/ip/ip.go @@ -0,0 +1,13 @@ +package ip + +import ( + "91porn-server/common/constant" + + "github.com/gin-gonic/gin" +) + +// RealIP 获取真实IP +func RealIP(c *gin.Context) { + ip := c.ClientIP() + c.Set(constant.CtxIP, ip) +} diff --git a/app/middleware/ipblock/ipblock.go b/app/middleware/ipblock/ipblock.go new file mode 100644 index 0000000..906f441 --- /dev/null +++ b/app/middleware/ipblock/ipblock.go @@ -0,0 +1,103 @@ +package ipblock + +import ( + "net/http" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/stderr" + "91porn-server/models/v/ipblockmod" + + "github.com/gin-gonic/gin" +) + +func IPBlock(blockType string) gin.HandlerFunc { + return func(ctx *gin.Context) { + if !appg.ShouldEnforceIPRateLimit() { + return + } + ip := common.GetIP(ctx) + key := redisconst.IPBlockKey(blockType) + if count, err := appg.Redis.SCard(key); count == 0 || err != nil { + ipdata, err := ipblockmod.AggregateForIPArray(blockType) + if err == nil && len(ipdata.IPS) > 0 { + // 仅在集合为空时回源重建:直接 SAdd,去掉 Del 以消除"先删后加"的空窗期漏拦; + // TTL 设在 key 自身(此前误用 IPBlockKey(key) 二次包裹成 ip:block:ip:block:xxx, + // 导致真实集合永不过期、永不回源刷新) + _, _ = appg.Redis.SAdd(key, ipdata.IPS) + _, _ = appg.Redis.ExpireKey(key, redisconst.IPBlockExpire) + } + } + isBlock, err := appg.Redis.SISMember(key, ip) + //表示当前IP在限制名单中 + if isBlock && err == nil { + ctx.AbortWithStatusJSON(http.StatusOK, gin.H{ + "code": stderr.ErrReqForbidden, + "msg": stderr.ErrReqForbidden.Msg(), + "tip": stderr.ErrReqForbidden.Tip(), + }) + return + } + } +} + +func IPAutoBlock(blockType string, duration time.Duration, count int64) gin.HandlerFunc { + return func(ctx *gin.Context) { + if !appg.ShouldEnforceIPRateLimit() { + return + } + ip := common.GetIP(ctx) + if ip != "" { + key := redisconst.IPAutoBlockKey(blockType, ip) + cnt := appg.Redis.Incr(key) + if cnt == 1 { + _, _ = appg.Redis.ExpireKey(key, duration) + } + if cnt >= count { + switch blockType { + case constant.Register: + ctx.AbortWithStatusJSON(http.StatusOK, gin.H{ + "code": stderr.ErrLoginTooFrequently, + "msg": stderr.ErrLoginTooFrequently.Msg(), + "tip": stderr.ErrLoginTooFrequently.Tip(), + }) + default: + ctx.AbortWithStatusJSON(http.StatusOK, gin.H{ + "code": stderr.ErrReqForbidden, + "msg": stderr.ErrReqForbidden.Msg(), + "tip": stderr.ErrReqForbidden.Tip(), + }) + } + return + } + } + } +} + +func CountAutoBlock(blockType string, duration time.Duration, count int64) gin.HandlerFunc { + return func(ctx *gin.Context) { + var key string + switch blockType { + case constant.BlockComment: + uid, err := common.GetUID(ctx) + if err == nil { + key = redisconst.AutoBlockKey(blockType, uid) + } + } + cnt := appg.Redis.Incr(key) + if cnt == 1 { + _, _ = appg.Redis.ExpireKey(key, duration) + } + if cnt > count { + ctx.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{ + "code": stderr.ErrReqForbidden, + "msg": stderr.ErrReqForbidden.Msg(), + "tip": stderr.ErrReqForbidden.Tip(), + }) + return + } + } +} diff --git a/app/middleware/limitHandler/limit.go b/app/middleware/limitHandler/limit.go new file mode 100644 index 0000000..42dc31b --- /dev/null +++ b/app/middleware/limitHandler/limit.go @@ -0,0 +1,276 @@ +package limitHandler + +import ( + "errors" + "fmt" + "strconv" + "sync" + "time" + + "91porn-server/app/appg" + "91porn-server/app/service/m3u8ticket" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/usermod" + + "github.com/gin-gonic/gin" +) + +type RequestCountTime struct { + //请求的词数 + count int64 + //最后的访问时间 + lastTime int64 +} + +// 限制用户的每秒的请求的次数,1s10次的频率 +func FilterRequest(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + key := redisconst.RechargeLimtKey(uid) + Expire := redisconst.RechargeLimtKeyExpire() + if appg.Redis.IsExist(key) { + common.ServeJSON(ctx, stderr.PayBusy, err) + ctx.Abort() + return + } + go func() { _ = appg.Redis.Set(key, "-", Expire) }() +} + +// FilterRequestLimit 限制用户的每秒的请求的次数,1s1次的频率 +func FilterRequestLimit(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + key := redisconst.ReqLimtKey(uid, ctx.Request.URL.Path) + Expire := redisconst.ReqLimtKeyExpire() + if appg.Redis.IsExist(key) { + common.ServeJSON(ctx, stderr.PayBusy, err) + ctx.Abort() + return + } + go func() { _ = appg.Redis.Set(key, "-", Expire) }() +} + +// FilterRequestByUser 限制用户短时间内的请求次数,limit决定限制时间 +func FilterRequestByUser(keyFmt string, limit time.Duration, errCode stderr.Code) gin.HandlerFunc { + return func(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + ctx.Abort() + return + } + key := fmt.Sprintf(keyFmt, uid) + if appg.Redis.IsExist(key) { + common.ServeJSON(ctx, errCode, err) + ctx.Abort() + return + } + go func() { _ = appg.Redis.Set(key, "-", limit) }() + } +} + +// FilterSMSCaptchaByIP 限制单 IP 每自然日发送短信验证码的次数,超出 smsIPLimit 配置后拒绝 +func FilterSMSCaptchaByIP(ctx *gin.Context) { + if !appg.ShouldEnforceIPRateLimit() { + return + } + ip := ctx.GetString(constant.CtxIP) + if ip == "" { + ip = ctx.ClientIP() + } + key := redisconst.SMSCaptchaIPKey(ip) + cnt := appg.Redis.Incr(key) + if cnt == 1 { + _, _ = appg.Redis.ExpireKey(key, redisconst.SMSCaptchaIPExpire()) + } + limit := appg.Conf.Limit.SMSIPLimit + if limit <= 0 { + limit = 10 + } + if cnt > limit { + log.Warn("sms captcha ip rate limit exceeded", log.Any("ip", ip)) + common.ServeJSON(ctx, stderr.VisitLimit, nil) + ctx.Abort() + return + } +} + +// FilterByIP wraps an IP-based Gin limiter and bypasses it only in the test +// environment. Other environments retain the original fail-safe behavior. +func FilterByIP(handler gin.HandlerFunc) gin.HandlerFunc { + return func(ctx *gin.Context) { + if !appg.ShouldEnforceIPRateLimit() { + return + } + handler(ctx) + } +} + +// FilterSMSCaptchaByUID 限制单用户发送短信验证码:1分钟内只能发1次,每自然日最多5次 +func FilterSMSCaptchaByUID(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + ctx.Abort() + return + } + // 1分钟冷却:key 存在则拒绝 + cooldownKey := redisconst.SMSCaptchaUIDCooldownKey(uid) + if appg.Redis.IsExist(cooldownKey) { + log.Warn("sms captcha uid cooldown", log.Any("uid", uid)) + common.ServeJSON(ctx, stderr.VisitLimit, nil) + ctx.Abort() + return + } + // 每日上限:计数器超过 5 次拒绝 + dailyKey := redisconst.SMSCaptchaUIDKey(uid) + cnt := appg.Redis.Incr(dailyKey) + if cnt == 1 { + _, _ = appg.Redis.ExpireKey(dailyKey, redisconst.SMSCaptchaIPExpire()) + } + if cnt > 5 { + log.Warn("sms captcha uid daily limit exceeded", log.Any("uid", uid)) + common.ServeJSON(ctx, stderr.VisitLimit, nil) + ctx.Abort() + return + } + // 通过后写入冷却 key,1分钟内不能再发 + go func() { _ = appg.Redis.Set(cooldownKey, "-", redisconst.SMSCaptchaUIDCooldown) }() +} + +func FilterRequestByUserAndTerminal(keyFmt string, limit time.Duration, errCode stderr.Code) gin.HandlerFunc { + return func(ctx *gin.Context) { + ua, err := common.GetUA(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.BadUA, err) + ctx.Abort() + return + } + if ua.IsH5 == "1" { + return + } + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + ctx.Abort() + return + } + key := fmt.Sprintf(keyFmt, uid) + if appg.Redis.IsExist(key) { + common.ServeJSON(ctx, errCode, err) + ctx.Abort() + return + } + go func() { _ = appg.Redis.Set(key, "-", limit) }() + } +} + +const ( + limitCount = 7 + limitExpire = 3 + mapLength = 5000 +) + +var mu = sync.Mutex{} +var limitMap = make(map[string]*RequestCountTime) + +func Limit(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + key := ctx.Request.RequestURI + strconv.FormatInt(int64(uid), 10) + t1 := time.Now() + mu.Lock() + defer mu.Unlock() + if len(limitMap) > mapLength { + limitMap = make(map[string]*RequestCountTime) + } + v, ok := limitMap[key] + if !ok { + limitMap[key] = &RequestCountTime{ + lastTime: t1.Unix(), + } + v = limitMap[key] + } + if t1.Unix() > v.lastTime+limitExpire { + //超出计时周期 + v.lastTime = t1.Unix() + v.count = 0 + } + v.count++ + if v.count > limitCount { + log.Warn("abnormal user request too frequent", log.Any("uid", uid)) + err = errors.New("user request too frequent") + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } +} + +const ( + m3u8HourlyFreeLimit = 1800 // 普通用户每小时 m3u8 请求上限 + m3u8HourlyVipLimit = 3600 // 会员每小时 m3u8 请求上限 + // 命中限流时下发的提示 m3u8(替换 *source,不报错):普通用户与会员分别下发不同提示片 + m3u8LimitFallbackNormal = "/bktadminup/sp/zp/hb/7a/0q/860513c4947143b29083160ec41588b5.m3u8" // 普通用户 + m3u8LimitFallbackVip = "/bktadminup/sp/26/gx/7n/cr/359754435033419a95036ab33e520719.m3u8" // 会员 +) + +// M3u8HourlyLimit 限制每个用户每小时请求 m3u8 的次数:普通用户 50 次,会员 200 次。 +// 超过免费次数后仅会员可继续,且总量不超过会员上限。 +func M3u8HourlyLimit(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + if err != nil || uid == 0 { + return // 取不到用户则不限流(交由上游鉴权处理) + } + key := redisconst.M3u8HourlyCountKey(uid) + cnt := appg.Redis.Incr(key) + if cnt == 1 { + _, _ = appg.Redis.ExpireKey(key, redisconst.M3u8HourlyCountExpire) + } + if cnt <= m3u8HourlyFreeLimit { + return + } + isVip := isM3u8VipUser(uid) + // 命中限流:非会员超普通上限、或会员超会员上限。不报错,改写 *source 为对应提示 m3u8 交后续 handler 下发 + if !isVip || cnt > m3u8HourlyVipLimit { + log.Warn("m3u8 hourly rate limit exceeded", log.Any("uid", uid), log.Any("count", cnt), log.Any("vip", isVip)) + fallback := m3u8LimitFallbackNormal + if isVip { + fallback = m3u8LimitFallbackVip + } + // 按最新规则(带票 + JHA 前缀)签发提示片地址,使严格验票路由(h5/m3u8)能识别并下发对应提示片, + // 不被验票逻辑当作非法票据覆盖;未开启票据时 BuildSignedURL 返回空,退回明文兜底(此时下游也不验票)。 + ua := "" + if u, uaErr := common.GetUA(ctx); uaErr == nil { + ua = u.UserAgent + } + if signed := m3u8ticket.BuildSignedURL(uid, fallback, common.GetIP(ctx), ua, true, false); signed != "" { + fallback = signed + } + for i := range ctx.Params { + if ctx.Params[i].Key == "source" { + ctx.Params[i].Value = fallback + } + } + return + } +} + +func isM3u8VipUser(uid uint64) bool { + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return false + } + return u.IsVIP(time.Now()) +} diff --git a/app/middleware/requestEncrypt/decrypt.go b/app/middleware/requestEncrypt/decrypt.go new file mode 100644 index 0000000..6b8dd03 --- /dev/null +++ b/app/middleware/requestEncrypt/decrypt.go @@ -0,0 +1,92 @@ +package requestEncrypt + +import ( + "bytes" + _ "embed" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/url" + "strings" + + "91porn-server/common/constant" + "91porn-server/common/crypt" + "91porn-server/common/log" + "91porn-server/middleware/ua" + + "github.com/gin-gonic/gin" +) + +var ( + // 白名单,不需要验证token的api + // 在新的Auth中间件逻辑下,配置了白名单后,用户未登录,context中的uid会被设为0;否则,会绑定用户真实uid。 + // 需要白名单中API的handler自己对用户登录状态进行判断。 + whitelist = map[string]bool{ + "/swagger": true, + "/api/app/vid/m3u8": true, //前端测试 下载m3u8 添加到白名单 ,生产环境中需要从白名单中剔除 + } +) + +//go:embed public.pem +var PubKey []byte + +type Req struct { + Data string `json:"data" form:"data"` + Sign string `json:"sign" form:"sign"` +} + +// ReqDecode 解密 +func ReqDecode(ctx *gin.Context) { + var err error + val, exists := ctx.Get(constant.CtxUA) + if !exists { + return + } + for url, ok := range whitelist { + if ok && (strings.HasPrefix(ctx.Request.URL.Path, url) || strings.HasPrefix(ctx.FullPath(), url)) { + return + } + } + u, ok := val.(ua.UA) + if !ok { + return + } + if u.Terminal != constant.TerminalH5 && u.Terminal != constant.TerminalWeb { //h5 和纯web都加密 + return + } + var data Req + if err = ctx.ShouldBind(&data); err != nil { + return + } + if data.Data == "" { + return + } + dataBaseBytes, _ := base64.StdEncoding.DecodeString(data.Data) + dataBytes, err := crypt.AesDecrypt(string(dataBaseBytes), "BxJand%xf5h3sycH") + if err != nil { + log.Warn("ReqDecode is error", log.Any("data", data.Data), log.Any("ua", u)) + } + if ctx.Request.Method == "GET" { + m := make(map[string]interface{}) + dec := json.NewDecoder(strings.NewReader(dataBytes)) + dec.UseNumber() // 保留数字原始表示,避免 float64 精度丢失/科学计数法 + _ = dec.Decode(&m) + values := url.Values{} + for k, v := range m { + // 解密后的 JSON 值可能是数字、布尔等非字符串类型,不能直接断言为 string,否则会 panic + switch val := v.(type) { + case string: + values.Add(k, val) + case nil: + values.Add(k, "") + default: + values.Add(k, fmt.Sprintf("%v", val)) + } + } + ctx.Request.URL.RawQuery = values.Encode() + ctx.Request.Form = values + return + } + ctx.Request.Body = io.NopCloser(bytes.NewBuffer([]byte(dataBytes))) +} diff --git a/app/middleware/requestEncrypt/encrypt.go b/app/middleware/requestEncrypt/encrypt.go new file mode 100644 index 0000000..a9a01ec --- /dev/null +++ b/app/middleware/requestEncrypt/encrypt.go @@ -0,0 +1 @@ +package requestEncrypt diff --git a/app/middleware/requestEncrypt/private.pem b/app/middleware/requestEncrypt/private.pem new file mode 100644 index 0000000..daf9919 --- /dev/null +++ b/app/middleware/requestEncrypt/private.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAMD+4d71tbi9jlB1 +kfEtibr3M/phrmzIHtVPoiLlcOmhX1Z29WsHpZDEA/B48p09ksQ3EQrVJfo3Xe1J +V8KM0UiI8RcoA/nQUdJBnFNCt2xH1LXKYFYH0hIl5nhewUsnr8tdCV0aHzPO1Q3d +SF1mQ6YrJvTVWuz5BBz3NisU14kNAgMBAAECgYBAIHDMtLf8+n8fHPGxQYBSL3GF +8I8UdipIln05OyOZfZVAFabWOWQ6BeeJL6btuFfb+rAe+VP1IBCFl6kha8jdzQjr +48mujwFKSwtFUHJukTbG/pgQWjnblSPcK9XStu5caiwGG9ehBdPgKurMvr/5w0NL +VtYr22fVlfpUjHTDwQJBAO71ziG1Goz2dy1lZvb6m1MHx2vsBbWnoFzGL6LQ+Mis +FB6/DkFS5ayyGkubqC9HLCzvUzl/uO/F9F9J704WIDsCQQDOwf+0m8a1WGzNnN4N +ApyfV3ZMyMgcoFAgyS4HJ3ODrkfKIp3BUAKMWo7DZDNQy0h8RuIx8J1aFMP8vmb1 +e29XAkBOE2gvYUH0js7vuTMEPWuknGUPIQXYjZV62pJT611uC7NjPF+G/nPmkia/ +T3OLRwDrCaMypUWiqhYnUDvtUpNjAkAoTLvwkiRrGG8srJTDaGzqyftu//uxSLYV +jeEA1/m2AvRPO+wPz+6PON92ykMOTnE0eASCw2mIM1/4FYyv8H1FAkAWhNRdcXbB +yuvFbO7xSsstrTlyq6AqHXfCwT+NYRsa7eAQucOMQUY+6zKXsZ2WzvoiN+y7MbqJ +VheW/ortGjBA +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/app/middleware/requestEncrypt/privatePKCs1.pem b/app/middleware/requestEncrypt/privatePKCs1.pem new file mode 100644 index 0000000..b8cc071 --- /dev/null +++ b/app/middleware/requestEncrypt/privatePKCs1.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQDA/uHe9bW4vY5QdZHxLYm69zP6Ya5syB7VT6Ii5XDpoV9WdvVr +B6WQxAPwePKdPZLENxEK1SX6N13tSVfCjNFIiPEXKAP50FHSQZxTQrdsR9S1ymBW +B9ISJeZ4XsFLJ6/LXQldGh8zztUN3UhdZkOmKyb01Vrs+QQc9zYrFNeJDQIDAQAB +AoGAQCBwzLS3/Pp/HxzxsUGAUi9xhfCPFHYqSJZ9OTsjmX2VQBWm1jlkOgXniS+m +7bhX2/qwHvlT9SAQhZepIWvI3c0I6+PJro8BSksLRVBybpE2xv6YEFo525Uj3CvV +0rbuXGosBhvXoQXT4CrqzL6/+cNDS1bWK9tn1ZX6VIx0w8ECQQDu9c4htRqM9nct +ZWb2+ptTB8dr7AW1p6Bcxi+i0PjIrBQevw5BUuWsshpLm6gvRyws71M5f7jvxfRf +Se9OFiA7AkEAzsH/tJvGtVhszZzeDQKcn1d2TMjIHKBQIMkuBydzg65HyiKdwVAC +jFqOw2QzUMtIfEbiMfCdWhTD/L5m9XtvVwJAThNoL2FB9I7O77kzBD1rpJxlDyEF +2I2VetqSU+tdbguzYzxfhv5z5pImv09zi0cA6wmjMqVFoqoWJ1A77VKTYwJAKEy7 +8JIkaxhvLKyUw2hs6sn7bv/7sUi2FY3hANf5tgL0TzvsD8/ujzjfdspDDk5xNHgE +gsNpiDNf+BWMr/B9RQJAFoTUXXF2wcrrxWzu8UrLLa05cqugKh13wsE/jWEbGu3g +ELnDjEFGPusyl7Gdls76IjfsuzG6iVYXlv6K7RowQA== +-----END RSA PRIVATE KEY----- diff --git a/app/middleware/requestEncrypt/public.pem b/app/middleware/requestEncrypt/public.pem new file mode 100644 index 0000000..a7c6d98 --- /dev/null +++ b/app/middleware/requestEncrypt/public.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA/uHe9bW4vY5QdZHxLYm69zP6 +Ya5syB7VT6Ii5XDpoV9WdvVrB6WQxAPwePKdPZLENxEK1SX6N13tSVfCjNFIiPEX +KAP50FHSQZxTQrdsR9S1ymBWB9ISJeZ4XsFLJ6/LXQldGh8zztUN3UhdZkOmKyb0 +1Vrs+QQc9zYrFNeJDQIDAQAB +-----END PUBLIC KEY----- diff --git a/app/middleware/vercheck/vercheck.go b/app/middleware/vercheck/vercheck.go new file mode 100644 index 0000000..a54ecc6 --- /dev/null +++ b/app/middleware/vercheck/vercheck.go @@ -0,0 +1,66 @@ +package vercheck + +import ( + "strings" + + "91porn-server/app/service/versionser" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/version" + "91porn-server/models/v/versionmod" + + "github.com/gin-gonic/gin" +) + +// 检查版本更新 +func VerCheck() gin.HandlerFunc { + return func(ctx *gin.Context) { + ua, err := common.GetUA(ctx) + if err != nil || ua.SysType == "" { + log.Warn("get ua error or us is empty", log.E(err)) + return + } + var sys string + if strings.Contains(strings.ToLower(ua.SysType), constant.DeviceTypeAndroid) { + sys = constant.DeviceTypeAndroid + } + if strings.Contains(strings.ToLower(ua.SysType), constant.DeviceTypeIOS) { + sys = constant.DeviceTypeIOS + } + //查找最新版本信息 + ver, err := versionmod.FindVersionExcluedBuildId(sys) + if err != nil || ver.ID.IsZero() { + log.Warn("check version get version error", log.E(err)) + return + } + //最新 为强制更新 需要强制更新 + vn, _ := version.New(ver.VersionName) + vc, _ := version.New(ua.Ver) + if vn.GT(vc) && versionser.HandleVersion(ua.Ver, &ver) && ver.ForcedUpdate { + common.ServeJSON(ctx, stderr.ErrVersionUpdate, ver) + ctx.Abort() + return + } + } +} + +// VerH5Check 检查h5版本更新 +func VerH5Check(ctx *gin.Context) { + ua, err := common.GetUA(ctx) + if err != nil { + log.Warn("get ua error", log.E(err)) + return + } + if !strings.Contains(strings.ToLower(ua.SysType), constant.DeviceTypeH5) { + return + } + //查找最新版本信息 + ver, err := versionmod.FindVersion(constant.DeviceTypeH5) + if err != nil { + log.Warn("check version get version error", log.E(err)) + return + } + ctx.Writer.Header().Set("Cur-Ver", ver.VersionName) +} diff --git a/app/middleware/verfyparam/verfyparam.go b/app/middleware/verfyparam/verfyparam.go new file mode 100644 index 0000000..2e44b18 --- /dev/null +++ b/app/middleware/verfyparam/verfyparam.go @@ -0,0 +1,194 @@ +package verfyparam + +import ( + "91porn-server/common/timeutil/timerange" + "strconv" + "strings" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/crypt" + "91porn-server/common/log" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +var ( + // 白名单,不需要验证token的api + whitelist = map[string]bool{ + "/swagger": true, + "/api/app/ping/check": true, + "/api/app/ping/pass": true, + "/api/app/daichong": true, + "/api/app/vid/sec": true, + "/api/app/vid/new/sec": true, + "/api/app/vid/lsjsec": true, + // H.265 云转码使用 Handler 自己的路径绑定、限时 HMAC 鉴权。 + "/api/app/vid/transcode/m3u8": true, + "/api/app/im/whiteSign": true, + "/api/app/im/sign": true, + "/api/app/vid/upload": true, + } +) + +// 内部使用 +const InnerSecret string = "F^hgNT%MBpai+3qkz05NZtB5Ts@a_gRekEHJ3@KcF)T5>5.U7JLbx3!P1nxt#LhV" + +type Sign struct { + Nonce string `json:"nonce"` + TimeStamp string `json:"timestamp"` + Path string `json:"path"` + UserAgent string `json:"userAgent"` + Token string `json:"token"` +} + +func VerifyReplayAttackRequest(replayAttack appg.ReplayAttackConfig) gin.HandlerFunc { + return func(ctx *gin.Context) { + if !replayAttack.Enable { + return + } + flag := common.IsGTESpecifyVer(ctx, constant.Ver3_6_0) + if !flag { + return + } + for url, ok := range whitelist { + if ok && strings.HasPrefix(ctx.Request.URL.Path, url) { + return + } + } + var sign, timestamp, nonce string + ua := ctx.Request.UserAgent() + token := ctx.GetHeader("Authorization") + apiKey := ctx.GetHeader("x-api-key") + timestamp, sign, nonce = getXapiKey(apiKey) + if sign == InnerSecret { + return + } + t, _ := strconv.ParseInt(timestamp, 10, 64) + if t == 0 || sign == "" || nonce == "" { + ctx.Abort() + common.ServeJSON(ctx, stderr.ErrInvalidRequestReplayAttack, nil) + log.Info("VerifyReplayAttackRequest bad request UA", log.Any("apiKey", apiKey), log.Any("ip", ctx.ClientIP())) + return + } + nowTime := time.Now().UTC().Unix() + if nowTime > t+replayAttack.WindowDurationSeconds || nowTime < t-replayAttack.WindowDurationSeconds { + ctx.Abort() + common.ServeJSON(ctx, stderr.ErrInvalidRequestReplayAttack, nil) + log.Info("VerifyReplayAttackRequest bad request time window", log.Any("apiKey", apiKey), log.Any("ip", ctx.ClientIP())) + return + } + redisKey := redisconst.ReplayNonceKey(nonce) + if appg.Redis.IsExist(redisKey) { + ctx.Abort() + common.ServeJSON(ctx, stderr.ErrInvalidRequestReplayAttack, nil) + log.Info("VerifyReplayAttackRequest bad request dup nonce", log.Any("apiKey", apiKey), log.Any("ip", ctx.ClientIP())) + return + } + s := Sign{ + Nonce: nonce, + TimeStamp: timestamp, + Path: ctx.Request.URL.Path, + UserAgent: ua, + Token: token, + } + hmac := GeneratorSign(s, replayAttack.Key) + if hmac != sign { + ctx.Abort() + common.ServeJSON(ctx, stderr.ErrInvalidRequestReplayAttack, nil) + return + } + if err := appg.Redis.Set(redisKey, 1, 2*time.Duration(replayAttack.WindowDurationSeconds)*time.Second); err != nil { + log.Info("VerifyReplayAttackRequest Save Redis Err", log.Any("key", redisKey), log.E(err)) + } + } +} + +func CheckReplayAttackRequest() gin.HandlerFunc { + return func(ctx *gin.Context) { + if !appg.ShouldEnforceIPRateLimit() { + return + } + for url, ok := range whitelist { + if ok && strings.HasPrefix(ctx.Request.URL.Path, url) { + return + } + } + ua := ctx.Request.UserAgent() + ip := common.GetIP(ctx) + var now = time.Now() + var recentSecond = timerange.RecentSecond(now, 10) + checkApiKey := redisconst.CheckApiKey(ip, ctx.Request.URL.Path, recentSecond) + + if ua == "" { + ctx.Abort() + common.ServeJSON(ctx, stderr.ErrInvalidRequestReplayAttack, nil) + log.InfoX(ctx, "CheckReplayAttackRequest bad request UA", log.Any("apiKey", checkApiKey), log.Any("ip", ip)) + return + } + data, err := appg.Redis.Get(checkApiKey) + if err != nil { + log.InfoX(ctx, "CheckReplayAttackRequest get Redis Err", log.Any("key", checkApiKey), log.E(err)) + } + if data == nil { + err := appg.Redis.Set(checkApiKey, "1", redisconst.CheckApiKeyExpire) + if err != nil { + log.InfoX(ctx, "CheckReplayAttackRequest Save Redis Err", log.Any("key", checkApiKey), log.E(err)) + } + } + + if data != nil { + currentValue, err := strconv.ParseInt(*data, 10, 64) + if err != nil { + log.InfoX(ctx, "CheckReplayAttackRequest strconv.ParseInt Err", log.Any("key", checkApiKey), log.E(err)) + } + if currentValue > 10 { + ctx.Abort() + common.ServeJSON(ctx, stderr.ErrInvalidRequestReplayAttack, nil) + log.InfoX(ctx, "CheckReplayAttackRequest bad request", log.Any("currentValue", currentValue), log.Any("apiKey", checkApiKey), log.Any("ip", ip)) + return + } + currentValue += 1 + + setData := strconv.FormatInt(currentValue, 10) + err = appg.Redis.Set(checkApiKey, setData, redisconst.CheckApiKeyExpire) + if err != nil { + log.InfoX(ctx, "CheckReplayAttackRequest set Redis Err", log.Any("key", checkApiKey), log.E(err)) + } + } + ctx.Next() + } +} + +func getXapiKey(apiKey string) (timestamp, sign, nonce string) { + if apiKey != "" { + apiKeys := strings.Split(apiKey, ";") + for _, v := range apiKeys { + vss := strings.Split(strings.TrimSpace(v), "=") + if len(vss) < 2 { + log.Warn("x-api-key miss", log.Any("x-api-key", apiKey)) + continue + } + switch strings.TrimSpace(vss[0]) { + case "timestamp": + timestamp = strings.TrimSpace(vss[1]) + case "sign": + sign = strings.TrimSpace(vss[1]) + case "nonce": + nonce = strings.TrimSpace(vss[1]) + default: + log.Warn("x-api-key miss", log.Any("x-api-key", apiKey)) + } + } + } + return +} + +func GeneratorSign(s Sign, sec string) string { + str, _ := crypt.StructToStr(s) + return crypt.StrToHmacSha1(str, sec) +} diff --git a/app/middleware/verfyparam/verfyparam_test.go b/app/middleware/verfyparam/verfyparam_test.go new file mode 100644 index 0000000..4cb469a --- /dev/null +++ b/app/middleware/verfyparam/verfyparam_test.go @@ -0,0 +1,32 @@ +package verfyparam + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestTranscodeM3u8BypassesAppReplayValidationWithoutUserAgent(t *testing.T) { + gin.SetMode(gin.TestMode) + router := gin.New() + router.Use(CheckReplayAttackRequest()) + router.GET("/api/app/vid/transcode/m3u8/*source", func(ctx *gin.Context) { + ctx.Status(http.StatusNoContent) + }) + + req := httptest.NewRequest( + http.MethodGet, + "/api/app/vid/transcode/m3u8/laosiji/m3m/source.m3u8", + nil, + ) + req.Header.Del("User-Agent") + recorder := httptest.NewRecorder() + + router.ServeHTTP(recorder, req) + + if recorder.Code != http.StatusNoContent { + t.Fatalf("light m3u8 request was blocked: status=%d body=%s", recorder.Code, recorder.Body.String()) + } +} diff --git a/app/middleware/visitlog/visitlog.go b/app/middleware/visitlog/visitlog.go new file mode 100644 index 0000000..33f498d --- /dev/null +++ b/app/middleware/visitlog/visitlog.go @@ -0,0 +1,134 @@ +package visitlog + +import ( + "91porn-server/app/service/adser" + "91porn-server/common/constant" + "context" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/timeutil" + "91porn-server/middleware/ua" + "91porn-server/models/l/visitlogmod" + "91porn-server/models/v/usermod" + + "github.com/gin-gonic/gin" +) + +// LogMiddleware LogMiddleware +func Log(c *gin.Context) { + uid, err := common.GetUID(c) + if err != nil { + return + } + if uid == 0 { + // log.Info("uid is empty ") // + return + } + uas, err := common.GetUA(c) + if err != nil { + log.Warn("get ua error", log.E(err)) + return + } + now := time.Now() + //统计访问频率 + userVisitLogKey := redisconst.UserVisitLogsKey(now) + //2021-01-11去掉用户行为记录 + //registerBehaKey := redisconst.RegistBehaviorKey(uid) + //每日访问日志 + //common.Go(func() { + // redisVisit(uid, uas.SysType, now) + //}) + var exists bool + if val, err := appg.Redis.GetBit(userVisitLogKey, int64(uid)); err == nil { + exists = val == 1 + } + if !exists { //有值了就返回 + _ = appg.Redis.SetBit(userVisitLogKey, int64(uid), 1) + _, _ = appg.Redis.ExpireKey(userVisitLogKey, redisconst.UserVisitLogExpire(now)) + common.Go(func() { + ip := common.GetIP(c) + recordVisitLogFull(uid, ip, uas, now) + }) + common.Go(func() { + user, err := usermod.FindUserByUID(uid) + if err != nil { + log.Warn("find user by uid error", log.E(err)) + return + } + if user == nil { + return + } + _ = adser.UpsertAdStat(context.Background(), user, time.Now(), 0, 0, 0) + }) + } +} + +//func redisVisit(uid uint64, sysType string, now time.Time) { +// recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) +// redisKey := redisconst.VisitKey(recentMinute) +// _, _ = appg.Redis.SAdd(redisKey, usermod.VisitValue{UID: uid, SysType: sysType}.JsonString()) +// _, _ = appg.Redis.ExpireKey(redisKey, redisconst.VisitExpireMax) +//} + +func recordVisitLog(uid uint64, ip string, ua ua.UA, now time.Time) { + res, err := visitlogmod.UpsertUserVisit(&visitlogmod.VisitLog{ + SumDate: timeutil.BeginningOfDay(now), + UID: uid, + IP: ip, + SysType: common.HandleSysType(ua.SysType), + Ver: ua.Ver, + DevType: ua.DevType, + DevID: ua.DevID, + BuildID: ua.BuildID, + IsDeduction: true, + }) + if err != nil { + log.Warn("recordVisitLog UpsertUserVisit", log.E(err)) + return + } + if res != nil && res.UpsertedCount > 0 { + if err := usermod.ChangeVisit(uid, ua.Ver, ua.SysType, now); err != nil { + log.Warn("recordVisitLog UserSelector", log.E(err)) + } + } +} + +func recordVisitLogFull(uid uint64, ip string, ua ua.UA, now time.Time) { + u, _ := usermod.FindUserByUID(uid) + if u == nil { + recordVisitLog(uid, ip, ua, now) + return + } + res, err := visitlogmod.UpsertUserVisit(&visitlogmod.VisitLog{ + SumDate: timeutil.BeginningOfDay(now), + UID: uid, + IP: ip, + SysType: common.HandleSysType(ua.SysType), + Ver: ua.Ver, + DevType: ua.DevType, + DevID: ua.DevID, + BuildID: ua.BuildID, + IsDirect: u.IsDirect, + DistrictCode: u.DistrictCode, + RegisterTime: u.CreatedAt, + IsDeduction: false, + }) + if err != nil { + log.Warn("recordVisitLog UpsertUserVisit", log.E(err)) + return + } + if res != nil && res.UpsertedCount > 0 { + if err := usermod.ChangeVisit(uid, ua.Ver, ua.SysType, now); err != nil { + log.Warn("recordVisitLog UserSelector", log.E(err)) + } + } + // 游客信息不记录 + if ua.Terminal == constant.TerminalWeb { + return + } + +} diff --git a/app/middleware/watcher/watch.go b/app/middleware/watcher/watch.go new file mode 100644 index 0000000..04db2a1 --- /dev/null +++ b/app/middleware/watcher/watch.go @@ -0,0 +1,28 @@ +package watcher + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/v/usermod" + "net/http" + + "github.com/gin-gonic/gin" +) + +// 异常用户处理 +func WatcherUnusualUser() gin.HandlerFunc { + return func(ctx *gin.Context) { + uid, err := common.GetUID(ctx) + //检查当前用户注册得分情况 + if uid != 0 && err == nil { + user, _ := usermod.FindUserByUIDForNoCache(uid) + if user != nil { + if user.TrueScore == -1 || (user.TrueScore <= constant.UserLowestTrueScore && user.TrueScore > 0) { + ctx.AbortWithStatusJSON(http.StatusForbidden, stderr.UserIsException.Struct()) + return + } + } + } + } +} diff --git a/app/proto/ping.go b/app/proto/ping.go new file mode 100644 index 0000000..6c76086 --- /dev/null +++ b/app/proto/ping.go @@ -0,0 +1,282 @@ +package proto + +import ( + "time" + + "91porn-server/app/service/annouser" + "91porn-server/common/constant" + "91porn-server/models/v/advanceordermod" + "91porn-server/models/v/bannerjumpmod" + "91porn-server/models/v/jingangmod" + "91porn-server/models/v/paymentguidemod" + "91porn-server/models/v/sourcemod" + "91porn-server/models/v/systemmod" + "91porn-server/models/v/versionmod" +) + +// AdsInfo 广告列表 +type AdsInfo struct { + //公告id + ID string `json:"id" bson:"_id"` + //广告标题 + Title string `json:"title" bson:"title"` + //广告描述 + Description string `json:"description" bson:"description"` + //广告封面图 + Cover string `json:"cover" bson:"cover"` + //广告跳转地址 + Href string `json:"href" bson:"href"` + //广告位置 + Position int `json:"position" bson:"position" swaggertype:"integer"` + PositionName string `json:"positionName" bson:"positionName"` + //排序号 + SortCode int `json:"sortCode" bson:"sortCode"` + //封面尺寸 + CoverImgSize string `json:"coverImgSize" bson:"coverImgSize"` + WatchTime int `json:"watchTime"` // 视频广告时长 + +} + +type AdsRes struct { + AdsList []AdsInfo `json:"adsInfoList"` + //公告列表 + AnnounList interface{} `json:"announList"` + //系统公告(强制更新弃用) + AnnounInfo []*annouser.Annou `json:"-"` +} + +// 只返回弹窗 浮标 大背景 等 +type AdvanceSource struct { + PageBackground string `json:"pageBackground"` // 大背景 + PageVidBackground string `json:"pageVidBackground"` // 金币视频区背景 + ButtonBackground string `json:"buttonBackground"` // 支付按钮 立即预定 + ButtonWaitBackground string `json:"buttonWaitBackground"` // 支付按钮 已预定,未到支付时间 + ButtonProcBackground string `json:"buttonProcBackground"` // 支付按钮 已预定,去支付尾款 + EnterBgWait string `json:"enterBgWait"` // 入口按钮背景(未付款) + EnterBgProc string `json:"enterBgProc"` // 入口按钮背景(付尾款) + PopBgWait string `json:"popBgWait"` // 弹窗背景(未付款) + PopBgProc string `json:"popBgProc"` // 弹窗背景(付尾款) + Banner string `json:"banner"` // 立即预定 + BannerWait string `json:"bannerWait"` // 已预定,未到支付时间 + BannerProc string `json:"bannerProc"` // 已预定,去支付尾款 +} + +type SysInfo struct { + Ads AdsRes `json:"ads"` + //域名信息 + SourceList []*sourcemod.SourceRes `json:"sourceList"` + //接口域名 + Domain []string `json:"domain"` + //系统配置 + SystemConfigList []*systemmod.Config `json:"systemConfigList"` + //非会员每日可观看次数 + TotalWatch uint64 `json:"totalWatch"` + //楼凤H5(金主特制) + LouFengH5 string `json:"louFengH5"` + //版本信息 + Ver []*versionmod.VersionBody `json:"ver"` + JGArea []*jingangmod.JGModel `json:"jgArea"` //随机banner + RandomBanner string `json:"randomBanner"` + //2023新春抽奖 + //Active2023URL string `json:"active_2023_url"` + //长视频广告时间(单位: 秒) + AdsTimeLongVideo uint64 `json:"adsTimeLongVideo"` + //黑料h5地址 + HlH5URL string `json:"hlH5"` + LuckyDrawIcon string `json:"luckyDrawIcon"` // 抽奖浮动图标 + LuckyDrawH5 string `json:"luckyDrawH5"` // 抽奖H5链接 + AiUndressPrice int64 `json:"aiUndressPrice"` // AI脱衣价格 + AiImageToVideoPrice int64 `json:"aiImageToVideoPrice"` // AI图生视频金币 + AiTextToImagePrice int64 `json:"aiTextToImagePrice"` // AI绘图金币 + AiTextToNovelPrice int64 `json:"aiTextToNovelPrice"` // AI小说金币 + SendMsgPrice int64 `json:"sendMsgPrice"` // 发送私信价格 + AdvanceStatus advanceordermod.AdvanceStatus `json:"advanceStatus"` // 预售状态 + AiBubble []string `json:"aiBubble"` //AI女友气泡内容 + AiCharacterImg string `json:"aiCharacterImg"` // AI卡通人物形象 + Broadcast bool `json:"broadcast"` // 直播开关 + StoreIsOpen bool `json:"storeIsOpen"` // 是否开启店铺 + BackgroundTheme constant.UIThemeEnum `json:"backgroundTheme"` // 背景主题 0:默认主题,1:新春主题 + AdvancePage AdvanceSource `json:"advancePage"` // 预售页面资源 + HotSearchTerms []string `json:"hotSearchTerms"` // 热搜词 + SearchHintWord []string `json:"searchHintWord"` // 搜索提示词 + FestivalUi string `json:"festivalUi"` // 节日ui,如果为空值/默认 则展示默认的 + BannerJump map[int]bannerjumpmod.BannerJumpInfo `json:"bannerJump"` // 限时Banner活动 + BannerJumpList []bannerjumpmod.BannerJumpInfo `json:"bannerJumpList"` // 限时Banner活动列表 + AiGirlFriend bool `json:"aiGirlFriend"` // ai女友 + AiUndress bool `json:"aiUndress"` // ai脱衣 + AiImageChangeFace bool `json:"aiImageChangeFace"` // ai图片换脸 + AiVideoChangeFace bool `json:"aiVideoChangeFace"` // ai视频换脸 + DarkWebVipName string `json:"darkWebVipName"` // 暗网会员卡名字 + DarkWebVipId string `json:"darkWebVipId"` // 暗网会员卡id + PrivateZoneVipName string `json:"privateZoneVipName"` // 私密圈推荐会员卡名字 + PrivateZoneVipId string `json:"privateZoneVipId"` // 私密圈跳转会员卡id + RecommendVipIds []string `json:"recommendVipIds"` // 特邀会员卡id + ShortDramaCardID string `json:"shortDramaCardId"` // 短剧播放页默认会员卡id + ShortDramaEntryPopupEnabled bool `json:"shortDramaEntryPopupEnabled"` // 短剧首次进入悬浮窗总开关 + DefaultEntryPage string `json:"defaultEntryPage"` // 默认进入页面:home/drama + DefaultEntryAudience string `json:"defaultEntryAudience"` // 默认进入页面生效人群:new_user/all_users + NewbieSaleTime int64 `json:"newbieSaleTime"` // 新人特惠销售时间 单位 小时} + QmdlUrl string `json:"qmdlUrl"` // 全民代理地址 + AIMateH5 string `json:"aiMateH5"` // AI伴侣链接 + PersonalCenterBackground string `json:"personalCenterBackground"` // 个人中心背景图 + Video1 string `json:"video1"` // 创作技巧视频1 + Video2 string `json:"video2"` // 创作技巧视频2 + ReturnSaleVipIds []string `json:"returnSaleVipIds"` // 返场会员卡id + OldReturnSaleTime int64 `json:"oldReturnSaleTime"` // 返场销售时间 单位 小时 + ReportUrl string `json:"reportUrl"` // 数据上报地址 + ReportSDKConfig string `json:"reportSdkConfig"` // 数据上报SDK域名配置 + FreeMark bool `json:"freeMark"` // 免费角标是否展示 + VipMark bool `json:"vipMark"` // vip角标是否展示 + CoinMark bool `json:"coinMark"` // 金币角标是否展示 + AiSwitchConf []AISwitchConf `json:"aiSwitchConf"` // AI科技排序和开关配置 + SignIcon string `json:"signIcon"` // 签到图标 + DarkWebEnable bool `json:"darkWebEnable"` // 暗网是否开启 + DarkWebImg string `json:"darkWebImg"` // 暗网图片 + DarkWebIcon string `json:"darkWebIcon"` // 暗网图标 + DarkWebIconName string `json:"darkWebIconName"` // 暗网图标名称 + PaymentStatusPopup string `json:"paymentStatusPopup"` // 用户付费状态分层 + PaymentStatusPopupConfig PaymentStatusPopupConfig `json:"paymentStatusPopupConfig"` // 付费状态弹窗配置 + PaymentGuide PaymentGuidePing `json:"paymentGuide"` // 新版付费引导配置 +} + +type SysInfos struct { + //广告列表 + AdsList []AdsInfo `json:"adsInfoList"` + //公告列表 + AnnounList interface{} `json:"announList"` + //版本信息 + Ver []*versionmod.VersionBody `json:"ver"` + //域名信息 + SourceList []*sourcemod.SourceRes `json:"sourceList"` + //接口域名 + Domain []string `json:"domain"` + //系统配置 + SystemConfigList []*systemmod.Config `json:"systemConfigList"` + //非会员每日可观看次数 + TotalWatch uint64 `json:"totalWatch"` + //EKey rsa公钥 + EKey []byte `json:"eKey"` + //楼凤H5(金主特制) + LouFengH5 string `json:"louFengH5"` + //随机banner + RandomBanner string `json:"randomBanner"` + //2023新春抽奖 + //Active2023URL string `json:"active_2023_url"` + //长视频广告时间(单位: 秒) + AdsTimeLongVideo uint64 `json:"adsTimeLongVideo"` + JGArea []*jingangmod.JGModel `json:"jgArea"` // 金刚区配置 + //黑料h5地址 + HlH5URL string `json:"hlH5"` + LuckyDrawIcon string `json:"luckyDrawIcon"` // 抽奖浮动图标 + LuckyDrawH5 string `json:"luckyDrawH5"` // 抽奖H5链接 + AiUndressPrice int64 `json:"aiUndressPrice"` // AI脱衣价格 + AiImageToVideoPrice int64 `json:"aiImageToVideoPrice"` // AI图生视频金币 + AiTextToImagePrice int64 `json:"aiTextToImagePrice"` // AI绘图金币 + SendMsgPrice int64 `json:"sendMsgPrice"` // 发送私信价格 + AdvanceStatus advanceordermod.AdvanceStatus `json:"advanceStatus"` // 预售状态 + AndLink string `json:"andLink"` // ios下载链接 + IosLink string `json:"iosLink"` // 安卓下载链接 + AiBubble []string `json:"aiBubble"` // AI女友气泡内容 + AiCharacterImg string `json:"aiCharacterImg"` // AI卡通人物形象 + Broadcast bool `json:"broadcast"` // 直播开关 + StoreIsOpen bool `json:"storeIsOpen"` // 是否开启店铺 + BackgroundTheme constant.UIThemeEnum `json:"backgroundTheme"` // 背景主题 0:默认主题,1:新春主题 + AdvancePage AdvanceSource `json:"advancePage"` // 预售页面资源 + ShopIosLink string `json:"shopIosLink"` // ios商店包下载链接 + HotSearchTerms []string `json:"hotSearchTerms"` // 热搜词 + SearchHintWord []string `json:"searchHintWord"` // 搜索提示词 + FestivalUi string `json:"festivalUi"` // 节日ui,如果为空值/默认 则展示默认的 + BannerJump map[int]bannerjumpmod.BannerJumpInfo `json:"bannerJump"` // 限时Banner活动 + BannerJumpList []bannerjumpmod.BannerJumpInfo `json:"bannerJumpList"` // 限时Banner活动列表 + AiGirlFriend bool `json:"aiGirlFriend"` // ai女友 + AiUndress bool `json:"aiUndress"` // ai脱衣 + AiImageChangeFace bool `json:"aiImageChangeFace"` // ai图片换脸 + AiVideoChangeFace bool `json:"aiVideoChangeFace"` // ai视频换脸 + AiTextToNovelPrice int64 `json:"aiTextToNovelPrice"` // AI小说金币 + QmdlUrl string `json:"qmdlUrl"` // 全民代理地址 + DarkWebVipName string `json:"darkWebVipName"` // 暗网会员卡名字 + DarkWebVipId string `json:"darkWebVipId"` // 暗网会员卡id + RecommendVipIds []string `json:"recommendVipIds"` // 特邀会员卡id + ShortDramaCardID string `json:"shortDramaCardId"` // 短剧播放页默认会员卡id + ShortDramaEntryPopupEnabled bool `json:"shortDramaEntryPopupEnabled"` // 短剧首次进入悬浮窗总开关 + DefaultEntryPage string `json:"defaultEntryPage"` // 默认进入页面:home/drama + DefaultEntryAudience string `json:"defaultEntryAudience"` // 默认进入页面生效人群:new_user/all_users + PrivateZoneVipName string `json:"privateZoneVipName"` // 私密圈推荐会员卡名字 + PrivateZoneVipId string `json:"privateZoneVipId"` // 私密圈跳转会员卡id + NewbieSaleTime int64 `json:"newbieSaleTime"` // 新人特惠销售时间 单位 小时} + AIMateH5 string `json:"aiMateH5"` // AI伴侣链接 + PersonalCenterBackground string `json:"personalCenterBackground"` // 个人中心背景图 + Video1 string `json:"video1"` // 创作技巧视频1 + Video2 string `json:"video2"` // 创作技巧视频2 + ReturnSaleVipIds []string `json:"returnSaleVipIds"` // 返场会员卡id + OldReturnSaleTime int64 `json:"oldReturnSaleTime"` // 返场销售时间 单位 小时 + ReportUrl string `json:"reportUrl"` // 数据上报地址 + ReportSDKConfig string `json:"reportSdkConfig"` // 数据上报SDK域名配置 + FreeMark bool `json:"freeMark"` // 免费角标是否展示 + VipMark bool `json:"vipMark"` // vip角标是否展示 + CoinMark bool `json:"coinMark"` // 金币角标是否展示 + AiSwitchConf []AISwitchConf `json:"aiSwitchConf"` // AI科技排序和开关配置 + SignIcon string `json:"signIcon"` // 签到图标 + DarkWebEnable bool `json:"darkWebEnable"` // 暗网是否开启 + DarkWebImg string `json:"darkWebImg"` // 暗网图片 + DarkWebIcon string `json:"darkWebIcon"` // 暗网图标 + DarkWebIconName string `json:"darkWebIconName"` // 暗网图标名称 + PaymentStatusPopup string `json:"paymentStatusPopup"` // 用户付费状态分层 + PaymentStatusPopupTimeDown int64 `json:"paymentStatusPopupTimeDown"` // 用户付费状态弹窗倒计时 + PaymentStatusPopupConfig PaymentStatusPopupConfig `json:"paymentStatusPopupConfig"` // 付费状态弹窗配置 + PaymentGuide PaymentGuidePing `json:"paymentGuide"` // 新版付费引导配置 +} + +type AISwitchConf struct { + Type int `json:"type" bson:"type"` // AI类型 1:AI脱衣 2:AI视频换脸 3:AI图片换脸 4:图生视频 5:文生图 6:AI小说 7:ai女友 + Sort int `json:"sort" bson:"sort"` // 排序 + IsOpen bool `json:"isOpen" bson:"isOpen"` // 是否开启 +} + +// PaymentStatusPopupConfig 用户分层弹窗配置 +type PaymentStatusPopupConfig struct { + Homepage string `json:"homepage"` // 首页弹窗 + HomepageFlot string `json:"homepageFlot"` // 首页浮窗 + PlayPage string `json:"playPage"` // 播放页弹窗 + MeTab string `json:"meTab"` // 我的tab弹窗 + VipCard string `json:"vipCard"` // 会员卡弹窗 + LastDiscountTime time.Time `json:"lastDiscountTime"` // 最后优惠时间(零值表示无倒计时) +} + +// PaymentGuidePing 新版付费引导 Ping 下发配置。 +// 普通场景直接下发已解析的展示数据;会员内容上新场景只下发 enabled。 +type PaymentGuidePing struct { + Enabled bool `json:"enabled"` + Segment string `json:"segment"` + Scenes map[string]PaymentGuidePingScene `json:"scenes"` +} + +type PaymentGuidePingScene struct { + Enabled bool `json:"enabled"` + Show *bool `json:"show,omitempty"` + TotalWatchCount *uint64 `json:"totalWatchCount,omitempty"` + ConfigID string `json:"configId,omitempty"` + Style string `json:"style,omitempty"` + Title string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + Cover *string `json:"cover,omitempty"` + ProductID *string `json:"productId,omitempty"` + DurationSeconds *int64 `json:"durationSeconds,omitempty"` + Action *paymentguidemod.Action `json:"action,omitempty"` +} + +// SysInfoRefresh /ping/domain/refresh 按需刷新返回 +type SysInfoRefresh struct { + PaymentPopup SysInfoRefreshPaymentPopup `json:"paymentPopup"` // 用户付费状态弹窗 + PaymentGuide PaymentGuidePing `json:"paymentGuide"` // 新版付费引导配置 +} + +type SysInfoRefreshPaymentPopup struct { + PaymentStatusPopup string `json:"paymentStatusPopup"` // 用户付费状态分层 + Homepage string `json:"homepage"` // 首页弹窗 + HomepageFlot string `json:"homepageFlot"` // 首页浮窗 + PlayPage string `json:"playPage"` // 播放页弹窗 + MeTab string `json:"meTab"` // 我的tab弹窗 + VipCard string `json:"vipCard"` // 会员卡弹窗 + LastDiscountTime time.Time `json:"lastDiscountTime"` // 最后优惠时间(零值表示无倒计时) +} diff --git a/app/proto/ping_short_drama_card_test.go b/app/proto/ping_short_drama_card_test.go new file mode 100644 index 0000000..9251a1a --- /dev/null +++ b/app/proto/ping_short_drama_card_test.go @@ -0,0 +1,27 @@ +package proto + +import ( + "encoding/json" + "testing" +) + +func TestPingResponsesIncludeShortDramaCardID(t *testing.T) { + const productID = "6a8f066778316ccfc08c4ce7" + responses := []any{ + SysInfo{ShortDramaCardID: productID}, + SysInfos{ShortDramaCardID: productID}, + } + for _, response := range responses { + data, err := json.Marshal(response) + if err != nil { + t.Fatal(err) + } + var decoded map[string]any + if err = json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + if got := decoded["shortDramaCardId"]; got != productID { + t.Fatalf("shortDramaCardId = %v, want %s", got, productID) + } + } +} diff --git a/app/proto/product.go b/app/proto/product.go new file mode 100644 index 0000000..fddec11 --- /dev/null +++ b/app/proto/product.go @@ -0,0 +1,64 @@ +package proto + +import ( + "91porn-server/models/v/integralconfigmod" + "time" + + "91porn-server/common/daichong" + "91porn-server/models/v/productmod" + "91porn-server/models/v/rchgamtmod" +) + +// ProductList 充值金额列表 +type ProductList struct { + //展示样式 1heng 2 shu + ShowType int64 `json:"showType"` + //位置 + Position string `json:"position"` + //列表 + List []VIPListRes `json:"list"` + //楼凤位置id + PositionID string `json:"positionID"` +} + +// GoldRes 充值金额列表 +type VIPListRes struct { + productmod.Product + RechargeType []rchgamtmod.PayChannelRes `json:"rchgType" bson:"rchgType"` //充值方式 + BadgeType string `json:"badgeType" bson:"-"` // A/B实验套餐角标类型 + BadgeText string `json:"badgeText" bson:"-"` // A/B实验套餐角标文案 +} + +type VIPCardBadgeStyle struct { + BadgeType string `json:"badgeType"` + BackgroundColor string `json:"backgroundColor"` + TextColor string `json:"textColor"` +} + +type VIPCardUIConfig struct { + BackgroundImage string `json:"backgroundImage"` + BadgeStyles []VIPCardBadgeStyle `json:"badgeStyles"` +} + +type ProductRes struct { + ExperimentID string `json:"experimentId,omitempty" bson:"-"` // VIP卡片实验ID + ExperimentStatus string `json:"experimentStatus,omitempty" bson:"-"` // 本次响应的VIP卡片实验状态 + Variant string `json:"variant,omitempty" bson:"-"` // 用户命中的实验分组 A/B + SkinKey string `json:"skinKey,omitempty" bson:"-"` // 分组对应卡皮标识 + DefaultProductID string `json:"defaultProductId,omitempty" bson:"-"` // 默认选中套餐ID + UIConfig *VIPCardUIConfig `json:"uiConfig,omitempty" bson:"-"` // 用户命中分组的UI配置 + List []ProductList `json:"list" bson:"list"` // 产品列表数据 + IntegralList []integralconfigmod.APPIntegralConfig `json:"integralList" bson:"integralList"` // 积分兑换列表 + Daichong daichong.Chat `json:"daichong" bson:"daichong"` // 代充商人数据 + IsNewUser bool `json:"isNewUser" bson:"isNewUser"` // 是否新手 +} + +type TransactionResp struct { + ProductName string `json:"productName"` + OrderId string `json:"orderId"` + Money int64 `json:"money"` + PaymentAt time.Time `json:"paymentAt"` + CreatedAt time.Time `json:"createdAt"` + PayType string `json:"payType"` + Status int `json:"status"` +} diff --git a/app/proto/product_test.go b/app/proto/product_test.go new file mode 100644 index 0000000..5024991 --- /dev/null +++ b/app/proto/product_test.go @@ -0,0 +1,55 @@ +package proto + +import ( + "bytes" + "encoding/json" + "testing" +) + +func TestProductResUIConfigJSONContract(t *testing.T) { + response := ProductRes{ + UIConfig: &VIPCardUIConfig{ + BackgroundImage: "vip-card-skin-a.png", + BadgeStyles: []VIPCardBadgeStyle{{ + BadgeType: "MOST_POPULAR", + BackgroundColor: "#2B251A", + TextColor: "#F7D98C", + }}, + }, + } + data, err := json.Marshal(response) + if err != nil { + t.Fatalf("json.Marshal() error = %v", err) + } + for _, field := range [][]byte{ + []byte(`"uiConfig"`), + []byte(`"backgroundImage":"vip-card-skin-a.png"`), + []byte(`"badgeType":"MOST_POPULAR"`), + []byte(`"backgroundColor":"#2B251A"`), + []byte(`"textColor":"#F7D98C"`), + } { + if !bytes.Contains(data, field) { + t.Fatalf("JSON response missing %s: %s", field, data) + } + } +} + +func TestProductResOmitsEmptyUIConfig(t *testing.T) { + data, err := json.Marshal(ProductRes{}) + if err != nil { + t.Fatalf("json.Marshal() error = %v", err) + } + if bytes.Contains(data, []byte(`"uiConfig"`)) { + t.Fatalf("legacy JSON response contains uiConfig: %s", data) + } +} + +func TestProductResIncludesExperimentStatus(t *testing.T) { + data, err := json.Marshal(ProductRes{ExperimentStatus: "DISABLED"}) + if err != nil { + t.Fatalf("json.Marshal() error = %v", err) + } + if !bytes.Contains(data, []byte(`"experimentStatus":"DISABLED"`)) { + t.Fatalf("JSON response missing experimentStatus: %s", data) + } +} diff --git a/app/proto/pub.go b/app/proto/pub.go new file mode 100644 index 0000000..757a8fd --- /dev/null +++ b/app/proto/pub.go @@ -0,0 +1,4 @@ +package proto + +type GinH struct { +} diff --git a/app/proto/recharge.go b/app/proto/recharge.go new file mode 100644 index 0000000..44af25c --- /dev/null +++ b/app/proto/recharge.go @@ -0,0 +1,36 @@ +package proto + +import ( + "91porn-server/common/daichong" + "91porn-server/models/v/rchgamtmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type CurryenyResp struct { + //代充 + daichong.Chat `json:"daichong"` + // + List []*CurrencyListResponse `json:"list"` +} + +type CurrencyListResponse struct { + //表id + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + //货币数量 + Amount int64 `json:"amount" bson:"amount" binding:"required"` + //货币价格 + Money int64 `json:"money" bson:"money"` + //优惠描述 + CouponDesc string `json:"couponDesc" bson:"couponDesc"` + //赠送vip天数 + GiveVipDays int `json:"giveVipDays" bson:"giveVipDays"` + //赠送楼凤解锁次数 + LouFengUnlockTimes int `json:"louFengUnlockTimes" bson:"louFengUnlockTimes"` + //类型名称 + TypeName string `json:"typeName" bson:"typeName" binding:"required"` + //充值方式 + RechargeType []rchgamtmod.PayChannelRes `json:"rchgType" bson:"rchgType"` + //赠送金币 + GiveGold int64 `json:"giveGold" bson:"giveGold"` +} diff --git a/app/proto/recomm.go b/app/proto/recomm.go new file mode 100644 index 0000000..b4de320 --- /dev/null +++ b/app/proto/recomm.go @@ -0,0 +1,9 @@ +package proto + +import "91porn-server/models/v/vidmod" + +type LightVideoRes struct { + HomeVideo []*vidmod.VideoInfo `json:"homeVideo"` + ZoneVideo []*vidmod.VideoInfo `json:"zoneVideo"` + ADs []*AdsInfo `json:"ads"` +} diff --git a/app/proto/search.go b/app/proto/search.go new file mode 100644 index 0000000..5bb24aa --- /dev/null +++ b/app/proto/search.go @@ -0,0 +1,21 @@ +package proto + +import ( + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Tag 标签信息 +type Tag struct { + ID primitive.ObjectID `json:"id" bson:"_id"` // 标签id + Name string `json:"name" bson:"tagName"` // 标签名字 + Hot int64 `json:"hot" bson:"hot"` // 热度 +} + +// HotPublisher 标签信息 +type HotPublisher struct { + usermod.BaseInfo `json:",inline"` + List []*vidmod.VideoInfo `json:"list"` +} diff --git a/app/proto/statsync.go b/app/proto/statsync.go new file mode 100644 index 0000000..4f53b23 --- /dev/null +++ b/app/proto/statsync.go @@ -0,0 +1,54 @@ +package proto + +// 新用户注册 +type NewUserRegisterMsg struct { + UserId uint64 `json:"user_id"` // 用户id + DeviceId string `json:"device_id"` // 用户设备标识 + Platform string `json:"platform"` // android ios h5 + Coin int64 `json:"coin"` // 用户金币 + IsVip int `json:"is_vip"` // 是否vip 0 否 1 是 + InviteUserId uint64 `json:"invite_user_id"` // 邀请用户id + RegistTime int64 `json:"regist_time"` // 用户注册时间 + RegisterIp string `json:"register_ip"` // 用户注册ip + ChannelCode string `json:"channel_code"` // 用户渠道码 区分大小写 + LastOnlineTime int64 `json:"last_online_time"` // 最后在线时间 unix时间戳 秒 +} + +// 新用户邀请 +type NewUserInviteMsg struct { + Id string `json:"id"` // 记录id + UserId uint64 `json:"user_id"` // 用户id + InviteUserId uint64 `json:"invite_user_id"` // 邀请用户id +} + +// 新用户访问 +type NewUserAccessMsg struct { + UserId uint64 `json:"user_id"` // 用户id + DeviceId string `json:"device_id"` // 用户设备标识 + Platform string `json:"platform"` // android ios h5 + Coin int64 `json:"coin"` // 用户金币 + IsVip int `json:"is_vip"` // 是否vip 0 否 1 是 + InviteUserId uint64 `json:"invite_user_id"` // 邀请用户id + RegistTime int64 `json:"regist_time"` // 用户注册时间 + RegisterIp string `json:"register_ip"` // 用户注册ip + ChannelCode string `json:"channel_code"` // 用户渠道码 区分大小写 + LastOnlineTime int64 `json:"last_online_time"` // 最后在线时间 unix时间戳 秒 +} + +// 新用户充值消息 +type NewUserRechargeMsg struct { + Id string `json:"id"` // 订单id + OID string `json:"order_no"` // 第三方支付流水id + OrderId string `json:"app_order_no"` // 流水id + UserId uint64 `json:"user_id"` // 用户ID + Money int64 `json:"order_amount"` // 订单金额,分 + Channel string `json:"payment_channel"` // 支付渠道名字 + RechargeType string `json:"payment_method"` // 支付编码,如:wechat + OrderType string `json:"order_type"` // 订单产品类型 coin/vip + OrderTime int64 `json:"order_time"` // 下单时间 + PayTime int64 `json:"pay_time"` // 支付时间 + PayAmount int64 `json:"pay_amount"` // 支付金额,分 + IsPaid int `json:"is_paid"` // 是否支付 0 未支付 1 已支付 + Poundage int32 `json:"poundage"` // 手续费,分 + ProductType int `json:"product_type"` // 支付产品类型 0 站群 1 棋牌 +} diff --git a/app/router/active_2023.go b/app/router/active_2023.go new file mode 100644 index 0000000..8c06874 --- /dev/null +++ b/app/router/active_2023.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/app/api/active2023ctrl" + + "github.com/gin-gonic/gin" +) + +// 2023春节抽奖活动 +func active2023Router(router *gin.RouterGroup) { + group := router.Group("/active2023") + { + group.GET("/user_info", active2023ctrl.UserInfo) // 获取抽奖用户信息 + group.POST("/lottery", active2023ctrl.Lottery) // 抽奖 + } +} diff --git a/app/router/activity.go b/app/router/activity.go new file mode 100644 index 0000000..39bfd13 --- /dev/null +++ b/app/router/activity.go @@ -0,0 +1,24 @@ +package router + +import ( + "91porn-server/app/api/activityctrl" + "91porn-server/app/middleware/activityauth" + + "github.com/gin-gonic/gin" +) + +// activityCallbackRouter 活动服回调路由(独立鉴权,不走主auth) +func activityCallbackRouter(router *gin.Engine) { + group := router.Group("/activity", activityauth.HmacAuth) + { + group.POST("/currency/list", activityctrl.CurrencyList) + group.POST("/vip/list", activityctrl.ProductList) + group.POST("/recharge/order", activityctrl.RechargeOrder) + group.POST("/coin/buy", activityctrl.BuyCoinProduct) + group.POST("/user/balance", activityctrl.UserBalance) + group.POST("/app/info", activityctrl.AppInfo) + group.POST("/reward", activityctrl.Reward) + group.POST("/reward/batch", activityctrl.BatchReward) + group.POST("/deduct", activityctrl.Deduct) + } +} diff --git a/app/router/actv.go b/app/router/actv.go new file mode 100644 index 0000000..1be18bf --- /dev/null +++ b/app/router/actv.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/app/api/actvctrl" + + "github.com/gin-gonic/gin" +) + +// 活动专页 +func actvRouter(router *gin.RouterGroup) { + group := router.Group("/actv") + { + group.GET("/list", actvctrl.GetActivities) + group.GET("/getById", actvctrl.GetActiveByID) + } +} diff --git a/app/router/ads.go b/app/router/ads.go new file mode 100644 index 0000000..082db59 --- /dev/null +++ b/app/router/ads.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/app/api/adsctrl" + + "github.com/gin-gonic/gin" +) + +func adsRouter(router *gin.RouterGroup) { + group := router.Group("/ads") + { + group.POST("/click", adsctrl.AdsClick) + group.GET("/list", adsctrl.List) + group.POST("/click/stat", adsctrl.AdsClickStat) + } +} diff --git a/app/router/advance_config.go b/app/router/advance_config.go new file mode 100644 index 0000000..19c9559 --- /dev/null +++ b/app/router/advance_config.go @@ -0,0 +1,13 @@ +package router + +import ( + "91porn-server/app/api/advance_config_ctrl" + "github.com/gin-gonic/gin" +) + +func advanceConfigRouter(router *gin.RouterGroup) { + group := router.Group("/advance_config") + { + group.GET("/list", advance_config_ctrl.GetAdvanceConfig) + } +} diff --git a/app/router/ai_change_face.go b/app/router/ai_change_face.go new file mode 100644 index 0000000..172d903 --- /dev/null +++ b/app/router/ai_change_face.go @@ -0,0 +1,23 @@ +package router + +import ( + "91porn-server/app/api/ai_changeface_ctrl" + "91porn-server/app/api/ai_undress_ctrl" + "github.com/gin-gonic/gin" +) + +func aiChangeFaceRouter(router *gin.RouterGroup) { + group := router.Group("/ai/changeface") + { + group.GET("/list", ai_changeface_ctrl.List) // ai换脸订单列表 + group.POST("/generate", ai_changeface_ctrl.Generate) + group.POST("/hide", ai_changeface_ctrl.Hide) + } + + imgGroup := router.Group("/ai/img") + { + imgGroup.GET("/list", ai_undress_ctrl.AiImgList) + imgGroup.POST("/generate", ai_undress_ctrl.AiImgGenerate) + imgGroup.POST("/hide", ai_undress_ctrl.AiChangeFaceImgHide) + } +} diff --git a/app/router/ai_image_to_video.go b/app/router/ai_image_to_video.go new file mode 100755 index 0000000..4417d6a --- /dev/null +++ b/app/router/ai_image_to_video.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/app/api/ai_image_to_video_ctrl" + "github.com/gin-gonic/gin" +) + +// aiImageToVideo 移动端接口 +func aiImageToVideoRouter(router *gin.RouterGroup) { + group := router.Group("/ai/imagetovideo") + { + group.GET("/list", ai_image_to_video_ctrl.List) + group.POST("/generate", ai_image_to_video_ctrl.Generate) + group.POST("/hide", ai_image_to_video_ctrl.Hide) + } +} diff --git a/app/router/ai_mate.go b/app/router/ai_mate.go new file mode 100644 index 0000000..6aef649 --- /dev/null +++ b/app/router/ai_mate.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/app/api/ai_mate_ctrl" + "github.com/gin-gonic/gin" +) + +func aiMateRouter(router *gin.RouterGroup) { + group := router.Group("/aimate") + group.GET("/currencys", ai_mate_ctrl.GetCurrencys) + group.POST("/exchange", ai_mate_ctrl.Exchange) + group.POST("/sync", ai_mate_ctrl.SyncInfo) + group.GET("/login", ai_mate_ctrl.Login) + group.GET("/getBalance", ai_mate_ctrl.GetBalance) + group.GET("/gianBalance", ai_mate_ctrl.GianBalance) +} diff --git a/app/router/ai_mate_test.go b/app/router/ai_mate_test.go new file mode 100644 index 0000000..fdba6a1 --- /dev/null +++ b/app/router/ai_mate_test.go @@ -0,0 +1,56 @@ +package router + +import ( + "testing" + + "github.com/gin-gonic/gin" +) + +func TestAiMateH5CompatibilityRoutes(t *testing.T) { + gin.SetMode(gin.TestMode) + engine := gin.New() + aiMateRouter(engine.Group("/api/app")) + + routes := make(map[string]struct{}) + for _, route := range engine.Routes() { + routes[route.Method+" "+route.Path] = struct{}{} + } + expected := []string{ + "GET /api/app/aimate/login", + "GET /api/app/aimate/getBalance", + "GET /api/app/aimate/gianBalance", + "GET /api/app/aimate/currencys", + "POST /api/app/aimate/exchange", + } + for _, route := range expected { + if _, ok := routes[route]; !ok { + t.Errorf("route %q is not registered", route) + } + } +} + +func TestAiMateV2Route(t *testing.T) { + gin.SetMode(gin.TestMode) + engine := gin.New() + aiMateV2Router(engine.Group("/api/app")) + + for _, route := range engine.Routes() { + if route.Method == "POST" && route.Path == "/api/app/aimatev2/url" { + return + } + } + t.Fatal("POST /api/app/aimatev2/url is not registered") +} + +func TestContentUpdateMarkerRoute(t *testing.T) { + gin.SetMode(gin.TestMode) + engine := gin.New() + contentMarkerRouter(engine.Group("/api/app")) + + for _, route := range engine.Routes() { + if route.Method == "GET" && route.Path == "/api/app/content/update-markers" { + return + } + } + t.Fatal("GET /api/app/content/update-markers is not registered") +} diff --git a/app/router/ai_mate_v2.go b/app/router/ai_mate_v2.go new file mode 100644 index 0000000..f4c417c --- /dev/null +++ b/app/router/ai_mate_v2.go @@ -0,0 +1,12 @@ +package router + +import ( + "91porn-server/app/api/ai_mate_v2_ctrl" + + "github.com/gin-gonic/gin" +) + +func aiMateV2Router(router *gin.RouterGroup) { + group := router.Group("/aimatev2") + group.POST("/url", ai_mate_v2_ctrl.URL) +} diff --git a/app/router/ai_mod.go b/app/router/ai_mod.go new file mode 100644 index 0000000..9722c3d --- /dev/null +++ b/app/router/ai_mod.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/app/api/ai_changeface_ctrl" + "github.com/gin-gonic/gin" +) + +func aiModRouter(router *gin.RouterGroup) { + group := router.Group("/ai/mod") + { + group.GET("/list", ai_changeface_ctrl.ModList) // ai换脸视频模版列表 + group.GET("/v2/list", ai_changeface_ctrl.ModListV2) // ai换脸视频模版列表 + group.GET("/info", ai_changeface_ctrl.ModInfo) // ai模版详情 + } +} diff --git a/app/router/ai_text_to_image.go b/app/router/ai_text_to_image.go new file mode 100755 index 0000000..1622523 --- /dev/null +++ b/app/router/ai_text_to_image.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/app/api/ai_text_to_image_ctrl" + "github.com/gin-gonic/gin" +) + +// aiTextToImage 移动端接口 +func aiTextToImageRouter(router *gin.RouterGroup) { + group := router.Group("/ai/text_to_image") + { + group.GET("/list", ai_text_to_image_ctrl.List) + group.POST("/generate", ai_text_to_image_ctrl.Generate) + group.POST("/hide", ai_text_to_image_ctrl.Hide) + } +} diff --git a/app/router/ai_text_to_novel.go b/app/router/ai_text_to_novel.go new file mode 100755 index 0000000..626cb5e --- /dev/null +++ b/app/router/ai_text_to_novel.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/app/api/ai_text_to_novel_ctrl" + "github.com/gin-gonic/gin" +) + +// aiTextToNovel 移动端接口 +func aiTextToNovelRouter(router *gin.RouterGroup) { + group := router.Group("/ai_text_to_novel") + { + group.GET("/list", ai_text_to_novel_ctrl.List) + group.POST("/generate", ai_text_to_novel_ctrl.Generate) + group.POST("/hide", ai_text_to_novel_ctrl.Hide) + group.GET("/info", ai_text_to_novel_ctrl.Info) + } +} diff --git a/app/router/ai_undress.go b/app/router/ai_undress.go new file mode 100644 index 0000000..7f04815 --- /dev/null +++ b/app/router/ai_undress.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/app/api/ai_undress_ctrl" + + "github.com/gin-gonic/gin" +) + +func aiUndressRouter(router *gin.RouterGroup) { + group := router.Group("/ai/undress") + { + group.GET("/list", ai_undress_ctrl.List) + group.POST("/generate", ai_undress_ctrl.Generate) + group.POST("/hide", ai_undress_ctrl.AiUndressHide) + } +} diff --git a/app/router/aiplaza.go b/app/router/aiplaza.go new file mode 100755 index 0000000..e5ca6a1 --- /dev/null +++ b/app/router/aiplaza.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/app/api/aiplazactrl" + "github.com/gin-gonic/gin" +) + +// aiPlaza 移动端接口 +func aiPlazaRouter(router *gin.RouterGroup) { + group := router.Group("/aiplaza") + { + group.GET("/list", aiplazactrl.List) + group.GET("/info", aiplazactrl.Info) + group.POST("/share", aiplazactrl.Share) + } +} diff --git a/app/router/aitemplatemodule.go b/app/router/aitemplatemodule.go new file mode 100755 index 0000000..96ccce0 --- /dev/null +++ b/app/router/aitemplatemodule.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/app/api/aitemplatemodulectrl" + "github.com/gin-gonic/gin" +) + +// aiTemplateModule 移动端接口 +func aiTemplateModuleRouter(router *gin.RouterGroup) { + group := router.Group("/ai_template_module") + { + group.GET("/list", aitemplatemodulectrl.List) + group.GET("/info", aitemplatemodulectrl.Info) + group.GET("/all", aitemplatemodulectrl.AllList) + } +} diff --git a/app/router/analytics.go b/app/router/analytics.go new file mode 100644 index 0000000..42b9ddb --- /dev/null +++ b/app/router/analytics.go @@ -0,0 +1,14 @@ +package router + +import ( + "91porn-server/app/api/analyticsctrl" + + "github.com/gin-gonic/gin" +) + +func analyticsRouter(router *gin.RouterGroup) { + group := router.Group("/analytics") + { + group.POST("/events", analyticsctrl.Events) + } +} diff --git a/app/router/analytics_test.go b/app/router/analytics_test.go new file mode 100644 index 0000000..2ad9327 --- /dev/null +++ b/app/router/analytics_test.go @@ -0,0 +1,18 @@ +package router + +import ( + "testing" + + "github.com/gin-gonic/gin" +) + +func TestVIPCardAnalyticsRoute(t *testing.T) { + engine := gin.New() + analyticsRouter(engine.Group("/api/app")) + for _, route := range engine.Routes() { + if route.Method == "POST" && route.Path == "/api/app/analytics/events" { + return + } + } + t.Fatal("POST /api/app/analytics/events is not registered") +} diff --git a/app/router/annou.go b/app/router/annou.go new file mode 100644 index 0000000..5c5e7b8 --- /dev/null +++ b/app/router/annou.go @@ -0,0 +1,14 @@ +package router + +import ( + "91porn-server/app/api/annouctrl" + "github.com/gin-gonic/gin" +) + +func annouRouter(router *gin.RouterGroup) { + group := router.Group("/annou") + { + group.GET("/list", annouctrl.GetAnnou) + group.GET("/msg/list", annouctrl.GetAnnous) + } +} diff --git a/app/router/backpack.go b/app/router/backpack.go new file mode 100644 index 0000000..24da8f3 --- /dev/null +++ b/app/router/backpack.go @@ -0,0 +1,14 @@ +package router + +import ( + "91porn-server/app/api/backpackctrl" + + "github.com/gin-gonic/gin" +) + +func backpackRouter(router *gin.RouterGroup) { + backpack := router.Group("/backpack") + { + backpack.GET("", backpackctrl.GetCouponList) + } +} diff --git a/app/router/base.go b/app/router/base.go new file mode 100644 index 0000000..301beef --- /dev/null +++ b/app/router/base.go @@ -0,0 +1,199 @@ +package router + +import ( + "91porn-server/app/api/sharectrl" + "91porn-server/app/api/updownctrl" + "91porn-server/app/appg" + "91porn-server/app/middleware/authuser" + "91porn-server/app/middleware/datacenter" + "91porn-server/app/middleware/ip" + "91porn-server/app/middleware/requestEncrypt" + "91porn-server/app/middleware/verfyparam" + "91porn-server/app/middleware/visitlog" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/common/tg" + "91porn-server/middleware/cors" + "91porn-server/middleware/ginzap" + "91porn-server/middleware/requestid" + "91porn-server/middleware/ua" + "fmt" + "net/http" + "runtime/debug" + "time" + + "github.com/didip/tollbooth" + "github.com/didip/tollbooth/limiter" + "github.com/gin-contrib/pprof" + "github.com/gin-gonic/gin" +) + +var Lmt, RechargeLmt, GlobalLmt *limiter.Limiter + +// SetupGin 初始化gin router +func SetupGin() *gin.Engine { + if appg.Conf.Base.Env == constant.ProdEnv { + gin.SetMode(gin.ReleaseMode) + } else { + gin.SetMode(gin.DebugMode) + } + router := gin.New() + router.ForwardedByClientIP = true + if appg.Conf.Base.PProf { + pprof.Register(router) // 性能分析工具 + } + router.Use( + ginzap.Logger([]string{constant.CtxUserID, constant.CtxIP}), + ginzap.Recovery(true, func(err interface{}) { + var stack string + if !stderr.IsContain(err, stderr.WriteBrokenError) { + stack = string(debug.Stack()) + } + common.Go(func() { + _ = appg.Bot.Send(tg.Msg{ChatId: tg.ChatIdServerTeam, Text: fmt.Sprintf("[App-Server] panic:%+v, stack:%s", err, stack)}) + }) + })) + router.Use(cors.Cors(&cors.Options{ + Origin: nil, + Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodHead, http.MethodDelete}, + AllowedHeaders: []string{"Content-Length", "Content-Type", "Authorization", "X-User-Agent", "User-Agent", "X-Authorization", "X-Request-ID", "temp", "log-type"}, + ExposedHeaders: []string{"Content-Disposition", "Refresh-Authorization", "Cur-Ver", "X-Request-ID"}, + OptionsSuccessStatus: http.StatusOK, + })) + router.Use(ua.UAer, ip.RealIP, requestid.New()) + router.HandleMethodNotAllowed = true + router.NoMethod(func(c *gin.Context) { + c.JSON(http.StatusMethodNotAllowed, gin.H{"result": false, "error": "Method Not Allowed"}) + }) + router.NoRoute(func(c *gin.Context) { + c.JSON(http.StatusNotFound, gin.H{"result": false, "error": "Endpoint Not Found"}) + }) + Init() + registerRouter(router) + return router +} + +func registerRouter(router *gin.Engine) { + //root Router 根组 + rootRouter := router.Group("/api/app", + verfyparam.CheckReplayAttackRequest(), + verfyparam.VerifyReplayAttackRequest(appg.Conf.Base.ReplayAttack), + requestEncrypt.ReqDecode, + authuser.Auth, + visitlog.Log, + datacenter.OnlineStat, + //ipblock.IPBlock(constant.GlobalBlock), + //limitHandler.LimitHandler(GlobalLmt), + ) + { + updownloadRouter(rootRouter) + walletRouter(rootRouter) + shareRouter(rootRouter) + vidRouter(rootRouter) + recommendRouter(rootRouter) + userRouter(rootRouter) + tagRouter(rootRouter) + searchRouter(rootRouter) + mineRouter(rootRouter) + notificationRoute(rootRouter) + rankRouter(rootRouter) + toneRouter(rootRouter) + wonderRouter(rootRouter) + vipRouter(rootRouter) + productRouter(rootRouter) + paymentGuideRouter(rootRouter) + sceneBannerRouter(rootRouter) + analyticsRouter(rootRouter) + imdRouter(rootRouter) + rchgRouter(rootRouter) + versionRouter(rootRouter) + pingRouter(rootRouter) + withdrawRouter(rootRouter) + annouRouter(rootRouter) + staticRouter(rootRouter) + txnactRouter(rootRouter) + adsRouter(rootRouter) + infmtRouter(rootRouter) + hotspotRouter(rootRouter) + exchangeRouter(rootRouter) + newActivityRoute(rootRouter) + userInviteRouter(rootRouter) + taskRouter(rootRouter) + backpackRouter(rootRouter) + moduleRouter(rootRouter) + contentMarkerRouter(rootRouter) + smsRoute(rootRouter) + publishRouter(rootRouter) + videoActivityRouter(rootRouter) + aiUndressRouter(rootRouter) + thumbsRouter(rootRouter) + officialRouter(rootRouter) + commentRouter(rootRouter) + followRouter(rootRouter) + proxyRouter(rootRouter) + aiChangeFaceRouter(rootRouter) + messageRouter(rootRouter) + aiModRouter(rootRouter) + videoGoldCoinRouter(rootRouter) + recreationRouter(rootRouter) + advanceConfigRouter(rootRouter) + couponRouter(rootRouter) + integralRouter(rootRouter) + signRecordRouter(rootRouter) + aiMateRouter(rootRouter) + aiMateV2Router(rootRouter) + storeWalletRouter(rootRouter) + mediaRouter(rootRouter) + dramaRouter(rootRouter) + mediaBookshelfRouter(rootRouter) + mediaContentRouter(rootRouter) + mediaTagRouter(rootRouter) + imGroupRouter(rootRouter) + imMessageRouter(rootRouter) + nakedChatRouter(rootRouter) + nakedChatOrderRouter(rootRouter) + aiTextToImageRouter(rootRouter) + aiImageToVideoRouter(rootRouter) + aiTemplateModuleRouter(rootRouter) + aiPlazaRouter(rootRouter) + aiTextToNovelRouter(rootRouter) + checkinRouter(rootRouter) + customerApiRouter(rootRouter) + healthCheckRouter(rootRouter) + } + thirdRouter := router.Group("/3rd") + { + defrayRouter(thirdRouter) + } + { + shareRouter := router.Group("/share") + shareRouter.POST("/info", sharectrl.Info) + shareRouter.GET("/m3u8/*source", updownctrl.DownloadM3u8H5) + } + // 客服系统回调相关 + customerRouter(router) + // 活动服回调相关 + activityCallbackRouter(router) + + // 品牌站相关接口 + newOfficialWebsiteRoute(router) +} + +func Init() { + GlobalLmt = tollbooth.NewLimiter(float64(appg.Conf.Limit.GlobalLimit), &limiter.ExpirableOptions{DefaultExpirationTTL: time.Second}) + Lmt = tollbooth.NewLimiter(float64(appg.Conf.Limit.CommentLimit), &limiter.ExpirableOptions{DefaultExpirationTTL: time.Second}) + RechargeLmt = tollbooth.NewLimiter(float64(appg.Conf.Limit.RechargeLimit), &limiter.ExpirableOptions{DefaultExpirationTTL: time.Minute}) + Lmt.SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"}) + Lmt.SetMethods([]string{"GET", "POST"}) + Lmt.SetMessageContentType("application/json; charset=utf-8") + Lmt.SetStatusCode(http.StatusNoContent) + GlobalLmt.SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"}) + GlobalLmt.SetMethods([]string{"GET", "POST"}) + GlobalLmt.SetMessageContentType("application/json; charset=utf-8") + GlobalLmt.SetStatusCode(http.StatusNoContent) + RechargeLmt.SetIPLookups([]string{"RemoteAddr", "X-Forwarded-For", "X-Real-IP"}) + RechargeLmt.SetMethods([]string{"POST"}) + RechargeLmt.SetMessageContentType("application/json; charset=utf-8") + RechargeLmt.SetStatusCode(http.StatusNoContent) +} diff --git a/app/router/checkin.go b/app/router/checkin.go new file mode 100644 index 0000000..aa13ca6 --- /dev/null +++ b/app/router/checkin.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/app/api/checkinctrl" + + "github.com/gin-gonic/gin" +) + +func checkinRouter(router *gin.RouterGroup) { + checkin := router.Group("/checkin") + { + checkin.POST("/click", checkinctrl.UserCheckin) // 签到 + checkin.POST("/prize", checkinctrl.GetCheckinPrize) // 获取签到奖品 + checkin.POST("/claim_vip", checkinctrl.ClaimVipCheckinPrize) // 补领VIP签到奖励 + } +} diff --git a/app/router/comment.go b/app/router/comment.go new file mode 100644 index 0000000..9f9c33c --- /dev/null +++ b/app/router/comment.go @@ -0,0 +1,23 @@ +package router + +import ( + "91porn-server/app/api/commentctrl" + "91porn-server/app/middleware/ipblock" + "91porn-server/app/middleware/limitHandler" + "91porn-server/common/constant" + + "github.com/didip/tollbooth_gin" + "github.com/gin-gonic/gin" +) + +func commentRouter(router *gin.RouterGroup) { + group := router.Group("/comment") + { + group.GET("/list", commentctrl.List) + group.GET("/info", commentctrl.Info) + group.POST("/send", ipblock.IPBlock(constant.BlockComment), limitHandler.FilterByIP(tollbooth_gin.LimitHandler(Lmt)), commentctrl.Send) + //非帖子内容评论,目前有愿望工单 + group.POST("/sendV2", ipblock.IPBlock(constant.BlockComment), limitHandler.FilterByIP(tollbooth_gin.LimitHandler(Lmt)), commentctrl.NoVidSend) + group.GET("/reply/list", commentctrl.ReplyList) + } +} diff --git a/app/router/content_marker.go b/app/router/content_marker.go new file mode 100644 index 0000000..8e4bb9b --- /dev/null +++ b/app/router/content_marker.go @@ -0,0 +1,12 @@ +package router + +import ( + "91porn-server/app/api/contentmarkerctrl" + + "github.com/gin-gonic/gin" +) + +func contentMarkerRouter(router *gin.RouterGroup) { + group := router.Group("/content") + group.GET("/update-markers", contentmarkerctrl.Get) +} diff --git a/app/router/coupon.go b/app/router/coupon.go new file mode 100644 index 0000000..17e40ac --- /dev/null +++ b/app/router/coupon.go @@ -0,0 +1,20 @@ +package router + +import ( + "91porn-server/app/api/couponctl" + + "github.com/gin-gonic/gin" +) + +func couponRouter(router *gin.RouterGroup) { + group := router.Group("/coupon") + { + group.GET("/list", couponctl.List) + group.POST("/upload", couponctl.Upload) + group.DELETE("", couponctl.Delete) + } + luckGroup := router.Group("/luckyDraw") + { + luckGroup.GET("/gain", couponctl.Gain) + } +} diff --git a/app/router/customer.go b/app/router/customer.go new file mode 100644 index 0000000..1642249 --- /dev/null +++ b/app/router/customer.go @@ -0,0 +1,19 @@ +package router + +import ( + "91porn-server/app/api/customerCtrl" + "github.com/gin-gonic/gin" +) + +func customerApiRouter(router *gin.RouterGroup) { + router.GET("/customer/url", customerCtrl.Url) +} + +func customerRouter(router *gin.Engine) { + customer := router.Group("/customer") + user := customer.Group("/user") + { + user.GET("/backpack", customerCtrl.Backpack) + user.GET("/recharge", customerCtrl.Recharge) + } +} diff --git a/app/router/defray.go b/app/router/defray.go new file mode 100644 index 0000000..f479bbf --- /dev/null +++ b/app/router/defray.go @@ -0,0 +1,30 @@ +package router + +import ( + "91porn-server/app/api/rechargectrl" + "91porn-server/app/api/withdrawctrl" + + "github.com/gin-gonic/gin" +) + +// defrayRouter 支付回调 +func defrayRouter(router *gin.RouterGroup) { + groupDefray := router.Group("/defray/callback") + { + //大白鲨充值回调 + groupDefray.POST("/dabaisha", rechargectrl.DaBaiShaCallBack) + groupDefray.POST("/pay_center", rechargectrl.PayCenterCallBack) + groupDefray.POST("/refund", rechargectrl.RefundCallBack) + //充值回调函数 + rchg := groupDefray.Group("/rchg") + { + rchg.POST("/:name", rechargectrl.CallBack) + rchg.GET("/:name", rechargectrl.CallBack) + } + echg := groupDefray.Group("/echg") + { + echg.POST("/:name", withdrawctrl.YinseCallBack) + } + //groupDefray.POST("/echg/goldfish", withdrawctrl.GoldFishCallBack) + } +} diff --git a/app/router/drama.go b/app/router/drama.go new file mode 100644 index 0000000..63b0707 --- /dev/null +++ b/app/router/drama.go @@ -0,0 +1,20 @@ +package router + +import ( + "91porn-server/app/api/dramactrl" + + "github.com/gin-gonic/gin" +) + +func dramaRouter(router *gin.RouterGroup) { + group := router.Group("/media/drama") + { + group.GET("/channel/config", dramactrl.ChannelConfig) + group.GET("/feed", dramactrl.Feed) + group.GET("/list", dramactrl.List) + group.GET("/topics", dramactrl.Topics) + group.GET("/topic/works", dramactrl.TopicWorks) + group.POST("/download/authorize", dramactrl.DownloadAuthorize) + group.POST("/events", dramactrl.SaveEvents) + } +} diff --git a/app/router/drama_test.go b/app/router/drama_test.go new file mode 100644 index 0000000..89cbfb4 --- /dev/null +++ b/app/router/drama_test.go @@ -0,0 +1,27 @@ +package router + +import ( + "testing" + + "github.com/gin-gonic/gin" +) + +func TestDramaRoutesExcludeServerProgress(t *testing.T) { + gin.SetMode(gin.TestMode) + engine := gin.New() + dramaRouter(engine.Group("/api/app")) + + routes := make(map[string]struct{}) + for _, route := range engine.Routes() { + routes[route.Method+" "+route.Path] = struct{}{} + } + if _, ok := routes["POST /api/app/media/drama/progress"]; ok { + t.Fatal("server-side drama progress route must not be registered") + } + if _, ok := routes["POST /api/app/media/drama/events"]; !ok { + t.Fatal("drama analytics route must remain registered") + } + if _, ok := routes["GET /api/app/media/drama/card/products"]; ok { + t.Fatal("standalone drama card product route must not be registered") + } +} diff --git a/app/router/exchcode.go b/app/router/exchcode.go new file mode 100644 index 0000000..22471c4 --- /dev/null +++ b/app/router/exchcode.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/app/api/exchcodectrl" + + "github.com/gin-gonic/gin" +) + +func exchangeRouter(router *gin.RouterGroup) { + group := router.Group("/code") + { + group.POST("/exchange", exchcodectrl.CodeExchange) + group.POST("/webexchange", exchcodectrl.WebCodeExchange) + group.GET("/userRecord", exchcodectrl.UserRecord) + } +} diff --git a/app/router/follow.go b/app/router/follow.go new file mode 100644 index 0000000..ab08483 --- /dev/null +++ b/app/router/follow.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/app/api/followctrl" + "github.com/gin-gonic/gin" +) + +func followRouter(router *gin.RouterGroup) { + group := router.Group("/follow") + { + group.GET("/list", followctrl.GetFollowList) + group.GET("/fans/list", followctrl.GetFansList) + group.GET("/dynamics/list", followctrl.DynamicsList) + group.GET("/list/short", followctrl.GetFollowUpUsersWithShort) + + } +} diff --git a/app/router/goldextra.go b/app/router/goldextra.go new file mode 100644 index 0000000..8a742dd --- /dev/null +++ b/app/router/goldextra.go @@ -0,0 +1,14 @@ +package router + +import ( + "91porn-server/app/api/goldextractrl" + + "github.com/gin-gonic/gin" +) + +func goldExtraRouter(router *gin.RouterGroup) { + ge := router.Group("/goldExtra") + { + ge.GET("/userList", goldextractrl.UserGoldExtra) + } +} diff --git a/app/router/health.go b/app/router/health.go new file mode 100644 index 0000000..e61973d --- /dev/null +++ b/app/router/health.go @@ -0,0 +1,13 @@ +package router + +import ( + "91porn-server/app/api/health_check_ctrl" + "github.com/gin-gonic/gin" +) + +func healthCheckRouter(router *gin.RouterGroup) { + group := router.Group("/health") + { + group.POST("/ping", health_check_ctrl.Ping) + } +} diff --git a/app/router/hotspot.go b/app/router/hotspot.go new file mode 100644 index 0000000..9833812 --- /dev/null +++ b/app/router/hotspot.go @@ -0,0 +1,18 @@ +package router + +import ( + "91porn-server/app/api/hotspotctr" + + "github.com/gin-gonic/gin" +) + +func hotspotRouter(router *gin.RouterGroup) { + group := router.Group("/hotspot") + { + group.GET("/htag", hotspotctr.HotTag) //热门话题 + group.GET("/hotvid/list", hotspotctr.HotVidList) //今日最热视屏 + group.GET("/rank", hotspotctr.Rank) //排行榜 + group.GET("/area", hotspotctr.Area) //专区 + group.GET("/wonder/list", hotspotctr.WonderTagList) //发现精彩 + } +} diff --git a/app/router/im.go b/app/router/im.go new file mode 100644 index 0000000..de59258 --- /dev/null +++ b/app/router/im.go @@ -0,0 +1,25 @@ +package router + +import ( + "91porn-server/app/api/imctrl" + "91porn-server/app/middleware/limitHandler" + + "github.com/gin-gonic/gin" +) + +func imdRouter(router *gin.RouterGroup) { + im := router.Group("/im") + { + im.GET("/sign", imctrl.GetImSign) + im.GET("/whiteSign", imctrl.GetWhiteImSign) + im.GET("/newSign", imctrl.ImSign) + im.POST("/token", imctrl.Token) + im.POST("/user-id", imctrl.UserID) + im.POST("/friend/ensure", imctrl.EnsureFriend) + // im skd获取 + //im.POST("/friend/list", imctrl.FriendList) + //im.POST("/message/history", imctrl.MessageHistory) + im.POST("/message/send", limitHandler.FilterRequestLimit, imctrl.SendMessage) + im.POST("/ad", imctrl.GetIMAd) + } +} diff --git a/app/router/imgroup.go b/app/router/imgroup.go new file mode 100755 index 0000000..a189b53 --- /dev/null +++ b/app/router/imgroup.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/app/api/imgroupctrl" + "github.com/gin-gonic/gin" +) + +// imGroup 移动端接口 +func imGroupRouter(router *gin.RouterGroup) { + group := router.Group("/imgroup") + { + group.GET("/list", imgroupctrl.List) + group.GET("/info", imgroupctrl.Info) + group.GET("/hasjoin", imgroupctrl.HasJoin) + } +} diff --git a/app/router/imgroupmember.go b/app/router/imgroupmember.go new file mode 100755 index 0000000..ef0231a --- /dev/null +++ b/app/router/imgroupmember.go @@ -0,0 +1,14 @@ +package router + +import ( + "91porn-server/app/api/imgroupmemberctrl" + "github.com/gin-gonic/gin" +) + +// imGroupMember 移动端接口 +func imGroupMemberRouter(router *gin.RouterGroup) { + group := router.Group("/imgroupmember") + { + group.GET("/list", imgroupmemberctrl.List) + } +} diff --git a/app/router/immessage.go b/app/router/immessage.go new file mode 100755 index 0000000..8b41c3d --- /dev/null +++ b/app/router/immessage.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/app/api/immessagectrl" + "github.com/gin-gonic/gin" +) + +// imMessage 移动端接口 todo: 在 app/router/base.go 中新增路由 +func imMessageRouter(router *gin.RouterGroup) { + group := router.Group("/immessage") + { + group.GET("/list", immessagectrl.List) + group.POST("/send", immessagectrl.Send) + } +} diff --git a/app/router/infmt.go b/app/router/infmt.go new file mode 100644 index 0000000..ed4ec8d --- /dev/null +++ b/app/router/infmt.go @@ -0,0 +1,19 @@ +package router + +import ( + "91porn-server/app/api/infmtctrl" + + "github.com/gin-gonic/gin" +) + +func infmtRouter(router *gin.RouterGroup) { + inform := router.Group("/inform") + { + inform.GET("/preview", infmtctrl.Preview) + //通知类 + notice := inform.Group("/notice") + { + notice.GET("/list", infmtctrl.NoticeList) + } + } +} diff --git a/app/router/integral.go b/app/router/integral.go new file mode 100644 index 0000000..94fa395 --- /dev/null +++ b/app/router/integral.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/app/api/integeralctrl" + + "github.com/gin-gonic/gin" +) + +func integralRouter(router *gin.RouterGroup) { + group := router.Group("/integral") + { + group.GET("/list", integeralctrl.GetList) + group.GET("/record/list", integeralctrl.GetRecordList) + group.POST("/exchangeIntegral", integeralctrl.ExchangeIntegral) + } +} diff --git a/app/router/iteration_routes_test.go b/app/router/iteration_routes_test.go new file mode 100644 index 0000000..c570515 --- /dev/null +++ b/app/router/iteration_routes_test.go @@ -0,0 +1,32 @@ +package router + +import ( + "testing" + + "github.com/gin-gonic/gin" +) + +func TestIterationAppRoutes(t *testing.T) { + gin.SetMode(gin.TestMode) + engine := gin.New() + group := engine.Group("/api/app") + vidRouter(group) + paymentGuideRouter(group) + sceneBannerRouter(group) + + routes := make(map[string]struct{}) + for _, route := range engine.Routes() { + routes[route.Method+" "+route.Path] = struct{}{} + } + expected := []string{ + "GET /api/app/vid/module/:subModuleID/refresh", + "GET /api/app/payment/guide", + "POST /api/app/payment/guide/impression", + "GET /api/app/banner/list", + } + for _, route := range expected { + if _, ok := routes[route]; !ok { + t.Errorf("route %q is not registered", route) + } + } +} diff --git a/app/router/like.go b/app/router/like.go new file mode 100644 index 0000000..635fd53 --- /dev/null +++ b/app/router/like.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/app/api/likectrl" + + "github.com/gin-gonic/gin" +) + +func thumbsRouter(router *gin.RouterGroup) { + { + router.POST("/thumbsUp", likectrl.ThumbsUp) + router.POST("/thumbsDown", likectrl.ThumbsDown) + router.GET("/like/record/list", likectrl.RecordList) + } +} diff --git a/app/router/media.go b/app/router/media.go new file mode 100755 index 0000000..82fce54 --- /dev/null +++ b/app/router/media.go @@ -0,0 +1,24 @@ +package router + +import ( + "91porn-server/app/api/mediactrl" + "github.com/gin-gonic/gin" +) + +// mediaRouter 移动端接口 +func mediaRouter(router *gin.RouterGroup) { + group := router.Group("/media") + { + group.GET("/list", mediactrl.List) + group.GET("/info", mediactrl.Info) + group.GET("/topic", mediactrl.GetTopicList) + group.GET("/library", mediactrl.Library) + group.POST("/library/search", mediactrl.LibrarySearch) + group.GET("/search", mediactrl.Search) + // 搜索推荐标签 + group.GET("/recommend", mediactrl.Recommend) + group.GET("/ranking", mediactrl.Ranking) + group.GET("/hot", mediactrl.Hot) + group.GET("/my_buy", mediactrl.MyBuy) + } +} diff --git a/app/router/mediabookshelf.go b/app/router/mediabookshelf.go new file mode 100755 index 0000000..51fbe6a --- /dev/null +++ b/app/router/mediabookshelf.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/app/api/mediabookshelfctrl" + "github.com/gin-gonic/gin" +) + +// mediaBookshelf 移动端接口 +func mediaBookshelfRouter(router *gin.RouterGroup) { + group := router.Group("/media_bookshelf") + { + group.GET("/list", mediabookshelfctrl.List) + group.POST("/add", mediabookshelfctrl.Add) + group.POST("/del/batch", mediabookshelfctrl.DelBatch) + group.POST("/del", mediabookshelfctrl.Del) + } +} diff --git a/app/router/mediacontent.go b/app/router/mediacontent.go new file mode 100755 index 0000000..0c64932 --- /dev/null +++ b/app/router/mediacontent.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/app/api/mediacontentctrl" + "github.com/gin-gonic/gin" +) + +// mediaContent 移动端接口 +func mediaContentRouter(router *gin.RouterGroup) { + group := router.Group("/media_content") + { + group.GET("/list", mediacontentctrl.List) + group.GET("/info", mediacontentctrl.Info) + } +} diff --git a/app/router/mediatag.go b/app/router/mediatag.go new file mode 100755 index 0000000..3e30131 --- /dev/null +++ b/app/router/mediatag.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/app/api/mediatagctrl" + "github.com/gin-gonic/gin" +) + +// mediaTag 移动端接口 +func mediaTagRouter(router *gin.RouterGroup) { + group := router.Group("/media_tag") + { + group.GET("/list", mediatagctrl.List) + group.GET("/info", mediatagctrl.Info) + } +} diff --git a/app/router/message.go b/app/router/message.go new file mode 100644 index 0000000..583103e --- /dev/null +++ b/app/router/message.go @@ -0,0 +1,21 @@ +package router + +import ( + "91porn-server/app/api/messagectrl" + "91porn-server/app/middleware/limitHandler" + "github.com/gin-gonic/gin" +) + +func messageRouter(router *gin.RouterGroup) { + group := router.Group("/msg") + { + group.GET("/dynamic/noRedNum", messagectrl.NoRedNum) + group.GET("/dynamic/list", messagectrl.List) + group.GET("/session/list", messagectrl.SessionList) + group.POST("/session/del", messagectrl.DelSession) + group.GET("/session/get", messagectrl.GetSessionId) + group.GET("/message/list", messagectrl.MessageList) + group.POST("/priLetter/add", limitHandler.FilterRequestLimit, messagectrl.PrivateLetter) + group.POST("/read", messagectrl.Read) + } +} diff --git a/app/router/mine.go b/app/router/mine.go new file mode 100644 index 0000000..3ff1bd4 --- /dev/null +++ b/app/router/mine.go @@ -0,0 +1,23 @@ +package router + +import ( + "91porn-server/app/api/minectrl" + + "github.com/gin-gonic/gin" +) + +func mineRouter(router *gin.RouterGroup) { + group := router.Group("/mine") + { + group.POST("/report", minectrl.Report) + group.GET("/report/types/list", minectrl.ReportTypesList) + group.POST("/feedback", minectrl.FeedBack) + group.GET("/feedback/list", minectrl.FeedBackList) + group.POST("/follow", minectrl.Follow) + group.POST("/collect", minectrl.Collect) //用户收藏信息 + group.POST("/collect/batch/cancel", minectrl.BatchCancelCollect) //批量取消收藏 + group.GET("/collect/infoList", minectrl.InfoList) + group.GET("/userresource/list", minectrl.UserResourceList) + group.POST("/download/use", minectrl.UserDownload) + } +} diff --git a/app/router/module.go b/app/router/module.go new file mode 100644 index 0000000..7838196 --- /dev/null +++ b/app/router/module.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/app/api/modulectrl" + + "github.com/gin-gonic/gin" +) + +func moduleRouter(r *gin.RouterGroup) { + module := r.Group("/modules") + { + module.GET("/list", modulectrl.List) + module.GET("/announce", modulectrl.Announcements) + } +} diff --git a/app/router/nakedchat.go b/app/router/nakedchat.go new file mode 100755 index 0000000..1a2b1ed --- /dev/null +++ b/app/router/nakedchat.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/app/api/nakedchatctrl" + "github.com/gin-gonic/gin" +) + +// nakedChat 移动端接口 +func nakedChatRouter(router *gin.RouterGroup) { + group := router.Group("/nakedchat") + { + group.GET("/list", nakedchatctrl.List) + group.GET("/info", nakedchatctrl.Info) + } +} diff --git a/app/router/nakedchatorder.go b/app/router/nakedchatorder.go new file mode 100755 index 0000000..c87cd7d --- /dev/null +++ b/app/router/nakedchatorder.go @@ -0,0 +1,14 @@ +package router + +import ( + "91porn-server/app/api/nakedchatorderctrl" + "github.com/gin-gonic/gin" +) + +// nakedChatOrder 移动端接口 +func nakedChatOrderRouter(router *gin.RouterGroup) { + group := router.Group("/nakedchatorder") + { + group.GET("/list", nakedchatorderctrl.List) + } +} diff --git a/app/router/newactivity.go b/app/router/newactivity.go new file mode 100644 index 0000000..8593acc --- /dev/null +++ b/app/router/newactivity.go @@ -0,0 +1,31 @@ +package router + +import ( + "91porn-server/app/api/newactivityctrl" + + "github.com/gin-gonic/gin" +) + +func newActivityRoute(router *gin.RouterGroup) { + g := router.Group("/newactivity") + { + g.POST("userBanlance", newactivityctrl.UserBalance) + g.POST("modelList", newactivityctrl.ModelList) + g.POST("findOneModel", newactivityctrl.FindOneModel) + g.POST("buyGiftsSchedule", newactivityctrl.BuyGiftsSchedule) + g.POST("buyGifts", newactivityctrl.BuyGifts) + g.POST("winRecords", newactivityctrl.WinRecords) + //问卷调查 + questionnaire := g.Group("/questionnaire") + { + questionnaire.POST("/submit", newactivityctrl.Submit) + questionnaire.GET("getQuestionnaireByUser", newactivityctrl.GetQuestionnaireByUsers) + } + } + game := router.Group("/game") + { + game.GET("userInfo", newactivityctrl.UserInfo) + game.GET("deduct", newactivityctrl.Deduct2Coins) + game.GET("code", newactivityctrl.RecordRewardCode) + } +} diff --git a/app/router/notification.go b/app/router/notification.go new file mode 100644 index 0000000..00e3a87 --- /dev/null +++ b/app/router/notification.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/app/api/notictrl" + "91porn-server/app/middleware/limitHandler" + + "github.com/gin-gonic/gin" +) + +func notificationRoute(router *gin.RouterGroup) { + group := router.Group("/notification") + { + group.POST("/captcha", limitHandler.FilterSMSCaptchaByIP, limitHandler.FilterSMSCaptchaByUID, notictrl.SendCaptcha) + } +} diff --git a/app/router/official.go b/app/router/official.go new file mode 100644 index 0000000..984cdbf --- /dev/null +++ b/app/router/official.go @@ -0,0 +1,13 @@ +package router + +import ( + "91porn-server/app/api/officialctrl" + "github.com/gin-gonic/gin" +) + +func officialRouter(router *gin.RouterGroup) { + group := router.Group("/official") + { + group.GET("/list", officialctrl.List) + } +} diff --git a/app/router/officialWebsite.go b/app/router/officialWebsite.go new file mode 100644 index 0000000..6e4cb6a --- /dev/null +++ b/app/router/officialWebsite.go @@ -0,0 +1,62 @@ +package router + +import ( + "91porn-server/app/api/officialWebsitectrl" + "91porn-server/app/api/updownctrl" + + "github.com/gin-gonic/gin" +) + +func newOfficialWebsiteRoute(router *gin.Engine) { + g := router.Group("/officialWebsite") + g.GET("", officialWebsitectrl.GetBasicData) // 品牌站基础数据接口,包含基础静态数据和CMS动态数据 + + hero := g.Group("/hero") + { + hero.GET("/list", officialWebsitectrl.HeroList) + hero.GET("/:id", officialWebsitectrl.HeroDetail) + } + + album := g.Group("/album") + { + album.GET("/list", officialWebsitectrl.AlbumList) + album.GET("/:id", officialWebsitectrl.AlbumDetail) + } + + video := g.Group("/video") + { + video.GET("/list", officialWebsitectrl.VideoList) + video.GET("/:id", officialWebsitectrl.VideoDetail) + video.GET("/:id/m3u8/*source", officialWebsitectrl.VideoCheck, updownctrl.DownloadM3u8H5) + } + + tag := g.Group("/tag") + { + tag.GET("/list", officialWebsitectrl.TagList) + } + + recruitForm := g.Group("/recruitForm") + { + recruitForm.POST("/", officialWebsitectrl.RecruitForm) + } + + partner := g.Group("/partner") + { + partner.GET("/list", officialWebsitectrl.PartnerList) + } + + jobList := g.Group("/jobList") + { + jobList.GET("/list", officialWebsitectrl.JobList) + } + + business := g.Group("/business") + { + business.GET("/list", officialWebsitectrl.BusinessList) + } + news := g.Group("/news") + { + news.GET("/list", officialWebsitectrl.NewsList) + news.GET("/:id", officialWebsitectrl.NewsDetail) + } +} diff --git a/app/router/payment_guide.go b/app/router/payment_guide.go new file mode 100644 index 0000000..2c4ea85 --- /dev/null +++ b/app/router/payment_guide.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/app/api/paymentguidectrl" + + "github.com/gin-gonic/gin" +) + +func paymentGuideRouter(router *gin.RouterGroup) { + group := router.Group("/payment") + { + group.GET("/guide", paymentguidectrl.Get) + group.POST("/guide/impression", paymentguidectrl.Impression) + } +} diff --git a/app/router/ping.go b/app/router/ping.go new file mode 100644 index 0000000..f46ed58 --- /dev/null +++ b/app/router/ping.go @@ -0,0 +1,24 @@ +package router + +import ( + "91porn-server/app/api/pingctrl" + + "github.com/gin-gonic/gin" +) + +func pingRouter(router *gin.RouterGroup) { + group := router.Group("/ping") + { + group.GET("/domain", pingctrl.DomainList) + group.GET("/check", pingctrl.Ping) + group.GET("/pass/:buildId/:ver", pingctrl.Pass) + group.GET("/sysDate", pingctrl.GetSysDate) + group.GET("/v", pingctrl.M) + group.GET("/domain/h5", pingctrl.Domain) + group.GET("/domain/refresh", pingctrl.DomainRefresh) + // 检查消息小红点 + group.GET("/checkMessageTip", pingctrl.CheckMessageTip) + group.GET("/store_url", pingctrl.StoreUrl) + group.GET("/banner/:id", pingctrl.GetBannerJump) + } +} diff --git a/app/router/product.go b/app/router/product.go new file mode 100644 index 0000000..c158a57 --- /dev/null +++ b/app/router/product.go @@ -0,0 +1,21 @@ +package router + +import ( + "91porn-server/app/api/productctrl" + "91porn-server/app/middleware/limitHandler" + + "github.com/gin-gonic/gin" +) + +func productRouter(router *gin.RouterGroup) { + group := router.Group("/product") + { + group.GET("/coupon", productctrl.GetCouponDetail) + group.POST("/buy", limitHandler.FilterRequestLimit, productctrl.BuyProduct) + group.POST("/delBrought", productctrl.DelBroughtProductHistory) + group.GET("/getCoin", productctrl.GetCoinMonthCoin) + group.POST("/exchangeIntegral", productctrl.ExchangeIntegral) + group.GET("/getAwVip", productctrl.GetAwVip) + group.GET("/advanceStatus", productctrl.AdvanceStatus) + } +} diff --git a/app/router/proxy.go b/app/router/proxy.go new file mode 100644 index 0000000..bc68322 --- /dev/null +++ b/app/router/proxy.go @@ -0,0 +1,13 @@ +package router + +import ( + "91porn-server/app/api/userctrl" + "github.com/gin-gonic/gin" +) + +func proxyRouter(router *gin.RouterGroup) { + group := router.Group("/proxy") + { + group.POST("/bind", userctrl.Invite) + } +} diff --git a/app/router/publish.go b/app/router/publish.go new file mode 100644 index 0000000..596f591 --- /dev/null +++ b/app/router/publish.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/app/api/publishctrl" + + "github.com/gin-gonic/gin" +) + +func publishRouter(router *gin.RouterGroup) { + group := router.Group("/publish") + { + group.GET("/details", publishctrl.Details) + group.GET("/leaderboard", publishctrl.WeekLeaderboard) + } +} diff --git a/app/router/rank.go b/app/router/rank.go new file mode 100644 index 0000000..4c2ca7b --- /dev/null +++ b/app/router/rank.go @@ -0,0 +1,14 @@ +package router + +import ( + "91porn-server/app/api/rankctrl" + + "github.com/gin-gonic/gin" +) + +func rankRouter(router *gin.RouterGroup) { + group := router.Group("/rank") + { + group.GET("/hotsearch/list", rankctrl.HotSearchList) + } +} diff --git a/app/router/rchg.go b/app/router/rchg.go new file mode 100644 index 0000000..7ab1f03 --- /dev/null +++ b/app/router/rchg.go @@ -0,0 +1,25 @@ +package router + +import ( + "91porn-server/app/api/rechargectrl" + + "github.com/gin-gonic/gin" +) + +func rchgRouter(router *gin.RouterGroup) { + group := router.Group("/mine") + { + // 获取充值类型 + group.GET("/rechargeTypeList", rechargectrl.GetRechargeType) + // 获取充值类型 + group.GET("/rchgTypeList", rechargectrl.GetRechargeType) + //获取充值记录列表 + group.GET("/rchg/order", rechargectrl.GetRecHistory) + //获取货币列表 + group.GET("/currencys", rechargectrl.CurrencyList) + // 充值 + group.POST("/topay", rechargectrl.NewRecharge) + // 获取用户的交易记录 + group.POST("/transaction", rechargectrl.GetUserTransactions) + } +} diff --git a/app/router/recomm.go b/app/router/recomm.go new file mode 100644 index 0000000..9a12544 --- /dev/null +++ b/app/router/recomm.go @@ -0,0 +1,18 @@ +package router + +import ( + "91porn-server/app/api/recommctrl" + + "github.com/gin-gonic/gin" +) + +func recommendRouter(router *gin.RouterGroup) { + group := router.Group("/recommend") + { + group.GET("/user/list", recommctrl.GetUserList) + group.GET("/light/vids", recommctrl.GetLightVidList) + group.GET("/vid/ad", recommctrl.GetVidAd) + group.GET("/vid/list", recommctrl.GetVidList) + group.GET("/vid/list/discover", recommctrl.GetShortDiscoverList) + } +} diff --git a/app/router/recreation.go b/app/router/recreation.go new file mode 100644 index 0000000..264e0b5 --- /dev/null +++ b/app/router/recreation.go @@ -0,0 +1,12 @@ +package router + +import ( + "91porn-server/app/api/recreationctrl" + "github.com/gin-gonic/gin" +) + +func recreationRouter(router *gin.RouterGroup) { + group := router.Group("/recreation") + group.GET("/list", recreationctrl.List) + group.POST("/click", recreationctrl.Click) +} diff --git a/app/router/scene_banner.go b/app/router/scene_banner.go new file mode 100644 index 0000000..1e5bd79 --- /dev/null +++ b/app/router/scene_banner.go @@ -0,0 +1,14 @@ +package router + +import ( + "91porn-server/app/api/scenebannerctrl" + + "github.com/gin-gonic/gin" +) + +func sceneBannerRouter(router *gin.RouterGroup) { + group := router.Group("/banner") + { + group.GET("/list", scenebannerctrl.List) + } +} diff --git a/app/router/search.go b/app/router/search.go new file mode 100644 index 0000000..d81b9f0 --- /dev/null +++ b/app/router/search.go @@ -0,0 +1,20 @@ +package router + +import ( + "91porn-server/app/api/searchctrl" + + "github.com/gin-gonic/gin" +) + +func searchRouter(router *gin.RouterGroup) { + group := router.Group("/search") + { + group.GET("/index", searchctrl.IndexList) + group.POST("/list", searchctrl.List) + group.GET("/wonder/list", searchctrl.WonderTagList) + group.GET("/hotTag", searchctrl.HotTagSearch) + group.GET("/hotVid/list", searchctrl.HotVid) + group.GET("/hotPublisher/list", searchctrl.HotPublisher) + group.GET("/publisher/list", searchctrl.PublisherList) + } +} diff --git a/app/router/share.go b/app/router/share.go new file mode 100644 index 0000000..a72a6ac --- /dev/null +++ b/app/router/share.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/app/api/sharectrl" + + "github.com/gin-gonic/gin" +) + +func shareRouter(router *gin.RouterGroup) { + group := router.Group("/share") + { + group.POST("/output", sharectrl.GeneratorQrCode) + group.GET("/count", sharectrl.GetShareCnt) + } +} diff --git a/app/router/signrecord.go b/app/router/signrecord.go new file mode 100755 index 0000000..e513a05 --- /dev/null +++ b/app/router/signrecord.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/app/api/signrecordctrl" + + "github.com/gin-gonic/gin" +) + +// signRecord 移动端接口 +func signRecordRouter(router *gin.RouterGroup) { + group := router.Group("/sign_record") + { + group.POST("/resign", signrecordctrl.AgainSign) + } +} diff --git a/app/router/sms.go b/app/router/sms.go new file mode 100644 index 0000000..8885371 --- /dev/null +++ b/app/router/sms.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/app/api/smsctrl" + "91porn-server/app/middleware/limitHandler" + + "github.com/gin-gonic/gin" +) + +func smsRoute(router *gin.RouterGroup) { + group := router.Group("/sms") + { + group.POST("/captcha", limitHandler.FilterSMSCaptchaByIP, smsctrl.SendCaptcha) + } +} diff --git a/app/router/static.go b/app/router/static.go new file mode 100644 index 0000000..c7bcd45 --- /dev/null +++ b/app/router/static.go @@ -0,0 +1,13 @@ +package router + +import ( + "91porn-server/app/api/staticctrl" + + "github.com/gin-gonic/gin" +) + +func staticRouter(router *gin.RouterGroup) { + group := router.Group("static") + group.GET("/faq/html/index", staticctrl.HtmlFaqPage) + group.GET("/faq/tmpl/index", staticctrl.TmplFaqPage) +} diff --git a/app/router/store.go b/app/router/store.go new file mode 100644 index 0000000..7f43c96 --- /dev/null +++ b/app/router/store.go @@ -0,0 +1,18 @@ +package router + +import ( + "91porn-server/app/api/walletctrl" + "github.com/gin-gonic/gin" +) + +func storeWalletRouter(router *gin.RouterGroup) { + wallet := router.Group("/store_wallet") + { + // 获取余额 + wallet.GET("/balance", walletctrl.WalletBalance) + // 扣款 + wallet.POST("/deduct", walletctrl.WalletDeduct) + // 退款 + wallet.POST("/refund", walletctrl.WalletRefund) + } +} diff --git a/app/router/tag.go b/app/router/tag.go new file mode 100644 index 0000000..57973c6 --- /dev/null +++ b/app/router/tag.go @@ -0,0 +1,26 @@ +package router + +import ( + "91porn-server/app/api/tagctrl" + + "github.com/gin-gonic/gin" +) + +func tagRouter(router *gin.RouterGroup) { + group := router.Group("/tag") + { + group.GET("/list", tagctrl.TagList) //获取专题所有标签列表 + group.GET("/v2/list", tagctrl.V2TagList) //上传常用的标签列表 2020-10-20 后新版本使用 + group.GET("/user/list", tagctrl.UserTagList) //查询用户标签列表 + group.GET("/vid/list", tagctrl.VidList) //根据标签列表查询视频列表(第二次请求查询同一标签下的第二波视频) + group.GET("/media/list", tagctrl.MediaList) + group.GET("/related/list", tagctrl.RelatedTagList) //根据标签名字模糊查询标签信息 + group.GET("/group", tagctrl.Group) //查询所有的标签,显示标签列表及下面视频(初始化查询,查询6个标签,和每个标签下面的三个视频) + group.GET("/info", tagctrl.GetTagInfo) //获取标签详情 + // group.GET("/list/most-played", tagctrl.TagListMostPlayed) + group.GET("/conf/list", tagctrl.TagConfList) //获取后台配置的所有标签 + group.POST("/add", tagctrl.AddUserTag) //根据标签搜索结果 添加标签到用户标签列表中 + group.POST("/add/new", tagctrl.AddNewTag) //新增标签,添加到标签库 + group.DELETE("/deleteUserTag", tagctrl.DeleteUserTag) //删除用户标签 + } +} diff --git a/app/router/task.go b/app/router/task.go new file mode 100644 index 0000000..4106551 --- /dev/null +++ b/app/router/task.go @@ -0,0 +1,25 @@ +package router + +import ( + "91porn-server/app/api/taskctrl" + + "github.com/gin-gonic/gin" +) + +func taskRouter(router *gin.RouterGroup) { + task := router.Group("/task") + { + // 本项目暂未用到 + //task.GET("", taskctrl.GetTaskList) // 获取普通任务(不包括一次性任务和每日任务) + //task.POST("/boon", taskctrl.GetBoon) // 用户领取普通任务奖励(不包括一次性任务和每日任务) + //task.GET("/detail", taskctrl.GetTaskDetails) // 获取普通任务详情 + + task.GET("/sign", taskctrl.GetSignDetails) // 获取签到信息 + task.POST("/sign", taskctrl.Sign) // 用户签到 + task.POST("/resign", taskctrl.ReSign) // 用户补签 + task.GET("/sign-extra-prizes", taskctrl.SignExtraPrizes) // 签到额外信息 + task.GET("/list", taskctrl.GetNewTask) // 获取每日任务列表 + task.POST("/receive", taskctrl.Receive) + task.POST("/do", taskctrl.Do) + } +} diff --git a/app/router/tone.go b/app/router/tone.go new file mode 100644 index 0000000..0dcab3d --- /dev/null +++ b/app/router/tone.go @@ -0,0 +1,14 @@ +package router + +import ( + "91porn-server/app/api/tonectrl" + + "github.com/gin-gonic/gin" +) + +func toneRouter(router *gin.RouterGroup) { + group := router.Group("/tone") + { + group.POST("/vid/list", tonectrl.VidList) + } +} diff --git a/app/router/txnact.go b/app/router/txnact.go new file mode 100644 index 0000000..863c718 --- /dev/null +++ b/app/router/txnact.go @@ -0,0 +1,47 @@ +package router + +import ( + "91porn-server/app/api/txnactctr" + + "github.com/gin-gonic/gin" +) + +func txnactRouter(router *gin.RouterGroup) { + group := router.Group("/mine") + { + txnactGroup := group.Group("/txnact") + txnactGroup.DELETE("/del", txnactctr.DelTxAccount) + { + bank := txnactGroup.Group("/bank") + { + //银行卡绑定 + bank.GET("/get", txnactctr.GetBanks) + bank.POST("/add", txnactctr.AddBank) + bank.POST("/update", txnactctr.EditTransactionAct) + } + alipay := txnactGroup.Group("/alipay") + { + //支付宝 + alipay.GET("/get", txnactctr.GetAlipays) + alipay.POST("/add", txnactctr.AddAliPay) + alipay.POST("/update", txnactctr.EditTransactionAct) + } + zfb := txnactGroup.Group("/zfb") + { + //支付宝 + zfb.GET("/get", txnactctr.GetAlipays) + zfb.POST("/add", txnactctr.AddAliPay) + zfb.POST("/update", txnactctr.EditTransactionAct) + + } + yh := txnactGroup.Group("/yh") + { + //银行卡绑定 + yh.GET("/get", txnactctr.GetBanks) + yh.POST("/add", txnactctr.AddBank) + yh.POST("/update", txnactctr.EditTransactionAct) + } + + } + } +} diff --git a/app/router/updownLoad.go b/app/router/updownLoad.go new file mode 100644 index 0000000..7b8219b --- /dev/null +++ b/app/router/updownLoad.go @@ -0,0 +1,29 @@ +package router + +import ( + "91porn-server/app/api/updownctrl" + "91porn-server/app/middleware/limitHandler" + + "github.com/gin-gonic/gin" +) + +func updownloadRouter(router *gin.RouterGroup) { + group := router.Group("/vid") + { + group.POST("/upload", updownctrl.Upload) + group.POST("/uploadStatic", updownctrl.UploadStatic) + group.POST("/uploadStatic/batch", updownctrl.UploadStaticBatch) + group.POST("/uploadDotStream", updownctrl.UploadDotStream) + group.POST("/uploadDotJson", updownctrl.UploadDotJson) + group.GET("/m3u8/*source", limitHandler.M3u8HourlyLimit, updownctrl.DownloadM3u8) + group.StaticFile("/sec", "config/enc.key") + group.StaticFile("/new/sec", "config/enc.key") + group.StaticFile("/pms/sec", "config/enc_pms.key") + group.StaticFile("/pms/mt_sec", "config/enkeymt.key") + group.StaticFile("/lsjsec", "config/lsjm3u8.key") + group.StaticFile("/m3u8sec", "config/m3u8.key") + group.GET("/h5/m3u8/*source", updownctrl.RequireM3u8Ticket, limitHandler.Limit, limitHandler.M3u8HourlyLimit, updownctrl.DownloadM3u8H5) + group.GET("/h5/light/m3u8/*source", updownctrl.RequireM3u8Ticket, updownctrl.DownloadM3u8H5) + group.GET("/transcode/m3u8/*source", updownctrl.DownloadTranscodeM3u8) + } +} diff --git a/app/router/user.go b/app/router/user.go new file mode 100644 index 0000000..d7d1266 --- /dev/null +++ b/app/router/user.go @@ -0,0 +1,54 @@ +package router + +import ( + "91porn-server/app/api/userctrl" + + "github.com/gin-gonic/gin" +) + +func userRouter(router *gin.RouterGroup) { + group := router.Group("/mine") + { + group.POST("/mobileLoginOnly", userctrl.MobileLogin) + group.POST("/mobileBind", userctrl.MobileBind) + group.POST("/login", userctrl.LoginWithDevice) + group.POST("/info", userctrl.Modify) + group.GET("/info", userctrl.Info) + group.GET("/rights", userctrl.MyRights) + group.GET("/setting", userctrl.ISetting) + group.POST("/setting", userctrl.ModifySetting) + group.GET("comment", userctrl.Comment) + group.GET("/like", + // TODO: 前端请求太过频繁时会造成页码重复、丢失,临时加上频率限制 + // limitHandler.FilterRequestByUserAndTerminal(redisconst.GetUserLikeRateLimitKey(), + // redisconst.GetUserLikeRateLimitExpire(), stderr.GetLikesBusy), + userctrl.Like) + group.GET("/collection", userctrl.Collection) + group.GET("/publish", userctrl.PublishList) + group.GET("/publish/num", userctrl.PublishNum) + group.POST("/publish/delete", userctrl.PublishWorkDelete) + group.GET("/buyVid", + // limitHandler.FilterRequestByUserAndTerminal(redisconst.GetUserBuyVidRateLimitKey(), + // redisconst.GetUserLikeRateLimitExpire(), stderr.GetLikesBusy), + userctrl.BuyVidList) + group.GET("/faq/list", userctrl.FaqList) + group.GET("/faq/cate/list", userctrl.FaqCateList) + group.GET("/certificate/qr", userctrl.QrCode) + group.GET("/autoLocate", userctrl.AutoLocate) + group.POST("/inviteBind", userctrl.Invite) + group.POST("/inviter", userctrl.Inviter) + group.GET("/announce", userctrl.Announce) + group.GET("/announce/list", userctrl.AnnounceList) + group.POST("/login/h5", + userctrl.LoginWithH5) + group.POST("/login/h5/qr", userctrl.LoginByQRCode) + group.POST("/privilege/consume", userctrl.ConsumePrivilege) + + } + group.POST("/resetpassword", userctrl.ResetPassWord) + group.POST("/resetpassword/mobileverify", userctrl.MobileVerify) + group.POST("/register", userctrl.Register) + group.POST("/qrcode/extract", userctrl.ExtractQRCode) + group.GET("/watch_record/list", userctrl.WatchRecord) + group.GET("/portrait", userctrl.Portrait) +} diff --git a/app/router/userInvite.go b/app/router/userInvite.go new file mode 100644 index 0000000..49c221f --- /dev/null +++ b/app/router/userInvite.go @@ -0,0 +1,23 @@ +package router + +import ( + "91porn-server/app/api/statcenterctl" + + "github.com/gin-gonic/gin" +) + +// userInviteRouter 全名代理 +func userInviteRouter(router *gin.RouterGroup) { + group := router.Group("/userinvite") + { + group.POST("/callback/recharge", statcenterctl.StatcenterRechargeCallBack) // 充值成功回掉 + group.POST("/userlist", statcenterctl.UserInviteList) + group.POST("/incomelist", statcenterctl.UserInviteIncomeList) + group.POST("/info", statcenterctl.UserInviteInfo) + group.POST("/videolist", statcenterctl.UserVideoList) + } + statgroup := router.Group("/statcenter") + { + statgroup.POST("/sync", statcenterctl.StatcenterSyncList) + } +} diff --git a/app/router/version.go b/app/router/version.go new file mode 100644 index 0000000..781cb38 --- /dev/null +++ b/app/router/version.go @@ -0,0 +1,14 @@ +package router + +import ( + "91porn-server/app/api/versionctrl" + + "github.com/gin-gonic/gin" +) + +func versionRouter(router *gin.RouterGroup) { + group := router.Group("/mine") + { + group.GET("/ver", versionctrl.GetVersion) + } +} diff --git a/app/router/vid.go b/app/router/vid.go new file mode 100644 index 0000000..ffdbf82 --- /dev/null +++ b/app/router/vid.go @@ -0,0 +1,49 @@ +package router + +import ( + "91porn-server/app/api/vidctrl" + + "github.com/gin-gonic/gin" +) + +func vidRouter(router *gin.RouterGroup) { + group := router.Group("/vid") + { + group.GET("/location/list", vidctrl.GetLocationList) + group.GET("/location/hot", vidctrl.GetLocationHot) + group.GET("/info", vidctrl.GetVidInfo) + group.POST("/submit", vidctrl.Submit) + group.POST("/play", vidctrl.PlayRecord) + group.POST("/play/consume", vidctrl.ConsumeWatchCount) + group.GET("/hot/list", vidctrl.HotList) + group.GET("/hotkw/list", vidctrl.HotKWList) + group.GET("/location/info", vidctrl.GetLocation) + group.GET("/user/count", vidctrl.GetWatchCount) + group.POST("/remove", vidctrl.RemoveMyVideos) + group.GET("/news/list", vidctrl.GetNewsList) + group.POST("/news/unlike", vidctrl.DoUnlike) + group.GET("/community/recommend", vidctrl.CommunityRecommend) + group.GET("/module/:subModuleID/refresh", vidctrl.RefreshModuleVideos) + group.GET("/module/:subModuleID", vidctrl.ModuleVideoList) + group.GET("/section/:sectionID", vidctrl.SectionVideoList) + group.GET("/section/changeVideo/:sectionID", vidctrl.ChangeSectionVideoList) + group.GET("/section/changeMedia/:sectionID", vidctrl.ChangeSectionMediaList) + group.GET("/section/list", vidctrl.SectionList) + group.GET("/moduleVideo", vidctrl.AllVideosOfModule) + group.GET("/list", vidctrl.List) + group.GET("/ranking", vidctrl.Ranking) + group.GET("/sections", vidctrl.Sections) + group.GET("/module/short/all", vidctrl.RecommendShortVideos) + group.GET("/recommend/list", vidctrl.RecommendList) + group.POST("/recommand/vidPlay", vidctrl.VidPlayRecommand) // 视频播放页的推荐视频列表 + group.GET("/recommand/search", vidctrl.SearchRecommand) // 搜索页面的视频播放列表 + group.GET("/library", vidctrl.Library) // 片库首页 + group.POST("/library/search", vidctrl.LibrarySearch) // 片库搜索 + // 折扣专区相关 + group.GET("/discount/list", vidctrl.DiscountVidList) + group.GET("/discount/area", vidctrl.DiscountArea) + group.GET("/module/all/:subModuleID", vidctrl.SubModuleAllVideoList) // 首页视频猜你喜欢 + group.GET("/home/new/list", vidctrl.HomeMostNewModuleVideoList) // 91pron 主页最新 + + } +} diff --git a/app/router/vid_consume_route_test.go b/app/router/vid_consume_route_test.go new file mode 100644 index 0000000..54725f8 --- /dev/null +++ b/app/router/vid_consume_route_test.go @@ -0,0 +1,30 @@ +package router + +import ( + "testing" + + "github.com/gin-gonic/gin" +) + +func TestVideoPlayConsumeRoute(t *testing.T) { + gin.SetMode(gin.TestMode) + engine := gin.New() + vidRouter(engine.Group("/api/app")) + + foundConsume := false + foundLegacyCount := false + for _, route := range engine.Routes() { + if route.Method == "POST" && route.Path == "/api/app/vid/play/consume" { + foundConsume = true + } + if route.Method == "GET" && route.Path == "/api/app/vid/user/count" { + foundLegacyCount = true + } + } + if !foundConsume { + t.Error("POST /api/app/vid/play/consume is not registered") + } + if !foundLegacyCount { + t.Error("legacy GET /api/app/vid/user/count is not registered") + } +} diff --git a/app/router/video_gold_coin.go b/app/router/video_gold_coin.go new file mode 100644 index 0000000..19c100a --- /dev/null +++ b/app/router/video_gold_coin.go @@ -0,0 +1,13 @@ +package router + +import ( + "91porn-server/app/api/video_gold_coin_ctrl" + "github.com/gin-gonic/gin" +) + +func videoGoldCoinRouter(router *gin.RouterGroup) { + group := router.Group("/video_gold_coin") + { + group.GET("/list", video_gold_coin_ctrl.GetVideoGoldCoin) + } +} diff --git a/app/router/videoactivity.go b/app/router/videoactivity.go new file mode 100644 index 0000000..3833e0d --- /dev/null +++ b/app/router/videoactivity.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/app/api/videoactivityctrl" + + "github.com/gin-gonic/gin" +) + +func videoActivityRouter(router *gin.RouterGroup) { + group := router.Group("/video_activity") + { + group.GET("/list", videoactivityctrl.List) + group.GET("/history_record", videoactivityctrl.HistoryRecord) + } +} diff --git a/app/router/vip.go b/app/router/vip.go new file mode 100644 index 0000000..a55ed80 --- /dev/null +++ b/app/router/vip.go @@ -0,0 +1,20 @@ +package router + +import ( + "91porn-server/app/api/vipctrl" + + "github.com/gin-gonic/gin" +) + +func vipRouter(router *gin.RouterGroup) { + group := router.Group("/vip") + { + group.GET("/product", vipctrl.Product) + group.GET("/history", vipctrl.PurchaseHistory) + group.GET("/tiroCountdown", vipctrl.CountdownTiroCard) + group.GET("/Announ", vipctrl.VIPUpAnnouns) //升级公告 + group.GET("/upInfo", vipctrl.VIPUpInfo) //升级信息 + group.POST("/up", vipctrl.VIPUp) //升级信息 + //group.GET("/recommend", vipctrl.VipRecommend) + } +} diff --git a/app/router/wallet.go b/app/router/wallet.go new file mode 100644 index 0000000..cd95fcb --- /dev/null +++ b/app/router/wallet.go @@ -0,0 +1,25 @@ +package router + +import ( + "91porn-server/app/api/walletctrl" + + "github.com/gin-gonic/gin" +) + +func walletRouter(router *gin.RouterGroup) { + group := router.Group("/mine") + { + //获取钱包数量 + group.GET("/wallet", walletctrl.GetWalletAmount) + //获取钱包数量 + group.GET("/qianbao", walletctrl.GetWalletAmount) + //我的收益入支出(玩家与玩家之间) + group.GET("/income/works", walletctrl.GetWorksIncome) + group.GET("/bills", walletctrl.GetBill) + // group.GET("/zhangdan", walletctrl.GetBill) + group.GET("/iIncomes", walletctrl.GetIncome) + group.GET("/getAllIncome", walletctrl.GetAllIncome) + group.GET("/zhangdan", walletctrl.GetNewBill) + group.GET("/fruitCoin/bill", walletctrl.FruitCoinBill) + } +} diff --git a/app/router/withdraw.go b/app/router/withdraw.go new file mode 100644 index 0000000..fb37f37 --- /dev/null +++ b/app/router/withdraw.go @@ -0,0 +1,27 @@ +package router + +import ( + "91porn-server/app/api/withdrawctrl" + "91porn-server/app/middleware/limitHandler" + "91porn-server/common/constant/redisconst" + "91porn-server/common/stderr" + + "github.com/gin-gonic/gin" +) + +func withdrawRouter(router *gin.RouterGroup) { + { + //提现 + router.POST("/withdraw", + limitHandler.FilterRequestByUser(redisconst.WithdrawLimtKey(), redisconst.WithdrawLimtKeyExpire(), + stderr.WithdrawBusy), + withdrawctrl.Withdraw, + ) + //提现类型 + router.GET("/withdraw/type", withdrawctrl.GetType) + //获取提现订单列表 + router.GET("/withdraw/order", withdrawctrl.WithdrawOrder) + //获取提现配置列表 + router.GET("/withdraw/cfg", withdrawctrl.GetWithdrawCfg) + } +} diff --git a/app/router/wonder.go b/app/router/wonder.go new file mode 100644 index 0000000..c36ed8f --- /dev/null +++ b/app/router/wonder.go @@ -0,0 +1,14 @@ +package router + +import ( + "91porn-server/app/api/wonderctrl" + + "github.com/gin-gonic/gin" +) + +func wonderRouter(router *gin.RouterGroup) { + group := router.Group("/wonder") + { + group.POST("/vid/list", wonderctrl.VidList) + } +} diff --git a/app/service/active2023ser/lottery.go b/app/service/active2023ser/lottery.go new file mode 100644 index 0000000..bea8105 --- /dev/null +++ b/app/service/active2023ser/lottery.go @@ -0,0 +1,414 @@ +package active2023ser + +import ( + "errors" + "fmt" + "time" + + "91porn-server/app/appg" + "91porn-server/common/db" + "91porn-server/models/commod" + "91porn-server/models/v/active2023mod" + "91porn-server/models/v/goldextramod" + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/videocoupon" + "91porn-server/models/v/videodiscountmod" + "91porn-server/models/v/walletmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func Lottery(userInfo *usermod.User, req LotteryReq) error { + wallet, err := walletmod.GetWallet(userInfo.UID) + if err != nil { + return err + } + if wallet == nil { + return errors.New("钱包信息未找到") + } + isLottery := true // 默认为抽奖 + desc := "2023新春抽奖" + if req.Gold == 0 && req.Count == 0 { // 不使用免费抽奖次数又不消耗金币, 则为领取保底福利 + isLottery = false + desc = "2023新春福利" + } + return appg.VideoDB.Trans(func(t *db.MongoTool) error { + var transactionLogs []txnmod.TransactionLog // 用户所有抽奖花费金, 获奖金币以及游戏币发放记录 + if isLottery { + if req.Gold == 0 { // 使用免费抽奖次数 + if err = active2023mod.UseLotteryCountFree(t, int64(userInfo.UID), int64(req.Count)); err != nil { + return err + } + } else { // 使用金币抽奖 + if err = walletmod.DebitAmountDefault(t, int64(userInfo.UID), int64(req.Gold)); err != nil { + return err + } + wallet.Amount -= int64(req.Gold) + if err = active2023mod.UserLotteryCountPay(t, int64(userInfo.UID), int64(req.Count)); err != nil { + return err + } + transactionType := txnmod.Active2023Cost + transactionLogs = []txnmod.TransactionLog{txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: userInfo.UID, + Amount: -int64(req.Gold), + ActualAmount: -float64(req.Gold), + TranType: transactionType.Key(), + TranTypeInt: int64(transactionType), + Desc: fmt.Sprintf("%s-金币[%d个]", transactionType.Key(), req.Gold), + RealAmount: walletmod.GetRealAmount(wallet), + }} + } + } + var totalGold, totalVipDates uint64 // totalGold: 用户总获奖金币数; totalVipDates: 用户总获奖vip天数 + var pzs []active2023mod.Prize // 用户所有奖励, 用以存库 + var videoCoupons []videocoupon.UserGoldVideoCoupon // 用户所有获视频折扣券 + var goldExtras []goldextramod.GoldExtra // 用户所有获奖金币加购券 + var videoDiscounts []productmod.Product // 用户所有获奖视频折扣券 + var vipCards []productmod.Product // 用户所有获奖vip卡 + now := time.Now() + pzs = make([]active2023mod.Prize, len(req.Prizes)) + for i, pz := range req.Prizes { + pzs[i] = active2023mod.Prize{ + UID: int64(userInfo.UID), + Type: int64(pz.Type), + Name: pz.Name, + Unit: int64(pz.Price), + CreatedAt: now, + UpdatedAt: now, + } + switch pz.Type { + case active2023mod.PrizeTypeGold: // 金币 + if pz.Price <= 0 { + return errors.New("金币数量设置错误") + } + totalGold += pz.Price + transactionType := txnmod.Active2023Reward + wallet.Amount += int64(pz.Price) + transactionLogs = append(transactionLogs, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: userInfo.UID, + Amount: int64(pz.Price), + ActualAmount: float64(pz.Price), + TranType: transactionType.Key(), + TranTypeInt: int64(transactionType), + Desc: fmt.Sprintf("%s-金币[%d个]", transactionType.Key(), pz.Price), + RealAmount: walletmod.GetRealAmount(wallet), + }) + case active2023mod.PrizeTypeGameGold: // 游戏币 + return errors.New("瓦力游戏已下架") + case active2023mod.PrizeTypeProduct: // 现有产品 + if pz.Param1 == "" { + return errors.New("产品id为空") + } + productID, err := primitive.ObjectIDFromHex(pz.Param1) + if err != nil { + return errors.New("无效的产品id") + } + p, err := productmod.FindProduct(productID, "") + if err != nil { + return err + } + if p == nil { + return errors.New("产品不存在") + } + pzs[i].ProductID = &productID + switch p.ProductType { + case commod.VideoDiscount: // 视频折扣卡 + videoDiscounts = append(videoDiscounts, *p) + case commod.VIP: // 会员卡 + vipCards = append(vipCards, *p) + default: + return errors.New("不支持的产品类型") + } + case active2023mod.PrizeTypeVIPDate: // 会员日期 + if pz.Price <= 0 { + return errors.New("vip天数设置错误") + } + transactionType := txnmod.Active2023Reward + transactionLogs = append(transactionLogs, txnmod.TransactionLog{ + UID: userInfo.UID, + Amount: 0, + ActualAmount: 0, + TranType: transactionType.Key(), + TranTypeInt: int64(transactionType), + TransNo: primitive.NilObjectID, + Desc: fmt.Sprintf("%s获得VIP天数%d天", desc, pz.Price), + DiscDoc: userInfo.DiscDoc, + SysType: userInfo.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + }) + totalVipDates += pz.Price + case active2023mod.PrizeTypeGoldCoinBonus: // 金币加赠券 + if pz.Price <= 0 { + return errors.New("金币加赠券面值设置错误") + } + expired := now.Add(time.Hour * 24 * 7) // 默认一周过期 + if pz.ValidDate > 0 { + expired = now.Add(time.Hour * 24 * time.Duration(pz.ValidDate)) + } + goldExtras = append(goldExtras, goldextramod.GoldExtra{ + Uid: userInfo.UID, + Name: "金币加赠券", + Amount: pz.Price, + Expired: expired, + Used: false, + Desc: "充值加送金币", + Source: desc, + CreatedAt: now, + UpdatedAt: now, + }) + case active2023mod.PrizeTypeGoldWatch: // 金币观影券 + if pz.Price <= 0 { + return errors.New("会员观影券面值设置错误") + } + videoCoupons = append(videoCoupons, videocoupon.UserGoldVideoCoupon{ + UID: userInfo.UID, + Num: int(pz.Price), + Source: videocoupon.GoldVideoCouponSource(desc), + Used: false, + CreatedAt: now, + UpdatedAt: now, + }) + case active2023mod.PrizeTypeInKind: // 实物奖励 + default: + return fmt.Errorf("不支持的奖品类型: %d", pz.Type) + } + } + needRefreshUserCache := false + if totalGold > 0 { + if wallet, err = walletmod.CreditAmount(t, int64(totalGold), userInfo.UID); err != nil { + return err + } + } + if totalVipDates > 0 { + vipLevel := 1 // 默认vip等级为1 + start := time.Now() + discount := userInfo.PayVidDiscount + if userInfo.VipExpireDate.After(start) { // 如果用户VIP未过期, 则VIP等级和视频折扣等级需要参考用户现在的情况 + start = userInfo.VipExpireDate + if userInfo.VipLevel > vipLevel { + vipLevel = userInfo.VipLevel + } + } + vipExpireDate := start.Add(time.Hour * 24 * time.Duration(totalVipDates)) + sel := usermod.UserSelector{VipExpireDate: &vipExpireDate, VipLevel: &vipLevel, PayVidDiscount: &discount} + if err := usermod.UpdateVIP(t, userInfo.UID, userInfo.VipExpireDate, sel); err != nil { + return err + } + userInfo.VipExpireDate = vipExpireDate + userInfo.VipLevel = vipLevel + needRefreshUserCache = true + } + if len(videoCoupons) > 0 { + if err = videocoupon.InsertManyTrans(t, videoCoupons); err != nil { + return err + } + } + if len(transactionLogs) > 0 { + if err = txnmod.InsertManyTransactionLog(t, transactionLogs); err != nil { + return err + } + } + if len(goldExtras) > 0 { + if err = goldextramod.InsertGoldExtras(t, &goldExtras); err != nil { + return err + } + } + if len(videoDiscounts) > 0 { + if err = dealVideoDiscardCards(t, userInfo, videoDiscounts, desc); err != nil { + return err + } + needRefreshUserCache = true + } + if len(vipCards) > 0 { + if err = dealVipCards(t, userInfo, vipCards, desc); err != nil { + return err + } + needRefreshUserCache = true + } + ids, err := active2023mod.InsertUserPrizes(t, &pzs) + if err != nil { + return err + } + if err := active2023mod.InsertUserLottery(t, &active2023mod.UserLottery{ + UID: int64(userInfo.UID), + Count: int64(req.Count), + UseGold: int64(req.Gold), + Prizes: ids, + Desc: desc, + CreatedAt: now, + UpdatedAt: now, + }); err != nil { + return err + } + if needRefreshUserCache { + go func() { _, _ = usermod.RefreshCacheAndGetUser(userInfo.UID) }() + } + return nil + }) +} + +func dealVideoDiscardCards(t *db.MongoTool, userInfo *usermod.User, products []productmod.Product, desc string) error { + productIDsLen := len(products) + if productIDsLen == 0 { + return nil + } + videoDiscountLog, err := videodiscountmod.GetByUID(userInfo.UID) + if err != nil { + return err + } + // 最终过期时间 + var videoDiscountTotal int // 最终折扣 + var totalDates int // 总增加天数 + // 所有视频折扣卡都加入日志 + // 计算出最后用户的折扣和过期时间, 只更新一次用户配置 + if videoDiscountLog != (videodiscountmod.VideoDiscountLog{}) { // 当存在用户配置时, 需要跟用户配置做对比 + if videoDiscountLog.VideoDiscount > 0 && videoDiscountLog.VideoDiscount < 10 { // 如果用户有有效现有折扣 + videoDiscountTotal = videoDiscountLog.VideoDiscount // 默认折扣取用户折扣 + } + } + productHistorys := make([]prdcthsomod.ProductHistory, productIDsLen) + transactionLogs := make([]txnmod.TransactionLog, productIDsLen) + for i, p := range products { + // 查找视频折扣卡详情 + if p.Duration <= 0 || p.VideoDiscount >= 10 || p.VideoDiscount <= 0 { + return errors.New("视频折扣卡配置不正确") + } + // 添加记录 + historyID := primitive.NewObjectID() + productHistorys[i] = prdcthsomod.ProductHistory{ + ID: historyID, + UID: userInfo.UID, + ProductID: p.ID, + Name: p.Name, + ProductType: commod.VideoDiscount, + DiscDoc: userInfo.DiscDoc, + SysType: userInfo.SysType, + ProductSnapShot: &p, + } + transactionLogs[i] = txnmod.TransactionLog{ + UID: userInfo.UID, + Amount: -p.DiscountedPrice, + ActualAmount: 0, + TranType: txnmod.VideoDiscount.Key(), + TranTypeInt: int64(txnmod.VideoDiscount), + TransNo: historyID, + Desc: desc + "获得-" + p.Name, + DiscDoc: userInfo.DiscDoc, + SysType: userInfo.SysType, + } + if p.Duration <= 0 { + p.Duration = 7 // 如果没有设置有效过期时间, 则默认为7天 + } + totalDates += p.Duration + videoDiscount := p.VideoDiscount // 默认折扣: 产品折扣 + // 最终取折扣值比较小的(折扣值越小说明享受的折扣越大) + if videoDiscountTotal == 0 { + videoDiscountTotal = videoDiscount + } else if videoDiscountTotal > videoDiscount { + videoDiscountTotal = videoDiscount + } + } + // 写入历史纪录 + if err = prdcthsomod.InsertManyProductHistory(t, productHistorys); err != nil { + return err + } + if err = txnmod.InsertManyTransactionLog(t, transactionLogs); err != nil { + return err + } + now := time.Now() + start := now + if videoDiscountLog != (videodiscountmod.VideoDiscountLog{}) { // 当存在用户配置时, 需要跟用户配置做对比 + if videoDiscountLog.Expiration.After(now) { // 如果用户现有折扣未过期, 则过期时间从用户现有折扣过期时间算起去加折扣卡有效期 + start = videoDiscountLog.Expiration + } + } + expireAt := start.Add(time.Hour * 24 * time.Duration(totalDates)) + // 修改用户折扣 + set := videodiscountmod.EditSelector{ + UID: &userInfo.UID, + Expiration: &expireAt, + VideoDiscount: &videoDiscountTotal, + } + return videodiscountmod.Upsert(t, &set) +} + +// 抽奖中的vip卡只计算等级, 天数和视频折扣三种权益 +// 不计算vip卡配置中附赠的金币和游戏币 +func dealVipCards(t *db.MongoTool, userInfo *usermod.User, products []productmod.Product, desc string) error { + productIDsLen := len(products) + if productIDsLen == 0 { + return nil + } + productHistorys := make([]prdcthsomod.ProductHistory, productIDsLen) + transactionLogs := make([]txnmod.TransactionLog, productIDsLen) + totalDuration := 0 // 所有VIP卡增加的天数总和 + lowestDiscount := 10 // 所有VIP卡最大打折, 10表示原价不打折 + hightestVipLevel := 1 // 最高VIP等级 + for i, p := range products { + if p.PayVidDiscount <= 0 || p.PayVidDiscount > 10 { + return errors.New("VIP卡配置不正确") + } + if lowestDiscount > p.PayVidDiscount { + lowestDiscount = p.PayVidDiscount + } + if hightestVipLevel < p.VipLevel { + hightestVipLevel = p.VipLevel + } + totalDuration += p.Duration + // 添加记录 + historyID := primitive.NewObjectID() + productHistorys[i] = prdcthsomod.ProductHistory{ + ID: historyID, + UID: userInfo.UID, + ProductID: p.ID, + Name: p.Name, + ProductType: commod.VIP, + DiscDoc: userInfo.DiscDoc, + SysType: userInfo.SysType, + ProductSnapShot: &p, + } + transactionLogs[i] = txnmod.TransactionLog{ + UID: userInfo.UID, + Amount: -p.DiscountedPrice, + ActualAmount: 0, + TranType: txnmod.Active2023VIP.Key(), + TranTypeInt: int64(txnmod.Active2023VIP), + TransNo: historyID, + Desc: desc + "获得-" + p.Name, + DiscDoc: userInfo.DiscDoc, + SysType: userInfo.SysType, + } + } + // 写入历史纪录 + if err := prdcthsomod.InsertManyProductHistory(t, productHistorys); err != nil { + return err + } + if err := txnmod.InsertManyTransactionLog(t, transactionLogs); err != nil { + return err + } + start := time.Now() + if userInfo.VipExpireDate.After(start) { // 如果用户VIP未过期, 则VIP等级和视频折扣等级需要参考用户现在的情况 + start = userInfo.VipExpireDate + } + vipExpireDate := start.Add(time.Hour * 24 * time.Duration(totalDuration)) + if userInfo.VipLevel > hightestVipLevel { + hightestVipLevel = userInfo.VipLevel + } + if (userInfo.PayVidDiscount > 0 && userInfo.PayVidDiscount < 10) && userInfo.PayVidDiscount < lowestDiscount { + lowestDiscount = userInfo.PayVidDiscount + } + + sel := usermod.UserSelector{VipExpireDate: &vipExpireDate, VipLevel: &hightestVipLevel, PayVidDiscount: &lowestDiscount} + if err := usermod.UpdateVIP(t, userInfo.UID, userInfo.VipExpireDate, sel); err != nil { + return err + } + userInfo.VipExpireDate = vipExpireDate + userInfo.VipLevel = hightestVipLevel + userInfo.PayVidDiscount = lowestDiscount + return nil +} diff --git a/app/service/active2023ser/type.go b/app/service/active2023ser/type.go new file mode 100644 index 0000000..2eeae9a --- /dev/null +++ b/app/service/active2023ser/type.go @@ -0,0 +1,15 @@ +package active2023ser + +type LotteryReq struct { + Count uint64 `form:"count" json:"count"` // 抽奖次数 + Gold uint64 `form:"gold" json:"gold"` // 抽奖花费金币. 如果使用的是免费次数, 该值为0 + Prizes []Prize `form:"prizes" json:"prizes" binding:"required,min=1,max=10"` // 奖品情况 +} + +type Prize struct { + Param1 string `json:"param1"` // 附加字段. 目前仅用于视频折扣卡, 表示视频折扣卡id + Type uint64 `form:"prizeType" json:"prizeType" binding:"required"` // 奖品类型 + Name string `form:"name" json:"name"` // 奖品名称. 非实物奖品可以不传 + Price uint64 `form:"price" json:"price"` // 奖品面值, 配合奖品类型代表具体奖品. 金币: 金币数量; 金币加购券: 加购券金币面值; 观影券: 观影券抵扣面值; 会员礼包: 会员延续天数 + ValidDate uint64 `form:"validDate" json:"validDate"` // 有效期(单位: 天) +} diff --git a/app/service/activityclient/client.go b/app/service/activityclient/client.go new file mode 100644 index 0000000..5993f5a --- /dev/null +++ b/app/service/activityclient/client.go @@ -0,0 +1,7 @@ +package activityclient + +// Start 启动活动服客户端的所有定时同步任务(域名 + 红包场次等) +func Start() { + startDomainSync() + startHongbaoSync() +} diff --git a/app/service/activityclient/deduct.go b/app/service/activityclient/deduct.go new file mode 100644 index 0000000..2fb3781 --- /dev/null +++ b/app/service/activityclient/deduct.go @@ -0,0 +1,120 @@ +package activityclient + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/httputil" + "91porn-server/common/log" +) + +// 抵扣券状态回调(与 activity-public-server /api/app/deduct/coupon/notify 约定一致)。 +// 券状态流转由本服双回调驱动:建单成功->used、支付成功->verified。 +const ( + DeductCouponStatusUsed = "used" // 建单成功: 券 active -> used(下单占用/已使用),需携 deductAmount + DeductCouponStatusVerified = "verified" // 支付成功: 券 used -> verified(已核销),usedCount++ + DeductCouponStatusFailed = "failed" // 支付失败: 券 used -> active(释放) + DeductCouponStatusClosed = "closed" // 订单关闭/超时: 券 used -> active(释放) +) + +const deductCouponNotifyPath = "/api/app/deduct/coupon/notify" + +// deductCouponNotifyReq 支付结果回调请求体 +type deductCouponNotifyReq struct { + CouponID string `json:"couponId"` // 下单时收到的券号,原样回传 + OrderID string `json:"orderId,omitempty"` // 本服订单号,活动服存档对账用 + Status string `json:"status"` // paid/failed/closed + DeductAmount int64 `json:"deductAmount,omitempty"` // 实际抵扣金额(分),对账用 +} + +// NotifyDeductCoupon 向活动服回调抵扣券支付结果。 +// 鉴权走方向B(X-Svc-*),密钥用 secretKey 原始字节(BuildSvcHeaders 已按此实现)。 +// 单次调用;活动服对 couponId 幂等,重复回调安全。 +// 返回 nil 表示活动服已受理(HTTP 200 且 body.code==200)。 +func NotifyDeductCoupon(ctx context.Context, couponID, orderID, status string, deductAmount int64) error { + conf := appg.Conf.ActivityServer + if conf.ApiUrl == "" || conf.AppId == "" { + return fmt.Errorf("活动服配置缺失(apiUrl/appId)") + } + if couponID == "" { + return fmt.Errorf("couponId 为空") + } + + body, err := json.Marshal(deductCouponNotifyReq{ + CouponID: couponID, + OrderID: orderID, + Status: status, + DeductAmount: deductAmount, + }) + if err != nil { + return fmt.Errorf("marshal notify body failed: %w", err) + } + + // 签名针对下面实际发送的 body 字节;httputil 对 []byte 原样透传,两者一致 + headers := BuildSvcHeaders(conf.AppId, conf.SecretKey, "POST", deductCouponNotifyPath, "", body) + if headers == nil { + return fmt.Errorf("生成服务间签名头失败") + } + headers["Content-Type"] = "application/json" + + resp, err := httputil.DefaultClientPostWithCtx(ctx, conf.ApiUrl+deductCouponNotifyPath, headers, body) + if err != nil { + return fmt.Errorf("请求活动服失败: %w", err) + } + defer resp.Body.Close() + respBody, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("活动服回调HTTP状态异常: %d, body: %s", resp.StatusCode, string(respBody)) + } + var out struct { + Code int `json:"code"` + Msg string `json:"msg"` + } + if err := json.Unmarshal(respBody, &out); err != nil { + return fmt.Errorf("解析活动服回调响应失败: %w, body: %s", err, string(respBody)) + } + if out.Code != 200 { + return fmt.Errorf("活动服回调返回非成功: code=%d, msg=%s", out.Code, out.Msg) + } + return nil +} + +// NotifyDeductCouponAsync 异步回调抵扣券状态,失败按 1s/3s/5s/10s 退避重试。 +// 独立 goroutine 执行,不阻塞下单/支付主流程;最终仍失败仅记录错误—— +// 活动服会在 used 券超 30 分钟未核销时由定时任务兜底释放回 active,不会丢一致性。 +func NotifyDeductCouponAsync(couponID, orderID, status string, deductAmount int64) { + if couponID == "" { + return + } + common.Go(func() { + backoffs := []time.Duration{0, time.Second, 3 * time.Second, 5 * time.Second, 10 * time.Second} + var lastErr error + for i, d := range backoffs { + if d > 0 { + time.Sleep(d) + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + lastErr = NotifyDeductCoupon(ctx, couponID, orderID, status, deductAmount) + cancel() + if lastErr == nil { + if i > 0 { + log.Info("活动服-抵扣券回调重试成功", + log.Any("couponId", couponID), log.Any("status", status), log.Any("attempt", i+1)) + } + return + } + log.Warn("活动服-抵扣券回调失败,待重试", + log.Any("couponId", couponID), log.Any("orderId", orderID), + log.Any("status", status), log.Any("attempt", i+1), log.E(lastErr)) + } + log.Error("活动服-抵扣券回调最终失败(依赖活动服到期兜底释放)", + log.Any("couponId", couponID), log.Any("orderId", orderID), + log.Any("status", status), log.Any("deductAmount", deductAmount), log.E(lastErr)) + }) +} diff --git a/app/service/activityclient/domain.go b/app/service/activityclient/domain.go new file mode 100644 index 0000000..a1b43b6 --- /dev/null +++ b/app/service/activityclient/domain.go @@ -0,0 +1,161 @@ +package activityclient + +import ( + "encoding/json" + "math/rand" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" +) + +const ( + ReplaceDomain = "https://activity.domain.com" +) + +// DomainItem 活动服域名项(与 activity-public-server 的 /api/app/domains 返回结构一致) +type DomainItem struct { + Domain string `json:"domain"` // 域名 + Remark string `json:"remark"` // 备注 +} + +// domainsResp 活动服域名接口响应 +type domainsResp struct { + Code int `json:"code"` + Data []DomainItem `json:"data"` +} + +var ( + cachedDomains []DomainItem + domainsMu sync.RWMutex +) + +// GetActivityDomains 获取缓存的活动服域名列表 +func GetActivityDomains() []DomainItem { + domainsMu.RLock() + defer domainsMu.RUnlock() + return cachedDomains +} + +// GetActivityDomain 随机获取一个可用的活动服域名 +func GetActivityDomain() string { + domainsMu.RLock() + defer domainsMu.RUnlock() + n := len(cachedDomains) + if n > 0 { + return cachedDomains[rand.Intn(n)].Domain + } + return "" +} + +// ReplaceActivityDomain 替换活动服占位域名,并追加授权参数 appId、sign +func ReplaceActivityDomain(rawURL string, user *usermod.User, w *walletmod.Wallet) string { + if !strings.Contains(rawURL, ReplaceDomain) { + return rawURL + } + domain := GetActivityDomain() + if domain == "" { + return rawURL + } + rawURL = strings.ReplaceAll(rawURL, ReplaceDomain, domain) + + if user == nil { + return rawURL + } + + conf := appg.Conf.ActivityServer + now := time.Now() + rechargeAmount := 0 + if w != nil { + rechargeAmount = int(w.Consumption) + } + payload := &SignPayload{ + AppId: conf.AppId, + UserId: strconv.FormatUint(user.UID, 10), + Nickname: user.Name, + Avatar: user.Portrait, + Ts: now.Unix(), + RegisteredAt: user.CreatedAt.Unix(), + RechargeAmount: rechargeAmount, + } + sign, err := EncryptSign(conf.SecretKey, payload) + if err != nil { + log.Error("活动服-生成签名失败", log.E(err)) + return rawURL + } + + if strings.Contains(rawURL, "?") { + rawURL += "&inner=1&appId=" + conf.AppId + "&sign=" + sign + } else { + rawURL += "?inner=1&appId=" + conf.AppId + "&sign=" + sign + } + return rawURL +} + +var hrefRe = regexp.MustCompile(`href="([^"]*)"`) + +// ReplaceActivityDomainInHTML 替换富文本 HTML 中所有 href 里的活动服占位域名 +func ReplaceActivityDomainInHTML(htmlStr string, user *usermod.User, w *walletmod.Wallet) string { + if !strings.Contains(htmlStr, ReplaceDomain) { + return htmlStr + } + return hrefRe.ReplaceAllStringFunc(htmlStr, func(match string) string { + url := match[6 : len(match)-1] // 去掉 href=" 前缀和 " 后缀 + return `href="` + ReplaceActivityDomain(url, user, w) + `"` + }) +} + +// startDomainSync 定时拉取活动服域名(每5分钟) +func startDomainSync() { + fetchDomains() + common.Go(func() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + for range ticker.C { + fetchDomains() + } + }) +} + +// fetchDomains 从活动服拉取可用域名列表 +func fetchDomains() { + conf := appg.Conf.ActivityServer + apiUrl := conf.ApiUrl + if apiUrl == "" { + return + } + + const path = "/api/app/domains" + url := apiUrl + path + headers := BuildSvcHeaders(conf.AppId, conf.SecretKey, "GET", path, "", nil) + _, body, err := httputil.DefaultClientGetBytes(url, headers) + if err != nil { + log.Error("活动服-拉取域名列表失败", log.Any("url", url), log.E(err)) + return + } + + var resp domainsResp + if err := json.Unmarshal(body, &resp); err != nil { + log.Error("活动服-解析域名列表失败", log.Any("body", string(body)), log.E(err)) + return + } + + if resp.Code != 200 { + log.Warn("活动服-域名列表返回非成功状态", log.Any("code", resp.Code)) + return + } + + domainsMu.Lock() + cachedDomains = resp.Data + domainsMu.Unlock() + + log.Info("活动服-域名列表更新成功", log.Any("count", len(resp.Data))) +} diff --git a/app/service/activityclient/hongbao.go b/app/service/activityclient/hongbao.go new file mode 100644 index 0000000..94faea5 --- /dev/null +++ b/app/service/activityclient/hongbao.go @@ -0,0 +1,100 @@ +package activityclient + +import ( + "encoding/json" + "sync" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/httputil" + "91porn-server/common/log" +) + +// HongbaoActivity 与 activity-public-server activityMod.HongbaoActivity 保持一致 +type HongbaoActivity struct { + ID string `json:"id"` + Name string `json:"name"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + TriggerTimes []string `json:"triggerTimes"` + SessionDuration int `json:"sessionDuration"` + SessionClickLimit int `json:"sessionClickLimit"` + AppIds []string `json:"appIds"` + Enabled bool `json:"enabled"` +} + +// UpcomingSessionItem 进行中/即将开启的场次 +type UpcomingSessionItem struct { + SessionId string `json:"sessionId,omitempty"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Status string `json:"status"` // active=进行中, upcoming=未来即将开启 +} + +// HongbaoUpcomingResp 拉取响应内容 +type HongbaoUpcomingResp struct { + Activity *HongbaoActivity `json:"activity"` + List []UpcomingSessionItem `json:"list"` +} + +type hongbaoApiResp struct { + Code int `json:"code"` + Data HongbaoUpcomingResp `json:"data"` +} + +var ( + cachedHongbao HongbaoUpcomingResp + cachedHongbaoMu sync.RWMutex +) + +// GetHongbaoUpcoming 获取缓存的红包场次 +func GetHongbaoUpcoming() HongbaoUpcomingResp { + cachedHongbaoMu.RLock() + defer cachedHongbaoMu.RUnlock() + return cachedHongbao +} + +// startHongbaoSync 启动定时拉取红包场次(每10秒) +func startHongbaoSync() { + fetchHongbaoUpcoming() + common.Go(func() { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + for range ticker.C { + fetchHongbaoUpcoming() + } + }) +} + +// fetchHongbaoUpcoming 调活动服 /api/app/hongbao/upcoming +func fetchHongbaoUpcoming() { + conf := appg.Conf.ActivityServer + if conf.ApiUrl == "" || conf.AppId == "" { + return + } + + const path = "/api/app/hongbao/upcoming" + rawQuery := "appId=" + conf.AppId + url := conf.ApiUrl + path + "?" + rawQuery + headers := BuildSvcHeaders(conf.AppId, conf.SecretKey, "GET", path, rawQuery, nil) + _, body, err := httputil.DefaultClientGetBytes(url, headers) + if err != nil { + log.Error("活动服-拉取红包场次失败", log.Any("url", url), log.E(err)) + return + } + + var resp hongbaoApiResp + if err := json.Unmarshal(body, &resp); err != nil { + log.Error("活动服-解析红包场次失败", log.Any("body", string(body)), log.E(err)) + return + } + if resp.Code != 200 { + log.Warn("活动服-红包场次返回非成功状态", log.Any("code", resp.Code)) + return + } + + cachedHongbaoMu.Lock() + cachedHongbao = resp.Data + cachedHongbaoMu.Unlock() +} diff --git a/app/service/activityclient/hongbao_countdown.go b/app/service/activityclient/hongbao_countdown.go new file mode 100644 index 0000000..57fab70 --- /dev/null +++ b/app/service/activityclient/hongbao_countdown.go @@ -0,0 +1,70 @@ +package activityclient + +import ( + "net/url" + "time" +) + +// LinkTypeHongbaoRain 跳转链接中标识红包雨活动的 type 参数值 +const LinkTypeHongbaoRain = "hongbaoRain" + +// ResolveCountdownByLink 根据跳转链接的 type 参数推导倒计时类型和场次时间。 +// 适用于 link 自身携带活动类型标识、但 DB 未单独建 countdownType 字段的业务(如任务列表)。 +// +// 返回值约定: +// - link 含 type=hongbaoRain 且有可用红包雨场次:返回 (场次开始, 场次结束, 1, true) +// - link 含 type=hongbaoRain 但无可用场次:返回 (零, 零, 1, false),调用方据此过滤该条目 +// - 其他情况:返回 (零, 零, 0, true),无倒计时 +func ResolveCountdownByLink(link string) (start, end time.Time, countdownType int, ok bool) { + if linkType(link) != LinkTypeHongbaoRain { + return time.Time{}, time.Time{}, 0, true + } + s, e, hit := NextHongbaoSession(time.Now()) + if !hit { + return time.Time{}, time.Time{}, 1, false + } + return s, e, 1, true +} + +// linkType 提取链接 query 中的 type 参数,解析失败返回空串 +func linkType(link string) string { + if link == "" { + return "" + } + u, err := url.Parse(link) + if err != nil { + return "" + } + return u.Query().Get("type") +} + +// NextHongbaoSession 取一场用于倒计时展示的红包雨场次: +// - 当前已处于某场次内(StartTime <= now < EndTime):返回该场次 +// - 否则返回最近一场未来未过期场次 +// - 无任何未过期场次:返回 (零值, 零值, false) +func NextHongbaoSession(now time.Time) (start, end time.Time, ok bool) { + data := GetHongbaoUpcoming() + var ( + nearestStart time.Time + nearestEnd time.Time + ) + for _, s := range data.List { + // 跳过已结束场次 + if !s.EndTime.IsZero() && !s.EndTime.After(now) { + continue + } + // 当前已在场次内,直接返回 + if !s.StartTime.After(now) { + return s.StartTime, s.EndTime, true + } + // 未来场次,取开始时间最早的 + if nearestStart.IsZero() || s.StartTime.Before(nearestStart) { + nearestStart = s.StartTime + nearestEnd = s.EndTime + } + } + if nearestStart.IsZero() { + return time.Time{}, time.Time{}, false + } + return nearestStart, nearestEnd, true +} diff --git a/app/service/activityclient/sign.go b/app/service/activityclient/sign.go new file mode 100644 index 0000000..e549331 --- /dev/null +++ b/app/service/activityclient/sign.go @@ -0,0 +1,37 @@ +package activityclient + +import ( + "encoding/base64" + "encoding/json" + "fmt" + + "91porn-server/common/crypt" +) + +// SignPayload 签名载荷(与 activity-public-server 保持一致) +type SignPayload struct { + AppId string `json:"appId"` // 应用ID + UserId string `json:"userId"` // 用户ID + Nickname string `json:"nickname"` // 用户昵称 + Avatar string `json:"avatar"` // 用户头像 + Ts int64 `json:"ts"` // 时间戳(秒) + RegisteredAt int64 `json:"registeredAt"` // 用户注册时间(Unix秒) + RechargeAmount int `json:"rechargeAmount"` // 充值金额(分) +} + +// EncryptSign AES-CBC 加密签名载荷,返回 Base64 URL Safe 编码 +func EncryptSign(secretKeyBase64 string, payload *SignPayload) (string, error) { + secretKey, err := base64.StdEncoding.DecodeString(secretKeyBase64) + if err != nil { + return "", fmt.Errorf("decode secretKey failed: %w", err) + } + plaintext, err := json.Marshal(payload) + if err != nil { + return "", fmt.Errorf("marshal payload failed: %w", err) + } + ciphertext, err := crypt.AESCBCPck5Encrypt(plaintext, secretKey) + if err != nil { + return "", fmt.Errorf("encrypt failed: %w", err) + } + return base64.URLEncoding.EncodeToString(ciphertext), nil +} diff --git a/app/service/activityclient/svcsign.go b/app/service/activityclient/svcsign.go new file mode 100644 index 0000000..1315ad2 --- /dev/null +++ b/app/service/activityclient/svcsign.go @@ -0,0 +1,69 @@ +package activityclient + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "strconv" + "strings" + "time" +) + +// 与 activity-public-server middleware/svcauth 协议保持一致: +// +// X-Svc-AppId : 调用方 appId +// X-Svc-Ts : 请求生成的 Unix 秒(服务端校验 ±300s) +// X-Svc-Sign : HMAC_SHA256(canonical, secret) 的 hex 小写 +// +// canonical 串结构("\n" 分隔): +// +// appId +// ts +// METHOD // 大写 +// path // URL.Path,不含 query +// rawQuery // URL.RawQuery 原样,不重排序 +// sha256_hex(body) // 空 body 也参与运算 +const ( + headerSvcAppId = "X-Svc-AppId" + headerSvcTs = "X-Svc-Ts" + headerSvcSign = "X-Svc-Sign" +) + +// BuildSvcHeaders 生成服务间调用鉴权头。 +// method/path/rawQuery 必须与服务端 c.Request 上看到的一致; +// body 传原始字节(GET 等无 body 接口传 nil)。 +// appId 或 secret 为空时返回 nil,调用方按未签名处理。 +func BuildSvcHeaders(appId, secret, method, path, rawQuery string, body []byte) map[string]string { + if appId == "" || secret == "" { + return nil + } + ts := strconv.FormatInt(time.Now().Unix(), 10) + return map[string]string{ + headerSvcAppId: appId, + headerSvcTs: ts, + headerSvcSign: computeSvcSign(secret, appId, ts, strings.ToUpper(method), path, rawQuery, hashBody(body)), + } +} + +// hashBody 计算 body 的 sha256 hex;nil/空 body 也返回 sha256("") 的 hex +func hashBody(body []byte) string { + sum := sha256.Sum256(body) + return hex.EncodeToString(sum[:]) +} + +// computeSvcSign 按 canonical 串拼接后做 HMAC-SHA256 +func computeSvcSign(secret, appId, ts, method, path, rawQuery, bodyHash string) string { + h := hmac.New(sha256.New, []byte(secret)) + h.Write([]byte(appId)) + h.Write([]byte{'\n'}) + h.Write([]byte(ts)) + h.Write([]byte{'\n'}) + h.Write([]byte(method)) + h.Write([]byte{'\n'}) + h.Write([]byte(path)) + h.Write([]byte{'\n'}) + h.Write([]byte(rawQuery)) + h.Write([]byte{'\n'}) + h.Write([]byte(bodyHash)) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/app/service/activityser/activityser.go b/app/service/activityser/activityser.go new file mode 100644 index 0000000..66ad170 --- /dev/null +++ b/app/service/activityser/activityser.go @@ -0,0 +1,573 @@ +package activityser + +import ( + "context" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "time" + + "91porn-server/app/appg" + "91porn-server/app/proto" + "91porn-server/app/service/activityclient" + "91porn-server/app/service/customerser" + "91porn-server/app/service/productser" + "91porn-server/app/service/rechargeser" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/crypt" + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/common/stderr" + "91porn-server/middleware/ua" + "91porn-server/models/commod" + "91porn-server/models/v/advanceordermod" + "91porn-server/models/v/currencymod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/rchgamtmod" + "91porn-server/models/v/sourcemod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// PayChannel 支付渠道(精简字段) +type PayChannel struct { + Type string `json:"type"` + TypeName string `json:"typeName"` +} + +// ActivityCurrencyItem 活动服金币充值列表项 +type ActivityCurrencyItem struct { + ID primitive.ObjectID `json:"id"` + Coin int64 `json:"coin"` + Money int64 `json:"money"` + Name string `json:"typeName"` + GiveCoin int64 `json:"giveCoin"` + RechargeType []PayChannel `json:"rechargeType"` +} + +func toPayChannels(src []rchgamtmod.PayChannelRes) []PayChannel { + channels := make([]PayChannel, 0, len(src)) + for _, ch := range src { + channels = append(channels, PayChannel{ + Type: ch.Type, + TypeName: ch.TypeName, + }) + } + return channels +} + +// GetCurrencyList 获取金币充值列表 +func GetCurrencyList(ctx context.Context) ([]*ActivityCurrencyItem, stderr.Code) { + currencys, err := currencymod.List(commod.Gold) + if err != nil { + log.ErrorX(ctx, "活动服-获取金币列表异常", log.E(err)) + return nil, stderr.ErrNetWorkBusy + } + + removeRepeat := make(map[int64]struct{}) + var moneys []string + for _, cur := range currencys { + if _, ok := removeRepeat[cur.Price]; ok { + continue + } + removeRepeat[cur.Price] = struct{}{} + moneys = append(moneys, rechargeser.FenToYuan(cur.Price)) + } + + payReq := rchgutil.GainPayTypeReq{Money: moneys} + bc, err := payReq.GetPayType() + if err != nil { + log.ErrorX(ctx, "活动服-获取支付通道列表异常", log.E(err)) + return nil, stderr.ErrNetWorkBusy + } + + data := make([]*ActivityCurrencyItem, 0, len(currencys)) + for _, v := range currencys { + if v.Price < 100 { + continue + } + rechargeType := rechargeser.GetPayChannelDetails_New(v.Price, bc, 0) + if len(rechargeType) <= 0 { + continue + } + data = append(data, &ActivityCurrencyItem{ + ID: v.ID, + Coin: v.Coins, + Money: v.Price, + Name: v.Name, + GiveCoin: v.GiveGold, + RechargeType: toPayChannels(rechargeType), + }) + } + return data, stderr.Success +} + +// CardCategory 会员卡分类 +type CardCategory int + +const ( + DefaultCard CardCategory = 0 + PreSaleCard CardCategory = 1 + NewbieCard CardCategory = 2 +) + +// VipCardInfo 活动服会员卡信息 +type VipCardInfo struct { + Id string `json:"id"` + Category CardCategory `json:"category"` + VipGrade string `json:"vipGrade"` + Name string `json:"name"` + Desc string `json:"desc"` + PreMoney int64 `json:"preMoney"` + Money int64 `json:"money"` + CanUseCoin bool `json:"canUseCoin"` + DayCount int64 `json:"dayCount"` + GiveCoin int64 `json:"giveCoin"` + Sort int32 `json:"sort"` + Image string `json:"image"` + Rights []RightInfo `json:"rights"` + RechargeType []PayChannel `json:"rechargeType"` + ActivityStartAt time.Time `json:"activityStartAt"` + ActivityEndAt time.Time `json:"activityEndAt"` + CanUpgrade bool `json:"canUpgrade"` + PreSaleStep int `json:"preSaleStep"` // 预售卡阶段 0-预付阶段 1-尾款阶段 2-尾款付款完成(结束) +} + +// RightInfo 权益信息 +type RightInfo struct { + Name string `json:"name"` + Desc string `json:"desc"` + Image string `json:"image"` +} + +// VipDeduct 会员卡当前可用抵扣(vip/list 按抵扣后金额匹配支付通道用) +type VipDeduct struct { + ProductID string // 会员卡ID + DeductAmount int64 // 券面额(分) +} + +// GetProductList 获取会员卡列表。 +// deducts 非空时,对携带抵扣的普通会员卡按"抵扣后有效金额"重新匹配支付通道(rechargeType), +// money/preMoney 仍返回原价(避免与下单预览重复扣减)。 +func GetProductList(ctx context.Context, uid uint64, deducts []VipDeduct) ([]VipCardInfo, error) { + u, err := usermod.FindUserByUID(uid) + if err != nil { + return nil, fmt.Errorf("查询用户异常: %w", err) + } + if u == nil { + return nil, fmt.Errorf("用户不存在: %d", uid) + } + + productList, err := rechargeser.New_ProductList(uid, u.SysType, true, 0) + if err != nil { + return nil, err + } + + // 抵扣券:按抵扣后有效金额预匹配支付通道(命中的卡覆盖 rechargeType) + deductedChans := buildDeductedRechargeTypes(productList, deducts) + + var cards []VipCardInfo + for _, group := range productList { + for _, item := range group.List { + p := item.Product + card := VipCardInfo{ + Id: p.ID.Hex(), + VipGrade: fmt.Sprintf("%d", p.VipLevel), + Name: p.Name, + Desc: p.Desc, + PreMoney: p.OriginalPrice * 10, + Money: p.DiscountedPrice * 10, + CanUseCoin: p.IsAmountPay, + DayCount: int64(p.Duration), + GiveCoin: p.GiveCoin, + Sort: int32(p.Sort), + Image: p.BGImg, + CanUpgrade: p.IsUpgrade, + } + + switch p.ProductType { + case commod.AdvanceCard: + card.Category = PreSaleCard + advOrder, _ := advanceordermod.IsExist(bson.M{"productID": p.ID, "uid": uid}) + // advOrder.Status 取值(advanceordermod): + // 0 DEFAULT / 1 AdvanceProcessing(预付中) / 2 AdvanceSUCCESS(预付成功) + // 3 BalanceProcessing(尾款中) / 4 BalanceSUCCESS(尾款成功) + if advOrder == nil || advOrder.Status == advanceordermod.DEFAULT || advOrder.Status == advanceordermod.AdvanceProcessing { + // 预付阶段:尚未下单 / 预付中 + card.PreSaleStep = 0 + card.ActivityStartAt = p.ActivityTime + card.ActivityEndAt = p.EndTime + card.Money = p.AdvanceAmount * 10 + } else if advOrder.Status == advanceordermod.AdvanceSUCCESS || advOrder.Status == advanceordermod.BalanceProcessing { + // 尾款阶段:预付成功 / 尾款支付中 + card.PreSaleStep = 1 + card.ActivityStartAt = p.StartTime + card.ActivityEndAt = p.EndTime + card.Money = p.BalanceAmount * 10 + card.Image = p.NewBgImg + } else if advOrder.Status == advanceordermod.BalanceSUCCESS { + // 尾款付款完成(结束) + card.PreSaleStep = 2 + } + case commod.NEWUSERCard: + card.Category = NewbieCard + card.ActivityStartAt = u.CreatedAt + card.ActivityEndAt = card.ActivityStartAt.Add(time.Duration(p.ShowCountdownTime) * time.Hour) + if time.Now().After(card.ActivityEndAt) { + continue + } + default: + card.Category = DefaultCard + } + + if card.Image == "" { + card.Image = p.BGImg + } + + rights := make([]RightInfo, 0, len(p.NewPrivilege)) + for _, pr := range p.NewPrivilege { + rights = append(rights, RightInfo{ + Name: pr.Name, + Desc: pr.Desc, + Image: pr.Image, + }) + } + card.Rights = rights + card.RechargeType = toPayChannels(item.RechargeType) + // 抵扣券:按抵扣后金额匹配的支付通道覆盖原价通道 + if chans, ok := deductedChans[p.ID.Hex()]; ok { + card.RechargeType = chans + } + + cards = append(cards, card) + } + } + return cards, nil +} + +// minDeductPayFen 抵扣后最低实付(分),与下单折价封顶保持一致 +const minDeductPayFen = 1 + +// buildDeductedRechargeTypes 依据 deducts 计算各会员卡"抵扣后有效金额"对应的支付通道。 +// 支付通道按支付金额匹配(如微信要求达标金额),券抵扣后实付变小可能改变可用通道, +// 故对携带抵扣的普通会员卡(非预售卡)按 min(券面额, 原价-最低实付) 折后金额重新匹配 rechargeType。 +// 返回 productIdHex -> 支付通道;deducts 为空或无命中时返回 nil,调用方保留原价通道。 +func buildDeductedRechargeTypes(productList []proto.ProductList, deducts []VipDeduct) map[string][]PayChannel { + deductMap := make(map[string]int64, len(deducts)) + for _, d := range deducts { + if d.ProductID != "" && d.DeductAmount > 0 { + deductMap[d.ProductID] = d.DeductAmount + } + } + if len(deductMap) == 0 { + return nil + } + + type effEntry struct { + hex string + effFen int64 + } + var entries []effEntry + moneySet := make(map[string]struct{}) + var moneys []string + for _, group := range productList { + for _, item := range group.List { + p := item.Product + if p.ProductType == commod.AdvanceCard { + continue // 预售卡分阶段付款,不参与抵扣 + } + deduct, ok := deductMap[p.ID.Hex()] + if !ok { + continue + } + originFen := p.DiscountedPrice * 10 + maxDeduct := originFen - minDeductPayFen + if maxDeduct <= 0 { + continue + } + if deduct > maxDeduct { + deduct = maxDeduct + } + effFen := originFen - deduct + entries = append(entries, effEntry{hex: p.ID.Hex(), effFen: effFen}) + y := rechargeser.FenToYuan(effFen) + if _, exist := moneySet[y]; !exist { + moneySet[y] = struct{}{} + moneys = append(moneys, y) + } + } + } + if len(entries) == 0 { + return nil + } + + bc, err := (&rchgutil.GainPayTypeReq{Money: moneys}).GetPayTypeFromCache() + if err != nil { + log.Warn("活动服-抵扣后支付通道查询失败", log.Any("moneys", moneys), log.E(err)) + } + out := make(map[string][]PayChannel, len(entries)) + for _, e := range entries { + out[e.hex] = toPayChannels(rechargeser.GetPayChannelDetails_New(e.effFen, bc, 0)) + } + return out +} + +// UserBalance 用户余额信息 +type UserBalance struct { + Gold int64 `json:"gold"` // 金币余额 + Integral int64 `json:"integral"` // 积分余额 + LotteryFreeTimes int64 `json:"lotteryFreeTimes"` // 抽奖免费次数 + VipLevel int `json:"vipLevel,omitempty"` // VIP 等级;用户未开通或已过期时不下发 + VipExpireDate time.Time `json:"vipExpireDate,omitempty"` // VIP 截止时间;用户未开通或已过期时不下发 + PromotionCode string `json:"promotionCode,omitempty"` // 邀请码 + PromoteURL string `json:"promoteURL,omitempty"` // 邀请链接(已拼好 PromotionField + 邀请码) +} + +// GetUserBalance 获取用户金币/积分余额及 VIP 信息(VIP 已过期时不下发 VIP 字段) +func GetUserBalance(uid uint64) (*UserBalance, error) { + w, err := walletmod.GetWallet(uid) + if err != nil { + return nil, fmt.Errorf("查询钱包异常: %w", err) + } + res := &UserBalance{} + if w != nil { + res.Gold = w.Amount + res.Integral = w.Integral + res.LotteryFreeTimes = w.LotteryTimes + } + // 用户信息独立取数,单点失败不影响余额下发 + u, uerr := usermod.FindUserByUID(uid) + if uerr != nil { + log.Warn("活动服-查询用户信息失败", log.Any("uid", uid), log.E(uerr)) + return res, nil + } + if u == nil { + return res, nil + } + if u.IsVIP(time.Now()) { + res.VipLevel = u.VipLevel + res.VipExpireDate = u.VipExpireDate + } + // 邀请码 + 邀请链接(与 /api/app/mine/info 的 promoteURL 拼接方式一致) + res.PromotionCode = u.PromCode + if base := sourcemod.GetRandomPromotionURL(); base != "" && u.PromCode != "" { + res.PromoteURL = common.BindUrl(base, constant.PromotionField+u.PromCode) + } + return res, nil +} + +func convertBuyType(actBuyType int) commod.BuyType { + switch actBuyType { + case 1: + return commod.BuyGold + case 2: + return commod.BuyProduct + default: + return commod.BuyType(actBuyType) + } +} + +type RechargeAttribution struct { + ActivityID string + ExperimentID string + ExperimentVariant string + SessionID string +} + +// CreateRechargeOrder 创建充值订单。活动入口的 sourcePage 由服务端固定, +// 避免调用方伪造其他来源;其余实验字段仍走统一下单校验。 +func CreateRechargeOrder( + ctx context.Context, + uid uint64, + rechargeType, productID string, + buyType int, + ip string, + attribution RechargeAttribution, + couponID string, + deductAmount int64, +) (payUrl, mode string, err error) { + pid, err := primitive.ObjectIDFromHex(productID) + if err != nil { + return "", "", fmt.Errorf("invalid productId: %w", err) + } + + u, err := usermod.FindUserByUID(uid) + if err != nil { + return "", "", fmt.Errorf("查询用户异常: %w", err) + } + if u == nil { + return "", "", fmt.Errorf("用户不存在: %d", uid) + } + + realBuyType := convertBuyType(buyType) + + in := &rechargeser.RechargeRequest{ + UID: uid, + IP: ip, + RechargeType: rechargeType, + ProductID: pid, + BuyType: realBuyType, + SourcePage: rechargeser.OrderSourcePageH5Activity, + SourceRef: attribution.ActivityID, + ActivityID: attribution.ActivityID, + ExperimentID: attribution.ExperimentID, + ExperimentVariant: attribution.ExperimentVariant, + SessionID: attribution.SessionID, + } + // 抵扣券作为活动服附加对象单独传入,不混入客户端下单请求体 + var deduct *rechargeser.ActivityDeduct + if couponID != "" && deductAmount > 0 { + deduct = &rechargeser.ActivityDeduct{CouponID: couponID, DeductAmount: deductAmount} + } + return rechargeser.Recharge(ctx, in, ua.UA{SysType: u.SysType}, deduct) +} + +// BuyCoinProduct 金币购买商品 +func BuyCoinProduct(ctx context.Context, uid uint64, productID string, sysType string) (stderr.Code, error) { + pid, err := primitive.ObjectIDFromHex(productID) + if err != nil { + return stderr.ErrParamError, fmt.Errorf("invalid productId: %w", err) + } + + p, err := productmod.FindProduct(pid, sysType) + if err != nil || p == nil { + return stderr.ErrParamError, fmt.Errorf("商品不存在: %s", productID) + } + + if !p.IsAmountPay { + return stderr.ErrParamError, fmt.Errorf("该商品不支持金币购买") + } + + code := productser.Buy(uid, p.ProductType, pid, primitive.NilObjectID, primitive.NilObjectID, 0, "", sysType, "", 0, false, ua.UA{SysType: sysType}, "", productser.VIPExperimentAttribution{}) + if code != stderr.Success { + return code, fmt.Errorf("购买失败") + } + return stderr.Success, nil +} + +// GenerateSecretKey 生成 AES-256 密钥 +func GenerateSecretKey() (string, error) { + key := make([]byte, 32) + if _, err := rand.Read(key); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(key), nil +} + +// DecryptSign AES-CBC 解密签名载荷 +func DecryptSign(secretKeyBase64 string, signBase64 string) (*activityclient.SignPayload, error) { + secretKey, err := base64.StdEncoding.DecodeString(secretKeyBase64) + if err != nil { + return nil, fmt.Errorf("decode secretKey failed: %w", err) + } + signBytes, err := base64.URLEncoding.DecodeString(signBase64) + if err != nil { + signBytes, err = base64.StdEncoding.DecodeString(signBase64) + if err != nil { + return nil, fmt.Errorf("decode sign failed: %w", err) + } + } + plaintext, err := crypt.CoreAesDecrypt(signBytes, string(secretKey)) + if err != nil { + return nil, fmt.Errorf("decrypt sign failed: %w", err) + } + var payload activityclient.SignPayload + if err := json.Unmarshal([]byte(plaintext), &payload); err != nil { + return nil, fmt.Errorf("parse sign payload failed: %w", err) + } + return &payload, nil +} + +// HmacSHA256 用于回调签名 +func HmacSHA256(secretKeyBase64 string, data []byte) (string, error) { + keyBytes, err := base64.StdEncoding.DecodeString(secretKeyBase64) + if err != nil { + return "", fmt.Errorf("decode secretKey failed: %w", err) + } + h := hmac.New(sha256.New, keyBytes) + h.Write(data) + return hex.EncodeToString(h.Sum(nil)), nil +} + +// BuildSignedURL 生成带 sign 的活动服请求 URL +func BuildSignedURL(userId, nickname, avatar string) (signedURL, sign string, err error) { + conf := appg.Conf.ActivityServer + baseURL := activityclient.GetActivityDomain() + if baseURL == "" { + return "", "", fmt.Errorf("活动服域名未获取,请检查配置") + } + + now := time.Now() + payload := &activityclient.SignPayload{ + AppId: conf.AppId, + UserId: userId, + Nickname: nickname, + Avatar: avatar, + Ts: now.Unix(), + } + + sign, err = activityclient.EncryptSign(conf.SecretKey, payload) + if err != nil { + return "", "", fmt.Errorf("encrypt sign failed: %w", err) + } + + signedURL = fmt.Sprintf("%s?appId=%s&sign=%s", baseURL, conf.AppId, sign) + return signedURL, sign, nil +} + +// AppInfoResp 应用信息 +type AppInfoResp struct { + CustomerServiceUrl string `json:"customerServiceUrl"` +} + +// GetAppInfo 获取应用信息 +func GetAppInfo(ctx context.Context, uid uint64) (*AppInfoResp, error) { + resp, err := customerser.GetUrl(uid, ua.UA{}) + if err != nil { + return nil, fmt.Errorf("获取客服地址失败: %w", err) + } + return &AppInfoResp{ + CustomerServiceUrl: resp.Data.Url, + }, nil +} + +// BuildLoginCurl 生成活动服登录接口的 curl 命令(调试用) +func BuildLoginCurl(userId, nickname, avatar string) (string, error) { + conf := appg.Conf.ActivityServer + activityHost := activityclient.GetActivityDomain() + if activityHost == "" { + return "", fmt.Errorf("活动服域名未获取,请检查配置") + } + + now := time.Now() + payload := &activityclient.SignPayload{ + AppId: conf.AppId, + UserId: userId, + Nickname: nickname, + Avatar: avatar, + Ts: now.Unix(), + } + + sign, err := activityclient.EncryptSign(conf.SecretKey, payload) + if err != nil { + return "", fmt.Errorf("encrypt sign failed: %w", err) + } + + payloadJSON, _ := json.MarshalIndent(payload, "", " ") + + result := fmt.Sprintf("AppId: %s\nSecretKey: %s\n\n", conf.AppId, conf.SecretKey) + result += fmt.Sprintf("--- Generated Sign ---\nsign: %s\n\n", sign) + result += fmt.Sprintf("payload: %s\n\n", string(payloadJSON)) + result += fmt.Sprintf("--- CURL ---\ncurl -X POST '%s/api/app/index/login' \\\n -H 'Content-Type: application/json' \\\n -d '{\"appId\":\"%s\",\"sign\":\"%s\"}'", + activityHost, conf.AppId, sign) + + return result, nil +} diff --git a/app/service/activityser/deduct.go b/app/service/activityser/deduct.go new file mode 100644 index 0000000..600ad11 --- /dev/null +++ b/app/service/activityser/deduct.go @@ -0,0 +1,156 @@ +package activityser + +import ( + "context" + "fmt" + "strconv" + + "91porn-server/app/appg" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" +) + +// DeductType 扣款类型 +type DeductType int + +const ( + DeductGold DeductType = 1 // 金币 + DeductIntegral DeductType = 2 // 积分 + DeductLotteryTimes DeductType = 3 // 抽奖免费次数 +) + +// DeductReq 扣款请求 +type DeductReq struct { + UserId string `json:"userId" binding:"required"` + DeductType DeductType `json:"deductType" binding:"required"` + Amount int64 `json:"amount" binding:"required"` + ActivityId string `json:"activityId"` + Remark string `json:"remark"` +} + +// Deduct 活动扣款 +func Deduct(ctx context.Context, req *DeductReq) (stderr.Code, error) { + uid, err := strconv.ParseUint(req.UserId, 10, 64) + if err != nil { + return stderr.ErrParamError, fmt.Errorf("invalid userId: %s", req.UserId) + } + u, err := usermod.FindUserByUID(uid) + if err != nil { + return stderr.Failure, fmt.Errorf("查询用户异常: %w", err) + } + if u == nil { + return stderr.ErrParamError, fmt.Errorf("用户不存在: %s", req.UserId) + } + + if req.Amount <= 0 { + return stderr.ErrParamError, fmt.Errorf("扣款数量必须大于0") + } + + w, err := walletmod.GetWallet(uid) + if err != nil { + return stderr.Failure, fmt.Errorf("查询钱包异常: %w", err) + } + if w == nil { + w = &walletmod.Wallet{} + } + + desc := req.Remark + + switch req.DeductType { + case DeductGold: + if w.Amount < req.Amount { + return stderr.InsufficientGold, fmt.Errorf("金币余额不足,当前%d,需要%d", w.Amount, req.Amount) + } + return deductGold(ctx, uid, u, req, desc) + case DeductIntegral: + if w.Integral < req.Amount { + return stderr.InsufficientPoint, fmt.Errorf("积分余额不足,当前%d,需要%d", w.Integral, req.Amount) + } + return deductIntegral(ctx, uid, u, req, desc) + case DeductLotteryTimes: + if w.LotteryTimes < req.Amount { + return stderr.InsufficientLotteryFreeTimes, fmt.Errorf("抽奖免费次数不足,当前%d,需要%d", w.LotteryTimes, req.Amount) + } + return deductLotteryTimes(ctx, uid, u, req, desc) + default: + return stderr.ErrParamError, fmt.Errorf("不支持的扣款类型: %d", req.DeductType) + } +} + +func deductGold(ctx context.Context, uid uint64, u *usermod.User, req *DeductReq, desc string) (stderr.Code, error) { + amount := req.Amount + + err := appg.VideoDB.Trans(func(t *db.MongoTool) error { + w, err := walletmod.DebitAmount(t, amount, uid) + if err != nil { + return err + } + log.InfoX(ctx, "活动扣款-扣除金币", log.Any("uid", uid), log.Any("amount", amount), log.Any("activityId", req.ActivityId)) + return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + UID: uid, + Amount: -amount, + ActualAmount: float64(-amount), + TranType: txnmod.ActivityDeductGold.Key(), + TranTypeInt: int64(txnmod.ActivityDeductGold), + Desc: fmt.Sprintf("%s-扣除%d金币", desc, amount), + SysType: u.SysType, + RealAmount: w.RealAmount(), + }) + }) + if err != nil { + return stderr.InsufficientGold, fmt.Errorf("金币余额不足") + } + return stderr.Success, nil +} + +func deductIntegral(ctx context.Context, uid uint64, u *usermod.User, req *DeductReq, desc string) (stderr.Code, error) { + integral := req.Amount + + err := appg.VideoDB.Trans(func(t *db.MongoTool) error { + _, err := walletmod.DebitIntegral(t, integral, uid) + if err != nil { + return err + } + log.InfoX(ctx, "活动扣款-扣除积分", log.Any("uid", uid), log.Any("integral", integral), log.Any("activityId", req.ActivityId)) + return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + UID: uid, + Integral: -integral, + TranType: txnmod.ActivityDeductIntegral.Key(), + TranTypeInt: int64(txnmod.ActivityDeductIntegral), + Desc: fmt.Sprintf("%s-扣除%d积分", desc, integral), + SysType: u.SysType, + }) + }) + if err != nil { + return stderr.InsufficientPoint, fmt.Errorf("积分余额不足") + } + return stderr.Success, nil +} + +func deductLotteryTimes(ctx context.Context, uid uint64, u *usermod.User, req *DeductReq, desc string) (stderr.Code, error) { + times := req.Amount + + err := appg.VideoDB.Trans(func(t *db.MongoTool) error { + _, err := walletmod.DebitLotteryTimes(t, times, uid) + if err != nil { + return err + } + log.InfoX(ctx, "活动扣款-扣除抽奖免费次数", log.Any("uid", uid), log.Any("times", times), log.Any("activityId", req.ActivityId)) + return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + UID: uid, + Amount: -times, + TranType: txnmod.ActivityDeductLotteryTimes.Key(), + TranTypeInt: int64(txnmod.ActivityDeductLotteryTimes), + Desc: fmt.Sprintf("%s-扣除抽奖免费%d次", desc, times), + SysType: u.SysType, + }) + }) + if err != nil { + return stderr.InsufficientLotteryFreeTimes, fmt.Errorf("抽奖免费次数不足") + } + return stderr.Success, nil +} diff --git a/app/service/activityser/reward.go b/app/service/activityser/reward.go new file mode 100644 index 0000000..8f02c59 --- /dev/null +++ b/app/service/activityser/reward.go @@ -0,0 +1,378 @@ +package activityser + +import ( + "context" + "errors" + "fmt" + "strconv" + "time" + + "91porn-server/app/appg" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models/v/backpackmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/videocoupon" + "91porn-server/models/v/walletmod" +) + +// RewardType 奖励类型 +type RewardType int + +const ( + RewardGold RewardType = 1 // 金币 + RewardVIP RewardType = 2 // 会员卡(VIP天数) + RewardGoldBonusCoupon RewardType = 3 // 金币加赠券 + RewardGoldVideoCoupon RewardType = 4 // 金币观影券 + RewardAiChangeFaceFree RewardType = 5 // AI视频换脸免费次数 + RewardAiUndressFree RewardType = 6 // AI脱衣免费次数 + RewardIntegral RewardType = 7 // 积分 + RewardPhysical RewardType = 8 // 实物奖品 +) + +// AddressInfo 收货地址信息 +type AddressInfo struct { + Name string `json:"name"` + Phone string `json:"phone"` + Address string `json:"address"` +} + +// RewardReq 发奖请求 +type RewardReq struct { + RequestId string `json:"requestId"` + UserId string `json:"userId" binding:"required"` + RewardType RewardType `json:"rewardType" binding:"required"` + Amount int64 `json:"amount" binding:"required"` + CouponValue int64 `json:"couponValue"` + Duration int `json:"duration"` + ActivityId string `json:"activityId"` + RewardName string `json:"rewardName"` + Remark string `json:"remark"` + Address *AddressInfo `json:"address"` +} + +// RewardItem 批量发奖中的单个奖品 +type RewardItem struct { + RequestId string `json:"requestId"` + RewardType RewardType `json:"rewardType"` + Amount int64 `json:"amount"` + CouponValue int64 `json:"couponValue"` + Duration int `json:"duration"` + RewardName string `json:"rewardName"` + Address *AddressInfo `json:"address"` +} + +// BatchRewardReq 批量发奖请求 +type BatchRewardReq struct { + UserId string `json:"userId" binding:"required"` + ActivityId string `json:"activityId"` + Remark string `json:"remark"` + Rewards []RewardItem `json:"rewards" binding:"required"` +} + +// GrantBatchReward 批量发放活动奖励 +func GrantBatchReward(ctx context.Context, req *BatchRewardReq) error { + if len(req.Rewards) == 0 { + return nil + } + uid, err := strconv.ParseUint(req.UserId, 10, 64) + if err != nil { + return fmt.Errorf("invalid userId: %s", req.UserId) + } + u, err := usermod.FindUserByUID(uid) + if err != nil { + return fmt.Errorf("查询用户异常: %w", err) + } + if u == nil { + return fmt.Errorf("用户不存在: %s", req.UserId) + } + return appg.VideoDB.Trans(func(t *db.MongoTool) error { + for _, item := range req.Rewards { + single := &RewardReq{ + RequestId: item.RequestId, + UserId: req.UserId, + RewardType: item.RewardType, + Amount: item.Amount, + CouponValue: item.CouponValue, + Duration: item.Duration, + ActivityId: req.ActivityId, + RewardName: item.RewardName, + Remark: req.Remark, + Address: item.Address, + } + if err := grantRewardTx(ctx, t, uid, u, single); err != nil { + log.ErrorX(ctx, "批量发奖-单项失败已跳过", + log.Any("uid", req.UserId), + log.Any("requestId", item.RequestId), + log.Any("rewardType", item.RewardType), + log.E(err)) + } + } + return nil + }) +} + +// GrantReward 发放活动奖励(单个) +func GrantReward(ctx context.Context, req *RewardReq) error { + uid, err := strconv.ParseUint(req.UserId, 10, 64) + if err != nil { + return fmt.Errorf("invalid userId: %s", req.UserId) + } + u, err := usermod.FindUserByUID(uid) + if err != nil { + return fmt.Errorf("查询用户异常: %w", err) + } + if u == nil { + return fmt.Errorf("用户不存在: %s", req.UserId) + } + + return appg.VideoDB.Trans(func(t *db.MongoTool) error { + return grantRewardTx(ctx, t, uid, u, req) + }) +} + +func grantRewardTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq) error { + desc := req.Remark + + switch req.RewardType { + case RewardGold: + return grantGoldTx(ctx, t, uid, u, req, desc) + case RewardVIP: + return grantVIPTx(ctx, t, uid, u, req, desc) + case RewardGoldBonusCoupon: + return grantGoldBonusCouponTx(ctx, t, uid, u, req, desc) + case RewardGoldVideoCoupon: + return grantGoldVideoCouponTx(ctx, t, uid, u, req, desc) + case RewardAiChangeFaceFree: + return grantAiChangeFaceFreeTx(ctx, t, uid, u, req, desc) + case RewardAiUndressFree: + return grantAiUndressFreeTx(ctx, t, uid, u, req, desc) + case RewardIntegral: + return grantIntegralTx(ctx, t, uid, u, req, desc) + case RewardPhysical: + return grantPhysicalTx(ctx, t, uid, u, req, desc) + default: + return fmt.Errorf("不支持的奖励类型: %d", req.RewardType) + } +} + +func grantGoldTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error { + amount := req.Amount + creditPlan := walletmod.CreditPlan{Amount: &amount} + + w, err := walletmod.Credit(t, creditPlan, uid) + if err != nil { + return err + } + log.InfoX(ctx, "活动发放-发放金币", log.Any("uid", uid), log.Any("amount", amount), log.Any("activityId", req.ActivityId)) + return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + UID: uid, + Amount: amount, + ActualAmount: float64(amount), + TranType: txnmod.ActivityRewardGold.Key(), + TranTypeInt: int64(txnmod.ActivityRewardGold), + Desc: fmt.Sprintf("%s-发放%d金币", desc, amount), + SysType: u.SysType, + RealAmount: w.RealAmount(), + UniqueOrder: req.RequestId, + }) +} + +func grantVIPTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error { + days := int(req.Amount) + if days <= 0 { + return errors.New("VIP天数必须大于0") + } + + var expire time.Time + if u.VipExpireDate.After(time.Now()) { + expire = u.VipExpireDate.AddDate(0, 0, days) + } else { + expire = time.Now().AddDate(0, 0, days) + } + + vipLevel := 1 + if u.VipLevel > vipLevel { + vipLevel = u.VipLevel + } + + sel := usermod.UserSelector{ + VipExpireDate: &expire, + VipLevel: &vipLevel, + } + if err := usermod.UpdateVIP(t, uid, u.VipExpireDate, sel); err != nil { + return err + } + log.InfoX(ctx, "活动发放-发放VIP", log.Any("uid", uid), log.Any("days", days), log.Any("activityId", req.ActivityId)) + return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + UID: uid, + TranType: txnmod.ActivityRewardVIP.Key(), + TranTypeInt: int64(txnmod.ActivityRewardVIP), + Desc: fmt.Sprintf("%s-发放VIP%d天", desc, days), + SysType: u.SysType, + UniqueOrder: req.RequestId, + }) +} + +func grantGoldBonusCouponTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error { + if req.CouponValue <= 0 { + return errors.New("券额度(couponValue)必须大于0") + } + + if err := txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + UID: uid, + Amount: req.Amount, + TranType: txnmod.ActivityRewardGoldBonusCoupon.Key(), + TranTypeInt: int64(txnmod.ActivityRewardGoldBonusCoupon), + Desc: fmt.Sprintf("%s-发放金币加赠券%d张", desc, req.Amount), + SysType: u.SysType, + UniqueOrder: req.RequestId, + }); err != nil { + return err + } + + now := time.Now() + expiredAt := now.AddDate(0, 0, 30) + for i := int64(0); i < req.Amount; i++ { + item := backpackmod.Backpack{ + UID: uid, + GoodsType: backpackmod.GoldBonusCoupon, + GoodsValue: req.CouponValue, + GoodsOrigin: desc, + Status: backpackmod.Unused, + ExpiredTime: expiredAt, + CreateTime: now, + } + if err := backpackmod.AddGoods(t, uid, item); err != nil { + return err + } + } + log.InfoX(ctx, "活动发放-发放金币加赠券", log.Any("uid", uid), log.Any("couponValue", req.CouponValue), log.Any("amount", req.Amount)) + return nil +} + +func grantGoldVideoCouponTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error { + if req.CouponValue <= 0 { + return errors.New("券额度(couponValue)必须大于0") + } + + if err := txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + UID: uid, + Amount: req.Amount, + TranType: txnmod.ActivityRewardGoldVideoCoupon.Key(), + TranTypeInt: int64(txnmod.ActivityRewardGoldVideoCoupon), + Desc: fmt.Sprintf("%s-发放金币观影券%d张", desc, req.Amount), + SysType: u.SysType, + UniqueOrder: req.RequestId, + }); err != nil { + return err + } + + coupons := make([]videocoupon.UserGoldVideoCoupon, 0, req.Amount) + for i := int64(0); i < req.Amount; i++ { + coupons = append(coupons, videocoupon.UserGoldVideoCoupon{ + UID: uid, + Num: int(req.CouponValue), + Used: false, + Source: videocoupon.GoldVideoCouponSource(desc), + }) + } + if err := videocoupon.InsertManyTrans(t, coupons); err != nil { + return err + } + log.InfoX(ctx, "活动发放-发放金币观影券", log.Any("uid", uid), log.Any("couponValue", req.CouponValue), log.Any("amount", req.Amount)) + return nil +} + +func grantAiChangeFaceFreeTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error { + if err := txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + UID: uid, + Amount: req.Amount, + TranType: txnmod.ActivityRewardAiChangeFaceFree.Key(), + TranTypeInt: int64(txnmod.ActivityRewardAiChangeFaceFree), + Desc: fmt.Sprintf("%s-发放AI换脸免费%d次", desc, req.Amount), + SysType: u.SysType, + UniqueOrder: req.RequestId, + }); err != nil { + return err + } + + now := time.Now() + permanent := time.Date(2099, 12, 31, 23, 59, 59, 0, time.Local) + item := backpackmod.Backpack{ + UID: uid, + GoodsType: backpackmod.AiChangeFaceDiscount, + GoodsValue: req.Amount, + GoodsOrigin: desc, + Status: backpackmod.Unused, + ExpiredTime: permanent, + CreateTime: now, + } + if err := backpackmod.AddGoods(t, uid, item); err != nil { + return err + } + log.InfoX(ctx, "活动发放-发放AI换脸免费次数", log.Any("uid", uid), log.Any("times", req.Amount)) + return nil +} + +func grantIntegralTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error { + integral := req.Amount + + _, err := walletmod.CreditIntegral(t, integral, uid) + if err != nil { + return err + } + log.InfoX(ctx, "活动发放-发放积分", log.Any("uid", uid), log.Any("integral", integral), log.Any("activityId", req.ActivityId)) + return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + UID: uid, + Amount: integral, + TranType: txnmod.ActivityRewardIntegral.Key(), + TranTypeInt: int64(txnmod.ActivityRewardIntegral), + Desc: fmt.Sprintf("%s-发放%d积分", desc, integral), + SysType: u.SysType, + UniqueOrder: req.RequestId, + }) +} + +func grantAiUndressFreeTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error { + times := req.Amount + creditPlan := walletmod.CreditPlan{AiUndressFreeTimes: ×} + + _, err := walletmod.Credit(t, creditPlan, uid) + if err != nil { + return err + } + log.InfoX(ctx, "活动发放-发放AI脱衣免费次数", log.Any("uid", uid), log.Any("times", times), log.Any("activityId", req.ActivityId)) + return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + UID: uid, + Amount: times, + TranType: txnmod.ActivityRewardAiUndressFree.Key(), + TranTypeInt: int64(txnmod.ActivityRewardAiUndressFree), + Desc: fmt.Sprintf("%s-发放AI脱衣免费%d次", desc, times), + SysType: u.SysType, + UniqueOrder: req.RequestId, + }) +} + +func grantPhysicalTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error { + addrDesc := "" + if req.Address != nil { + addrDesc = fmt.Sprintf("【 收件人:%s 手机:%s 地址:%s】", req.Address.Name, req.Address.Phone, req.Address.Address) + } + rewardName := req.RewardName + if rewardName == "" { + rewardName = "实物奖品" + } + + log.InfoX(ctx, "活动发放-发放实物奖品", log.Any("uid", uid), log.Any("rewardName", rewardName), log.Any("activityId", req.ActivityId), log.Any("address", addrDesc)) + return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + UID: uid, + Amount: req.Amount, + TranType: txnmod.ActivityRewardPhysical.Key(), + TranTypeInt: int64(txnmod.ActivityRewardPhysical), + Desc: fmt.Sprintf("%s-发放%s%s", desc, rewardName, addrDesc), + SysType: u.SysType, + UniqueOrder: req.RequestId, + }) +} diff --git a/app/service/actvser/actv.go b/app/service/actvser/actv.go new file mode 100644 index 0000000..050f99d --- /dev/null +++ b/app/service/actvser/actv.go @@ -0,0 +1,51 @@ +package actvser + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + + "91porn-server/models/v/actmod" +) + +var dateMax = time.Date(2999, 12, 31, 0, 0, 0, 0, time.Local) + +func GetActivities(req GetActitiesRequest) ([]actmod.Activity, bool, error) { + skip := uint64((req.PageNumber - 1) * req.PageSize) + limit := uint64(req.PageSize + 1) + actvs, err := actmod.GetActivitiesValid(skip, limit) + if err != nil { + return nil, false, err + } + hasNext := false + if uint64(len(actvs)) > req.PageSize { + hasNext = true + actvs = actvs[:req.PageSize] + } + now := time.Now() + for i := range actvs { + if actvs[i].ExpiredIn.IsZero() { + actvs[i].ExpiredIn = dateMax + continue + } + if actvs[i].ExpiredIn.Before(now) { + actvs[i].Status = 2 // 状态设置为已过期 + } + } + return actvs, hasNext, nil +} + +func GetActiveByID(id primitive.ObjectID) (*actmod.Activity, error) { + actv, err := actmod.GetActivityByActivityID(id) + if err != nil { + return nil, err + } + if actv.ExpiredIn.IsZero() { + actv.ExpiredIn = dateMax + return actv, nil + } + if actv.Status == 1 && actv.ExpiredIn.Before(time.Now()) { + actv.Status = 2 + } + return actv, nil +} diff --git a/app/service/actvser/type.go b/app/service/actvser/type.go new file mode 100644 index 0000000..061c6c3 --- /dev/null +++ b/app/service/actvser/type.go @@ -0,0 +1,11 @@ +package actvser + +import "91porn-server/models/commod" + +type GetActitiesRequest struct { + commod.Page +} + +type GetActiveByIDRequest struct { + ID string `form:"id" json:"id" binding:"required"` +} diff --git a/app/service/adser/adser.go b/app/service/adser/adser.go new file mode 100644 index 0000000..420e16a --- /dev/null +++ b/app/service/adser/adser.go @@ -0,0 +1,83 @@ +package adser + +import ( + "sort" + "sync" + + "91porn-server/app/proto" + "91porn-server/app/service/annouser" + "91porn-server/common" + "91porn-server/models/v/adsmod" + "91porn-server/models/v/annoumod" + + "github.com/gin-gonic/gin" +) + +func toAdsResSlice(slice adsmod.AdsSlice) []proto.AdsInfo { + ads := make([]proto.AdsInfo, len(slice)) + for i, v := range slice { + ads[i] = proto.AdsInfo{ + // ID: v.ID, + Cover: v.Cover, + Href: v.Href, + Position: int(v.Position), + SortCode: v.SortCode, + } + } + return ads +} + +var sysAds = adsmod.SysAds + +func AdsList(adsType adsmod.AdsType, discCode string) (res proto.AdsRes) { + var ads []proto.AdsInfo + var announ []*annouser.Annou + wait := sync.WaitGroup{} + wait.Add(2) + common.Go(func() { + defer wait.Done() + if adsType == adsmod.SysAds { + adSlice, _ := adsmod.ActiveStartAndEndList(adsmod.Sort_sortCode_N1, nil, nil, (&adsmod.AdsTypeMatch{AdsType: &sysAds}).New()) + ads = toAdsResSlice(adSlice) + } else { + ads = toAdsResSlice(GetDiscAdSlice(discCode)) + } + }) + common.Go(func() { + defer wait.Done() + announInfo, _ := annouser.GetAnnouList(annoumod.TotalType) + announ = append(announ, announInfo) + }) + wait.Wait() + res.AdsList = ads + if gin.Mode() == gin.DebugMode { //测试模式下向外暴露链接 + res.AnnounInfo = announ + return + } + res.AnnounInfo = announ + return +} + +func GetDiscAdSlice(discCode string) adsmod.AdsSlice { + mats := []adsmod.Matcher{ + (&adsmod.DistrictCodeMatch{DistrictCode: &discCode}).New(), + } + discADSlice, _ := adsmod.ActiveStartAndEndList(adsmod.Sort_sortCode_N1, nil, nil, mats...) + //未配置渠道广告的广告位,展示系统广告 + mats = []adsmod.Matcher{ + (&adsmod.AdsTypeMatch{AdsType: &sysAds}).New(), + (&adsmod.AdPositionNinMatch{PositionList: discADSlice.ToPositions()}).New(), + } + ads, _ := adsmod.ActiveStartAndEndList(adsmod.Sort_sortCode_N1, nil, nil, mats...) + ads = append(ads, discADSlice...) + sort.Sort(ads) + return ads +} + +func AdList(adsType adsmod.AdsType, discCode string) []proto.AdsInfo { + if adsType == adsmod.SysAds { + adSlice, _ := adsmod.ActiveStartAndEndList(adsmod.Sort_sortCode_N1, nil, nil, (&adsmod.AdsTypeMatch{AdsType: &sysAds}).New()) + return toAdsResSlice(adSlice) + } + return toAdsResSlice(GetDiscAdSlice(discCode)) +} diff --git a/app/service/adser/advertiseV2.go b/app/service/adser/advertiseV2.go new file mode 100644 index 0000000..409c27f --- /dev/null +++ b/app/service/adser/advertiseV2.go @@ -0,0 +1,154 @@ +package adser + +import ( + "91porn-server/models/v/dailytaskmod" + "encoding/json" + "errors" + "fmt" + "net/http" + "time" + + "91porn-server/app/appg" + "91porn-server/app/service/taskser" + "91porn-server/common/constant/redisconst" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/models/commod" + "91porn-server/models/l/adsclicklogmod" + "91porn-server/models/v/usermod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AdvertiseReq struct { + AppId int32 `form:"appId" json:"appId" binding:"required"` // 应用id + LocId *int32 `form:"locId" json:"locId" binding:"omitempty"` // 位置id + UserId *uint64 `form:"userId" json:"userId" binding:"omitempty"` // 用户ID + IP *string `form:"ip" json:"ip" binding:"omitempty"` // 用户IP +} + +type AdvertiseRes struct { + AdvertiseList []AdvertiseInfo `json:"advertiseList"` // 广告列表 +} + +type AdsClickReq struct { + Id string `from:"id" json:"id" binding:"required"` // 广告id + AppId int32 `from:"appId" json:"appId" binding:"required"` // 应用id + ChannelCode string `from:"channelCode" json:"channelCode" binding:"channelCode"` +} + +type AdsClickResp struct { + Code int64 `from:"code" json:"code"` // 错误骂 +} + +// 外部广告信息 +type AdvertiseInfo struct { + Id int64 `json:"id"` // 广告id + Title string `json:"title"` // 广告标题 + CoverImg string `json:"coverImg"` // 封面 + LocId int32 `json:"locId"` // 位置id + JumpType int32 `json:"jumpType"` // 跳转方式 0:外部浏览器跳转 1:内部浏览器跳转 2:app内部跳转 + Link string `json:"link"` // 链接地址 + Sort int64 `json:"sort"` // 排序 +} + +func AdvertiseThreeServer(userId uint64, ip string) (*AdvertiseRes, error) { + str, err := appg.Redis.Get(redisconst.CenterAdvertiseCache) + if err != nil { + log.Warn(fmt.Sprintf("用户ID:%d;IP:%s;缓存获取广告列表信息异常:%v", userId, ip, err)) + } + var resp AdvertiseRes + if str != nil { + if err = json.Unmarshal([]byte(*str), &resp); err == nil { + return &resp, nil + } + log.Warn(fmt.Sprintf("用户ID:%d;IP:%s;解析缓存数据异常:%v", userId, ip, err)) + } + req := AdvertiseReq{ + AppId: commod.KFK_APPID, + IP: &ip, + } + if userId > 0 { + req.UserId = &userId + } + url := appg.Conf.URL.ProductUrl + "/api/stat/ads/get" + bodyStr, _ := json.Marshal(req) + code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr) + if err != nil { + log.Error("AdvertiseThreeServer POSTWithJResp ", log.Any("url", url), log.E(err)) + return nil, err + } + if code != http.StatusOK { + log.Error("AdvertiseThreeServer response status ", log.Any("code", code)) + return nil, fmt.Errorf("response status err") + } + data, err := json.Marshal(&resp) + if err != nil { + log.Warn(fmt.Sprintf("用户ID:%d;IP:%s;json序列化缓存数据异常:%v", userId, ip, err)) + return &resp, nil + } + if err = appg.Redis.Set(redisconst.CenterAdvertiseCache, data, 45*time.Second); err != nil { + log.Warn(fmt.Sprintf("用户ID:%d;IP:%s;保存缓存数据异常:%v", userId, ip, err)) + } + // log.Info("AdvertiseThreeServer response", log.Any("resp", resp)) + return &resp, nil +} + +func AdvertiseClickThreeServer(advertiseId string, uid uint64) error { + //查询用户信息 + u, err := usermod.FindUserByUID(uid) + if err != nil { + return err + } + if u.DistrictCode == "" { + u.DistrictCode = "system" + } + req := AdsClickReq{ + AppId: commod.KFK_APPID, + Id: advertiseId, + ChannelCode: u.DistrictCode, + } + resp := AdsClickResp{} + url := appg.Conf.URL.ProductUrl + "/api/product/ads/dayClick" + bodyStr, err := json.Marshal(req) + if err != nil { + log.Info(fmt.Sprintf("AdvertiseClickThreeServer json.Marshal is fail error:%+v/data:%+v", err, req)) + return err + } + code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr) + if err != nil { + log.Error("AdvertiseClickThreeServer POSTWithJResp ", log.Any("url", url), log.E(err)) + return err + } + if code != http.StatusOK { + log.Error("AdvertiseClickThreeServer response status ", log.Any("code", code)) + return fmt.Errorf("response status err") + } + return nil +} + +func AdvertiseNavigate(advertiseId string, uid uint64, ua, logType string) error { + advId, err := primitive.ObjectIDFromHex(advertiseId) + if err != nil { + return err + } + if advId.IsZero() { + return errors.New("empty advid") + } + header := map[string]string{ + "log-id": advertiseId, + "User-Agent": ua, + "log-type": logType, + } + targetUrl := appg.Conf.URL.NavigateUrl + "/api/application/dHH9h0Kp1074" + if _, _, err = httputil.DefaultClientGetBytes(targetUrl, header, nil); err != nil { + log.Error("navigate report fail", log.E(err)) + return err + } + if err := adsclicklogmod.AddClick(uid, advId, logType, 1); err != nil { + log.Error("adclick fail", log.E(err)) + return err + } + go func() { _ = taskser.CompleteDailyTask(nil, uid, dailytaskmod.DailyTaskTypeAdsClick) }() + return nil +} diff --git a/app/service/adser/jtAdvertise.go b/app/service/adser/jtAdvertise.go new file mode 100644 index 0000000..e4882a6 --- /dev/null +++ b/app/service/adser/jtAdvertise.go @@ -0,0 +1,262 @@ +package adser + +import ( + "91porn-server/app/proto" + "encoding/json" + "errors" + "fmt" + "net/http" + "sort" + "strconv" + "strings" + "time" + + "91porn-server/app/appg" + "91porn-server/app/service/activityclient" + "91porn-server/common" + "91porn-server/common/crypt" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" +) + +/* +测试: +部门CODE:1991129769626804224 +商户CODE:1991129808640122880 +应用CODE:jh_appid01 +客户CODE:1991129948159451136 +*/ + +type JtAdvertiseReq struct { + MerchantCode string `json:"merchantCode"` // 商户 code,必填 + AppCode string `json:"appCode"` // 应用 code,必填 + AdStatus int `json:"adStatus"` // 广告状态 1-上架,0-下架,2-未开始,3-已过期,4-即将到期 默认查询全部 + //AdvertiseLocationCode string `json:"advertiseLocationCode,omitempty"` // 广告位唯一标识,选填 + //AdvertiseCode string `json:"advertiseCode,omitempty"` // 广告唯一标识,选填 + //PageNo int `json:"pageNo,omitempty"` // 页码,全量查询不需要传,选填 + //PageSize int `json:"pageSize,omitempty"` // 每页条数,全量查询不需要传,选填 +} + +type JtAdvertiseRes struct { + Code int `json:"code"` // 返回状态码 + Msg string `json:"msg"` // 返回消息 + Data string `json:"data"` // 广告信息 - 加密 +} + +// AdSlot 广告位信息 +type AdSlot struct { + DeptCode string `json:"deptCode"` // 部门 code + MerchantCode string `json:"merchantCode"` // 商户 code + AppCode string `json:"appCode"` // 应用 code + LocationType int `json:"locationType"` // 广告位类型:0 → 开屏广告 1 → 弹窗广告 2 → 图标广告 3 → 九宫格广告 4 → 信息流广告 5 → 瀑布流广告 6 → 详情广告 7 → 片头广告 + AdvertiseLocationCode string `json:"advertiseLocationCode"` // 广告位标识 + AdvertiseLocationName string `json:"advertiseLocationName"` // 广告位名称 + DisplayMode int `json:"displayMode"` // 展示模式:0 → 多个轮播, 1 → 单体, 2 → 三分屏, 3 → 四分屏, 4 → 小图标, 5 → 大图标 + AdvertiseHeight int `json:"advertiseHeight"` // 高 + AdvertiseWidth int `json:"advertiseWidth"` // 宽 + MaterialType int `json:"materialType"` // 素材类型(视频/图片):1 → 视频, 2 → 图片, 3 → 视频、图片 + AdLimit int `json:"adLimit"` // 广告限制数量 + AdDetailInfoList []AdDetailInfo `json:"adDetailInfoList"` // 广告详情信息列表 +} + +// AdDetailInfo 广告详情信息 +type AdDetailInfo struct { + AdvertiseCode string `json:"advertiseCode"` // 广告业务code + CustomerCode string `json:"customerCode"` // 客户code + AdvertiseName string `json:"advertiseName"` // 广告名称 + AdvertiseUrl string `json:"advertiseUrl"` // 广告链接 + AdvertiseIcon string `json:"advertiseIcon"` // 广告图片地址 + AdvertiseType int `json:"advertiseType"` // 广告类型:1 → 播放器, 2 → 药台, 3 → 炮台, 4 → 黄游, 5 → 直播, 6 → BC + AdMode string `json:"adMode"` // 广告模式 + AppCode string `json:"appCode"` // 应用 code + LocationType int `json:"locationType"` // 广告位类型:0 → 开屏广告 1 → 弹窗广告 2 → 图标广告 3 → 九宫格广告 4 → 信息流广告 5 → 瀑布流广告 6 → 详情广告 7 → 片头广告 + AdvertiseDesc string `json:"advertiseDesc"` // 广告描述 + PageType string `json:"pageType"` // 内部路由页面类型:如 rechange_page → 会员充值页, gold_rechange_page → 金币充值页, invite_page → 邀请分享页 + StartTimeStamp int64 `json:"startTimeStamp"` // 广告开始时间 时间戳 + EndTimeStamp int64 `json:"endTimeStamp"` // 广告结束时间 时间戳 + Sort int `json:"sort"` // 排序 + AdExtData string `json:"adExtData"` // 扩展字段1 = CoverImgSize +} + +type Extra struct { + CoverImgSize string `json:"coverImgSize"` + WatchTime int `json:"watchTime"` + DownloadCount int64 `json:"downloadCount"` + Type string `json:"type"` // 应用类型 app:APP、web:网页 + Location string `json:"location"` // 位置 1:顶部 2:底部 + DisplayType string `json:"displayType"` // 展示类型 heng:横版 shu:竖版 +} + +func (ad AdDetailInfo) GetExtraData() (res Extra) { + _ = json.Unmarshal([]byte(ad.AdExtData), &res) + return +} + +func (ad AdDetailInfo) GetRealLink(user *usermod.User, w *walletmod.Wallet) string { + ad.AdvertiseUrl = activityclient.ReplaceActivityDomain(ad.AdvertiseUrl, user, w) + return strings.ReplaceAll(ad.AdvertiseUrl, "inner://yinseinner/", "yinseinner://") +} + +func (ad AdDetailInfo) GetCoverLsj() string { + if ad.AdvertiseIcon == "" { + return "" + } + + return strings.TrimLeft(ad.AdvertiseIcon, "/") +} + +func (ad AdDetailInfo) GetAdvertiseType() int { + // 内部顺序:1 应用 2 游戏 3 炮台 4 棋牌 5 直播 + // 外部顺序:1:应用 2:药台、3:炮台、4:游戏、5:直播、6:棋牌 + if ad.AdvertiseType == 4 { + return 2 + } + if ad.AdvertiseType == 6 { + return 4 + } + + return ad.AdvertiseType +} + +// JtAdvertiseThreeServer 集团广告中心 +func JtAdvertiseThreeServer() ([]AdSlot, error) { + var foreverCacheKey = fmt.Sprintf("jtAdForever-%s-%s", appg.Conf.AdCenter.MerchantCode, appg.Conf.AdCenter.AppCode) + redisKey := fmt.Sprintf("jtAd-%s-%s", appg.Conf.AdCenter.MerchantCode, appg.Conf.AdCenter.AppCode) + str, err := appg.Redis.Get(redisKey) + if err != nil { + log.Error(fmt.Sprintf("JtAdvertiseThreeServer 缓存获取广告列表信息异常:%v", err)) + } + var resp []AdSlot + if str != nil { + if err = json.Unmarshal([]byte(*str), &resp); err == nil { + return resp, nil + } + log.Error(fmt.Sprintf("JtAdvertiseThreeServer 解析缓存数据异常:%v", err)) + } + req := JtAdvertiseReq{ + MerchantCode: appg.Conf.AdCenter.MerchantCode, + AppCode: appg.Conf.AdCenter.AppCode, + AdStatus: 1, + } + + var serverResp *JtAdvertiseRes + url := appg.Conf.AdCenter.ApiDomain + "/openapi/getAdvertiseList" + bodyStr, _ := json.Marshal(req) + code, err := httputil.DefaultClientPostJsonWithResp(&serverResp, url, nil, bodyStr) + if err != nil { + log.Error("JtAdvertiseThreeServer POSTWithJResp ", log.Any("url", url), log.E(err)) + // 从最后请求成功的一次拿数据 + return getFromLastTime(foreverCacheKey, err) + } + if code != http.StatusOK { + log.Error("JtAdvertiseThreeServer response status ", log.Any("code", code)) + // 从最后请求成功的一次拿数据 + return getFromLastTime(foreverCacheKey, errors.New("response status err")) + } + + if serverResp.Code != 0 { + log.Error("JtAdvertiseThreeServer response code ", log.Any("code", serverResp.Code), log.Any("msg", serverResp.Msg)) + // 从最后请求成功的一次拿数据 + return getFromLastTime(foreverCacheKey, errors.New("response code err")) + } + + adsData, err := crypt.AdDecrypt(serverResp.Data, appg.Conf.AdCenter.AesKey) + if err != nil { + log.Error(fmt.Sprintf("JtAdvertiseThreeServer 解密广告数据异常:%v", err)) + return nil, errors.New("response data err") + } + err = json.Unmarshal([]byte(adsData), &resp) + if err != nil { + log.Error(fmt.Sprintf("JtAdvertiseThreeServer 解析 JSON 失败:%v", err)) + return nil, errors.New("data format error") + } + + // 对广告进行升序排序 + for i := range resp { + sort.Slice(resp[i].AdDetailInfoList, func(j, k int) bool { + return resp[i].AdDetailInfoList[j].Sort < resp[i].AdDetailInfoList[k].Sort + }) + } + + // 将最后一次请求成功的数据更新到二级缓存 + common.Go(func() { + setAdsCache(foreverCacheKey, resp) + }) + + common.Go(func() { + data, err := json.Marshal(&resp) + if err != nil { + log.Error(fmt.Sprintf("JtAdvertiseThreeServer json序列化缓存数据异常:%v", err)) + return + } + if err = appg.Redis.Set(redisKey, data, time.Minute); err != nil { + log.Error(fmt.Sprintf("JtAdvertiseThreeServer 保存缓存数据异常:%v", err)) + } + }) + + return resp, nil +} + +// getFromLastTime 返回最后一次请求的广告数据 +func getFromLastTime(foreverCacheKey string, e error) ([]AdSlot, error) { + str, err := appg.Redis.Get(foreverCacheKey) + if err != nil { + log.Error(fmt.Sprintf("getFromLastTime 缓存获取广告列表信息异常:%v", err)) + return nil, e + } + + var resp []AdSlot + if str != nil { + if err = json.Unmarshal([]byte(*str), &resp); err == nil { + return resp, nil + } + log.Error(fmt.Sprintf("getFromLastTime 解析缓存数据异常:%v", err)) + } + + return nil, e +} + +// setAdsCache 更新最后一次请求的广告数据 +func setAdsCache(foreverCacheKey string, resp []AdSlot) { + data, err := json.Marshal(&resp) + if err != nil { + log.Error(fmt.Sprintf("setAdsCache json序列化缓存数据异常:%v", err)) + return + } + + if err := appg.Redis.Set(foreverCacheKey, data, time.Hour*72); err != nil { + log.Error(fmt.Sprintf("setAdsCache 保存缓存数据异常:%v", err)) + } +} + +func GetAdvertiseLocation(adverId string) (res proto.AdsInfo) { + list, err := JtAdvertiseThreeServer() + if err != nil { + return + } + for _, adverLocation := range list { + for _, ad := range adverLocation.AdDetailInfoList { + if ad.AdvertiseCode == adverId { + pos, _ := strconv.Atoi(adverLocation.AdvertiseLocationCode) + extra := ad.GetExtraData() + res = proto.AdsInfo{ + ID: ad.AdvertiseCode, + Title: ad.AdvertiseName, + Description: ad.AdvertiseDesc, + Cover: ad.GetCoverLsj(), + Href: ad.GetRealLink(nil, nil), + Position: pos, + PositionName: adverLocation.AdvertiseLocationName, + SortCode: ad.Sort, + CoverImgSize: extra.CoverImgSize, + WatchTime: extra.WatchTime, + } + return res + } + } + + } + return +} diff --git a/app/service/adser/recreationSer.go b/app/service/adser/recreationSer.go new file mode 100644 index 0000000..4c51215 --- /dev/null +++ b/app/service/adser/recreationSer.go @@ -0,0 +1,222 @@ +package adser + +import ( + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/models/commod" + + "github.com/vmihailenco/msgpack/v5" +) + +/* +福利-游戏广告 100001 GameAppList +福利-直播广告 100002 ZbAppList +福利-炮台广告 100003 YpAppList +福利-棋牌广告 100004 QpAppList +福利-视频广告 100005 +福利-横版广告 100006 HengAppList +福利-横一广告 100007 +福利-竖版广告 100008 ShuAppList +福利-文字广告 100009 +福利-Banner广告 100099 AdvList +*/ +const ( + AdPosGame = 100001 // 游戏 + AdPosZhiBo = 100002 // 直播 + AdPosPaoTai = 100003 // 约炮 + AdPosQiPai = 100004 // 棋牌 + AdPosShiPin = 100005 // 视频 + AdPosHeng = 100006 // 横 + AdPosHeng1 = 100007 // 横1 + AdPosShu = 100008 // 竖 + AdPosWenZi = 100009 // 文字 + AdPosAds = 100099 // BANNER广告 +) + +// RecreationListRes 娱乐广告列表请求返回 +type RecreationListRes struct { + HengAppList []AppInfo `json:"hengApp" bson:"hengApp"` + ShuAppList []AppInfo `json:"shuApp" bson:"shuApp"` + AdvList []AdvInfo `json:"adv" bson:"adv"` + GameAppList []AppInfo `json:"gameApp" bson:"gameApp"` // 游戏 + YpAppList []AppInfo `json:"ypApp" bson:"ypApp"` // 约炮 + QpAppList []AppInfo `json:"qpApp" bson:"qpApp"` // 棋牌 + ZbAppList []AppInfo `json:"zbApp" bson:"zbApp"` // 直播 +} + +type CommonListRes struct { + Code int64 `json:"code"` + Data RecreationListRes `json:"data"` + Msg string `json:"msg"` + Time string `json:"time"` + Tip string `json:"tip"` +} + +// AppInfo 娱乐app实体信息 +type AppInfo struct { + Id string `json:"id"` // ID + Name string `json:"name"` // 应用名称 + Desc string `json:"desc"` // 应用描述 + Icon string `json:"icon"` // 应用图标 + Url string `json:"url"` // 应用链接 + Type string `json:"type"` // 应用类型 + DownloadNum int64 `json:"downloadNum"` // 应用下载次数 + //广告-集团上报数据 + Position int `json:"position"` // 集团广告code + PositionName string `json:"positionName"` // 集团广告名称 +} + +// AdvInfo 娱乐广告实体信息 +type AdvInfo struct { + Id string `json:"id"` // ID + Name string `json:"name"` // 广告名称 + Image string `json:"image"` // 广告图片 + Url string `json:"url"` // 广告链接 + Location string `json:"location"` // 位置 1:顶部 2:底部 + ModuleType int `json:"moduleType"` // 广告模块 1:应用 2:游戏 3:约炮 4:棋牌 5:直播 + //广告-集团上报数据 + Position int `json:"position"` // 集团广告code + PositionName string `json:"positionName"` // 集团广告名称 +} + +// RecreationClickInfo 娱乐广告点击参数信息 +type RecreationClickInfo struct { + Id string `form:"id" json:"id" binding:"required"` // 数据id + Type string `form:"type" json:"type" binding:"required"` // 数据类型,app or adv + SysType string `form:"sysType" json:"sysType" binding:"required"` // 用户设备类型. ios or android +} + +// RecreationThreeServer 娱乐广告列表,数据来源于导航站(navigate) +func RecreationThreeServer() (RecreationListRes, error) { + str, err := appg.Redis.Get(redisconst.NavigateRecreationCache) + if err != nil { + log.Warn(fmt.Sprintf("缓存获取娱乐广告信息异常:%v", err)) + return RecreationListRes{}, err + } + + var resp CommonListRes + if str != nil && *str != "" { + if err = msgpack.Unmarshal([]byte(*str), &resp); err == nil { + return resp.Data, nil + } + log.Warn(fmt.Sprintf("娱乐广告解析缓存数据异常:%v", err)) + } + //请求url + url := appg.Conf.URL.NavigateUrl + "/api/application/list/" + strconv.FormatInt(int64(commod.KFK_APPID), 10) + //请求 + code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, nil) + if err != nil { + log.Error("RecreationThreeServer POSTWithJResp", log.Any("url", url), log.E(err)) + return resp.Data, err + } + if code != http.StatusOK { + log.Error("RecreationThreeServer response status", log.Any("code", code)) + return resp.Data, errors.New("response status err") + } + if len(resp.Data.AdvList) > 0 || len(resp.Data.HengAppList) > 0 || len(resp.Data.ShuAppList) > 0 || + len(resp.Data.GameAppList) > 0 || len(resp.Data.YpAppList) > 0 || len(resp.Data.QpAppList) > 0 || len(resp.Data.ZbAppList) > 0 { + common.Go(func() { + data, err := msgpack.Marshal(resp) + if err != nil { + log.Warn(fmt.Sprintf("娱乐广告json序列化缓存数据异常:%v", err)) + return + } + if err = appg.Redis.Set(redisconst.NavigateRecreationCache, data, 6*time.Minute); err != nil { + log.Warn(fmt.Sprintf("娱乐广告保存缓存数据异常:%v", err)) + return + } + }) + } + return resp.Data, nil +} + +// RecreationClick 娱乐广告点击记录,请求第三方(navigate) +func RecreationClick(p RecreationClickInfo) { + //请求url + url := appg.Conf.URL.NavigateUrl + "/api/application/click" + //请求 + res, err := httputil.DefaultClientPostJson(url, nil, p) + if err != nil { + log.Error("RecreationClick POSTWithJResp", log.Any("url", url), log.E(err)) + return + } + if res.StatusCode != http.StatusOK { + log.Error("RecreationClick response status", log.Any("code", res.StatusCode)) + return + } +} + +// RecreationFromJt 从广告中心拉取福利页广告 +func RecreationFromJt() (RecreationListRes, error) { + var resp RecreationListRes + + jtAds, err := JtAdvertiseThreeServer() + if err != nil { + log.Error("JtAdvertiseThreeServer error occur", log.E(err)) + return resp, err + } + + for _, loc := range jtAds { + pos, err := strconv.Atoi(loc.AdvertiseLocationCode) + if err != nil { + log.Error("广告位置代码错误,必须为数字", log.Any("code", loc.AdvertiseLocationCode)) + continue + } + if pos < AdPosGame { + continue + } + + for _, ad := range loc.AdDetailInfoList { + extra := ad.GetExtraData() + if pos == AdPosAds { + resp.AdvList = append(resp.AdvList, AdvInfo{ + Id: ad.AdvertiseCode, + Name: ad.AdvertiseName, + Image: ad.GetCoverLsj(), + Url: ad.GetRealLink(nil, nil), + Location: extra.Location, + ModuleType: ad.GetAdvertiseType(), + Position: pos, + PositionName: loc.AdvertiseLocationName, + }) + continue + } + appInfo := AppInfo{ + Id: ad.AdvertiseCode, + Name: ad.AdvertiseName, + Desc: ad.AdvertiseDesc, + Icon: ad.GetCoverLsj(), + Url: ad.GetRealLink(nil, nil), + Type: extra.Type, + DownloadNum: extra.DownloadCount, + Position: pos, + PositionName: loc.AdvertiseLocationName, + } + switch pos { + case AdPosGame: + resp.GameAppList = append(resp.GameAppList, appInfo) + case AdPosZhiBo: + resp.ZbAppList = append(resp.ZbAppList, appInfo) + case AdPosPaoTai: + resp.YpAppList = append(resp.YpAppList, appInfo) + case AdPosQiPai: + resp.QpAppList = append(resp.QpAppList, appInfo) + case AdPosHeng: + resp.HengAppList = append(resp.HengAppList, appInfo) + case AdPosShu: + resp.ShuAppList = append(resp.ShuAppList, appInfo) + } + } + } + + return resp, nil +} diff --git a/app/service/adser/stat.go b/app/service/adser/stat.go new file mode 100644 index 0000000..df42b0d --- /dev/null +++ b/app/service/adser/stat.go @@ -0,0 +1,91 @@ +package adser + +import ( + "91porn-server/app/appg" + "91porn-server/common/constant" + "91porn-server/common/version" + "91porn-server/models/s/useradverstatmod" + "91porn-server/models/v/usermod" + "context" + "errors" + "time" + + "go.mongodb.org/mongo-driver/bson" +) + +func CanRecord(user *usermod.User) bool { + if user.SysType == constant.SysTypeAndroid && + //user.OriVer != "" && + user.AdGroup.InABC() { + return VersionCompare(user.OriVer, constant.AdGroupClientVersion) + } + return false +} + +func VersionCompare(v1, v2 string) bool { + ver1, err := version.New(v1) + if err != nil { + return false + } + ver2, err := version.New(v2) + if err != nil { + return false + } + return ver1.GTE(ver2) +} + +func UpsertAdStat(ctx context.Context, user *usermod.User, now time.Time, incAppClick, incAdClick int64, payAmount int64) error { + if !CanRecord(user) && appg.Conf.Base.Env != "test" { + return errors.New("user not eligible for ad stat recording") + } + local, _ := time.LoadLocation("Asia/Shanghai") + + // 把 day 归一化到当天 0 点,保证和索引一致 + now = now.In(local) + day := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, local) + + filter := bson.M{ + "userId": user.UID, + "date": day, + } + + inc := bson.M{} + if incAppClick != 0 { + inc["appClick"] = incAppClick + } + if incAdClick != 0 { + inc["adClick"] = incAdClick + } + deltaTotal := incAppClick + incAdClick + if deltaTotal != 0 { + inc["totalClick"] = deltaTotal + } + if payAmount != 0 { + inc["payCount"] = 1 + inc["payTotal"] = payAmount + } + createdAt := user.CreatedAt.In(local) + regDay := time.Date(createdAt.Year(), createdAt.Month(), createdAt.Day(), 0, 0, 0, 0, createdAt.Location()) + + update := bson.M{ + // 只在插入时写入的字段 + "$setOnInsert": bson.M{ + "userId": user.UID, + "date": day, + "createdAt": now, + "districtCode": user.DistrictCode, + "sysType": user.SysType, + "adGroup": user.AdGroup, + "regDay": regDay, + "version": user.OriVer, + }, + // 每次更新都要改的字段 + "$set": bson.M{ + "updatedAt": now, + }, + } + if len(inc) > 0 { + update["$inc"] = inc + } + return useradverstatmod.UpsertOne(filter, update) +} diff --git a/app/service/advance_config_ser/advance_config.go b/app/service/advance_config_ser/advance_config.go new file mode 100644 index 0000000..b739136 --- /dev/null +++ b/app/service/advance_config_ser/advance_config.go @@ -0,0 +1,134 @@ +package advance_config_ser + +import ( + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/models/commod" + "91porn-server/models/v/advanceconfigmod" + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/productmod" + "fmt" + "github.com/vmihailenco/msgpack/v5" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" + "sync" + "time" +) + +func GainAdvanceConfig(uid uint64) (data advanceconfigmod.AppAdvanceRes, err error) { + var resp advanceconfigmod.AppAdvanceRes + + str, err := appg.Redis.Get(redisconst.GameAdvanceKey(uid)) + if err != nil { + log.Warn(fmt.Sprintf("uid:%v;缓存获取游戏预售列表信息异常:%v", uid, err)) + } + if str != nil { + if err = msgpack.Unmarshal([]byte(*str), &resp); err == nil { + return resp, nil + } + log.Warn(fmt.Sprintf("uid:%v;解析游戏预售缓存数据异常:%v", uid, err)) + } + + wg := sync.WaitGroup{} + wg.Add(2) + common.Go(func() { + defer wg.Done() + filter := bson.M{"isDelete": false, "status": 1} + op := options.Find().SetSort(bson.M{"sortCode": -1}) + list, err := advanceconfigmod.QueryAllDocument(filter, op) + if err != nil { + log.Error(fmt.Sprintf("uid:%v, advanceconfigmod QueryAllDocument err:%v", uid, err)) + return + } + + for _, a := range list { + ad := advanceconfigmod.AppAdvanceConfig{ + Type: a.Type, + Cover: a.Cover, + Url: a.Url, + Name: a.Name, + SourceURL: a.SourceURL, + } + resp.List = append(resp.List, &ad) + } + }) + common.Go(func() { + defer wg.Done() + + products, err := productmod.FindByProductType(commod.GameAdvanceCard) + if err != nil { + log.Error(fmt.Sprintf("uid:%v, productmod FindByProductType err:%v", uid, err)) + return + } + + var gameCards []*advanceconfigmod.ProductCard + if products != nil && len(products) > 0 { + resp.ActivityDetail.ActivityTime = products[0].ActivityTime + resp.ActivityDetail.StartTime = products[0].StartTime + resp.ActivityDetail.EndTime = products[0].EndTime + for _, p := range products { + var privilege []advanceconfigmod.PrivilegeInfo + if len(p.NewPrivilege) > 0 { + for _, np := range p.NewPrivilege { + privilege = append(privilege, advanceconfigmod.PrivilegeInfo{ + Name: np.Name, + Image: np.Image, + Desc: np.Desc, + }) + } + } + + gc := &advanceconfigmod.ProductCard{ + ID: p.ID, + Type: p.Type, + Name: p.Name, + Position: p.Position, + BGImg: p.BGImg, + SendGame: p.SendGame, + OriginalPrice: p.OriginalPrice, + DiscountedPrice: *p.DiscountedPriceIos, + DiscountedPriceIos: *p.DiscountedPriceIos, + DiscountedPriceAnd: *p.DiscountedPriceAnd, + Desc: p.Desc, + NewPrivilege: privilege, + } + gameCards = append(gameCards, gc) + } + } + + // 获取用户购买记录 + histories, err := prdcthsomod.FindUserProductHistoryByType(uid) + if err != nil { + log.Error(fmt.Sprintf("uid:%v, prdcthsomod FindUserProductHistoryByType err:%v", uid, err)) + return + } + + if len(histories) > 0 { + resp.ActivityDetail.Status = 1 + if len(gameCards) > 0 { + for _, g := range gameCards { + for _, h := range histories { + if g.ID.Hex() == h.ProductID.Hex() { + g.IsPay = true + g.GameCode = append(g.GameCode, h.GameCode) + } + } + } + } + } + + resp.CardList = gameCards + + }) + wg.Wait() + + common.Go(func() { + bytes, _ := msgpack.Marshal(resp) + if err = appg.Redis.Set(redisconst.GameAdvanceKey(uid), bytes, 2*time.Minute); err != nil { + log.Warn(fmt.Sprintf("uid:%v;保存游戏预售缓存数据异常:%v", uid, err)) + } + }) + return resp, nil +} diff --git a/app/service/advance_ser/advance.go b/app/service/advance_ser/advance.go new file mode 100644 index 0000000..0eab296 --- /dev/null +++ b/app/service/advance_ser/advance.go @@ -0,0 +1,111 @@ +package advance_ser + +import ( + "91porn-server/models/commod" + "91porn-server/models/v/advanceordermod" + "91porn-server/models/v/productmod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +func GainAdvanceStatus(uid uint64) (resp advanceordermod.AdvanceStatus) { + var ( + data advanceordermod.AdvanceStatus + pId primitive.ObjectID + activityPopUp bool + gameActivityPopUp bool + ) + //str, err := appg.Redis.Get(redisconst.AdvanceKey(uid)) + //if err != nil { + // log.Warn(fmt.Sprintf("uid:%v;缓存获取预售列表信息异常:%v", uid, err)) + //} + //if str != nil { + // if err = msgpack.Unmarshal([]byte(*str), &data); err == nil { + // return data + // } + // log.Warn(fmt.Sprintf("uid:%v;解析预售缓存数据异常:%v", uid, err)) + //} + + // 1 获取活动开启状态 + var types = []commod.ProductType{productmod.AdvanceCard, productmod.GameAdvanceCard} + pr, err := productmod.FindByProductTypes(types) + if err != nil { + return data + } + + now := time.Now() + if pr != nil && len(pr) > 0 { + for _, p := range pr { + if p.ProductType == productmod.AdvanceCard && p.ActivityTime.Before(now) && p.EndTime.After(now) { + if !activityPopUp && p.IsHomePopUp { + activityPopUp = true + } + data.ActivityStatus = true + pId = p.ID + data.BalanceAmount = p.BalanceAmount / 10 + data.StartTime = p.StartTime + data.EndTime = p.EndTime + continue + } + if p.ProductType == productmod.GameAdvanceCard && p.ActivityTime.Before(now) && p.EndTime.After(now) { + if !gameActivityPopUp && p.IsHomePopUp { + gameActivityPopUp = true + } + data.GameActivityStatus = true + continue + } + } + } + data.ActivityPopUp = activityPopUp + data.GameActivityPopUp = gameActivityPopUp + + // 2 获取尾款支付状态 + advanceOrders, err := advanceordermod.IsExist(bson.M{"uid": uid, "productID": pId}) + if err != nil { + return data + } + + if advanceOrders != nil && !advanceOrders.ID.IsZero() { + if (advanceOrders.Status == advanceordermod.AdvanceSUCCESS || advanceOrders.Status == advanceordermod.BalanceProcessing) && advanceOrders.EndTime.After(now) { + data.BalancePayment = true + data.OId = advanceOrders.ID.Hex() + // 预付款相关免费权益信息 + data.PrivilegeLimit = advanceordermod.PrivilegeLimit{ + HasLimit: true, + Limit: struct { + CoinVideoLimitPerDay int64 `json:"coinVideoLimitPerDay"` + LuckyDrawLimitPerDay int64 `json:"luckyDrawLimitPerDay"` + AiUndressLimitPerDay int64 `json:"aiUndressLimitPerDay"` + DownloadLimitPerDay int64 `json:"downloadLimitPerDay"` + }{ + advanceOrders.PrepaidPrivilege.CoinVideoLimitPerDay, + advanceOrders.PrepaidPrivilege.LuckyDrawLimitPerDay, + advanceOrders.PrepaidPrivilege.AiUndressLimitPerDay, + advanceOrders.PrepaidPrivilege.DownloadLimitPerDay, + }, + Remain: struct { + TodayCoinVideoCount int64 `json:"todayCoinVideoCount"` + TodayLuckyDrawCount int64 `json:"todayLuckyDrawCount"` + TodayAiUndressCount int64 `json:"todayAiUndressCount"` + TodayDownloadCount int64 `json:"todayDownloadCount"` + }{ + advanceOrders.PrepaidPrivilege.CoinVideoLimitPerDay - advanceOrders.TodayUse.CoinVideoCount, + advanceOrders.PrepaidPrivilege.LuckyDrawLimitPerDay - advanceOrders.TodayUse.LuckyDrawCount, + advanceOrders.PrepaidPrivilege.AiUndressLimitPerDay - advanceOrders.TodayUse.AiUndressCount, + advanceOrders.PrepaidPrivilege.DownloadLimitPerDay - advanceOrders.TodayUse.DownloadCount, + }, + } + } + data.Status = advanceOrders.Status + } + + //common.Go(func() { + // bytes, _ := msgpack.Marshal(data) + // if err = appg.Redis.Set(redisconst.AdvanceKey(uid), bytes, 2*time.Minute); err != nil { + // log.Warn(fmt.Sprintf("uid:%v;保存预售缓存数据异常:%v", uid, err)) + // } + //}) + + return data +} diff --git a/app/service/ai_changeface_img_ser/ai_changeface_img.go b/app/service/ai_changeface_img_ser/ai_changeface_img.go new file mode 100644 index 0000000..ba29a3f --- /dev/null +++ b/app/service/ai_changeface_img_ser/ai_changeface_img.go @@ -0,0 +1,329 @@ +package ai_changeface_img_ser + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/ai_undress_server" + "91porn-server/app/service/aiautoser" + "91porn-server/common" + "91porn-server/common/aiService" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/middleware/ua" + "91porn-server/models/commod" + "91porn-server/models/v/advanceordermod" + "91porn-server/models/v/aichangefaceimgmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + "fmt" + "time" + + "github.com/golang-module/carbon/v2" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func List(uid uint64, req *aichangefaceimgmod.ListRequest) (interface{}, stderr.Code) { + var data = map[string]interface{}{ + "list": []interface{}{}, + "count": 0, + } + count, err := aichangefaceimgmod.CountDocument(req.Filter(uid)) + if err != nil { + return nil, stderr.ErrDbQueryError + } + if count == 0 { + return data, stderr.Success + } + opts := options.Find().SetSkip(int64(req.Skip())).SetLimit(int64(req.Limit() + 1)).SetSort(bson.M{"createdAt": -1}) + list, err := aichangefaceimgmod.QueryAllDocument(req.Filter(uid), opts) + if err != nil { + return data, stderr.ErrDbQueryError + } + hasNext := false + if len(list) > int(req.Limit()) { + list = list[:req.Limit()] + hasNext = true + } + data["count"] = count // 兼容旧版本 + data["hasNext"] = hasNext + data["list"] = list + return data, stderr.Success +} + +func Generate(uid uint64, req *aichangefaceimgmod.GenerateRequest, ua ua.UA, ip string) stderr.Code { + // 获取AI模版图 + id, err := primitive.ObjectIDFromHex(req.MId) + if err != nil { + return stderr.ErrParamError + } + mod, err := aiService.NewAiService( + aiService.AppId(int(commod.KFK_APPID)), + aiService.Url(appg.Conf.URL.AiServer), + aiService.Redis(appg.Redis), + ).GetTemplate(id) + if mod.ID.IsZero() { + return stderr.CodeEmptyData + } + + req.Coin = int64(mod.Coin) + user, err := usermod.FindUserByUID(uid) + if err != nil { + return stderr.ErrDbQueryError + } + + if user.IsVIP(time.Now()) { + req.Coin = int64(mod.VipCoin) + } + // 获取用户是否是复购 + isRepurchase, err := txnmod.CheckRepurchaseByTransTypes(uid, []txnmod.TransType{ + txnmod.AiChangeFaceImgDebitFreeTimes, + txnmod.AiChangeFaceImgDebitGold, + txnmod.AiChangeFaceImgDebitIncomeGold, + }) + if err != nil { + log.Error("txnmod.CheckRepurchaseByTransTypes fail", log.E(err)) + return stderr.ErrDbQueryError + } + privilegeFreeCount := int64(0) + picCount := int64(1) + hasPrivilege, advance := advanceordermod.GetUserValidOrder(uid) + if hasPrivilege { + // 获取可用免费次数 + validCount := advance.PrepaidPrivilege.AiUndressLimitPerDay - advance.TodayUse.AiUndressCount + if validCount >= picCount { + // 扣除免费次数 + debitPlan := advanceordermod.DebitPlan{} + debitPlan.AiUndressCount = &picCount + if err := advanceordermod.Debit(nil, uid, debitPlan); err != nil { + return stderr.ErrDbUpdateError + } + privilegeFreeCount = picCount + picCount = 0 + } else if validCount > 0 { + // 扣除免费次数 + debitPlan := advanceordermod.DebitPlan{} + debitPlan.AiUndressCount = &validCount + if err := advanceordermod.Debit(nil, uid, debitPlan); err != nil { + return stderr.ErrDbUpdateError + } + privilegeFreeCount = validCount + picCount -= validCount + } + if validCount > 0 { + l := &txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: 1, + ActualAmount: float64(-1), + TranType: txnmod.AiChangeFaceImgDebitFreeTimes.Key(), + TranTypeInt: int64(txnmod.AiChangeFaceImgDebitFreeTimes), + Desc: fmt.Sprintf("生成AI图片换脸扣除免费次数[%d次]", privilegeFreeCount), + RealAmount: decimal.Decimal{}, + SysType: user.SysType, + IsRepurchase: isRepurchase, + } + txnErr := txnmod.InsertTransactionLog(nil, l) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Generate txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + } + } + var debitFreeCount, debitAmountCoins, debitIncomeCoins int64 + + if picCount > 0 { + // 获取用户钱包 + w, err := walletmod.GetWallet(uid) + if err != nil { + return stderr.ErrDbQueryError + } + // 校验用户余额 + if w == nil { + return stderr.InsufficientBalance + } + + debitFreeCount, debitAmountCoins, debitIncomeCoins = ai_undress_server.TotalDebit(req.Coin, picCount, *w) + if debitFreeCount > 0 { + req.IsFreeTimes = true + } + // 如果收费的部分余额不够支付 + if req.Coin*(picCount-debitFreeCount) > (w.Amount + w.Income) { + return stderr.InsufficientBalance + } + } + var orderId primitive.ObjectID + var orderCreatedAt time.Time + if err = appg.VideoDB.Trans(func(tool *db.MongoTool) error { + var tl []txnmod.TransactionLog + if debitFreeCount > 0 { + expire := carbon.Tomorrow().StartOfDay().StdTime().Sub(time.Now()) + // 累加次数(跟ai脱衣共用这个次数) + appg.Redis.IncrBy(redisconst.AiFreeUndressTodayUseTimesKey(uid), debitFreeCount, expire) + // 扣除钱剩余次数 + wallet, wErr := walletmod.DebitAiFreeTimes(tool, debitFreeCount, uid) + if wErr != nil { + log.Error(fmt.Sprintf("Handle AIChangeFaceImg Generate walletmod.DebitAiFreeTimes error:%+v:", wErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: 1, + ActualAmount: float64(-1), + TranType: txnmod.AiChangeFaceImgDebitFreeTimes.Key(), + TranTypeInt: int64(txnmod.AiChangeFaceImgDebitFreeTimes), + Desc: fmt.Sprintf("生成AI图片换脸扣除免费次数[%d次]", debitFreeCount), + RealAmount: walletmod.GetRealAmount(wallet), + SysType: user.SysType, + IsRepurchase: isRepurchase, + }) + } + + if debitIncomeCoins > 0 || debitAmountCoins > 0 { + // 扣除钱包余额 + wallet, wErr := walletmod.DebitAmountAndIncome(tool, debitAmountCoins, debitIncomeCoins, uid) + if wErr != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Generate walletmod.DebitAmount error:%+v:", wErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + + if debitAmountCoins > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -debitAmountCoins, + ActualAmount: float64(-debitAmountCoins), + TranType: txnmod.AiChangeFaceImgDebitGold.Key(), + TranTypeInt: int64(txnmod.AiChangeFaceImgDebitGold), + Desc: fmt.Sprintf("生成AI图片换脸金币-%d", debitAmountCoins), + RealAmount: walletmod.GetRealAmount(wallet), + SysType: user.SysType, + IsRepurchase: isRepurchase, + }) + } + if debitIncomeCoins > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -debitIncomeCoins, + ActualAmount: float64(-debitIncomeCoins), + TranType: txnmod.AiChangeFaceImgDebitIncomeGold.Key(), + TranTypeInt: int64(txnmod.AiChangeFaceImgDebitIncomeGold), + Desc: fmt.Sprintf("生成AI图片换脸收益金币-%d", debitIncomeCoins), + RealAmount: walletmod.GetRealAmount(wallet), + SysType: user.SysType, + IsRepurchase: isRepurchase, + }) + } + } + + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(tool, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Generate txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + } + + // 新增AI脱衣记录 + orderId = primitive.NewObjectID() + orderCreatedAt = time.Now() + if err = aichangefaceimgmod.InsertOne(tool, req.Generate(orderId, orderCreatedAt, uid, mod.Title, mod.Cover, privilegeFreeCount, debitFreeCount, debitAmountCoins, debitIncomeCoins)); err != nil { + log.Error(fmt.Sprintf("Handle Aichangefaceimg Generate InsertOnes error:%+v:", err), log.Any("uid", uid)) + return stderr.ErrDbInsertError + } + + return nil + }); err != nil { + log.Error(fmt.Sprintf("Handle Aichangefaceimg Generate Trans error:%+v;uid:%v;", err, uid)) + return stderr.ErrDbUpdateError + } + common.Go(func() { + if err := aiautoser.SubmitImage(orderId); err != nil { + log.Error("AI图片换脸自动处理失败", log.Any("id", orderId.Hex()), log.E(err)) + } + }) + + return stderr.Success +} + +func AiChangeFaceImgHide(uid uint64, req *aichangefaceimgmod.DelRequest) stderr.Code { + acf, err := aichangefaceimgmod.FindByID(nil, req.ID) + if err != nil { + return stderr.ErrDbQueryError + } + if acf.ID.IsZero() { + return stderr.CodeEmptyData + } + if acf.UID != uid { + return stderr.Failure + } + if acf.Status == aichangefaceimgmod.Processing || acf.Status == aichangefaceimgmod.StatusSubmit { + return stderr.AiGenningDelForbidden + } + if err = aichangefaceimgmod.Edit(nil, req.Filter(), req.Update()); err != nil { + return stderr.ErrDbQueryError + } + return stderr.Success +} + +func TotalDebit(price int64, count int64, w walletmod.Wallet) (fCount, amountCoins, incomeCoins int64) { + var ( + debitFreeTimes int64 + debitAmountCoins int64 + debitIncomeCoins int64 + ) + if w.AiUndressFreeTimes > 0 && w.AiUndressFreeTimes >= count { + debitFreeTimes = count + } + + if w.AiUndressFreeTimes > 0 && w.AiUndressFreeTimes < count { + debitFreeTimes = w.AiUndressFreeTimes + debitCoins := (count - debitFreeTimes) * price + if w.Amount >= debitCoins { + debitAmountCoins = debitCoins + } + if w.Amount < debitCoins { + debitAmountCoins = w.Amount + debitIncomeCoins = debitCoins - w.Amount + } + } + + if w.AiUndressFreeTimes <= 0 { + debitCoins := price * count + if w.Amount >= debitCoins { + debitAmountCoins = debitCoins + } + if w.Amount < debitCoins { + debitAmountCoins = w.Amount + debitIncomeCoins = debitCoins - w.Amount + } + } + return debitFreeTimes, debitAmountCoins, debitIncomeCoins +} + +func CheckBalance(price int64, count int64, w walletmod.Wallet) bool { + var checkStats bool + var totalCoins = w.Amount + if w.Income != 0 { + totalCoins += w.Income + } + if w.AiUndressFreeTimes > 0 && w.AiUndressFreeTimes >= count { + checkStats = true + } + + if w.AiUndressFreeTimes > 0 && w.AiUndressFreeTimes < count && totalCoins >= price*(count-w.AiUndressFreeTimes) { + checkStats = true + } + + if w.AiUndressFreeTimes <= 0 && totalCoins > price*count { + checkStats = true + } + return checkStats +} diff --git a/app/service/ai_changeface_ser/ai_changeface.go b/app/service/ai_changeface_ser/ai_changeface.go new file mode 100644 index 0000000..6e43e87 --- /dev/null +++ b/app/service/ai_changeface_ser/ai_changeface.go @@ -0,0 +1,384 @@ +package ai_changeface_ser + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/aiautoser" + "91porn-server/common" + "91porn-server/common/aiService" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/middleware/ua" + "91porn-server/models/commod" + "91porn-server/models/v/aichangefacemod" + "91porn-server/models/v/aichangefacevidmod" + "91porn-server/models/v/backpackmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + "encoding/json" + "errors" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func List(uid uint64, status *aichangefacemod.AiChangeFaceStatus, skip, limit int) ([]aichangefacemod.AiChangeFace, bool, error) { + acfs, err := aichangefacemod.ListByUid(uid, status, skip, limit+1) + if err != nil { + return nil, false, err + } + hasNext := false + if len(acfs) > limit { + acfs = acfs[:limit] + hasNext = true + } + return acfs, hasNext, nil +} + +func Generate(uid uint64, pics []string, vidModId primitive.ObjectID, discount []primitive.ObjectID, shareTitle string, shareStatus int, ua ua.UA, ip string) stderr.Code { + vidMod, err := aiService.NewAiService( + aiService.AppId(int(commod.KFK_APPID)), + aiService.Url(appg.Conf.URL.AiServer), + aiService.Redis(appg.Redis), + ).GetTemplate(vidModId) + if vidMod.ID.IsZero() { + return stderr.Failure + } + var discountValue int64 + now := time.Now() + var dsId []primitive.ObjectID + for _, d := range discount { + dsId = append(dsId, d) + bp, err := backpackmod.GetByID(nil, d) + if err != nil { + return stderr.ErrDbQueryError + } + if bp == nil { // 抵扣券不存在 + return stderr.Failure + } + if bp.GoodsType != backpackmod.AiChangeFaceDiscount { // 抵扣券类型不对 + return stderr.Failure + } + if bp.Status == backpackmod.Used { // 抵扣券已被使用 + return stderr.Failure + } + if bp.ExpiredTime.Before(now) { // 抵扣券已过期 + return stderr.Failure + } + discountValue += bp.GoodsValue + } + originPrice := vidMod.Coin + + user, err := usermod.FindUserByUID(uid) + if err != nil { + return stderr.ErrDbQueryError + } + + if user.IsVIP(time.Now()) { + originPrice = vidMod.VipCoin + } + + price := int64(originPrice) - discountValue + if price < 0 { + price = 0 + } + + // 获取钱包信息 + wlt, err := walletmod.GetWallet(uid) + if err != nil { + return stderr.ErrDbQueryError + } + + if wlt == nil || wlt.Amount+wlt.Income < price { + return stderr.InsufficientBalance + } + + // 扣除的普通金币和收益金币 + debitAmt, debitIncome := TotalDebit(price, wlt) + // 获取用户是否是复购 + isRepurchase, err := txnmod.CheckRepurchaseByTransTypes(uid, []txnmod.TransType{ + txnmod.AiChangefaceDebitGold, + txnmod.AiChangefaceDebitInComeGold, + }) + if err != nil { + log.Error("txnmod.CheckRepurchaseByTransTypes fail", log.E(err)) + return stderr.ErrDbQueryError + } + var orderId primitive.ObjectID + var orderCreatedAt time.Time + if err = appg.VideoDB.Trans(func(tool *db.MongoTool) error { + var tl []txnmod.TransactionLog + if debitAmt > 0 || debitIncome > 0 { + // 扣除钱包余额 + wallet, err := walletmod.DebitAmountAndIncome(tool, debitAmt, debitIncome, uid) + if err != nil { + log.Error(fmt.Sprintf("Handle AiChangeface Generate walletmod.DebitAmount error:%+v:", err), log.Any("uid", uid)) + if err.Error() == "no enough balance" { + return stderr.InsufficientBalance + } + return stderr.ErrDbUpdateError + } + + if debitAmt > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -debitAmt, + ActualAmount: -float64(debitAmt), + TranType: txnmod.AiChangefaceDebitGold.Key(), + TranTypeInt: int64(txnmod.AiChangefaceDebitGold), + Desc: fmt.Sprintf("生成AI换脸金币-%d", debitAmt), + RealAmount: walletmod.GetRealAmount(wallet), + SysType: user.SysType, + IsRepurchase: isRepurchase, + }) + } + + if debitIncome > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -debitIncome, + ActualAmount: -float64(debitIncome), + TranType: txnmod.AiChangefaceDebitGold.Key(), + TranTypeInt: int64(txnmod.AiChangefaceDebitGold), + Desc: fmt.Sprintf("生成AI换脸收益金币-%d", debitIncome), + RealAmount: walletmod.GetRealAmount(wallet), + SysType: user.SysType, + IsRepurchase: isRepurchase, + }) + } + } + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(tool, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle AiChangeface Generate txnmod.InsertManyTransactionLog error:%+v:", txnErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + } + + // 抵扣券使用 + if len(dsId) > 0 { + modified, err := backpackmod.UseManyGoods(tool, dsId) + if err != nil { + return stderr.ErrDbUpdateError + } + if modified != int64(len(dsId)) { + return stderr.Failure + } + } + + // 新增AI脱衣记录 + orderId = primitive.NewObjectID() + orderCreatedAt = time.Now() + if err = aichangefacemod.AddAiChangeFace(tool, orderId, orderCreatedAt, uid, pics, vidMod, debitAmt, debitIncome, discount, shareTitle, shareStatus); err != nil { + log.Error(fmt.Sprintf("Handle AiChangeface Generate InsertOnes error:%+v:", err), log.Any("uid", uid)) + return stderr.ErrDbInsertError + } + + return nil + }); err != nil { + if e, ok := err.(stderr.Code); ok { + return e + } + return stderr.ErrDbQueryError + } + common.Go(func() { + if err := aiautoser.SubmitVideo(orderId); err != nil { + log.Error("AI视频换脸自动处理失败", log.Any("id", orderId.Hex()), log.E(err)) + } + }) + + return stderr.Success +} + +func TotalDebit(coins int64, w *walletmod.Wallet) (amtDebit, income int64) { + var ( + deAmt int64 + deIncome int64 + ) + if w.Amount >= coins { + deAmt = coins + } + if w.Amount < coins && w.Amount+w.Income >= coins { + deAmt = w.Amount + deIncome = coins - w.Amount + } + return deAmt, deIncome +} + +// ModList 获取模版列表 +func ModList(uid uint64) (data *aichangefacevidmod.AppResponse, err error) { + var ret aichangefacevidmod.AppResponse + key := redisconst.AIModCache + str, err := appg.Redis.Get(key) + if err != nil { + log.Warn(fmt.Sprintf("用户ID:%d;缓存获取AI模版列表信息异常:%v", uid, err)) + } + + if str != nil { + if err = json.Unmarshal([]byte(*str), &ret); err == nil { + return &ret, nil + } + log.Warn(fmt.Sprintf("用户ID:%d;解析缓存数据异常:%v", uid, err)) + } + + mods, err := aichangefacevidmod.GetAllMods() + if err != nil { + return &ret, err + } + + if len(mods) > 0 { + for _, m := range mods { + if m.ModuleType == aichangefacevidmod.AIUndress { + ret.AiUndressMod = append(ret.AiUndressMod, aichangefacevidmod.AiUndressMod{ + ID: m.ID, + Cover: m.Cover, + }) + } + if m.ModuleType == aichangefacevidmod.AIImgToVideo { + ret.AiImgToVideoMod = append(ret.AiImgToVideoMod, aichangefacevidmod.AiImgToVideoMod{ + ID: m.ID, + Title: m.Title, + SceneType: m.SceneType, + Cover: m.Cover, + NewUrl: m.NewUrl, + }) + } + if m.ModuleType == aichangefacevidmod.AITextToImage { + ret.AiTextToImgMod = append(ret.AiTextToImgMod, aichangefacevidmod.AiTextToImgMod{ + ID: m.ID, + Cover: m.Cover, + Title: m.Title, + StyleType: m.HotValue, + }) + } + //if m.ModuleType == aichangefacevidmod.AIVideoChangeFace { + // ret.AiChangeFaceVideoMod = append(ret.AiChangeFaceVideoMod, aichangefacevidmod.AiChangeMod{ + // ID: m.ID, + // Title: m.Title, + // SourceURL: m.SourceURL, + // Cover: m.Cover, + // PlayTime: m.PlayTime, + // Type: m.Type, + // HotMark: m.HotMark, + // HotValue: m.HotValue, + // Coin: m.Coin, + // VipCoin: m.VipCoin, + // }) + //} + //if m.ModuleType == aichangefacevidmod.AIImgChangeFace { + // ret.AiChangeFaceMod = append(ret.AiChangeFaceMod, aichangefacevidmod.AiChangeFaceMod{ + // Title: m.Title, + // ID: m.ID, + // Cover: m.Cover, + // HotMark: m.HotMark, + // HotValue: m.HotValue, + // Coin: m.Coin, + // VipCoin: m.VipCoin, + // }) + //} + } + } + common.Go(func() { + d, err := json.Marshal(ret) + if err != nil { + return + } + if err = appg.Redis.Set(key, d, 5*time.Minute); err != nil { + log.Warn(fmt.Sprintf("用户ID:%d;保存缓存数据异常:%v", uid, err)) + } + }) + return &ret, nil +} + +type ModListV2Req struct { + Type int `json:"type" form:"type"` // 0-图片换脸 1-视频换脸 + CategoryId string `json:"categoryId" form:"categoryId"` // 模版分类id,如果为空,则默认第一个模版分类 +} + +type ModListV2Resp struct { + CategoryList []*aiService.Category `json:"categoryList"` + TemplateList []*aiService.Template `json:"templateList"` + CategoryId string `json:"categoryId"` // 当前返回的分类id下的模版 +} + +// ModListV2 获取模版列表 +func ModListV2(req ModListV2Req) (resp ModListV2Resp, err error) { + ai := aiService.NewAiService( + aiService.AppId(int(commod.KFK_APPID)), + aiService.Url(appg.Conf.URL.AiServer), + aiService.Redis(appg.Redis), + ) + resp.CategoryList, resp.CategoryId, resp.TemplateList, err = ai.GetTemplateList(req.Type, req.CategoryId) + if err != nil { + return + } + + return resp, nil +} + +type ModInfoReq struct { + Id string `json:"id" form:"id"` + Type int `json:"type" form:"type"` // 1-ai图片换脸 2-ai视频换脸 3-ai脱衣 4-ai图生视频 5-ai绘画 +} + +type ModInfoResp struct { + AiUndressMod aichangefacevidmod.AiUndressMod `json:"aiUndressMod" bson:"aiUndressMod"` // AI脱衣模版 + AiTextToImgMod aichangefacevidmod.AiTextToImgMod `json:"aiTextToImgMod" bson:"aiTextToImgMod"` // AI绘图模型 + AiImgToVideoMod aichangefacevidmod.AiImgToVideoMod `json:"aiImgToVideoMod" bson:"aiImgToVideoMod"` // AI图生视频模型 + AiChangeFaceMod aiService.Template `json:"aiChangeFaceMod" bson:"aiChangeFaceMod"` // Ai视频/图片换脸模型 +} + +func (p *ModInfoReq) GetInfo() (resp ModInfoResp, err error) { + id, _ := primitive.ObjectIDFromHex(p.Id) + if id.IsZero() { + return resp, errors.New("id无效") + } + if p.Type > 2 { + // 从本地的表里获取 + m, err := aichangefacevidmod.GetModById(id) + if err != nil { + return resp, err + } + if m.ModuleType == aichangefacevidmod.AIUndress { + resp.AiUndressMod = aichangefacevidmod.AiUndressMod{ + ID: m.ID, + Cover: m.Cover, + } + } + if m.ModuleType == aichangefacevidmod.AIImgToVideo { + resp.AiImgToVideoMod = aichangefacevidmod.AiImgToVideoMod{ + ID: m.ID, + Cover: m.Cover, + NewUrl: m.NewUrl, + } + } + if m.ModuleType == aichangefacevidmod.AITextToImage { + resp.AiTextToImgMod = aichangefacevidmod.AiTextToImgMod{ + ID: m.ID, + Cover: m.Cover, + Title: m.Title, + StyleType: m.HotValue, + } + } + return resp, nil + } + + // 获取视频/图片换脸模版 + ai := aiService.NewAiService( + aiService.AppId(int(commod.KFK_APPID)), + aiService.Url(appg.Conf.URL.AiServer), + aiService.Redis(appg.Redis), + ) + template, err := ai.GetTemplate(id) + if err != nil { + log.Error("ai.GetTemplate fail", log.Any("id", id), log.E(err)) + return + } + resp.AiChangeFaceMod = template + return +} diff --git a/app/service/ai_image_to_video_ser/ai_image_to_video.go b/app/service/ai_image_to_video_ser/ai_image_to_video.go new file mode 100755 index 0000000..4d66bb1 --- /dev/null +++ b/app/service/ai_image_to_video_ser/ai_image_to_video.go @@ -0,0 +1,359 @@ +package ai_image_to_video_ser + +import ( + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/middleware/ua" + "91porn-server/models/cache/aiimagetovideodata" + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/commod" + "91porn-server/models/v/aichangefacevidmod" + "91porn-server/models/v/aiimagetovideomod" + "91porn-server/models/v/sysconfmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AppQueryListReq struct { + Status int `json:"status" form:"status"` // 状态 1、排队中 2、生成成功 3、生成失败 + UID uint64 `json:"-"` // 用户ID + commod.Page +} + +func (p *AppQueryListReq) Filter() primitive.M { + filter := bson.M{} + switch p.Status { + case 1: + filter["status"] = bson.M{"$in": []aiimagetovideomod.AiImageToVideoStatus{aiimagetovideomod.StatusOrderSuccess, aiimagetovideomod.StatusSubmitted}} + case 2: + filter["status"] = aiimagetovideomod.StatusGenerationSuccess + case 3: + filter["status"] = bson.M{"$in": []aiimagetovideomod.AiImageToVideoStatus{aiimagetovideomod.StatusGenerationFailed, aiimagetovideomod.StatusRefunded}} + } + filter["uid"] = p.UID + filter["isHide"] = false + return filter +} + +type AppListRes struct { + HasNext bool `json:"hasNext"` // 是否有下一页 + List []*aiimagetovideomod.AiImageToVideoInfo `json:"list"` // 列表 +} + +// GetList 获取列表 +func (p *AppQueryListReq) GetList() (AppListRes, error) { + var res AppListRes + var err error + + sort := bson.D{{"createdAt", -1}} + + // 获取列表 + var data []aiimagetovideomod.AiImageToVideo + data, _, res.HasNext, err = aiimagetovideomod.GetList(p.Filter(), int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + log.Error("获取AI图生视频列表列表数据错误", log.Any("Params", *p), log.E(err)) + return res, err + } + res.List = aiimagetovideodata.FormatAppDataList(data) + return res, nil +} + +type GenerateReq struct { + MID string `json:"mid" bson:"mid"` // 模版ID + OriginPic string `json:"originPic" bson:"originPic"` // 脱衣原图 + Coin int64 `json:"-"` // 此次脱衣金币个数 + UID uint64 `json:"-"` // 用户ID + ShareTitle string `json:"shareTitle" bson:"shareTitle"` // 分享标题 + ShareStatus int `json:"shareStatus" bson:"shareStatus"` // 是否分享 0-不分享 1-分享 + +} + +func (p *GenerateReq) GenerateInfo(orderId primitive.ObjectID, orderCreatedAt time.Time, debitCoins, debitIncomeCoins int64, sceneType int) aiimagetovideomod.AiImageToVideo { + return aiimagetovideomod.AiImageToVideo{ + ID: orderId, + SceneType: sceneType, + Coin: debitIncomeCoins + debitCoins, + DebitAmountCoin: debitCoins, + DebitIncomeCoin: debitIncomeCoins, + IsFreeTimes: false, + ImgUrl: p.OriginPic, + UID: p.UID, + Status: int(aiimagetovideomod.StatusOrderSuccess), + IsHide: false, + ShareTitle: p.ShareTitle, + ShareStatus: p.ShareStatus, + UpdatedAt: orderCreatedAt, + CreatedAt: orderCreatedAt, + } +} + +// Generate 生成订单 +func (p *GenerateReq) Generate(ua ua.UA, ip string) (stderr.Code, error) { + // 默认脱衣 + sceneType := int(aiimagetovideomod.SceneTagLiftShirt) + + if p.MID != "" { + // 获取模版信息 + id, _ := primitive.ObjectIDFromHex(p.MID) + mod, err := aichangefacevidmod.GetModById(id) + if err != nil { + return stderr.ErrDbQueryError, err + } + if mod.ID.IsZero() { + return stderr.CodeEmptyData, errors.New("mod is null") + } + sceneType = mod.SceneType + } + + // 获取用户信息 + user, err := usermod.FindUserByUID(p.UID) + if err != nil { + return stderr.ErrDbQueryError, err + } + if user.ID.IsZero() { + return stderr.UserIsNotExists, errors.New("user is null") + } + configure, _ := sysconfdata.GetAllFromCache() + p.Coin = configure.GetInt(sysconfmod.VCodeAiImageToVideoPrice) + var debitAmountCoins, debitIncomeCoins int64 + + if p.Coin > 0 { + // 获取用户钱包 + w, err := walletmod.GetWallet(p.UID) + if err != nil { + return stderr.ErrDbQueryError, err + } + if w == nil { + return stderr.InsufficientBalance, errors.New("Insufficient balance") + } + + // 如果收费的部分余额不够支付 + if p.Coin > (w.Amount + w.Income) { + return stderr.InsufficientBalance, errors.New("Insufficient balance") + } + + debitAmountCoins, debitIncomeCoins = TotalDebit(p.Coin, *w) + } + // 获取用户是否是复购 + isRepurchase, err := txnmod.CheckRepurchaseByTransTypes(p.UID, []txnmod.TransType{ + txnmod.AiImageToVideoDebitGold, + txnmod.AiImageToVideoDebitInComeGold, + }) + if err != nil { + log.Error("txnmod.CheckRepurchaseByTransTypes fail", log.E(err)) + return stderr.ErrDbQueryError, err + } + var orderId primitive.ObjectID + var orderCreatedAt time.Time + if err = appg.VideoDB.Trans(func(tool *db.MongoTool) error { + var tl []txnmod.TransactionLog + if debitIncomeCoins > 0 || debitAmountCoins > 0 { + // 扣除钱包余额 + wallet, wErr := walletmod.DebitAmountAndIncome(tool, debitAmountCoins, debitIncomeCoins, p.UID) + if wErr != nil { + log.Error(fmt.Sprintf("Handle imagetovideo Generate walletmod.DebitAmount error:%+v:", wErr), log.Any("uid", p.UID)) + return stderr.ErrDbQueryError + } + + if debitAmountCoins > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: p.UID, + Amount: -debitAmountCoins, + ActualAmount: float64(-debitAmountCoins), + TranType: txnmod.AiImageToVideoDebitGold.Key(), + TranTypeInt: int64(txnmod.AiImageToVideoDebitGold), + Desc: fmt.Sprintf("生成AI图生视频金币-%d", debitAmountCoins), + RealAmount: walletmod.GetRealAmount(wallet), + SysType: user.SysType, + IsRepurchase: isRepurchase, + }) + } + if debitIncomeCoins > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: p.UID, + Amount: -debitIncomeCoins, + ActualAmount: float64(-debitIncomeCoins), + TranType: txnmod.AiImageToVideoDebitInComeGold.Key(), + TranTypeInt: int64(txnmod.AiImageToVideoDebitInComeGold), + Desc: fmt.Sprintf("生成AI图生视频收益金币-%d", debitIncomeCoins), + RealAmount: walletmod.GetRealAmount(wallet), + SysType: user.SysType, + IsRepurchase: isRepurchase, + }) + } + } + + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(tool, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle imagetovideo Generate txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", p.UID)) + return stderr.ErrDbQueryError + } + } + + // 新增AI图生视频记录 + orderId = primitive.NewObjectID() + orderCreatedAt = time.Now() + order := p.GenerateInfo(orderId, orderCreatedAt, debitAmountCoins, debitIncomeCoins, sceneType) + if _, err = aiimagetovideomod.Insert(tool, order); err != nil { + log.Error(fmt.Sprintf("Handle imagetovideo Generate InsertOnes error:%+v:", err), log.Any("uid", p.UID)) + return stderr.ErrDbInsertError + } + return nil + }); err != nil { + log.Error(fmt.Sprintf("Handle imagetovideo Generate Trans error:%+v;uid:%v;", err, p.UID)) + return stderr.ErrDbUpdateError, err + } + + common.Go(func() { + autoUploadOrder(orderId) + }) + return stderr.Success, nil +} + +type AutoUploadReq struct { + AppID int `json:"appId"` // 应用 ID + UID uint64 `json:"uid"` // 用户 ID + ImgURL string `json:"imgUrl"` // 图片 URL + AppOrderNum string `json:"appOrderNum"` // 应用订单号 + NotifyURL string `json:"notifyUrl"` // 通知 URL + SceneType int `json:"sceneType" bson:"sceneType"` // 模版场景类型 +} + +func autoUploadOrder(id primitive.ObjectID) error { + item, err := aiimagetovideomod.GetInfo(id) + if err != nil { + return err + } + + if item.ID.IsZero() { + return errors.New(" aiimagetovideo order is null") + } + + // 将请求参数编码为JSON + requestBody := AutoUploadReq{ + AppID: int(commod.KFK_APPID), + UID: item.UID, + ImgURL: item.ImgUrl, + AppOrderNum: item.ID.Hex(), + SceneType: item.SceneType, + NotifyURL: fmt.Sprintf("%v/api/web/admin/ai/image_to_video/callback", appg.Conf.URL.AiImageToVideoCallbackUrl), + } + + jsonData, err := json.Marshal(requestBody) + if err != nil { + return err + } + + apiURL := getAutoUploadUrl() + + // 创建请求对象 + req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("创建请求失败: %v", err) + } + + // 通过http.Client发送请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + // 读取响应内容 + respBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("读取响应内容失败: %v", err) + } + + // 如果返回状态码非200,可以考虑返回错误 + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("请求失败,状态码:%d,内容:%s", resp.StatusCode, string(respBytes)) + } + + // 更新订单 + updateCond := bson.M{ + "status": aiimagetovideomod.StatusSubmitted, + "updatedAt": time.Now(), + } + _, err = aiimagetovideomod.UpdateByID(nil, id, updateCond) + if err != nil { + return err + } + return nil +} + +func getAutoUploadUrl() string { + return fmt.Sprintf("%v/api/comfyui/prd/create_job", appg.Conf.URL.AiImageToVideoUrl) +} + +func TotalDebit(price int64, w walletmod.Wallet) (amountCoins, incomeCoins int64) { + var ( + debitAmountCoins int64 + debitIncomeCoins int64 + ) + + // 计算当日实际扣减的金额 + debitCoins := price + if w.Amount >= debitCoins { + debitAmountCoins = debitCoins + } else { + debitAmountCoins = w.Amount + debitIncomeCoins = debitCoins - w.Amount + } + return debitAmountCoins, debitIncomeCoins +} + +type HideReq struct { + ID string `json:"id"` // AI订单ID + UID uint64 `json:"-"` // 用户ID +} + +// Hide 删除订单 +func (p *HideReq) Hide() error { + oid, err := primitive.ObjectIDFromHex(p.ID) + if err != nil { + return err + } + + item, err := aiimagetovideomod.GetInfo(oid) + if err != nil { + log.Error("获取AI图生视频列表详情数据错误", log.Any("ID", p.ID), log.E(err)) + return err + } + + if item.ID.IsZero() { + return errors.New("AI订单不存在") + } + + if item.UID != p.UID { + return errors.New("只能删除自己的订单") + } + + if item.Status == int(aiimagetovideomod.StatusSubmitted) { + return errors.New("不能删除排队中的订单") + } + + if err = aiimagetovideomod.Hide(p.UID, oid); err != nil { + return err + } + + return nil +} diff --git a/app/service/ai_mate_ser/ai_mate.go b/app/service/ai_mate_ser/ai_mate.go new file mode 100644 index 0000000..e77c1ec --- /dev/null +++ b/app/service/ai_mate_ser/ai_mate.go @@ -0,0 +1,383 @@ +package ai_mate_ser + +import ( + "91porn-server/app/appg" + "91porn-server/common" + aimate "91porn-server/common/aiMate" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/common/stderr" + "91porn-server/middleware/ua" + "91porn-server/models/commod" + "91porn-server/models/v/currencymod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + "errors" + "fmt" + "time" + + "github.com/go-redsync/redsync/v4" + "go.mongodb.org/mongo-driver/bson" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func GetApiUrl() string { + var apiUrl string + if appg.Conf.URL.AIMateH5 != "" { + if appg.Conf.Base.Env == "prod" { + apiUrl = fmt.Sprintf("%s/?aId=%d", appg.Conf.URL.AIMateH5, commod.KFK_APPID) + } else { + apiUrl = fmt.Sprintf("%s/?aId=%d&devTest=1", appg.Conf.URL.AIMateH5, commod.KFK_APPID) + } + } + return apiUrl +} + +type LoginResp struct { + URL string `json:"url"` +} + +type loginDependencies struct { + findUser func(uint64) (*usermod.User, error) + updateUser func(uint64, usermod.UserSelector) (*usermod.User, error) + getWallet func(uint64) (*walletmod.Wallet, error) + getRemoteBalance func(uint64, string) (float64, error) + getAccessURL func(uint64, string, string, float64) (string, error) + newUUID func() string +} + +func defaultLoginDependencies() loginDependencies { + return loginDependencies{ + findUser: usermod.FindUserByUID, + updateUser: usermod.Update, + getWallet: walletmod.GetWallet, + getRemoteBalance: aimate.GetUserAiBalance, + getAccessURL: aimate.GetUserAccessAiUrl, + newUUID: common.UUID, + } +} + +// Login 获取当前用户的 AI 女友第三方登录地址。 +func Login(uid uint64) (LoginResp, error) { + if appg.Redis == nil { + return LoginResp{}, errors.New("redis is unavailable") + } + lock := redis.BuildLock( + appg.Redis, + redisconst.GetAiMateLoginLockKey(uid), + redsync.WithExpiry(30*time.Second), + redsync.WithTries(1), + ) + if err := lock.Lock(); err != nil { + return LoginResp{}, fmt.Errorf("acquire ai mate login lock: %w", err) + } + defer func() { + if _, err := lock.Unlock(); err != nil { + log.Warn("ai_mate_ser Login unlock failed", log.Any("uid", uid), log.E(err)) + } + }() + return login(uid, defaultLoginDependencies()) +} + +func login(uid uint64, deps loginDependencies) (LoginResp, error) { + user, err := deps.findUser(uid) + if err != nil { + return LoginResp{}, err + } + if user == nil || user.ID.IsZero() { + return LoginResp{}, errors.New("ai mate login user not found") + } + wallet, err := deps.getWallet(uid) + if err != nil { + return LoginResp{}, err + } + + localBalance := float64(0) + if wallet != nil { + localBalance = wallet.AiMateBalance + } + aiMateUID := user.AiMateUid + remoteBalance := float64(0) + if aiMateUID == "" { + aiMateUID = deps.newUUID() + if aiMateUID == "" { + return LoginResp{}, errors.New("generate ai mate uid failed") + } + if _, err = deps.updateUser(uid, usermod.UserSelector{AiMateUid: &aiMateUID}); err != nil { + return LoginResp{}, err + } + } else { + remoteBalance, err = deps.getRemoteBalance(uid, aiMateUID) + if err != nil { + return LoginResp{}, err + } + } + + url, err := deps.getAccessURL(uid, aiMateUID, user.Name, balanceTopUp(localBalance, remoteBalance)) + if err != nil { + return LoginResp{}, err + } + if url == "" { + return LoginResp{}, errors.New("ai mate login url is empty") + } + return LoginResp{URL: url}, nil +} + +func balanceTopUp(localBalance, remoteBalance float64) float64 { + if localBalance <= remoteBalance || localBalance <= 0 { + return 0 + } + return localBalance - remoteBalance +} + +// GetCurrencyList 获取AI伴侣币列表 +func GetCurrencyList() (code stderr.Code, data interface{}) { + code = stderr.Success + cs := make([]currencymod.CurrencyApp, 0) + //查询 + currencys, err := currencymod.List(commod.AiMateCoin) + if err != nil { + log.Error("ai_mate_ser GetCurrencyList currencymod.List err", log.E(err)) + code = stderr.ErrDbQueryError + return + } + if len(currencys) > 0 { + for _, c := range currencys { + cs = append(cs, currencymod.CurrencyApp{ + ID: c.ID, + Name: c.Name, + Type: c.Type, + Coins: c.Coins, + Price: c.Price, + CouponDesc: c.CouponDesc, + }) + } + } + data = cs + return +} + +// ExchangeReq 兑换请求参数 +type ExchangeReq struct { + Id primitive.ObjectID `json:"id" binding:"required"` +} + +// Exchange 获取AI伴侣币兑换 +func Exchange(uid uint64, req ExchangeReq, ua ua.UA, ip string) (code stderr.Code) { + code = stderr.Success + //查询用户信息 + user, err := usermod.FindUserByUID(uid) + if err != nil { + log.Error("ai_mate_ser Exchange usermod.FindUserByUID err", log.Any("uid", uid), log.E(err)) + code = stderr.ErrDbQueryError + return + } + if user == nil || user.ID.IsZero() { + log.Error("ai_mate_ser Exchange user not exist", log.Any("uid", uid)) + code = stderr.UserIsNotExists + return + } + //查询货币是否存在 + currency, err := currencymod.Get(req.Id) + if err != nil { + log.Error("ai_mate_ser Exchange currencymod.List err", log.E(err)) + code = stderr.ErrDbQueryError + return + } + if currency.ID.IsZero() { + log.Error("ai_mate_ser Exchange currency not exist", log.Any("id", req.Id.Hex())) + code = stderr.CodeEmptyData + return + } + if !currency.IsActive { + log.Error("ai_mate_ser Exchange currency isActive is false", log.Any("id", req.Id.Hex())) + code = stderr.ErrDataInvalid + return + } + if currency.Type != commod.AiMateCoin { + log.Error("ai_mate_ser Exchange currency type not commod.AiMateCoin", log.Any("id", req.Id.Hex())) + code = stderr.ExchangeCodeInvalid + return + } + //查询用户钱包 + wallet, err := walletmod.GetWallet(uid) + if err != nil { + code = stderr.ErrDbQueryError + log.Error("ai_mate_ser Exchange walletmod.GetWallet err:", log.E(err), log.Any("uid", uid)) + return + } + //需要支付的金额 + price := currency.Price + if wallet == nil || wallet.Amount < price { + code = stderr.InsufficientBalance + log.Error("ai_mate_ser Exchange user Insufficient balance", log.Any("uid", uid), log.Any("id", currency.ID.Hex())) + return + } + + //兑换业务 + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + //扣除金币余额,增加AI伴侣余额 + cAmount := -price + cAiMateBalance := float64(currency.Coins) + p := walletmod.CreditPlan{ + Amount: &cAmount, + AiMateBalance: &cAiMateBalance, + } + wallet, err := walletmod.Credit(t, p, uid) + if err != nil { //扣钱 + return err + } + + //新增交易日志 + txnLog := &txnmod.TransactionLog{ + ID: primitive.NewObjectID(), + UID: uid, + Amount: -price, + ActualAmount: float64(-price), + TranType: txnmod.AiMateCurrencyExchange.Key(), + TranTypeInt: int64(txnmod.AiMateCurrencyExchange), + TransNo: currency.ID, + Desc: "AI伴侣币购买-" + currency.Name, + DiscDoc: user.DiscDoc, + SysType: user.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + AiMatePoint: cAiMateBalance, + RealAiMatePoint: wallet.AiMateBalance, + CreatedAt: time.Now(), + } + if err = txnmod.InsertTransactionLog(t, txnLog); err != nil { + return err + } + + return nil + }); err != nil { + log.Warn("ai_mate_ser Exchange Transaction fail", log.Any("uid", uid), log.Any("id", currency.ID.Hex()), log.E(err)) + code = stderr.BuyFailed + return + } + return +} + +type BalanceResp struct { + Balance float64 `json:"balance" bson:"balance"` // 余额 + LastOrderId string `json:"lastOrderId" bson:"lastOrderId"` +} + +// GetNewBalance 获取AI伴侣余余额 +func GetNewBalance(uid uint64) (BalanceResp, error) { + var ret BalanceResp + + // 查询用户信息 + user, err := usermod.FindUserByUID(uid) + if err != nil { + log.Error("ai_mate_ser GetBalance usermod.FindUserByUID err:", log.E(err), log.Any("uid", uid)) + return ret, err + } + if user == nil || user.ID.IsZero() { + log.Error("ai_mate_ser GetBalance user not exist", log.Any("uid", uid)) + return ret, errors.New("ai_mate_ser GetBalance user not exist") + + } + + // 查询用户钱包ai伴侣余额 + wallet, err := walletmod.GetWallet(uid) + if err != nil { + log.Error("ai_mate_ser GetBalance walletmod.GetWallet err:", log.E(err), log.Any("uid", uid)) + return ret, err + } + + if wallet != nil && wallet.AiMateBalance > 0 { + ret.Balance = wallet.AiMateBalance + ret.LastOrderId = wallet.LastAiMateRecordId + } + return ret, nil +} + +type SyncInfoRes struct { + UID uint64 `json:"uid" bson:"uid"` // 用户id + TotalTokens int `json:"totalTokens" bson:"totalTokens"` // 总共使用的 token 数量 + Amount float64 `json:"amount" bson:"amount"` // 花费积分 + OrderID string `json:"orderId" bson:"orderId"` // 订单ID +} + +func (q *SyncInfoRes) Sync() error { + // 同步用户信息 + userInfo, err := usermod.FindUserByUIDForNoCache(q.UID) + if err != nil { + return err + } + + if userInfo == nil || userInfo.ID.IsZero() { + log.Warn(fmt.Sprintf("user is null")) + return nil + } + + // 获取 + cond := bson.M{"tranTypeInt": txnmod.AiMateChat, "uid": q.UID, "productID": q.OrderID} + transactionLog, err := txnmod.FindByCond(cond) + if err != nil { + return err + } + + // 重复上传 + if !transactionLog.ID.IsZero() { + return nil + } + // 获取余额 + wallet, err := walletmod.GetWallet(q.UID) + if err != nil { + return err + } + + if wallet == nil || wallet.ID.IsZero() { + return errors.New("wallet is null") + } + + amt := -q.Amount + // 获取用户是否是复购 + isRepurchase, err := txnmod.CheckRepurchaseByTransTypes(q.UID, []txnmod.TransType{ + txnmod.AiMateChat, + }) + if err != nil { + log.Error("txnmod.CheckRepurchaseByTransTypes fail", log.E(err)) + return err + } + // 新增日志流水记录 + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + if q.Amount > 0 { + // 扣除余额 + w, err := walletmod.CreditAiMate(t, amt, q.UID, q.OrderID) + if err != nil { + return err + } + po := int64(-q.Amount) + + // 添加流水 + if err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: userInfo.UID, + TranType: txnmod.AiMateChat.Key(), + TranTypeInt: int64(txnmod.AiMateChat), + Desc: fmt.Sprintf("AI女友聊天-花费:%.2f积分", q.Amount), + DiscDoc: userInfo.DiscDoc, + SysType: userInfo.SysType, + Integral: po, + ProductID: &q.OrderID, + AiMatePoint: -q.Amount, + RealAiMatePoint: w.AiMateBalance, + IsRepurchase: isRepurchase, + }); err != nil { + return err + } + } + return nil + }); err != nil { + log.Error(fmt.Sprintf("uid:%v,ai_mate sync trans err:%v", q.UID, err)) + return err + } + return nil +} diff --git a/app/service/ai_mate_ser/ai_mate_test.go b/app/service/ai_mate_ser/ai_mate_test.go new file mode 100644 index 0000000..41c5d4a --- /dev/null +++ b/app/service/ai_mate_ser/ai_mate_test.go @@ -0,0 +1,164 @@ +package ai_mate_ser + +import ( + "errors" + "testing" + + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestLoginCreatesAiMateUserAndCreditsLocalBalance(t *testing.T) { + const uid = uint64(32239213) + var ( + updatedAiMateUID string + accessAiMateUID string + creditedBalance float64 + ) + deps := loginDependencies{ + findUser: func(gotUID uint64) (*usermod.User, error) { + if gotUID != uid { + t.Fatalf("findUser uid = %d, want %d", gotUID, uid) + } + return &usermod.User{ + ID: primitive.NewObjectID(), + UID: uid, + Name: "tester", + }, nil + }, + updateUser: func(gotUID uint64, selector usermod.UserSelector) (*usermod.User, error) { + if gotUID != uid || selector.AiMateUid == nil { + t.Fatalf("unexpected updateUser arguments: uid=%d selector=%+v", gotUID, selector) + } + updatedAiMateUID = *selector.AiMateUid + return &usermod.User{}, nil + }, + getWallet: func(uint64) (*walletmod.Wallet, error) { + return &walletmod.Wallet{AiMateBalance: 12}, nil + }, + getRemoteBalance: func(uint64, string) (float64, error) { + t.Fatal("new AI mate user must not query a remote balance") + return 0, nil + }, + getAccessURL: func(gotUID uint64, aiMateUID, name string, balance float64) (string, error) { + if gotUID != uid || name != "tester" { + t.Fatalf("unexpected access URL arguments: uid=%d name=%q", gotUID, name) + } + accessAiMateUID = aiMateUID + creditedBalance = balance + return "https://example.com/login", nil + }, + newUUID: func() string { return "new-ai-mate-uid" }, + } + + got, err := login(uid, deps) + if err != nil { + t.Fatalf("login() error = %v", err) + } + if got.URL != "https://example.com/login" { + t.Fatalf("login() URL = %q", got.URL) + } + if updatedAiMateUID != "new-ai-mate-uid" || accessAiMateUID != updatedAiMateUID { + t.Fatalf("AI mate uid update=%q access=%q", updatedAiMateUID, accessAiMateUID) + } + if creditedBalance != 12 { + t.Fatalf("credited balance = %v, want 12", creditedBalance) + } +} + +func TestLoginOnlyCreditsBalanceDifference(t *testing.T) { + const uid = uint64(32239213) + var creditedBalance float64 + deps := loginDependencies{ + findUser: func(uint64) (*usermod.User, error) { + return &usermod.User{ + ID: primitive.NewObjectID(), + UID: uid, + Name: "tester", + AiMateUid: "existing-ai-mate-uid", + }, nil + }, + updateUser: func(uint64, usermod.UserSelector) (*usermod.User, error) { + t.Fatal("existing AI mate user must not be updated") + return nil, nil + }, + getWallet: func(uint64) (*walletmod.Wallet, error) { + return &walletmod.Wallet{AiMateBalance: 15}, nil + }, + getRemoteBalance: func(gotUID uint64, aiMateUID string) (float64, error) { + if gotUID != uid || aiMateUID != "existing-ai-mate-uid" { + t.Fatalf("unexpected remote balance arguments: uid=%d aiMateUID=%q", gotUID, aiMateUID) + } + return 9, nil + }, + getAccessURL: func(_ uint64, _ string, _ string, balance float64) (string, error) { + creditedBalance = balance + return "https://example.com/login", nil + }, + newUUID: func() string { + t.Fatal("existing AI mate user must not generate another uid") + return "" + }, + } + + if _, err := login(uid, deps); err != nil { + t.Fatalf("login() error = %v", err) + } + if creditedBalance != 6 { + t.Fatalf("credited balance = %v, want 6", creditedBalance) + } +} + +func TestLoginStopsWhenRemoteBalanceCannotBeRead(t *testing.T) { + expectedErr := errors.New("remote unavailable") + accessCalled := false + deps := loginDependencies{ + findUser: func(uint64) (*usermod.User, error) { + return &usermod.User{ + ID: primitive.NewObjectID(), + UID: 1, + AiMateUid: "existing-ai-mate-uid", + }, nil + }, + getWallet: func(uint64) (*walletmod.Wallet, error) { + return &walletmod.Wallet{AiMateBalance: 10}, nil + }, + getRemoteBalance: func(uint64, string) (float64, error) { + return 0, expectedErr + }, + getAccessURL: func(uint64, string, string, float64) (string, error) { + accessCalled = true + return "", nil + }, + } + + if _, err := login(1, deps); !errors.Is(err, expectedErr) { + t.Fatalf("login() error = %v, want %v", err, expectedErr) + } + if accessCalled { + t.Fatal("access URL must not be requested when the remote balance is unknown") + } +} + +func TestBalanceTopUp(t *testing.T) { + tests := []struct { + name string + local float64 + remote float64 + want float64 + }{ + {name: "new credit", local: 10, remote: 4, want: 6}, + {name: "already synchronized", local: 10, remote: 10, want: 0}, + {name: "remote ahead", local: 8, remote: 10, want: 0}, + {name: "empty", local: 0, remote: 0, want: 0}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := balanceTopUp(test.local, test.remote); got != test.want { + t.Fatalf("balanceTopUp(%v, %v) = %v, want %v", test.local, test.remote, got, test.want) + } + }) + } +} diff --git a/app/service/ai_text_to_image_ser/aitexttoimage.go b/app/service/ai_text_to_image_ser/aitexttoimage.go new file mode 100755 index 0000000..717287b --- /dev/null +++ b/app/service/ai_text_to_image_ser/aitexttoimage.go @@ -0,0 +1,372 @@ +package ai_text_to_image_ser + +import ( + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/middleware/ua" + "91porn-server/models/cache/aitexttoimagedata" + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/commod" + "91porn-server/models/v/aichangefacevidmod" + "91porn-server/models/v/aitexttoimagemod" + "91porn-server/models/v/sysconfmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AppQueryListReq struct { + Status int `json:"status" form:"status"` // 状态 1、排队中 2、生成成功 3、生成失败 + UID uint64 `json:"-"` // 用户ID + commod.Page +} + +func (p *AppQueryListReq) Filter() primitive.M { + filter := bson.M{} + switch p.Status { + case 1: + filter["status"] = bson.M{"$in": []aitexttoimagemod.AiTextToImageStatus{aitexttoimagemod.StatusOrderSuccess, aitexttoimagemod.StatusSubmitted}} + case 2: + filter["status"] = aitexttoimagemod.StatusGenerationSuccess + case 3: + filter["status"] = bson.M{"$in": []aitexttoimagemod.AiTextToImageStatus{aitexttoimagemod.StatusGenerationFailed, aitexttoimagemod.StatusRefunded}} + } + filter["uid"] = p.UID + filter["isHide"] = false + return filter +} + +type AppListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []*aitexttoimagemod.AiTextToImageInfo `json:"list"` +} + +// GetList 获取列表 +func (p *AppQueryListReq) GetList() AppListRes { + var res AppListRes + var err error + + sort := bson.D{{"createdAt", -1}} + // 获取列表 + var data []aitexttoimagemod.AiTextToImage + data, res.Total, res.HasNext, err = aitexttoimagemod.GetList(p.Filter(), int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + log.Error("获取AI绘图列表列表数据错误", log.Any("Params", *p), log.E(err)) + return res + } + res.List = aitexttoimagedata.FormatAppDataList(data) + + // 获取所有的模版 + cond := bson.M{"status": 1, "moduleType": 5} + mods, err := aichangefacevidmod.GetAllModsByCond(cond) + if err != nil { + return res + } + + if len(mods) > 0 { + for i := range res.List { + for _, m := range mods { + if res.List[i].StyleType == m.StyleType { + res.List[i].StyleUrl = m.Cover + } + } + } + } + return res +} + +type GenerateReq struct { + Text string `json:"text"` // 绘图描述 + StyleType int `json:"styleType"` // 绘图风格 + AspectRatio string `json:"aspectRatio"` // 绘图比例 1:1,4:3,3:4,16:9,9:16 + Coin int64 `json:"-"` // 此次脱衣金币个数 + UID uint64 `json:"-"` // 用户ID + ShareTitle string `json:"shareTitle"` // 分享标题 + ShareStatus int `json:"shareStatus"` // 是否分享 0-不分享 1-分享 +} + +func (p *GenerateReq) GenerateInfo(orderId primitive.ObjectID, orderCreatedAt time.Time, debitCoins, debitIncomeCoins int64) aitexttoimagemod.AiTextToImage { + return aitexttoimagemod.AiTextToImage{ + ID: orderId, + Coin: debitIncomeCoins + debitCoins, + DebitAmountCoin: debitCoins, + DebitIncomeCoin: debitIncomeCoins, + IsFreeTimes: false, + Text: p.Text, + StyleType: p.StyleType, + AspectRatio: p.AspectRatio, + UID: p.UID, + Status: int(aitexttoimagemod.StatusOrderSuccess), + IsHide: false, + ShareTitle: p.ShareTitle, + ShareStatus: p.ShareStatus, + UpdatedAt: orderCreatedAt, + CreatedAt: orderCreatedAt, + } +} + +// Generate 生成订单 +func (p *GenerateReq) Generate(ua ua.UA, ip string) (stderr.Code, error) { + // 获取用户信息 + user, err := usermod.FindUserByUID(p.UID) + if err != nil { + return stderr.ErrDbQueryError, err + } + if user.ID.IsZero() { + return stderr.UserIsNotExists, errors.New("user is null") + } + configure, _ := sysconfdata.GetAllFromCache() + p.Coin = configure.GetInt(sysconfmod.VCodeAiTextToImagePrice) + + var debitAmountCoins, debitIncomeCoins int64 + + if p.Coin > 0 { + // 获取用户钱包 + w, err := walletmod.GetWallet(p.UID) + if err != nil { + return stderr.ErrDbQueryError, err + } + if w == nil { + return stderr.InsufficientBalance, errors.New("Insufficient balance") + } + + // 如果收费的部分余额不够支付 + if p.Coin > (w.Amount + w.Income) { + return stderr.InsufficientBalance, errors.New("Insufficient balance") + } + + debitAmountCoins, debitIncomeCoins = TotalDebit(p.Coin, *w) + } + // 获取用户是否是复购 + isRepurchase, err := txnmod.CheckRepurchaseByTransTypes(p.UID, []txnmod.TransType{ + txnmod.AiTextToImageDebitGold, + txnmod.AiTextToImageDebitInComeGold, + }) + if err != nil { + log.Error("txnmod.CheckRepurchaseByTransTypes fail", log.E(err)) + return stderr.ErrDbQueryError, err + } + var orderId primitive.ObjectID + var orderCreatedAt time.Time + if err = appg.VideoDB.Trans(func(tool *db.MongoTool) error { + var tl []txnmod.TransactionLog + if debitIncomeCoins > 0 || debitAmountCoins > 0 { + // 扣除钱包余额 + wallet, wErr := walletmod.DebitAmountAndIncome(tool, debitAmountCoins, debitIncomeCoins, p.UID) + if wErr != nil { + log.Error(fmt.Sprintf("Handle imagetovideo Generate walletmod.DebitAmount error:%+v:", wErr), log.Any("uid", p.UID)) + return stderr.ErrDbQueryError + } + + if debitAmountCoins > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: p.UID, + Amount: -debitAmountCoins, + ActualAmount: float64(-debitAmountCoins), + TranType: txnmod.AiTextToImageDebitGold.Key(), + TranTypeInt: int64(txnmod.AiTextToImageDebitGold), + Desc: fmt.Sprintf("生成AI绘图金币-%d", debitAmountCoins), + RealAmount: walletmod.GetRealAmount(wallet), + SysType: user.SysType, + IsRepurchase: isRepurchase, + }) + } + if debitIncomeCoins > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: p.UID, + Amount: -debitIncomeCoins, + ActualAmount: float64(-debitIncomeCoins), + TranType: txnmod.AiTextToImageDebitInComeGold.Key(), + TranTypeInt: int64(txnmod.AiTextToImageDebitInComeGold), + Desc: fmt.Sprintf("生成AI绘图收益金币-%d", debitIncomeCoins), + RealAmount: walletmod.GetRealAmount(wallet), + SysType: user.SysType, + IsRepurchase: isRepurchase, + }) + } + } + + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(tool, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle imagetovideo Generate txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", p.UID)) + return stderr.ErrDbQueryError + } + } + + // 新增AI图生视频记录 + orderId = primitive.NewObjectID() + orderCreatedAt = time.Now() + order := p.GenerateInfo(orderId, orderCreatedAt, debitAmountCoins, debitIncomeCoins) + if _, err = aitexttoimagemod.Insert(tool, order); err != nil { + log.Error(fmt.Sprintf("Handle imagetovideo Generate InsertOnes error:%+v:", err), log.Any("uid", p.UID)) + return stderr.ErrDbInsertError + } + return nil + }); err != nil { + log.Error(fmt.Sprintf("Handle imagetovideo Generate Trans error:%+v;uid:%v;", err, p.UID)) + return stderr.ErrDbUpdateError, err + } + + common.Go(func() { + autoUploadOrder(orderId) + }) + return stderr.Success, nil +} + +type AutoUploadReq struct { + AppID int `json:"appId"` // 应用 ID + UID uint64 `json:"uid"` // 用户 ID + Text string `json:"text"` // 绘图描述 + StyleType int `json:"styleType"` // 绘图风格 + AspectRatio string `json:"aspectRatio"` // 绘图比例 1:1,4:3,3:4,16:9,9:16 + AppOrderNum string `json:"appOrderNum"` // 应用订单号 + NotifyURL string `json:"notifyUrl"` // 通知 URL +} + +func autoUploadOrder(id primitive.ObjectID) error { + item, err := aitexttoimagemod.GetInfo(id) + if err != nil { + return err + } + + if item.ID.IsZero() { + return errors.New(" aiimagetovideo order is null") + } + + // 将请求参数编码为JSON + requestBody := AutoUploadReq{ + AppID: int(commod.KFK_APPID), + UID: item.UID, + Text: item.Text, + AspectRatio: item.AspectRatio, + StyleType: item.StyleType, + AppOrderNum: item.ID.Hex(), + NotifyURL: fmt.Sprintf("%v/api/web/admin/ai/text_to_image/callback", appg.Conf.URL.AiImageToVideoCallbackUrl), + } + + jsonData, err := json.Marshal(requestBody) + if err != nil { + return err + } + + apiURL := getAutoUploadUrl() + + // 创建请求对象 + req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("创建请求失败: %v", err) + } + + // 通过http.Client发送请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + // 读取响应内容 + respBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("读取响应内容失败: %v", err) + } + + // 如果返回状态码非200,可以考虑返回错误 + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("请求失败,状态码:%d,内容:%s", resp.StatusCode, string(respBytes)) + } + + // 更新订单 + updateCond := bson.M{ + "status": aitexttoimagemod.StatusSubmitted, + "updatedAt": time.Now(), + } + _, err = aitexttoimagemod.UpdateByID(nil, id, updateCond) + if err != nil { + return err + } + return nil +} + +func getAutoUploadUrl() string { + return fmt.Sprintf("%v/api/comfyui/prd/create_text_job", appg.Conf.URL.AiImageToVideoUrl) +} + +func TotalDebit(price int64, w walletmod.Wallet) (amountCoins, incomeCoins int64) { + var ( + debitAmountCoins int64 + debitIncomeCoins int64 + ) + + // 计算当日实际扣减的金额 + debitCoins := price + if w.Amount >= debitCoins { + debitAmountCoins = debitCoins + } else { + debitAmountCoins = w.Amount + debitIncomeCoins = debitCoins - w.Amount + } + return debitAmountCoins, debitIncomeCoins +} + +type HideReq struct { + ID string `json:"id"` // AI订单ID + UID uint64 `json:"-"` // 用户ID +} + +// Hide 删除订单 +func (p *HideReq) Hide() error { + oid, err := primitive.ObjectIDFromHex(p.ID) + if err != nil { + return err + } + + item, err := aitexttoimagemod.GetInfo(oid) + if err != nil { + log.Error("获取AI绘图列表详情数据错误", log.Any("ID", p.ID), log.E(err)) + return err + } + + if item.ID.IsZero() { + return errors.New("AI订单不存在") + } + + if item.UID != p.UID { + return errors.New("只能删除自己的订单") + } + + if err = validateHideStatus(item.Status); err != nil { + return err + } + + if err = aitexttoimagemod.Hide(p.UID, oid); err != nil { + return err + } + + return nil +} + +func validateHideStatus(status int) error { + if status == int(aitexttoimagemod.StatusOrderSuccess) || + status == int(aitexttoimagemod.StatusSubmitted) { + return stderr.AiGenningDelForbidden + } + return nil +} diff --git a/app/service/ai_text_to_novel_ser/aitexttonovel.go b/app/service/ai_text_to_novel_ser/aitexttonovel.go new file mode 100755 index 0000000..e3f4785 --- /dev/null +++ b/app/service/ai_text_to_novel_ser/aitexttonovel.go @@ -0,0 +1,385 @@ +package ai_text_to_novel_ser + +import ( + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/middleware/ua" + "91porn-server/models/cache/aitexttonoveldata" + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/commod" + "91porn-server/models/v/aitexttonovelmod" + "91porn-server/models/v/sysconfmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AppQueryListReq struct { + Status int `json:"status" form:"status"` // 状态 1、排队中 2、生成成功 3、生成失败 + UID uint64 `json:"-"` // 用户ID + commod.Page +} + +func (p *AppQueryListReq) Filter() primitive.M { + filter := bson.M{} + switch p.Status { + case 1: + filter["status"] = bson.M{"$in": []aitexttonovelmod.AiTextToNovelStatus{aitexttonovelmod.StatusOrderSuccess, aitexttonovelmod.StatusSubmitted}} + case 2: + filter["status"] = aitexttonovelmod.StatusGenerationSuccess + case 3: + filter["status"] = bson.M{"$in": []aitexttonovelmod.AiTextToNovelStatus{aitexttonovelmod.StatusGenerationFailed, aitexttonovelmod.StatusRefunded}} + } + filter["uid"] = p.UID + filter["isHide"] = false + return filter +} + +type AppListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []*aitexttonovelmod.AiTextToNovel `json:"list"` +} + +// GetList 获取列表 +func (p *AppQueryListReq) GetList() AppListRes { + var res AppListRes + var err error + + sort := bson.D{{"createdAt", -1}} + // 获取列表 + var data []aitexttonovelmod.AiTextToNovel + data, res.Total, res.HasNext, err = aitexttonovelmod.GetList(p.Filter(), int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + log.Error("获取AI小说列表列表数据错误", log.Any("Params", *p), log.E(err)) + return res + } + res.List = aitexttonoveldata.FormatAppDataList(data) + + return res +} + +type AppQueryInfoReq struct { + ID string `json:"id" form:"id"` // id +} +type AppQueryInfoRes = *aitexttonovelmod.AiTextToNovel + +// GetInfo 获取详情 +func (p *AppQueryInfoReq) GetInfo() (res AppQueryInfoRes, err error) { + oid, err := primitive.ObjectIDFromHex(p.ID) + if err != nil { + return res, err + } + + var item aitexttonovelmod.AiTextToNovel + item, err = aitexttonovelmod.GetInfo(oid) + if err != nil { + log.Error("获取AI小说列表详情数据错误", log.Any("ID", p.ID), log.E(err)) + return + } + + res = aitexttonoveldata.FormatAppData(item) + return +} + +type GenerateReq struct { + Description string `json:"description"` // 剧情描述/故事情节 + CharacterSetting string `json:"characterSetting"` // 人物设定 + LocationScene string `json:"locationScene"` // 地点场景 + Details string `json:"details"` // 细节说明/其他要求 + ModelType int `json:"modelType"` // 模型 1:AI小艺 2:AI小萌 + Coin int64 `json:"-"` // 此次脱衣金币个数 + UID uint64 `json:"-"` // 用户ID +} + +func (p *GenerateReq) GenerateInfo(orderId primitive.ObjectID, orderCreatedAt time.Time, debitCoins, debitIncomeCoins int64) aitexttonovelmod.AiTextToNovel { + return aitexttonovelmod.AiTextToNovel{ + ID: orderId, + Coin: debitIncomeCoins + debitCoins, + DebitAmountCoin: debitCoins, + DebitIncomeCoin: debitIncomeCoins, + IsFreeTimes: false, + Description: p.Description, + CharacterSetting: p.CharacterSetting, + LocationScene: p.LocationScene, + Details: p.Details, + ModelType: p.ModelType, + UID: p.UID, + Status: int(aitexttonovelmod.StatusOrderSuccess), + IsHide: false, + UpdatedAt: orderCreatedAt, + CreatedAt: orderCreatedAt, + } +} + +// Generate 生成订单 +func (p *GenerateReq) Generate(ua ua.UA, ip string) (stderr.Code, error) { + // 获取用户信息 + user, err := usermod.FindUserByUID(p.UID) + if err != nil { + return stderr.ErrDbQueryError, err + } + if user.ID.IsZero() { + return stderr.UserIsNotExists, errors.New("user is null") + } + configure, _ := sysconfdata.GetAllFromCache() + p.Coin = configure.GetInt(sysconfmod.VCodeAiTextToNovelPrice) + + var debitAmountCoins, debitIncomeCoins int64 + + if p.Coin > 0 { + // 获取用户钱包 + w, err := walletmod.GetWallet(p.UID) + if err != nil { + return stderr.ErrDbQueryError, err + } + if w == nil { + return stderr.InsufficientBalance, errors.New("Insufficient balance") + } + + // 如果收费的部分余额不够支付 + if p.Coin > (w.Amount + w.Income) { + return stderr.InsufficientBalance, errors.New("Insufficient balance") + } + + debitAmountCoins, debitIncomeCoins = TotalDebit(p.Coin, *w) + } + + // 获取用户是否是复购 + isRepurchase, err := txnmod.CheckRepurchaseByTransTypes(p.UID, []txnmod.TransType{ + txnmod.AiTextToNovelDebitGold, + txnmod.AiTextToNovelDebitInComeGold, + }) + if err != nil { + log.Error("txnmod.CheckRepurchaseByTransTypes fail", log.E(err)) + return stderr.ErrDbQueryError, err + } + + var orderId primitive.ObjectID + var orderCreatedAt time.Time + if err = appg.VideoDB.Trans(func(tool *db.MongoTool) error { + var tl []txnmod.TransactionLog + if debitIncomeCoins > 0 || debitAmountCoins > 0 { + // 扣除钱包余额 + wallet, wErr := walletmod.DebitAmountAndIncome(tool, debitAmountCoins, debitIncomeCoins, p.UID) + if wErr != nil { + log.Error(fmt.Sprintf("Handle imagetovideo Generate walletmod.DebitAmount error:%+v:", wErr), log.Any("uid", p.UID)) + return stderr.ErrDbQueryError + } + + if debitAmountCoins > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: p.UID, + Amount: -debitAmountCoins, + ActualAmount: float64(-debitAmountCoins), + TranType: txnmod.AiTextToNovelDebitGold.Key(), + TranTypeInt: int64(txnmod.AiTextToNovelDebitGold), + Desc: fmt.Sprintf("生成AI小说扣除金币-%d", debitAmountCoins), + RealAmount: walletmod.GetRealAmount(wallet), + SysType: user.SysType, + IsRepurchase: isRepurchase, + }) + } + if debitIncomeCoins > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: p.UID, + Amount: -debitIncomeCoins, + ActualAmount: float64(-debitIncomeCoins), + TranType: txnmod.AiTextToNovelDebitInComeGold.Key(), + TranTypeInt: int64(txnmod.AiTextToNovelDebitInComeGold), + Desc: fmt.Sprintf("生成AI小说扣除收益金币-%d", debitIncomeCoins), + RealAmount: walletmod.GetRealAmount(wallet), + SysType: user.SysType, + IsRepurchase: isRepurchase, + }) + } + } + + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(tool, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle imagetovideo Generate txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", p.UID)) + return stderr.ErrDbQueryError + } + } + + // 新增AI图生视频记录 + orderId = primitive.NewObjectID() + orderCreatedAt = time.Now() + order := p.GenerateInfo(orderId, orderCreatedAt, debitAmountCoins, debitIncomeCoins) + if _, err = aitexttonovelmod.Insert(tool, order); err != nil { + log.Error(fmt.Sprintf("Handle imagetovideo Generate InsertOnes error:%+v:", err), log.Any("uid", p.UID)) + return stderr.ErrDbInsertError + } + + return nil + }); err != nil { + log.Error(fmt.Sprintf("Handle imagetovideo Generate Trans error:%+v;uid:%v;", err, p.UID)) + return stderr.ErrDbUpdateError, err + } + common.Go(func() { + _ = autoUploadOrder(orderId) + }) + + return stderr.Success, nil +} + +type AutoUploadReq struct { + AppID int `json:"appId"` // 应用ID + UID uint64 `json:"uid"` // 用户ID + Description string `json:"description"` // 剧情描述/故事情节 + CharacterSetting string `json:"characterSetting"` // 绘图风格 + LocationScene string `json:"locationScene"` // 人物设定 + Details string `json:"details"` // 地点场景 + ModelType int `json:"modelType"` // AI模型 + AppOrderNum string `json:"appOrderNum"` // 应用订单号 + NotifyURL string `json:"notifyUrl"` // 通知 URL +} + +func autoUploadOrder(id primitive.ObjectID) error { + item, err := aitexttonovelmod.GetInfo(id) + if err != nil { + return err + } + + if item.ID.IsZero() { + return errors.New(" ai text to novel order is null") + } + + // 将请求参数编码为JSON + requestBody := AutoUploadReq{ + AppID: int(commod.KFK_APPID), + UID: item.UID, + Description: item.Description, + CharacterSetting: item.CharacterSetting, + LocationScene: item.LocationScene, + Details: item.Details, + ModelType: item.ModelType, + AppOrderNum: item.ID.Hex(), + NotifyURL: fmt.Sprintf("%v/api/web/admin/ai/text_to_novel/callback", appg.Conf.URL.AiImageToVideoCallbackUrl), + } + + jsonData, err := json.Marshal(requestBody) + if err != nil { + return err + } + + apiURL := getAutoUploadUrl() + + // 创建请求对象 + req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("创建请求失败: %v", err) + } + + // 通过http.Client发送请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + // 读取响应内容 + respBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("读取响应内容失败: %v", err) + } + + // 如果返回状态码非200,可以考虑返回错误 + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("请求失败,状态码:%d,内容:%s", resp.StatusCode, string(respBytes)) + } + + // 更新订单 + updateCond := bson.M{ + "status": aitexttonovelmod.StatusSubmitted, + "updatedAt": time.Now(), + } + _, err = aitexttonovelmod.UpdateByID(nil, id, updateCond) + if err != nil { + return err + } + return nil +} + +func getAutoUploadUrl() string { + return fmt.Sprintf("%v/api/comfyui/prd/create_novel_job", appg.Conf.URL.AiImageToVideoUrl) +} + +func TotalDebit(price int64, w walletmod.Wallet) (amountCoins, incomeCoins int64) { + var ( + debitAmountCoins int64 + debitIncomeCoins int64 + ) + + // 计算当日实际扣减的金额 + debitCoins := price + if w.Amount >= debitCoins { + debitAmountCoins = debitCoins + } else { + debitAmountCoins = w.Amount + debitIncomeCoins = debitCoins - w.Amount + } + return debitAmountCoins, debitIncomeCoins +} + +type HideReq struct { + ID string `json:"id"` // AI订单ID + UID uint64 `json:"-"` // 用户ID +} + +// Hide 删除订单 +func (p *HideReq) Hide() error { + oid, err := primitive.ObjectIDFromHex(p.ID) + if err != nil { + return err + } + + item, err := aitexttonovelmod.GetInfo(oid) + if err != nil { + log.Error("获取AI绘图列表详情数据错误", log.Any("ID", p.ID), log.E(err)) + return err + } + + if item.ID.IsZero() { + return errors.New("AI订单不存在") + } + + if item.UID != p.UID { + return errors.New("只能删除自己的订单") + } + + if err = validateHideStatus(item.Status); err != nil { + return err + } + + if err = aitexttonovelmod.Hide(p.UID, oid); err != nil { + return err + } + + return nil +} + +func validateHideStatus(status int) error { + if status == int(aitexttonovelmod.StatusOrderSuccess) || + status == int(aitexttonovelmod.StatusSubmitted) { + return stderr.AiGenningDelForbidden + } + return nil +} diff --git a/app/service/ai_undress_server/ai_undress.go b/app/service/ai_undress_server/ai_undress.go new file mode 100644 index 0000000..3f34342 --- /dev/null +++ b/app/service/ai_undress_server/ai_undress.go @@ -0,0 +1,330 @@ +package ai_undress_server + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/aiautoser" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/middleware/ua" + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/v/advanceordermod" + "91porn-server/models/v/aiUnDressmod" + "91porn-server/models/v/sysconfmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + "fmt" + "strconv" + "time" + + "github.com/go-redis/redis" + carbon "github.com/golang-module/carbon/v2" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func List(uid uint64, req *aiUnDressmod.ListRequest) (interface{}, stderr.Code) { + var data = map[string]interface{}{ + "list": []interface{}{}, + "count": 0, + } + count, err := aiUnDressmod.CountDocument(req.Filter(uid)) + if err != nil { + return nil, stderr.ErrDbQueryError + } + if count == 0 { + return data, stderr.Success + } + opts := options.Find().SetSkip(int64(req.Skip())).SetLimit(int64(req.Limit() + 1)).SetSort(bson.M{"updatedAt": -1}) + list, err := aiUnDressmod.QueryAllDocument(req.Filter(uid), opts) + if err != nil { + return data, stderr.ErrDbQueryError + } + hasNext := false + if len(list) > int(req.Limit()) { + list = list[:req.Limit()] + hasNext = true + } + data["count"] = count // 兼容旧版本 + data["hasNext"] = hasNext + data["list"] = list + return data, stderr.Success +} + +func Generate(uid uint64, req *aiUnDressmod.GenerateRequest, ua ua.UA, ip string) stderr.Code { + if req.OriginPic == nil || len(req.OriginPic) == 0 { + return stderr.ErrParamError + } + // 获取用户信息 + user, err := usermod.FindUserByUID(uid) + if err != nil { + return stderr.ErrDbQueryError + } + if user.ID.IsZero() { + return stderr.UserIsNotExists + } + configure, _ := sysconfdata.GetAllFromCache() + req.Coin = configure.GetInt(sysconfmod.VCodeAiUndressPrice) + picCount := int64(len(req.OriginPic)) + privilegeFreeCount := int64(0) + // 获取用户是否是复购 + isRepurchase, err := txnmod.CheckRepurchaseByTransTypes(uid, []txnmod.TransType{ + txnmod.AiUndressDebitFreeTimes, + txnmod.AiUndressDebitGold, + txnmod.AiUndressDebitIncomeGold, + }) + if err != nil { + log.Error("txnmod.CheckRepurchaseByTransTypes fail", log.E(err)) + return stderr.ErrDbQueryError + } + hasPrivilege, advance := advanceordermod.GetUserValidOrder(uid) + if hasPrivilege { + // 获取可用免费次数 + validCount := advance.PrepaidPrivilege.AiUndressLimitPerDay - advance.TodayUse.AiUndressCount + if validCount >= picCount { + // 扣除免费次数 + debitPlan := advanceordermod.DebitPlan{} + debitPlan.AiUndressCount = &picCount + if err := advanceordermod.Debit(nil, uid, debitPlan); err != nil { + return stderr.ErrDbUpdateError + } + privilegeFreeCount = picCount + picCount = 0 + } else if validCount > 0 { + // 扣除免费次数 + debitPlan := advanceordermod.DebitPlan{} + debitPlan.AiUndressCount = &validCount + if err := advanceordermod.Debit(nil, uid, debitPlan); err != nil { + return stderr.ErrDbUpdateError + } + privilegeFreeCount = validCount + picCount -= validCount + } + if validCount > 0 { + l := &txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: 1, + ActualAmount: float64(-1), + TranType: txnmod.AiUndressDebitFreeTimes.Key(), + TranTypeInt: int64(txnmod.AiUndressDebitFreeTimes), + Desc: fmt.Sprintf("生成AI脱衣扣除免费次数[%d次]", privilegeFreeCount), + RealAmount: decimal.Decimal{}, + SysType: user.SysType, + IsRepurchase: isRepurchase, + } + txnErr := txnmod.InsertTransactionLog(nil, l) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Generate txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + } + } + var debitFreeCount, debitAmountCoins, debitIncomeCoins int64 + + if picCount > 0 { + // 获取用户钱包 + w, err := walletmod.GetWallet(uid) + if err != nil { + return stderr.ErrDbQueryError + } + // 校验用户余额 + if w == nil || !CheckBalance(req.Coin, picCount, *w) { + return stderr.InsufficientBalance + } + + debitFreeCount, debitAmountCoins, debitIncomeCoins = TotalDebit(req.Coin, picCount, *w) + if w.AiUndressFreeTimes > 0 { + req.IsFreeTimes = true + } + } + var orderId primitive.ObjectID + var orderCreatedAt time.Time + if err = appg.VideoDB.Trans(func(tool *db.MongoTool) error { + var tl []txnmod.TransactionLog + if debitFreeCount > 0 { + expire := carbon.Tomorrow().StartOfDay().StdTime().Sub(time.Now()) + // 累加次数 + appg.Redis.IncrBy(redisconst.AiFreeUndressTodayUseTimesKey(uid), debitFreeCount, expire) + // 扣除钱剩余次数 + wallet, wErr := walletmod.DebitAiFreeTimes(tool, debitFreeCount, uid) + if wErr != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Generate walletmod.DebitAiFreeTimes error:%+v:", wErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: 1, + ActualAmount: float64(-1), + TranType: txnmod.AiUndressDebitFreeTimes.Key(), + TranTypeInt: int64(txnmod.AiUndressDebitFreeTimes), + Desc: fmt.Sprintf("生成AI脱衣扣除免费次数[%d次]", debitFreeCount), + RealAmount: walletmod.GetRealAmount(wallet), + SysType: user.SysType, + IsRepurchase: isRepurchase, + }) + } + + if debitIncomeCoins > 0 || debitAmountCoins > 0 { + // 扣除钱包余额 + wallet, wErr := walletmod.DebitAmountAndIncome(tool, debitAmountCoins, debitIncomeCoins, uid) + if wErr != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Generate walletmod.DebitAmount error:%+v:", wErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + + if debitAmountCoins > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -debitAmountCoins, + ActualAmount: float64(-debitAmountCoins), + TranType: txnmod.AiUndressDebitGold.Key(), + TranTypeInt: int64(txnmod.AiUndressDebitGold), + Desc: fmt.Sprintf("生成AI脱衣金币-%d", debitAmountCoins), + RealAmount: walletmod.GetRealAmount(wallet), + SysType: user.SysType, + IsRepurchase: isRepurchase, + }) + } + if debitIncomeCoins > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -debitIncomeCoins, + ActualAmount: float64(-debitIncomeCoins), + TranType: txnmod.AiUndressDebitIncomeGold.Key(), + TranTypeInt: int64(txnmod.AiUndressDebitIncomeGold), + Desc: fmt.Sprintf("生成AI脱衣收益金币-%d", debitIncomeCoins), + RealAmount: walletmod.GetRealAmount(wallet), + SysType: user.SysType, + IsRepurchase: isRepurchase, + }) + } + } + + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(tool, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Generate txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + } + + // 新增AI脱衣记录 + orderId = primitive.NewObjectID() + orderCreatedAt = time.Now() + if err := aiUnDressmod.InsertMany(tool, req.GenerateMany(orderId, orderCreatedAt, uid, privilegeFreeCount, debitFreeCount, debitAmountCoins, debitIncomeCoins, req.Coin, req.ShareTitle, req.ShareStatus)); err != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Generate InsertOnes error:%+v:", err), log.Any("uid", uid)) + return stderr.ErrDbInsertError + } + + return nil + }); err != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Generate Trans error:%+v;uid:%v;", err, uid)) + return stderr.ErrDbUpdateError + } + common.Go(func() { + if err := aiautoser.SubmitUndress(orderId); err != nil { + log.Error("AI脱衣自动处理失败", log.Any("id", orderId.Hex()), log.E(err)) + } + }) + + return stderr.Success +} + +func AiUndressHide(uid uint64, req *aiUnDressmod.DelRequest) stderr.Code { + acf, err := aiUnDressmod.FindByID(nil, req.ID) + if err != nil { + return stderr.ErrDbQueryError + } + if acf.ID.IsZero() { + return stderr.CodeEmptyData + } + if acf.UID != uid { + return stderr.Failure + } + if acf.Status == aiUnDressmod.Processing || acf.Status == aiUnDressmod.SubmitOrder { + return stderr.AiGenningDelForbidden + } + if err = aiUnDressmod.Edit(nil, req.Filter(), req.Update()); err != nil { + return stderr.ErrDbQueryError + } + return stderr.Success +} + +func TotalDebit(price int64, count int64, w walletmod.Wallet) (fCount, amountCoins, incomeCoins int64) { + var ( + debitFreeTimes int64 + debitAmountCoins int64 + debitIncomeCoins int64 + ) + + // 当日剩余ai免费脱衣次数 + todayAiUndressFreeTimes := GetTodayRemainingAiFreeUndressTimes(w) + // 实际使用的免费脱衣次数 + if todayAiUndressFreeTimes >= count { + return count, 0, 0 + } + debitFreeTimes = todayAiUndressFreeTimes + // 计算当日实际扣减的金额 + debitCoins := (count - debitFreeTimes) * price + if w.Amount >= debitCoins { + debitAmountCoins = debitCoins + } else { + debitAmountCoins = w.Amount + debitIncomeCoins = debitCoins - w.Amount + } + + return debitFreeTimes, debitAmountCoins, debitIncomeCoins +} + +// GetTodayRemainingAiFreeUndressTimes 获取当日剩余的免费ai脱衣的次数 +func GetTodayRemainingAiFreeUndressTimes(w walletmod.Wallet) (num int64) { + var todayUseAiUndressFreeTimes int // 当日已使用的免费ai脱衣次数 + //获取当日已经使用的ai免费脱衣次数 + str, err := appg.Redis.Get(redisconst.AiFreeUndressTodayUseTimesKey(w.UID)) + if err != nil && err != redis.Nil { + log.Error(fmt.Sprintf("get AiUndressTodayUseTimes err%v", err)) + // redis报错返回已经当日最大限制次数,避免被刷 + todayUseAiUndressFreeTimes = 0 + } else if str != nil { + todayUseAiUndressFreeTimes, _ = strconv.Atoi(*str) + } + num = int64(appg.Conf.AiFreeUnDressDailyLimit - todayUseAiUndressFreeTimes) + // 如果用户剩余的免费次数不足,以剩余次数为准 + if num >= w.AiUndressFreeTimes { + num = w.AiUndressFreeTimes + } + if num < 0 { + num = 0 + } + return +} + +func CheckBalance(price int64, count int64, w walletmod.Wallet) bool { + var checkStats bool + var totalCoins = w.Amount + if w.Income != 0 { + totalCoins += w.Income + } + if w.AiUndressFreeTimes > 0 && w.AiUndressFreeTimes >= count { + checkStats = true + } + + if w.AiUndressFreeTimes > 0 && w.AiUndressFreeTimes < count && totalCoins >= price*(count-w.AiUndressFreeTimes) { + checkStats = true + } + + if w.AiUndressFreeTimes <= 0 && totalCoins >= price*count { + checkStats = true + } + return checkStats +} diff --git a/app/service/aiautoser/automation.go b/app/service/aiautoser/automation.go new file mode 100644 index 0000000..ad35a95 --- /dev/null +++ b/app/service/aiautoser/automation.go @@ -0,0 +1,329 @@ +package aiautoser + +import ( + "errors" + "fmt" + "strconv" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/models/commod" + "91porn-server/models/v/aiUnDressmod" + "91porn-server/models/v/aichangefaceimgmod" + "91porn-server/models/v/aichangefacemod" + "91porn-server/models/v/backpackmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/walletmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const autoOperator = "AI_AUTO" + +type changeFaceRequest struct { + AppID int `json:"app_id"` + OrderNumApp string `json:"order_num_app"` + Title string `json:"title"` + SourceImageURL string `json:"source_image_url"` + NotifyURL string `json:"notify_url"` +} + +type changeFaceResponse struct { + Code int `json:"code"` +} + +// SubmitVideo automatically approves and submits an AI video face-swap order. +// Submission failures are handled with the same refund policy as the existing +// Web callback flow. +func SubmitVideo(id primitive.ObjectID) error { + order, err := aichangefacemod.FindByID(nil, id) + if err != nil { + return err + } + transitioned, err := aichangefacemod.TransitionStatus(nil, id, aichangefacemod.StatusGenning, aichangefacemod.StatusSubmit, bson.M{ + "operator": autoOperator, + "remark": "已自动提交", + }) + if err != nil || !transitioned { + return err + } + + notifyURL, notifyErr := appg.AICallbackURL("/api/web/admin/ai/changeface/callback") + if notifyErr != nil || len(order.Pic) == 0 || order.Pic[0] == "" || appg.Conf.URL.AiServer == "" { + return refundVideo(id, "自动提交参数不完整") + } + req := changeFaceRequest{ + AppID: int(commod.KFK_APPID), + OrderNumApp: id.Hex(), + Title: order.ModTitle, + SourceImageURL: order.Pic[0], + NotifyURL: notifyURL, + } + if err := postChangeFace(req); err != nil { + log.Error("AI视频换脸自动提交失败", log.Any("id", id.Hex()), log.E(err)) + return refundVideo(id, err.Error()) + } + return nil +} + +// SubmitImage automatically approves and submits an AI image face-swap order. +func SubmitImage(id primitive.ObjectID) error { + order, err := aichangefaceimgmod.FindByID(nil, id) + if err != nil { + return err + } + transitioned, err := aichangefaceimgmod.TransitionStatus(nil, id, aichangefaceimgmod.Processing, aichangefaceimgmod.StatusSubmit, bson.M{ + "updateAct": autoOperator, + "remark": "已自动提交", + }) + if err != nil || !transitioned { + return err + } + + notifyURL, notifyErr := appg.AICallbackURL("/api/web/admin/ai/change_face_img/callback") + if notifyErr != nil || order.OriginPic == "" || appg.Conf.URL.AiServer == "" { + return refundImage(id, "自动提交参数不完整") + } + req := changeFaceRequest{ + AppID: int(commod.KFK_APPID), + OrderNumApp: id.Hex(), + Title: order.ModTitle, + SourceImageURL: order.OriginPic, + NotifyURL: notifyURL, + } + if err := postChangeFace(req); err != nil { + log.Error("AI图片换脸自动提交失败", log.Any("id", id.Hex()), log.E(err)) + return refundImage(id, err.Error()) + } + return nil +} + +// SubmitUndress automatically approves and submits an AI undress order. +func SubmitUndress(id primitive.ObjectID) error { + order, err := aiUnDressmod.FindByID(nil, id) + if err != nil { + return err + } + transitioned, err := aiUnDressmod.TransitionStatus(nil, id, aiUnDressmod.Processing, aiUnDressmod.SubmitOrder, bson.M{ + "updateAct": autoOperator, + "remark": "已自动提交", + }) + if err != nil || !transitioned { + return err + } + + notifyURL, notifyErr := appg.AICallbackURL("/api/web/admin/ai/undress/callback") + if notifyErr != nil || len(order.OriginPics) == 0 || appg.Conf.URL.AiUndressServer == "" { + return refundUndress(id, "自动提交参数不完整") + } + req := aiUnDressmod.AiUndressOrderReq{ + AppId: int(commod.KFK_APPID), + FileUrl: order.OriginPics, + UserId: strconv.FormatUint(order.UID, 10), + AppOrderNum: id.Hex(), + NotifyUrl: notifyURL, + } + var resp aiUnDressmod.AiUndressOrderResp + statusCode, err := httputil.DefaultClientPostJsonWithResp( + &resp, + common.BindUrl(appg.Conf.URL.AiUndressServer, "/api/undress/prd/create_job"), + nil, + req, + ) + if err != nil || statusCode != 200 { + if err == nil { + err = fmt.Errorf("third-party HTTP status %d", statusCode) + } + log.Error("AI脱衣自动提交失败", log.Any("id", id.Hex()), log.E(err)) + return refundUndress(id, err.Error()) + } + return nil +} + +func postChangeFace(req changeFaceRequest) error { + var resp changeFaceResponse + statusCode, err := httputil.DefaultClientPostJsonWithResp( + &resp, + common.BindUrl(appg.Conf.URL.AiServer, "/api/changeface/prd/changeFaceByVideo"), + nil, + req, + ) + if err != nil { + return err + } + if statusCode != 200 { + return fmt.Errorf("third-party HTTP status %d", statusCode) + } + if resp.Code == 40000 { + return errors.New("third-party template is unavailable") + } + return nil +} + +func refundVideo(id primitive.ObjectID, remark string) error { + return appg.VideoDB.Trans(func(tool *db.MongoTool) error { + order, err := aichangefacemod.FindByID(tool, id) + if err != nil { + return err + } + transitioned, err := aichangefacemod.TransitionStatus(tool, id, aichangefacemod.StatusSubmit, aichangefacemod.StatusRefund, bson.M{ + "operator": autoOperator, + "remark": remark, + }) + if err != nil || !transitioned { + return err + } + if len(order.Discount) > 0 { + if _, err = backpackmod.UnuseManyGoods(tool, order.Discount); err != nil { + return err + } + } + if order.Coin == 0 && order.IncomeCoin == 0 { + return nil + } + wallet, err := walletmod.ReturnAmountAndIncome(tool, -order.Coin, -order.IncomeCoin, order.Uid) + if err != nil { + return err + } + totalCoin := order.Coin + order.IncomeCoin + return txnmod.InsertTransactionLog(tool, &txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: order.Uid, + Amount: totalCoin, + ActualAmount: float64(totalCoin), + TranType: txnmod.AiChangefaceDebitGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiChangefaceDebitGoldReturn), + Desc: fmt.Sprintf("AI换脸金币退返-%d", totalCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }) + }) +} + +func refundImage(id primitive.ObjectID, remark string) error { + return appg.VideoDB.Trans(func(tool *db.MongoTool) error { + order, err := aichangefaceimgmod.FindByID(tool, id) + if err != nil { + return err + } + transitioned, err := aichangefaceimgmod.TransitionStatus(tool, id, aichangefaceimgmod.StatusSubmit, aichangefaceimgmod.REFUND, bson.M{ + "updateAct": autoOperator, + "remark": remark, + }) + if err != nil || !transitioned { + return err + } + return refundImageBalance(tool, order) + }) +} + +func refundImageBalance(tool *db.MongoTool, order *aichangefaceimgmod.AiChangeFaceImg) error { + logs := make([]txnmod.TransactionLog, 0, 3) + if order.Count > 0 || order.IsFreeTimes { + count := order.Count + if count == 0 { + count = 1 + } + wallet, err := walletmod.DebitAiFreeTimes(tool, -count, order.UID) + if err != nil { + return err + } + logs = append(logs, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), UID: order.UID, Amount: count, ActualAmount: float64(count), + TranType: txnmod.AiChangeFaceImgDebitFreeTimesReturn.Key(), TranTypeInt: int64(txnmod.AiChangeFaceImgDebitFreeTimesReturn), + Desc: fmt.Sprintf("AI图片换脸免费次数退返[%d次]", count), RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if order.DebitAmountCoin > 0 || order.DebitIncomeCoin > 0 { + wallet, err := walletmod.ReturnAmountAndIncome(tool, -order.DebitAmountCoin, -order.DebitIncomeCoin, order.UID) + if err != nil { + return err + } + if order.DebitIncomeCoin > 0 { + logs = append(logs, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), UID: order.UID, Amount: order.DebitIncomeCoin, ActualAmount: float64(order.DebitIncomeCoin), + TranType: txnmod.AiChangeFaceImgReturnIncomeGold.Key(), TranTypeInt: int64(txnmod.AiChangeFaceImgReturnIncomeGold), + Desc: fmt.Sprintf("AI图片换脸收益金币退返-%d", order.DebitIncomeCoin), RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if order.DebitAmountCoin > 0 { + logs = append(logs, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), UID: order.UID, Amount: order.DebitAmountCoin, ActualAmount: float64(order.DebitAmountCoin), + TranType: txnmod.AiChangeFaceImgReturnGold.Key(), TranTypeInt: int64(txnmod.AiChangeFaceImgReturnGold), + Desc: fmt.Sprintf("AI图片换脸金币退返-%d", order.DebitAmountCoin), RealAmount: walletmod.GetRealAmount(wallet), + }) + } + } + if len(logs) == 0 { + return nil + } + return txnmod.InsertManyTransactionLog(tool, logs) +} + +func refundUndress(id primitive.ObjectID, remark string) error { + return appg.VideoDB.Trans(func(tool *db.MongoTool) error { + order, err := aiUnDressmod.FindByID(tool, id) + if err != nil { + return err + } + transitioned, err := aiUnDressmod.TransitionStatus(tool, id, aiUnDressmod.SubmitOrder, aiUnDressmod.REFUND, bson.M{ + "updateAct": autoOperator, + "remark": remark, + }) + if err != nil || !transitioned { + return err + } + return refundUndressBalance(tool, order) + }) +} + +func refundUndressBalance(tool *db.MongoTool, order *aiUnDressmod.AiUnDress) error { + logs := make([]txnmod.TransactionLog, 0, 3) + if order.Count > 0 || order.IsFreeTimes { + count := order.Count + if count == 0 { + count = 1 + } + wallet, err := walletmod.DebitAiFreeTimes(tool, -count, order.UID) + if err != nil { + return err + } + logs = append(logs, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), UID: order.UID, Amount: count, ActualAmount: float64(count), + TranType: txnmod.AiUndressDebitFreeTimesReturn.Key(), TranTypeInt: int64(txnmod.AiUndressDebitFreeTimesReturn), + Desc: fmt.Sprintf("AI脱衣免费次数退返[%d次]", count), RealAmount: walletmod.GetRealAmount(wallet), + }) + } + debitAmount := order.DebitAmountCoin + debitIncome := order.DebitIncomeCoin + if !order.IsFreeTimes && debitAmount == 0 && debitIncome == 0 { + debitAmount = order.Coin + } + if debitAmount > 0 || debitIncome > 0 { + wallet, err := walletmod.ReturnAmountAndIncome(tool, -debitAmount, -debitIncome, order.UID) + if err != nil { + return err + } + if debitIncome > 0 { + logs = append(logs, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), UID: order.UID, Amount: debitIncome, ActualAmount: float64(debitIncome), + TranType: txnmod.AiUndressDebitIncomeGoldReturn.Key(), TranTypeInt: int64(txnmod.AiUndressDebitIncomeGoldReturn), + Desc: fmt.Sprintf("AI脱衣收益金币退返-%d", debitIncome), RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if debitAmount > 0 { + logs = append(logs, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), UID: order.UID, Amount: debitAmount, ActualAmount: float64(debitAmount), + TranType: txnmod.AiUndressDebitGoldReturn.Key(), TranTypeInt: int64(txnmod.AiUndressDebitGoldReturn), + Desc: fmt.Sprintf("AI脱衣金币退返-%d", debitAmount), RealAmount: walletmod.GetRealAmount(wallet), + }) + } + } + if len(logs) == 0 { + return nil + } + return txnmod.InsertManyTransactionLog(tool, logs) +} diff --git a/app/service/aiplazaser/aiplaza.go b/app/service/aiplazaser/aiplaza.go new file mode 100755 index 0000000..2a429b0 --- /dev/null +++ b/app/service/aiplazaser/aiplaza.go @@ -0,0 +1,325 @@ +package aiplazaser + +import ( + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/cache/aiplazadata" + "91porn-server/models/commod" + "91porn-server/models/v/aiUnDressmod" + "91porn-server/models/v/aichangefaceimgmod" + "91porn-server/models/v/aichangefacemod" + "91porn-server/models/v/aiimagetovideomod" + "91porn-server/models/v/aiplazamod" + "91porn-server/models/v/aitexttoimagemod" + "fmt" + "go.mongodb.org/mongo-driver/bson/primitive" + + "go.mongodb.org/mongo-driver/bson" +) + +type AppQueryListReq struct { + Type *int `json:"type" form:"type"` // 1-ai图片换脸 2-ai视频换脸 3-ai脱衣 4-ai图生视频 5-ai绘画 不传则查看全部 + SortType int `json:"sortType" form:"sortType"` // 1-推荐 2-最新上架 3-最多观看 4-最多收藏 + commod.Page +} + +type AppListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []*aiplazamod.AiPlazaInfo `json:"list"` +} + +// GetList 获取列表 +func (p *AppQueryListReq) GetList() AppListRes { + var res AppListRes + var err error + filter := bson.M{ + "status": 1, + } + if p.Type != nil { + filter["type"] = p.Type + } + sort := bson.D{{"reviewAt", -1}} + switch p.SortType { + case 2: + // 最新上架 + sort = bson.D{{"reviewAt", -1}} + case 3: + // 最多观看 + sort = bson.D{{"viewCount", -1}, {"reviewAt", -1}} + case 4: + // 最多收藏 + sort = bson.D{{"collectCount", -1}, {"reviewAt", -1}} + default: // 1 + // 推荐 + //filter["reviewAt"] = bson.M{"$gt": carbon.NewCarbon().StartOfDay().SubDays(30).StdTime()} + sort = bson.D{{"sortCode", -1}, {"reviewAt", -1}} + } + + // 获取列表 + var data []aiplazamod.AiPlaza + data, res.Total, res.HasNext, err = aiplazadata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + log.Error("获取ai广场帖子列表数据错误", log.Any("Params", *p), log.E(err)) + return res + } + res.List = aiplazadata.FormatAppDataList(data) + + return res +} + +type AppQueryInfoReq struct { + ID string `json:"id" form:"id"` // id +} + +type AppQueryInfoResp struct { + Detail *aiplazamod.AiPlazaInfo +} + +// GetInfo 获取详情 +func (p *AppQueryInfoReq) GetInfo(uid uint64) (res AppQueryInfoResp, err error) { + oid, err := primitive.ObjectIDFromHex(p.ID) + if err != nil { + return res, err + } + + var item aiplazamod.AiPlaza + item, err = aiplazadata.GetInfoFromCache(oid) + if err != nil { + log.Error("获取ai广场帖子详情数据错误", log.Any("ID", p.ID), log.E(err)) + return + } + res.Detail = aiplazadata.FormatAppData(uid, item) + return +} + +type ShareReq struct { + Type int `json:"type" form:"type" binding:"required"` // 1-ai图片换脸 2-ai视频换脸 3-ai脱衣 4-ai图生视频 5-ai绘画 + OrderId primitive.ObjectID `json:"orderId" form:"orderId" binding:"required"` + Title string `json:"title" form:"title"` +} + +// Create 发布数据 +func (p *ShareReq) Create(uid uint64) error { + // 根据类型获取订单 + switch p.Type { + case 1: + // ai图片换脸 + return p.shareAiImageChangeFace(uid) + case 2: + // ai视频换脸 + return p.shareAiVideoChangeFace(uid) + case 3: + // ai脱衣 + return p.shareAiUndress(uid) + case 4: + // ai图生视频 + return p.shareAiImageToVideo(uid) + case 5: + // ai绘画 + return p.shareAiTextToImage(uid) + default: + // 其他类型的不处理 + return nil + } +} + +func (p *ShareReq) shareAiImageChangeFace(uid uint64) (err error) { + // ai图片换脸 + aichangeFaceInfo, err := aichangefaceimgmod.FindByID(nil, p.OrderId) + if aichangeFaceInfo == nil && err != nil { + return stderr.ErrDbQueryError + } + if aichangeFaceInfo.UID != uid || aichangeFaceInfo.Status != aichangefaceimgmod.SUCCESS { + return stderr.ErrDbQueryError + } + // 修改状态 + if err = aichangefaceimgmod.Update(nil, p.OrderId, bson.M{ + "shareStatus": 1, + "shareTitle": p.Title, + }); err != nil { + return stderr.ErrDbUpdateError + } + + // 新增一个分享 + alPlaza := aiplazamod.AiPlaza{ + Type: 1, + OrderId: aichangeFaceInfo.ID, + Template: aichangeFaceInfo.MId, + Uid: aichangeFaceInfo.UID, + Title: p.Title, + OriginalImage: aichangeFaceInfo.ModPic, + OriginalVideo: "", + OriginContent: "", + GenerateVideo: "", + Status: aiplazamod.DefaultStatus, + } + if len(aichangeFaceInfo.NewPic) > 0 { + alPlaza.GenerateImage = aichangeFaceInfo.NewPic[0] + } + _, err = aiplazamod.Insert(nil, alPlaza) + if err != nil { + log.Error("新增ai广场帖子失败", log.Any("alPlaza", alPlaza), log.E(err)) + return + } + return +} + +func (p *ShareReq) shareAiVideoChangeFace(uid uint64) (err error) { + //查询该笔订单是否存在 + aichangeFaceInfo, err := aichangefacemod.FindByID(nil, p.OrderId) + if aichangeFaceInfo == nil && err != nil { + return stderr.ErrDbQueryError + } + if aichangeFaceInfo.Uid != uid || aichangeFaceInfo.Status != aichangefacemod.StatusComplete { + return stderr.ErrDbQueryError + } + if err = aichangefacemod.Update(nil, p.OrderId, bson.M{"$set": bson.M{ + "shareStatus": 1, + "shareTitle": p.Title, + }}); err != nil { + return stderr.ErrDbUpdateError + } + // 新增一个分享 + alPlaza := aiplazamod.AiPlaza{ + Type: 2, + OrderId: aichangeFaceInfo.ID, + Template: aichangeFaceInfo.VidID.Hex(), + Uid: aichangeFaceInfo.Uid, + Title: p.Title, + OriginalImage: "", + OriginalVideo: aichangeFaceInfo.ModVideo, + OriginalVideoCover: aichangeFaceInfo.ModCover, + OriginContent: "", + GenerateVideo: aichangeFaceInfo.Url, + Status: aiplazamod.DefaultStatus, + } + if len(aichangeFaceInfo.Pic) > 0 { + alPlaza.GenerateVideoCover = aichangeFaceInfo.Pic[0] + } + _, err = aiplazamod.Insert(nil, alPlaza) + if err != nil { + log.Error("新增ai广场帖子失败", log.Any("alPlaza", alPlaza), log.E(err)) + return + } + return +} + +func (p *ShareReq) shareAiUndress(uid uint64) (err error) { + data, err := aiUnDressmod.FindDataByID(p.OrderId.Hex()) + if err != nil { + return stderr.ErrDbQueryError + } + if data.UID != uid || data.Status != aiUnDressmod.SUCCESS { + return stderr.ErrDbQueryError + } + // 修改订单状态 + if err = aiUnDressmod.Edit(nil, bson.M{"_id": p.OrderId}, bson.M{"$set": bson.M{ + "shareStatus": 1, + "shareTitle": p.Title, + }}); err != nil { + log.Error(fmt.Sprintf("Handle AiUnDress CallBackOrder aiUnDressmod.Edit error:%+v:", err), log.Any("uid", uid)) + return stderr.ErrDbUpdateError + } + + // 新增一个分享 + alPlaza := aiplazamod.AiPlaza{ + Type: 3, + OrderId: data.ID, + Template: "", + Uid: data.UID, + Title: p.Title, + OriginalImage: data.OriginPic, + OriginalVideo: "", + OriginContent: "", + //GenerateImage: data.NewPic[0], + GenerateVideo: "", + Status: aiplazamod.DefaultStatus, + } + if len(data.NewPic) > 0 { + alPlaza.GenerateImage = data.NewPic[0] + } + _, err = aiplazamod.Insert(nil, alPlaza) + if err != nil { + log.Error("新增ai广场帖子失败", log.Any("alPlaza", alPlaza), log.E(err)) + return + } + return +} + +func (p *ShareReq) shareAiImageToVideo(uid uint64) (err error) { + // 查询该笔订单是否存在 + data, err := aiimagetovideomod.GetInfo(p.OrderId) + if err != nil { + return stderr.ErrDbQueryError + } + if data.UID != uid || data.Status != int(aiimagetovideomod.StatusGenerationSuccess) { + return stderr.ErrDbQueryError + } + // 修改订单状态 + if _, err = aiimagetovideomod.UpdateByID(nil, p.OrderId, bson.M{ + "shareStatus": 1, + "shareTitle": p.Title, + }); err != nil { + return stderr.ErrDbUpdateError + } + + // 新增一个分享 + alPlaza := aiplazamod.AiPlaza{ + Type: 4, + OrderId: data.ID, + Template: "", + Uid: data.UID, + Title: p.Title, + OriginalImage: data.ImgUrl, + OriginalVideo: "", + OriginContent: "", + GenerateImage: data.NewImgUrl, + GenerateVideo: "", + Status: aiplazamod.DefaultStatus, + } + _, err = aiplazamod.Insert(nil, alPlaza) + if err != nil { + log.Error("新增ai广场帖子失败", log.Any("alPlaza", alPlaza), log.E(err)) + return + } + return +} + +func (p *ShareReq) shareAiTextToImage(uid uint64) (err error) { + // 查询该笔订单是否存在 + data, err := aitexttoimagemod.GetInfo(p.OrderId) + if err != nil { + return stderr.ErrDbQueryError + } + if data.UID != uid || data.Status != int(aitexttoimagemod.StatusGenerationSuccess) { + return stderr.ErrDbQueryError + } + // 修改订单状态 + if _, err = aitexttoimagemod.UpdateByID(nil, p.OrderId, bson.M{ + "shareStatus": 1, + "shareTitle": p.Title, + }); err != nil { + return stderr.ErrDbUpdateError + } + // 新增一个分享 + alPlaza := aiplazamod.AiPlaza{ + Type: 5, + OrderId: data.ID, + Template: "", + Uid: data.UID, + Title: p.Title, + OriginalImage: "", + OriginalVideo: "", + OriginContent: data.Text, + GenerateImage: data.NewImgUrl, + GenerateVideo: "", + Status: aiplazamod.DefaultStatus, + } + _, err = aiplazamod.Insert(nil, alPlaza) + if err != nil { + log.Error("新增ai广场帖子失败", log.Any("alPlaza", alPlaza), log.E(err)) + return + } + return +} diff --git a/app/service/aiser/ai.go b/app/service/aiser/ai.go new file mode 100644 index 0000000..57a4242 --- /dev/null +++ b/app/service/aiser/ai.go @@ -0,0 +1,244 @@ +package aiser + +import ( + "context" + "errors" + "fmt" + "math" + "time" + + "91porn-server/app/appg" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/laosiji_app" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/models/commod" + "91porn-server/models/v/fundtransferlogmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + + "github.com/go-redsync/redsync/v4" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const aiFundLockExpiry = 30 * time.Second + +type GetAuthURLResp struct { + URL string `json:"url"` +} + +type transferDependencies struct { + findLatest func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) + findLatestOut func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) + getWallet func(uint64) (*walletmod.Wallet, error) + findUser func(uint64) (*usermod.User, error) + getAuthURL func(context.Context, laosiji_app.GetAiMateURLReq) (laosiji_app.GetAiMateURLResp, error) + bringOut func(context.Context, laosiji_app.AiMateBringOutReq) (laosiji_app.AiMateBringOutResp, error) + debitAndLog func(uint64, int64, time.Time) error + creditAndLog func(uint64, int64, float64, string, time.Time) error + now func() time.Time +} + +func defaultTransferDependencies() transferDependencies { + return transferDependencies{ + findLatest: fundtransferlogmod.FindLatestByUIDAndCategory, + findLatestOut: fundtransferlogmod.FindLatestOutByUIDAndCategory, + getWallet: walletmod.GetWallet, + findUser: usermod.FindUserByUID, + getAuthURL: laosiji_app.GetAiMateURL, + bringOut: laosiji_app.AiMateBringOut, + debitAndLog: debitAndLog, + creditAndLog: creditAndLog, + now: time.Now, + } +} + +func acquireLock(uid uint64) (func(), error) { + if appg.Redis == nil { + return nil, errors.New("redis is unavailable") + } + lock := redis.BuildLock( + appg.Redis, + redisconst.GetAiFundLockKey(uid), + redsync.WithExpiry(aiFundLockExpiry), + redsync.WithTries(1), + ) + if err := lock.Lock(); err != nil { + return nil, err + } + return func() { + if _, err := lock.Unlock(); err != nil { + log.Warn("unlock AI fund lock failed", log.Any("uid", uid), log.E(err)) + } + }, nil +} + +// GetAuthURL 先回收上一次第三方余额,再将主钱包金币上分并返回AI女友授权地址。 +func GetAuthURL(ctx context.Context, uid uint64) (GetAuthURLResp, error) { + unlock, err := acquireLock(uid) + if err != nil { + return GetAuthURLResp{}, errors.New("操作频繁,请稍后再试") + } + defer unlock() + return getAuthURL(ctx, uid, defaultTransferDependencies()) +} + +func getAuthURL(ctx context.Context, uid uint64, deps transferDependencies) (GetAuthURLResp, error) { + if err := settleDown(ctx, uid, deps); err != nil { + return GetAuthURLResp{}, fmt.Errorf("下分失败: %w", err) + } + + wallet, err := deps.getWallet(uid) + if err != nil { + return GetAuthURLResp{}, fmt.Errorf("获取钱包失败: %w", err) + } + var amount int64 + if wallet != nil { + amount = wallet.Amount + } + + var remainder float64 + lastOut, err := deps.findLatestOut(uid, fundtransferlogmod.CategoryAiGirlfriend) + if err != nil { + return GetAuthURLResp{}, fmt.Errorf("获取下分余数失败: %w", err) + } + if lastOut != nil { + remainder = lastOut.Remainder + } + + nickname, avatar := "用户", "" + if user, findErr := deps.findUser(uid); findErr == nil && user != nil { + if user.Name != "" { + nickname = user.Name + } + avatar = user.Portrait + } + username := laosiji_app.GetUserName(appg.Conf.Base.Env, commod.KFK_APPID, uid) + authResp, err := deps.getAuthURL(ctx, laosiji_app.GetAiMateURLReq{ + Username: username, + Nickname: nickname, + Asset: assetAmount(amount, remainder), + Currency: "CNY", + Theme: "dark", + UserAvatar: avatar, + }) + if err != nil { + return GetAuthURLResp{}, fmt.Errorf("获取AI女友地址失败: %w", err) + } + if authResp.AuthURL == "" { + return GetAuthURLResp{}, errors.New("AI女友地址为空") + } + + if amount > 0 { + if err = deps.debitAndLog(uid, amount, deps.now()); err != nil { + return GetAuthURLResp{}, fmt.Errorf("上分扣除金币失败: %w", err) + } + } + return GetAuthURLResp{URL: authResp.AuthURL}, nil +} + +// TrySettleDown 在钱包查询前尝试将AI女友剩余余额下分回主钱包。 +func TrySettleDown(ctx context.Context, uid uint64) error { + unlock, err := acquireLock(uid) + if err != nil { + return errors.New("操作频繁,请稍后再试") + } + defer unlock() + return settleDown(ctx, uid, defaultTransferDependencies()) +} + +func settleDown(ctx context.Context, uid uint64, deps transferDependencies) error { + latest, err := deps.findLatest(uid, fundtransferlogmod.CategoryAiGirlfriend) + if err != nil { + return err + } + if latest == nil || latest.FundType != fundtransferlogmod.FundTypeIn { + return nil + } + + resp, err := deps.bringOut(ctx, laosiji_app.AiMateBringOutReq{ + Username: laosiji_app.GetUserName(appg.Conf.Base.Env, commod.KFK_APPID, uid), + }) + if err != nil { + return err + } + returnAmount, remainder, err := balanceToWallet(resp.Balance) + if err != nil { + return err + } + desc := "AI女友下分" + if returnAmount == 0 { + desc += "-金币为0" + } + return deps.creditAndLog(uid, returnAmount, remainder, desc, deps.now()) +} + +func assetAmount(amount int64, remainder float64) string { + amountDecimal := decimal.NewFromInt(amount).Div(decimal.NewFromInt(10)) + return amountDecimal.Add(decimal.NewFromFloat(remainder)).Round(2).StringFixed(2) +} + +func balanceToWallet(balance string) (int64, float64, error) { + value, err := decimal.NewFromString(balance) + if err != nil { + return 0, 0, fmt.Errorf("无效的第三方余额: %w", err) + } + if value.IsNegative() { + return 0, 0, errors.New("第三方余额不能为负数") + } + coins := value.Mul(decimal.NewFromInt(10)).Floor() + if coins.GreaterThan(decimal.NewFromInt(math.MaxInt64)) { + return 0, 0, errors.New("第三方余额超出范围") + } + returnAmount := coins.IntPart() + remainderDecimal := value.Sub(decimal.NewFromInt(returnAmount).Div(decimal.NewFromInt(10))).Round(2) + remainder, _ := remainderDecimal.Float64() + return returnAmount, remainder, nil +} + +func debitAndLog(uid uint64, amount int64, now time.Time) error { + if appg.VideoDB == nil { + return errors.New("video database is unavailable") + } + return appg.VideoDB.Trans(func(t *db.MongoTool) error { + wallet, err := walletmod.DebitAmount(t, amount, uid) + if err != nil { + return err + } + return fundtransferlogmod.Insert(t, &fundtransferlogmod.FundTransferLog{ + ID: primitive.NewObjectID(), + UID: uid, + Category: fundtransferlogmod.CategoryAiGirlfriend, + FundType: fundtransferlogmod.FundTypeIn, + Amount: amount, + Balance: wallet.Amount, + Desc: "AI女友上分", + CreatedAt: now, + }) + }) +} + +func creditAndLog(uid uint64, amount int64, remainder float64, desc string, now time.Time) error { + if appg.VideoDB == nil { + return errors.New("video database is unavailable") + } + return appg.VideoDB.Trans(func(t *db.MongoTool) error { + wallet, err := walletmod.CreditAmount(t, amount, uid) + if err != nil { + return err + } + return fundtransferlogmod.Insert(t, &fundtransferlogmod.FundTransferLog{ + ID: primitive.NewObjectID(), + UID: uid, + Category: fundtransferlogmod.CategoryAiGirlfriend, + FundType: fundtransferlogmod.FundTypeOut, + Amount: amount, + Balance: wallet.Amount, + Remainder: remainder, + Desc: desc, + CreatedAt: now, + }) + }) +} diff --git a/app/service/aiser/ai_test.go b/app/service/aiser/ai_test.go new file mode 100644 index 0000000..d7a74d1 --- /dev/null +++ b/app/service/aiser/ai_test.go @@ -0,0 +1,219 @@ +package aiser + +import ( + "context" + "errors" + "testing" + "time" + + "91porn-server/app/appg" + "91porn-server/common/laosiji_app" + "91porn-server/models/v/fundtransferlogmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" +) + +func TestGetAuthURLTransfersWalletAmount(t *testing.T) { + originalConfig := appg.Conf + appg.Conf = &appg.GlobalConfig{} + appg.Conf.Base.Env = "test" + t.Cleanup(func() { appg.Conf = originalConfig }) + + var ( + gotAsset string + gotDebit int64 + gotUser string + debitTime time.Time + ) + now := time.Date(2026, 7, 27, 12, 0, 0, 0, time.UTC) + deps := transferDependencies{ + findLatest: func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) { + return nil, nil + }, + findLatestOut: func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) { + return &fundtransferlogmod.FundTransferLog{Remainder: 0.04}, nil + }, + getWallet: func(uint64) (*walletmod.Wallet, error) { + return &walletmod.Wallet{Amount: 123}, nil + }, + findUser: func(uint64) (*usermod.User, error) { + return &usermod.User{Name: "测试", Portrait: "avatar"}, nil + }, + getAuthURL: func(_ context.Context, req laosiji_app.GetAiMateURLReq) (laosiji_app.GetAiMateURLResp, error) { + gotAsset = req.Asset + gotUser = req.Username + return laosiji_app.GetAiMateURLResp{AuthURL: "https://example.com/auth"}, nil + }, + bringOut: func(context.Context, laosiji_app.AiMateBringOutReq) (laosiji_app.AiMateBringOutResp, error) { + t.Fatal("bringOut must not be called without pending transfer") + return laosiji_app.AiMateBringOutResp{}, nil + }, + debitAndLog: func(_ uint64, amount int64, at time.Time) error { + gotDebit, debitTime = amount, at + return nil + }, + creditAndLog: func(uint64, int64, float64, string, time.Time) error { + t.Fatal("creditAndLog must not be called") + return nil + }, + now: func() time.Time { return now }, + } + + resp, err := getAuthURL(context.Background(), 99, deps) + if err != nil { + t.Fatalf("getAuthURL() error = %v", err) + } + if resp.URL != "https://example.com/auth" { + t.Fatalf("URL = %q", resp.URL) + } + if gotAsset != "12.34" { + t.Fatalf("asset = %q, want 12.34", gotAsset) + } + if gotUser != "TEST-204_99" { + t.Fatalf("username = %q", gotUser) + } + if gotDebit != 123 || !debitTime.Equal(now) { + t.Fatalf("debit = %d at %s", gotDebit, debitTime) + } +} + +func TestGetAuthURLDoesNotDebitWhenThirdPartyFails(t *testing.T) { + originalConfig := appg.Conf + appg.Conf = &appg.GlobalConfig{} + t.Cleanup(func() { appg.Conf = originalConfig }) + + deps := transferDependencies{ + findLatest: func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) { + return nil, nil + }, + findLatestOut: func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) { + return nil, nil + }, + getWallet: func(uint64) (*walletmod.Wallet, error) { + return &walletmod.Wallet{Amount: 100}, nil + }, + findUser: func(uint64) (*usermod.User, error) { return nil, nil }, + getAuthURL: func(context.Context, laosiji_app.GetAiMateURLReq) (laosiji_app.GetAiMateURLResp, error) { + return laosiji_app.GetAiMateURLResp{}, errors.New("remote failed") + }, + debitAndLog: func(uint64, int64, time.Time) error { + t.Fatal("debitAndLog must not be called after remote failure") + return nil + }, + now: time.Now, + } + if _, err := getAuthURL(context.Background(), 99, deps); err == nil { + t.Fatal("getAuthURL() error = nil") + } +} + +func TestGetAuthURLSettlesPendingTransferBeforeNewTransfer(t *testing.T) { + originalConfig := appg.Conf + appg.Conf = &appg.GlobalConfig{} + appg.Conf.Base.Env = "test" + t.Cleanup(func() { appg.Conf = originalConfig }) + + steps := make([]string, 0, 5) + deps := transferDependencies{ + findLatest: func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) { + steps = append(steps, "find-pending") + return &fundtransferlogmod.FundTransferLog{FundType: fundtransferlogmod.FundTypeIn}, nil + }, + bringOut: func(context.Context, laosiji_app.AiMateBringOutReq) (laosiji_app.AiMateBringOutResp, error) { + steps = append(steps, "bring-out") + return laosiji_app.AiMateBringOutResp{Balance: "1.00"}, nil + }, + creditAndLog: func(uint64, int64, float64, string, time.Time) error { + steps = append(steps, "credit") + return nil + }, + getWallet: func(uint64) (*walletmod.Wallet, error) { + steps = append(steps, "wallet") + return &walletmod.Wallet{Amount: 20}, nil + }, + findLatestOut: func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) { + return nil, nil + }, + findUser: func(uint64) (*usermod.User, error) { return nil, nil }, + getAuthURL: func(context.Context, laosiji_app.GetAiMateURLReq) (laosiji_app.GetAiMateURLResp, error) { + steps = append(steps, "auth") + return laosiji_app.GetAiMateURLResp{AuthURL: "https://example.com/auth"}, nil + }, + debitAndLog: func(uint64, int64, time.Time) error { return nil }, + now: time.Now, + } + + if _, err := getAuthURL(context.Background(), 99, deps); err != nil { + t.Fatalf("getAuthURL() error = %v", err) + } + want := []string{"find-pending", "bring-out", "credit", "wallet", "auth"} + if len(steps) != len(want) { + t.Fatalf("steps = %v, want %v", steps, want) + } + for index := range want { + if steps[index] != want[index] { + t.Fatalf("steps = %v, want %v", steps, want) + } + } +} + +func TestSettleDownCreditsConvertedBalance(t *testing.T) { + originalConfig := appg.Conf + appg.Conf = &appg.GlobalConfig{} + appg.Conf.Base.Env = "prod" + t.Cleanup(func() { appg.Conf = originalConfig }) + + var ( + gotAmount int64 + gotRemainder float64 + gotDesc string + gotUsername string + ) + deps := transferDependencies{ + findLatest: func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) { + return &fundtransferlogmod.FundTransferLog{FundType: fundtransferlogmod.FundTypeIn}, nil + }, + bringOut: func(_ context.Context, req laosiji_app.AiMateBringOutReq) (laosiji_app.AiMateBringOutResp, error) { + gotUsername = req.Username + return laosiji_app.AiMateBringOutResp{Balance: "1.23"}, nil + }, + creditAndLog: func(_ uint64, amount int64, remainder float64, desc string, _ time.Time) error { + gotAmount, gotRemainder, gotDesc = amount, remainder, desc + return nil + }, + now: time.Now, + } + if err := settleDown(context.Background(), 88, deps); err != nil { + t.Fatalf("settleDown() error = %v", err) + } + if gotUsername != "JHA-204_88" { + t.Fatalf("username = %q", gotUsername) + } + if gotAmount != 12 || gotRemainder != 0.03 || gotDesc != "AI女友下分" { + t.Fatalf("credit amount=%d remainder=%v desc=%q", gotAmount, gotRemainder, gotDesc) + } +} + +func TestBalanceToWallet(t *testing.T) { + tests := []struct { + balance string + amount int64 + remainder float64 + wantErr bool + }{ + {balance: "0", amount: 0, remainder: 0}, + {balance: "1.20", amount: 12, remainder: 0}, + {balance: "1.29", amount: 12, remainder: 0.09}, + {balance: "-1", wantErr: true}, + {balance: "invalid", wantErr: true}, + } + for _, test := range tests { + amount, remainder, err := balanceToWallet(test.balance) + if (err != nil) != test.wantErr { + t.Fatalf("balanceToWallet(%q) error = %v", test.balance, err) + } + if amount != test.amount || remainder != test.remainder { + t.Fatalf("balanceToWallet(%q) = (%d,%v), want (%d,%v)", test.balance, amount, remainder, test.amount, test.remainder) + } + } +} diff --git a/app/service/aitemplatemoduleser/aitemplatemodule.go b/app/service/aitemplatemoduleser/aitemplatemodule.go new file mode 100755 index 0000000..b298976 --- /dev/null +++ b/app/service/aitemplatemoduleser/aitemplatemodule.go @@ -0,0 +1,87 @@ +package aitemplatemoduleser + +import ( + "91porn-server/common/log" + "91porn-server/models/cache/aitemplatemoduledata" + "91porn-server/models/commod" + "91porn-server/models/v/aitemplatemodulemod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AppQueryListReq struct { + commod.Page +} + +type AppListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []*aitemplatemodulemod.AiTemplateModuleInfo `json:"list"` +} + +// GetList 获取列表 +func (p *AppQueryListReq) GetList() AppListRes { + var res AppListRes + var err error + filter := bson.M{} + + sort := bson.D{{"_id", -1}} + // 获取列表 + var data []aitemplatemodulemod.AiTemplateModule + data, res.Total, res.HasNext, err = aitemplatemoduledata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + log.Error("获取AI模版模块列表列表数据错误", log.Any("Params", *p), log.E(err)) + return res + } + res.List = aitemplatemoduledata.FormatAppDataList(data) + + return res +} + +type AppQueryAllListReq struct { +} + +type AppAllListRes struct { + List []*aitemplatemodulemod.AiTemplateModuleInfo `json:"list"` +} + +// GetList 获取列表 +func (p *AppQueryAllListReq) GetList() AppAllListRes { + var res AppAllListRes + var err error + filter := bson.M{"status": 1} + sort := bson.D{{"sortNum", -1}} + // 获取列表 + data, err := aitemplatemodulemod.GetAll(filter, sort) + if err != nil { + log.Error("获取AI模版模块列表列表数据错误", log.Any("Params", *p), log.E(err)) + return res + } + + res.List = aitemplatemoduledata.FormatAppDataList(data) + return res +} + +type AppQueryInfoReq struct { + ID string `json:"id" form:"id"` // id +} +type AppQueryInfoRes = *aitemplatemodulemod.AiTemplateModuleInfo + +// GetInfo 获取详情 +func (p *AppQueryInfoReq) GetInfo() (res AppQueryInfoRes, err error) { + oid, err := primitive.ObjectIDFromHex(p.ID) + if err != nil { + return res, err + } + + var item aitemplatemodulemod.AiTemplateModule + item, err = aitemplatemoduledata.GetInfoFromCache(oid) + if err != nil { + log.Error("获取AI模版模块列表详情数据错误", log.Any("ID", p.ID), log.E(err)) + return + } + + res = aitemplatemoduledata.FormatAppData(item) + return +} diff --git a/app/service/annouser/annou.go b/app/service/annouser/annou.go new file mode 100644 index 0000000..326af79 --- /dev/null +++ b/app/service/annouser/annou.go @@ -0,0 +1,63 @@ +package annouser + +import ( + "91porn-server/common" + "91porn-server/common/localcache" + "91porn-server/models/v/adsmod" + "91porn-server/models/v/annoumod" + "fmt" + "time" + + "91porn-server/common/log" +) + +// GetAnnouList 获取指定类型的公告 +func GetAnnouList(t int) (out *Annou, err error) { + log.Info(fmt.Sprintf("[Params-%s]", "GetAnnouList"), log.Any("type", t)) + cfg, ok := localcache.C.Get(adsmod.GetAnnounceByTypeList) + if ok { + return cfg.(*Annou), nil + } + if out, err = getAnnouList(); err != nil { + log.Error(fmt.Sprintf("[HTTP-%s]==> request fail error:%v:", "GetAnnouList", err), log.Any("type", t)) + return + } + common.Go(func() { + localcache.C.Set(adsmod.GetAnnounceByTypeList, out, 10*time.Minute) + }) + return +} + +// AnnouList 公告列表 +func AnnouList() (out []*Annou, err error) { + log.Info(fmt.Sprintf("[Params-%s]", "GetAnnouList")) + cfg, ok := localcache.C.Get(adsmod.GetAnnounceList) + if ok { + return cfg.([]*Annou), nil + } + if out, err = annouList(); err != nil { + log.Error(fmt.Sprintf("[HTTP-%s]==> request fail error:%v:", "GetAnnouList", err)) + return + } + common.Go(func() { + localcache.C.Set(adsmod.GetAnnounceList, out, 5*time.Minute) + }) + return +} + +// MsgAnnouList 消息模块公告列表 +func MsgAnnouList(req annoumod.MsgListReq) (out *MsgAnnousResp, err error) { + log.Info(fmt.Sprintf("[Params-%s]", "MsgAnnouList")) + cfg, ok := localcache.C.Get(adsmod.GetMsgAnnounceList) + if ok { + return cfg.(*MsgAnnousResp), nil + } + if out, err = annouLists(req.PageNumber, req.PageSize); err != nil { + log.Error(fmt.Sprintf("[HTTP-%s]==> request fail error:%v:", "MsgAnnouList", err)) + return + } + common.Go(func() { + localcache.C.Set(adsmod.GetMsgAnnounceList, out, 10*time.Minute) + }) + return +} diff --git a/app/service/annouser/http.go b/app/service/annouser/http.go new file mode 100644 index 0000000..dd7ce8f --- /dev/null +++ b/app/service/annouser/http.go @@ -0,0 +1,56 @@ +package annouser + +import ( + "91porn-server/app/appg" + "91porn-server/common/httputil" + "errors" + "net/http" +) + +// 从数据中心获取数据 +func getAnnouList() (*Annou, error) { + var ( + url string = appg.Conf.URL.DataCentreURL + GetAnnouListRouter + params QueryAllCond = QueryAllCond{AppId} + out CommonResponse + ) + if code, err := httputil.DefaultClientGetWithResp(&out, url, nil, params); err != nil { + return nil, err + } else if code != http.StatusOK { + return nil, errors.New(NetWorkError) + } + if len(out.Data) > 0 { + return out.Data[0], nil + } + return nil, nil +} + +// 从数据中心获取数据 +func annouList() ([]*Annou, error) { + var ( + url string = appg.Conf.URL.DataCentreURL + GetAnnouListRouter + params QueryAllCond = QueryAllCond{AppId} + out CommonResponse + ) + if code, err := httputil.DefaultClientGetWithResp(&out, url, nil, params); err != nil { + return nil, err + } else if code != http.StatusOK { + return nil, errors.New(NetWorkError) + } + return out.Data, nil +} + +// 从数据中心分页获取数据 +func annouLists(pageNumber, pageSize int) (*MsgAnnousResp, error) { + var ( + url string = appg.Conf.URL.DataCentreURL + GetAnnouListsRouter + params QueryByPageCond = QueryByPageCond{AppId, pageNumber, pageSize} + out MsgAnnousResponse + ) + if code, err := httputil.DefaultClientGetWithResp(&out, url, nil, params); err != nil { + return nil, err + } else if code != http.StatusOK { + return nil, errors.New(NetWorkError) + } + return out.Data, nil +} diff --git a/app/service/annouser/type.go b/app/service/annouser/type.go new file mode 100644 index 0000000..f794f6b --- /dev/null +++ b/app/service/annouser/type.go @@ -0,0 +1,55 @@ +package annouser + +const ( + GetAnnouListRouter = "/api/stat/announce/get" + GetAnnouListsRouter = "/api/stat/announce/out/list" + NetWorkError = "网络错误" + AppId = 31 +) + +type QueryAllCond struct { + AppId int32 `json:"appId" binding:"required"` +} + +type QueryByPageCond struct { + AppId int32 `json:"appId" binding:"required"` + PageNumber int `form:"pageNum" json:"pageNum"` + PageSize int `form:"pageSize" json:"pageSize"` +} + +// Annou 公告模型 +type Annou struct { + //id + ID string `json:"id"` + //名字 + Title string `json:"title"` + //内容 + Content string `json:"content"` + //广告图 + Cover string `json:"cover"` + //链接地址 + Href string `json:"href"` + //类型 + Type int64 `json:"type"` +} + +type CommonResponse struct { + Code int64 `json:"code"` + Data []*Annou `json:"data"` + Msg string `json:"msg"` + Time string `json:"time"` + Tip string `json:"tip"` +} + +type MsgAnnousResponse struct { + Code int64 `json:"code"` + Data *MsgAnnousResp `json:"data"` + Msg string `json:"msg"` + Time string `json:"time"` + Tip string `json:"tip"` +} + +type MsgAnnousResp struct { + Total int64 `json:"total"` + List []*Annou `json:"list"` +} diff --git a/app/service/backpackser/backpack.go b/app/service/backpackser/backpack.go new file mode 100644 index 0000000..8405c64 --- /dev/null +++ b/app/service/backpackser/backpack.go @@ -0,0 +1,15 @@ +package backpackser + +import ( + "91porn-server/common/stderr" + "91porn-server/models/v/backpackmod" +) + +// 获取优惠卷列表 +func GetCouponList(uid uint64, types int, status int, limit, page int64) (interface{}, stderr.Code) { + list, err := backpackmod.GetCouponList(uid, types, status, limit, page) + if err != nil { + return nil, stderr.ErrDbQueryError + } + return list, stderr.Success +} diff --git a/app/service/checkinser/checkin.go b/app/service/checkinser/checkin.go new file mode 100644 index 0000000..902152c --- /dev/null +++ b/app/service/checkinser/checkin.go @@ -0,0 +1,644 @@ +package checkinser + +import ( + "strconv" + "time" + + "91porn-server/app/appg" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/timeutil" + "91porn-server/models/v/checkinconfigmod" + "91porn-server/models/v/checkinprizemod" + "91porn-server/models/v/prizemod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usercheckinmod" + "91porn-server/models/v/usermod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const ( + checkinLimitKeyPrefix = "checkin::limit::" + checkinLimitExpire = 3 * time.Second +) + +// CheckinInfo 签到信息 +type CheckinInfo struct { + TodayChecked bool `json:"todayChecked"` + ContinuouslyDays int64 `json:"continuouslyDays"` + CumulativeDays int64 `json:"cumulativeDays"` + DoubleReward bool `json:"doubleReward"` +} + +// CheckinConfigResp 签到配置返回 +type CheckinConfigResp struct { + Enable bool `json:"enable"` + Description string `json:"description"` + BackgroundImage string `json:"backgroundImage"` + IntegerExchangeList []checkinconfigmod.GiftItem `json:"integerExchangeList"` +} + +// PrizeBrief 奖品摘要 +type PrizeBrief struct { + PrizeImage string `json:"prizeImage"` + PrizeType prizemod.PrizeType `json:"prizeType"` + PrizeCount int64 `json:"prizeCount"` + PrizeTitle string `json:"prizeTitle"` +} + +type PrizeBriefList []*PrizeBrief + +// UserCheckinResp 用户签到返回 +type UserCheckinResp struct { + Message string `json:"message"` + Checkin CheckinInfo `json:"checkin"` + Prizes PrizeBriefList `json:"prizes"` + PrizeVideo string `json:"prizeVideo"` +} + +// CheckinDto 签到奖品DTO +type CheckinDto struct { + checkinprizemod.CheckinPrize `bson:",inline"` + Score int64 `json:"score"` + PrizeType prizemod.PrizeType `json:"prizeType"` + IsReceive bool `json:"isReceive"` + IsCheckedIn bool `json:"isCheckedIn"` + CanClaim bool `json:"canClaim"` + IsExpired bool `json:"isExpired"` +} + +// GetCheckinPrizeResp 获取签到奖品返回 +type GetCheckinPrizeResp struct { + Prizes []CheckinDto `json:"prizes"` + BigPrizes []CheckinDto `json:"bigPrizes"` + Checkin CheckinInfo `json:"checkin"` + Config CheckinConfigResp `json:"config"` +} + +// getCheckinConfig 获取签到配置 +func getCheckinConfig() *checkinconfigmod.CheckinConfig { + cfg, _ := checkinconfigmod.FindOne(bson.M{}) + return cfg +} + +// giveCheckinPrizeById 发放签到奖品 +func giveCheckinPrizeById(uid uint64, checkInPrize *checkinprizemod.CheckinPrize, isVip bool) (*PrizeBrief, error) { + if checkInPrize == nil || checkInPrize.PrizeId.IsZero() { + return nil, nil + } + prizes, err := prizemod.GetPrizeListByIDs([]primitive.ObjectID{checkInPrize.PrizeId}) + if err != nil || len(prizes) == 0 { + return nil, err + } + prize := *prizes[0] + // 非VIP专属奖励 && 是VIP用户 && 积分类型 → 积分+2 + if isVip && !checkInPrize.BigPrize && prize.Type == prizemod.Integral { + prize.Price += 2 + } + + handler, err := prizemod.Run(uid, prize, txnmod.SignBoon) + if err != nil { + return nil, err + } + if handler == nil { + return nil, nil + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + if err := handler.Run(t); err != nil { + return err + } + return txnmod.InsertManyTransactionLog(t, handler.GetTransactionLog()) + }); err != nil { + return nil, err + } + + prizeBrief := &PrizeBrief{ + PrizeImage: prize.Image, + PrizeType: prize.Type, + PrizeCount: prize.Price, + PrizeTitle: checkInPrize.PrizeName, + } + return prizeBrief, nil +} + +// AddCheckin 添加一次签到记录 +func AddCheckin(uid uint64) (*UserCheckinResp, stderr.Code) { + // 3秒限速 + limitKey := checkinLimitKeyPrefix + strconv.FormatUint(uid, 10) + if ok, _ := appg.Redis.Setnx_NewOK(limitKey, 1, checkinLimitExpire); !ok { + return nil, stderr.ErrLoginTooFrequently + } + + now := time.Now().Local() + cfg := getCheckinConfig() + if cfg == nil || !cfg.Enable { + return nil, stderr.FunctionNotEnabled + } + + today := timeutil.BeginOfTime(now) + // 获取今天签到记录 + checkinToday, err := usercheckinmod.FindOne(bson.M{ + "userId": uid, + "date": today, + }) + if err != nil { + log.Error("AddCheckin:FindOne:today", log.E(err)) + return nil, stderr.ErrDbQueryError + } + if checkinToday != nil { + resp := &UserCheckinResp{ + Message: "今日已经签到,请勿重复签到", + Checkin: CheckinInfo{ + TodayChecked: true, + ContinuouslyDays: checkinToday.ContinuouslyDays, + CumulativeDays: checkinToday.CumulativeDays, + }, + } + return resp, stderr.Success + } + + // 插入签到记录 + checkin := usercheckinmod.UserCheckin{ + ID: primitive.NewObjectID(), + Date: today, + UserId: uid, + Prizes: nil, + Gave: false, + CreatedAt: now, + IsReset: false, + } + if err := usercheckinmod.InsertOne(&checkin); err != nil { + log.Error("AddCheckin:InsertOne failed", log.E(err)) + return nil, stderr.ErrDbInsertError + } + + // 查询本月签到记录 + limit := int64(now.Day()) + checkinOpt := options.Find() + checkinOpt.SetSort(bson.D{{Key: "date", Value: -1}}).SetLimit(limit) + checkinList, err := usercheckinmod.FindMany(bson.M{"userId": uid}, checkinOpt) + if err != nil { + log.Error("AddCheckin:FindMany", log.E(err)) + return nil, stderr.ErrDbQueryError + } + + ContinuouslyDays := int64(1) + CumulativeDays := int64(1) + doubleBonusReceived := false + beginOfCurrentMonth := timeutil.BeginningOfMonth(now) + yesterday := today.Add(-24 * time.Hour) + + for i, item := range checkinList { + if i == 0 { + continue // 跳过今天刚插入的记录 + } + if item.Date.Before(beginOfCurrentMonth) { + continue + } + CumulativeDays += 1 + if i == 1 { + if item.Date.Equal(yesterday) { + ContinuouslyDays = item.ContinuouslyDays + 1 + } + } else if item.ContinuouslyDays == 7 { + doubleBonusReceived = true + } + } + + // 查找签到奖励 + prizeBriefList := make(PrizeBriefList, 0) + bgMediaUrl := "" + continuouslyPrizes, err := checkinprizemod.FindMany(bson.M{ + "status": true, + "checkinType": checkinprizemod.CheckinTypeContinuously, + "checkinDays": ContinuouslyDays, + }) + if err != nil { + log.Error("AddCheckin:FindMany prizes", log.E(err)) + return nil, stderr.ErrDbQueryError + } + + u, err := usermod.FindUserByUID(uid) + if err != nil { + return nil, stderr.ErrDbQueryError + } + prizeIds := make([]primitive.ObjectID, 0) + giveVipPrize := false + isVip := u != nil && u.IsPaidVIP() + continuousGiveTimes := int64(1) + + if len(continuouslyPrizes) > 0 { + if ContinuouslyDays == 7 && !doubleBonusReceived { + continuousGiveTimes = 2 + } + for _, item := range continuouslyPrizes { + if item.BigPrize && !isVip { + continue + } + for i := int64(0); i < continuousGiveTimes; i++ { + prizeBrief, err := giveCheckinPrizeById(uid, item, isVip) + if err != nil { + log.Error("giveCheckinPrizeById", log.E(err)) + continue + } + if prizeBrief == nil { + continue + } + prizeBriefList = append(prizeBriefList, prizeBrief) + prizeIds = append(prizeIds, item.PrizeId) + giveVipPrize = giveVipPrize || item.BigPrize + + // 7天翻倍,根据非VIP的奖励类型来返回奖励背景视频 + if !item.BigPrize && continuousGiveTimes == 2 && cfg.RewardBgVideos != nil { + for _, vItem := range cfg.RewardBgVideos { + if vItem.PrizeType == prizeBrief.PrizeType { + bgMediaUrl = vItem.BgMediaUrl + break + } + } + } + } + } + } + + // 更新签到记录 + update := bson.M{ + "continuouslyDays": ContinuouslyDays, + "cumulativeDays": CumulativeDays, + "prizes": prizeIds, + "gave": true, + "vipPrizeGave": giveVipPrize, + } + if err = usercheckinmod.UpdateOne(bson.M{"_id": checkin.ID}, bson.M{"$set": update}); err != nil { + log.Error("AddCheckin:UpdateOne", log.E(err)) + } + + resp := &UserCheckinResp{ + Message: "签到成功", + Checkin: CheckinInfo{ + TodayChecked: true, + ContinuouslyDays: ContinuouslyDays, + CumulativeDays: CumulativeDays, + DoubleReward: continuousGiveTimes == 2, + }, + Prizes: prizeBriefList, + PrizeVideo: bgMediaUrl, + } + return resp, stderr.Success +} + +// GetCheckinPrizes 获取签到奖品 +func GetCheckinPrizes(uid uint64) (*GetCheckinPrizeResp, stderr.Code) { + now := time.Now().Local() + cfg := getCheckinConfig() + cfgResp := CheckinConfigResp{} + if cfg != nil { + cfgResp.Enable = cfg.Enable + cfgResp.Description = cfg.Description + cfgResp.BackgroundImage = cfg.BackgroundImage + cfgResp.IntegerExchangeList = cfg.IntegerExchangeList + } + + // 获取所有启用的连续签到奖品 + opts := options.Find() + opts.SetSort(bson.D{{Key: "checkinDays", Value: 1}}) + var info []CheckinDto + err := checkinprizemod.FindManyWithBind(&info, bson.M{ + "status": true, + "checkinType": checkinprizemod.CheckinTypeContinuously, + }, opts) + if err != nil { + log.Error("GetCheckinPrizes:FindMany", log.E(err)) + return nil, stderr.ErrDbQueryError + } + + // 查询奖品信息,填充积分数据 + prizeIdSet := make(map[primitive.ObjectID]struct{}) + for _, item := range info { + prizeIdSet[item.PrizeId] = struct{}{} + } + prizeIds := make([]primitive.ObjectID, 0, len(prizeIdSet)) + for id := range prizeIdSet { + prizeIds = append(prizeIds, id) + } + if len(prizeIds) > 0 { + prizeList, _ := prizemod.GetPrizeListByIDs(prizeIds) + prizeMap := make(map[primitive.ObjectID]*prizemod.Prize) + for _, p := range prizeList { + prizeMap[p.ID] = p + } + for i, v := range info { + if prize, ok := prizeMap[v.PrizeId]; ok { + info[i].Score = int64(prize.Count) + info[i].PrizeType = prize.Type + if prize.Type == prizemod.Gold { + info[i].Score = int64(prize.Count) / 10 + } + } + } + } + + // 分组 + normalPrizes := make([]CheckinDto, 0) + bigPrizes := make([]CheckinDto, 0) + for _, item := range info { + if item.BigPrize { + bigPrizes = append(bigPrizes, item) + } else { + normalPrizes = append(normalPrizes, item) + } + } + + // 签到记录 + limit := int64(now.Day()) + checkinOpt := options.Find() + checkinOpt.SetSort(bson.D{{Key: "date", Value: -1}}).SetLimit(limit) + checkinList, err := usercheckinmod.FindMany(bson.M{"userId": uid}, checkinOpt) + if err != nil { + log.Error("GetCheckinPrizes:FindMany checkins", log.E(err)) + return nil, stderr.ErrDbQueryError + } + + ContinuouslyDays := int64(0) + CumulativeDays := int64(0) + lastCheckinDay := timeutil.BeginOfTime(now) + doubleRewardsReceived := false + isTodayCheckin := false + + // 查询用户VIP状态 + isVip := false + if u, uErr := usermod.FindUserByUID(uid); uErr == nil && u != nil { + isVip = u.IsPaidVIP() + } + + if len(checkinList) > 0 { + if checkinList[0].Date.Equal(lastCheckinDay) { + isTodayCheckin = true + } else if checkinList[0].Date.Equal(lastCheckinDay.Add(-24 * time.Hour)) { + lastCheckinDay = lastCheckinDay.Add(-24 * time.Hour) + } + beginOfCurrentMonth := timeutil.BeginningOfMonth(now) + streakBroken := false + for _, item := range checkinList { + CumulativeDays += 1 + if lastCheckinDay.Before(beginOfCurrentMonth) { + lastCheckinDay = beginOfCurrentMonth + } + if !streakBroken && item.Date.Equal(lastCheckinDay) { + ContinuouslyDays += 1 + lastCheckinDay = item.Date.Add(-24 * time.Hour) + } else { + streakBroken = true + } + if ContinuouslyDays >= 7 { + doubleRewardsReceived = true + } + } + // 只对当前连续签到的记录映射IsReceive和IsCheckedIn + for i := int64(0); i < ContinuouslyDays; i++ { + record := checkinList[ContinuouslyDays-1-i] + idx := int(i) + if idx < len(normalPrizes) { + normalPrizes[idx].IsReceive = record.Gave + normalPrizes[idx].IsCheckedIn = true + } + if idx < len(bigPrizes) { + bigPrizes[idx].IsReceive = record.VipPrizeGave + bigPrizes[idx].IsCheckedIn = true + } + } + } + + // 标记可补领的VIP奖励 + if isVip && isTodayCheckin && len(checkinList) > 0 { + for _, item := range checkinList { + if !item.Date.Equal(timeutil.BeginOfTime(now)) { + continue + } + if item.VipPrizeGave { + continue + } + for i := range bigPrizes { + if bigPrizes[i].CheckinDays == item.ContinuouslyDays { + bigPrizes[i].CanClaim = true + break + } + } + } + } + + // 标记已过期的大会员奖励:已签到、未领取、不能补领、且不是今天签到对应的那天 + todayPrizeIdx := int(ContinuouslyDays - 1) // 今天签到对应的bigPrizes索引 + for i := range bigPrizes { + if i == todayPrizeIdx && isTodayCheckin { + continue + } + if bigPrizes[i].IsCheckedIn && !bigPrizes[i].IsReceive && !bigPrizes[i].CanClaim { + bigPrizes[i].IsExpired = true + } + } + + doubleReward := !doubleRewardsReceived && ContinuouslyDays == 6 && !isTodayCheckin + resp := &GetCheckinPrizeResp{ + Prizes: normalPrizes, + BigPrizes: bigPrizes, + Checkin: CheckinInfo{ + TodayChecked: isTodayCheckin, + DoubleReward: doubleReward, + ContinuouslyDays: ContinuouslyDays, + CumulativeDays: CumulativeDays, + }, + Config: cfgResp, + } + return resp, stderr.Success +} + +// ClaimVipCheckinPrizes 补领VIP签到奖励 +func ClaimVipCheckinPrizes(uid uint64) (*UserCheckinResp, stderr.Code) { + // 3秒限速 + limitKey := checkinLimitKeyPrefix + strconv.FormatUint(uid, 10) + if ok, _ := appg.Redis.Setnx_NewOK(limitKey, 1, checkinLimitExpire); !ok { + return nil, stderr.ErrLoginTooFrequently + } + + now := time.Now().Local() + cfg := getCheckinConfig() + if cfg == nil || !cfg.Enable { + return nil, stderr.FunctionNotEnabled + } + + // 校验用户是否是VIP + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return nil, stderr.UserIsNotExists + } + if !u.IsPaidVIP() { + return nil, stderr.NoVipPrivilege + } + + // 查询当天未领取VIP奖励的签到记录 + today := timeutil.BeginOfTime(now) + checkinOpt := options.Find() + checkinOpt.SetSort(bson.D{{Key: "date", Value: 1}}) + unclaimed, err := usercheckinmod.FindMany(bson.M{ + "userId": uid, + "vipPrizeGave": false, + "date": today, + }, checkinOpt) + if err != nil { + return nil, stderr.ErrDbQueryError + } + if len(unclaimed) == 0 { + return nil, stderr.Failure + } + + // 获取所有启用的连续签到VIP奖励配置 + vipPrizes, err := checkinprizemod.FindMany(bson.M{ + "status": true, + "checkinType": checkinprizemod.CheckinTypeContinuously, + "bigPrize": true, + }) + if err != nil { + return nil, stderr.ErrDbQueryError + } + vipPrizeMap := make(map[int64]*checkinprizemod.CheckinPrize) + for _, p := range vipPrizes { + vipPrizeMap[p.CheckinDays] = p + } + + // 逐条补领 + prizeBriefList := make(PrizeBriefList, 0) + for _, record := range unclaimed { + prize, ok := vipPrizeMap[record.ContinuouslyDays] + if !ok { + continue + } + // 原子更新防并发 + if err = usercheckinmod.UpdateOne(bson.M{ + "_id": record.ID, + "vipPrizeGave": false, + }, bson.M{ + "$set": bson.M{"vipPrizeGave": true}, + }); err != nil { + continue + } + prizeBrief, gErr := giveCheckinPrizeById(uid, prize, true) + if gErr != nil { + log.Error("ClaimVipCheckinPrizes:giveCheckinPrizeById", log.E(gErr), log.Any("recordId", record.ID)) + continue + } + if prizeBrief != nil { + prizeBriefList = append(prizeBriefList, prizeBrief) + } + } + + if len(prizeBriefList) == 0 { + return nil, stderr.Failure + } + + resp := &UserCheckinResp{ + Message: "补领成功", + Prizes: prizeBriefList, + } + return resp, stderr.Success +} + +// GetCheckinConfig 获取签到配置(给后台用) +func GetCheckinConfig() (*checkinconfigmod.CheckinConfig, error) { + cfg, err := checkinconfigmod.FindOne(bson.M{}) + if err != nil { + return nil, err + } + if cfg == nil { + return &checkinconfigmod.CheckinConfig{}, nil + } + return cfg, nil +} + +// UpdateCheckinConfig 更新签到配置 +func UpdateCheckinConfig(update bson.M) error { + count, err := checkinconfigmod.Count(bson.M{}) + if err != nil { + return err + } + if count == 0 { + newConfig := &checkinconfigmod.CheckinConfig{} + return checkinconfigmod.InsertOne(newConfig) + } + // 查找并更新第一条 + cfg, err := checkinconfigmod.FindOne(bson.M{}) + if err != nil { + return err + } + return checkinconfigmod.UpdateOne(bson.M{"_id": cfg.ID}, update) +} + +// GetCheckinPrizeList 获取签到奖品列表(后台) +func GetCheckinPrizeList(pageNumber, pageSize int64) (checkinprizemod.CheckinPrizeList, int64, error) { + filter := bson.M{} + total, err := checkinprizemod.Count(filter) + if err != nil { + return nil, 0, err + } + if total == 0 { + return make(checkinprizemod.CheckinPrizeList, 0), 0, nil + } + opts := options.Find() + opts.SetLimit(pageSize) + opts.SetSkip((pageNumber - 1) * pageSize) + opts.SetSort(bson.D{{Key: "checkinDays", Value: 1}}) + data, err := checkinprizemod.FindMany(filter, opts) + if err != nil { + return nil, 0, err + } + return data, total, nil +} + +// AddCheckinPrize 添加签到奖品 +func AddCheckinPrize(prize *checkinprizemod.CheckinPrize) error { + now := time.Now() + prize.CreatedAt = now + prize.UpdatedAt = now + return checkinprizemod.InsertOne(prize) +} + +// UpdateCheckinPrize 更新签到奖品 +func UpdateCheckinPrize(id primitive.ObjectID, update bson.M) error { + return checkinprizemod.UpdateOne(bson.M{"_id": id}, bson.M{"$set": update}) +} + +// DeleteCheckinPrize 删除签到奖品 +func DeleteCheckinPrize(id primitive.ObjectID) error { + _, err := checkinprizemod.DeleteOne(bson.M{"_id": id}) + return err +} + +// GetUserCheckinList 获取用户签到历史(后台) +func GetUserCheckinList(filter bson.M, pageNumber, pageSize int64) (usercheckinmod.CheckinList, int64, error) { + total, err := usercheckinmod.Count(filter) + if err != nil { + return nil, 0, err + } + if total == 0 { + return make(usercheckinmod.CheckinList, 0), 0, nil + } + opts := options.Find() + opts.SetSort(bson.D{{Key: "createdAt", Value: -1}}) + opts.SetSkip((pageNumber - 1) * pageSize) + opts.SetLimit(pageSize) + data, err := usercheckinmod.FindMany(filter, opts) + if err != nil { + return nil, 0, err + } + return data, total, nil +} + +// DeleteUserCheckin 删除用户签到记录 +func DeleteUserCheckin(id primitive.ObjectID) error { + _, err := usercheckinmod.DeleteOne(bson.M{"_id": id}) + return err +} diff --git a/app/service/collectser/collectser.go b/app/service/collectser/collectser.go new file mode 100644 index 0000000..5345f2a --- /dev/null +++ b/app/service/collectser/collectser.go @@ -0,0 +1,240 @@ +package collectser + +import ( + "91porn-server/app/service/taskser" + "91porn-server/app/service/vidhelpser" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/middleware/ua" + "91porn-server/models/cache/aiplazadata" + "91porn-server/models/commod" + "91porn-server/models/v/aiplazamod" + "91porn-server/models/v/collectmod" + "91porn-server/models/v/dailytaskmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// DoCollect 收藏标签 +func DoCollect(uid uint64, cType string, objID primitive.ObjectID, isCollect bool, ua ua.UA, ip string) (code stderr.Code, err error) { + coll, err := collectmod.CollFindOneByUidAndObjID(uid, objID) + if err != nil { + code = stderr.CollectErrGetDataFailed + log.Error("app service collect DoCollect CollFindOneByUidAndObjID err", log.Any("err", err)) + return + } + if isCollect && !coll.ID.IsZero() { + code = stderr.CollectHasCollected + return + } else if !isCollect && coll.ID.IsZero() { + code = stderr.CollectHasNotCollected + return + } + cMod := collectmod.Collect{ + ObjID: objID, + UID: uid, + Type: cType, + } + if isCollect { + if err = collectmod.CollInsertOne(&cMod); err != nil { + code = stderr.CollectErrCollectFailed + log.Error("app service collect DoCollect CollInsertOne err", log.Any("err", err)) + return + } + if incErr := vidmod.IncrementRecommendInteraction(objID, vidmod.RecommendInteractionCollect); incErr != nil { + // 推荐累计失败不回滚已成功的收藏,记录后由监控补偿。 + log.Error("increment recommend collect failed", log.E(incErr), log.Any("objID", objID)) + } + } else { + if err = collectmod.CollDeleteOne(uid, cType, objID); err != nil { + code = stderr.CollectErrCancelFailed + log.Error("app service collect DoCollect CollDeleteOne err", log.Any("err", err)) + return + } + } + + inc := -1 + if isCollect { + inc = 1 + // 每日任务完成 + taskser.CompleteDailyTask(nil, uid, dailytaskmod.DailyDoCollect) + } + switch cType { + case constant.CollectTypeAiPlaza: + err = aiplazamod.IncCollectCount(objID) + case constant.CollectTypeSP, constant.CollectTypeShort, constant.CollectTypeCover, constant.CollectTypePIC, constant.SEED_LINK: + err = vidmod.IncForwardCount(inc, objID) + case constant.CollectTypeTag: + err = tagmod.IncreaseTagCollectCount(inc, objID) + } + if err != nil { + code = stderr.CollectErrCollectFailed + log.Error("app service collect DoCollect IncreaseTagCollectCount err", log.Any("err", err)) + return + } + reportVideoCollect(uid, cType, objID, !isCollect, ua, ip) + + code = stderr.Success + return +} + +// DoBatchCancelCollect 批量取消收藏 +func DoBatchCancelCollect(uid uint64, cType string, objIds []primitive.ObjectID, ua ua.UA, ip string) (code stderr.Code, err error) { + colls, err := collectmod.CollFindOneByUidAndObjIds(uid, objIds) + if err != nil { + code = stderr.CollectErrGetDataFailed + log.Error("app service collect DoBatchCollect CollFindOneByUidAndObjIds err", log.Any("err", err)) + return + } + var uniqueIds []string + var oids []primitive.ObjectID + for _, coll := range colls { + if !coll.ID.IsZero() { + oids = append(oids, coll.ObjID) + uniqueIds = append(uniqueIds, collectmod.Unique(uid, cType, coll.ObjID)) + } + } + if len(uniqueIds) == 0 { + code = stderr.CollectHasNotCollected + return + } + + if err = collectmod.CollDeleteMany(uniqueIds); err != nil { + code = stderr.CollectErrCancelFailed + log.Error("app service collect DoCollect CollDeleteMany err", log.Any("err", err)) + return + } + + switch cType { + case constant.CollectTypeSP, constant.CollectTypeShort, constant.CollectTypeCover, constant.CollectTypePIC, constant.CollectTypeSEED_LINK: + err = vidmod.IncForwardCount(-1, oids...) + case constant.CollectTypeTag: + err = tagmod.IncreaseTagCollectCount(-1, oids...) + } + if err != nil { + code = stderr.CollectErrCollectFailed + log.Error("app service collect DoCollect IncreaseTagCollectCount err", log.Any("err", err)) + return + } + for _, oid := range oids { + reportVideoCollect(uid, cType, oid, true, ua, ip) + } + code = stderr.Success + return +} + +func reportVideoCollect(uid uint64, cType string, objID primitive.ObjectID, cancel bool, uaInfo ua.UA, ip string) { + if !shouldReportVideoCollect(cType) || objID.IsZero() { + return + } + common.Go(func() { + user, err := usermod.FindUserByUID(uid) + if err != nil || user == nil { + return + } + video, err := vidmod.GetVideoInfo(objID.Hex()) + if err != nil || video.ID.IsZero() { + return + } + }) +} + +func shouldReportVideoCollect(cType string) bool { + switch cType { + case constant.CollectTypeSP, constant.CollectTypeShort, constant.CollectTypeCover, constant.CollectTypePIC, constant.CollectTypeSEED_LINK: + return true + default: + return false + } +} + +// GetInfoList 收藏详情列表 +func GetInfoList(uid uint64, cType string, page commod.Page) (code stderr.Code, hasNext bool, data interface{}, err error) { + stdQuery := commod.StdQuery{Page: &commod.PageBy{Num: page.PageNumber, Size: page.PageSize + 1, CheckNext: true}, Order: &[]commod.OrderBy{}} + modList, err := collectmod.CollFindCollects(uid, cType, stdQuery) + if err != nil { + code = stderr.CollectErrGetDataFailed + log.Error("app service collect GetInfoList CollFindCollects err", log.Any("err", err)) + return + } + + cIDs := make([]primitive.ObjectID, len(modList)) // 收藏对象id + for i := range modList { + cIDs[i] = modList[i].ObjID + } + + if len(cIDs) <= 0 { + code = stderr.Success + return + } + if len(cIDs) > int(page.PageSize) { + hasNext = true + cIDs = cIDs[:page.PageSize] + } + switch cType { + case constant.AiPlaza: + // 获取ai广场 + // 获取ai广场帖子 + list, err := aiplazamod.GetAll(bson.M{"_id": bson.M{"$in": cIDs}}, nil) + if err != nil { + log.Error("获取收藏的ai广场帖子失败", log.Any("uid", uid), log.E(err)) + break + } + data = aiplazadata.FormatAppDataList(list) + case constant.CollectTypeShort, constant.CollectTypeSP, constant.CollectTypeCover, constant.CollectTypePIC, constant.CollectTypeSEED_LINK: + vidList := vidhelpser.GetVideosByIDsNoCache(uid, cIDs) + data = vidList + if len(vidList) > int(page.PageSize) { + hasNext = true + data = vidList[:page.PageSize] + } + break + case constant.CollectTypeTag: + tags, err := getTagsInfo(cIDs) + if err != nil { + code = stderr.CollectErrGetDataFailed + return code, hasNext, data, err + } + data = tags + if len(tags) > int(page.PageSize) { + hasNext = true + data = tags[:page.PageSize] + } + break + + default: + } + code = stderr.Success + return +} + +func getTagsInfo(ids []primitive.ObjectID) ([]tagmod.RespInfo, error) { + tList, err := tagmod.FindTagsByIDS(ids) + if err != nil { + log.Error("app service collect getTagsInfo FindTagsByIDS err", log.Any("err", err)) + return nil, err + } + var tInfoList []tagmod.RespInfo + for _, v := range tList { + tTmp := tagmod.RespInfo{ + ID: v.ID, + Name: v.TagName, + CoverImg: v.CoverImg, + Description: v.Description, + PlayCount: v.TPlayCount, + HasCollected: true, + VideoCount: v.VideoCount, + } + tInfoList = append(tInfoList, tTmp) + } + if tInfoList == nil || len(tInfoList) <= 0 { + tInfoList = []tagmod.RespInfo{} + } + return tInfoList, nil +} diff --git a/app/service/commentser/cmtchild.go b/app/service/commentser/cmtchild.go new file mode 100644 index 0000000..02f5f9b --- /dev/null +++ b/app/service/commentser/cmtchild.go @@ -0,0 +1,71 @@ +package commentser + +import ( + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/cmtmod" + "time" +) + +// GetChildCmtList 获取子评论列表 +func GetChildCmtList(uid uint64, objID ObjectID, cid ObjectID, fstID ObjectID, curTime time.Time, page commod.Page) (code stderr.Code, data []cmtmod.ChildRespList, err error) { + // 获取作者 + //videoInfo, err := vidmod.GetVideoInfo(objID.Hex()) + //if err != nil { + // code = stderr.CommentErrGetDataFail + // return + //} + skip, limit := getNewPageInfo(0, int64(page.PageNumber), int64(page.PageSize)) + aTotal, aList, err := getAuthorChildCmtList(objID, cid, fstID, skip, limit, curTime) + if err != nil { + code = stderr.CommentErrGetDataFail + return + } + if len(aList) > int(page.PageSize) { + data, err = getChildCommentTackInfo(uid, aList) + code = stderr.Success + return + } + // 获取热评及其回复 + skip, limit = getNewPageInfo(aTotal, int64(page.PageNumber), int64(page.PageSize)) + _, hList, err := getHotChildCmtList(objID, cid, fstID, skip, limit, curTime) + if err != nil { + code = stderr.CommentErrGetDataFail + return + } + aList = append(aList, hList...) + //if len(aList) > page.PageSize { + data, err = getChildCommentTackInfo(uid, aList) + code = stderr.Success + return + //} + // 获取普通评论 + //skip, limit = getNewPageInfo(aTotal+hTotal, int64(page.PageNumber), int64(page.PageSize)) + //cList, err := getChildCmtList(objID, cid, fstID, skip, limit, curTime) + //if err != nil { + // return + //} + //aList = append(aList, cList...) + //data, err = getChildCommentTackInfo(uid, aList) + //if err != nil { + // code = stderr.CommentErrGetDataFail + // return + //} + //code = stderr.Success + //return +} + +// 获取作者的子评论及其回复和数量 +func getAuthorChildCmtList(objID ObjectID, cid ObjectID, fstId ObjectID, skip int64, limit int64, curTime time.Time) (total int64, data []cmtmod.Comment, err error) { + return cmtmod.GetAuthorChildCmtList(objID, cid, fstId, skip, limit, curTime) +} + +// 获取热门的子评论及其回复和数量 +func getHotChildCmtList(objID ObjectID, cid ObjectID, fstId ObjectID, skip int64, limit int64, curTime time.Time) (total int64, data []cmtmod.Comment, err error) { + return cmtmod.GetHotChildCmtList(objID, cid, fstId, skip, limit, curTime) +} + +// 获取普通子评论 +func getChildCmtList(objID ObjectID, cid ObjectID, fstId ObjectID, skip int64, limit int64, curTime time.Time) (data []cmtmod.Comment, err error) { + return cmtmod.GetChildCmtList(objID, cid, fstId, skip, limit, curTime) +} diff --git a/app/service/commentser/cmtcoverter.go b/app/service/commentser/cmtcoverter.go new file mode 100644 index 0000000..0694b66 --- /dev/null +++ b/app/service/commentser/cmtcoverter.go @@ -0,0 +1,328 @@ +package commentser + +import ( + "91porn-server/app/service/userser" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/followmod" + "91porn-server/models/v/likemod" + "91porn-server/models/v/usermod" + "go.mongodb.org/mongo-driver/bson/primitive" + "sync" + "time" +) + +type ObjectID = primitive.ObjectID + +// 获取父评论的附加信息--用户头像/是否点赞/是否是关注 +func getParentCommentTackInfo(uid uint64, cModList []cmtmod.Comment) (data []cmtmod.ParentRespList, err error) { + now := time.Now() + if cModList == nil { + return []cmtmod.ParentRespList{}, err + } + var CIDs []ObjectID + var UIDs []uint64 + for _, value := range cModList { + if uid != value.UserID && value.IsAdvertiser { + continue + } + tmp := cmtmod.ParentRespList{ + ID: value.ID.Hex(), + ObjID: value.ObjID.Hex(), + UserID: value.UserID, + LikeCount: value.LikeCount, + LinkStr: value.LinkStr, + IsFollow: false, + IsAuthor: value.IsAuthor, + IsLike: false, + IsDelete: value.IsDelete, + IsGodComment: value.IsGodComment, + CommCount: value.CommentCount, + CommCountBurden: value.CommentCount, + Content: value.Content, + Image: value.Image, + Status: value.Status, + City: value.City, + QuoteType: value.QuoteType, + QuoteID: value.QuoteID, + QuoteTitle: value.QuoteTitle, + QuoteImg: value.QuoteImg, + CreatedAt: value.CreatedAt, + Info: []cmtmod.ChildRespList{}, // 空值处理 + } + CIDs = append(CIDs, value.ID) + UIDs = append(UIDs, value.UserID) + data = append(data, tmp) + } + var wg = sync.WaitGroup{} + var errs []error + wg.Add(3) + // 获取点赞记录信息 + common.Go(func() { + defer wg.Done() + likedList, err1 := likemod.FindLikesByObjIDS(constant.LikeTypeComment, uid, CIDs) + if err1 != nil { + errs = append(errs, err1) + return + } + for k, v := range data { + for _, ls := range likedList { + if v.ID == ls.ObjID.Hex() { + data[k].IsLike = true + break + } + } + } + }) + // 获取用户昵称/性别/头像地址 + common.Go(func() { + defer wg.Done() + users, err1 := usermod.FindUsersByUID(UIDs) + if err1 != nil { + errs = append(errs, err1) + return + } + + for k, v := range data { + for _, u := range users { + if v.UserID == u.UID { + data[k].UserName = u.Name + data[k].UserPortrait = u.Portrait + data[k].Gender = u.Gender + data[k].Age = u.Age() + data[k].Level = u.VipLevel + data[k].VipLevel = u.VipLevel + data[k].SuperUser = u.GetSuperUser(now) + data[k].Awards = u.Awards + data[k].AwardsExpire = userser.HandleUserAwardsExpire(u) + data[k].VipExpireDate = u.VipExpireDate + break + } + } + } + }) + // 是否关注 + common.Go(func() { + defer wg.Done() + fMap, err1 := followmod.IsFollowUsers(uid, UIDs) + if err1 != nil { + errs = append(errs, err1) + return + } + for k, v := range data { + if fMap[v.UserID] { + data[k].IsFollow = true + } else { + data[k].IsFollow = false + } + } + }) + wg.Wait() + if len(errs) > 0 { + err = errs[0] + return + } + return +} + +// 获取父评论的作者或者热门的首条子评论 +func getFstChildCmtOfParent(isAdvertiser bool, objID ObjectID, list []cmtmod.ParentRespList, ids []ObjectID, uid uint64, author uint64, curTime time.Time) (data []cmtmod.ParentRespList, err error) { + if list == nil || ids == nil { + return + } + // 先获取作者的子评论 + aList, err := cmtmod.GetFstAuthorCmtOfParentCmt(isAdvertiser, objID, ids, author, curTime) + if err != nil { + return + } + var cmtMap = make(map[ObjectID]*cmtmod.Comment) + for _, v := range aList { + _v := v + cmtMap[v.CID] = &_v + } + // 获取其余的子评论 + cList, err := cmtmod.GetFstCmtOfParentCmt(isAdvertiser, objID, ids, curTime) + for _, v := range cList { + _v := v + if cmtMap[v.CID] == nil { + cmtMap[v.CID] = &_v + } + } + var childList []cmtmod.Comment + for _, v := range cmtMap { + childList = append(childList, *v) + } + cResList, err := getChildCommentTackInfo(uid, childList) + var childMap = make(map[string]*cmtmod.ChildRespList) + for _, v := range cResList { + _v := v + childMap[v.CID] = &_v + } + for _, v := range list { + if childMap[v.ID] != nil { + v.Info = []cmtmod.ChildRespList{*childMap[v.ID]} + data = append(data, v) + } else { + data = append(data, v) + } + } + return +} + +// 获取子评论的附加信息--用户头像/是否点赞/是否是关注 +func getChildCommentTackInfo(uid uint64, cModList []cmtmod.Comment) (data []cmtmod.ChildRespList, err error) { + now := time.Now() + if len(cModList) == 0 { + return + } + var CIDs []ObjectID + var UIDs []uint64 + var toUIDs []uint64 + + for _, value := range cModList { + if uid != value.UserID && value.IsAdvertiser { + continue + } + tmp := cmtmod.ChildRespList{ + ID: value.ID.Hex(), + ObjID: value.ObjID.Hex(), + CID: value.CID.Hex(), + UserID: value.UserID, + ToUserID: value.ToUserID, + LikeCount: value.LikeCount, + LinkStr: value.LinkStr, + Level: value.Level, + IsFollow: false, + IsAuthor: value.IsAuthor, + IsLike: false, + IsDelete: value.IsDelete, + Content: value.Content, + Image: value.Image, + Status: value.Status, + City: value.City, + QuoteType: value.QuoteType, + QuoteID: value.QuoteID, + QuoteTitle: value.QuoteTitle, + QuoteImg: value.QuoteImg, + CreatedAt: value.CreatedAt, + } + CIDs = append(CIDs, value.ID) + UIDs = append(UIDs, value.UserID) + data = append(data, tmp) + if value.ToUserID != 0 { + toUIDs = append(UIDs, value.ToUserID) + } + } + var wg = sync.WaitGroup{} + var errs []error + wg.Add(4) + // 是否点赞 + common.Go(func() { + defer wg.Done() + likedList, err1 := likemod.FindLikesByObjIDS(constant.LikeTypeComment, uid, CIDs) + if err1 != nil { + errs = append(errs, err1) + return + } + for k, v := range data { + for _, ls := range likedList { + if v.ID == ls.ObjID.Hex() { + data[k].IsLike = true + break + } + } + } + }) + // 获取用户昵称/性别/头像地址 + common.Go(func() { + defer wg.Done() + users, err1 := usermod.FindUsersByUID(UIDs) + if err1 != nil { + errs = append(errs, err1) + return + } + + for k, v := range data { + for _, u := range users { + if v.UserID == u.UID { + data[k].UserName = u.Name + data[k].UserPortrait = u.Portrait + data[k].Gender = u.Gender + data[k].Age = u.Age() + data[k].VipLevel = u.VipLevel + data[k].Awards = u.Awards + data[k].AwardsExpire = userser.HandleUserAwardsExpire(u) + data[k].SuperUser = u.GetSuperUser(now) + data[k].VipExpireDate = u.VipExpireDate + break + } + } + } + }) + // 获取回复用户 + common.Go(func() { + defer wg.Done() + if toUIDs == nil || len(toUIDs) == 0 { + return + } + toUsers, err1 := usermod.FindUsersByUID(toUIDs) + if err1 != nil { + errs = append(errs, err1) + return + } + for k, v := range data { + for _, u := range toUsers { + if v.ToUserID == u.UID { + data[k].ToUserName = u.Name + break + } + } + } + return + }) + // 是否关注 + common.Go(func() { + defer wg.Done() + followers, err1 := followmod.IsFollowUsers(uid, UIDs) + if err1 != nil { + errs = append(errs, err1) + return + } + for k, v := range data { + data[k].IsFollow = followers[v.UserID] + } + }) + wg.Wait() + if len(errs) > 0 { + err = errs[0] + return + } + return +} + +// 计算分页信息 +func getNewPageInfo(total int64, pageNumber int64, pageSize int64) (int64, int64) { + var curPage = pageNumber - total/pageSize + var skip, limit int64 + if curPage == 1 { + skip = 0 + limit = (pageSize - total%pageSize) + 1 + } else { + skip = pageSize - total%pageSize + (curPage-2)*pageSize + limit = (pageSize) + 1 + } + return skip, limit +} + +// 排序 +func sortCmtList(ids []ObjectID, cmtList []cmtmod.Comment) (list []cmtmod.Comment) { + var cMap = make(map[ObjectID]cmtmod.Comment) + for _, v := range cmtList { + cMap[v.ID] = v + } + for _, v := range ids { + list = append(list, cMap[v]) + } + return +} diff --git a/app/service/commentser/cmtlimitser.go b/app/service/commentser/cmtlimitser.go new file mode 100644 index 0000000..5d55883 --- /dev/null +++ b/app/service/commentser/cmtlimitser.go @@ -0,0 +1,32 @@ +package commentser + +import ( + "91porn-server/app/appg" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "time" +) + +// 评论发表限制 +func canPublishComment(uid uint64) (isCan bool, err error) { + var LimitExpire = time.Second * time.Duration(appg.Conf.CmtLimit.CmtLimitExpire) + var key = redisconst.AutoBlockKey(constant.FrequencyComment, uid) + cnt := appg.Redis.Incr(key) + if cnt == 1 { + _, _ = appg.Redis.ExpireKey(key, LimitExpire) + } + + if cnt > appg.Conf.CmtLimit.CommentCount { + // err = usermod.UpdateById(nil, uid, time.Now().Add(12*time.Hour)) + // if err != nil { + // return + // } + return + } + //code := CheckCountLimit(uid) + //if code != stderr.Success { + // return + //} + isCan = true + return +} diff --git a/app/service/commentser/cmtparent.go b/app/service/commentser/cmtparent.go new file mode 100644 index 0000000..a89e41f --- /dev/null +++ b/app/service/commentser/cmtparent.go @@ -0,0 +1,251 @@ +package commentser + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/cache/quicksearchdata" + "91porn-server/models/commod" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "fmt" + "math/rand" + "sync" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetParentCmtList 获取父评论列表 +func GetParentCmtList(uid uint64, objID ObjectID, objType cmtmod.OType, curTime time.Time, page commod.Page) (code stderr.Code, data []cmtmod.ParentRespList, hasNext bool, err error) { + user, err := usermod.FindUserByUID(uid) + if err != nil { + code = stderr.ErrDbQueryError + return + } + if user == nil || user.ID.IsZero() || user.HasBanned || user.HasLocked { + code = stderr.UserIsNotExists + return + } + var publisherID uint64 + if objType == cmtmod.OTypeCartoon || objType == cmtmod.OTypeDrama { + media, mediaErr := mediamod.GetInfo(objID) + if mediaErr != nil || media.ID.IsZero() || media.Status != 1 || media.IsDelete || + (objType == cmtmod.OTypeDrama && media.MediaType != mediamod.MediaTypeDrama) { + code = stderr.CommentErrGetDataFail + err = mediaErr + return + } + } else { + // 获取视频作者 + videoInfo, videoErr := vidmod.GetVideoInfo(objID.Hex()) + if videoErr != nil { + code = stderr.CommentErrGetDataFail + err = videoErr + return + } + publisherID = videoInfo.PublisherID + } + skip, limit := getNewPageInfo(0, int64(page.PageNumber), int64(page.PageSize)) + var ( + isGodList = make([]cmtmod.Comment, 0) + aList = make([]cmtmod.Comment, 0) + hLikeList = make([]cmtmod.Comment, 0) + hReplyList = make([]cmtmod.Comment, 0) + ) + wg := sync.WaitGroup{} + wg.Add(3) + common.Go(func() { + defer wg.Done() + // 获神评论 + isGodList, err = getIsGodCommentParentCmtList(user.IsAdvertiser, objID, curTime) + if err != nil { + code = stderr.CommentErrGetDataFail + return + } + }) + common.Go(func() { + defer wg.Done() + // 获取作者的评论 + aList, err = getAuthorParentCmtList(user.IsAdvertiser, objID, publisherID, curTime) + if err != nil { + code = stderr.CommentErrGetDataFail + return + } + }) + common.Go(func() { + defer wg.Done() + // 获取热门评论(回复数) + hReplyList, err = getHotReplyParentCmeList(user.UID, user.IsAdvertiser, objID, 0, 3, curTime) + if err != nil { + code = stderr.CommentErrGetDataFail + return + } + noIds := []primitive.ObjectID{} + for _, v := range hReplyList { + noIds = append(noIds, v.ID) + } + // 获取热门评论(点赞数) + hLikeList, err = getHotParentCmeList(user.UID, user.IsAdvertiser, noIds, objID, 0, 5, curTime) + if err != nil { + code = stderr.CommentErrGetDataFail + return + } + }) + + wg.Wait() + aList = append(isGodList, aList...) // 神评论+作者评论 + aList = append(aList, hLikeList...) + aList = append(aList, hReplyList...) + var mList []cmtmod.Comment + if len(aList) > int(page.PageNumber*page.PageSize) { + mList = aList[skip : skip+limit] + } else if len(aList) > int((page.PageNumber-1)*page.PageSize) && len(aList) < int(page.PageNumber*page.PageSize) { + mList = aList[skip:] + } + var aIds []ObjectID // 作者+热评的id + for _, v := range aList { + aIds = append(aIds, v.ID) + } + if aIds == nil { + aIds = []ObjectID{} + } + if len(mList) <= int(page.PageSize) { + skip, limit = getNewPageInfo(int64(len(aList)), int64(page.PageNumber), int64(page.PageSize)) + //获取普通评论 + cList, err := getParentCmtList(user.IsAdvertiser, objID, aIds, skip, limit, curTime) + if err != nil { + code = stderr.CommentErrGetDataFail + return code, data, false, err + } + mList = append(mList, cList...) + } + list, err := getParentCommentTackInfo(uid, mList) + if err != nil { + code = stderr.CommentErrGetDataFail + return + } + var ids []ObjectID + for _, v := range mList { + ids = append(ids, v.ID) + } + // 获取首条子评论 + data, err = getFstChildCmtOfParent(user.IsAdvertiser, objID, list, ids, uid, publisherID, curTime) + if err != nil { + code = stderr.CommentErrGetDataFail + return + } + if len(data) > int(page.PageSize) { + hasNext = true + data = data[:page.PageSize] + } + code = stderr.Success + return +} + +func QuickSearch(objID string, data []cmtmod.ParentRespList) []cmtmod.ParentRespList { + //获取置顶评论,走缓存 + quickSearches, err := quicksearchdata.GetAllFromCache(bson.M{"enabled": true, "type": 1}) + if err != nil { + return data + } + count := len(quickSearches) + if count > 0 { + var ( + info = make([]cmtmod.ChildRespList, 0) + s = rand.Intn(count) + q = quickSearches[s] + ) + cmts := []cmtmod.ParentRespList{ + cmtmod.ParentRespList{ + Info: info, + ID: q.ID.Hex(), + ObjID: objID, + UserID: 100001, + UserName: "91PORN运营", + UserPortrait: "cf230705/image/cg/227/fg/1bw/a86065d7ab4cbb11ebbebaefc0ee4444.png", + Content: q.Title, + LinkStr: q.Link, + LinkType: q.LinkType, + SearchKeyword: q.SearchKeyword, + Level: 1, + Status: 1, + City: "香港", + CreatedAt: time.Now(), + }} + user, err := usermod.FindUserByUID(100001) + if err == nil && user != nil { + cmts[0].UserName = user.Name + cmts[0].UserPortrait = user.Portrait + } + data = append(cmts, data...) + } + return data +} + +// 获取作者的父评论及其数量 +func getAuthorParentCmtList(isAdvertiser bool, objID ObjectID, author uint64, curTime time.Time) (data []cmtmod.Comment, err error) { + return cmtmod.GetAuthorParentCmt(isAdvertiser, objID, author, curTime) +} + +// 获取神评论及其数量 +func getIsGodCommentParentCmtList(isAdvertiser bool, objID ObjectID, curTime time.Time) (data []cmtmod.Comment, err error) { + return cmtmod.GetIsGodCommentParentCmt(isAdvertiser, objID, curTime) +} + +// 获取热门评论(点赞数) +func getHotParentCmeList(uid uint64, isAdvertiser bool, noIds []primitive.ObjectID, objID ObjectID, skip int64, limit int64, cutTime time.Time) (data []cmtmod.Comment, err error) { + return cmtmod.GetHotParentCmtList(uid, isAdvertiser, noIds, objID, skip, limit, cutTime) +} + +// 获取热门评论(回复数) +func getHotReplyParentCmeList(uid uint64, isAdvertiser bool, objID ObjectID, skip int64, limit int64, cutTime time.Time) (data []cmtmod.Comment, err error) { + return cmtmod.GetHotReplyParentCmtList(uid, isAdvertiser, objID, skip, limit, cutTime) +} + +// 获取其余评论 +func getParentCmtList(isAdvertiser bool, objID ObjectID, ids []ObjectID, skip int64, limit int64, cutTime time.Time) (data []cmtmod.Comment, err error) { + // 获取作者+热评 + return cmtmod.GetParentCmtList(isAdvertiser, objID, ids, skip, limit, cutTime) +} + +// GetMyCmtList 获取我的评论列表 +func GetMyCmtList(uid uint64, page commod.Page) (list []cmtmod.MyCmt, hasNext bool, err error) { + skip := int64(page.Skip()) + list, hasNext, err = cmtmod.GetMyCmtList(uid, skip, int64(page.PageSize)) + if err != nil { + log.Error(fmt.Sprintf("cmtmod.GetMyCmtList err:%v", err)) + return + } + if len(list) == 0 { + list = []cmtmod.MyCmt{} + return + } + vids := []primitive.ObjectID{} + for _, v := range list { + vids = append(vids, v.ObjID) + } + vidList, err := vidmod.GetVideoListByIDsNoStatus(vids) + if err != nil { + log.Error(fmt.Sprintf("vidmod.GetVideoListByIDsNoStatus err:%v", err)) + return + } + videoMap := make(map[primitive.ObjectID]*vidmod.VideoModel) + for _, v := range vidList { + videoMap[v.ID] = v + } + for i, v := range list { + // 从map中取出video对象 + vid, ok := videoMap[v.ObjID] + if !ok { + continue + } + v.VidTitle = vid.Title + v.VidCover = vid.Cover + list[i] = v + } + return +} diff --git a/app/service/commentser/cmtsend.go b/app/service/commentser/cmtsend.go new file mode 100644 index 0000000..a4a4868 --- /dev/null +++ b/app/service/commentser/cmtsend.go @@ -0,0 +1,276 @@ +package commentser + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/vidhelpser" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/aiplazamod" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "time" + "unicode" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 发布评论后获取返回结果 +func getRespInfoAfterPublish(uid uint64, id ObjectID, level int) (code stderr.Code, data interface{}, err error) { + var cmt cmtmod.Comment + if cmt, err = cmtmod.CmtFindOneByID(id); err != nil { + code = stderr.CommentErrGetDataFail + return + } + if level == 1 { + var info []cmtmod.ParentRespList + if info, err = getParentCommentTackInfo(uid, []cmtmod.Comment{cmt}); err != nil { + code = stderr.CommentErrSendFail + return + } + data = info[0] + } else { + var info []cmtmod.ChildRespList + info, err = getChildCommentTackInfo(uid, []cmtmod.Comment{cmt}) + if code = stderr.CommentErrSendFail; err != nil { + return + } + data = info[0] + } + code = stderr.Success + return +} + +// 检查用户状态 +func checkUserStatus(uid uint64) (code stderr.Code, data *usermod.User, err error) { + // 获取用户状态 + data, err = usermod.FindUserByUID(uid) + if err != nil || data == nil { + code = stderr.CommentUserInfoErr + return code, data, err + } + if data.HasBanned { + code = stderr.CommentUserHasBanned + return + } + + if data.BannedTime.After(time.Now()) { + code = stderr.CommentUserHasBanned + return + } + // if data.Mobile == "" { + // code = stderr.CommentUserNotBind + // return code, data, nil + // } + code = stderr.Success + return +} + +// 检查视频状态 +func checkVideoStatus(videoID string) (code stderr.Code, videoInfo vidmod.VideoModel, err error) { + videoInfo, err = vidmod.GetVideoInfo(videoID) + if err != nil { + code = stderr.CommentVideoInfoErr + return code, videoInfo, err + } + if videoInfo.ID.IsZero() { + code = stderr.CommentErrVideoIsInvalid + return code, videoInfo, err + } + // 视频未通过审核/未审核 + if videoInfo.Status != 1 && videoInfo.Status != 3 { + code = stderr.CommentErrVideoIsInvalid + return code, videoInfo, err + } + code = stderr.Success + return code, videoInfo, err +} + +// 获取相关信息 +func getTrackInfo(uid uint64, publisher uint64, isAuthor bool, ip string, param cmtmod.PublishReqInfo, cmtType cmtmod.CmtType) (code stderr.Code, data cmtmod.Comment, err error) { + //var waitGroup sync.WaitGroup + var city = "-" + //var replacedContent string + var replacedContent = param.Content + var cid ObjectID + var err1 error + if param.CID != "" { + if cid, err = primitive.ObjectIDFromHex(param.CID); err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + } + //waitGroup.Add(1) + //// 过滤评论 + //common.Go(func() { + // defer waitGroup.Done() + // replacedContent, err1 = filterser.TextFilter(param.Content) + //}) + + // 获取城市城市 + // common.Go(func() { + // defer waitGroup.Done() + // city = vidhelpser.GetLocationByIP(ip) + // }) + //waitGroup.Wait() + if err = err1; err1 != nil { + code = stderr.CommentErrSendFail + return + } + isRobot := false + if uid < constant.RobotUIDLimit { + isRobot = true + } + + now := time.Now() + data = cmtmod.Comment{ + ObjType: param.ObjType, + ObjID: param.ObjID, + CID: cid, + PublisherID: publisher, + UserID: uid, + SortCnt: 0, + ToUserID: param.ToUserID, + Content: replacedContent, + OrgContent: param.Content, + Image: param.Image, + Status: 2, + Level: param.Level, + AuthorMark: isAuthor, + IsAuthor: isAuthor, + IsDelete: false, + CommentCount: 0, + LikeCount: 0, + IPAddr: ip, + City: city, + IsRobot: isRobot, + QuoteType: param.QuoteType, + QuoteID: param.QuoteID, + QuoteImg: param.QuoteImg, + QuoteTitle: param.QuoteTitle, + CreatedAt: now, + } + if param.RID != "" { + rid, err := primitive.ObjectIDFromHex(param.RID) + if err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + rCmt, err := cmtmod.CmtFindOneByID(rid) + if err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + user, err := usermod.FindUserByUID(rCmt.UserID) + if err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + data.RID = rid + data.GID = rCmt.GID + data.AuthorMark = rCmt.AuthorMark + data.SortCnt = rCmt.SortCnt + data.Type = cmtmod.CmtTypeReply + data.PublisherID = rCmt.UserID + data.ToUserName = user.Name + data.ToUserID = rCmt.UserID + data.CID = rCmt.CID + } else if param.RID == "" && !cid.IsZero() { + data.Type = cmtmod.CmtTypeComment + cCmt, err := cmtmod.CmtFindOneByID(cid) + if err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + data.PublisherID = cCmt.UserID + } else { + data.PublisherID = publisher + data.Type = cmtType + } + code = stderr.Success + return +} + +// 设置分组id +func setGid(cid string, rid string, id ObjectID) (err error) { + if cid != "" && rid == "" { + return cmtmod.SetGid(id) + } + return +} + +// 增加评论数 +func incCommentCount(cid string, oType cmtmod.OType, objID ObjectID) (code stderr.Code, err error) { + if cid != "" { + var objCid ObjectID + if objCid, err = primitive.ObjectIDFromHex(cid); err != nil { + code = stderr.CommentErrSendFail + return code, err + } + // 根据cid获取评论信息 + if toComment, err := cmtmod.CmtFindOneByID(objCid); err != nil || (toComment.Level != 1 && toComment.Level != 2) { + code = stderr.CommentErrSendFail + log.Error("app service comment CmtFindOneByID err", log.E(err), log.Any("toComment level", toComment.Level)) + return code, err + } + if err = cmtmod.CmtIncreaseCommentCountByID(objCid); err != nil { + code = stderr.CommentErrIncCountFailed + return code, err + } + } + if oType == cmtmod.OTypeCartoon || oType == cmtmod.OTypeDrama { + if err = mediamod.IncrCommentCountByID(objID, 1); err != nil { + code = stderr.CommentErrIncCountFailed + return + } + } else if oType == cmtmod.OTypeAiPlaza { + aiplazamod.IncCommentCount(objID) + } else { + // 增加视频评论数 + if err = vidhelpser.IncCommentCount(objID); err != nil { + code = stderr.CommentErrIncCountFailed + return + } + } + + code = stderr.Success + return +} + +// 检查是否为vip,非vip用户只能发表5个字 +func checkVip(vipLevel int, content string) stderr.Code { + var charCnt int + for _, v := range content { + if unicode.Is(unicode.Scripts["Han"], v) { + charCnt++ + } else { + charCnt++ + } + if charCnt > 5 { + break + } + } + if vipLevel == 0 && charCnt > 5 { + return stderr.CommentNotVip + } + return stderr.Success +} + +func CheckCountLimit(uid uint64) stderr.Code { + var key = redisconst.AutoBlockKey(constant.BlockComment, uid) + cnt := appg.Redis.Incr(key) + if cnt == 1 { + _, _ = appg.Redis.ExpireKey(key, 12*time.Hour) + } + if cnt > appg.Conf.CmtLimit.CommentCount { + err := usermod.UpdateById(nil, uid, time.Now().Add(12*time.Hour)) + if err != nil { + return stderr.ErrDbUpdateError + } + return stderr.CommentCountOverLimit + } + return stderr.Success +} diff --git a/app/service/commentser/commentser.go b/app/service/commentser/commentser.go new file mode 100644 index 0000000..3eec0be --- /dev/null +++ b/app/service/commentser/commentser.go @@ -0,0 +1,355 @@ +package commentser + +import ( + "91porn-server/app/service/messageser" + "91porn-server/app/service/taskser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/truthutil" + "91porn-server/middleware/ua" + "91porn-server/models/commod" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/dailytaskmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/messagemod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "errors" + "fmt" + "time" + "unicode/utf8" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetVideoTotalComments 获取视频的评论数 +func GetVideoTotalComments(objID primitive.ObjectID) (code stderr.Code, data int64, err error) { + return GetTotalComments(objID, cmtmod.OTypeVideo) +} + +func GetTotalComments(objID primitive.ObjectID, objType cmtmod.OType) (code stderr.Code, data int64, err error) { + if objType == "" { + objType = cmtmod.OTypeVideo + } + params := bson.M{ + "objID": objID, + "objType": objType, + "level": 1, + "status": 1, + "isDelete": false, + "isAdvertiser": bson.M{"$in": []any{nil, false}}, + } + data, err = cmtmod.GetCount(params) + if err != nil { + code = stderr.ErrNetWorkBusy + return code, 0, err + } + code = stderr.Success + return code, data, err +} + +// 获取二级评论列表 +func GetSndCommentsList(uid uint64, objID primitive.ObjectID, cid primitive.ObjectID, fstID primitive.ObjectID, curTime time.Time, page commod.Page) (code stderr.Code, data []cmtmod.ChildRespList, err error) { + // 获取评论信息 + stdQuery := commod.StdQuery{Page: &commod.PageBy{Num: page.PageNumber, Size: page.PageSize, CheckNext: true}, Order: &[]commod.OrderBy{}} + modList, err := cmtmod.CmtFindSecondLevelComments(objID, cid, fstID, curTime, stdQuery) + if err != nil { + code = stderr.ErrNetWorkBusy + log.Error("app service comment GetSndCommentsList CmtFindSecondLevelComments err", log.E(err)) + return + } + // 空值直接返回 + if modList == nil || len(modList) <= 0 { + code = stderr.Success + data = []cmtmod.ChildRespList{} + return + } + // 获取评论附加信息 + cList, err := getChildCommentTackInfo(uid, modList) + if err != nil { + code = stderr.ErrNetWorkBusy + log.Error("app service comment GetSndCommentsList getSndCommentTackInfo err", log.E(err)) + return + } + code = stderr.Success + data = cList + return +} + +// PublishComment 发表评论 +func PublishComment(uid uint64, ua ua.UA, ip string, param cmtmod.PublishReqInfo) (code stderr.Code, data interface{}, err error) { + // 获取用户状态 + var user, publisher *usermod.User + if code, user, err = checkUserStatus(uid); err != nil || code != stderr.Success || user == nil { + return code, data, err + } + + // 评论限制 + if isCan, err := canPublishComment(uid); err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } else if !isCan { + code = stderr.CommentPublishTooFrequent + return code, data, err + } + + //检测评论是否包含网址,包含网址则发布不予处理 + if uid > 200000 && !truthutil.CheckIsValid(param.Content, 1) { + log.Warn("comment send CheckIsValid fail", log.Any("uid", uid), log.Any("content", param.Content)) + return stderr.Success, stderr.Success.Msg(), nil + } + + // 校验视频是否通过审核 + var videoInfo vidmod.VideoModel + var mediaInfo mediamod.Media + + var isAuthor bool + var isOneself bool + if param.ObjType == "" || param.ObjType == cmtmod.OTypeVideo { + if code, videoInfo, err = checkVideoStatus(param.ObjID.Hex()); err != nil || code != stderr.Success || videoInfo.ID.IsZero() { + return code, data, err + } + + // 获取视频作者信息 + if publisher, err = usermod.FindUserByUID(videoInfo.PublisherID); err != nil { + log.Error("获取评论视频作者信息", log.Any("uid", uid), log.Any("ip", ip), log.Any("param", param), log.E(err)) + return stderr.CommentErrSendFail, nil, err + } + + if publisher == nil || publisher.ID.IsZero() { + return stderr.UserIsNotExists, nil, err + } + if uid == videoInfo.PublisherID { + isAuthor = true + } + } + if param.ObjType == cmtmod.OTypeCartoon || param.ObjType == cmtmod.OTypeDrama { + mediaInfo, err = mediamod.GetInfo(param.ObjID) + if err != nil || mediaInfo.ID.IsZero() || mediaInfo.IsDelete || mediaInfo.Status != 1 { + return stderr.CommentErrVideoIsInvalid, nil, err + } + if param.ObjType == cmtmod.OTypeDrama && mediaInfo.MediaType != mediamod.MediaTypeDrama { + return stderr.CommentErrVideoIsInvalid, nil, errors.New("invalid drama") + } + } + + var cmtMod cmtmod.Comment + if code, cmtMod, err = getTrackInfo(uid, videoInfo.PublisherID, isAuthor, ip, param, cmtmod.CmtTypeVideo); code != stderr.Success || err != nil { + return code, data, err + } + cmtMod.IsAdvertiser = user.IsAdvertiser + if !mediaInfo.ID.IsZero() { + cmtMod.Title = mediaInfo.Title + } + var sUid uint64 + if param.CID != "" { + // 根据回复ID获取用户信息 + oid, _ := primitive.ObjectIDFromHex(param.CID) + cmt, err := cmtmod.CmtFindOneByID(oid) + if err != nil { + code = stderr.ErrDbQueryError + return code, data, err + } + // 校验是否是回复自己的评论 + if cmt.UserID != uid { + sUid = cmt.UserID + } + if cmt.UserID == uid { + isOneself = true + } + } + + // 保存到数据库 + id, err := cmtmod.CmtInsertOne(&cmtMod) + fmt.Println(id.Hex()) + if err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + if err = setGid(param.CID, param.RID, id); err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + // 推荐互动不按用户/账号身份过滤;运营后台直接配置的 fake* 展示值 + // 不经过该真实评论落库链路,因此仍不会进入推荐累计分。 + if param.ObjType == "" || param.ObjType == cmtmod.OTypeVideo { + if incErr := vidmod.IncrementRecommendInteraction(param.ObjID, vidmod.RecommendInteractionComment); incErr != nil { + // 评论已落库,推荐累计失败只记录,不能向客户端返回发布失败。 + log.Error("increment recommend comment failed", log.E(incErr), log.Any("objID", param.ObjID)) + } + } + if param.ObjType == cmtmod.OTypeDrama { + if incErr := mediamod.IncrCommentCountByID(param.ObjID, 1); incErr != nil { + log.Error("increment drama comment count failed", log.E(incErr), log.Any("objID", param.ObjID)) + } + } + // 增加评论数(审核只统计已审核通过的评论) + //if code, err = incCommentCount(param.CID, param.ObjType, param.ObjID); code != stderr.Success || err != nil { + // return code, data, err + //} + //存动态 + common.Go(func() { + if param.Content != "" && utf8.RuneCountInString(param.Content) >= 10 { + _ = taskser.CompleteDailyTask(nil, uid, dailytaskmod.DailyComment) + } + + if !isAuthor && videoInfo.Title != "" { + dynamicContent := fmt.Sprintf("%s 于 %s 评论了你的作品", user.Name, time.Now().Format("15:04:05")) + messageser.PublicAddMsg(messagemod.PublicAddMsg{ + SendUid: uid, + TakeUid: videoInfo.PublisherID, + MsgType: messagemod.CommentMsg, + ObjId: videoInfo.ID, + ObjName: videoInfo.Title, + ObjCover: videoInfo.Cover, + Content: dynamicContent, //param.Content, + }) + } + if !isOneself && videoInfo.Title != "" && param.CID != "" && sUid != 0 { + dynamicContent := fmt.Sprintf("%s 于 %s 回复了你的评论", user.Name, time.Now().Format("15:04:05")) + messageser.PublicAddMsg(messagemod.PublicAddMsg{ + SendUid: uid, + TakeUid: sUid, + MsgType: messagemod.ReplyCommentMsg, + ObjId: videoInfo.ID, + ObjName: videoInfo.Title, + ObjCover: videoInfo.Cover, + Content: dynamicContent, //param.Content, + }) + } + }) + //if (user.VipLevel == 0 || user.VipExpireDate.Before(now)) && !user.GetSuperUser(now) { + // if err = appg.Redis.Set(redisconst.CommentCacheKey(uid), data, 1*time.Minute); err != nil { + // log.Warn(fmt.Sprintf("用户ID:%d;IP:%s;保存缓存数据异常:%v", uid, ip, err)) + // } + //} + + // 获取该评论 + return getRespInfoAfterPublish(uid, id, param.Level) +} + +//func DailyComment(uid uint64) { +// key := redisconst.DailyTaskCacheKey(uid, dailytaskmod.DailyComment, timeutil.BeginOfToday()) +// if appg.Redis.Exists(key) { +// return +// } +// tasks, err := dailytaskmod.GetUserDailyTaskByType(nil, dailytaskmod.DailyComment) +// if err != nil { +// return +// } +// +// if tasks == nil || tasks.ID.IsZero() { +// return +// } +// +// // 获取任务记录 +// today := timeutil.BeginningOfDay(time.Now()) +// item, err := taskrecordmod.GetUserRecordByTime(uid, tasks.ID, today) +// if item == nil || item.ID.IsZero() { +// now := time.Now() +// // 更新今日登录记录 +// filter := bson.M{"uid": uid, "recordTime": today, "taskId": tasks.ID} +// cond := bson.M{ +// "uid": uid, +// "recordTime": today, +// "taskId": tasks.ID, +// "type": taskrecordmod.DailyTask, +// "taskType": taskrecordmod.DailyComment, +// "status": taskrecordmod.UnReceived, +// "count": 1, +// "updateTime": now, +// "createTime": now, +// } +// if err := taskrecordmod.InsertTask(nil, filter, cond); err != nil { +// return +// } +// } +// // 判断每日评论只可三次 +// if item != nil && !item.ID.IsZero() && item.Count < 3 { +// filter := bson.M{"uid": uid, "recordTime": today, "taskId": tasks.ID} +// if err := taskrecordmod.CurrentCountUpdate(nil, filter); err != nil { +// return +// } +// } +// // 每日超过三次评论放入缓存 +// if item != nil && !item.ID.IsZero() && item.Count > 2 { +// err = appg.Redis.Set(key, "_", time.Hour*24) +// if err != nil { +// return +// } +// } +// return +//} + +// NoVidPublishComment 非视频帖子发表评论 +func NoVidPublishComment(uid uint64, ip, cmtType string, param cmtmod.PublishReqInfo) (code stderr.Code, data interface{}, err error) { + var publisherID uint64 + // 获取用户状态 + var user *usermod.User + if code, user, err = checkUserStatus(uid); err != nil || code != stderr.Success || user == nil { + return code, data, err + } + + // 评论限制 + if isCan, err := canPublishComment(uid); err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } else if !isCan { + code = stderr.CommentPublishTooFrequent + return code, data, err + } + + //检测评论是否包含网址,包含网址则发布不予处理 + if uid > 200000 && !truthutil.CheckIsValid(param.Content, 1) { + log.Warn("comment send CheckIsValid fail", log.Any("uid", uid), log.Any("content", param.Content)) + return stderr.Success, stderr.Success.Msg(), nil + } + + var isAuthor bool + if uid == publisherID { + isAuthor = true + } + var cmtMod cmtmod.Comment + if code, cmtMod, err = getTrackInfo(uid, publisherID, isAuthor, ip, param, cmtmod.CmtNoVideo); code != stderr.Success || err != nil { + return code, data, err + } + cmtMod.IsAdvertiser = user.IsAdvertiser + // 保存到数据库 + id, err := cmtmod.CmtInsertOne(&cmtMod) + if err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + if err = setGid(param.CID, param.RID, id); err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + common.Go(func() { + // 增加评论数 + if param.Level == 2 { + _id, err := primitive.ObjectIDFromHex(param.CID) + if err != nil { + log.Warn("NoVidPublishComment primitive.ObjectIDFromHex err,", log.Any("CID", param.CID), log.Any("ToUserID", param.ToUserID)) + } + if err = cmtmod.CmtIncreaseCommentCountByID(_id); err != nil { + log.Warn("NoVidPublishComment cmtmod.CmtIncreaseCommentCountByID err,", log.Any("err", err.Error()), log.Any("CID", param.CID), log.Any("ToUserID", param.ToUserID)) + } + } + }) + //存动态 + //common.Go(func() { + // if objName != "" { + //messageser.PublicAddMsg(messagemod.PublicAddMsg{ + // SendUid: uid, + // TakeUid: publisherID, + // MsgType: messagemod.DesireMsg, + // ObjId: objID, + // ObjName: objName, + //}) + // } + //}) + // 获取该评论 + return getRespInfoAfterPublish(uid, id, param.Level) +} diff --git a/app/service/commentser/reply.go b/app/service/commentser/reply.go new file mode 100644 index 0000000..c911652 --- /dev/null +++ b/app/service/commentser/reply.go @@ -0,0 +1,118 @@ +package commentser + +import ( + "91porn-server/app/service/searcher" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/usermod" + "sync" + "time" +) + +type VideoRes = searcher.VideoRes + +type UserBInfo = usermod.BaseInfo + +type Comment = cmtmod.Comment + +type ReplyInfo struct { + RUID uint64 `json:"rUID"` //回复者UID + RName string `json:"rName"` //回复者名字 + RPortrait string `json:"rPortrait"` //回复者头像 + RContent string `json:"rContent"` //回复内容 + RTime time.Time `json:"rTime"` //回复时间 + RType cmtmod.CmtType `json:"rType"` //回复类型 + Video VideoRes `json:"video"` //视屏信息 +} + +type ReplyPage struct { + HasNext bool `json:"hasNext"` + List []ReplyInfo `json:"list"` +} + +func ReplyPages(uid uint64, skip, limit int64) (ReplyPage, error) { + limitEx := limit + 1 + cmtList, err := cmtmod.ListByPublisherID(skip, limitEx, uid) + if err != nil { + return ReplyPage{}, err + } + hasNext := false + if len(cmtList) > int(limit) { + hasNext = true + cmtList = cmtList[:limit] + } + vids := make([]ObjectID, 0, len(cmtList)) + uids := make([]uint64, 0, len(cmtList)) + for _, v := range cmtList { + vids = append(vids, v.ObjID) //视屏ID + uids = append(uids, v.UserID) + } + var ( + wg = sync.WaitGroup{} + videoResList []VideoRes + userInfos []*UserBInfo + ) + wg.Add(2) + common.Go(func() { + defer wg.Done() + list, err := searcher.GetVideoResList(uid, vids) + if err != nil { + log.Error("commentser GetVideoResList faild", log.E(err)) + } + videoResList = list + }) + common.Go(func() { + defer wg.Done() + list, err := usermod.GetUsersBaseInfo(uids) + if err != nil { + log.Error("commentser GetUsersBaseInfo faild", log.E(err)) + } + userInfos = list + }) + wg.Wait() + videoResMap := make(map[ObjectID]VideoRes, len(videoResList)) + for _, v := range videoResList { + vidp := v.VideoInfo.ID + if vidp != nil { + videoResMap[*vidp] = v + } + } + userInfoMap := make(map[uint64]*usermod.BaseInfo, len(userInfos)) + for _, v := range userInfos { + userInfoMap[v.UID] = v + } + replyInfos := toReplyInfoList(cmtList, videoResMap, userInfoMap) + return ReplyPage{ + hasNext, + replyInfos, + }, nil +} + +func toReplyInfoList(cmtList []Comment, + videoResMap map[ObjectID]VideoRes, + userInfoMap map[uint64]*usermod.BaseInfo, +) []ReplyInfo { + replyInfos := make([]ReplyInfo, 0, len(cmtList)) + for _, v := range cmtList { + userInfo, ok := userInfoMap[v.UserID] + if !ok { + continue + } + videoRes, ok := videoResMap[v.ObjID] + if !ok { + continue + } + replyInfo := ReplyInfo{ + RUID: userInfo.UID, + RName: userInfo.Name, + RPortrait: userInfo.Portrait, + RContent: v.Content, + RType: v.Type, + RTime: v.CreatedAt, + Video: videoRes, + } + replyInfos = append(replyInfos, replyInfo) + } + return replyInfos +} diff --git a/app/service/common.go b/app/service/common.go new file mode 100644 index 0000000..231149b --- /dev/null +++ b/app/service/common.go @@ -0,0 +1,534 @@ +package service + +import ( + "91porn-server/models/v/advanceordermod" + "91porn-server/models/v/taskmod" + "context" + "errors" + "fmt" + "strconv" + "sync" + "time" + + "91porn-server/app/service/productser" + "91porn-server/app/service/taskser" + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models/commod" + "91porn-server/models/v/currencymod" + "91porn-server/models/v/goldextramod" + "91porn-server/models/v/oncetaskmod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// BuyGold 购买金币 payMoney单位为分 +func BuyGold(ctx context.Context, order rchgordmod.RechargeOrder, payMoney int64) (func(*db.MongoTool) error, int64, error) { + var gold = payMoney / 10 + // 查询金币配置 + currency, err := currencymod.Get(order.ProductID) + if err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值金币-订单号[%s] 产品ID[%v] 查询异常[%v]", order.ID, order.ProductID, err)) + return nil, 0, err + } + // 未匹配到对应配置 + if currency.ID.IsZero() { + log.ErrorX(ctx, fmt.Sprintf("充值金币-订单号[%s] 产品ID[%v] 不存在", order.ID, order.ProductID)) + } else { // 匹配到对应配置 + // 金额不能低于配置金额1元并且有赠送金额 + if (payMoney+100-currency.Price) > 0 && currency.GiveGold > 0 { + gold += currency.GiveGold + } + } + return func(t *db.MongoTool) error { + // 增加金币与总充值金额 + originGold := gold + goldExtra := uint64(0) + if order.GoldExtraID != nil { + extra, err := goldextramod.GetUserGoldExtraByID(t, *order.GoldExtraID) + if err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值金币-订单号[%s] 查询金币加购券[%v] 出错: %s", order.ID, *order.GoldExtraID, err.Error())) + return err + } + if extra.Uid != order.UID || extra.Used || extra.Expired.Before(time.Now()) { + log.ErrorX(ctx, fmt.Sprintf("充值金币-订单号[%s] 查询金币加购券[%v] 无效", order.ID, *order.GoldExtraID)) + return errors.New("无效的金币加购券") + } + if err := goldextramod.UseGoldExtra(t, order.UID, *order.GoldExtraID); err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值金币-订单号[%s] 使用金币加购券[%v] 出错: %s", order.ID, *order.GoldExtraID, err.Error())) + return err + } + goldExtra = extra.Amount + gold += int64(extra.Amount) + } + w, err := walletmod.Credit(t, walletmod.CreditPlan{ + Amount: &gold, + Consumption: &payMoney, + }, order.UID) + if err != nil { + return err + } + + // 完成一次性任务 + //if err = taskser.CompleteOnceTask(t, order.UID, oncetaskmod.OnceTaskTypeUserBuyCoin); err != nil { + // fmt.Println("buy coin complete once task err", order.UID, err.Error()) + // return err + //} + + desc := "官方充值-到账" + strconv.FormatInt(gold, 10) + "金币" + if goldExtra != 0 { + desc = "官方充值-到账" + strconv.FormatInt(originGold, 10) + "金币, 消耗金币加购券增加" + strconv.FormatUint(goldExtra, 10) + "金币" + } + err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + TransNo: order.ID, + UID: order.UID, + Amount: gold, + ActualAmount: float64(gold), + TranType: txnmod.OfficialRech.Key(), + TranTypeInt: int64(txnmod.OfficialRech), + ChannelType: order.RechargeType, + Desc: desc, + DiscDoc: order.DiscDoc, + SysType: order.DevType, + RealAmount: w.RealAmount(), + }) + if err != nil { + return err + } + common.Go(func() { + // 完成任务 + _ = taskser.CompleteOnceTask(nil, order.UID, oncetaskmod.OnceTaskTypeUserBuyCoin) + taskser.CompleteGrowthTask(order.UID, taskmod.GrowthBuyGold, w.Consumption) + }) + return nil + }, gold, nil +} + +// BuyFruitCoin 购买果币 +func BuyFruitCoin(ctx context.Context, order rchgordmod.RechargeOrder, payMoney int64) (func(*db.MongoTool) error, error) { + fruitCoin := payMoney / 100 + // 查询货币币配置 + currency, err := currencymod.Get(order.ProductID) + if err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值金币-订单号[%s] 产品ID[%v] 查询异常[%v]", order.ID, order.ProductID, err)) + return nil, err + } + // 未匹配到对应配置 + if currency.ID.IsZero() { + log.ErrorX(ctx, fmt.Sprintf("充值金币-订单号[%s] 产品ID[%v] 不存在", order.ID, order.ProductID)) + } else { // 匹配到对应配置 + // 金额不能低于配置金额1元并且有赠送金额 + if (payMoney+100-currency.Price) > 0 && currency.GiveFruitCoin > 0 { + fruitCoin += currency.GiveFruitCoin + } + } + return func(t *db.MongoTool) error { + // 增加果币与总充值金额 + w, err := walletmod.Credit(t, walletmod.CreditPlan{ + FruitCoin: &fruitCoin, + Consumption: &payMoney, + }, order.UID) + if err != nil { + return err + } + return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + TransNo: order.ID, + UID: order.UID, + FruitCoin: fruitCoin, + TranType: txnmod.OfficialRech.Key(), + TranTypeInt: int64(txnmod.OfficialRech), + ChannelType: order.RechargeType, + Desc: "官方充值-到账" + strconv.FormatInt(fruitCoin, 10) + "果币", + DiscDoc: order.DiscDoc, + SysType: order.DevType, + FruitCoinBalance: w.FruitCoin, + }) + }, nil +} + +// BuyGameCoin 购买游戏币 +func BuyGameCoin(ctx context.Context, order rchgordmod.RechargeOrder, payMoney int64) (func(*db.MongoTool) error, error) { + var ( + currency currencymod.Currency + u *usermod.User + err error + wg sync.WaitGroup + creditPlan = walletmod.CreditPlan{Consumption: &payMoney} + txnLogs = make([]txnmod.TransactionLog, 0) + productId = order.ProductID.Hex() + isFirstPay bool + ) + wg.Add(2) + common.Go(func() { + defer wg.Done() + // 查询货币信息 + currency, err = currencymod.Get(order.ProductID) + if err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%s] 产品ID[%v] 查询异常[%v]", order.ID, order.ProductID, err)) + } + }) + common.Go(func() { + defer wg.Done() + // 查询用户信息 + u, err = usermod.FindUserByUID(order.UID) + if err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%s] 用户ID[%d] 查询用户信息异常 [%v]", order.ID, order.UID, err)) + } + }) + wg.Wait() + if err != nil { + return nil, err + } + if !u.HasFirstChargeVID { + item, err := rchgordmod.FindOne(nil, bson.M{"uid": u.UID, "productType": 1, "status": rchgordmod.SUCCESS}) + if err != nil { + log.ErrorX(ctx, fmt.Sprintf("货币列表==>棋牌首次充值查询异常[%v]", err)) + } + if item == nil { + isFirstPay = true + } else { + payTrue := true + _, err = usermod.UpdateTrans(nil, u.UID, usermod.UserSelector{HasFirstChargeWaLi: &payTrue}) + if err != nil { + return nil, err + } + } + } + // 未匹配到对应配置 + if currency.ID.IsZero() { + log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%s] 产品ID[%v] 未匹配到对应配置", order.ID, order.ProductID)) + } else { // 匹配到对应配置 + // 金额不能低于配置金额1元并且有赠送金额 + if (payMoney + 100 - currency.Price) > 0 { + // 赠送金币 + if currency.GiveGold > 0 { + creditPlan.Amount = ¤cy.GiveGold + } + // 赠送果币 + if currency.GiveFruitCoin > 0 { + creditPlan.FruitCoin = ¤cy.GiveFruitCoin + } + } + } + if u == nil { + log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%s] 用户ID[%d]不存在", order.ID, order.UID)) + return nil, fmt.Errorf("充值游戏币-订单号[%s] 用户ID[%d]不存在", order.ID, order.UID) + } + return func(t *db.MongoTool) error { + // 赠送会员天数 + if currency.GiveVipDays > 0 || (currency.FirstGiveVipDays > 0 && isFirstPay) { + level, expire := currency.HandlerGiveVip(u.VipLevel, u.VipExpireDate, isFirstPay) + if err := usermod.VipInfoChange(t, order.UID, level, expire); err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%v] 产品ID[%v] 会员卡变更异常[%v]", + order.ID, order.ProductID, err)) + return err + } + } + // 赠送楼凤解锁次数 + if currency.LouFengUnlockTimes > 0 { + if err := usermod.LouFengUnlockTimesIncr(t, order.UID, currency.LouFengUnlockTimes); err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%v] 产品ID[%v] 楼凤解锁次数变更异常[%v]", + order.ID, order.ProductID, err)) + return err + } + } + // 钱包变更 + w, err := walletmod.Credit(t, creditPlan, order.UID) + if err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%v] 产品ID[%v] 钱包变更异常[%v]", + order.ID, order.ProductID, err)) + return err + } + // 赠送金币记录 + if (payMoney+100-currency.Price) > 0 && currency.GiveGold > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: u.UID, + TranType: txnmod.CurrencyGive.Key(), + TranTypeInt: int64(txnmod.CurrencyGive), + Desc: fmt.Sprintf("官方充值赠送-到账%d金币", currency.GiveGold), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + Amount: currency.GiveGold, + ActualAmount: float64(currency.GiveGold), + ProductID: &productId, + RealAmount: w.RealAmount(), + }) + } + // 赠送果币记录 + if (payMoney+100-currency.Price) > 0 && currency.GiveFruitCoin > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: u.UID, + TranType: txnmod.CurrencyGive.Key(), + TranTypeInt: int64(txnmod.CurrencyGive), + Desc: fmt.Sprintf("官方充值赠送-到账%d果币", currency.GiveFruitCoin), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + FruitCoin: currency.GiveFruitCoin, + ProductID: &productId, + FruitCoinBalance: w.FruitCoin, + }) + } + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%v] 产品ID[%v] 记录资金流水记录异常[%v]", + order.ID, order.ProductID, err)) + return err + } + if order.RechargeType == "usdt" { + // 增加usdt可提现额度 + if err = walletmod.AddUsdtAmount(t, order.UID, payMoney); err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%v] 产品ID[%v] 增加usdt可提现额度异常[%v]", + order.ID, order.ProductID, err)) + return err + } + } + if isFirstPay { + _, err = usermod.UpdateTrans(t, u.UID, usermod.UserSelector{HasFirstChargeWaLi: &isFirstPay}) + if err != nil { + return err + } + } + return nil + }, nil +} + +// BuyProduct 购买商品 +func BuyProduct(ctx context.Context, order rchgordmod.RechargeOrder, payMoney int64) (func(*db.MongoTool) error, int, time.Time, error) { + var ( + creditPlan = walletmod.CreditPlan{Consumption: &payMoney} + u *usermod.User + p *productmod.Product + err error + wg sync.WaitGroup + csm int + w *walletmod.Wallet + vipExpireTime time.Time + ) + wg.Add(3) + common.Go(func() { + defer wg.Done() + // 查询会员卡信息 + p, err = productmod.FindProduct(order.ProductID, order.DevType) + if err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%v] 产品ID[%v] 查询会员卡信息异常[%v]", order.ID, order.ProductID, err)) + } + + // 检查用户VIP升级 + productser.CheckUserUpgrade(order.UID, p) + }) + common.Go(func() { + defer wg.Done() + // 查询用户信息 + u, err = usermod.FindUserByUID(order.UID) + if err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%s] 用户ID[%d] 查询用户信息异常 [%v]", order.ID, order.UID, err)) + } + }) + common.Go(func() { + defer wg.Done() + var errG error + w, errG = walletmod.GetWallet(order.UID) + if errG != nil { + log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%s] 用户ID[%d] 查询用户钱包异常 [%v]", order.ID, order.UID, err)) + } + }) + wg.Wait() + if p == nil { + log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%v] 产品ID[%v]不存在", order.ID, order.ProductID)) + return nil, 0, vipExpireTime, fmt.Errorf("充值购买会员卡-订单号[%v] 产品ID[%v]不存在", order.ID, order.ProductID) + } + // 正常分成 + if p.ChanSplitMod == 1 { + csm = rchgordmod.ChanShareModVIP + } + if u == nil { + log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%v] 用户ID[%d]不存在", order.ID, order.UID)) + return nil, 0, vipExpireTime, fmt.Errorf("充值购买会员卡-订单号[%v] 用户ID[%d]不存在", order.ID, order.UID) + } + // 充值金额低于会员卡售价1元 转化为充值对应金额金币 + // 会员抵扣券抵扣后,期望实付 = 原价 - 实际抵扣额;否则折价支付会被误判为"低于售价"而全额转金币,导致VIP不发放 + expectedPrice := p.DiscountedPrice*10 - order.DeductAmount + if expectedPrice < 0 { + expectedPrice = 0 + } + var ( + disPay = payMoney - expectedPrice + fn func(t *db.MongoTool) error + ) + if (p.ProductType != commod.AdvanceCard) && disPay < -100 { + var ( + gold = payMoney / 10 + ) + creditPlan.Amount = &gold + // 金币分成 + csm = rchgordmod.ChanShareCoin + return func(t *db.MongoTool) error { + w, err := walletmod.Credit(t, creditPlan, order.UID) + if err != nil { + return err + } + //插入一条支出流水 + if err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + TransNo: order.ID, + UID: order.UID, + Amount: gold, + ActualAmount: float64(gold), + TranType: txnmod.OfficialRech.Key(), + TranTypeInt: int64(txnmod.OfficialRech), + ChannelType: order.RechargeType, + Desc: fmt.Sprintf("官方充值购买[%s]实际支付[%d]元;全额转化为[%d]金币", p.Name, payMoney/100, payMoney/10), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + RealAmount: w.RealAmount(), + }); err != nil { + log.ErrorX(ctx, fmt.Sprintf("rechargeSer CallbackAddCoins Trans fail error:%+v order:%+v", err, order)) + return err + } + return nil + }, csm, vipExpireTime, nil + } + // 购买商品处理 + switch p.ProductType { + case commod.MeetingCard: + fn = productser.NewBuyMeetingCard(order, p, "官方充值-购买[%s]", w) + case commod.OTHER, commod.PhysicalGoods: + fn = productser.NewBuyOtherCard(order, p, "官方充值-购买[%s]", w) + case commod.VIP, commod.NEWUSERCard: + if fn, vipExpireTime, err = productser.NewBuyVIP(order, p, "官方充值-购买[%s]", w); err != nil { + return nil, 0, vipExpireTime, err + } + case commod.AdvanceCard: + if err = handleAdvanceOrderPre(order, p, u); err != nil { + log.ErrorX(ctx, fmt.Sprintf("官方充值-订单号[%s] 预处理预售卡异常[%v]", order.ID, err)) + } + if fn, vipExpireTime, err = productser.BuyAdvanceVIP(order, p, "官方充值-购买[%s]", w, payMoney); err != nil { + return nil, 0, vipExpireTime, err + } + case commod.GameAdvanceCard: + if fn, vipExpireTime, err = productser.BuyGameAdvanceVIP(order, p, "官方充值-购买[%s]", w); err != nil { + return nil, 0, vipExpireTime, err + } + case commod.VideoDiscount: + if fn, err = productser.NewBuyVideoDiscountCard(order, p, "官方充值-购买[%s]", w); err != nil { + return nil, 0, vipExpireTime, err + } + case commod.VideoFreeCard: + if fn, err = productser.NewBuyVideoFreeCard(order, p, "官方充值-购买[%s]", w); err != nil { + return nil, 0, vipExpireTime, err + } + case commod.CoinMonthCard: + if fn, err = productser.NewBuyCoinMonthCard(order, p, "官方充值-购买[%s]", w); err != nil { + return nil, 0, vipExpireTime, err + } + case commod.WhoringCard: + if fn, err = productser.NewBuyWhoringCard(order, p, "官方充值-购买[%s]", w); err != nil { + return nil, 0, vipExpireTime, err + } + default: + return nil, 0, vipExpireTime, fmt.Errorf("充值购买会员卡-订单号[%s] 商品ID[%s] 未知商品", order.ID, order.ProductID) + } + // 返回事务函数 + return func(t *db.MongoTool) error { + // 充值金额大于会员卡售价1角 转化为充值对应金额金币 + if disPay > 9 { + var ( + gold = disPay / 10 + ) + creditPlan.Amount = &gold + w, err := walletmod.Credit(t, creditPlan, order.UID) + if err != nil { + return err + } + //插入一条支出流水 + if err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + TransNo: order.ID, + UID: order.UID, + Amount: gold, + ActualAmount: float64(gold), + TranType: txnmod.OfficialRech.Key(), + TranTypeInt: int64(txnmod.OfficialRech), + ChannelType: order.RechargeType, + Desc: fmt.Sprintf("官方充值购买[%s]实际支付[%d]元;超额部分转化为[%d]金币", p.Name, payMoney/100, disPay), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + RealAmount: w.RealAmount(), + }); err != nil { + log.ErrorX(ctx, fmt.Sprintf("rechargeSer CallbackAddCoins Trans fail error:%+v order:%+v", err, order)) + return err + } + } + if err = fn(t); err != nil { + return err + } + if (p.ProductType == commod.VIP || p.ProductType == commod.NEWUSERCard || p.ProductType == commod.AdvanceCard) && p.Duration > 0 { + common.Go(func() { + fmt.Println("buyvip complete once task", u.UID) + if err = taskser.CompleteOnceTask(t, order.UID, oncetaskmod.OnceTaskTypeUserBuyVip); err != nil { + fmt.Println("buyvip complete once task err", u.UID, err.Error()) + } + }) + return nil + } + return nil + }, csm, vipExpireTime, nil +} + +func handleAdvanceOrderPre(order rchgordmod.RechargeOrder, product *productmod.Product, u *usermod.User) error { + data, err := advanceordermod.IsExist(bson.M{"productID": product.ID, "uid": u.UID}) + if err != nil { + return errors.New("advance order query IsExist err") + } + + // 预付订单 + var advanceOrder advanceordermod.AdvanceOrder + // 如果没有预售单,直接写入一笔 + if data == nil || data.ID.IsZero() { + prepaidPrivilege := advanceordermod.AdvanceCardPrepaidPrivilege{} + if product.PrepaidPrivilege != nil { + prepaidPrivilege = advanceordermod.AdvanceCardPrepaidPrivilege{ + CoinVideoLimitPerDay: product.PrepaidPrivilege.CoinVideoLimitPerDay, + LuckyDrawLimitPerDay: product.PrepaidPrivilege.LuckyDrawLimitPerDay, + AiUndressLimitPerDay: product.PrepaidPrivilege.AiUndressLimitPerDay, + DownloadLimitPerDay: product.PrepaidPrivilege.DownloadLimitPerDay, + } + } + // 创建预售订单 + now := time.Now() + advanceOrder.UID = u.UID + advanceOrder.CreatedAt = now + advanceOrder.Status = advanceordermod.AdvanceProcessing + advanceOrder.AdvanceAmount = product.AdvanceAmount * 10 + advanceOrder.BalanceAmount = product.BalanceAmount * 10 + advanceOrder.StartTime = product.StartTime + advanceOrder.EndTime = product.EndTime + advanceOrder.ProductID = product.ID + advanceOrder.TotalAmount = (product.AdvanceAmount + product.BalanceAmount) * 10 + advanceOrder.AdvOid = order.ID.Hex() + advanceOrder.PrepaidPrivilege = prepaidPrivilege + advanceOrder.TodayUse = advanceordermod.DayUse{} + _, err = advanceordermod.CreditOrder(nil, &advanceOrder) + if err != nil { + log.Error("handleAdvanceOrderPre 创建预售订单失败", log.Any("advanceOrder", advanceOrder), log.E(err)) + return err + } + return nil + } + if data.Status != advanceordermod.AdvanceSUCCESS { + return nil + } + data.Status = advanceordermod.BalanceProcessing + err = advanceordermod.Update(nil, data.ID, bson.M{"status": advanceordermod.BalanceProcessing, "balOid": order.ID.Hex()}) + if err != nil { + return err + } + + return nil +} diff --git a/app/service/contentmarkerser/content_marker.go b/app/service/contentmarkerser/content_marker.go new file mode 100644 index 0000000..4fcfc86 --- /dev/null +++ b/app/service/contentmarkerser/content_marker.go @@ -0,0 +1,182 @@ +package contentmarkerser + +import ( + "encoding/json" + "time" + + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ModuleMarker struct { + ModuleID string `json:"moduleId"` + LatestAt *time.Time `json:"latestAt"` +} + +type Response struct { + HomeLatestAt *time.Time `json:"homeLatestAt"` + Modules []ModuleMarker `json:"modules"` +} + +// MarkerCacheReader is the Redis JSON subset used to share marker snapshots. +type MarkerCacheReader interface { + GetWithJson2Any(key string, val any) error +} + +// MarkerCache adds atomic cache population to the marker snapshot reader. +type MarkerCache interface { + MarkerCacheReader + SetNX(key string, value interface{}, expiration time.Duration) (bool, error) +} + +// GetCached returns the shared marker snapshot, loading and caching it on a miss. +func GetCached(now time.Time, cache MarkerCache) (Response, error) { + data, err := getCached(now, cache, Get) + if err != nil { + return Response{}, err + } + if latestAt, cacheHit := homeLatestAtFromCachedList(cache); cacheHit { + data.HomeLatestAt = latestAt + } + return data, nil +} + +func getCached(now time.Time, cache MarkerCache, load func(time.Time) (Response, error)) (Response, error) { + if data, ok := ReadCached(cache); ok { + return data, nil + } + + data, err := load(now) + if err != nil { + return Response{}, err + } + if cache != nil { + payload, marshalErr := json.Marshal(data) + if marshalErr != nil { + log.Warn("marshal content update markers failed", log.E(marshalErr)) + return data, nil + } + stored, cacheErr := cache.SetNX( + redisconst.ContentUpdateMarkersCache, + string(payload), + redisconst.ContentUpdateMarkersCacheExpire, + ) + if cacheErr != nil { + log.Warn("cache content update markers failed", log.E(cacheErr)) + } else if !stored { + if current, ok := ReadCached(cache); ok { + return current, nil + } + } + } + return data, nil +} + +// ReadCached returns only an existing marker snapshot and never queries MongoDB. +func ReadCached(cache MarkerCacheReader) (Response, bool) { + if cache == nil { + return Response{}, false + } + var data Response + if err := cache.GetWithJson2Any(redisconst.ContentUpdateMarkersCache, &data); err != nil { + return Response{}, false + } + return data, true +} + +type cachedHomeLatestList struct { + Videos []struct { + ReviewAt time.Time `json:"reviewAt"` + } `json:"videos"` +} + +const ( + homeLatestListSortType = 1 + homeLatestListPageNumber = 1 + homeLatestListPageSize = 10 +) + +// homeLatestAtFromCachedList makes the "latest" red dot describe the same +// first-page snapshot that the client can currently read from the list cache. +func homeLatestAtFromCachedList(cache MarkerCacheReader) (*time.Time, bool) { + if cache == nil { + return nil, false + } + var data cachedHomeLatestList + key := redisconst.GetMostNewModuleVideoListKey( + homeLatestListSortType, + homeLatestListPageNumber, + homeLatestListPageSize, + ) + if err := cache.GetWithJson2Any(key, &data); err != nil { + return nil, false + } + if len(data.Videos) == 0 || data.Videos[0].ReviewAt.IsZero() { + return nil, true + } + latestAt := data.Videos[0].ReviewAt + return &latestAt, true +} + +func Get(now time.Time) (Response, error) { + modules, err := moduleconfmod.GetAllActiveModule(now) + if err != nil { + return Response{}, err + } + excluded, err := moduleconfmod.ExcludedVideoModuleIDs(now, false) + if err != nil { + return Response{}, err + } + + baseFilter := bson.M{"status": vidmod.CheckPass, "newsType": vidmod.SP} + if len(excluded) > 0 { + baseFilter["mId"] = bson.M{"$nin": excluded} + } + if darkTag, findErr := tagmod.FindOneTagByName("暗网"); findErr == nil && !darkTag.ID.IsZero() { + baseFilter["tags"] = bson.M{"$ne": darkTag.ID} + } + + homeLatestAt, err := vidmod.LatestReviewAt(baseFilter) + if err != nil { + return Response{}, err + } + + moduleIDs := make([]string, 0, len(modules)) + moduleObjectIDs := make([]primitive.ObjectID, 0, len(modules)) + for _, module := range modules { + moduleIDs = append(moduleIDs, module.ID.Hex()) + moduleObjectIDs = append(moduleObjectIDs, module.ID) + } + moduleTimes, err := vidmod.LatestReviewAtByModules(moduleIDs) + if err != nil { + return Response{}, err + } + mediaModuleTimes, err := mediamod.LatestContentAtByModules(moduleObjectIDs) + if err != nil { + return Response{}, err + } + markers := make([]ModuleMarker, 0, len(modules)) + for _, module := range modules { + var latest *time.Time + if value, ok := moduleTimes[module.ID.Hex()]; ok { + valueCopy := value + latest = &valueCopy + } + if value, ok := mediaModuleTimes[module.ID.Hex()]; ok && (latest == nil || value.After(*latest)) { + valueCopy := value + latest = &valueCopy + } + markers = append(markers, ModuleMarker{ModuleID: module.ID.Hex(), LatestAt: latest}) + } + return Response{ + HomeLatestAt: homeLatestAt, + Modules: markers, + }, nil +} diff --git a/app/service/contentmarkerser/content_marker_test.go b/app/service/contentmarkerser/content_marker_test.go new file mode 100644 index 0000000..e00099f --- /dev/null +++ b/app/service/contentmarkerser/content_marker_test.go @@ -0,0 +1,229 @@ +package contentmarkerser + +import ( + "bytes" + "encoding/json" + "errors" + "sync" + "testing" + "time" + + "91porn-server/common/constant/redisconst" +) + +type markerCacheStub struct { + data Response + getErr error + setData Response + setCalls int + + latestListCached bool + latestListReviewAt *time.Time +} + +type markerRaceCache struct { + mu sync.Mutex + payload string + exists bool +} + +func (c *markerRaceCache) GetWithJson2Any(_ string, val any) error { + c.mu.Lock() + defer c.mu.Unlock() + if !c.exists { + return errors.New("cache miss") + } + return json.Unmarshal([]byte(c.payload), val) +} + +func (c *markerRaceCache) SetNX(_ string, val interface{}, _ time.Duration) (bool, error) { + c.mu.Lock() + defer c.mu.Unlock() + if c.exists { + return false, nil + } + payload, ok := val.(string) + if !ok { + return false, errors.New("cache value is not JSON text") + } + c.payload = payload + c.exists = true + return true, nil +} + +func (c *markerCacheStub) GetWithJson2Any(key string, val any) error { + if key == redisconst.GetMostNewModuleVideoListKey( + homeLatestListSortType, + homeLatestListPageNumber, + homeLatestListPageSize, + ) { + if !c.latestListCached { + return errors.New("latest list cache miss") + } + data := cachedHomeLatestList{} + if c.latestListReviewAt != nil { + data.Videos = append(data.Videos, struct { + ReviewAt time.Time `json:"reviewAt"` + }{ReviewAt: *c.latestListReviewAt}) + } + payload, err := json.Marshal(data) + if err != nil { + return err + } + return json.Unmarshal(payload, val) + } + if c.getErr != nil { + return c.getErr + } + payload, err := json.Marshal(c.data) + if err != nil { + return err + } + return json.Unmarshal(payload, val) +} + +func (c *markerCacheStub) SetNX(_ string, val interface{}, _ time.Duration) (bool, error) { + c.setCalls++ + payload, ok := val.(string) + if !ok { + return false, errors.New("cache value is not JSON text") + } + if err := json.Unmarshal([]byte(payload), &c.setData); err != nil { + return false, err + } + return true, nil +} + +func TestResponseOmitsTodayLatestAt(t *testing.T) { + payload, err := json.Marshal(Response{}) + if err != nil { + t.Fatalf("json.Marshal(Response{}) error = %v", err) + } + if bytes.Contains(payload, []byte("todayLatestAt")) { + t.Fatalf("response still contains removed todayLatestAt field: %s", payload) + } +} + +func TestGetCachedReturnsCachedMarkers(t *testing.T) { + latest := time.Date(2026, 8, 4, 11, 30, 0, 123000000, time.FixedZone("CST", 8*60*60)) + cache := &markerCacheStub{data: Response{HomeLatestAt: &latest}} + + got, err := GetCached(time.Now(), cache) + if err != nil { + t.Fatalf("GetCached() error = %v", err) + } + if got.HomeLatestAt == nil || !got.HomeLatestAt.Equal(latest) { + t.Fatalf("GetCached() homeLatestAt = %v, want %v", got.HomeLatestAt, latest) + } + if cache.setCalls != 0 { + t.Fatalf("GetCached() cache writes = %d, want 0", cache.setCalls) + } +} + +func TestReadCachedReportsMiss(t *testing.T) { + cache := &markerCacheStub{getErr: errors.New("cache miss")} + if _, ok := ReadCached(cache); ok { + t.Fatal("ReadCached() hit = true, want false") + } + if _, ok := ReadCached(nil); ok { + t.Fatal("ReadCached(nil) hit = true, want false") + } +} + +func TestGetCachedUsesHomeLatestAtFromCachedList(t *testing.T) { + markerLatest := time.Date(2026, 8, 4, 11, 30, 0, 0, time.UTC) + listLatest := markerLatest.Add(-2 * time.Minute) + cache := &markerCacheStub{ + data: Response{HomeLatestAt: &markerLatest}, + latestListCached: true, + latestListReviewAt: &listLatest, + } + + got, err := GetCached(time.Now(), cache) + if err != nil { + t.Fatalf("GetCached() error = %v", err) + } + if got.HomeLatestAt == nil || !got.HomeLatestAt.Equal(listLatest) { + t.Fatalf("GetCached() homeLatestAt = %v, want cached list time %v", got.HomeLatestAt, listLatest) + } +} + +func TestGetCachedClearsHomeLatestAtForCachedEmptyList(t *testing.T) { + markerLatest := time.Date(2026, 8, 4, 11, 30, 0, 0, time.UTC) + cache := &markerCacheStub{ + data: Response{HomeLatestAt: &markerLatest}, + latestListCached: true, + } + + got, err := GetCached(time.Now(), cache) + if err != nil { + t.Fatalf("GetCached() error = %v", err) + } + if got.HomeLatestAt != nil { + t.Fatalf("GetCached() homeLatestAt = %v, want nil for cached empty list", got.HomeLatestAt) + } +} + +func TestGetCachedLoadsAndWritesCacheBeforeReturning(t *testing.T) { + latest := time.Date(2026, 8, 4, 11, 31, 0, 0, time.UTC) + cache := &markerCacheStub{getErr: errors.New("cache miss")} + loadCalls := 0 + load := func(time.Time) (Response, error) { + loadCalls++ + return Response{HomeLatestAt: &latest}, nil + } + + got, err := getCached(time.Now(), cache, load) + if err != nil { + t.Fatalf("getCached() error = %v", err) + } + if loadCalls != 1 { + t.Fatalf("getCached() loader calls = %d, want 1", loadCalls) + } + if cache.setCalls != 1 || cache.setData.HomeLatestAt == nil { + t.Fatalf("getCached() cache write = (%d, %v), want one marker write", cache.setCalls, cache.setData.HomeLatestAt) + } + if got.HomeLatestAt == nil || !got.HomeLatestAt.Equal(latest) { + t.Fatalf("getCached() homeLatestAt = %v, want %v", got.HomeLatestAt, latest) + } +} + +func TestGetCachedConcurrentMissReturnsWinningSnapshot(t *testing.T) { + oldLatest := time.Date(2026, 8, 4, 11, 30, 0, 0, time.UTC) + newLatest := oldLatest.Add(time.Minute) + cache := &markerRaceCache{} + oldLoaderStarted := make(chan struct{}) + releaseOldLoader := make(chan struct{}) + type outcome struct { + data Response + err error + } + oldOutcome := make(chan outcome, 1) + + go func() { + data, err := getCached(time.Now(), cache, func(time.Time) (Response, error) { + close(oldLoaderStarted) + <-releaseOldLoader + return Response{HomeLatestAt: &oldLatest}, nil + }) + oldOutcome <- outcome{data: data, err: err} + }() + + <-oldLoaderStarted + newData, err := getCached(time.Now(), cache, func(time.Time) (Response, error) { + return Response{HomeLatestAt: &newLatest}, nil + }) + if err != nil { + t.Fatalf("new getCached() error = %v", err) + } + close(releaseOldLoader) + oldResult := <-oldOutcome + if oldResult.err != nil { + t.Fatalf("old getCached() error = %v", oldResult.err) + } + for name, data := range map[string]Response{"new": newData, "old": oldResult.data} { + if data.HomeLatestAt == nil || !data.HomeLatestAt.Equal(newLatest) { + t.Fatalf("%s getCached() homeLatestAt = %v, want winning %v", name, data.HomeLatestAt, newLatest) + } + } +} diff --git a/app/service/couponser/couponser.go b/app/service/couponser/couponser.go new file mode 100644 index 0000000..5e40c12 --- /dev/null +++ b/app/service/couponser/couponser.go @@ -0,0 +1,459 @@ +package couponser + +import ( + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/advanceordermod" + "91porn-server/models/v/backpackmod" + "91porn-server/models/v/coupon_record_mod" + "91porn-server/models/v/prize_record_mod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + "91porn-server/web/service/userser" + "bytes" + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "encoding/json" + "fmt" + "time" + + "github.com/gin-gonic/gin" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// List 优惠券列表 +func List(uid uint64, in *coupon_record_mod.AppListReq) (*coupon_record_mod.QueryAllRes, error) { + var out = &coupon_record_mod.QueryAllRes{} + count, err := coupon_record_mod.QueryAllCount(in.Filter(uid)) + if err != nil { + return nil, err + } + if count == 0 { + out.List = []*coupon_record_mod.AppCouponRecordRes{} + out.Total = 0 + return out, nil + } + out.Total = count + out.List, err = coupon_record_mod.QueryAllList(in.Filter(uid), in.Options()) + if err != nil { + log.Error(fmt.Sprintf("coupon_record_mod.QueryAllList err:%v", err), log.Any("uid", uid), log.Any("params", in)) + return nil, stderr.ErrDbQueryError + } + return out, nil +} + +// Upload 上传优惠券列表 +func Upload(uid uint64, in *prize_record_mod.AppUploadReq) error { + var ( + prizes []*prize_record_mod.Prize + transactionLogs []txnmod.TransactionLog + ) + // 获取用户信息 + u, err := usermod.FindUserByUID(uid) + if err != nil { + return stderr.ErrDbQueryError + } + + if u == nil { + return stderr.UserIsNotExists + } + + prizes = in.Prizes + if len(prizes) == 0 { + return stderr.PrizeRecordNoExists + } + + // 优先使用预售卡权益免费次数 + hasPrivilege, advance := advanceordermod.GetUserValidOrder(uid) + if hasPrivilege { + // 获取可用免费次数 + validCount := advance.PrepaidPrivilege.LuckyDrawLimitPerDay - advance.TodayUse.LuckyDrawCount + if validCount >= in.Count { + // 扣除免费次数 + debitPlan := advanceordermod.DebitPlan{} + debitPlan.LuckyDrawCount = &in.Count + if err := advanceordermod.Debit(nil, uid, debitPlan); err != nil { + return stderr.ErrDbUpdateError + } + + in.Count = 0 + } else if validCount > 0 { + // 扣除免费次数 + debitPlan := advanceordermod.DebitPlan{} + debitPlan.LuckyDrawCount = &validCount + if err := advanceordermod.Debit(nil, uid, debitPlan); err != nil { + return stderr.ErrDbUpdateError + } + + in.Count -= validCount + } + } + wal, err := walletmod.GetWallet(uid) + if err != nil { + return stderr.ErrDbQueryError + } + + totalMoney := wal.Amount + wal.Income + if totalMoney <= 0 && wal.LotteryTimes < in.Count { + log.Warn(fmt.Sprintf("用户ID:%d;用户余额不足,当前剩余次数:%v;抽奖扣除次数:%v", uid, wal.LotteryTimes, in.Count)) + return stderr.InsufficientBalance + } + + if in.Gold > 0 && in.Gold > totalMoney { + log.Warn(fmt.Sprintf("用户ID:%d;用户余额不足,当前剩余总余额:%v;抽奖扣除金额:%v", uid, totalMoney, in.Gold)) + return stderr.InsufficientBalance + } + + if wal.LotteryTimes < in.Count && in.Gold == 0 { + log.Warn(fmt.Sprintf("用户ID:%d;用户余额不足,当前剩余次数:%v;抽奖扣除次数:%v", uid, wal.LotteryTimes, in.Count)) + return stderr.InsufficientBalance + } + if in.Count != 0 || in.Gold != 0 { + var creditPlan = walletmod.CreditPlan{} + // 处理扣钱 + if in.Count > 0 && in.Gold == 0 { + inc := -in.Count + creditPlan.LotteryTimes = &inc + } + if in.Gold != 0 { + desc := -in.Gold + if wal.Amount < in.Gold { + descAmt := -wal.Amount + creditPlan.Amount = &descAmt + descInc := -(in.Gold - wal.Amount) + creditPlan.Income = &descInc + log.Info(fmt.Sprintf("用户ID:%d;抽奖进入这里扣出income:%v;amount:%v", uid, descInc, descAmt)) + } else if wal.Amount >= in.Gold { + creditPlan.Amount = &desc + log.Info(fmt.Sprintf("用户ID:%d;抽奖进入这里扣出;amount:%v", uid, in.Gold)) + } + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + // 增加金币与总充值金额 + wal, err = walletmod.DescCredit(t, creditPlan, uid) + if err != nil { + return err + } + if in.Gold > 0 { + out := txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: in.Gold, + ActualAmount: -float64(in.Gold), + TranType: txnmod.RaffleDeduction.Key(), + TranTypeInt: int64(txnmod.RaffleDeduction), + Desc: fmt.Sprintf("%s-金币[%d个]", txnmod.RaffleDeduction.Key(), in.Gold), + RealAmount: wal.RealAmount(), + CreatedAt: time.Now(), + } + transactionLogs = append(transactionLogs, out) + } + if len(transactionLogs) > 0 { + err = txnmod.InsertManyTransactionLog(t, transactionLogs) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;奖品派发资金流水日志插入异常:%v", uid, err)) + return err + } + } + return nil + }); err != nil { + return stderr.ErrDbTransError + } + } + common.Go(func() { + _ = HandlePrize(uid, in, u, wal) + }) + log.Info(fmt.Sprintf("用户ID:%d;业务处理完成", uid)) + return nil +} + +func HandlePrize(uid uint64, in *prize_record_mod.AppUploadReq, u *usermod.User, wal *walletmod.Wallet) error { + var ( + prizes []*prize_record_mod.Prize + transactionLogs []txnmod.TransactionLog + couponRecords []coupon_record_mod.CouponRecord + goodsList []backpackmod.Backpack + totalInc int64 + totalAIUndressInc int64 + err error + ) + prizes = in.Prizes + vipStatus := false + // 处理同类型 + for _, p := range prizes { + log.Info(fmt.Sprintf("--------------开始任务:%+v", p)) + + if p.PrizeType == prize_record_mod.PrizeTypeVIPCard { + id, _ := primitive.ObjectIDFromHex(p.Param1) + // 查询会员卡详情 + productDetail, err := productmod.FindProduct(id, "android") + if err != nil { + continue + } + vipExpire, vipLevel, payVidDiscount := prize_record_mod.CheckVipRenew(u, productDetail) + u.VipExpireDate = vipExpire + u.VipLevel = vipLevel + u.PayVidDiscount = payVidDiscount + vipStatus = true + } + if p.PrizeType == prize_record_mod.PrizeTypeVIP { + if u.VipLevel == 0 { + u.VipLevel = 1 + } + vipExpire, vipLevel := userser.CheckVipRenew(u, int(p.Price), u.VipLevel) + u.VipExpireDate = vipExpire + u.VipLevel = vipLevel + vipStatus = true + } + if p.PrizeType == prize_record_mod.PrizeTypeGold { + totalInc += p.Price + transactionLogs = append(transactionLogs, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: u.UID, + Amount: p.Price, + ActualAmount: float64(p.Price), + TranType: txnmod.PrizeRecord.Key(), + TranTypeInt: int64(txnmod.PrizeRecord), + Desc: fmt.Sprintf("%s-金币[%d个]", txnmod.PrizeRecord.Key(), p.Price), + RealAmount: wal.RealAmount().Add(decimal.NewFromInt(totalInc)), + }) + } + if p.PrizeType == prize_record_mod.PrizeTypeGoldCoinBonus { + cId := prize_record_mod.HandleCId(p.ID) + // 获取观影券是否存在 + c, err := coupon_record_mod.FindOneByCId(cId) + if err != nil { + return err + } + if !c.ID.IsZero() { + return stderr.New(400, "coupon_record is exist") + } + + // 保存用户优惠券 + now := time.Now() + newCo := coupon_record_mod.CouponRecord{ + CID: cId, + PID: p.ID, + UID: u.UID, + UserName: u.Name, + Name: p.Name, + Count: p.Count, + Price: p.Price, + Type: coupon_record_mod.PrizeTypeGoldCoinBonus, + Used: false, + IsDelete: false, + ExpireTime: now.Add(time.Hour * 24 * time.Duration(p.ValidDate)), + } + couponRecords = append(couponRecords, newCo) + } + // AI脱衣 + if p.PrizeType == prize_record_mod.PrizeTypeAiUndress { + totalAIUndressInc += p.Price + aiLog := txnmod.TransactionLog{ + UID: uid, + Amount: p.Price, + ActualAmount: float64(p.Price), + TranType: txnmod.PrizeRecord.Key(), + TranTypeInt: int64(txnmod.PrizeRecord), + TransNo: primitive.NewObjectID(), + Desc: fmt.Sprintf("%s-赠送AI脱衣免费次数[%v次]", txnmod.PrizeRecord.Key(), p.Price), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + } + transactionLogs = append(transactionLogs, aiLog) + } + // AI金币抵扣券 + if p.PrizeType == prize_record_mod.PrizeTypeGoldDiscountCoupon { + nowTime := time.Now() + goodsList = append(goodsList, backpackmod.Backpack{ + UID: uid, + GoodsName: p.Name, + GoodsType: backpackmod.AiChangeFaceDiscount, + GoodsValue: p.Price, + GoodsOrigin: "抽奖获得AI金币抵扣券", + Status: backpackmod.Unused, + ExpiredTime: nowTime.Add(time.Hour * 24 * time.Duration(p.ValidDate)), + CreateTime: nowTime, + }) + } + } + set := usermod.UserSelector{VipExpireDate: &u.VipExpireDate, VipLevel: &u.VipLevel, PayVidDiscount: &u.PayVidDiscount} + + // 根据类型处理业务 + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + if vipStatus { + if err = usermod.UpdateUserById(t, uid, set); err != nil { + log.Error(fmt.Sprintf("用户ID:%d;奖品派发VIP更新异常:%v", uid, err)) + return err + } + } + + if len(couponRecords) > 0 { + err = coupon_record_mod.InsertManyCouponRecords(t, couponRecords) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;奖品派发加赠券日志插入异常:%v", uid, err)) + return err + } + } + if totalInc > 0 { + _, err = walletmod.CreditAmount(t, totalInc, uid) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;奖品派发金币流水异常:%v", uid, err)) + return err + } + } + if len(transactionLogs) > 0 { + err = txnmod.InsertManyTransactionLog(t, transactionLogs) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;奖品派发资金流水日志插入异常:%v", uid, err)) + return err + } + } + if totalAIUndressInc > 0 { + _, err := walletmod.DebitAiFreeTimes(t, -totalAIUndressInc, uid) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;奖品派发AI免费次数异常:%v", uid, err)) + return err + } + } + if len(goodsList) > 0 { + if err := backpackmod.AddGoodsMany(t, uid, goodsList); err != nil { + log.Error(fmt.Sprintf("用户ID:%d;奖品派发AI抵扣券插入异常:%v", uid, err)) + return err + } + } + return nil + }); err != nil { + return stderr.ErrDbTransError + } + log.Info(fmt.Sprintf("用户ID:%d;抽奖派发成功", uid)) + return nil +} + +// Delete 删除优惠券列表 +func Delete(uid uint64, in *prize_record_mod.AppDeleteReq) error { + // 获取用户信息 + u, err := usermod.FindUserByUID(uid) + if err != nil { + return err + } + + if u == nil { + return stderr.UserIsNotExists + } + // 删除成功 + if err = coupon_record_mod.Remove(in.Filter()); err != nil { + return err + } + + log.Info(fmt.Sprintf("用户ID:%d;优惠券删除发成功", uid)) + return nil +} + +// Gain 获取用户信息 +func Gain(c *gin.Context, uid uint64) (data *coupon_record_mod.AppGainUserInfoRes, err error) { + info := coupon_record_mod.AppGainUserInfoRes{UID: uid, LotteryTimes: 0} + var conAmt int64 = 0 + // 获取用户信息 + u, err := usermod.FindUserByUID(uid) + if err != nil { + return nil, err + } + + if u == nil { + return nil, stderr.UserIsNotExists + } + + w, err := walletmod.GetWallet(uid) + if err != nil { + return nil, stderr.ErrDbQueryError + } + if w.Consumption != 0 { + //conAmt = w.Consumption / 10 + } + + if w.LotteryTimes != 0 { + info.LotteryTimes = w.LotteryTimes + } + // 检查预售权益免费次数 + hasPrivilege, advance := advanceordermod.GetUserValidOrder(uid) + if hasPrivilege { + // 获取可用免费次数 + validCount := advance.PrepaidPrivilege.LuckyDrawLimitPerDay - advance.TodayUse.LuckyDrawCount + if validCount > 0 { + info.LotteryTimes += validCount + } + } + t := c.Request.Header.Get("authorization") + sReq := coupon_record_mod.AppGainUserInfoReq{ + AppID: commod.KFK_APPID, + Token: t, + UserName: u.Name, + UID: uid, + TotalRecharge: conAmt, + Balance: w.Amount + w.Income, + } + + ct, _ := json.Marshal(sReq) + log.Info(fmt.Sprintf("Gain userInfo:%+v:", string(ct))) + + aesKey := []byte("nU7cLOX7t3yJHq8yeIMCfO9emiOWtdlN") + encrypt, err := AesPCK5Encrypt(ct, aesKey) + if err != nil { + log.Error(fmt.Sprintf("Gain userInfo aes err:%+v:", err)) + return nil, err + } + log.Info(fmt.Sprintf("Gain userInfo aes string:%+v:", encrypt)) + info.Data = *encrypt + return &info, nil +} + +func PKCS5Padding(plaintext []byte, blockSize int) []byte { + padding := blockSize - len(plaintext)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(plaintext, padtext...) +} + +func PKCS5UnPadding(origData []byte) []byte { + length := len(origData) + unpadding := int(origData[length-1]) + return origData[:(length - unpadding)] +} + +// AesPCK5Encrypt 加密 +func AesPCK5Encrypt(origData, key []byte) (*string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + blockSize := block.BlockSize() + origData = PKCS5Padding(origData, blockSize) + blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) + encrypted := make([]byte, len(origData)) + blockMode.CryptBlocks(encrypted, origData) + enData := base64.StdEncoding.EncodeToString(encrypted) + return &enData, nil +} + +// 解密 +func AesPCK5Decrypt(crypted, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + blockSize := block.BlockSize() + blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) + origData := make([]byte, len(crypted)) + blockMode.CryptBlocks(origData, crypted) + origData = PKCS5UnPadding(origData) + return origData, nil +} diff --git a/app/service/currencyser/currency.go b/app/service/currencyser/currency.go new file mode 100644 index 0000000..794c19d --- /dev/null +++ b/app/service/currencyser/currency.go @@ -0,0 +1,51 @@ +package currencyser + +import ( + "fmt" + "strconv" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models/v/currencymod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/walletmod" +) + +func BuyGold(r rchgordmod.RechargeOrder, payMoney int64) (func(*db.MongoTool) error, error) { + var gold = payMoney / 10 + // 查询金币配置 + currency, err := currencymod.Get(r.ProductID) + if err != nil { + log.Error(fmt.Sprintf("充值金币-产品ID[%v]查询异常[%v]", r.ProductID, err)) + return nil, err + } + if currency.ID.IsZero() { + log.Error(fmt.Sprintf("充值金币-产品ID[%v]不存在", r.ProductID)) + return nil, fmt.Errorf("充值金币-产品ID[%v]不存在", r.ProductID) + } + if currency.GiveGold > 0 { + gold += currency.GiveGold + } + return func(t *db.MongoTool) error { + w, err := walletmod.Credit(t, walletmod.CreditPlan{ + Amount: &gold, + Consumption: &payMoney, + }, r.UID) + if err != nil { + return err + } + return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + TransNo: r.ID, + UID: r.UID, + FruitCoin: gold, + TranType: txnmod.FruitCoinRecharge.Key(), + TranTypeInt: int64(txnmod.FruitCoinRecharge), + ChannelType: r.RechargeType, + Desc: "官方充值-到账" + strconv.FormatInt(gold, 10) + "金币", + DiscDoc: r.DiscDoc, + SysType: r.DevType, + RealAmount: w.RealAmount(), + }) + }, nil +} diff --git a/app/service/customerser/customer.go b/app/service/customerser/customer.go new file mode 100644 index 0000000..bca8b7b --- /dev/null +++ b/app/service/customerser/customer.go @@ -0,0 +1,284 @@ +package customerser + +import ( + "91porn-server/app/appg" + "91porn-server/common/crypt" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/middleware/ua" + "91porn-server/models/commod" + "91porn-server/models/l/visitlogmod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + "context" + "encoding/hex" + "errors" + "fmt" + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "net/http" + "net/url" + "strings" + "time" +) + +type GetUrlResp struct { + Code int `json:"code"` // 状态码 + Msg string `json:"msg"` // 状态描述 + Data struct { + Url string `json:"url"` + Params string `json:"params"` + } `json:"data"` +} + +func GetUrl(uid uint64, ua ua.UA) (resp GetUrlResp, err error) { + reqUrl := appg.Conf.Customer.Url + "/app/customer/user/getUrl" + ul := url.Values{} + ul.Add("userId", fmt.Sprintf("%v", uid)) + ul.Add("appId", appg.Conf.Customer.AppId) + ul.Add("deviceType", ua.DevType) + ul.Add("systemType", ua.SysType) + ul.Add("systemVersion", ua.Ver) + dcodeurl, _ := url.QueryUnescape(ul.Encode()) + sign, err := crypt.AesEncrypt(dcodeurl, appg.Conf.Customer.Secret) + if err != nil { + log.Warn("GetUrl fail", log.E(err)) + return + } + reqUrl = reqUrl + "?sign=" + hex.EncodeToString(sign) + "&appId=" + appg.Conf.Customer.AppId + //请求对应产品 + httpCode, err := httputil.DefaultClientGetWithResp(&resp, reqUrl, nil) + if err != nil { + log.Warn("GetUrl DefaultClientGetWithResp fail", log.Any("reqUrl", reqUrl), log.E(err)) + return + } + if httpCode != http.StatusOK { + log.Warn("GetUrl DefaultClientGetWithResp fail httpCode != http.StatusOK ", log.Any("reqUrl", reqUrl), log.E(err)) + err = errors.New("获取失败") + return + } + if resp.Code != 200 { + log.Warn("GetUrl DefaultClientGetWithResp fail resp.Code != 200", log.Any("reqUrl", reqUrl), log.Any("resp.Msg", resp.Msg), log.E(err)) + err = errors.New("获取失败") + return + } + return resp, nil +} + +type BackpackReq struct { + UserID int `form:"userId"` + Account string `form:"account"` + Phone string `form:"phone"` + InviteCode string `form:"inviteCode"` // 邀请码 +} + +type BackpackResp struct { + UserId string `json:"userId"` // 用户Id + Account string `json:"account"` // 账号 + Email string `json:"email"` // 邮箱 + UserName string `json:"userName"` // 用户姓名 + UserIp string `json:"userIp"` // 用户IP + UserIpLocation string `json:"userIpLocation"` // 用户IP属地 + DevType string `json:"devType"` // 设备类型 + DevId string `json:"devId"` // 设备id + SystemType string `json:"systemType" bson:"systemType"` // 系统类型 + SystemVersion string `json:"systemVersion" bson:"systemVersion"` // 系统版本 + AppId string `json:"appId"` // appId + AppName string `json:"appName"` // appName + VipExpiredTime time.Time `json:"vipExpiredTime"` // VIP过期时间 + VipLevel string `json:"vipLevel"` // VIP等级 + VipName string `json:"vipName"` // VIP名 + Balance float64 `json:"balance"` // 金币余额 + UserRights string `json:"userRights"` // 用户权益,eg:金币抵扣券*3,AI抵扣券*4 + InviteCode string `json:"inviteCode"` // 邀请码 +} + +func (p *BackpackReq) GetData(ctx *gin.Context, uid uint64, account, phone, inviteCode string) (resp BackpackResp, err error) { + var user *usermod.User + if uid > 0 { + // 获取用户信息 + user, err = usermod.FindUserByUID(uid) + + } else if account != "" { + user, err = usermod.FindUserByAccount(account) + } else if phone != "" { + if !strings.HasPrefix(phone, "+86") { + phone = "+86" + phone + } + user, err = usermod.FindUserByMobile(phone) + } else if inviteCode != "" { + user, err = usermod.FindUserPromotionCode(inviteCode) + } else { + return resp, nil + } + if err != nil { + log.Error("BackpackReq.GetData fail", log.Any("uid", uid), log.E(err)) + return + } + if user == nil { + return + } + // 获取用户余额 + wallet, err := walletmod.GetWallet(uid) + if err != nil { + log.Error("BackpackReq.GetData fail", log.Any("uid", uid), log.E(err)) + return + } + filter := bson.M{ + "uid": user.UID, + } + // 获取用户的最后的登录信息 + visit, err := visitlogmod.GetInfoByCond(filter, options.FindOne().SetSort(bson.D{{Key: "_id", Value: -1}})) + if err != nil { + log.Error("BackpackReq.GetData visitlogmod.GetInfoByCond fail", log.Any("uid", uid), log.E(err)) + } + resp = BackpackResp{ + UserId: fmt.Sprintf("%v", user.UID), + UserName: user.Name, + Account: user.Account, + Email: user.Email, + UserIp: visit.IP, + DevType: user.LastSysType, + DevId: user.DevID, + SystemType: user.SysType, + SystemVersion: user.LastVer, + AppId: appg.Conf.Customer.AppId, + AppName: commod.KFK_APP_NAME, + VipExpiredTime: user.VipExpireDate, + VipName: user.VipName, + VipLevel: fmt.Sprintf("%v", user.VipLevel), + //UserRights :user. + InviteCode: user.PromCode, + } + userRights := []string{} + if wallet != nil { + resp.Balance = float64(wallet.Amount) + //if wallet.AiChangeFaceImgFreeTimes > 0 { + // userRights = append(userRights, fmt.Sprintf("AI图片换脸免费次数*%v", wallet.AiChangeFaceImgFreeTimes)) + //} + if wallet.AiUndressFreeTimes > 0 { + userRights = append(userRights, fmt.Sprintf("AI脱衣免费次数*%v", wallet.AiUndressFreeTimes)) + } + if wallet.DownloadCount > 0 { + userRights = append(userRights, fmt.Sprintf("下载次数*%v", wallet.DownloadCount)) + } + if wallet.LotteryTimes > 0 { + userRights = append(userRights, fmt.Sprintf("抽奖次数*%v", wallet.LotteryTimes)) + } + + userRights = append(userRights, fmt.Sprintf("AI女友余额=%v", wallet.AiMateBalance)) + userRights = append(userRights, fmt.Sprintf("金币余额=%v", wallet.Amount)) + userRights = append(userRights, fmt.Sprintf("金币收益=%v", wallet.Income)) + } + resp.UserRights = strings.Join(userRights, ",") + return resp, nil +} + +type RechargeReq struct { + UserID int `form:"userId"` +} + +type RechargeResp []RechargeItem + +type RechargeItem struct { + UserId string `json:"userId"` // 用户Id + UserName string `json:"userName"` // 用户姓名 + UserIp string `json:"userIp"` // 用户IP + DevType string `json:"devType"` // 设备类型 + AppId string `json:"appId"` // appId + AppName string `json:"appName"` // appName + PayType string `json:"payType"` // 支付方式 + OrderId string `json:"orderId"` // 订单号 + ChannelOid string `json:"channelOid"` // 渠道订单号 + Money float64 `json:"money"` // 订单金额 + PayMoney float64 `json:"payMoney"` // 支付金额 + Status string `json:"status"` // 订单状态 + ProductName string `json:"productName"` // 商品名称 + Remark string `json:"remark"` // 备注 + CreateTime time.Time `json:"createTime"` // 下单时间 + PayTime time.Time `json:"payTime"` // 支付时间 + NotifyTime time.Time `json:"notifyTime"` // 回调时间 +} + +func (p *RechargeReq) GetData(ctx context.Context, uid uint64) (resp RechargeResp, err error) { + _, list, err := rchgordmod.FindRechargesOrders(bson.M{ + "uid": uid, + }, options.Find().SetLimit(200).SetSort(bson.M{"_id": -1})) + if err != nil { + log.Error("RechargeReq.GetData fail", log.Any("uid", uid), log.E(err)) + return + } + productIds := []primitive.ObjectID{} + for _, v := range list { + if v.BuyType == commod.BuyProduct { + productIds = append(productIds, v.ProductID) + } + } + productMap := getProductMap(productIds) + for _, v := range list { + var status string + switch v.Status { + case 2: + //2付款失败 + status = "付款失败" + case 3: + // 3付款成功 + status = "付款成功" + case 4: + // 4已经退款 + status = "已经退款" + default: + status = "进行中" + } + item := RechargeItem{ + UserId: fmt.Sprintf("%v", v.UID), + UserName: v.Name, + UserIp: v.UserIP, + DevType: v.DevType, + AppId: appg.Conf.Customer.AppId, + AppName: commod.KFK_APP_NAME, + PayType: v.RechargeType, + OrderId: v.ID.Hex(), + ChannelOid: v.OID, + Money: float64(v.Money) / 100, + PayMoney: float64(v.PayMoney) / 100, + Status: status, + ProductName: v.ProductID.Hex(), + Remark: v.Remark, + CreateTime: v.CreatedAt, + PayTime: v.PaymentAt, + NotifyTime: v.SuccessAt, + } + switch v.BuyType { + case commod.BuyProduct: + product, ok := productMap[v.ProductID] + if ok { + item.ProductName = product.Name + } + case commod.BuyGold: + item.ProductName = "金币" + case commod.BuyFruitCoin: + item.ProductName = "果币" + case commod.BuyGameCoin: + item.ProductName = "游戏币" + } + resp = append(resp, item) + } + return +} + +func getProductMap(productIds []primitive.ObjectID) map[primitive.ObjectID]productmod.Product { + productMap := make(map[primitive.ObjectID]productmod.Product) + if len(productIds) == 0 { + return productMap + } + list, _ := productmod.FindByProductIDs(productIds) + for _, v := range list { + productMap[v.ID] = v + } + return productMap +} diff --git a/app/service/dramaser/activity.go b/app/service/dramaser/activity.go new file mode 100644 index 0000000..9b65d88 --- /dev/null +++ b/app/service/dramaser/activity.go @@ -0,0 +1,174 @@ +package dramaser + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "time" + + "91porn-server/app/appg" + "91porn-server/models/v/eventmod" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const dramaEventDedupTTL = 30 * 24 * time.Hour + +var analyticsEventNames = map[string]struct{}{ + "DRAMA_EXPOSURE": {}, "DRAMA_CLICK": {}, "DRAMA_PLAY_START": {}, + "DRAMA_PLAY_PROGRESS": {}, "DRAMA_PLAY_FINISH": {}, "DRAMA_PAYWALL_EXPOSURE": {}, + "DRAMA_UNLOCK_CLICK": {}, "DRAMA_CARD_CLICK": {}, "DRAMA_RECHARGE_SUCCESS": {}, + "DRAMA_UNLOCK_SUCCESS": {}, "DRAMA_CARD_PURCHASE_SUCCESS": {}, +} + +var dramaScenes = map[string]struct{}{ + "ai_feed": {}, "hot_drama": {}, "detail": {}, "favorite": {}, + "like": {}, "history": {}, "purchased": {}, "share": {}, +} + +func SaveEvents(uid uint64, requestID string, req EventsRequest) (EventsResponse, error) { + requestID = strings.TrimSpace(requestID) + if requestID == "" || len(requestID) > 128 { + return EventsResponse{}, fmt.Errorf("X-Request-ID is required and must not exceed 128 characters") + } + if len(req.Events) == 0 || len(req.Events) > 50 { + return EventsResponse{}, fmt.Errorf("events must contain 1 to 50 items") + } + response := EventsResponse{} + mediaCache := make(map[primitive.ObjectID]mediamod.Media) + contentCache := make(map[primitive.ObjectID]mediacontentmod.MediaContent) + for _, event := range req.Events { + if err := validateAnalyticsEvent(event); err != nil { + return EventsResponse{}, err + } + if err := validateAnalyticsObjects(event, mediaCache, contentCache); err != nil { + return EventsResponse{}, err + } + key := dramaIdempotencyKey("event", uid, event.EventID) + acquired := true + if appg.Redis != nil { + var err error + acquired, err = appg.Redis.SetNX(key, "1", dramaEventDedupTTL) + if err != nil { + return EventsResponse{}, err + } + } else { + exists, err := eventmod.Exists(event.EventID) + if err != nil { + return EventsResponse{}, err + } + acquired = !exists + } + if !acquired { + response.Duplicated++ + continue + } + payload, err := json.Marshal(struct { + UID uint64 `json:"uid"` + AnalyticsEvent + }{UID: uid, AnalyticsEvent: event}) + if err != nil { + return EventsResponse{}, err + } + now := time.Now() + if err = eventmod.Insert(nil, &eventmod.Event{ + EventId: event.EventID, EventType: event.EventName, Data: string(payload), + Status: eventmod.Default, CreatedAt: now, UpdatedAt: now, + }); err != nil { + if appg.Redis != nil { + _, _ = appg.Redis.Del(key) + } + return EventsResponse{}, err + } + response.Accepted++ + } + return response, nil +} + +func validateAnalyticsObjects( + event AnalyticsEvent, + mediaCache map[primitive.ObjectID]mediamod.Media, + contentCache map[primitive.ObjectID]mediacontentmod.MediaContent, +) error { + media, ok := mediaCache[event.MediaID] + if !ok { + var err error + media, err = mediamod.GetInfo(event.MediaID) + if err != nil { + return err + } + mediaCache[event.MediaID] = media + } + if media.ID.IsZero() || media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete { + return fmt.Errorf("invalid drama event mediaId") + } + if event.ContentID.IsZero() { + return nil + } + content, ok := contentCache[event.ContentID] + if !ok { + var err error + content, err = mediacontentmod.GetInfo(event.ContentID, true) + if err != nil { + return err + } + contentCache[event.ContentID] = content + } + if content.MediaID != event.MediaID || content.MediaType != mediamod.MediaTypeDrama || content.IsDelete { + return fmt.Errorf("event contentId does not belong to mediaId") + } + if event.EpisodeNumber > 0 && content.EpisodeNumber != event.EpisodeNumber { + return fmt.Errorf("event episodeNumber does not match contentId") + } + return nil +} + +func validateAnalyticsEvent(event AnalyticsEvent) error { + event.EventID = strings.TrimSpace(event.EventID) + if event.EventID == "" || len(event.EventID) > 128 { + return fmt.Errorf("eventId is required and must not exceed 128 characters") + } + if _, ok := analyticsEventNames[event.EventName]; !ok { + return fmt.Errorf("unsupported eventName: %s", event.EventName) + } + if _, ok := dramaScenes[event.Scene]; !ok { + return fmt.Errorf("unsupported scene: %s", event.Scene) + } + if event.MediaID.IsZero() || event.OccurredAt.IsZero() || strings.TrimSpace(event.SessionID) == "" { + return fmt.Errorf("sessionId, occurredAt and mediaId are required") + } + requireContent := event.EventName != "DRAMA_EXPOSURE" && event.EventName != "DRAMA_CLICK" + if requireContent && event.ContentID.IsZero() { + return fmt.Errorf("contentId is required for %s", event.EventName) + } + if event.EventName == "DRAMA_EXPOSURE" && event.Scene == "ai_feed" && (event.QueueVersion == "" || event.Position < 0) { + return fmt.Errorf("queueVersion and position are required for feed exposure") + } + if strings.HasPrefix(event.EventName, "DRAMA_PLAY_") { + if event.EpisodeNumber < 1 || (event.PlayMode != "feed" && event.PlayMode != "continuous") { + return fmt.Errorf("episodeNumber and playMode are required for play events") + } + } + if event.EventName == "DRAMA_PLAY_PROGRESS" && (event.Progress < 0 || event.Duration <= 0) { + return fmt.Errorf("progressSeconds and durationSeconds are required for progress events") + } + if event.EventName == "DRAMA_PLAY_FINISH" && event.Duration <= 0 { + return fmt.Errorf("durationSeconds is required for finish events") + } + if strings.Contains(event.EventName, "PAYWALL") || strings.Contains(event.EventName, "UNLOCK") || + strings.Contains(event.EventName, "CARD") || strings.Contains(event.EventName, "RECHARGE") { + if strings.TrimSpace(event.CheckoutContextID) == "" { + return fmt.Errorf("checkoutContextId is required for %s", event.EventName) + } + } + return nil +} + +func dramaIdempotencyKey(scope string, uid uint64, id string) string { + sum := sha256.Sum256([]byte(fmt.Sprintf("%s:%d:%s", scope, uid, strings.TrimSpace(id)))) + return "drama:" + scope + ":dedup:" + hex.EncodeToString(sum[:]) +} diff --git a/app/service/dramaser/download.go b/app/service/dramaser/download.go new file mode 100644 index 0000000..8fc7e45 --- /dev/null +++ b/app/service/dramaser/download.go @@ -0,0 +1,109 @@ +package dramaser + +import ( + "context" + "crypto/sha256" + "errors" + "fmt" + "strings" + "time" + + "91porn-server/app/appg" + "91porn-server/common/db" + "91porn-server/models/v/media_buy_record_mod" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var ( + ErrDramaEntitlementRequired = errors.New("active drama entitlement is required") + ErrDownloadResourceInvalid = errors.New("short drama episode is unavailable") +) + +const downloadTicketTTL = 6 * time.Hour + +type DownloadAuthorizeRequest struct { + MediaID primitive.ObjectID `json:"mediaId" binding:"required"` + ContentID primitive.ObjectID `json:"contentId" binding:"required"` +} + +type DownloadAuthorizeResponse struct { + MediaID primitive.ObjectID `json:"mediaId"` + ContentID primitive.ObjectID `json:"contentId"` + EpisodeNumber int `json:"episodeNumber"` + Name string `json:"name"` + Cover string `json:"cover"` + DownloadURL string `json:"downloadUrl"` + H265DownloadURL string `json:"h265DownloadUrl"` + MediaSize int64 `json:"mediaSize"` + ExpiresAt time.Time `json:"expiresAt"` + RemainingDownloadCount int64 `json:"remainingDownloadCount"` + Charged bool `json:"charged"` +} + +func AuthorizeDownload(ctx context.Context, uid uint64, requestID string, req DownloadAuthorizeRequest, now time.Time) (DownloadAuthorizeResponse, error) { + requestID = strings.TrimSpace(requestID) + if len(requestID) < 1 || len(requestID) > 128 { + return DownloadAuthorizeResponse{}, errors.New("X-Request-ID长度必须为1到128") + } + media, err := mediamod.GetInfo(req.MediaID) + if err != nil || media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete { + return DownloadAuthorizeResponse{}, ErrDownloadResourceInvalid + } + content, err := mediacontentmod.GetInfo(req.ContentID) + if err != nil || content.MediaID != req.MediaID || content.MediaType != mediamod.MediaTypeDrama || content.Status != 1 || !content.IsActive || content.IsDelete { + return DownloadAuthorizeResponse{}, ErrDownloadResourceInvalid + } + requestKey := downloadRequestKey(uid, requestID) + fingerprint := req.MediaID.Hex() + ":" + req.ContentID.Hex() + var wallet *walletmod.Wallet + var charged bool + for attempt := 0; attempt < 3; attempt++ { + err = appg.VideoDB.TransCtx(ctx, func(t *db.MongoTool) error { + user, userErr := usermod.FindUserByUIDTrans(t, uid) + if userErr != nil { + return userErr + } + bought := false + if user != nil && !user.HasLocked && !user.DramaExpire.After(now) { + bought, userErr = media_buy_record_mod.IsBuyWithTool(t, uid, req.MediaID, req.ContentID) + if userErr != nil { + return userErr + } + } + if !canAuthorizeDramaDownload(user, bought, now) { + return ErrDramaEntitlementRequired + } + wallet, charged, userErr = walletmod.AuthorizeDramaDownload(t, uid, requestKey, fingerprint, now) + return userErr + }) + if !errors.Is(err, walletmod.ErrDownloadAuthorizationRace) { + break + } + } + if err != nil { + return DownloadAuthorizeResponse{}, err + } + name := strings.TrimSpace(content.Name) + if name == "" { + name = fmt.Sprintf("第%d集", content.EpisodeNumber) + } + return DownloadAuthorizeResponse{ + MediaID: req.MediaID, ContentID: req.ContentID, EpisodeNumber: content.EpisodeNumber, + Name: name, Cover: content.Cover, DownloadURL: content.VideoUrl, + H265DownloadURL: appg.H265URLForApp(content.H265Url), MediaSize: content.MediaSize, + ExpiresAt: now.Add(downloadTicketTTL), RemainingDownloadCount: wallet.DownloadCount, Charged: charged, + }, nil +} + +func downloadRequestKey(uid uint64, requestID string) string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%d:%s", uid, requestID)))) +} + +func canAuthorizeDramaDownload(user *usermod.User, bought bool, now time.Time) bool { + return user != nil && !user.HasLocked && (user.DramaExpire.After(now) || bought) +} diff --git a/app/service/dramaser/drama.go b/app/service/dramaser/drama.go new file mode 100644 index 0000000..f5d42b4 --- /dev/null +++ b/app/service/dramaser/drama.go @@ -0,0 +1,341 @@ +package dramaser + +import ( + "context" + "fmt" + "hash/fnv" + "math/rand" + "sort" + "strings" + "time" + + "91porn-server/app/appg" + "91porn-server/app/service/mediacontentser" + "91porn-server/app/service/mediaser" + recommendqueue "91porn-server/common/shortrecommend" + "91porn-server/models/cache/mediadata" + "91porn-server/models/v/media_buy_record_mod" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const dramaFeedCandidateLimit = 500 + +func GetChannelConfig(now time.Time) (ChannelConfig, error) { + modules, err := moduleconfmod.GetModuleConfByType(moduleconfmod.Drama) + if err != nil { + return ChannelConfig{}, err + } + active := make([]moduleconfmod.ModuleConf, 0, len(modules)) + for _, module := range modules { + if module.IsActiveAt(now) { + active = append(active, module) + } + } + sort.SliceStable(active, func(i, j int) bool { return active[i].SortNum > active[j].SortNum }) + var aiModuleID, hotModuleID primitive.ObjectID + for _, module := range active { + name := strings.ToLower(strings.TrimSpace(module.ModuleName)) + switch { + case strings.Contains(name, "热门") || strings.Contains(name, "hot"): + if hotModuleID.IsZero() { + hotModuleID = module.ID + } + case strings.Contains(name, "ai"): + if aiModuleID.IsZero() { + aiModuleID = module.ID + } + } + } + if aiModuleID.IsZero() && len(active) > 0 { + aiModuleID = active[0].ID + } + if hotModuleID.IsZero() && len(active) > 1 { + hotModuleID = active[1].ID + } + return ChannelConfig{ + BottomTabTitle: "短剧", + DefaultTab: TabAIDrama, + AdEnabled: false, + Tabs: []ChannelTab{ + {Key: TabLive, Title: "直播", Enabled: true, Sort: 1}, + {Key: TabDouyin, Title: "抖音", Enabled: true, Sort: 2}, + {Key: TabAIDrama, Title: "AI短剧", Enabled: !aiModuleID.IsZero(), Sort: 3, ModuleID: objectIDString(aiModuleID)}, + {Key: TabHotDrama, Title: "热门短剧", Enabled: !hotModuleID.IsZero(), Sort: 4, ModuleID: objectIDString(hotModuleID)}, + }, + }, nil +} + +func objectIDString(id primitive.ObjectID) string { + if id.IsZero() { + return "" + } + return id.Hex() +} + +func GetList(uid uint64, req ListRequest) (ListResponse, error) { + filter := bson.M{"mediaType": mediamod.MediaTypeDrama, "status": 1, "isDelete": false} + sortBy := dramaListSort(req.SortType) + total, err := mediamod.QueryAllCount(filter) + if err != nil { + return ListResponse{}, err + } + list, hasNext, err := mediadata.GetListFromCache(filter, req.Skip64(), req.Limit64(), sortBy) + if err != nil { + return ListResponse{}, err + } + return ListResponse{Total: total, HasNext: hasNext, List: mediaser.FillMedias(list, uid, true)}, nil +} + +func dramaListSort(sortType int) bson.D { + sortBy := bson.D{{Key: "sortCode", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}} + switch sortType { + case 2: + sortBy = bson.D{{Key: "latestPublishedAt", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}} + case 3: + sortBy = bson.D{{Key: "countLike", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}} + case 4: + sortBy = bson.D{{Key: "countCollect", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}} + } + return sortBy +} + +func GetFeed(ctx context.Context, uid uint64, pageSize uint64, now time.Time) (FeedResponse, error) { + filter := bson.M{"mediaType": mediamod.MediaTypeDrama, "status": 1, "isDelete": false} + candidates, _, err := mediadata.GetListFromCache( + filter, 0, dramaFeedCandidateLimit, + bson.D{{Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}, + ) + if err != nil { + return FeedResponse{}, err + } + ordered := orderFeedCandidates(candidates, now) + if len(ordered) == 0 { + return FeedResponse{QueueVersion: "drama-" + recommendqueue.VersionAt(now), List: []FeedItem{}}, nil + } + queueIDs := make([]string, 0, len(ordered)) + mediaByID := make(map[string]*mediamod.Media, len(ordered)) + for _, media := range ordered { + if media == nil { + continue + } + id := media.ID.Hex() + queueIDs = append(queueIDs, id) + mediaByID[id] = media + } + queueVersion := dramaFeedQueueVersion(now, queueIDs) + reservation, err := reserveDramaFeedPage( + ctx, appg.Redis, uid, int(pageSize)*dramaFeedScanMultiplier, queueVersion, queueIDs, + ) + if err != nil { + return FeedResponse{}, err + } + committed := false + defer func() { + if !committed { + abortCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + _ = abortDramaFeedPage(abortCtx, appg.Redis, uid, reservation) + } + }() + reservedMedias := make([]*mediamod.Media, 0, len(reservation.IDs)) + for _, id := range reservation.IDs { + if media := mediaByID[id]; media != nil { + reservedMedias = append(reservedMedias, media) + } + } + hasCard, err := mediacontentser.ActiveDramaCardStatus(uid) + if err != nil { + return FeedResponse{}, err + } + mediaResponses := mediaser.FillMedias(reservedMedias, uid, true, hasCard) + episodes, err := firstEpisodeMap(uid, reservedMedias, hasCard) + if err != nil { + return FeedResponse{}, err + } + responseByID := make(map[string]*mediamod.AppMediaBase, len(mediaResponses)) + for _, media := range mediaResponses { + if media != nil { + responseByID[media.ID.Hex()] = media + } + } + items := make([]FeedItem, 0, pageSize) + scanned := 0 + for _, id := range reservation.IDs { + scanned++ + media := responseByID[id] + if media == nil { + continue + } + episode, ok := episodes[media.ID] + if !ok { + continue + } + items = append(items, FeedItem{Media: media, Content: episode}) + if len(items) >= int(pageSize) { + break + } + } + if err := commitDramaFeedPage(ctx, appg.Redis, uid, reservation, scanned); err != nil { + return FeedResponse{}, err + } + committed = true + return FeedResponse{ + QueueVersion: "drama-" + reservation.Version, + HasNext: reservation.Length > len(items), + List: items, + }, nil +} + +// dramaFeedQueueVersion 将当天候选集快照写入版本。上架、下架导致候选集变化时, +// 新请求会切换到独立队列;ID 排序后再计算,避免仅推荐顺序变化造成队列频繁重建。 +func dramaFeedQueueVersion(now time.Time, queueIDs []string) string { + ids := append([]string(nil), queueIDs...) + sort.Strings(ids) + h := fnv.New64a() + for _, id := range ids { + _, _ = h.Write([]byte(id)) + _, _ = h.Write([]byte{0}) + } + return recommendqueue.VersionWithRevision(now, fmt.Sprintf("%016x", h.Sum64())) +} + +func orderFeedCandidates(candidates []*mediamod.Media, now time.Time) []*mediamod.Media { + recommended := make([]*mediamod.Media, 0) + regular := make([]vidmod.RecommendCandidate, 0) + mediaByID := make(map[primitive.ObjectID]*mediamod.Media, len(candidates)) + for _, media := range candidates { + if media == nil { + continue + } + mediaByID[media.ID] = media + candidate := vidmod.RecommendCandidate{ + ID: media.ID, ReviewAt: dramaReviewAt(media), RecommendScore: dramaInteractionScore(media), + } + if media.SortCode > 0 || !media.Choice.IsZero() { + recommended = append(recommended, media) + continue + } + regular = append(regular, candidate) + } + rng := rand.New(rand.NewSource(dramaQueueSeed(now))) + rng.Shuffle(len(recommended), func(i, j int) { + recommended[i], recommended[j] = recommended[j], recommended[i] + }) + // 直接复用91P短视频队列的17条互动高分+3条24小时新内容组装算法。 + regularIDs := recommendqueue.Assemble(regular, now) + ordered := make([]*mediamod.Media, 0, len(candidates)) + ordered = append(ordered, recommended...) + ordered = appendDramaIDs(ordered, regularIDs, mediaByID) + return ordered +} + +func dramaQueueSeed(now time.Time) int64 { + h := fnv.New64a() + _, _ = h.Write([]byte("drama:" + recommendqueue.VersionAt(now))) + return int64(h.Sum64()) +} + +func dramaInteractionScore(media *mediamod.Media) int64 { + if media == nil { + return 0 + } + return vidmod.CalculateRecommendScore(media.CountLike, media.CountCollect, media.CountComment, media.CountShare) +} + +func dramaReviewAt(media *mediamod.Media) time.Time { + if media != nil && !media.LatestPublishedAt.IsZero() { + return media.LatestPublishedAt + } + if media == nil { + return time.Time{} + } + return media.CreatedAt +} + +func appendDramaIDs( + ordered []*mediamod.Media, + ids []string, + mediaByID map[primitive.ObjectID]*mediamod.Media, +) []*mediamod.Media { + for _, rawID := range ids { + id, err := primitive.ObjectIDFromHex(rawID) + if err != nil { + continue + } + if media := mediaByID[id]; media != nil { + ordered = append(ordered, media) + } + } + return ordered +} + +func firstEpisodeMap(uid uint64, medias []*mediamod.Media, hasCard bool) (map[primitive.ObjectID]mediacontentser.AppMediaContent, error) { + result := make(map[primitive.ObjectID]mediacontentser.AppMediaContent, len(medias)) + if len(medias) == 0 { + return result, nil + } + mediaIDs := make([]primitive.ObjectID, 0, len(medias)) + mediaMap := make(map[primitive.ObjectID]*mediamod.Media, len(medias)) + for _, media := range medias { + if media != nil { + mediaIDs = append(mediaIDs, media.ID) + mediaMap[media.ID] = media + } + } + contents, err := mediacontentmod.QueryAllList( + bson.M{"mediaId": bson.M{"$in": mediaIDs}, "mediaType": mediamod.MediaTypeDrama, "episodeNumber": 1, "isActive": true, "isDelete": false}, + options.Find().SetSort(bson.D{{Key: "createdAt", Value: 1}}), + ) + if err != nil { + return nil, err + } + contentIDs := make([]primitive.ObjectID, 0, len(contents)) + for _, content := range contents { + if content != nil { + contentIDs = append(contentIDs, content.ID) + } + } + bought, err := media_buy_record_mod.IsContentBuyBatch(uid, contentIDs) + if err != nil { + return nil, err + } + for _, content := range contents { + if content == nil || result[content.MediaID].ID != primitive.NilObjectID { + continue + } + media := mediaMap[content.MediaID] + if media == nil { + continue + } + isFree, canPlay, accessType := mediacontentser.ResolveDramaAccess(*content, *media, bought[content.ID], hasCard) + name := strings.TrimSpace(content.Name) + if name == "" { + name = fmt.Sprintf("第%d集", content.EpisodeNumber) + } + item := mediacontentser.AppMediaContent{ + ID: content.ID, MediaID: content.MediaID, MediaType: content.MediaType, + EpisodeNumber: content.EpisodeNumber, ListenPermission: content.ListenPermission, + VideoUrl: content.VideoUrl, H265Url: appg.H265URLForApp(content.H265Url), AudioUrl: content.AudioUrl, + PlayTime: content.PlayTime, Price: content.Price, Name: name, Cover: content.Cover, + HasBuy: bought[content.ID], IsFree: isFree, HasDramaCard: hasCard, + CanPlay: canPlay, AccessType: accessType, Ratio: content.Ratio, + PreviewEnabled: content.PreviewEnabled, PreviewStart: content.PreviewStart, PreviewSeconds: content.PreviewSeconds, + CreatedAt: content.CreatedAt, UpdateTime: content.UpdateTime, + } + if !item.CanPlay { + if item.PreviewEnabled { + item.PreviewVideoUrl, item.PreviewH265Url = item.VideoUrl, item.H265Url + } + item.VideoUrl, item.H265Url, item.AudioUrl = "", "", "" + } + result[content.MediaID] = item + } + return result, nil +} diff --git a/app/service/dramaser/drama_test.go b/app/service/dramaser/drama_test.go new file mode 100644 index 0000000..4c493f5 --- /dev/null +++ b/app/service/dramaser/drama_test.go @@ -0,0 +1,115 @@ +package dramaser + +import ( + "fmt" + "net/http/httptest" + "reflect" + "testing" + "time" + + "91porn-server/models/v/mediamod" + + "github.com/gin-gonic/gin/binding" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestDramaListSortMatchesMediaLibraryEnum(t *testing.T) { + tests := []struct { + name string + sortType int + want bson.D + }{ + {name: "热门推荐", sortType: 1, want: bson.D{{Key: "sortCode", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}}, + {name: "最新上架", sortType: 2, want: bson.D{{Key: "latestPublishedAt", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}}, + {name: "最热", sortType: 3, want: bson.D{{Key: "countLike", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}}, + {name: "最多收藏", sortType: 4, want: bson.D{{Key: "countCollect", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + request := httptest.NewRequest("GET", fmt.Sprintf("/?pageNumber=1&pageSize=20&sortType=%d", test.sortType), nil) + var params ListRequest + if err := binding.Query.Bind(request, ¶ms); err != nil { + t.Fatalf("bind sortType=%d: %v", test.sortType, err) + } + if got := dramaListSort(params.SortType); !reflect.DeepEqual(got, test.want) { + t.Fatalf("dramaListSort(%d) = %#v, want %#v", test.sortType, got, test.want) + } + }) + } + + legacyRequest := httptest.NewRequest("GET", "/?pageNumber=1&pageSize=20&sortType=7", nil) + if err := binding.Query.Bind(legacyRequest, &ListRequest{}); err == nil { + t.Fatal("bind legacy sortType=7 succeeded, want validation error") + } +} + +func TestDramaFeedQueueVersionChangesWithCandidateSet(t *testing.T) { + now := time.Date(2026, 8, 28, 10, 0, 0, 0, time.FixedZone("CST", 8*60*60)) + first := dramaFeedQueueVersion(now, []string{"media-b", "media-a"}) + sameSet := dramaFeedQueueVersion(now.Add(time.Hour), []string{"media-a", "media-b"}) + withNewDrama := dramaFeedQueueVersion(now.Add(time.Hour), []string{"media-a", "media-b", "media-c"}) + nextDay := dramaFeedQueueVersion(now.Add(24*time.Hour), []string{"media-a", "media-b"}) + + if first != sameSet { + t.Fatalf("same-day candidate set produced different versions: %q != %q", first, sameSet) + } + if first == withNewDrama { + t.Fatalf("new candidate did not change queue version: %q", first) + } + if first == nextDay { + t.Fatalf("next day did not change queue version: %q", first) + } +} + +func TestOrderFeedCandidatesPrioritizesRecommendedAndIsStable(t *testing.T) { + now := time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC) + recommended := &mediamod.Media{ID: primitive.NewObjectID(), SortCode: 10, CreatedAt: now.Add(-time.Hour)} + high := &mediamod.Media{ID: primitive.NewObjectID(), CountLike: 100, CreatedAt: now.Add(-48 * time.Hour)} + recent := &mediamod.Media{ID: primitive.NewObjectID(), CreatedAt: now.Add(-time.Hour)} + + candidates := []*mediamod.Media{high, recent, recommended} + ordered := orderFeedCandidates(candidates, now) + again := orderFeedCandidates(candidates, now.Add(time.Minute)) + if len(ordered) != 3 || len(again) != 3 { + t.Fatalf("ordered lengths = %d/%d, want 3/3", len(ordered), len(again)) + } + if ordered[0].ID != recommended.ID { + t.Fatalf("first item = %s, want recommended %s", ordered[0].ID, recommended.ID) + } + for i := range ordered { + if ordered[i].ID != again[i].ID { + t.Fatalf("same-day queue changed at %d: %s != %s", i, ordered[i].ID, again[i].ID) + } + } +} + +func TestValidateAnalyticsEventRequiresCheckoutContextForPaywall(t *testing.T) { + event := AnalyticsEvent{ + EventID: "event-1", EventName: "DRAMA_PAYWALL_EXPOSURE", SessionID: "session-1", + OccurredAt: time.Now(), MediaID: primitive.NewObjectID(), ContentID: primitive.NewObjectID(), + Scene: "detail", + } + if err := validateAnalyticsEvent(event); err == nil { + t.Fatal("validateAnalyticsEvent() should reject missing checkoutContextId") + } + event.CheckoutContextID = "drama-checkout-test" + if err := validateAnalyticsEvent(event); err != nil { + t.Fatalf("validateAnalyticsEvent() error = %v", err) + } +} + +func TestDramaInteractionScoreUses91PWeights(t *testing.T) { + media := &mediamod.Media{ + CountLike: 1, CountCollect: 2, CountComment: 3, CountShare: 4, CountBrowse: 999, + } + if got, want := dramaInteractionScore(media), int64(34); got != want { + t.Fatalf("dramaInteractionScore() = %d, want %d", got, want) + } +} + +func TestObjectIDStringReturnsEmptyForZeroID(t *testing.T) { + if got := objectIDString(primitive.NilObjectID); got != "" { + t.Fatalf("objectIDString(zero) = %q, want empty", got) + } +} diff --git a/app/service/dramaser/feed_cursor.go b/app/service/dramaser/feed_cursor.go new file mode 100644 index 0000000..b079e61 --- /dev/null +++ b/app/service/dramaser/feed_cursor.go @@ -0,0 +1,274 @@ +package dramaser + +import ( + "context" + cryptorand "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "strconv" + "time" + + redisutil "91porn-server/common/redis" +) + +const ( + dramaFeedScanMultiplier = 5 + dramaFeedQueueTTL = 72 * time.Hour + dramaFeedLeaseTTL = 15 * time.Second +) + +var ( + errDramaFeedQueueMissing = errors.New("drama feed queue is missing") + errDramaFeedCursorBusy = errors.New("drama feed cursor is busy") +) + +type dramaFeedReservation struct { + Version string + Length int + Offset int + Reserved int + Token string + IDs []string +} + +type dramaFeedScriptClient interface { + RunScriptContext(context.Context, *redisutil.Script, []string, ...interface{}) (interface{}, error) +} + +const publishDramaFeedQueueScriptSource = ` +if redis.call('EXISTS', KEYS[1]) == 1 then + return 0 +end +for i = 2, #ARGV do + redis.call('RPUSH', KEYS[1], ARGV[i]) +end +redis.call('EXPIRE', KEYS[1], ARGV[1]) +return 1 +` + +const reserveDramaFeedPageScriptSource = ` +local length = redis.call('LLEN', KEYS[1]) +if length <= 0 then + return {'QUEUE_MISSING'} +end +if redis.call('EXISTS', KEYS[3]) == 1 then + return {'BUSY'} +end +local offset = tonumber(redis.call('HGET', KEYS[2], ARGV[1]) or '0') % length +local reserved = math.min(tonumber(ARGV[2]), length) +redis.call('HSET', KEYS[3], + 'token', ARGV[3], 'length', length, 'offset', offset, 'reserved', reserved) +redis.call('EXPIRE', KEYS[3], ARGV[4]) +local result = {'RESERVED', ARGV[5], tostring(length), tostring(offset), tostring(reserved), ARGV[3]} +for i = 0, reserved - 1 do + table.insert(result, redis.call('LINDEX', KEYS[1], (offset + i) % length)) +end +return result +` + +const commitDramaFeedPageScriptSource = ` +local lease = redis.call('HMGET', KEYS[3], 'token', 'length', 'offset', 'reserved') +if not lease[1] then return -1 end +if lease[1] ~= ARGV[2] or tonumber(lease[2]) ~= tonumber(ARGV[3]) or + tonumber(lease[3]) ~= tonumber(ARGV[4]) or tonumber(lease[4]) ~= tonumber(ARGV[5]) then + return -2 +end +local length = redis.call('LLEN', KEYS[1]) +if length <= 0 or length ~= tonumber(ARGV[3]) then return -3 end +local consumed = tonumber(ARGV[6]) +if not consumed or consumed <= 0 or consumed > tonumber(ARGV[5]) then return -2 end +local currentOffset = tonumber(redis.call('HGET', KEYS[2], ARGV[1]) or '0') % length +if currentOffset ~= tonumber(ARGV[4]) then return -2 end +redis.call('HSET', KEYS[2], ARGV[1], (currentOffset + consumed) % length) +redis.call('EXPIRE', KEYS[2], ARGV[7]) +redis.call('DEL', KEYS[3]) +return 1 +` + +const abortDramaFeedPageScriptSource = ` +if redis.call('HGET', KEYS[1], 'token') ~= ARGV[1] then + return 0 +end +return redis.call('DEL', KEYS[1]) +` + +var ( + publishDramaFeedQueueScript = redisutil.NewScript(publishDramaFeedQueueScriptSource) + reserveDramaFeedPageScript = redisutil.NewScript(reserveDramaFeedPageScriptSource) + commitDramaFeedPageScript = redisutil.NewScript(commitDramaFeedPageScriptSource) + abortDramaFeedPageScript = redisutil.NewScript(abortDramaFeedPageScriptSource) +) + +func dramaFeedQueueKey(version string) string { + return "recommend:drama:queue:" + version +} + +func dramaFeedOffsetKey(version string) string { + return "recommend:drama:offset:" + version +} + +func dramaFeedLeaseKey(version string, uid uint64) string { + return "recommend:drama:reservation:" + version + ":" + strconv.FormatUint(uid, 10) +} + +func reserveDramaFeedPage( + ctx context.Context, + client dramaFeedScriptClient, + uid uint64, + size int, + version string, + queueIDs []string, +) (dramaFeedReservation, error) { + if ctx == nil { + return dramaFeedReservation{}, fmt.Errorf("drama feed context must not be nil") + } + if client == nil { + return dramaFeedReservation{}, fmt.Errorf("drama feed Redis client must not be nil") + } + if uid == 0 || size <= 0 || version == "" || len(queueIDs) == 0 { + return dramaFeedReservation{}, fmt.Errorf("drama feed reservation arguments are invalid") + } + token, err := newDramaFeedToken() + if err != nil { + return dramaFeedReservation{}, err + } + reservation, err := runReserveDramaFeedPage(ctx, client, uid, size, version, token) + if !errors.Is(err, errDramaFeedQueueMissing) { + return reservation, err + } + args := make([]interface{}, 1, len(queueIDs)+1) + args[0] = int64(dramaFeedQueueTTL / time.Second) + for _, id := range queueIDs { + args = append(args, id) + } + if _, err = client.RunScriptContext( + ctx, publishDramaFeedQueueScript, []string{dramaFeedQueueKey(version)}, args..., + ); err != nil { + return dramaFeedReservation{}, err + } + return runReserveDramaFeedPage(ctx, client, uid, size, version, token) +} + +func runReserveDramaFeedPage( + ctx context.Context, + client dramaFeedScriptClient, + uid uint64, + size int, + version, token string, +) (dramaFeedReservation, error) { + raw, err := client.RunScriptContext( + ctx, + reserveDramaFeedPageScript, + []string{ + dramaFeedQueueKey(version), + dramaFeedOffsetKey(version), + dramaFeedLeaseKey(version, uid), + }, + strconv.FormatUint(uid, 10), + size, + token, + int64(dramaFeedLeaseTTL/time.Second), + version, + ) + if err != nil { + return dramaFeedReservation{}, err + } + return parseDramaFeedReservation(raw) +} + +func parseDramaFeedReservation(raw interface{}) (dramaFeedReservation, error) { + items, ok := raw.([]interface{}) + if !ok || len(items) == 0 { + return dramaFeedReservation{}, fmt.Errorf("drama feed reserve returned malformed result") + } + switch fmt.Sprint(items[0]) { + case "QUEUE_MISSING": + return dramaFeedReservation{}, errDramaFeedQueueMissing + case "BUSY": + return dramaFeedReservation{}, errDramaFeedCursorBusy + case "RESERVED": + default: + return dramaFeedReservation{}, fmt.Errorf("drama feed reserve returned unknown status") + } + if len(items) < 7 { + return dramaFeedReservation{}, fmt.Errorf("drama feed reserve returned incomplete result") + } + result := dramaFeedReservation{Version: fmt.Sprint(items[1]), Token: fmt.Sprint(items[5])} + var err error + if result.Length, err = strconv.Atoi(fmt.Sprint(items[2])); err != nil { + return dramaFeedReservation{}, fmt.Errorf("drama feed reserve returned invalid length: %w", err) + } + if result.Offset, err = strconv.Atoi(fmt.Sprint(items[3])); err != nil { + return dramaFeedReservation{}, fmt.Errorf("drama feed reserve returned invalid offset: %w", err) + } + if result.Reserved, err = strconv.Atoi(fmt.Sprint(items[4])); err != nil { + return dramaFeedReservation{}, fmt.Errorf("drama feed reserve returned invalid size: %w", err) + } + if result.Version == "" || result.Token == "" || result.Length <= 0 || + result.Offset < 0 || result.Offset >= result.Length || result.Reserved <= 0 || + result.Reserved > result.Length || len(items)-6 != result.Reserved { + return dramaFeedReservation{}, fmt.Errorf("drama feed reserve returned invalid bounds") + } + for _, item := range items[6:] { + result.IDs = append(result.IDs, fmt.Sprint(item)) + } + return result, nil +} + +func commitDramaFeedPage( + ctx context.Context, + client dramaFeedScriptClient, + uid uint64, + reservation dramaFeedReservation, + consumed int, +) error { + if client == nil { + return fmt.Errorf("drama feed Redis client must not be nil") + } + raw, err := client.RunScriptContext( + ctx, + commitDramaFeedPageScript, + []string{ + dramaFeedQueueKey(reservation.Version), + dramaFeedOffsetKey(reservation.Version), + dramaFeedLeaseKey(reservation.Version, uid), + }, + strconv.FormatUint(uid, 10), reservation.Token, reservation.Length, + reservation.Offset, reservation.Reserved, consumed, + int64(dramaFeedQueueTTL/time.Second), + ) + if err != nil { + return err + } + if fmt.Sprint(raw) != "1" { + return fmt.Errorf("drama feed commit rejected: %v", raw) + } + return nil +} + +func abortDramaFeedPage( + ctx context.Context, + client dramaFeedScriptClient, + uid uint64, + reservation dramaFeedReservation, +) error { + if client == nil || reservation.Version == "" || reservation.Token == "" { + return nil + } + _, err := client.RunScriptContext( + ctx, + abortDramaFeedPageScript, + []string{dramaFeedLeaseKey(reservation.Version, uid)}, + reservation.Token, + ) + return err +} + +func newDramaFeedToken() (string, error) { + value := make([]byte, 16) + if _, err := cryptorand.Read(value); err != nil { + return "", fmt.Errorf("generate drama feed reservation token: %w", err) + } + return hex.EncodeToString(value), nil +} diff --git a/app/service/dramaser/feed_cursor_test.go b/app/service/dramaser/feed_cursor_test.go new file mode 100644 index 0000000..172075b --- /dev/null +++ b/app/service/dramaser/feed_cursor_test.go @@ -0,0 +1,84 @@ +package dramaser + +import ( + "context" + "fmt" + "testing" + + redisutil "91porn-server/common/redis" +) + +type fakeDramaFeedRedis struct { + calls int + run func(int, *redisutil.Script, []string, []interface{}) (interface{}, error) +} + +func (f *fakeDramaFeedRedis) RunScriptContext( + _ context.Context, + script *redisutil.Script, + keys []string, + args ...interface{}, +) (interface{}, error) { + f.calls++ + return f.run(f.calls, script, keys, args) +} + +func TestReserveDramaFeedPagePublishesOnceAndReturnsRingSegment(t *testing.T) { + ids := []string{"a", "b", "c", "d"} + client := &fakeDramaFeedRedis{} + client.run = func(call int, script *redisutil.Script, keys []string, args []interface{}) (interface{}, error) { + switch call { + case 1: + if script != reserveDramaFeedPageScript { + t.Fatal("first call should reserve existing queue") + } + return []interface{}{"QUEUE_MISSING"}, nil + case 2: + if script != publishDramaFeedQueueScript || keys[0] != "recommend:drama:queue:20260826" { + t.Fatalf("unexpected publish call: keys=%v", keys) + } + if len(args) != len(ids)+1 { + t.Fatalf("publish args = %d, want %d", len(args), len(ids)+1) + } + return int64(1), nil + case 3: + if script != reserveDramaFeedPageScript { + t.Fatal("third call should reserve published queue") + } + token := fmt.Sprint(args[2]) + return []interface{}{"RESERVED", "20260826", "4", "2", "2", token, "c", "d"}, nil + default: + t.Fatalf("unexpected Redis call %d", call) + return nil, nil + } + } + + reservation, err := reserveDramaFeedPage(context.Background(), client, 7, 2, "20260826", ids) + if err != nil { + t.Fatalf("reserveDramaFeedPage() error = %v", err) + } + if reservation.Offset != 2 || reservation.Reserved != 2 || + fmt.Sprint(reservation.IDs) != "[c d]" { + t.Fatalf("reservation = %+v", reservation) + } +} + +func TestCommitDramaFeedPageAdvancesOnlyConsumedPrefix(t *testing.T) { + reservation := dramaFeedReservation{ + Version: "20260826", Length: 20, Offset: 5, Reserved: 10, + Token: "token", IDs: []string{"a", "b"}, + } + client := &fakeDramaFeedRedis{} + client.run = func(call int, script *redisutil.Script, keys []string, args []interface{}) (interface{}, error) { + if script != commitDramaFeedPageScript { + t.Fatal("expected commit script") + } + if keys[1] != "recommend:drama:offset:20260826" || fmt.Sprint(args[5]) != "4" { + t.Fatalf("commit keys/args mismatch: keys=%v args=%v", keys, args) + } + return int64(1), nil + } + if err := commitDramaFeedPage(context.Background(), client, 7, reservation, 4); err != nil { + t.Fatalf("commitDramaFeedPage() error = %v", err) + } +} diff --git a/app/service/dramaser/topic.go b/app/service/dramaser/topic.go new file mode 100644 index 0000000..f9ca4ac --- /dev/null +++ b/app/service/dramaser/topic.go @@ -0,0 +1,241 @@ +package dramaser + +import ( + "encoding/json" + "errors" + "fmt" + "sort" + "time" + + "91porn-server/app/appg" + "91porn-server/app/service/mediaser" + "91porn-server/common/dramatopic" + "91porn-server/common/log" + "91porn-server/models/commod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/modulesectionmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const everyoneLikesCacheTTL = 30 * time.Second + +type TopicSummary struct { + TopicID string `json:"topicId"` + Name string `json:"name"` + TopicType string `json:"topicType"` + SystemKey string `json:"systemKey"` + Sort int `json:"sort"` + WorkCount int64 `json:"workCount"` +} + +type TopicListResponse struct { + List []TopicSummary `json:"list"` +} + +type TopicWorksRequest struct { + TopicID string `form:"topicId" json:"topicId" binding:"required"` + commod.Page +} + +type TopicWorksResponse struct { + Topic TopicSummary `json:"topic"` + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []*mediamod.AppMediaBase `json:"list"` +} + +func GetTopics(now time.Time) (TopicListResponse, error) { + moduleID, ok, err := dramatopic.ModuleID(now) + if err != nil { + return TopicListResponse{}, err + } + total, err := mediamod.QueryAllCount(activeDramaFilter()) + if err != nil { + return TopicListResponse{}, err + } + sections := make([]modulesectionmod.Section, 0) + if ok { + sections, err = modulesectionmod.ListBySubModule(moduleID, 1000) + if err != nil { + return TopicListResponse{}, err + } + } + sectionIDs := make([]primitive.ObjectID, 0, len(sections)) + for _, section := range sections { + if section.Status != nil && *section.Status == 1 { + sectionIDs = append(sectionIDs, section.ID) + } + } + counts, err := mediamod.CountActiveDramaBySectionIDs(sectionIDs) + if err != nil { + return TopicListResponse{}, err + } + items := make([]topicSortItem, 0, 3+len(sections)) + for _, system := range dramatopic.SystemTopics() { + items = append(items, topicSortItem{summary: TopicSummary{ + TopicID: system.ID, Name: system.Name, TopicType: dramatopic.TypeSystem, + SystemKey: system.SystemKey, Sort: system.Sort, WorkCount: total, + }, systemOrder: system.TieOrder}) + } + for _, section := range sections { + if section.Status == nil || *section.Status != 1 { + continue + } + items = append(items, topicSortItem{summary: TopicSummary{ + TopicID: section.ID.Hex(), Name: section.SectionName, TopicType: dramatopic.TypeCustom, + Sort: valueOrZero(section.Sort), WorkCount: counts[section.ID], + }, updatedAt: section.UpdatedAt}) + } + sortTopicItems(items) + response := TopicListResponse{List: make([]TopicSummary, 0, len(items))} + for _, item := range items { + response.List = append(response.List, item.summary) + } + return response, nil +} + +type topicSortItem struct { + summary TopicSummary + systemOrder int + updatedAt time.Time +} + +func sortTopicItems(items []topicSortItem) { + sort.SliceStable(items, func(i, j int) bool { + if items[i].summary.Sort != items[j].summary.Sort { + return items[i].summary.Sort > items[j].summary.Sort + } + if items[i].systemOrder != items[j].systemOrder { + return items[i].systemOrder > items[j].systemOrder + } + return items[i].updatedAt.After(items[j].updatedAt) + }) +} + +func GetTopicWorks(uid uint64, req TopicWorksRequest, now time.Time) (TopicWorksResponse, error) { + if system, ok := dramatopic.FindSystem(req.TopicID); ok { + return getSystemTopicWorks(uid, req, system) + } + moduleID, ok, err := dramatopic.ModuleID(now) + if err != nil || !ok { + if err == nil { + err = errors.New("短剧专题模块未配置") + } + return TopicWorksResponse{}, err + } + topicID, err := primitive.ObjectIDFromHex(req.TopicID) + if err != nil { + return TopicWorksResponse{}, errors.New("无效的专题ID") + } + section, err := modulesectionmod.GetBySectionByID(topicID) + if err != nil || section.SubModuleID != moduleID || section.DeletedAt != nil || section.Status == nil || *section.Status != 1 { + if err == nil { + err = errors.New("专题不存在或已停用") + } + return TopicWorksResponse{}, err + } + filter := activeDramaFilter() + filter["sId"] = topicID + total, err := mediamod.QueryAllCount(filter) + if err != nil { + return TopicWorksResponse{}, err + } + list, err := mediamod.QueryAllList(filter, options.Find(). + SetSkip(req.Skip64()).SetLimit(req.Limit64()+1). + SetSort(bson.D{{Key: "sectionSort", Value: -1}, {Key: "latestPublishedAt", Value: -1}, {Key: "_id", Value: -1}})) + if err != nil { + return TopicWorksResponse{}, err + } + hasNext := len(list) > int(req.PageSize) + if hasNext { + list = list[:req.PageSize] + } + return TopicWorksResponse{ + Topic: TopicSummary{TopicID: req.TopicID, Name: section.SectionName, TopicType: dramatopic.TypeCustom, Sort: valueOrZero(section.Sort), WorkCount: total}, + Total: total, HasNext: hasNext, List: fillTopicMedias(list, uid), + }, nil +} + +func getSystemTopicWorks(uid uint64, req TopicWorksRequest, topic dramatopic.SystemTopic) (TopicWorksResponse, error) { + filter := activeDramaFilter() + total, err := mediamod.QueryAllCount(filter) + if err != nil { + return TopicWorksResponse{}, err + } + var list []*mediamod.Media + var hasNext bool + if topic.ID == dramatopic.IDEveryone { + list, err = listEveryoneLikes(req.Skip64(), req.Limit64()+1) + if err != nil { + return TopicWorksResponse{}, err + } + hasNext = len(list) > int(req.PageSize) + if hasNext { + list = list[:req.PageSize] + } + } else { + sortBy := bson.D{{Key: "sortCode", Value: -1}, {Key: "latestPublishedAt", Value: -1}, {Key: "_id", Value: -1}} + if topic.ID == dramatopic.IDLatest { + sortBy = bson.D{{Key: "latestPublishedAt", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}} + } + list, err = mediamod.QueryAllList(filter, options.Find().SetSkip(req.Skip64()).SetLimit(req.Limit64()+1).SetSort(sortBy)) + if err != nil { + return TopicWorksResponse{}, err + } + hasNext = len(list) > int(req.PageSize) + if hasNext { + list = list[:req.PageSize] + } + } + return TopicWorksResponse{ + Topic: TopicSummary{TopicID: topic.ID, Name: topic.Name, TopicType: dramatopic.TypeSystem, SystemKey: topic.SystemKey, Sort: topic.Sort, WorkCount: total}, + Total: total, HasNext: hasNext, List: fillTopicMedias(list, uid), + }, nil +} + +func fillTopicMedias(list []*mediamod.Media, uid uint64) []*mediamod.AppMediaBase { + filled := mediaser.FillMedias(list, uid, true) + if filled == nil { + return []*mediamod.AppMediaBase{} + } + return filled +} + +func listEveryoneLikes(skip, limit int64) ([]*mediamod.Media, error) { + key := fmt.Sprintf("drama:topic:everyone-likes:v1:%d:%d", skip, limit) + if appg.Redis != nil { + cached, err := appg.Redis.Get(key) + if err == nil && cached != nil { + var list []*mediamod.Media + if jsonErr := json.Unmarshal([]byte(*cached), &list); jsonErr == nil { + return list, nil + } + } + } + list, err := mediamod.ListActiveDramaByInteractionScore(skip, limit) + if err != nil { + return nil, err + } + if appg.Redis != nil { + if data, marshalErr := json.Marshal(list); marshalErr == nil { + if cacheErr := appg.Redis.Set(key, data, everyoneLikesCacheTTL); cacheErr != nil { + log.Warn("缓存大家爱看短剧列表失败", log.E(cacheErr)) + } + } + } + return list, nil +} + +func activeDramaFilter() bson.M { + return bson.M{"mediaType": mediamod.MediaTypeDrama, "status": 1, "isDelete": false} +} + +func valueOrZero(value *int) int { + if value == nil { + return 0 + } + return *value +} diff --git a/app/service/dramaser/type.go b/app/service/dramaser/type.go new file mode 100644 index 0000000..2d0f491 --- /dev/null +++ b/app/service/dramaser/type.go @@ -0,0 +1,85 @@ +package dramaser + +import ( + "time" + + "91porn-server/app/service/mediacontentser" + "91porn-server/models/commod" + "91porn-server/models/v/mediamod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + TabLive = "live" + TabDouyin = "douyin" + TabAIDrama = "ai_drama" + TabHotDrama = "hot_drama" +) + +type ChannelTab struct { + Key string `json:"key"` + Title string `json:"title"` + Enabled bool `json:"enabled"` + Sort int `json:"sort"` + ModuleID string `json:"moduleId"` +} + +type ChannelConfig struct { + BottomTabTitle string `json:"bottomTabTitle"` + DefaultTab string `json:"defaultTab"` + AdEnabled bool `json:"adEnabled"` + Tabs []ChannelTab `json:"tabs"` +} + +type FeedRequest struct { + PageSize uint64 `form:"pageSize" json:"pageSize" binding:"required,min=1,max=50"` +} + +type FeedItem struct { + Media *mediamod.AppMediaBase `json:"media"` + Content mediacontentser.AppMediaContent `json:"content"` +} + +type FeedResponse struct { + QueueVersion string `json:"queueVersion"` + HasNext bool `json:"hasNext"` + List []FeedItem `json:"list"` +} + +type ListRequest struct { + commod.Page + SortType int `form:"sortType" json:"sortType" binding:"required,oneof=1 2 3 4"` +} + +type ListResponse struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []*mediamod.AppMediaBase `json:"list"` +} + +type AnalyticsEvent struct { + EventID string `json:"eventId" binding:"required,max=128"` + EventName string `json:"eventName" binding:"required"` + SessionID string `json:"sessionId" binding:"required,max=128"` + OccurredAt time.Time `json:"occurredAt" binding:"required"` + MediaID primitive.ObjectID `json:"mediaId" binding:"required"` + ContentID primitive.ObjectID `json:"contentId"` + EpisodeNumber int `json:"episodeNumber"` + Scene string `json:"scene" binding:"required"` + PlayMode string `json:"playMode"` + Progress int `json:"progressSeconds"` + Duration int `json:"durationSeconds"` + QueueVersion string `json:"queueVersion"` + Position int `json:"position"` + CheckoutContextID string `json:"checkoutContextId"` +} + +type EventsRequest struct { + Events []AnalyticsEvent `json:"events" binding:"required,min=1,max=50,dive"` +} + +type EventsResponse struct { + Accepted int `json:"accepted"` + Duplicated int `json:"duplicated"` +} diff --git a/app/service/emailser/email.go b/app/service/emailser/email.go new file mode 100644 index 0000000..7548a94 --- /dev/null +++ b/app/service/emailser/email.go @@ -0,0 +1,54 @@ +package emailser + +import ( + "context" + "strings" + + "91porn-server/app/appg" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/stderr" + + "github.com/badoux/checkmail" +) + +// CheckEmail 校验邮件地址是否合法 +func CheckEmail(ctx context.Context, emailStr string) stderr.Code { + if emailStr == "" { + return stderr.ErrInvalidEmail + } + if err := checkmail.ValidateFormat(emailStr); err != nil { + log.ErrorX(ctx, "invalid email", log.Any("email", emailStr), log.E(err)) + return stderr.ErrInvalidEmail + } + return stderr.Success +} + +// VerifyCaptchaCode 核对验证码 +func VerifyCaptchaCode(ctx context.Context, emailAddr, code string) stderr.Code { + emailAddr = strings.TrimSpace(emailAddr) + if strings.HasSuffix(emailAddr, "@dsp.xyz") { // pass the test email + log.InfoX(ctx, "VerifyCaptchaCode dismiss test email", log.Any("email", emailAddr)) + return stderr.Success + } + if code := CheckEmail(ctx, emailAddr); code != stderr.Success { + return code + } + redisKey := redisconst.GetEmailCaptchaKey(emailAddr) + redisCode, err := appg.Redis.Get(redisKey) + if err != nil { + log.ErrorX(ctx, "VerifyCaptchaCode redis get err", log.Any("emailAddr", emailAddr), + log.Any("redisKey", redisKey), log.E(err)) + return stderr.ErrNetWorkBusy + } + if redisCode == nil { + log.InfoX(ctx, "VerifyCaptchaCode nil code in redis", log.Any("emailAddr", emailAddr), log.Any("code", code)) + return stderr.ErrCaptcha + } + if *redisCode != code { + log.InfoX(ctx, "VerifyCaptchaCode invalid", log.Any("emailAddr", emailAddr), log.Any("code", code), + log.Any("redisCode", redisCode)) + return stderr.ErrCaptchaInvalid + } + return stderr.Success +} diff --git a/app/service/exchcodeser/exchcodeser.go b/app/service/exchcodeser/exchcodeser.go new file mode 100644 index 0000000..9cf6518 --- /dev/null +++ b/app/service/exchcodeser/exchcodeser.go @@ -0,0 +1,203 @@ +package exchcodeser + +import ( + "91porn-server/models/v/backpackmod" + "91porn-server/models/v/videocoupon" + "strconv" + "time" + + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/exchlogmod" + "91porn-server/models/v/exchcodemod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" +) + +// 兑换码兑换 +func CodeExchange(uid uint64, exchangeCode string) (code stderr.Code, data interface{}, err error) { + // 获取该兑换码 + cMod, err := exchcodemod.GetExchangeCodeByCode(exchangeCode) + if err != nil || cMod == nil { + code = stderr.ExchangeCodeExchangeFailed + return + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + code = stderr.UserIsNotExists + return + } + if cMod.Channel == "XUA01" && cMod.Channel != u.DistrictCode { + code = stderr.ExchangeCodeInvalidAuthority + return + } + // 无该兑换码或者不是此app的兑换码 + if cMod == nil || cMod.App != exchcodemod.AppTypeYSVideo || cMod.Authority == exchcodemod.AuthorityFilmVip { + code = stderr.ExchangeCodeInvalidCode + return + } + var now = time.Now() + // 未生效 + if cMod.EffectiveAt.After(now) { + code = stderr.ExchangeCodeIsNotEffective + return + } + // 已过期 + if cMod.InvalidAt.Before(now) { + code = stderr.ExchangeCodeIsExpired + return + } + //使用次数已满 + if cMod.UsableNum <= 0 { + code = stderr.ExchangeCodeInvalidCode + return + } + // 同一个的兑换码不能兑换两次 + lMod, err := exchlogmod.GetLogByUIDAndCode(uid, cMod.Code) + if err != nil || (!lMod.ID.IsZero() && cMod.BatchNum != "YS-dzwj") { + code = stderr.ExchangeCodeRepeatExchange + return + } + // 兑换 + if code, err = exchange(uid, cMod); err != nil || code != stderr.Success { + return + } + return doReward(uid, cMod.Authority, cMod.Reward, cMod.RewardCount) +} + +// 兑换并记录日志 +func exchange(uid uint64, cMod *exchcodemod.ExchCode) (code stderr.Code, err error) { + if err = exchcodemod.Exchange(cMod.Code); err != nil { + code = stderr.ExchangeCodeExchangeFailed + return + } + // 插入日志 + var logMod = exchlogmod.ExchangeLog{ + Code: cMod.Code, + UserID: uid, + BatchNum: cMod.BatchNum, + Channel: cMod.Channel, + Authority: cMod.Authority, + Reward: cMod.Reward, + RewardCount: cMod.RewardCount, + CreatedAt: time.Now(), + } + if err = exchlogmod.Insert(logMod); err != nil { + code = stderr.ExchangeCodeExchangeFailed + return + } + code = stderr.Success + return +} + +// 发放奖品 +func doReward(uid uint64, eType exchcodemod.AuthorityType, reward int, rewardCount int) (code stderr.Code, data interface{}, err error) { + // 兼容之前的数据 + if rewardCount == 0 { + rewardCount = 1 + } + now := time.Now() + // 获取用户信息 + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + code = stderr.UserIsNotExists + return + } + switch eType { + case exchcodemod.AuthorityGold: + if _, err := walletmod.CreditAmount(nil, int64(reward*rewardCount), uid); err != nil { + code = stderr.ExchangeCodeExchangeFailed + return code, nil, err + } + case exchcodemod.AuthorityShortVideoVip, exchcodemod.AuthorityFilmVip, exchcodemod.AuthoritySuper: // vip增加天数 + var now = time.Now() + var vipExpireDate = time.Time{} + if u.VipExpireDate.Before(now) { + vipExpireDate = now.AddDate(0, 0, reward*rewardCount) + } else { + vipExpireDate = u.VipExpireDate.AddDate(0, 0, reward*rewardCount) + } + vipLevel := usermod.RenewVIPLevel(u.VipExpireDate, now, u.VipLevel, 1) + var update = usermod.UserSelector{VipExpireDate: &vipExpireDate, VipLevel: &vipLevel} + if u, err = usermod.Update(uid, update); err != nil || u == nil { + code = stderr.ExchangeCodeExchangeFailed + return code, nil, err + } + data = u + case exchcodemod.Authority3dPermanentVIP: + s := usermod.ThreeDayVIP_SnapVip + permanentVIPLevel := 2 + permanentVIPExpireDate := now.AddDate(30, 0, 0) + if u, err := usermod.UpdateSnapVip(u.UID, usermod.UserSelector{SnapVip: &s, VipLevel: &permanentVIPLevel, VipExpireDate: &permanentVIPExpireDate, OriginVipLevel: &u.VipLevel, OriginVipExpire: &u.VipExpireDate}); err != nil || u == nil { + code = stderr.ExchangeCodeExchangeFailed + return code, nil, err + } + case exchcodemod.AuthorityVideoCoupon: // 观影券 + for i := 0; i < rewardCount; i++ { + videoCoupon := videocoupon.UserGoldVideoCoupon{ + UID: uid, + Num: reward, + Used: false, + Source: videocoupon.GoldVideoCouponSourceExchange, + UpdatedAt: now, + CreatedAt: now, + } + if err := videocoupon.InsertOne(videoCoupon); err != nil { + code = stderr.ExchangeCodeExchangeFailed + continue + } + backpack := backpackmod.Backpack{ + UID: uid, + GoodsType: backpackmod.ExchangeCoupon, + GoodsValue: int64(videoCoupon.Num), + GoodsOrigin: string(videocoupon.GoldVideoCouponSourceExchange), + Status: backpackmod.Unused, + ExpiredTime: now.AddDate(0, 0, 7), + CreateTime: now, + } + backpackmod.AddGoods(nil, uid, backpack) + } + default: + code = stderr.ExchangeCodeInvalidCode + return + } + code = stderr.Success + return +} + +// 查询用户兑换记录 +func UserExchageRecord(uid uint64, page commod.Page) (stderr.Code, interface{}, error) { + total, data, err := exchlogmod.GetExchangeLogList(exchlogmod.FilterDoc{ + UserID: &uid, + }, page) + if err != nil { + return stderr.ExchangeCodeExchangeFailed, nil, nil + } + for i := range data { + // 兼容兑换量 + if data[i].RewardCount == 0 { + data[i].RewardCount = 1 + } + switch data[i].Authority { + case exchcodemod.AuthorityShortVideoVip: + data[i].Desc = "视频VIPx" + strconv.Itoa(data[i].RewardCount) + case exchcodemod.AuthorityFilmVip: + data[i].Desc = "影视VIP(影院)" + strconv.Itoa(data[i].RewardCount) + case exchcodemod.AuthoritySuper: + data[i].Desc = "超级VIPx" + strconv.Itoa(data[i].RewardCount) + case exchcodemod.AuthorityGold: + data[i].Desc = "兑换金币x" + strconv.Itoa(data[i].Reward*data[i].RewardCount) + case exchcodemod.Authority3dPermanentVIP: + data[i].Desc = "3天VIPx" + strconv.Itoa(data[i].Reward*data[i].RewardCount) + case exchcodemod.AuthorityVideoCoupon: + data[i].Desc = "观影券" + strconv.Itoa(data[i].Reward) + "x" + strconv.Itoa(data[i].RewardCount) + "张" + } + } + return stderr.Success, struct { + Data []exchlogmod.ExchangeLog `json:"data"` + Total int64 `json:"total"` + }{ + Data: data, + Total: total, + }, nil +} diff --git a/app/service/feedbackser/feedback.go b/app/service/feedbackser/feedback.go new file mode 100644 index 0000000..2285f57 --- /dev/null +++ b/app/service/feedbackser/feedback.go @@ -0,0 +1,39 @@ +package feedbackser + +import ( + "91porn-server/common/stderr" + "91porn-server/middleware/ua" + "91porn-server/models/commod" + "91porn-server/models/v/feedbackmod" +) + +type FeedbackReq struct { + CateInfo string `json:"cateInfo" binding:"lte=20"` // 问题分类 限制10个字 + Desc string `json:"desc" binding:"required,lte=100"` // 描述 限制50个字 + Region string `json:"region" binding:"lte=30"` // 地区 限制10个字 + DevInfo string `json:"devInfo" binding:"lte=20"` // 设备信息 限制10个字 + ISP string `json:"ISP" binding:"lte=20"` // 运营商 限制10个字 + Contact string `json:"contact" binding:"lte=30"` // 联系方式 限制20个字 + Images []string `json:"images"` // 图片信息 +} + +func (q FeedbackReq) Submit(ua ua.UA, uid uint64) (code stderr.Code, err error) { + var cInfo = feedbackmod.ContentInfo{ + CateInfo: q.CateInfo, + Desc: q.Desc, + Region: q.Region, + DevInfo: q.DevInfo, + ISP: q.ISP, + Contact: q.Contact, + Images: q.Images, + } + return feedbackmod.FeedBackInsert(feedbackmod.FeedBack{UID: uid, Content: cInfo, Version: ua.Ver, DevType: ua.DevType}) +} + +// GetFeedBackList 获取反馈列表 +func GetFeedBackList(page commod.Page, uid uint64) (data commod.ListResp, err error) { + var feedbacks []feedbackmod.FeedBack + feedbacks, data.HasNext, err = feedbackmod.GetFeedBackList(page.PageSize, page.PageNumber, uid) + data.List = feedbacks + return +} diff --git a/app/service/filterser/filter.go b/app/service/filterser/filter.go new file mode 100644 index 0000000..bfa0f87 --- /dev/null +++ b/app/service/filterser/filter.go @@ -0,0 +1,27 @@ +package filterser + +import ( + "91porn-server/common/filter" + "91porn-server/common/log" +) + +func TextFilter(content string) (string, error) { + if filter.WordFilter == nil { + return content, nil + } + dst, err := filter.WordFilter.Replace(content, filter.ReplaceMark) + if err != nil { + log.Error("service filter TextFilter Replace error", log.E(err)) + } + return dst, err +} + +// CheckIllegalWords checks if user's input has illegal words. +// Return non-zero when there are illegal words, return err when something unexpected happened. +func CheckIllegalWords(content string) (int, error) { + if filter.WordFilter == nil { + return 1, nil + } + illegalWords, err := filter.WordFilter.Filter(content) + return len(illegalWords), err +} diff --git a/app/service/followser/followser.go b/app/service/followser/followser.go new file mode 100644 index 0000000..9509bb5 --- /dev/null +++ b/app/service/followser/followser.go @@ -0,0 +1,360 @@ +package followser + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/vidhelpser" + "91porn-server/common/log" + "91porn-server/common/services/follow" + "91porn-server/common/stderr" + topser "91porn-server/common/top" + "91porn-server/common/top/weektop" + "91porn-server/models/commod" + "91porn-server/models/v/followmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "errors" + "fmt" + goredis "github.com/go-redis/redis" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" + "strconv" + "time" +) + +func encodeUserBase(uids []uint64, uInfo []*usermod.BaseInfoVip, mModel map[uint64]followmod.FollowModel, mFollow map[uint64]bool) []*followmod.BaseInfo { + var infos []*followmod.BaseInfo + mInfo := make(map[uint64]usermod.BaseInfoVip) + for _, u := range uInfo { + if u == nil { + continue + } + mInfo[u.UID] = *u + } + for _, u := range uids { + bi := followmod.BaseInfo{ + UID: u, + Name: mInfo[u].Name, + Gender: mInfo[u].Gender, + Portrait: mInfo[u].Portrait, + HasLocked: mInfo[u].HasLocked, + HasBanned: mInfo[u].HasBanned, + VipLevel: mInfo[u].VipLevel, + IsVip: mInfo[u].IsVip, + HasFollow: true, + Via: mModel[u].Via, + Fans: mInfo[u].Fans, + Summary: mInfo[u].Summary, + CreatedAt: mModel[u].CreatedAt, + SuperUser: mInfo[u].SuperUser, + Awards: mInfo[u].Awards, + TotalWorks: mInfo[u].TotalWorks, + } + infos = append(infos, &bi) + } + return infos +} + +// GetHisFollowList 获取他关注列表 +func GetHisFollowList(uid, oid uint64, page int, size int) (code stderr.Code, data interface{}) { + uids, mModel, hasNext, err := followmod.GetFollowList(oid, page, size, false) + if err != nil { + return stderr.ErrDbQueryError, nil + } + uInfos, err := usermod.GetUsersBaseInfoWithVip(uids) + if err != nil { + return stderr.ErrDbQueryError, nil + } + mFollow, err := followmod.IsFollowedByUsers(uid, uids) + if err != nil { + return stderr.ErrDbQueryError, nil + } + datas := encodeUserBase(uids, uInfos, mModel, mFollow) + data = followmod.ListResp{List: datas, HasNext: hasNext} + return stderr.Success, data +} + +// GetFollowList 获取关注列表 +func GetFollowList(uid uint64, page int, size int, isShort bool) (code stderr.Code, data interface{}) { + uids, mModel, hasNext, err := followmod.GetFollowList(uid, page, size, isShort) + if err != nil { + return stderr.ErrDbQueryError, nil + } + uInfos, err := usermod.GetUsersBaseInfoWithVip(uids) + if err != nil { + return stderr.ErrDbQueryError, nil + } + mFollow, err := followmod.IsFollowedByUsers(uid, uids) + if err != nil { + return stderr.ErrDbQueryError, nil + } + datas := encodeUserBase(uids, uInfos, mModel, mFollow) + data = followmod.ListResp{List: datas, HasNext: hasNext} + return stderr.Success, data +} + +// GetHisFansList 获取他的粉丝列表 +func GetHisFansList(uid, oid uint64, page int, size int) (code stderr.Code, data interface{}) { + uids, mModel, hasNext, err := followmod.GetFansList(oid, page, size) + if err != nil { + return stderr.ErrDbQueryError, nil + } + uInfos, err := usermod.GetUsersBaseInfoWithVip(uids) + if err != nil { + return stderr.ErrDbQueryError, nil + } + mFollow, err := followmod.IsFollowUsers(uid, uids) + if err != nil { + return stderr.ErrDbQueryError, nil + } + datas := encodeUserBase(uids, uInfos, mModel, mFollow) + data = followmod.ListResp{List: datas, HasNext: hasNext} + return stderr.Success, data +} + +// GetFansList 获取粉丝列表 +func GetFansList(uid uint64, page int, size int) (code stderr.Code, data interface{}) { + uids, mModel, hasNext, err := followmod.GetFansList(uid, page, size) + if err != nil { + return stderr.ErrDbQueryError, nil + } + uInfos, err := usermod.GetUsersBaseInfoWithVip(uids) + if err != nil { + return stderr.ErrDbQueryError, nil + } + mFollow, err := followmod.IsFollowUsers(uid, uids) + if err != nil { + return stderr.ErrDbQueryError, nil + } + datas := encodeUserBase(uids, uInfos, mModel, mFollow) + data = followmod.ListResp{List: datas, HasNext: hasNext} + return stderr.Success, data +} + +// GetDynamicsList 获取关注用户动态 +func GetDynamicsList(uid uint64, req followmod.AppDynamicsListReq) (data followmod.DynamicsResp, code stderr.Code) { + // 处理用户关注消息状态 + _ = follow.CancelTip(uid) + var infos []*vidmod.VideoModel + + // 获取官方帖子 + //vid, err := vidmod.FindOneByUid(bson.M{"publisherID": 100000, "newsType": "COVER", "status": 1}) + //if err != nil { + // return stderr.ErrDbQueryError, nil + //} + // + //if vid != nil && !vid.ID.IsZero() { + // infos = append(infos, vid) + //} + + uids, err := followmod.GetTotalFollowList(uid) + if err != nil { + return data, stderr.ErrDbQueryError + } + if len(uids) == 0 { + //if len(infos) > 0 { + // list := vidhelpser.EncodeVideoInfo(uid, infos) + // data = followmod.DynamicsResp{VInfos: list, List: list, TotalPages: 1, HasNext: false} + //} else { + //data = followmod.DynamicsResp{VInfos: nil, List: nil, HasNext: false, TotalPages: 0} + //} + return data, stderr.Success + } + filter := bson.M{ + "publisherID": bson.M{"$in": uids}, + "newsType": vidmod.SHORT, // 默认短视频 + "status": vidmod.CheckPass, + } + if req.NewsType != "" { + filter["newsType"] = req.NewsType + } + sort := bson.D{{Key: "reviewAt", Value: -1}} + opt := options.Find().SetSkip(req.Skip64()).SetLimit(req.Limit64()).SetSort(sort) + videos, hasNext, err := vidmod.FindList(filter, opt) + if err != nil { + return data, stderr.ErrDbQueryError + } + if len(videos) == 0 { + //if len(infos) > 0 { + // list := vidhelpser.EncodeVideoInfo(uid, infos) + // data = followmod.DynamicsResp{VInfos: list, List: list, TotalPages: 1, HasNext: false} + //} else { + //data = followmod.DynamicsResp{VInfos: nil, List: nil, HasNext: false, TotalPages: 0} + //} + return data, stderr.Success + } + infos = append(infos, videos...) + list := vidhelpser.EncodeVideoInfo(uid, infos) + data = followmod.DynamicsResp{VInfos: list, List: list, TotalPages: 0, HasNext: hasNext} + return data, stderr.Success +} + +// 短视频-获取关注用户和作品,若无关注,则获取推荐用户及作品 +type ( + GetFollowUpUsersWithShortReq struct { + commod.Page + ShowType int `json:"showType" form:"showType"` //0:优先获取已关注没有则获取推荐 1:已关注 2:推荐 + } + GetFollowUpUsersWithShortRep struct { + RecoUsers []*RecoUser `json:"recoUsers"` // 官方推荐(没有关注用户则使用此数据) + //FollowUsers []*UserInfo `json:"followUsers"` // 已关注的博主列表 + List []*vidmod.VideoInfo `json:"list"` // 已关注的博主发布的视频列表 + ShowType int `json:"showType"` // 1:已关注 2:推荐 + HasNext bool `json:"hasNext"` // 是否有下一页 + } + UserInfo struct { + Uid uint64 `json:"uid" bson:"uid"` //UID + Gender string `json:"gender" bson:"gender"` //性别 + Name string `json:"name" bson:"name"` //姓名 + Portrait string `json:"portrait" bson:"portrait"` //头像 + Fans int64 `json:"fans" bson:"fans"` //粉丝数 + Summary string `json:"summary" bson:"summary"` //简介 + OfficialCert bool `json:"officialCert" bson:"officialCert"` //是否官方认证 + + IsFollow bool `json:"isFollow"` //是否关注 + TotalWorks int64 `json:"totalWorks" bson:"totalWorks"` // 总作品数 + } + RecoUser struct { + UserInfo UserInfo `json:"userInfo"` // 用户信息 + List []*vidmod.VideoBase `json:"list"` // 用户发布的视频信息 + } +) + +// GetFollowUpUserListWithShort 获取用户关注的UP主短视频 或 短视频推荐UP主以及UP主发布的短视频 +func (req *GetFollowUpUsersWithShortReq) GetFollowUpUserListWithShort(uid uint64) (data GetFollowUpUsersWithShortRep, code stderr.Code) { + if req.ShowType <= 1 { + data, code = req.GetFollowUpUserListAndShortVideo(uid) + if code != stderr.Success || data.ShowType == 1 { + return data, code + } + } + // 短视频推荐 + data, code = req.getRecoUserByShortVideo() + return data, stderr.Success +} + +// GetFollowUpUserListAndShortVideo 获取关注用户和用户下的短视频 +func (req *GetFollowUpUsersWithShortReq) GetFollowUpUserListAndShortVideo(uid uint64) (data GetFollowUpUsersWithShortRep, code stderr.Code) { + // 已关注的用户列表不需要展示 + var uids []uint64 + uids, err := followmod.GetTotalFollowListLimit(uid, 30) + if err != nil { + return data, stderr.ErrDbQueryError + } + if len(uids) == 0 { + return data, stderr.Success + } + + //users, err := usermod.FindUsersMapByUID(uids) + //if err != nil { + // return data, stderr.Failure + //} + // + //if len(users) == 0 { + // return data, stderr.Success + //} + // + //for _, user := range users { + // data.FollowUsers = append(data.FollowUsers, &UserInfo{ + // Uid: user.UID, + // Gender: user.Gender, + // Name: user.Name, + // Portrait: user.Portrait, + // Fans: user.Fans, + // OfficialCert: user.OfficialCert, + // IsFollow: true, + // Summary: user.Summary, + // TotalWorks: user.TotalWorks, + // }) + //} + cond := bson.M{"newsType": vidmod.SHORT, "publisherID": bson.M{"$in": uids}, "status": vidmod.CheckPass} // 下层函数已经设置过滤status + sort := bson.D{{Key: "reviewAt", Value: -1}} + videos, hasNext, err := vidmod.FindList(cond, options.Find().SetSort(sort).SetLimit(req.Limit64()).SetSkip(req.Skip64())) + if err != nil { + return data, stderr.Failure + } + + list := vidhelpser.EncodeVideoInfo(uid, videos) + data.List = list + data.HasNext = hasNext + data.ShowType = 1 // 当前是关注用户 + + return data, stderr.Success +} + +// getRecoUserByShortVideo 获取推荐用户 +func (req *GetFollowUpUsersWithShortReq) getRecoUserByShortVideo() (data GetFollowUpUsersWithShortRep, code stderr.Code) { + + req.PageSize = 6 // 短视频推荐每页固定条数 + key := fmt.Sprintf("recoUserAndShortVideo:%d_%d", req.PageSize, req.PageNumber) + err := appg.Redis.GetWithJson2Any(key, &data) + if err == nil { + return data, stderr.Success + } + if !errors.Is(err, goredis.Nil) { + log.Warn("GetFollowUpUserPublishVideosReq.getRecoVideoList error", log.E(err)) + } + + defer func() { + _ = appg.Redis.SetWithAny2Json(key, data, time.Minute*5) + }() + + recoUsers, _, hasNext := weektop.GetTopByPage(topser.TypeRecommendUser, req.Limit64(), req.Limit64()+1) + if len(recoUsers) > int(req.Limit64()) { + recoUsers = recoUsers[:req.Limit64()] + hasNext = true + } + if len(recoUsers) == 0 { + return data, stderr.Success + } + var ( + dataMap = map[uint64]*RecoUser{} + uids []uint64 + ) + + for _, v := range recoUsers { + uid, e := strconv.ParseUint(v, 10, 64) + if e != nil { + continue + } + + uids = append(uids, uid) + } + users, err := usermod.FindUsersByUID(uids) + if err != nil { + return data, stderr.Failure + } + + uids = uids[:0] + for _, user := range users { + recoUser := &RecoUser{ + UserInfo: UserInfo{ + Uid: user.UID, + Gender: user.Gender, + Name: user.Name, + Portrait: user.Portrait, + Fans: user.Fans, + Summary: user.Summary, + OfficialCert: user.OfficialCert, + TotalWorks: user.TotalWorks, + }, + } + dataMap[user.UID] = recoUser + // 获取用户的视频 + filterTmp := bson.M{"newsType": vidmod.SP, "publisherID": user.UID, "status": vidmod.CheckPass} + optTmp := options.Find().SetSort(bson.D{{"reviewAt", -1}}).SetLimit(6) + list, _, err := vidmod.FindList(filterTmp, optTmp) + if err != nil { + return data, stderr.Failure + } + for _, model := range list { + baseVid := vidhelpser.ToVideoBaseInfo(model, nil, 0) + recoUser.List = append(recoUser.List, baseVid) + } + data.RecoUsers = append(data.RecoUsers, recoUser) + } + + data.HasNext = hasNext + data.ShowType = 2 // 当前是推荐用户 + + return data, stderr.Success +} diff --git a/app/service/health_check_ser/health.go b/app/service/health_check_ser/health.go new file mode 100644 index 0000000..d9d1b2f --- /dev/null +++ b/app/service/health_check_ser/health.go @@ -0,0 +1,546 @@ +package health_check_ser + +import ( + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/models/commod" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "go.mongodb.org/mongo-driver/x/mongo/driver/connstring" + "io/ioutil" + "math/rand" + "net" + "net/url" + "os/exec" + "regexp" + "runtime" + "strconv" + "strings" + "syscall" + "time" +) + +type PingReq struct { + GroupId string `json:"groupId"` // 平台分组ID + Timestamp int64 `json:"timestamp"` // Unix时间戳(秒) + Sign string `json:"sign"` // HMAC-SHA256签名 +} + +// Ping 服务健康检测 +func (p *PingReq) Ping() (ret SystemStatus, err error) { + str, err := appg.Redis.Get(redisconst.MonitorCacheKey) + if err != nil { + log.Warn(fmt.Sprintf("缓存获取服务健康检测信息异常:%v", err)) + } + if str != nil { + var resp SystemStatus + if err = json.Unmarshal([]byte(*str), &resp); err == nil { + resp.Timestamp = time.Now().Unix() + return resp, nil + } + log.Warn(fmt.Sprintf("解析缓存服务健康检测信息数据异常:%v", err)) + } + + // 校验请求信息 + if err = p.checkInfo(&ret); err != nil { + return ret, fmt.Errorf("校验请求信息失败: %v", err) + } + + // 获取服务器基本信息 + if err = p.getServerInfo(&ret); err != nil { + return ret, fmt.Errorf("获取服务器信息失败: %v", err) + } + + // 获取硬件资源信息 + if err = p.getHardwareInfo(&ret); err != nil { + return ret, fmt.Errorf("获取硬件信息失败: %v", err) + } + + // 检查各项服务状态 + p.getServiceStatus(&ret) + + common.Go(func() { + bytes, _ := json.Marshal(ret) + if err = appg.Redis.Set(redisconst.MonitorCacheKey, bytes, redisconst.GetMonitorCacheExpired()); err != nil { + log.Warn(fmt.Sprintf("保存缓存数据异常:%v", err)) + } + }) + return ret, nil +} + +// checkInfo 获取服务器请求信息 +func (p *PingReq) checkInfo(status *SystemStatus) error { + // 1、验证时间戳有效性(允许±5分钟误差) + currentTime := time.Now().Unix() + timeDiff := absInt64(currentTime - p.Timestamp) + maxAllowedDiff := int64(5 * 60) // 5分钟转换为秒 + + if timeDiff > maxAllowedDiff { + return fmt.Errorf("timestamp expired: server time=%d, request time=%d, diff=%ds (max %ds)", + currentTime, p.Timestamp, timeDiff, maxAllowedDiff) + } + + // 2、验证签名 + if err := p.verifySignature(); err != nil { + return fmt.Errorf("signature verification failed: %v", err) + } + + // 返回赋值 + status.Timestamp = time.Now().Unix() + return nil +} + +// absInt64 计算int64绝对值 +func absInt64(n int64) int64 { + if n < 0 { + return -n + } + return n +} + +// verifySignature 验证HMAC-SHA256签名 +func (p *PingReq) verifySignature() error { + // 构建签名:groupId + ":" + timestamp + signatureBase := fmt.Sprintf("%s:%d", p.GroupId, p.Timestamp) + + // 计算HMAC-SHA256 + mac := hmac.New(sha256.New, []byte(appg.Conf.MonitorReport.SecretKey)) + mac.Write([]byte(signatureBase)) + expectedSignature := hex.EncodeToString(mac.Sum(nil)) + + // 比较签名(使用常量时间比较防止时序攻击) + if !hmac.Equal([]byte(p.Sign), []byte(expectedSignature)) { + log.Error(fmt.Sprintf("verifySignature err,sign:%v, chenckSign:%v", p.Sign, expectedSignature)) + return fmt.Errorf("invalid signature") + } + + return nil +} + +// getServerInfo 获取服务器基本信息 +func (p *PingReq) getServerInfo(status *SystemStatus) error { + // 获取项目名称 + status.Name = commod.KFK_APP_NAME + + // 获取本机IP地址 + ip, err := getLocalIP() + if err != nil { + status.Ip = "127.0.0.1" + } else { + status.Ip = ip + } + + return nil +} + +// getLocalIP 获取本机非回环IP地址 +func getLocalIP() (string, error) { + addrs, err := net.InterfaceAddrs() + if err != nil { + return "", err + } + + for _, addr := range addrs { + if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() { + if ipNet.IP.To4() != nil { + return ipNet.IP.String(), nil + } + } + } + + return "127.0.0.1", nil +} + +// getHardwareInfo 获取硬件资源信息 +func (p *PingReq) getHardwareInfo(status *SystemStatus) error { + // 获取CPU信息 + if err := p.getCPUInfo(&status.Cpu); err != nil { + status.Cpu.Cores = runtime.NumCPU() + status.Cpu.Used = 0.0 + } + + // 获取内存信息 + if err := p.getMemoryInfo(&status.Memory); err != nil { + // 设置默认值或使用runtime信息 + status.Memory.Total = 0 + status.Memory.Used = 0 + } + + // 获取磁盘信息 + if err := p.getDiskInfo(&status.Disk); err != nil { + status.Disk.Total = 0 + status.Disk.Used = 0 + } + + return nil +} + +// getCPUInfo 获取CPU信息(跨平台实现) +func (p *PingReq) getCPUInfo(cpu *CpuStatus) error { + cpu.Cores = runtime.NumCPU() + + // 根据操作系统获取CPU使用率 + var err error + switch runtime.GOOS { + case "linux": + cpu.Used, err = getLinuxCPUUsage() + case "darwin": // macOS + cpu.Used, err = getDarwinCPUUsage() + case "windows": + cpu.Used, err = getWindowsCPUUsage() + default: + cpu.Used = 0.0 + return fmt.Errorf("unsupported OS: %s", runtime.GOOS) + } + + if err != nil { + cpu.Used = 0.0 + return err + } + + return nil +} + +// getLinuxCPUUsage 获取Linux系统CPU使用率 +func getLinuxCPUUsage() (float64, error) { + // 读取/proc/stat文件获取CPU信息 + data, err := ioutil.ReadFile("/proc/stat") + if err != nil { + return 0.0, err + } + + lines := strings.Split(string(data), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "cpu ") { + fields := strings.Fields(line) + if len(fields) >= 8 { + var total, idle uint64 + for i := 1; i < len(fields); i++ { + val, _ := strconv.ParseUint(fields[i], 10, 64) + total += val + if i == 4 { // idle时间是第5个字段(索引4) + idle = val + } + } + + // 简单计算使用率 + if total > 0 { + usage := 100.0 * (float64(total-idle) / float64(total)) + return usage, nil + } + } + } + } + + return 0.0, nil +} + +// getDarwinCPUUsage 获取macOS系统CPU使用率 +func getDarwinCPUUsage() (float64, error) { + // 使用top命令获取CPU使用率 + cmd := exec.Command("top", "-l", "1", "-n", "0") + output, err := cmd.Output() + if err != nil { + return 0.0, err + } + + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.Contains(line, "CPU usage") { + // 解析CPU使用率,例如:CPU usage: 16.34% user, 16.98% sys, 66.66% idle + usage, err := parseTopCPUUsageEnhanced(line) + if err != nil { + return 30, nil + } + return usage, nil + } + } + + return 0.0, nil +} + +func parseTopCPUUsageEnhanced(line string) (float64, error) { + // 移除多余的空格 + line = strings.TrimSpace(line) + + // 处理不同格式: + // 1. "CPU usage: 16.34% user, 16.98% sys, 66.66% idle" + // 2. "CPU Usage: 25% user, 10% sys, 65% idle" + // 3. "CPU: 10.5% user, 5.2% sys, 84.3% idle" + + // 查找user和sys的百分比 + userRe := regexp.MustCompile(`(\d+\.?\d*)%\s*user`) + sysRe := regexp.MustCompile(`(\d+\.?\d*)%\s*sys`) + + var user, sys float64 + var err error + + if userMatch := userRe.FindStringSubmatch(line); userMatch != nil { + user, err = strconv.ParseFloat(userMatch[1], 64) + if err != nil { + return 0.0, err + } + } + + if sysMatch := sysRe.FindStringSubmatch(line); sysMatch != nil { + sys, err = strconv.ParseFloat(sysMatch[1], 64) + if err != nil { + return 0.0, err + } + } + + // 如果找到了user和sys,计算总和 + if user > 0 || sys > 0 { + cpuUsage := user + sys + + // 验证合理性 + if cpuUsage >= 0 && cpuUsage <= 100 { + return cpuUsage, nil + } + } + + // 备选:尝试匹配idle,然后计算100-idle + idleRe := regexp.MustCompile(`(\d+\.?\d*)%\s*idle`) + if idleMatch := idleRe.FindStringSubmatch(line); idleMatch != nil { + idle, err := strconv.ParseFloat(idleMatch[1], 64) + if err == nil && idle >= 0 && idle <= 100 { + return 100 - idle, nil + } + } + + return 0.0, fmt.Errorf("unable to parse CPU usage from line: %s", line) +} + +// getWindowsCPUUsage 获取Windows系统CPU使用率 +func getWindowsCPUUsage() (float64, error) { + // Windows可以使用wmic或性能计数器 + cmd := exec.Command("wmic", "cpu", "get", "loadpercentage") + output, err := cmd.Output() + if err != nil { + return 0.0, err + } + + lines := strings.Split(string(output), "\n") + if len(lines) >= 2 { + usageStr := strings.TrimSpace(lines[1]) + if usage, err := strconv.ParseFloat(usageStr, 64); err == nil { + return usage, nil + } + } + + return 0.0, nil +} + +// getMemoryInfo 获取内存信息 +func (p *PingReq) getMemoryInfo(memory *MemoryStatus) error { + switch runtime.GOOS { + case "linux": + return getLinuxMemoryInfo(memory) + case "darwin": + return getDarwinMemoryInfo(memory) + case "windows": + return getWindowsMemoryInfo(memory) + default: + return fmt.Errorf("unsupported OS: %s", runtime.GOOS) + } +} + +// getLinuxMemoryInfo 获取Linux内存信息 +func getLinuxMemoryInfo(memory *MemoryStatus) error { + data, err := ioutil.ReadFile("/proc/meminfo") + if err != nil { + return err + } + + var memTotal, memAvailable int + + lines := strings.Split(string(data), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "MemTotal:") { + fields := strings.Fields(line) + if len(fields) >= 2 { + val, _ := strconv.Atoi(fields[1]) + memTotal = val / 1024 / 1024 // 转换为GB + } + } else if strings.HasPrefix(line, "MemAvailable:") { + fields := strings.Fields(line) + if len(fields) >= 2 { + val, _ := strconv.Atoi(fields[1]) + memAvailable = val / 1024 / 1024 // 转换为GB + } + } + } + + if memTotal > 0 { + memory.Total = memTotal + memory.Used = memTotal - memAvailable + } + + return nil +} + +// getDarwinMemoryInfo 获取macOS内存信息 +func getDarwinMemoryInfo(memory *MemoryStatus) error { + cmd := exec.Command("sysctl", "-n", "hw.memsize") + output, err := cmd.Output() + if err != nil { + return err + } + + totalStr := strings.TrimSpace(string(output)) + if total, err := strconv.ParseInt(totalStr, 10, 64); err == nil { + memory.Total = int(total / 1024 / 1024 / 1024) // 转换为GB + memory.Used = memory.Total / 3 // 示例值,实际需要更准确的获取方式 + } + + return nil +} + +// getWindowsMemoryInfo 获取Windows内存信息 +func getWindowsMemoryInfo(memory *MemoryStatus) error { + cmd := exec.Command("wmic", "ComputerSystem", "get", "TotalPhysicalMemory") + output, err := cmd.Output() + if err != nil { + return err + } + + lines := strings.Split(string(output), "\n") + if len(lines) >= 2 { + totalStr := strings.TrimSpace(lines[1]) + if total, err := strconv.ParseInt(totalStr, 10, 64); err == nil { + memory.Total = int(total / 1024 / 1024 / 1024) // 转换为GB + memory.Used = memory.Total / 4 // 示例值 + } + } + + return nil +} + +// getDiskInfo 获取磁盘信息 +func (p *PingReq) getDiskInfo(disk *DiskStatus) error { + // 这里简化处理,获取根目录磁盘使用情况 + var stat syscall.Statfs_t + err := syscall.Statfs("/", &stat) + if err != nil { + return err + } + + // 计算总空间和已用空间 + total := stat.Blocks * uint64(stat.Bsize) + free := stat.Bfree * uint64(stat.Bsize) + + disk.Total = int(total / 1024 / 1024 / 1024) // 转换为GB + disk.Used = int((total - free) / 1024 / 1024 / 1024) // 转换为GB + + return nil +} + +// getServiceStatus 检查各项服务状态 +func (p *PingReq) getServiceStatus(status *SystemStatus) { + // 检查MySQL + status.Mysql = p.checkService(ServiceMySQL) + + // 检查MongoDB + status.Mongodb = p.checkService(ServiceMongoDB) + + // 检查Redis + status.Redis = p.checkService(ServiceRedis) + + // 检查Elasticsearch + status.Elasticsearch = p.checkService(ServiceElasticsearch) + + // 检查Kafka + status.Kafka = p.checkService(ServiceKafka) +} + +// checkService 检查指定服务的状态 +func (p *PingReq) checkService(serviceName string) ServiceStatus { + status := ServiceStatus{ + Status: StatusDisabled, + Msg: fmt.Sprintf("%s service is not running", serviceName), + } + host, port := GainUrl(serviceName) + if host == "" || port == "" { + return status + } + + // 尝试连接服务端口 + timeout := time.Second + conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), timeout) + if err != nil { + // 尝试通过进程检查 + if p.isProcessRunning(serviceName) { + status.Status = StatusOK + status.Msg = fmt.Sprintf("%s process is running", serviceName) + } + return status + } + + defer conn.Close() + status.Status = StatusOK + status.Msg = fmt.Sprintf("%s service is running on port %s", serviceName, port) + + return status +} + +func GainUrl(serviceName string) (host, port string) { + switch serviceName { + case ServiceMySQL: + // todo + case ServiceMongoDB: + cs, err := connstring.Parse(appg.Conf.Mongo.VideoDbUrl) + if err != nil { + return host, port + } + randomIndex := rand.Intn(len(cs.Hosts)) + mongoUrl := cs.Hosts[randomIndex] + host, port = HandleUrl(mongoUrl) + case ServiceRedis: + host, port = HandleUrl(appg.Conf.Redis.URL) + case ServiceElasticsearch: + host, port = HandleUrl(appg.Conf.Elastic.VideoUrl) + case ServiceKafka: + host, port = HandleUrl(appg.Conf.Kafka.Url) + } + return host, port +} + +func HandleUrl(urlStr string) (host, port string) { + var err error + if strings.Contains(urlStr, "://") { + // 有协议头,需要解析 + if u, parseErr := url.Parse(urlStr); parseErr == nil { + if host, port, err = net.SplitHostPort(u.Host); err != nil { + return host, port + } + } + } else { + // 没有协议头,直接拆分 + if host, port, err = net.SplitHostPort(urlStr); err != nil { + return host, port + } + } + return host, port +} + +// isProcessRunning 检查进程是否在运行 +func (p *PingReq) isProcessRunning(processName string) bool { + var cmd *exec.Cmd + + switch runtime.GOOS { + case "linux", "darwin": + cmd = exec.Command("pgrep", "-f", processName) + case "windows": + cmd = exec.Command("tasklist", "/FI", fmt.Sprintf("IMAGENAME eq %s.exe", processName)) + default: + return false + } + + err := cmd.Run() + return err == nil +} diff --git a/app/service/health_check_ser/type.go b/app/service/health_check_ser/type.go new file mode 100644 index 0000000..c9f65f0 --- /dev/null +++ b/app/service/health_check_ser/type.go @@ -0,0 +1,54 @@ +package health_check_ser + +// 校验服务类型常量定义 +const ( + ServiceMySQL = "mysql" // MySQL数据库服务 + ServiceMongoDB = "mongodb" // MongoDB数据库服务 + ServiceRedis = "redis" // Redis缓存服务 + ServiceElasticsearch = "elasticsearch" // Elasticsearch搜索服务 + ServiceKafka = "kafka" // Kafka消息队列服务 +) + +// SystemStatus 表示服务器的完整状态监控信息 +type SystemStatus struct { + Name string `json:"serverName"` // 服务器名称 + Ip string `json:"serverIp"` // 服务器IP地址 + Cpu CpuStatus `json:"cpu"` // CPU状态 + Memory MemoryStatus `json:"memory"` // 内存状态 + Disk DiskStatus `json:"disk"` // 磁盘状态 + Mysql ServiceStatus `json:"mysql"` // MySQL数据库 + Mongodb ServiceStatus `json:"mongodb"` // MongoDB数据库 + Redis ServiceStatus `json:"redis"` // Redis缓存 + Elasticsearch ServiceStatus `json:"elasticsearch"` // Elasticsearch服务 + Kafka ServiceStatus `json:"kafka"` // Kafka消息队列 + Timestamp int64 `json:"timestamp"` // 时间戳(Unix时间戳) +} + +// CpuStatus CPU状态 +type CpuStatus struct { + Cores int `json:"cores"` // CPU核心数 + Used float64 `json:"used"` // 当前负载(使用中的核心数) +} + +// MemoryStatus 内存状态 +type MemoryStatus struct { + Total int `json:"total"` // 内存总量(单位建议统一,如GB) + Used int `json:"used"` // 已使用内存(单位建议统一,如GB) +} + +// DiskStatus 磁盘状态 +type DiskStatus struct { + Total int `json:"total"` // 磁盘总量(单位建议统一,如GB) + Used int `json:"used"` // 已使用磁盘空间(单位建议统一,如GB) +} + +// ServiceStatus 通用服务状态 +type ServiceStatus struct { + Status string `json:"status"` // 状态:ok-正常,err-异常,disabled-未启用 + Msg string `json:"msg"` // 异常时的错误信息 +} + +const ( + StatusOK = "ok" // 服务状态开启 + StatusDisabled = "disabled" // 服务状态关闭 +) diff --git a/app/service/imadser/advertise.go b/app/service/imadser/advertise.go new file mode 100644 index 0000000..fa1282d --- /dev/null +++ b/app/service/imadser/advertise.go @@ -0,0 +1,133 @@ +package imadser + +import ( + "sort" + "strings" + + "91porn-server/app/service/adser" + "91porn-server/common/enum/imad" +) + +const maxAdsPerPosition = 10 + +type IMAdItem struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Cover string `json:"cover"` + Href string `json:"href"` + Position string `json:"position"` + PositionName string `json:"positionName"` + SortCode int `json:"sortCode"` + CoverImgSize string `json:"coverImgSize"` + WatchTime int `json:"watchTime"` +} + +type AdPositionGroup struct { + Position string `json:"position"` + PositionName string `json:"positionName"` + List []IMAdItem `json:"list"` +} + +func GetAdsByPositionCode(positionCode string) (positionName string, list []IMAdItem) { + slots, err := adser.JtAdvertiseThreeServer() + if err != nil { + return "", []IMAdItem{} + } + return SelectAdsByPositionCode(slots, positionCode) +} + +func GetAdsByPositionCodes(positionCodes []string) []AdPositionGroup { + slots, err := adser.JtAdvertiseThreeServer() + if err != nil { + return []AdPositionGroup{} + } + return SelectAdsByPositionCodes(slots, positionCodes) +} + +func SelectAdsByPositionCode(slots []adser.AdSlot, positionCode string) (string, []IMAdItem) { + positionCode = strings.TrimSpace(positionCode) + matches := make([]IMAdItem, 0) + positionName := "" + for _, slot := range slots { + if strings.TrimSpace(slot.AdvertiseLocationCode) != positionCode { + continue + } + positionName = slot.AdvertiseLocationName + for _, ad := range slot.AdDetailInfoList { + extra := ad.GetExtraData() + matches = append(matches, IMAdItem{ + ID: ad.AdvertiseCode, + Title: ad.AdvertiseName, + Description: ad.AdvertiseDesc, + Cover: ad.GetCoverLsj(), + Href: ad.GetRealLink(nil, nil), + Position: positionCode, + PositionName: slot.AdvertiseLocationName, + SortCode: ad.Sort, + CoverImgSize: extra.CoverImgSize, + WatchTime: extra.WatchTime, + }) + } + } + sort.SliceStable(matches, func(i, j int) bool { + return matches[i].SortCode < matches[j].SortCode + }) + if len(matches) > maxAdsPerPosition { + matches = matches[:maxAdsPerPosition] + } + return positionName, matches +} + +func SelectAdsByPositionCodes(slots []adser.AdSlot, positionCodes []string) []AdPositionGroup { + normalized := normalizePositionCodes(positionCodes) + groups := make([]AdPositionGroup, 0, len(normalized)) + for _, positionCode := range normalized { + positionName, list := SelectAdsByPositionCode(slots, positionCode) + groups = append(groups, AdPositionGroup{ + Position: positionCode, + PositionName: positionName, + List: list, + }) + } + return groups +} + +func ListIMAdPositionCodesWithAds() []string { + slots, err := adser.JtAdvertiseThreeServer() + if err != nil { + return nil + } + positions := make([]string, 0) + seen := make(map[string]struct{}) + for _, slot := range slots { + code := strings.TrimSpace(slot.AdvertiseLocationCode) + if !imad.IsPositionCode(code) || len(slot.AdDetailInfoList) == 0 { + continue + } + if _, ok := seen[code]; ok { + continue + } + seen[code] = struct{}{} + positions = append(positions, code) + } + sort.Strings(positions) + return positions +} + +func normalizePositionCodes(positionCodes []string) []string { + seen := make(map[string]struct{}, len(positionCodes)) + normalized := make([]string, 0, len(positionCodes)) + for _, positionCode := range positionCodes { + positionCode = strings.TrimSpace(positionCode) + if positionCode == "" { + continue + } + if _, ok := seen[positionCode]; ok { + continue + } + seen[positionCode] = struct{}{} + normalized = append(normalized, positionCode) + } + return normalized +} diff --git a/app/service/imgroupmemberser/imgroupmember.go b/app/service/imgroupmemberser/imgroupmember.go new file mode 100755 index 0000000..1f80175 --- /dev/null +++ b/app/service/imgroupmemberser/imgroupmember.go @@ -0,0 +1,61 @@ +package imgroupmemberser + +import ( + "91porn-server/common/log" + "91porn-server/models/cache/imgroupmemberdata" + "91porn-server/models/commod" + "91porn-server/models/v/imgroupmembermod" + + "go.mongodb.org/mongo-driver/bson" +) + +type AppQueryListReq struct { + commod.Page +} + +type AppListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []*imgroupmembermod.ImGroupMemberInfo `json:"list"` +} + +// GetList 获取列表 +func (p *AppQueryListReq) GetList() AppListRes { + var res AppListRes + var err error + filter := bson.M{} + + sort := bson.D{{"_id", -1}} + // 获取列表 + var data []imgroupmembermod.ImGroupMember + data, res.Total, res.HasNext, err = imgroupmemberdata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + log.Error("获取im群组成员列表数据错误", log.Any("Params", *p), log.E(err)) + return res + } + res.List = imgroupmemberdata.FormatAppDataList(data) + + return res +} + +type HasJoinReq struct { + GroupId int64 `json:"groupId" form:"groupId" binding:"required"` +} +type HasJoinRes struct { + Status int `json:"status"` // 0-已被禁言 1-可发言 2-未加入 +} + +// GetInfo 获取详情 +func (p *HasJoinReq) GetInfo(uid uint64) (res HasJoinRes, err error) { + item, err := imgroupmembermod.GetInfoByCond(bson.M{"groupId": p.GroupId, "uid": uid}) + if err != nil { + log.Error("获取im群组成员详情数据错误", log.Any("groupId", p.GroupId), log.Any("uid", uid), log.E(err)) + return + } + if item.ID.IsZero() || item.Status == 2 { + res.Status = 2 //.未加入 + } else { + res.Status = item.Status + } + return +} diff --git a/app/service/imgroupser/imgroup.go b/app/service/imgroupser/imgroup.go new file mode 100755 index 0000000..0410c3c --- /dev/null +++ b/app/service/imgroupser/imgroup.go @@ -0,0 +1,63 @@ +package imgroupser + +import ( + "91porn-server/common/log" + "91porn-server/models/cache/imgroupdata" + "91porn-server/models/commod" + "91porn-server/models/v/imgroupmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AppQueryListReq struct { + commod.Page +} + +type AppListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []*imgroupmod.ImGroupInfo `json:"list"` +} + +// GetList 获取列表 +func (p *AppQueryListReq) GetList() AppListRes { + var res AppListRes + var err error + filter := bson.M{"status": 1} + + sort := bson.D{{"_id", -1}} + // 获取列表 + var data []imgroupmod.ImGroup + data, res.Total, res.HasNext, err = imgroupdata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + log.Error("获取im群组列表数据错误", log.Any("Params", *p), log.E(err)) + return res + } + res.List = imgroupdata.FormatAppDataList(data) + + return res +} + +type AppQueryInfoReq struct { + ID string `json:"id" form:"id"` // id +} +type AppQueryInfoRes = *imgroupmod.ImGroupInfo + +// GetInfo 获取详情 +func (p *AppQueryInfoReq) GetInfo() (res AppQueryInfoRes, err error) { + oid, err := primitive.ObjectIDFromHex(p.ID) + if err != nil { + return res, err + } + + var item imgroupmod.ImGroup + item, err = imgroupdata.GetInfoFromCache(oid) + if err != nil { + log.Error("获取im群组详情数据错误", log.Any("ID", p.ID), log.E(err)) + return + } + + res = imgroupdata.FormatAppData(item) + return +} diff --git a/app/service/immessageser/immessage.go b/app/service/immessageser/immessage.go new file mode 100755 index 0000000..520c308 --- /dev/null +++ b/app/service/immessageser/immessage.go @@ -0,0 +1,80 @@ +package immessageser + +import ( + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/cache/immessagedata" + "91porn-server/models/commod" + "91porn-server/models/v/imgroupmembermod" + "91porn-server/models/v/immessagemod" + "time" + + "go.mongodb.org/mongo-driver/bson" +) + +type AppQueryListReq struct { + GroupId int64 `json:"groupId" form:"groupId" binding:"required"` + commod.Page +} + +type AppListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []*immessagemod.ImMessageInfo `json:"list"` +} + +// GetList 获取列表 +func (p *AppQueryListReq) GetList() AppListRes { + var res AppListRes + var err error + filter := bson.M{ + "groupId": p.GroupId, + } + + sort := bson.D{{"_id", -1}} + // 获取列表 + var data []immessagemod.ImMessage + data, res.Total, res.HasNext, err = immessagemod.GetList(filter, int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + log.Error("获取im消息列表数据错误", log.Any("Params", *p), log.E(err)) + return res + } + res.List = immessagedata.FormatAppDataList(data) + + return res +} + +type SendReq struct { + GroupId int64 `json:"groupId" form:"groupId" binding:"required"` // 群组id + Content string `json:"content" form:"content"` // 消息内容 + Image string `json:"image" form:"image"` // 图片 +} + +// Create 发布数据 +func (p *SendReq) Create(uid uint64) error { + // 判断是否是群成员 + member, err := imgroupmembermod.GetInfoByCond(bson.M{"groupId": p.GroupId, "uid": uid}) + if err != nil { + log.Error("发送群组消息失败", log.Any("params", *p), log.E(err)) + return stderr.NewCustomErr(stderr.Failure, "发送失败") + } + if member.ID.IsZero() || member.Status == 2 { + return stderr.NewCustomErr(stderr.Failure, "发送失败,您还未加入群组") + } + if member.Status == 0 { + return stderr.NewCustomErr(stderr.Failure, "发送失败,您已经被该群禁言") + } + data := immessagemod.ImMessage{ + GroupId: p.GroupId, + Uid: uid, + Content: p.Content, + Image: p.Image, + CreatedAt: time.Now(), + } + + // 创建数据 + if _, err = immessagemod.Insert(nil, data); err != nil { + return err + } + return nil +} diff --git a/app/service/imser/bridge.go b/app/service/imser/bridge.go new file mode 100644 index 0000000..10a5010 --- /dev/null +++ b/app/service/imser/bridge.go @@ -0,0 +1,312 @@ +package imser + +import ( + "fmt" + "strings" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/imclient" + "91porn-server/models/v/imusermod" + "91porn-server/models/v/usermod" +) + +const friendListFirstPageNow int64 = 1 + +type IMUserMapping struct { + UID uint64 + IMUserID int64 +} + +type IMFriendItem struct { + FriendIMUserID int64 `json:"friendImUserId"` + UID uint64 `json:"uid,omitempty"` + NickName string `json:"nickName,omitempty"` + Avatar string `json:"avatar,omitempty"` + Remark string `json:"remark,omitempty"` + Status int `json:"status"` + CreatedAt int64 `json:"createdAt,omitempty"` + UpdatedAt int64 `json:"updatedAt,omitempty"` +} + +type IMHistoryMessage struct { + MessageID string `json:"messageId"` + SenderID int64 `json:"senderId"` + ReceiverID int64 `json:"receiverId"` + SenderUserID uint64 `json:"senderUserId,omitempty"` + SenderNickName string `json:"senderNickName,omitempty"` + SenderAvatar string `json:"senderAvatar,omitempty"` + ReceiverUserID uint64 `json:"receiverUserId,omitempty"` + ReceiverNickName string `json:"receiverNickName,omitempty"` + ReceiverAvatar string `json:"receiverAvatar,omitempty"` + Content string `json:"content"` + MessageType string `json:"messageType"` + Seq int64 `json:"seq"` + CreatedAt int64 `json:"createdAt"` + FromSelf bool `json:"fromSelf"` +} + +// ResolveStoredIMUserID 解析用户的 IM 平台 user_id: +// 优先 usermod.ImUserID → imusermod 映射表 → 都没有则调用 IM 注册接口创建。 +// 保证返回的 imUserId > 0,或返回错误,调用方无需再判 0。 +func ResolveStoredIMUserID(uid uint64) (int64, error) { + // 直接复用 EnsureSDKUser:它的"查→兜底注册"流程跟这里需要的语义一致。 + return EnsureSDKUser(uid) +} + +func ResolveUIDByIMUserID(imUserID int64) (uint64, error) { + if imUserID <= 0 { + return 0, fmt.Errorf("imUserId is empty") + } + // imUserId → uid 以 imusermod 为唯一真源 + mapping, err := imusermod.FindByIMUserIDQuiet(imUserID) + if err != nil { + return 0, err + } + if mapping.UID <= 0 { + return 0, fmt.Errorf("im user not found") + } + return mapping.UID, nil +} + +func BusinessUsersByIMUserIDs(imUserIDs []int64) (map[int64]*usermod.User, error) { + out := make(map[int64]*usermod.User) + ids := uniquePositiveInt64(imUserIDs) + if len(ids) == 0 { + return out, nil + } + // imUserId → uid 走 imusermod(唯一真源),再按 uid 批量取业务用户 + mappings, err := imusermod.FindByIMUserIDsQuiet(ids) + if err != nil { + return nil, err + } + uids := make([]uint64, 0, len(mappings)) + uidByIM := make(map[int64]uint64, len(mappings)) + for _, mapping := range mappings { + if mapping.UID <= 0 || mapping.IMUserID <= 0 { + continue + } + uids = append(uids, mapping.UID) + uidByIM[mapping.IMUserID] = mapping.UID + } + if len(uids) == 0 { + return out, nil + } + usersByUID, err := usermod.FindUsersMapByUID(uids) + if err != nil { + return nil, err + } + for imUserID, uid := range uidByIM { + if u := usersByUID[uid]; u != nil { + out[imUserID] = u + } + } + return out, nil +} + +func EnsureFriendsBidirectional(uid, peerUID uint64) (selfIMUserID, peerIMUserID int64, added bool, err error) { + if uid == 0 || peerUID == 0 { + return 0, 0, false, fmt.Errorf("uid is empty") + } + if uid == peerUID { + return 0, 0, false, fmt.Errorf("cannot add self") + } + c := newSDKClient() + if !c.Enabled() { + return 0, 0, false, nil + } + selfIMUserID, err = EnsureSDKUser(uid) + if err != nil { + return 0, 0, false, err + } + peerIMUserID, err = EnsureSDKUser(peerUID) + if err != nil { + return 0, 0, false, err + } + addedForward, err := ensureFriendDirect(c, selfIMUserID, peerIMUserID) + if err != nil { + return 0, 0, false, err + } + addedReverse, err := ensureFriendDirect(c, peerIMUserID, selfIMUserID) + if err != nil { + return 0, 0, false, err + } + return selfIMUserID, peerIMUserID, addedForward || addedReverse, nil +} + +func FriendList(uid uint64, now int64) (imUserID int64, usedNow int64, nextNow int64, friends []IMFriendItem, err error) { + c := newSDKClient() + if !c.Enabled() { + return 0, 0, 0, []IMFriendItem{}, nil + } + imUserID, err = EnsureSDKUser(uid) + if err != nil { + return 0, 0, 0, nil, err + } + if now <= 0 { + now = friendListFirstPageNow + } + var relations []imclient.Friend + err = withAppToken(c, func(token string) error { + var listErr error + relations, listErr = c.FriendList(imUserID, now, token) + return listErr + }) + if err != nil { + return 0, 0, 0, nil, err + } + friendIMIDs := make([]int64, 0, len(relations)) + for _, relation := range relations { + if relation.FriendID > 0 { + friendIMIDs = append(friendIMIDs, relation.FriendID) + } + } + userByIM, err := BusinessUsersByIMUserIDs(friendIMIDs) + if err != nil { + return 0, 0, 0, nil, err + } + nextNow = now + items := make([]IMFriendItem, 0, len(relations)) + for _, relation := range relations { + if relation.UpdatedAt > nextNow { + nextNow = relation.UpdatedAt + } + item := IMFriendItem{ + FriendIMUserID: relation.FriendID, + Remark: relation.Remark, + Status: relation.Status, + CreatedAt: relation.CreatedAt, + UpdatedAt: relation.UpdatedAt, + } + if u := userByIM[relation.FriendID]; u != nil { + item.UID = u.UID + item.NickName = u.Name + item.Avatar = common.BindUrl(appg.Conf.URL.OriginUrl, u.Portrait) + } + items = append(items, item) + } + return imUserID, now, nextNow, items, nil +} + +func HistoryMessages(uid, peerUID uint64, req imclient.HistoryMessageRequest) (selfIMUserID, peerIMUserID int64, messages []IMHistoryMessage, err error) { + c := newSDKClient() + if !c.Enabled() { + return 0, 0, []IMHistoryMessage{}, nil + } + selfIMUserID, err = EnsureSDKUser(uid) + if err != nil { + return 0, 0, nil, err + } + peerIMUserID, err = ResolveStoredIMUserID(peerUID) + if err != nil { + return 0, 0, nil, err + } + if peerIMUserID <= 0 { + return 0, 0, nil, fmt.Errorf("peer im user not found") + } + req.UserID1 = selfIMUserID + req.UserID2 = peerIMUserID + var history []imclient.MessageDetail + err = withAppToken(c, func(token string) error { + var historyErr error + history, historyErr = c.HistoryMessages(req, token) + return historyErr + }) + if err != nil { + return 0, 0, nil, err + } + messageIMIDs := make([]int64, 0, len(history)*2) + for _, msg := range history { + if msg.SenderID > 0 { + messageIMIDs = append(messageIMIDs, msg.SenderID) + } + if msg.ReceiverID > 0 { + messageIMIDs = append(messageIMIDs, msg.ReceiverID) + } + } + userByIM, err := BusinessUsersByIMUserIDs(messageIMIDs) + if err != nil { + return 0, 0, nil, err + } + out := make([]IMHistoryMessage, 0, len(history)) + for _, msg := range history { + item := IMHistoryMessage{ + MessageID: msg.MessageID, + SenderID: msg.SenderID, + ReceiverID: msg.ReceiverID, + Content: msg.Content, + MessageType: msg.MessageType, + Seq: msg.Seq, + CreatedAt: msg.CreatedAt, + FromSelf: msg.SenderID == selfIMUserID, + } + if u := userByIM[msg.SenderID]; u != nil { + item.SenderUserID = u.UID + item.SenderNickName = u.Name + item.SenderAvatar = common.BindUrl(appg.Conf.URL.OriginUrl, u.Portrait) + } + if u := userByIM[msg.ReceiverID]; u != nil { + item.ReceiverUserID = u.UID + item.ReceiverNickName = u.Name + item.ReceiverAvatar = common.BindUrl(appg.Conf.URL.OriginUrl, u.Portrait) + } + out = append(out, item) + } + return selfIMUserID, peerIMUserID, out, nil +} + +func ensureFriendDirect(c *imclient.Client, userIMID, friendIMID int64) (bool, error) { + var added bool + err := withAppToken(c, func(token string) error { + relations, err := c.FriendList(userIMID, friendListFirstPageNow, token) + if err != nil { + return err + } + for _, relation := range relations { + if relation.FriendID == friendIMID && relation.Status == 0 { + added = false + return nil + } + } + err = c.DirectAddFriend(imclient.DirectAddFriendRequest{ + UserID: userIMID, + FriendID: friendIMID, + }, token) + if err == nil { + added = true + } + return err + }) + if isDuplicateFriendError(err) { + return false, nil + } + return added, err +} + +func isDuplicateFriendError(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "already") || + strings.Contains(msg, "exists") || + strings.Contains(msg, "重复") || + strings.Contains(msg, "已存在") || + strings.Contains(msg, "好友关系已存在") +} + +func uniquePositiveInt64(in []int64) []int64 { + out := make([]int64, 0, len(in)) + seen := make(map[int64]struct{}, len(in)) + for _, v := range in { + if v <= 0 { + continue + } + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + out = append(out, v) + } + return out +} diff --git a/app/service/imser/imser.go b/app/service/imser/imser.go new file mode 100644 index 0000000..8e292f0 --- /dev/null +++ b/app/service/imser/imser.go @@ -0,0 +1,89 @@ +package imser + +import ( + "encoding/hex" + "net/url" + "strconv" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/crypt" + "91porn-server/common/log" + "91porn-server/middleware/ua" + "91porn-server/models/v/usermod" +) + +// GetSign 获取IM sign +func GetSign(uid uint64, ua ua.UA) string { + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return "" + } + // IsVip := false + // if userModel.VipExpireDate.After(time.Now()) { + // IsVip = true + // } + var voiceChatNotify string + if common.IsSpecifyVerBaseOnUa(ua, constant.Ver3_1_0) { + voiceChatNotify = "支持语音聊天" + } else { + voiceChatNotify = "不支持语音聊天" + } + ul := url.Values{} + ul.Add("id", strconv.FormatUint(uid, 10)) + ul.Add("appId", appg.Conf.Kefu.AppID) + ul.Add("platName", appg.Conf.Kefu.PlatName) + ul.Add("userName", u.Name+"**"+ua.SysType+"**"+ua.DevType+"**"+ua.Ver+"**"+voiceChatNotify) + ul.Add("isVip", "false") + ul.Add("avatar", common.BindUrl(appg.Conf.URL.OriginUrl, u.Portrait)) + dcodeurl, _ := url.QueryUnescape(ul.Encode()) + sign, err := crypt.AesEncrypt(dcodeurl, appg.Conf.Kefu.AppSecret) + if err != nil { + log.Warn("create im sign wrong", log.Any("warn", err)) + return "" + } + return "/kefu/?sign=" + hex.EncodeToString(sign) + "&appId=" + appg.Conf.Kefu.AppID + "&theme=" + appg.Conf.Kefu.Theme +} + +// GetWhiteSign 获取White IM sign +func GetWhiteSign() string { + return "/kefu/?" + "appId=" + appg.Conf.Kefu.AppID + "&anonymous=true" +} + +// GetSignNew 新获取IM sign +func GetSignNew(uid uint64, ua ua.UA) string { + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return "" + } + // IsVip := false + // if userModel.VipExpireDate.After(time.Now()) { + // IsVip = true + // } + var voiceChatNotify string + if common.IsSpecifyVerBaseOnUa(ua, constant.Ver3_1_0) { + voiceChatNotify = "支持语音聊天" + } else { + voiceChatNotify = "不支持语音聊天" + } + ul := url.Values{} + ul.Add("id", strconv.FormatUint(uid, 10)) + ul.Add("appId", appg.Conf.Kefu.AppID) + ul.Add("platName", appg.Conf.Kefu.PlatName) + ul.Add("userName", u.Name+"**"+ua.SysType+"**"+ua.DevType+"**"+ua.Ver+"**"+voiceChatNotify) + ul.Add("isVip", "false") + ul.Add("avatar", common.BindUrl(appg.Conf.URL.OriginUrl, u.Portrait)) + dcodeurl, _ := url.QueryUnescape(ul.Encode()) + sign, err := crypt.AesEncrypt(dcodeurl, appg.Conf.Kefu.AppSecret) + if err != nil { + log.Warn("create im sign wrong", log.Any("warn", err)) + return "" + } + return "/kefu/customer/im?sign=" + hex.EncodeToString(sign) + "&appId=" + appg.Conf.Kefu.AppID + "&theme=" + appg.Conf.Kefu.Theme +} + +// GetWhiteSignNew 新获取White IM sign +func GetWhiteSignNew() string { + return "/kefu/customer/im?appId=" + appg.Conf.Kefu.AppID + "&anonymous=true" +} diff --git a/app/service/imser/sdk.go b/app/service/imser/sdk.go new file mode 100644 index 0000000..c455402 --- /dev/null +++ b/app/service/imser/sdk.go @@ -0,0 +1,458 @@ +package imser + +import ( + "fmt" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/imclient" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/models/v/imusermod" + "91porn-server/models/v/usermod" + + "github.com/go-redsync/redsync/v4" +) + +const ( + defaultIMV2DynamicConfigDomain = "https://sdk.api.cdflow.cn" + defaultIMV2SocketURL = "wss://wss.cdflow.cn:32214" + + // ensureSDKUser 抢锁失败后,轮询 imUser 表的间隔与最大次数(10 × 200ms = 最多等 2s) + imSDKEnsureWaitInterval = 200 * time.Millisecond + imSDKEnsureWaitTries = 10 +) + +var appTokenCache = struct { + sync.RWMutex + token string + expiresAt time.Time +}{} + +type SDKAuthInfo struct { + Enabled bool `json:"enabled"` + ImToken string `json:"imToken"` + Token string `json:"token,omitempty"` + UserID uint64 `json:"userId"` + ImUserID int64 `json:"imUserId"` + Registered bool `json:"registered"` + NewlyRegistered bool `json:"newlyRegistered,omitempty"` + DynamicConfigDomain string `json:"dynamicConfigDomain"` + SocketURL string `json:"socketURL"` + SysType string `json:"sysType,omitempty"` + OS string `json:"os,omitempty"` + OSType string `json:"osType,omitempty"` +} + +type SDKPublicConfig struct { + Enable bool `json:"enable"` + BaseURL string `json:"baseUrl"` + SocketURL string `json:"socketUrl"` + MerchantCode string `json:"merchantCode"` + TenantCode string `json:"tenantCode"` +} + +func SDKEnabled() bool { + return newSDKClient().Enabled() +} + +func GetSDKPublicConfig() SDKPublicConfig { + cfg := sdkConfig() + return SDKPublicConfig{ + Enable: newSDKClient().Enabled(), + BaseURL: cfg.BaseURL, + SocketURL: appg.Conf.ImV2.SocketURL, + MerchantCode: cfg.MerchantCode, + TenantCode: cfg.TenantCode, + } +} + +func GetSDKAuth(uid uint64) (*SDKAuthInfo, error) { + c := newSDKClient() + if !c.Enabled() { + reason := c.DisabledReason() + log.Error("GetSDKAuth im sdk disabled", log.Any("uid", uid), log.Any("reason", reason)) + return nil, fmt.Errorf("im sdk is disabled: %s", reason) + } + // 确保 IM 用户存在(内部以 imUser 表为准 + 并发锁),并得知本次是否新注册 + imUserID, newlyRegistered, err := ensureSDKUser(uid) + if err != nil { + return nil, err + } + if err := withAppToken(c, func(token string) error { + return c.SetOnlineStatus(imclient.SetOnlineStatusRequest{ + UserID: imUserID, + ShowOnlineStatus: true, + }, token) + }); err != nil { + log.Warn("set im online status visible failed", log.Any("uid", uid), log.Any("imUserId", imUserID), log.E(err)) + } + token, err := cachedUserToken(c, imUserID) + if err != nil { + log.Error("get user im token fail", log.Any("uid", uid), log.Any("imUserId", imUserID), log.E(err)) + return nil, err + } + return &SDKAuthInfo{ + Enabled: true, + ImToken: token, + Token: token, + UserID: uid, + ImUserID: imUserID, + Registered: imUserID > 0, + NewlyRegistered: newlyRegistered, + DynamicConfigDomain: dynamicConfigDomain(), + SocketURL: socketURL(), + OS: "web", + OSType: "web", + }, nil +} + +func SendPrivateMessage(sendUID, takeUID uint64, content string, imgURLs []string) error { + c := newSDKClient() + if !c.Enabled() { + return nil + } + senderID, err := EnsureSDKUser(sendUID) + if err != nil { + return err + } + receiverID, err := EnsureSDKUser(takeUID) + if err != nil { + return err + } + if content != "" { + if err = withAppToken(c, func(token string) error { + return c.SendMessage(imclient.SendMessageRequest{ + SenderID: senderID, + ReceiverID: receiverID, + Content: content, + MessageType: imclient.MessageTypeText, + }, token) + }); err != nil { + return err + } + } + for _, imgURL := range imgURLs { + if imgURL == "" { + continue + } + if err = withAppToken(c, func(token string) error { + return c.SendMessage(imclient.SendMessageRequest{ + SenderID: senderID, + ReceiverID: receiverID, + Content: "", + MessageType: imclient.MessageTypeImage, + Attachment: &imclient.Attachment{ + URL: imgURL, + }, + }, token) + }); err != nil { + return err + } + } + return nil +} + +func SyncSDKBaseInfo(uid uint64, name, portrait, signature *string) error { + c := newSDKClient() + if !c.Enabled() { + return nil + } + imUserID, err := EnsureSDKUser(uid) + if err != nil { + return err + } + req := imclient.UpdateBaseInfoRequest{ + UserID: imUserID, + } + if name != nil { + req.Name = *name + } + if portrait != nil { + req.ImgURL = common.BindUrl(appg.Conf.URL.OriginUrl, *portrait) + } + if signature != nil { + req.Signature = *signature + } + if req.Name == "" && req.ImgURL == "" && req.Signature == "" { + return nil + } + return withAppToken(c, func(token string) error { + return c.UpdateBaseInfo(req, token) + }) +} + +// EnsureSDKUser 确保 IM 用户存在并返回 imUserID。 +func EnsureSDKUser(uid uint64) (int64, error) { + imUserID, _, err := ensureSDKUser(uid) + return imUserID, err +} + +// ensureSDKUser 确保 IM 用户存在并返回 imUserID;newlyRegistered 表示本次是否真正发起了 SDK 注册 +// (命中 imUser 表或回填历史用户均为 false)。以 imUser 表为准判断是否已注册。 +func ensureSDKUser(uid uint64) (int64, bool, error) { + if uid == 0 { + log.Warn("EnsureSDKUser uid is zero") + return 0, false, fmt.Errorf("uid is empty") + } + // fast path:以 imUser 表为准,已注册直接返回 + if mapping, _ := imusermod.FindByUIDQuiet(uid); mapping.IMUserID > 0 { + return mapping.IMUserID, false, nil + } + // 未注册 → 抢锁串行化,防止并发重复注册。只抢一次、不自旋。 + mu := redis.BuildLock(appg.Redis, redisconst.IMSDKEnsureLockKey(uid), + redsync.WithExpiry(10*time.Second), + redsync.WithTries(1), + ) + if err := mu.Lock(); err != nil { + // 没抢到锁 → 已有别的请求在注册:每 200ms 轮询一次 imUser 表,最多等 2s + for i := 0; i < imSDKEnsureWaitTries; i++ { + time.Sleep(imSDKEnsureWaitInterval) + if mapping, _ := imusermod.FindByUIDQuiet(uid); mapping.IMUserID > 0 { + return mapping.IMUserID, false, nil + } + } + return 0, false, fmt.Errorf("ensure sdk user busy: wait imUser timeout, uid=%d", uid) + } + defer func() { _, _ = mu.Unlock() }() + // 双重检查:等锁期间可能已被其他请求注册完,避免重复注册 + if mapping, _ := imusermod.FindByUIDQuiet(uid); mapping.IMUserID > 0 { + return mapping.IMUserID, false, nil + } + u, err := usermod.FindUserByUID(uid) + if err != nil { + log.Warn(fmt.Sprintf("EnsureSDKUser usermod.FindUserByUID fail uid=%d", uid), log.E(err)) + return 0, false, err + } + if u == nil { + log.Warn(fmt.Sprintf("EnsureSDKUser user is nil uid=%d", uid)) + return 0, false, fmt.Errorf("user %d not found", uid) + } + // 一切以 imUser 表为准:表里无映射即视为未注册,直接走 SDK 注册(不信任 user.ImUserID,不回填) + c := newSDKClient() + thirdPartyID := strconv.FormatUint(uid, 10) + var imUserID int64 + err = withAppToken(c, func(token string) error { + var registerErr error + imUserID, registerErr = c.Register(imclient.RegisterRequest{ + ThirdPartyID: thirdPartyID, + Password: sdkPassword(uid), + Nickname: u.Name, + Avatar: u.Portrait, + }, token) + return registerErr + }) + if err != nil { + log.Warn(fmt.Sprintf("EnsureSDKUser c.Register fail uid=%d", uid), log.E(err)) + return 0, false, err + } + if imUserID <= 0 { + log.Warn(fmt.Sprintf("EnsureSDKUser im register returned empty user id uid=%d", uid), log.E(err)) + return 0, false, fmt.Errorf("im register returned empty user id") + } + if err = imusermod.UpsertByUID(uid, imUserID, thirdPartyID); err != nil { + log.Warn(fmt.Sprintf("EnsureSDKUser upsert fail uid=%d", uid), log.E(err)) + return 0, false, err + } + return imUserID, true, nil +} + +// InitAppToken 启动时异步取一次 appToken(不阻断启动),并每小时刷新一次,写入内存+redis。 +func InitAppToken() { + if !SDKEnabled() { + return + } + common.Go(func() { + if err := refreshAppToken(); err != nil { + log.Warn("init app token failed", log.E(err)) + } + }) + common.Go(func() { + ticker := time.NewTicker(time.Hour) + defer ticker.Stop() + for range ticker.C { + if err := refreshAppToken(); err != nil { + log.Warn("refresh app token failed", log.E(err)) + } + } + }) +} + +// refreshAppToken 从 SDK 取最新 appToken,写入内存 + redis。 +func refreshAppToken() error { + c := newSDKClient() + if !c.Enabled() { + return nil + } + token, err := c.AppToken() + if err != nil { + return err + } + if token == "" { + return fmt.Errorf("im app token empty") + } + ttl := sdkTokenTTL() + cacheSeconds := ttl - 60 + if cacheSeconds < 60 { + cacheSeconds = ttl + } + setToken(token, time.Duration(cacheSeconds)*time.Second) + return nil +} + +// setToken 写入内存 + redis。 +func setToken(token string, ttl time.Duration) { + appTokenCache.Lock() + appTokenCache.token = token + appTokenCache.expiresAt = time.Now().Add(ttl) + appTokenCache.Unlock() + if err := appg.Redis.Set(redisconst.IMSDKAppTokenKey, token, ttl); err != nil { + log.Warn("set app token to redis fail", log.E(err)) + } +} + +// getToken 取 appToken:优先内存(读锁),取不到再取 redis(命中则回种内存)。 +func getToken() string { + appTokenCache.RLock() + token := appTokenCache.token + appTokenCache.RUnlock() + if token != "" { + return token + } + if v, err := appg.Redis.Get(redisconst.IMSDKAppTokenKey); err == nil && v != nil && *v != "" { + appTokenCache.Lock() + appTokenCache.token = *v + appTokenCache.Unlock() + return *v + } + return "" +} + +func appToken(c *imclient.Client) (string, error) { + if token := getToken(); token != "" { + return token, nil + } + // 内存和 redis 都没有(启动初期或刷新失败)→ 兜底同步取一次 + if err := refreshAppToken(); err != nil { + return "", err + } + if token := getToken(); token != "" { + return token, nil + } + return "", fmt.Errorf("im app token unavailable") +} + +func invalidateAppToken() { + appTokenCache.Lock() + appTokenCache.token = "" + appTokenCache.expiresAt = time.Time{} + appTokenCache.Unlock() + _, _ = appg.Redis.Del(redisconst.IMSDKAppTokenKey) +} + +func withAppToken(c *imclient.Client, fn func(token string) error) error { + for attempt := 0; attempt < 2; attempt++ { + token, err := appToken(c) + if err != nil { + return err + } + err = fn(token) + if err == nil { + return nil + } + if attempt == 0 && imclient.IsSessionExpired(err) { + invalidateAppToken() + continue + } + return err + } + return fmt.Errorf("im app token retry exhausted") +} + +// cachedUserToken 获取 IM 用户 token,缓存 12 小时,避免每次鉴权都请求 SDK。 +// 缓存按 imUserID 维度,Redis 故障时降级为直接请求,不阻断鉴权。 +func cachedUserToken(c *imclient.Client, imUserID int64) (string, error) { + key := redisconst.IMSDKUserTokenKey(imUserID) + if cached, err := appg.Redis.Get(key); err != nil { + log.Warn("get im user token cache fail", log.Any("imUserId", imUserID), log.E(err)) + } else if cached != nil && *cached != "" { + return *cached, nil + } + token, err := c.UserToken(imUserID) + if err != nil { + return "", err + } + if token != "" { + if err = appg.Redis.Set(key, token, redisconst.IMSDKUserTokenExpire); err != nil { + log.Warn("set im user token cache fail", log.Any("imUserId", imUserID), log.E(err)) + } + } + return token, nil +} + +func sdkTokenTTL() int64 { + ttl := sdkConfig().TokenTTL + if ttl <= 0 { + ttl = 86400 + } + return ttl +} + +func newSDKClient() *imclient.Client { + return imclient.New(sdkConfig()) +} + +func sdkConfig() imclient.Config { + cfg := appg.Conf.ImV2 + return imclient.Config{ + Enable: true, + BaseURL: cfg.BaseURL, + MerchantCode: cfg.MerchantCode, + TenantCode: cfg.TenantCode, + AppKey: cfg.AppKey, + ClientID: cfg.ClientID, + ClientSecret: cfg.ClientSecret, + SignKey: imclient.DefaultSignKey, + AESKey: imclient.DefaultAESKey, + EnableSign: true, + EncryptTimestamp: true, + TokenTTL: 86400, + } +} + +func dynamicConfigDomain() string { + if domain := strings.TrimRight(appg.Conf.ImV2.DynamicConfigDomain, "/"); domain != "" { + return domain + } + rawBaseURL := strings.TrimSpace(appg.Conf.ImV2.BaseURL) + if rawBaseURL != "" { + if u, err := url.Parse(rawBaseURL); err == nil && u.Scheme != "" && u.Host != "" { + return u.Scheme + "://" + u.Host + } + } + return defaultIMV2DynamicConfigDomain +} + +func socketURL() string { + if socket := strings.TrimSpace(appg.Conf.ImV2.SocketURL); socket != "" { + return socket + } + return defaultIMV2SocketURL +} + +func sdkPassword(uid uint64) string { + return fmt.Sprintf("hjll%014d", uid%100000000000000) +} + +func LogSDKSendError(sendUID, takeUID uint64, err error) { + if err == nil { + return + } + log.Warn("im sdk send private message error", log.Any("sendUID", sendUID), log.Any("takeUID", takeUID), log.E(err)) +} diff --git a/app/service/infmtser/infmtser.go b/app/service/infmtser/infmtser.go new file mode 100644 index 0000000..c0fdaa9 --- /dev/null +++ b/app/service/infmtser/infmtser.go @@ -0,0 +1,234 @@ +package infmtser + +import ( + "sort" + "time" + + "91porn-server/models/v/noticefmtmod" + "91porn-server/models/v/noticerecdmod" + "91porn-server/models/v/usermod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ObjectID +type ObjectID = primitive.ObjectID + +type Sender = noticefmtmod.Sender + +type NoticeFmt = noticefmtmod.NoticeFmt + +type Notice struct { + SendAt time.Time `json:"sendAt"` //消息的发送时间 + Sender Sender `json:"sender"` //发送者 + Title string `json:"title"` //标题 + Content string `json:"content"` //内容 +} + +// 通知预览 +type NoticPreview struct { + NewsCount int64 `json:"newsCount"` + Notice +} + +type FMTEnableMatch = noticefmtmod.EnableMatch + +func UpdateNoticePreviewList(uid uint64, curTime time.Time) ([]NoticPreview, bool, error) { + enable := true + enableMatch := FMTEnableMatch{Enable: &enable} + noticeFmts, err := noticefmtmod.GetNoticeFmtListByUID(uid, enableMatch.New()) + if err != nil { + return nil, false, err + } + noticeFmtsLen := len(noticeFmts) + var ( + noticeCodeList = make([]string, noticeFmtsLen) + noticeFmtMap = make(map[string]NoticeFmt) + systemNoticeCodeList = make([]string, 0, noticeFmtsLen) + actAsstNoticeCodeList = make([]string, 0, noticeFmtsLen) + ) + for i, n := range noticeFmts { + sender := n.Sender + if sender == noticefmtmod.System { + systemNoticeCodeList = append(systemNoticeCodeList, n.NoticeCode) + } else if sender == noticefmtmod.ActAsst { + actAsstNoticeCodeList = append(actAsstNoticeCodeList, n.NoticeCode) + } + noticeFmtMap[n.NoticeCode] = n + noticeCodeList[i] = n.NoticeCode + } + lastSendAtMap, err := noticerecdmod.GetLastSendAtMap(uid, noticeCodeList) + if err != nil { + return nil, false, err + } + var nextSendAtMap = make(map[string]time.Time) //新通知 + for _, fmt := range noticeFmts { + nextSendTimeList := noticefmtmod.TimeSlice{} + switch fmt.NoticeType { + case noticefmtmod.MailNoticeType: + nextSendTimeList = fmt.MailNotice.NextSendTimeList(curTime, lastSendAtMap[fmt.NoticeCode]) + case noticefmtmod.RegularNoticeType: + nextSendTimeList = fmt.RegularNotice.NextSendTimeList(curTime, lastSendAtMap[fmt.NoticeCode]) + case noticefmtmod.PlanNoticeType: + nextSendTimeList = fmt.PlanNotice.NextSendTimeList(curTime, lastSendAtMap[fmt.NoticeCode]) + case noticefmtmod.SpecifyNoticeType: + nextSendTimeList = fmt.SpecifyNotice.NextSendTimeList(curTime, lastSendAtMap[fmt.NoticeCode]) + case noticefmtmod.AfterRegistNoticeType: + user, _ := usermod.FindUserByUID(uid) + if user != nil { + afterRegistNoticeEx := noticefmtmod.AfterRegistNoticeEx{AfterRegistNotice: fmt.AfterRegistNotice, CreatedAt: user.CreatedAt} + nextSendTimeList = afterRegistNoticeEx.NextSendTimeList(curTime, lastSendAtMap[fmt.NoticeCode]) + } + } + if nextSendTimeList.Len() != 0 { + nextSendAtMap[fmt.NoticeCode] = nextSendTimeList[0] + } + } + if err = noticerecdmod.UpsertLastSendAt(uid, nextSendAtMap); err != nil { + return nil, false, err + } + return getNoticePreview(uid, noticeFmtMap, systemNoticeCodeList, actAsstNoticeCodeList) +} + +func getNoticePreview(uid uint64, noticeFmtMap map[string]NoticeFmt, systemNoticeCodeList []string, actAsstNoticeCodeList []string) ([]NoticPreview, bool, error) { + sysRecordSlice, err := noticerecdmod.GetRecordSlice(uid, systemNoticeCodeList) + if err != nil { + return nil, false, err + } + astRecordSlice, err := noticerecdmod.GetRecordSlice(uid, actAsstNoticeCodeList) + if err != nil { + return nil, false, err + } + var noticTalkList []NoticPreview + totalHasNew := false + fill := func(recordSlice noticerecdmod.RecordSlice) { + if recordSlice.Len() == 0 { + return + } + sort.Sort(sort.Reverse(recordSlice)) //发送时间降序 + top := recordSlice[0] + noticeFmt, ok := noticeFmtMap[top.NoticeCode] + if !ok { + return + } + notice := Notice{ + SendAt: top.LastSendAt, + Sender: noticeFmt.Sender, + Title: noticeFmt.Title, + Content: noticeFmt.Content, + } + newsCount := int64(0) + for _, v := range recordSlice { + if v.LastSendAt.After(v.LastReadAt) { + newsCount++ + } + } + totalHasNew = totalHasNew || (newsCount > 0) + noticTalkList = append(noticTalkList, NoticPreview{newsCount, notice}) + } + fill(sysRecordSlice) + fill(astRecordSlice) + return noticTalkList, totalHasNew, nil +} + +type NoticePage struct { + HasNext bool `json:"hasNext"` + List []Notice `json:"list"` +} + +type FMTSenderMatch = noticefmtmod.SenderMatch + +type RecdNoticeCodeInMatch = noticerecdmod.NoticeCodeInMatch + +type RecdUIDMatch = noticerecdmod.UIDMatch + +func NoticePages(sender Sender, uid uint64, skip int64, limit int64) (NoticePage, error) { + enable := true + enableMatch := FMTEnableMatch{Enable: &enable} + senderMatch := FMTSenderMatch{Sender: &sender} + noticeFmts, err := noticefmtmod.GetNoticeFmtListByUID(uid, senderMatch.New(), enableMatch.New()) + if err != nil { + return NoticePage{}, err + } + noticeCodeList := make([]string, len(noticeFmts)) + fmtMap := make(map[string]NoticeFmt) + for i, fmt := range noticeFmts { + noticeCodeList[i] = fmt.NoticeCode + fmtMap[fmt.NoticeCode] = fmt + } + sort := bson.D{{Key: "lastSendAt", Value: -1}} + recdNoticeCodeInMatch := RecdNoticeCodeInMatch{NoticeCodes: noticeCodeList} + recdUIDMatch := RecdUIDMatch{UID: &uid} + limitEx := limit + 1 + list, err := noticerecdmod.List(sort, skip, limitEx, recdUIDMatch.New(), recdNoticeCodeInMatch.New()) + if err != nil { + return NoticePage{}, err + } + hasNext := false + if len(list) > int(limit) { + hasNext = true + list = list[:limit] + } + noticeList := make([]Notice, 0, list.Len()) + for _, v := range list { + noticeCode := v.NoticeCode + fmt, ok := fmtMap[noticeCode] + if !ok { + continue + } + notice := Notice{ + SendAt: v.LastSendAt, + Sender: fmt.Sender, + Title: fmt.Title, + Content: fmt.Content, + } + noticeList = append(noticeList, notice) + } + return NoticePage{ + hasNext, + noticeList, + }, nil +} + +func UpdateNoticeReadTime(sender Sender, uid uint64, readAt time.Time) error { + senderMatch := FMTSenderMatch{Sender: &sender} + noticeFmts, err := noticefmtmod.GetNoticeFmtListByUID(uid, senderMatch.New()) + if err != nil { + return err + } + noticeCodeList := make([]string, len(noticeFmts)) + for i, fmt := range noticeFmts { + noticeCodeList[i] = fmt.NoticeCode + } + return noticerecdmod.UpdateReadTime(readAt, uid, noticeCodeList) +} + +const ( + Fans = noticerecdmod.Fans //粉丝 + Like = noticerecdmod.Like //点赞 + Cmet = noticerecdmod.Cmet //评论 + Itte = noticerecdmod.Itte //互动 +) + +// 动态预览 +type TrendPreview struct { + Count int64 `json:"count"` //数量 +} + +// 动态预览映射 +func TrendPreviewMap(uid uint64) (map[noticerecdmod.TrendType]TrendPreview, bool) { + var ( + fensPreviewCount int64 //粉丝预览数 + likePreviewCount int64 //点赞预览数 + cmetPreviewCount int64 //评论预览数 + ittePreviewCount int64 //互动预览数 暂不做 + ) + hasNews := (fensPreviewCount + likePreviewCount + cmetPreviewCount + ittePreviewCount) > 0 + return map[noticerecdmod.TrendType]TrendPreview{ + Fans: {fensPreviewCount}, + Like: {likePreviewCount}, + Cmet: {cmetPreviewCount}, + Itte: {ittePreviewCount}, + }, hasNews +} diff --git a/app/service/integral_config_ser/integral_config.go b/app/service/integral_config_ser/integral_config.go new file mode 100644 index 0000000..28267f8 --- /dev/null +++ b/app/service/integral_config_ser/integral_config.go @@ -0,0 +1,524 @@ +package integral_config_ser + +import ( + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/backpackmod" + "91porn-server/models/v/coupon_record_mod" + "91porn-server/models/v/integralconfigmod" + "91porn-server/models/v/integralexcangemod" + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/videocoupon" + "91porn-server/models/v/walletmod" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func GetAllConfig() []integralconfigmod.APPIntegralConfig { + var data []integralconfigmod.APPIntegralConfig + op := options.Find().SetSort(bson.M{"duration": 1}) + filter := bson.M{"status": true} + list, err := integralconfigmod.QueryAllList(filter, op) + if err != nil { + return data + } + + if len(list) <= 0 { + return data + } + + for _, i := range list { + appdata := integralconfigmod.APPIntegralConfig{ + ID: i.ID, + Name: i.Name, + Type: i.Type, + Desc: i.Desc, + Price: i.Price, + Img: i.Img, + Duration: i.Duration, + } + data = append(data, appdata) + } + return data +} + +func ExchangeIntegral(uid uint64, in *integralconfigmod.ExchangeIntegralReq) stderr.Code { + user, err := usermod.FindUserByUID(uid) + if err != nil { + return stderr.ErrDbQueryError + } + + if user == nil || user.ID.IsZero() { + return stderr.UserIsNotExists + } + if user.HasBanned || user.HasLocked { + return stderr.ErrAccountHasBinded + } + wallet, err := walletmod.GetWallet(uid) + if err != nil { + return stderr.ErrDbQueryError + } + + if wallet.ID.IsZero() || wallet.Integral <= 0 { + return stderr.InsufficientIntegral + } + id, err := primitive.ObjectIDFromHex(in.ID) + if err != nil { + return stderr.Failure + } + // 获取积分配置 + config, err := integralconfigmod.FindOneById(id) + if err != nil { + return stderr.ErrDbQueryError + } + if config.ID.IsZero() { + return stderr.CodeEmptyData + } + + if config.Price > wallet.Integral { + return stderr.InsufficientIntegral + } + var code stderr.Code + switch config.Type { + case integralconfigmod.VIPDate: + // 1 VIP兑换 + code = HandeExchangeVip(config, user) + case integralconfigmod.AICount: + // 2 AI黑科技卷 + code = HandeExchangeAIUndressCount(config, user) + //code = HandeExchangeAICount(config, user) + case integralconfigmod.GoldCoinBonus: + // 3 金币加赠券 + code = HandeExchangeGoldCoinBonus(config, user) + case integralconfigmod.GoldWatch: + // 4 金币观影券 + code = HandeExchangeGoldWatch(config, user) + case integralconfigmod.InKind: + // 5 实物奖励 + code = HandeExchangeInKind(config, user, in) + case integralconfigmod.FreeCount: + // 6 幸运抽奖次数 + code = HandeExchangeFreeCount(config, user) + default: + code = stderr.Success + } + return code +} + +// HandeExchangeAIUndressCount 兑换ai脱衣次数 +func HandeExchangeAIUndressCount(config integralconfigmod.IntegralConfig, user *usermod.User) (code stderr.Code) { + uid := user.UID + integral := -config.Price + var creditPlan = walletmod.CreditPlan{Integral: &integral} + + undressTimes := config.EquivalentPrice + creditPlan.AiUndressFreeTimes = &undressTimes + if err := appg.VideoDB.Trans(func(t *db.MongoTool) error { + var err error + wallet, err := walletmod.Credit(t, creditPlan, uid) + if err != nil { //扣钱 + return err + } + + txnLogs := []txnmod.TransactionLog{ + { + UID: uid, + Integral: -config.Price, + ActualIntegral: float64(-config.Price), + TranType: txnmod.IntegralExchangeAICount.Key(), + TranTypeInt: int64(txnmod.IntegralExchangeAICount), + Desc: "积分兑换AI黑科技券-" + config.Name, + SysType: user.SysType, + RealIntegral: wallet.RealIntegral(), + }, + } + + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + log.Warn(fmt.Sprintf("HandeExchangeAIUndressCount Transaction Log err %s", err.Error())) + return err + } + + return nil + }); err != nil { + log.Warn(fmt.Sprintf("HandeExchangeAIUndressCount Transaction err %s", err.Error())) + return stderr.BuyFailed //通知消息 + } + return stderr.Success +} + +// HandeExchangeAICount 积分兑换AI黑科技劵 +func HandeExchangeAICount(config integralconfigmod.IntegralConfig, user *usermod.User) (code stderr.Code) { + var ( + goodsList []backpackmod.Backpack + now = time.Now() + uid = user.UID + ) + + goodsList = append(goodsList, backpackmod.Backpack{ + UID: user.UID, + GoodsName: config.Name, + GoodsType: backpackmod.AiChangeFaceDiscount, + GoodsValue: config.EquivalentPrice, + GoodsOrigin: "积分兑换AI黑科技劵", + GoodsDesc: config.Desc, + Status: backpackmod.Unused, + ExpiredTime: now.AddDate(0, 0, int(config.Duration)), + CreateTime: now, + }) + + if err := appg.VideoDB.Trans(func(t *db.MongoTool) error { + var err error + wallet, err := walletmod.DebitIntegral(t, config.Price, uid) + if err != nil { //扣钱 + return err + } + + if err := backpackmod.AddGoodsMany(t, uid, goodsList); err != nil { + return err + } + + txnLogs := []txnmod.TransactionLog{ + { + UID: uid, + Integral: -config.Price, + ActualIntegral: float64(-config.Price), + TranType: txnmod.IntegralExchangeAICount.Key(), + TranTypeInt: int64(txnmod.IntegralExchangeAICount), + Desc: "积分兑换AI黑科技券-" + config.Name, + SysType: user.SysType, + RealIntegral: wallet.RealIntegral(), + }, + } + + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + log.Warn(fmt.Sprintf("HandeExchangeAICount Transaction err %s", err.Error())) + return err + } + + return nil + }); err != nil { + log.Warn(fmt.Sprintf("HandeExchangeAICount Transaction err %s", err.Error())) + return stderr.BuyFailed //通知消息 + } + return stderr.Success +} + +// HandeExchangeGoldCoinBonus 积分兑换金币加赠券 +func HandeExchangeGoldCoinBonus(config integralconfigmod.IntegralConfig, user *usermod.User) (code stderr.Code) { + uid := user.UID + // 保存用户优惠券 + now := time.Now() + cId := HandleCId(config.ID) + // 获取观影券是否存在 + c, err := coupon_record_mod.FindOneByCId(cId) + if err != nil { + return stderr.Failure + } + if !c.ID.IsZero() { + return stderr.Failure + } + newCo := coupon_record_mod.CouponRecord{ + CID: cId, + UID: user.UID, + UserName: user.Name, + Name: config.Name, + Count: 1, + Price: config.EquivalentPrice, + Type: coupon_record_mod.PrizeTypeGoldCoinBonus, + Used: false, + IsDelete: false, + ExpireTime: now.Add(time.Hour * 24 * time.Duration(config.Duration)), + } + + if err := appg.VideoDB.Trans(func(t *db.MongoTool) error { + var err error + wallet, err := walletmod.DebitIntegral(t, config.Price, uid) + if err != nil { //扣钱 + return err + } + + txnLogs := []txnmod.TransactionLog{ + { + UID: uid, + Integral: -config.Price, + ActualIntegral: float64(-config.Price), + TranType: txnmod.IntegralExchangeGoldCoinBonus.Key(), + TranTypeInt: int64(txnmod.IntegralExchangeGoldCoinBonus), + Desc: "积分兑换金币加赠券-" + config.Name, + SysType: user.SysType, + RealIntegral: wallet.RealIntegral(), + }, + } + err = coupon_record_mod.InsertOne(newCo) + if err != nil { + return err + } + + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + log.Warn(fmt.Sprintf("HandeExchangeGoldCoinBonus Transaction err %s", err.Error())) + return err + } + + return nil + }); err != nil { + log.Warn(fmt.Sprintf("HandeExchangeGoldCoinBonus Transaction err %s", err.Error())) + return stderr.BuyFailed //通知消息 + } + return stderr.Success +} + +// HandeExchangeGoldWatch 积分兑换金币观影券 +func HandeExchangeGoldWatch(config integralconfigmod.IntegralConfig, user *usermod.User) (code stderr.Code) { + uid := user.UID + var coupons []videocoupon.UserGoldVideoCoupon + coupon := videocoupon.UserGoldVideoCoupon{ + UID: uid, + Num: int(config.EquivalentPrice), + Used: false, + Source: "积分兑换金币观影券", + } + coupons = append(coupons, coupon) + + if err := appg.VideoDB.Trans(func(t *db.MongoTool) error { + var err error + wallet, err := walletmod.DebitIntegral(t, config.Price, uid) + if err != nil { //扣钱 + return err + } + + txnLogs := []txnmod.TransactionLog{ + { + UID: uid, + Integral: -config.Price, + ActualIntegral: float64(-config.Price), + TranType: txnmod.IntegralExchangeGoldWatch.Key(), + TranTypeInt: int64(txnmod.IntegralExchangeGoldWatch), + Desc: "积分兑换金币观影券-" + config.Name, + SysType: user.SysType, + RealIntegral: wallet.RealIntegral(), + }, + } + // 赠送观影券 + if len(coupons) > 0 { + if err := videocoupon.InsertMany(coupons); err != nil { + return err + } + } + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + log.Warn(fmt.Sprintf("HandeExchangeGoldWatch Transaction err %s", err.Error())) + return err + } + + return nil + }); err != nil { + log.Warn(fmt.Sprintf("HandeExchangeGoldWatch Transaction err %s", err.Error())) + return stderr.BuyFailed //通知消息 + } + return stderr.Success +} + +// HandeExchangeFreeCount 积分兑换幸运抽奖次数 +func HandeExchangeFreeCount(config integralconfigmod.IntegralConfig, user *usermod.User) (code stderr.Code) { + uid := user.UID + integral := -config.Price + var creditPlan = walletmod.CreditPlan{Integral: &integral} + + lotteryTimes := config.EquivalentPrice + creditPlan.LotteryTimes = &lotteryTimes + if err := appg.VideoDB.Trans(func(t *db.MongoTool) error { + var err error + wallet, err := walletmod.Credit(t, creditPlan, uid) + if err != nil { //扣钱 + return err + } + + txnLogs := []txnmod.TransactionLog{ + { + UID: uid, + Integral: -config.Price, + ActualIntegral: float64(-config.Price), + TranType: txnmod.IntegralExchangeFreeCount.Key(), + TranTypeInt: int64(txnmod.IntegralExchangeFreeCount), + Desc: "积分兑换幸运抽奖次数-" + config.Name, + SysType: user.SysType, + RealIntegral: wallet.RealIntegral(), + }, + } + + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + log.Warn(fmt.Sprintf("HandeExchangeFreeCount Transaction err %s", err.Error())) + return err + } + + return nil + }); err != nil { + log.Warn(fmt.Sprintf("HandeExchangeFreeCount Transaction err %s", err.Error())) + return stderr.BuyFailed //通知消息 + } + return stderr.Success +} + +// HandeExchangeVip 积分兑换VIP +func HandeExchangeVip(config integralconfigmod.IntegralConfig, user *usermod.User) (code stderr.Code) { + var vipExpire time.Time + uid := user.UID + now := time.Now() + d := time.Hour * 24 * time.Duration(config.EquivalentPrice) + if user.VipExpireDate.After(now) { //renew + vipExpire = user.VipExpireDate.Add(d) + } else { + vipExpire = now.Add(d) + } + vipLevel := usermod.RenewVIPLevel(user.VipExpireDate, now, user.VipLevel, 1) + sel := usermod.UserSelector{VipExpireDate: &vipExpire, VipLevel: &vipLevel} + + if err := appg.VideoDB.Trans(func(t *db.MongoTool) error { + var err error + wallet, err := walletmod.DebitIntegral(t, config.Price, uid) + if err != nil { //扣钱 + return err + } + history := prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: primitive.NewObjectID(), + UID: uid, + ProductID: config.ID, + Name: config.Name, + Amount: config.Price, + Income: 0, + ProductType: prdcthsomod.VIP, + SysType: user.SysType, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + + // 修改会员 + if err = usermod.UpdateVIP(t, uid, user.VipExpireDate, sel); err != nil { + return err + } + txnLogs := []txnmod.TransactionLog{ + { + UID: uid, + Integral: -config.Price, + ActualIntegral: float64(-config.Price), + TranType: txnmod.IntegralExchangeVip.Key(), + TranTypeInt: int64(txnmod.IntegralExchangeVip), + TransNo: history.ID, + Desc: "积分兑换VIP-" + config.Name, + SysType: user.SysType, + RealIntegral: wallet.RealIntegral(), + }, + } + + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + log.Warn(fmt.Sprintf("HandeExchangeVip Transaction err %s", err.Error())) + return err + } + + return nil + }); err != nil { + log.Warn(fmt.Sprintf("HandeExchangeVip Transaction err %s", err.Error())) + return stderr.BuyFailed //通知消息 + } + return stderr.Success +} + +// HandeExchangeInKind 积分兑换实体 +func HandeExchangeInKind(config integralconfigmod.IntegralConfig, user *usermod.User, in *integralconfigmod.ExchangeIntegralReq) (code stderr.Code) { + //if in.Name == "" || in.Tel == "" || in.Address == "" { + // return stderr.ErrParamError + //} + uid := user.UID + now := time.Now() + + ie := integralexcangemod.IntegralExchange{ + EID: config.ID, + UID: user.UID, + Portrait: user.Portrait, + Name: in.Name, + Tel: in.Tel, + Address: in.Address, + Status: integralexcangemod.PENDING, + CreateTime: now, + } + + if err := appg.VideoDB.Trans(func(t *db.MongoTool) error { + var err error + wallet, err := walletmod.DebitIntegral(t, config.Price, uid) + if err != nil { //扣钱 + return err + } + + err = integralexcangemod.InsertOne(t, ie) + if err != nil { + return err + } + + txnLogs := []txnmod.TransactionLog{ + { + UID: uid, + Integral: -config.Price, + ActualIntegral: float64(-config.Price), + TranType: txnmod.IntegralExchangeInKind.Key(), + TranTypeInt: int64(txnmod.IntegralExchangeInKind), + Desc: "积分兑换实物-" + config.Name, + SysType: user.SysType, + RealIntegral: wallet.RealIntegral(), + }, + } + + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + log.Warn(fmt.Sprintf("HandeExchangeInKind Transaction err %s", err.Error())) + return err + } + + return nil + }); err != nil { + log.Warn(fmt.Sprintf("HandeExchangeInKind Transaction err %s", err.Error())) + return stderr.BuyFailed //通知消息 + } + return stderr.Success +} + +func HandleCId(id primitive.ObjectID) string { + return id.Hex() + common.UUID() +} + +func GetRecordConfig(uid uint64) []integralconfigmod.AppIntegralRecord { + var data []integralconfigmod.AppIntegralRecord + filter := bson.M{"tranTypeInt": bson.M{"$in": []txnmod.TransType{ + txnmod.IntegralExchangeVip, + txnmod.IntegralExchangeAICount, + txnmod.IntegralExchangeGoldCoinBonus, + txnmod.IntegralExchangeGoldWatch, + txnmod.IntegralExchangeInKind, + txnmod.IntegralExchangeFreeCount, + }}, "uid": uid} + op := options.Find().SetSort(bson.D{{"createdAt", -1}}) + all, err := txnmod.QueryAll(filter, op) + if err != nil { + return data + } + + for _, i := range all { + appdata := integralconfigmod.AppIntegralRecord{ + ID: i.ID, + Integral: i.Integral, + Name: i.TranType, + Desc: i.Desc, + Type: int(i.TranTypeInt), + CreatedAt: i.CreatedAt, + } + data = append(data, appdata) + } + return data +} diff --git a/app/service/likeser/likeser.go b/app/service/likeser/likeser.go new file mode 100644 index 0000000..cd26b88 --- /dev/null +++ b/app/service/likeser/likeser.go @@ -0,0 +1,279 @@ +package likeser + +import ( + "91porn-server/app/service/messageser" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/stderr" + topser "91porn-server/common/top" + topasist "91porn-server/common/top/asistant" + "91porn-server/models/v/aiplazamod" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/likemod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/messagemod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "errors" + "fmt" + "time" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ThumbsUp 视频/评论点赞 +func ThumbsUp(ctx *gin.Context, uid uint64, objType string, objID primitive.ObjectID, tagID primitive.ObjectID) (code stderr.Code, err error) { + var publisherID uint64 + var vMod vidmod.VideoModel + var cMod cmtmod.Comment + var media mediamod.Media + + switch objType { + case constant.LikeTypeSP, constant.LikeTypeShort, constant.LikeTypeCover, constant.LikeTypePic, constant.LikeTypeSEED_LINK: + publisherID, vMod, err = handleVideo(objID) + if err != nil { + return stderr.Failure, err + } + case constant.LikeTypeText, constant.LikeTypeCartoon, constant.LikeTypeComics, constant.LikeTypeDrama: + media, err = handleMedia(objID) + if err != nil { + return stderr.Failure, err + } + if objType == constant.LikeTypeDrama && (media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete) { + return stderr.LikeErrVideoIsInvalid, errors.New("drama is inactive") + } + + case constant.AiPlaza: + // ai广场 + + case constant.LikeTypeComment: + publisherID, cMod, vMod, err = handleComment(objID) + if err != nil { + return stderr.Failure, err + } + publisherID = cMod.UserID + } + + lMod, err := likemod.FindOneByUIDAndObjID(uid, objID) + if err != nil { + code = stderr.LikeErrGetDataFailed + log.Error("app service like ThumbsUp FindOneByUIDAndObjID err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objID", objID)) + return + } + if !lMod.ID.IsZero() { + code = stderr.LikeHasAlreadyThumbs + return + } + likeMod := likemod.Like{ + Type: objType, + ObjID: objID, + UserID: uid, + LikedUserID: publisherID, + } + + if !tagID.IsZero() && tagmod.IsExists(tagID) { + likeMod.TagID = tagID + _ = tagmod.IncreaseTagLikeCount(tagID) + } + + err = likemod.InsertOne(&likeMod) + if err != nil { + code = stderr.LikeErrThumbsFailed + log.Error("app service like ThumbsUp InsertOne err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objID", objID)) + return + } + common.Go(func() { + user, err := usermod.FindUserByUID(uid) + if err != nil { + return + } + // 增加视频/评论 点赞次数 + switch objType { + case constant.AiPlaza: + aiplazamod.IncLikeCount(objID) + case constant.CollectTypeSP, constant.CollectTypeShort, constant.CollectTypeCover, constant.CollectTypePIC: + topasist.Incr(topser.TypeVideo(objType), objID.Hex(), 1) + fallthrough + case constant.LikeTypeSEED_LINK: + if err := vidmod.IncrementRecommendInteraction(objID, vidmod.RecommendInteractionLike); err != nil { + log.Error("increment recommend like failed", log.E(err), log.Any("objID", objID)) + } + err = vidmod.IncLikeCount(objID) + if err != nil { + code = stderr.LikeErrIncCountFailed + log.Error("app service like ThumbsUp IncLikeCount err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objID", objID)) + return + } + // 发送动态消息 + _ = messageser.DynamicMsgUserGroup(messagemod.PublicAddMsg{ + SendUid: uid, + TakeUid: vMod.PublisherID, + MsgType: messagemod.LikeMsg, + ObjId: vMod.ID, + ObjName: vMod.Title, + ObjCover: vMod.Cover, + ObjType: vMod.NewsType, + }) + + break + case constant.LikeTypeComment: + err = cmtmod.CmtIncreaseLikeCountByID(1, objID) + if err != nil { + code = stderr.LikeErrIncCountFailed + log.Error("app service like ThumbsUp CmtIncreaseLikeCountByID err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objID", objID)) + return + } + dynamicContent := fmt.Sprintf("%s 于 %s 点赞了你的评论", user.Name, time.Now().Format("15:04:05")) + if !vMod.ID.IsZero() && vMod.PublisherID > 0 { + // 增加点赞消息通知动态 + messageser.DynamicMsgUserGroup(messagemod.PublicAddMsg{ + SendUid: uid, + TakeUid: publisherID, + MsgType: messagemod.LikeCommentMsg, + ObjId: vMod.ID, + ObjName: vMod.Title, + ObjCover: vMod.Cover, + ObjType: vMod.NewsType, + Content: dynamicContent, + }) + } + + case constant.LikeTypeCartoon, constant.LikeTypeComics, constant.LikeTypeText, constant.LikeTypeDrama: + err = mediamod.IncreaseCountLikeByIDs(1, objID) + if err != nil { + log.Error("app service media like ThumbsUp IncreaseLikeCountByID err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objID", objID)) + } + + if err != nil { + log.Error("ReportVideoPurchase fail", log.E(err)) + code = stderr.LikeErrThumbsFailed + } + } + }) + code = stderr.Success + return +} + +func handleMedia(id primitive.ObjectID) (media mediamod.Media, err error) { + // 获取播单列表 + media, err = mediamod.GetInfo(id) + if err != nil { + return media, err + } + + if media.ID.IsZero() { + return media, errors.New("media is null") + } + return media, nil +} + +func handleVideo(id primitive.ObjectID) (uid uint64, vMod vidmod.VideoModel, err error) { + var publishId uint64 + vMod, err = vidmod.GetVideoInfo(id.Hex()) + if err != nil { + log.Error("app service like ThumbsUp GetVideoInfo err", log.E(err), log.Any("uid", uid), log.Any("objID", id)) + return publishId, vMod, err + } + if vMod.ID.IsZero() { + log.Error("app service like ThumbsUp video mod is nil err", log.Any("uid", uid), log.Any("objID", id)) + return publishId, vMod, errors.New("video is null") + } + // 视频未通过审核/未审核 + if vMod.Status != 1 && vMod.Status != 3 { + return publishId, vMod, errors.New("video is status is err") + } + publishId = vMod.PublisherID + // 增加作者点赞数 + common.Go(func() { + usermod.IncLikeCount(publishId) + }) + return publishId, vMod, nil +} + +func handleComment(id primitive.ObjectID) (publishId uint64, cMod cmtmod.Comment, vMod vidmod.VideoModel, err error) { + + cMod, err = cmtmod.CmtFindOneByID(id) + if err != nil { + err = stderr.ErrDbQueryError + return + } + if cMod.ID.IsZero() { + err = stderr.ErrDbQueryError + } + if cMod.ObjType == "video" { + // 获取视频 + vMod, err = vidmod.GetVideoInfo(cMod.ObjID.Hex()) + if err != nil { + err = stderr.ErrDbQueryError + log.Error("app service like ThumbsUp GetVideoInfo err", log.E(err), log.Any("objID", cMod.ObjID.Hex())) + return + } + if vMod.ID.IsZero() { + err = stderr.ErrDbQueryError + log.Error("app service like ThumbsUp video mod is nil err", log.Any("objID", cMod.ObjID.Hex())) + return + } + // 视频未通过审核/未审核 + if vMod.Status != 1 && vMod.Status != 3 { + err = stderr.LikeErrVideoIsInvalid + return + } + } + + return cMod.UserID, cMod, vMod, err +} + +// ThumbsDown 取消点赞 +func ThumbsDown(ctx *gin.Context, uid uint64, objType string, objIDs ...primitive.ObjectID) (code stderr.Code, err error) { + if len(objIDs) == 0 { + return stderr.Success, nil + } + lMods, err := likemod.FindLikesByObjIDS(objType, uid, objIDs) + if err != nil { + code = stderr.LikeErrGetDataFailed + log.Error("app service like ThumbsDown FindOneByUIDAndObjID err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objIDs", objIDs)) + return + } + if len(lMods) == 0 { + code = stderr.LikeHasNotThumbs + return + } + err = likemod.DeleteMany(uid, objType, objIDs) + if err != nil { + code = stderr.LikeErrThumbsFailed + log.Error("app service like ThumbsDown DeleteOne err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objIDs", objIDs)) + return + } + // 减少视频/评论 点赞数 + switch objType { + case constant.AiPlaza: + aiplazamod.DecLikeCount(objIDs) + case constant.LikeTypeSP, constant.LikeTypeShort, constant.LikeTypeCover, constant.LikeTypePic, constant.LikeTypeSEED_LINK: + err = vidmod.DecLikeCount(objIDs...) + if err != nil { + code = stderr.LikeErrDecCountFailed + log.Error("app service like ThumbsDown DecLikeCount err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objIDs", objIDs)) + return + } + + case constant.LikeTypeCartoon, constant.LikeTypeComics, constant.LikeTypeText, constant.LikeTypeDrama: + err = mediamod.IncreaseCountLikeByIDs(-1, objIDs...) + if err != nil { + code = stderr.LikeErrGetDataFailed + return + } + + case constant.LikeTypeComment: + err = cmtmod.CmtIncreaseLikeCountByID(-1, objIDs...) + if err != nil { + code = stderr.LikeErrDecCountFailed + log.Error("app service like ThumbsDown CmtIncreaseLikeCountByID err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objIDs", objIDs)) + return + } + } + code = stderr.Success + return +} diff --git a/app/service/likeser/page.go b/app/service/likeser/page.go new file mode 100644 index 0000000..7984111 --- /dev/null +++ b/app/service/likeser/page.go @@ -0,0 +1,140 @@ +package likeser + +import ( + "sync" + "time" + + "91porn-server/app/service/searcher" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/models/v/likemod" + "91porn-server/models/v/usermod" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type VideoRes = searcher.VideoRes + +type UserBInfo = usermod.BaseInfo + +type ObjectID = primitive.ObjectID + +type Like = likemod.Like + +type Record struct { + //点赞者UID + LUID uint64 `json:"lUID"` + //点赞者名字 + LName string `json:"lName"` + //点赞者头像 + LPortrait string `json:"lPortrait"` + //点赞类型 video:对视频点赞 comment:对评论点赞 + LType string `json:"lType"` + //点赞时间 + LTime time.Time `json:"lTime"` + //视屏信息 + Video VideoRes `json:"video"` +} + +type RecordPage struct { + HasNext bool `json:"hasNext"` + List []Record `json:"list"` +} + +func RecordPages(uid uint64, skip, limit int64) (RecordPage, error) { + limitEx := limit + 1 + likeList, err := likemod.ListByLikedUserID(uid, skip, limitEx) + if err != nil { + return RecordPage{}, err + } + hasNext := false + if len(likeList) > int(limit) { + hasNext = true + likeList = likeList[:limit] + } + vidMap := make(map[ObjectID]ObjectID, len(likeList)) //object->vid map + vids := make([]ObjectID, 0, len(likeList)) + cmtIDs := make([]ObjectID, 0, len(likeList)) + uids := make([]uint64, 0, len(likeList)) + + for _, v := range likeList { + if v.Type == "video" { + vids = append(vids, v.ObjID) //视屏ID + vidMap[v.ObjID] = v.ObjID + } else if v.Type == "comment" { + cmtIDs = append(cmtIDs, v.ObjID) //评论ID + } + uids = append(uids, v.UserID) + } + + var ( + wg = sync.WaitGroup{} + videoResList []VideoRes + userInfos []*UserBInfo + ) + wg.Add(2) + common.Go(func() { + defer wg.Done() + list, err := searcher.GetVideoResList(uid, vids) + if err != nil { + log.Error("commentser GetVideoResList faild", log.E(err)) + } + videoResList = list + }) + common.Go(func() { + defer wg.Done() + list, err := usermod.GetUsersBaseInfo(uids) + if err != nil { + log.Error("commentser GetUsersBaseInfo faild", log.E(err)) + } + userInfos = list + }) + wg.Wait() + videoResMap := make(map[ObjectID]VideoRes, len(videoResList)) + for _, v := range videoResList { + vidp := v.VideoInfo.ID + if vidp != nil { + videoResMap[*vidp] = v + } + } + userInfoMap := make(map[uint64]*usermod.BaseInfo, len(userInfos)) + for _, v := range userInfos { + userInfoMap[v.UID] = v + } + recordList := tRecordList(likeList, vidMap, videoResMap, userInfoMap) + return RecordPage{ + hasNext, + recordList, + }, nil +} + +func tRecordList(likeList []Like, + vidMap map[ObjectID]ObjectID, + videoResMap map[ObjectID]VideoRes, + userInfoMap map[uint64]*usermod.BaseInfo, +) []Record { + recordList := make([]Record, 0, len(likeList)) + for _, v := range likeList { + userInfo, ok := userInfoMap[v.UserID] + if !ok { + continue + } + vid, ok := vidMap[v.ObjID] + if !ok { + continue + } + videoRes, ok := videoResMap[vid] + if !ok { + continue + } + record := Record{ + LUID: userInfo.UID, + LName: userInfo.Name, + LPortrait: userInfo.Portrait, + LType: v.Type, + LTime: v.CreatedAt, + Video: videoRes, + } + recordList = append(recordList, record) + } + return recordList +} diff --git a/app/service/luckydrawser/base.go b/app/service/luckydrawser/base.go new file mode 100644 index 0000000..2cd349b --- /dev/null +++ b/app/service/luckydrawser/base.go @@ -0,0 +1,102 @@ +package luckydrawser + +import "time" + +type PrizeType int32 + +const ( + // 实物 + InKindPrize PrizeType = iota + 1 + // VIP + VipPrize + // 金币 + GoldPrize +) + +type VipType int32 + +const ( + // 默认值 + Default VipType = iota + // 月卡 + MonthCard + // 季卡 + QuarterlyCard + // 年卡 + YearCard + // 终身会员卡 + LifelongCard +) + +type CenterBaseInfo struct { + FreeCount int32 `json:"freeCount"` // 免费次数 + RefreshTime time.Time `json:"refreshTime"` // 免费次数恢复时间 + PrizeList []*PrizeInfo `json:"prizeList"` // 奖品列表 + DrawPrize int64 `json:"drawPrize"` // 单次抽奖价格 + ActivityTitle string `json:"activityTitle"` // 活动标题 + ActivityDesc string `json:"activityDesc"` // 活动注意事项 + ActivityDate string `json:"activityDate"` // 活动时间 +} + +// 奖品信息 +type PrizeInfo struct { + PrizeId int64 `json:"prizeId"` + PrizeName string `json:"prizeName"` + PrizeImgCover string `json:"prizeImgCover"` + PrizePrice int64 `json:"prizePrice"` + PrizeCount int32 `json:"prizeCount"` + PrizeType int32 `json:"prizeType"` + VipCard int32 `json:"vipCard"` +} + +type CenterDrawReq struct { + AppId int32 `json:"appId"` // 应用id + ActivityId int64 `json:"activityId"` // 活动id + UserId int64 `json:"userId"` // 用户id + UserName string `json:"userName"` + UserAvatar string `json:"userAvatar"` + Count int `json:"count"` // 抽奖次数 +} + +// 抽奖返回 +type CenterDrawResp struct { + List []*PrizeInfo `json:"list"` +} + +// 抽奖记录 +type CenterRecordReq struct { + AppId int32 `json:"appId"` // 应用id + ActivityId int64 `json:"activityId"` // 活动id + UserId int64 `json:"userId"` // 用户id +} + +// 抽奖记录返回 +type CenterRecordResp struct { + List []*RecordInfo `json:"list"` +} + +// 跑马灯 +type CenterMarqueeReq struct { + AppId int32 `json:"appId"` // 应用id + ActivityId int64 `json:"activityId"` // 活动id +} + +// 跑马灯返回 +type CenterMarqueeResp struct { + List []*RecordInfo `json:"list"` +} + +// 幸运狼友 +type CenterLuckUserReq struct { + AppId int32 `json:"appId"` // 应用id + ActivityId int64 `json:"activityId"` // 活动id +} + +type CenterLuckUserResp struct { + List []*RecordInfo `json:"list"` +} + +type RecordInfo struct { + PrizeName string `json:"prizeName"` // 奖品名称 + CreateTime time.Time `json:"createTime"` // 创建时间 +} diff --git a/app/service/luckydrawser/lucky_draw.go b/app/service/luckydrawser/lucky_draw.go new file mode 100644 index 0000000..4829567 --- /dev/null +++ b/app/service/luckydrawser/lucky_draw.go @@ -0,0 +1,206 @@ +package luckydrawser + +import ( + "context" + "errors" + "fmt" + "time" + + "91porn-server/common/timeutil" + "91porn-server/models/v/activitymod" + "91porn-server/models/v/prizemod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func BaseInfo(ctx context.Context, in *BaseInfoReq) (out *BaseInfoResp, err error) { + now := time.Now() + wal, err := walletmod.GetWallet(in.UserId) + if err != nil { + return + } + id, err := primitive.ObjectIDFromHex(in.ActivityId) + if err != nil { + return nil, err + } + // 获取活动 + activityInfo, err := activitymod.Get(id) + if err != nil { + return nil, err + } + if activityInfo == nil { + return nil, errors.New("活动不存在") + } + if !activityInfo.Status { + return nil, errors.New("活动未开启") + } + // 活动时间判定 + if !now.After(activityInfo.StartTime) || now.After(activityInfo.EndTime) { + return nil, errors.New("活动已结束") + } + // 获取奖池配置 + prizePoolInfo, err := activitymod.GetPrizePool(id) + if err != nil { + return nil, err + } + if prizePoolInfo == nil { + return nil, errors.New("奖池配置为空") + } + var freeCount int32 + if prizePoolInfo.FreeCount > 0 { + // 获取用户免费记录 + userFree, err := activitymod.GetUserFree(id, in.UserId) + if err != nil { + return nil, err + } + if userFree == nil { + if err = activitymod.AddUserFree(id, in.UserId, prizePoolInfo.FreeCount); err != nil { + return nil, err + } + freeCount = prizePoolInfo.FreeCount + } else { + // 今日已使用 + if userFree.UpdateTime.After(timeutil.BeginningOfDay(time.Now())) { + freeCount = userFree.FreeCount + } else { + // 刷新免费次数 + if err = activitymod.UpdateOneSet(id, in.UserId, prizePoolInfo.FreeCount); err != nil { + return nil, err + } + freeCount = prizePoolInfo.FreeCount + } + } + } + // 获取奖品列表 + prizeList, err := prizemod.GetPrizeListByActivityID(id) + if err != nil { + return nil, err + } + if len(prizeList) == 0 { + return nil, errors.New("奖品配置为空") + } + // 获取用户当日抽奖消费金额 + consumerAmount, _, err := txnmod.GetIncomeByType(in.UserId, txnmod.JiuGongGeDraw, timeutil.BeginningOfDay(now), timeutil.BeginningOfDay(now.AddDate(0, 0, 1))) + out = &BaseInfoResp{ + Balance: wal.Amount, + FreeCount: freeCount, + Price: prizePoolInfo.Price, + Prizes: prizeList, + ConsumerAmount: -int64(consumerAmount), + ActivityTitle: activityInfo.Title, + ActivityDesc: activityInfo.Desc, + ActivityDate: activityInfo.StartTime.Format("2006-01-02") + "到" + activityInfo.EndTime.Format("2006-01-02"), + } + return +} + +func Draw(ctx context.Context, in *DrawReq) ([]*prizemod.Prize, error) { + id, err := primitive.ObjectIDFromHex(in.ActivityId) + if err != nil { + return nil, err + } + // 获取活动 + activityInfo, err := activitymod.Get(id) + if err != nil { + return nil, err + } + if activityInfo == nil { + return nil, errors.New("活动不存在") + } else { + if !activityInfo.Status { + return nil, errors.New("活动未开启") + } + // 活动时间判定 + now := time.Now() + if !now.After(activityInfo.StartTime) || now.After(activityInfo.EndTime) { + return nil, errors.New("活动已结束") + } + } + // 获取奖品列表 + prizeList, err := prizemod.GetPrizeListByActivityID(id) + if err != nil { + return nil, err + } + if len(prizeList) == 0 { + return nil, errors.New("奖品配置为空") + } + // 获取奖池配置 + prizePoolInfo, err := activitymod.GetPrizePool(id) + if err != nil { + return nil, err + } + if prizePoolInfo == nil { + return nil, errors.New("奖池配置为空") + } + // 奖池模式处理 + switch prizePoolInfo.Model { + case activitymod.Game: + case activitymod.Free: + return freeModelHanlder(in, id, prizeList, prizePoolInfo) + } + return nil, nil +} + +// 个人抽奖记录 +func Record(ctx context.Context, activityId string, uid uint64) (interface{}, error) { + id, err := primitive.ObjectIDFromHex(activityId) + if err != nil { + return nil, err + } + list, err := activitymod.QueryAllRecord(bson.M{"activityId": id, "uid": uid}, options.Find().SetLimit(100).SetSort(bson.M{"createTime": -1})) + if err != nil { + return nil, err + } + data := make([]map[string]interface{}, len(list)) + for i, v := range list { + data[i] = map[string]interface{}{ + "name": v.Name + "*" + decimal.NewFromInt32(v.Count).String(), + "createTime": v.CreateTime.Format("2006-01-02 15:04:05"), + } + } + return data, nil +} + +func Marquee(activityId string) ([]string, error) { + id, err := primitive.ObjectIDFromHex(activityId) + if err != nil { + return nil, err + } + list, err := activitymod.GetMarquee(id) + if err != nil { + return nil, err + } + listLen := len(list) + uids := make([]uint64, listLen) + for i, v := range list { + uids[i] = v.UID + } + userMap := make(map[uint64]string, 10) + if len(uids) > 0 { + userList, err := usermod.FindUsersByUID(uids) + if err != nil { + return nil, err + } + for _, v := range userList { + userMap[v.UID] = v.Name + } + for _, v := range userList { + userMap[v.UID] = v.Name + } + } + strs := make([]string, listLen) + for i, v := range list { + if v.UserName != "" { + strs[i] = fmt.Sprintf("恭喜%s 抽中%s", v.UserName, v.Name) + continue + } + strs[i] = fmt.Sprintf("恭喜%s 抽中%s", userMap[v.UID], v.Name) + } + return strs, nil +} diff --git a/app/service/luckydrawser/tool.go b/app/service/luckydrawser/tool.go new file mode 100644 index 0000000..2ef819d --- /dev/null +++ b/app/service/luckydrawser/tool.go @@ -0,0 +1,212 @@ +package luckydrawser + +import ( + "91porn-server/models/v/taskrecordmod" + "errors" + "fmt" + "math/rand" + "time" + + "91porn-server/app/appg" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models/v/activitymod" + "91porn-server/models/v/prizemod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/walletmod" + + "github.com/shopspring/decimal" + "github.com/smallnest/weighted" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 免费模式处理 +func freeModelHanlder(in *DrawReq, activityId primitive.ObjectID, prizeList []*prizemod.Prize, prizePool *activitymod.PrizePool) ([]*prizemod.Prize, error) { + var ( + wg *weighted.RandW = weighted.NewRandW() + drawPrice int64 // 本次抽奖总支出 + prizes []*prizemod.Prize // 本次抽奖结果 + reservoir int64 // 蓄水池金额 + systemIncome int64 // 系统收益金额 + systemRake int64 // 系统抽成金额 + now time.Time = time.Now() + ) + // 奖品分类 + prizeListLen := len(prizeList) + lowPrizes := make([]*prizemod.Prize, 0, prizeListLen) // 低级奖品列表 + middlePrizes := make([]*prizemod.Prize, 0, prizeListLen) // 中级奖品列表 + for _, v := range prizeList { + if v.Level == prizemod.Low { + lowPrizes = append(lowPrizes, v) + } + if v.Level == prizemod.Middle { + middlePrizes = append(middlePrizes, v) + } + wg.Add(v, int(v.Weights.Mul(decimal.NewFromInt(100)).IntPart())) + } + // 获取用户签到信息 + signInfo, err := taskrecordmod.GetByUID(in.UserId) + if err != nil { + return nil, err + } + if signInfo == nil { + if _, err = taskrecordmod.AddTaskRecord(in.UserId); err != nil { + return nil, err + } + } + // 获取用户免费次数 + userFree, err := activitymod.GetUserFree(activityId, in.UserId) + if err != nil { + return nil, err + } + // 使用金币抽奖 + if userFree != nil && userFree.FreeCount < int32(in.Count) { + drawPrice = prizePool.Price * in.Count + wal, err := walletmod.GetWallet(in.UserId) + if err != nil { + return nil, err + } + // 余额不足 + if wal.Amount < drawPrice { + return nil, errors.New("余额不足") + } + } + log.Info(fmt.Sprintf("本次抽奖所需金币为[%d]", drawPrice)) + // 本次抽奖总抽成收益 + systemRake += decimal.NewFromInt(drawPrice).Mul(decimal.NewFromFloat(0.1)).IntPart() + // 本次抽奖蓄水池抽成收益 + reservoir += decimal.NewFromInt(drawPrice).Mul(decimal.NewFromFloat(0.1)).IntPart() + // 本次抽奖系统收益 + systemIncome += drawPrice - systemRake - reservoir + drawRecord := make([]activitymod.UserRecord, in.Count) // 抽奖记录 + for i := 0; i < int(in.Count); i++ { + p, ok := wg.Next().(*prizemod.Prize) + if !ok || p == nil { + return nil, errors.New("异常错误") + } + log.Info(fmt.Sprintf("金币抽奖-正常获取奖品[%s]", p.Name)) + // 玩家使用免费抽奖 + if drawPrice == 0 { + // 替换为低级奖品 + if p.Level != prizemod.Low { + p = lowPrizes[rand.New(rand.NewSource(time.Now().UnixNano())).Intn(len(lowPrizes))] + } + } + // 金币抽奖 杀分判定 固定70%几率杀分 亏损达到预期2倍100%杀分 + if prizePool.SystemIncome < -(prizePool.EstimatedIncome/2) && drawPrice > 0 && p.Level != prizemod.Low { + if rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(100) > 30 || prizePool.SystemIncome < -(prizePool.EstimatedIncome*2) { + log.Info(fmt.Sprintf("金币抽奖-杀分判定成功[当前奖品:%s]", p.Name)) + // 替换为低级奖品 + p = lowPrizes[rand.New(rand.NewSource(time.Now().UnixNano())).Intn(len(lowPrizes))] + log.Info(fmt.Sprintf("金币抽奖-杀分判定成功[替换奖品:%s]", p.Name)) + } + } + // 放分判定 固定30%几率放分 + if prizePool.SystemIncome > prizePool.EstimatedIncome && rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(100) < 30 && drawPrice > 0 && p.Level != prizemod.Middle { + log.Info(fmt.Sprintf("金币抽奖-放分判定成功[当前奖品:%s]", p.Name)) + // 替换为中级奖品 + p = middlePrizes[rand.New(rand.NewSource(time.Now().UnixNano())).Intn(len(middlePrizes))] + log.Info(fmt.Sprintf("金币抽奖-放分判定成功[替换奖品:%s]", p.Name)) + } + // 高级奖品放出判定 + if p.Level == prizemod.High { + if prizePool.Reservoir < p.GetPrice() { + // 替换为中级奖品 + p = middlePrizes[rand.New(rand.NewSource(time.Now().UnixNano())).Intn(len(middlePrizes))] + } else { + // 扣除蓄水池 + prizePool.Reservoir -= p.GetPrice() + reservoir -= p.GetPrice() + } + } + // 增加系统收益 不包含本次抽奖收益 + prizePool.SystemIncome -= p.GetPrice() + systemIncome -= p.GetPrice() + prizes = append(prizes, p) + // 增加抽奖记录 + drawRecord[i] = activitymod.UserRecord{ + ActivityID: activityId, + VIPCardID: p.VIPCardID, + UID: in.UserId, + Name: p.Name, + Type: p.Type, + Price: p.Price, + Count: p.Count, + Value: p.Value, + Level: p.Level, + DrawPrice: prizePool.Price, + CreateTime: now, + } + } + hanlders := make([]prizemod.PrizeHanlder, 0, len(prizes)) + var addGoldHanlder prizemod.PrizeHanlder + var transactionLogs []txnmod.TransactionLog // 资金流水记录 + for _, v := range prizes { + hanlder, err := prizemod.Run(in.UserId, *v, txnmod.JiuGongGeDraw) + if err != nil { + return nil, err + } + switch v.Type { + case prizemod.Gold: + addGoldHanlder = hanlder + hanlders = append(hanlders, hanlder) + default: + if hanlder != nil { + hanlders = append(hanlders, hanlder) + transactionLogs = append(transactionLogs, hanlder.GetTransactionLog()...) + } + } + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + if reservoir != 0 || systemIncome != 0 || systemRake != 0 { + // 更新奖池 + if err := activitymod.UpdatePrizePoolInc(t, activityId, reservoir, systemIncome, systemRake); err != nil { + return err + } + } + // 更新用户免费次数 + if drawPrice == 0 { + if err := activitymod.UpdateOneUserFreeInc(t, activityId, in.UserId, -int32(in.Count)); err != nil { + return err + } + } else if drawPrice > 0 { + wal, err := walletmod.DebitAmount(t, drawPrice, in.UserId) + if err != nil { + return err + } + transactionLogs = append(transactionLogs, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: in.UserId, + Amount: -drawPrice, + ActualAmount: -float64(drawPrice), + TranType: txnmod.JiuGongGeDraw.Key(), + TranTypeInt: int64(txnmod.JiuGongGeDraw), + Desc: fmt.Sprintf("%s消费-%d个金币", "", drawPrice), + RealAmount: wal.RealAmount(), + }) + } + // 更新抽奖记录 + if len(drawRecord) > 0 { + if err = activitymod.AddMany(drawRecord); err != nil { + return err + } + } + for _, v := range hanlders { + if err = v.Run(t); err != nil { + return err + } + } + if addGoldHanlder != nil { + transactionLogs = append(transactionLogs, addGoldHanlder.GetTransactionLog()...) + } + if len(transactionLogs) > 0 { + if err = txnmod.InsertManyTransactionLog(t, transactionLogs); err != nil { + return err + } + } + return nil + }); err != nil { + return nil, err + } + return prizes, nil +} diff --git a/app/service/luckydrawser/type.go b/app/service/luckydrawser/type.go new file mode 100644 index 0000000..d6125a1 --- /dev/null +++ b/app/service/luckydrawser/type.go @@ -0,0 +1,47 @@ +package luckydrawser + +import "91porn-server/models/v/prizemod" + +type BaseInfoReq struct { + ActivityId string `json:"activityId" form:"activityId"` // 活动id + AppId uint64 `json:"appId" form:"appId"` // 活动id + UserId uint64 +} + +type BaseInfoResp struct { + Balance int64 `json:"balance"` // 用户余额(金币) + FreeCount int32 `json:"freeCount"` // 免费次数 + Price int64 `json:"price"` // 单次抽奖价格 + Prizes []*prizemod.Prize `json:"prizes"` // 奖品列表 + ConsumerAmount int64 `json:"consumerAmount"` // 当日消费金额 + ActivityTitle string `json:"activityTitle"` // 活动标题 + ActivityDesc string `json:"activityDesc"` // 活动注意事项 + ActivityDate string `json:"activityDate"` // 活动时间 +} + +type DrawReq struct { + ActivityId string `json:"activityId" form:"activityId"` // 活动id + Count int64 `json:"count" form:"count"` // 抽奖次数 + UserId uint64 +} + +type DrawResp []*PrizeInfo + +type RecordReq struct { + ActivityId string `json:"activityId" form:"activityId"` // 活动id +} + +type RecordResp []*RecordInfo + +type MarqueeReq struct { + ActivityId string `json:"activityId" form:"activityId"` // 活动id +} + +type MarqueeResp []string + +type LuckUserReq struct { + ActivityId uint64 `json:"activityId" form:"activityId"` // 活动id + AppId uint64 `json:"appId" form:"appId"` // 活动id +} + +type LuckUserResp []*RecordInfo diff --git a/app/service/m3u8ticket/sign.go b/app/service/m3u8ticket/sign.go new file mode 100644 index 0000000..9665fb6 --- /dev/null +++ b/app/service/m3u8ticket/sign.go @@ -0,0 +1,159 @@ +package m3u8ticket + +import ( + "reflect" + + "91porn-server/common" + "91porn-server/models/v/vidmod" + + "github.com/gin-gonic/gin" +) + +// maxSignDepth 反射兜底遍历响应体的最大深度,防止异常对象图导致过深递归(视频响应本身是浅树)。 +const maxSignDepth = 8 + +// ticketSigner 实现 vidmod.M3u8Signer:持有本次请求的签票上下文(用户、客户端 IP/UA), +// 由各响应体的 SignM3u8 回调,对交出的每个 m3u8 地址字段就地签票。 +type ticketSigner struct { + userID uint64 + ip string + ua string +} + +// SignM3u8URL 对单个 m3u8 地址字段就地签票;空串或签发失败(绝对地址、非 m3u8 等)时保持原值不变。 +func (s *ticketSigner) SignM3u8URL(field *string, playable, preview bool) { + if field == nil || *field == "" { + return + } + if signed := BuildSignedURL(s.userID, *field, s.ip, s.ua, playable, preview); signed != "" { + *field = signed + } +} + +func requestIP(c *gin.Context) string { + if c == nil { + return "" + } + return common.GetIP(c) +} + +func requestUA(c *gin.Context) string { + if c == nil { + return "" + } + u, err := common.GetUA(c) + if err != nil { + return "" + } + return u.UserAgent +} + +// Sign 是 SignResponse 的便捷封装:自动从上下文取登录用户ID,控制器只需在返回前加一行 +// m3u8ticket.Sign(ctx, data) 即可对响应体里的视频 m3u8 地址按需签票。 +func Sign(c *gin.Context, data interface{}) { + if c == nil { + return + } + SignResponse(c, common.TryGetUID(c), data) +} + +// SignResponse 是各视频接口的统一入口:对响应体 data 里的视频 m3u8 地址就地签票(不再限 H5)。 +// +// data 只要实现 vidmod.M3u8Signable(所有携带视频地址的响应体都已实现)即可零反射签票;另外兼容控制器直接 +// 下发的裸切片(recommser.GetAd 等)与 gin.H(RecommendList 等)。少数尚未实现接口的历史/边缘类型走反射兜底, +// 保证不漏签。data 为 nil / userID 为 0 / 未配置密钥(BuildSignedURL 返回空)时零副作用,兼容灰度未配置密钥。 +func SignResponse(c *gin.Context, userID uint64, data interface{}) { + if data == nil || userID == 0 { + return + } + // data 可能是服务出错时返回的类型化 nil 指针((*Resp)(nil)):接口本身非 nil,但对值接收者方法会解引用 + // nil 而 panic,先挡掉(等价于旧反射对 nil 指针的 IsNil 保护)。 + if rv := reflect.ValueOf(data); rv.Kind() == reflect.Ptr && rv.IsNil() { + return + } + signData(&ticketSigner{userID: userID, ip: requestIP(c), ua: requestUA(c)}, data) +} + +// signData 把 data 分派给对应签票实现:优先零反射的接口/裸切片/map,其余历史类型走反射兜底。 +func signData(s vidmod.M3u8Signer, data interface{}) { + switch d := data.(type) { + case nil: + return + case vidmod.M3u8Signable: + d.SignM3u8(s) + case []*vidmod.VideoInfo: + vidmod.SignM3u8Infos(s, d) + case []*vidmod.VideoInfoResp: + vidmod.SignM3u8Resps(s, d) + case gin.H: + for _, v := range d { + signData(s, v) + } + default: + signByReflect(s, reflect.ValueOf(data), 0) + } +} + +// signByReflect 是反射兜底:仅用于尚未实现 vidmod.M3u8Signable 的历史/边缘响应类型,语义与旧实现一致—— +// 遍历对象图,命中 *VideoInfo/*VideoInfoResp 即签票。正常高频接口都走上面的接口分支,不会到这里。 +// 待剩余类型补齐 SignM3u8 后,可整体删除本函数及 reflect 依赖。 +func signByReflect(s vidmod.M3u8Signer, rv reflect.Value, depth int) { + if depth > maxSignDepth || !rv.IsValid() { + return + } + switch rv.Kind() { + case reflect.Ptr: + if rv.IsNil() { + return + } + switch v := rv.Interface().(type) { + case *vidmod.VideoInfo: + v.SignM3u8(s) + return + case *vidmod.VideoInfoResp: + v.SignM3u8(s) + return + } + signByReflect(s, rv.Elem(), depth+1) + case reflect.Interface: + if rv.IsNil() { + return + } + signByReflect(s, rv.Elem(), depth+1) + case reflect.Struct: + if rv.CanAddr() { + switch v := rv.Addr().Interface().(type) { + case *vidmod.VideoInfo: + v.SignM3u8(s) + return + case *vidmod.VideoInfoResp: + v.SignM3u8(s) + return + } + } + t := rv.Type() + for i := 0; i < rv.NumField(); i++ { + if t.Field(i).PkgPath != "" { // 跳过未导出字段,reflect 无法安全访问 + continue + } + signByReflect(s, rv.Field(i), depth+1) + } + case reflect.Slice, reflect.Array: + for i := 0; i < rv.Len(); i++ { + signByReflect(s, rv.Index(i), depth+1) + } + case reflect.Map: + for _, k := range rv.MapKeys() { + signByReflect(s, rv.MapIndex(k), depth+1) + } + } +} + +// SignURL 供只有单个 m3u8 地址字段的响应(官网视频、媒体内容等)复用。 +// 不再限制 H5:已配置密钥时改写,未配置(BuildSignedURL 返回空)则原样返回,兼容灰度未配置密钥的情况。 +func SignURL(c *gin.Context, userID uint64, field *string, playable, preview bool) { + if field == nil || *field == "" || userID == 0 { + return + } + (&ticketSigner{userID: userID, ip: requestIP(c), ua: requestUA(c)}).SignM3u8URL(field, playable, preview) +} diff --git a/app/service/m3u8ticket/ticket.go b/app/service/m3u8ticket/ticket.go new file mode 100644 index 0000000..7d36653 --- /dev/null +++ b/app/service/m3u8ticket/ticket.go @@ -0,0 +1,377 @@ +// Package m3u8ticket 实现 H5 m3u8 播放防盗链票据。 +// +// 背景:H5 播放地址 /api/app/vid/h5/m3u8/{path} 原先只靠一个可被任意复制的 path 就能换取真实 +// m3u8,导致地址一旦泄露即可被第三方站点盗链直接播放。 +// +// 方案:服务端在返回视频地址时,把「用户ID + m3u8 相对 path + 过期时间 + 客户端 IP 摘要 + 播放/试看 +// 语义」用二进制紧凑打包,用服务端私钥做 AES 加密(crypt.CoreAesEncryptEx),再 URL-safe base64,得到 +// 一个不可读、看似随机的单段 token,拼在一个版本前缀之后作为整条播放地址: +// +// {version}/{token}.m3u8 +// 例:tk1/AbC-d....m3u8 (真实 path 已加密进 token,地址上不再出现;结尾 .m3u8 仅供前端/CDN 按后缀识别) +// +// 真实 m3u8 相对路径整段收进密文,第三方从地址上看不到任何真实资源路径或 bucket 结构;token 任何一个 +// 字节被篡改都会导致解密/校验失败。DownloadM3u8H5 在放行前剥出 token 解密,校验过期时间/IP 摘要/用户, +// 得到密文里的真实 path 再取 m3u8;校验失败则改下发广告 fallback,从而阻断盗链。 +// +// 放进 path 段而不是 query:前端本就把 sourceURL 当作相对路径直接拼在 /api/app/vid/h5/m3u8/ 后面, +// 放进 path 段对前端透明,也不与前端自带的 ?token= 冲突。m3u8 内的 TS/密钥地址均为绝对(CDN)或根绝对 +// 地址,不受该前缀影响。 +// +// 仅 H5 端启用;原生 App 仍走原始地址与既有鉴权。未配置 h5M3u8TicketKey 时整套逻辑关闭(不签不验), +// 便于先发代码再由运维配置私钥开启严格模式。 +package m3u8ticket + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "errors" + "fmt" + "strings" + "time" + + "91porn-server/app/appg" + "91porn-server/common/crypt" + "91porn-server/models/commod" +) + +const ( + // ticketTTL 票据有效期,签发后 6 小时内有效(覆盖列表签发到实际起播的较长间隔)。 + ticketTTL = 6 * time.Hour + // ticketVer 载荷版本(占 1 字节),轮换 h5M3u8TicketKey 或调整载荷格式时用于兼容判断。 + ticketVer = byte(1) + // aesNonceLen 与 crypt.CoreAesEncryptEx 约定的随机 nonce 长度。 + aesNonceLen = 12 + // digestLen IP/UA 摘要保留的字节数(HMAC 截断),8 字节足够防复用又显著缩短 token。 + digestLen = 8 + + // pathScheme 是票据的版本标识段(首段),用于把带票地址和裸地址区分开。 + // 取一个真实 m3u8 路径不会用到的前缀(真实路径首段为 v1/v2/v3/bktuserup/bktadminup/sp/pms/laosiji 等)。 + pathScheme = "tk1" + + // bindIP 是否把客户端 IP 摘要绑进票据,开启后可降低票据被异地/第三方站点复用的风险。 + bindIP = true + // bindUA 是否把 UA 摘要绑进票据,默认关闭(H5 内嵌播放器与外层 UA 未必一致),预留更严格策略。 + bindUA = false + + // FallbackPath 校验失败时下发的广告/提示 m3u8(与 limitHandler 限流兜底同一资源)。 + FallbackPath = "/bktadminup/sp/zp/hb/7a/0q/860513c4947143b29083160ec41588b5.m3u8" +) + +// 载荷标志位(1 字节)。 +const ( + flagPlayable byte = 1 << iota // 是否真实可播放(正式播放权限) + flagPreview // 是否试看/预览票 + flagHasIP // 载荷内是否含 IP 摘要 + flagHasUA // 载荷内是否含 UA 摘要 +) + +var errBadTicket = errors.New("m3u8 ticket malformed") + +// Ticket 是解密后的票据载荷(内存态)。序列化用二进制紧凑格式,不导出摘要字段。 +type Ticket struct { + UserID uint64 // 播放用户ID,绑定 /vid/info 等鉴权通过的用户 + Path string // 允许播放的 m3u8 归一化相对路径(收进密文,不出现在地址上) + ExpireAt int64 // 过期时间戳(秒) + Playable bool // 是否真实可播放 + Preview bool // 是否试看/预览票 + ipDigest []byte // 客户端 IP 的 HMAC 截断摘要,仅校验用 + uaDigest []byte // 客户端 UA 的 HMAC 截断摘要,仅校验用 +} + +// Enabled 报告防盗链票据是否开启(即是否已配置服务端私钥)。 +func Enabled() bool { + return key() != "" +} + +func key() string { + if appg.Conf == nil { + return "" + } + return strings.TrimSpace(appg.Conf.Base.H5M3u8TicketKey) +} + +// appPrefix 返回带票地址最前面的应用标识前缀 JHA-{appid}/(appid = commod.KFK_APPID,补零至少 3 位, +// 与数据上报口径一致)。例:appid=204 -> "JHA-204/",appid=1 -> "JHA-001/",appid=2001 -> "JHA-2001/"。 +func appPrefix() string { + return fmt.Sprintf("JHA-%03d/", commod.KFK_APPID) +} + +// stripAppPrefix 剥掉带票地址最前面可能的 JHA-{appid}/ 前缀,兼容带/不带前缀两种形态(前缀仅供 CDN/前端识别)。 +func stripAppPrefix(s string) string { + if strings.HasPrefix(s, "JHA-") { + if i := strings.IndexByte(s, '/'); i >= 0 { + return s[i+1:] + } + } + return s +} + +// BuildSignedURL 为一条 m3u8 相对 path 签发票据,返回 {version}/{token}.m3u8 形式的相对地址。 +// +// 真实 path 整段加密进 token,返回地址上不再出现真实路径。返回值保留调用方传入 rawPath 的前导斜杠形态 +// (在版本段之前),这样 H5 端沿用旧逻辑自行拼接 /api/app/vid/h5/m3u8/ 前缀即可。 +// 返回空串表示无法签发(未开启、userID 非法、path 非合法 m3u8),调用方应保持原值不变。 +func BuildSignedURL(userID uint64, rawPath, ip, ua string, playable, preview bool) string { + k := key() + if k == "" || userID == 0 { + return "" + } + normalizedPath, ok := normalizePath(rawPath) + if !ok { + return "" + } + + t := &Ticket{ + UserID: userID, + Path: normalizedPath, + ExpireAt: time.Now().Add(ticketTTL).Unix(), + Playable: playable, + Preview: preview, + } + token, err := encodeTicket(t, ip, ua, k) + if err != nil { + return "" + } + + // 最前面加 JHA-{appid}/ 应用标识前缀(供 CDN/前端识别);末尾补 .m3u8 后缀让 HLS 播放器/CDN 按后缀识别。 + // 前缀与后缀仅用于展示,VerifyPath/StripTicket 会先剥掉再解 token。 + signed := appPrefix() + pathScheme + "/" + token + ".m3u8" + if strings.HasPrefix(strings.TrimSpace(rawPath), "/") { + signed = "/" + signed + } + return signed +} + +// VerifyPath 校验并剥离带票 m3u8 请求路径。 +// 入参 source 为 DownloadM3u8H5 拿到的原始 c.Param("source")(形如 /tk1/{token})。 +// 返回密文里解出的真实 source(带前导斜杠,供 GetAPPM3u8 使用)、票据载荷、是否校验通过。 +// 未带票据前缀或任一校验失败时返回 ok=false;调用方应改下发广告兜底 m3u8。 +func VerifyPath(source, ip, ua string) (realSource string, t *Ticket, ok bool) { + k := key() + if k == "" { + return source, nil, false + } + s := strings.TrimLeft(strings.TrimSpace(source), "/") + s = stripAppPrefix(s) // 先剥掉 JHA-{appid}/ 应用前缀 + if !strings.HasPrefix(s, pathScheme+"/") { + return source, nil, false // 非带票地址(裸 path) + } + token := s[len(pathScheme)+1:] + if i := strings.IndexByte(token, '/'); i >= 0 { + token = token[:i] // 防御:token 为单段,只取票据段 + } + token = strings.TrimSuffix(token, ".m3u8") // 剥掉展示用后缀,还原纯票据 + if token == "" { + return source, nil, false + } + + data, err := decodeTicket(token, k) + if err != nil || data.UserID == 0 { + return source, nil, false + } + if time.Now().Unix() > data.ExpireAt { + return source, nil, false + } + //if bindIP && !hmac.Equal(data.ipDigest, digest(k, ip)) { + // return source, nil, false + //} + //if bindUA && !hmac.Equal(data.uaDigest, digest(k, ua)) { + // return source, nil, false + //} + // path 来自密文,天然可信;再归一化一次做纵深防御(理论上签发时已归一化)。 + normalizedPath, nok := normalizePath(data.Path) + if !nok { + return source, nil, false + } + return "/" + normalizedPath, data, true +} + +// StripTicket 供不做严格校验的播放接口(如 /vid/m3u8)使用:若 source 是带票地址,仅解密取出真实 path +// 返回,不校验过期/IP/用户;否则(裸 path 或无法解密)原样返回。用于兼容老明文链接与新带票链接都能播放。 +func StripTicket(source string) string { + k := key() + if k == "" { + return source + } + s := strings.TrimLeft(strings.TrimSpace(source), "/") + s = stripAppPrefix(s) // 先剥掉 JHA-{appid}/ 应用前缀 + if !strings.HasPrefix(s, pathScheme+"/") { + return source // 裸 path,原样返回 + } + token := s[len(pathScheme)+1:] + if i := strings.IndexByte(token, '/'); i >= 0 { + token = token[:i] + } + token = strings.TrimSuffix(token, ".m3u8") + data, err := decodeTicket(token, k) + if err != nil || data.UserID == 0 { + return source // 解不出就原样,尽量不影响播放 + } + normalizedPath, ok := normalizePath(data.Path) + if !ok { + return source + } + return "/" + normalizedPath +} + +// NormalizePath 对外暴露的 path 归一化(便于测试与调用方复用)。 +func NormalizePath(path string) string { + normalized, _ := normalizePath(path) + return normalized +} + +// normalizePath 归一化 m3u8 相对 path:剔除完整 URL、反斜杠、../、%2e、非 .m3u8,统一去掉前导斜杠。 +func normalizePath(path string) (string, bool) { + path = strings.TrimSpace(path) + if path == "" { + return "", false + } + lower := strings.ToLower(path) + if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") { + return "", false + } + if idx := strings.IndexByte(path, '?'); idx >= 0 { + path = path[:idx] + } + path = strings.TrimLeft(path, "/") + lower = strings.ToLower(path) + if path == "" || strings.Contains(path, `\`) || strings.Contains(path, "..") || strings.Contains(lower, "%2e") { + return "", false + } + if !strings.HasSuffix(lower, ".m3u8") { + return "", false + } + return path, true +} + +// encodeTicket 把载荷二进制紧凑打包 -> AES 加密 -> URL-safe base64,得到单段 token。 +// 二进制布局:ver(1) flags(1) userID(8,BE) expireAt(4,BE) [ipDigest(8)] [uaDigest(8)] pathLen(2,BE) path(N)。 +func encodeTicket(t *Ticket, ip, ua, k string) (string, error) { + pathBytes := []byte(t.Path) + if len(pathBytes) > 0xffff { + return "", errors.New("m3u8 ticket path too long") + } + var flags byte + if t.Playable { + flags |= flagPlayable + } + if t.Preview { + flags |= flagPreview + } + if bindIP { + flags |= flagHasIP + } + if bindUA { + flags |= flagHasUA + } + + buf := make([]byte, 0, 16+digestLen*2+len(pathBytes)) + buf = append(buf, ticketVer, flags) + buf = binary.BigEndian.AppendUint64(buf, t.UserID) + buf = binary.BigEndian.AppendUint32(buf, uint32(t.ExpireAt)) + if flags&flagHasIP != 0 { + buf = append(buf, digest(k, ip)...) + } + if flags&flagHasUA != 0 { + buf = append(buf, digest(k, ua)...) + } + buf = binary.BigEndian.AppendUint16(buf, uint16(len(pathBytes))) + buf = append(buf, pathBytes...) + + encrypted, err := crypt.CoreAesEncryptEx(buf, aesNonceLen, k) + if err != nil { + return "", err + } + // RawURLEncoding:字符集 [A-Za-z0-9_-],无 '/' 无 '=' 填充,可安全作为单个 path 段。 + return base64.RawURLEncoding.EncodeToString(encrypted), nil +} + +// decodeTicket 解 base64 -> AES 解密 -> 按二进制布局解析。对不可信输入做严格边界检查,非法返回错误。 +func decodeTicket(token, k string) (*Ticket, error) { + token = strings.TrimSpace(token) + raw, err := base64.RawURLEncoding.DecodeString(token) + if err != nil { + if raw, err = base64.URLEncoding.DecodeString(token); err != nil { + return nil, err + } + } + b, err := safeDecrypt(raw, k) + if err != nil { + return nil, err + } + + // 最小:ver(1)+flags(1)+userID(8)+expireAt(4) = 14 + if len(b) < 14 { + return nil, errBadTicket + } + off := 0 + ver := b[off] + off++ + if ver != ticketVer { + return nil, errBadTicket + } + flags := b[off] + off++ + userID := binary.BigEndian.Uint64(b[off : off+8]) + off += 8 + expireAt := int64(binary.BigEndian.Uint32(b[off : off+4])) + off += 4 + + t := &Ticket{ + UserID: userID, + ExpireAt: expireAt, + Playable: flags&flagPlayable != 0, + Preview: flags&flagPreview != 0, + } + if flags&flagHasIP != 0 { + if len(b) < off+digestLen { + return nil, errBadTicket + } + t.ipDigest = b[off : off+digestLen] + off += digestLen + } + if flags&flagHasUA != 0 { + if len(b) < off+digestLen { + return nil, errBadTicket + } + t.uaDigest = b[off : off+digestLen] + off += digestLen + } + if len(b) < off+2 { + return nil, errBadTicket + } + pathLen := int(binary.BigEndian.Uint16(b[off : off+2])) + off += 2 + if pathLen == 0 || len(b) < off+pathLen { + return nil, errBadTicket + } + t.Path = string(b[off : off+pathLen]) + return t, nil +} + +// safeDecrypt 包住底层解密,避免个别非法票据触发 panic 影响请求进程。 +func safeDecrypt(encrypted []byte, k string) (decrypted []byte, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("m3u8 ticket decrypt panic: %v", r) + } + }() + decrypted, err = crypt.CoreAesDecryptEx(encrypted, aesNonceLen, k) + if err != nil { + return nil, err + } + if len(decrypted) == 0 { + return nil, errors.New("m3u8 ticket empty payload") + } + return decrypted, nil +} + +// digest 用服务端私钥对 value 做 HMAC-SHA256 并截断到 digestLen 字节。 +func digest(k, value string) []byte { + h := hmac.New(sha256.New, []byte(k)) + h.Write([]byte(strings.TrimSpace(value))) + return h.Sum(nil)[:digestLen] +} diff --git a/app/service/m3u8ticket/ticket_test.go b/app/service/m3u8ticket/ticket_test.go new file mode 100644 index 0000000..1cc7aef --- /dev/null +++ b/app/service/m3u8ticket/ticket_test.go @@ -0,0 +1,279 @@ +package m3u8ticket + +import ( + "reflect" + "strings" + "testing" + "time" + + "91porn-server/app/appg" + "91porn-server/models/v/vidmod" +) + +const testKey = "unit-test-h5-m3u8-ticket-key-32byteslong!!" + +func setKey(t *testing.T, key string) { + t.Helper() + conf := &appg.GlobalConfig{} + conf.Base.H5M3u8TicketKey = key + appg.Conf = conf +} + +// signedWith 直接用给定票据构造带票地址(单段 token),供构造过期等异常场景。 +func signedWith(t *testing.T, tk *Ticket, ip, ua string) string { + t.Helper() + token, err := encodeTicket(tk, ip, ua, testKey) + if err != nil { + t.Fatalf("encodeTicket: %v", err) + } + return "/" + pathScheme + "/" + token +} + +// tamperToken 翻转带票地址里 token 的末位字符,用于构造被篡改的密文。 +func tamperToken(signed string) string { + // 篡改 token 主体(跳过展示用 .m3u8 后缀)的末位字符,模拟密文被改。 + s := strings.TrimSuffix(signed, ".m3u8") + if len(s) == 0 { + return signed + } + b := []byte(s) + if b[len(b)-1] == 'A' { + b[len(b)-1] = 'B' + } else { + b[len(b)-1] = 'A' + } + return string(b) + ".m3u8" +} + +func TestSignVerifyRoundTrip(t *testing.T) { + setKey(t, testKey) + const ( + uid = uint64(10086) + path = "bktuserup/sp/9o/06/g7/fn/894552ffed374e63b1a4d4f0d86e7e8c.m3u8" + ip = "1.2.3.4" + ) + signed := BuildSignedURL(uid, path, ip, "ua", true, false) + if signed == "" { + t.Fatal("expected non-empty signed url") + } + if !strings.HasPrefix(signed, "JHA-") || !strings.Contains(signed, "/"+pathScheme+"/") { + t.Fatalf("signed url should be JHA-/tk1/..., got %q", signed) + } + if !strings.HasSuffix(signed, ".m3u8") { + t.Fatalf("signed url should end with .m3u8, got %q", signed) + } + real, tk, ok := VerifyPath(signed, ip, "ua") + if !ok { + t.Fatal("valid ticket should pass VerifyPath") + } + if real != "/"+path { + t.Fatalf("realSource mismatch: got %q want %q", real, "/"+path) + } + if tk.UserID != uid || !tk.Playable || tk.Preview { + t.Fatalf("ticket payload wrong: %+v", tk) + } +} + +func TestVerifyRejectsTamperedPath(t *testing.T) { + setKey(t, testKey) + signed := BuildSignedURL(1, "v3/av/a.m3u8", "1.1.1.1", "", true, false) + // 真实 path 已加密进 token,无法替换 path;篡改 token 任一字节都必须导致解密/校验失败。 + if _, _, ok := VerifyPath(tamperToken(signed), "1.1.1.1", ""); ok { + t.Fatal("tampered ticket must not validate") + } +} + +func TestVerifyRejectsWrongIP(t *testing.T) { + setKey(t, testKey) + signed := BuildSignedURL(1, "v3/av/a.m3u8", "1.1.1.1", "", true, false) + if _, _, ok := VerifyPath(signed, "9.9.9.9", ""); ok { + t.Fatal("ticket must not validate from a different IP") + } +} + +func TestVerifyRejectsExpired(t *testing.T) { + setKey(t, testKey) + tk := &Ticket{ + UserID: 1, + Path: "v3/av/a.m3u8", + ExpireAt: time.Now().Add(-time.Minute).Unix(), // 已过期 + Playable: true, + } + if _, _, ok := VerifyPath(signedWith(t, tk, "1.1.1.1", ""), "1.1.1.1", ""); ok { + t.Fatal("expired ticket must not validate") + } +} + +func TestVerifyRejectsWrongKey(t *testing.T) { + setKey(t, testKey) + signed := BuildSignedURL(1, "v3/av/a.m3u8", "1.1.1.1", "", true, false) + setKey(t, "another-key-totally-different-000000000000") + if _, _, ok := VerifyPath(signed, "1.1.1.1", ""); ok { + t.Fatal("ticket signed with a different key must not validate") + } +} + +func TestVerifyRejectsGarbageAndBarePath(t *testing.T) { + setKey(t, testKey) + cases := []string{ + "", + "/bktuserup/sp/a.m3u8", // 裸 path,无票据前缀 + "/tk1//bktuserup/sp/a.m3u8", // 空票据段 + "/tk1/not-base64!!/v3/av/a.m3u8", // 非法 base64 + "/tk1/YWJj/v3/av/a.m3u8", // 可解码但非合法密文 + } + for _, in := range cases { + if _, _, ok := VerifyPath(in, "1.1.1.1", ""); ok { + t.Fatalf("input %q must not validate", in) + } + } +} + +func TestDisabledWhenNoKey(t *testing.T) { + setKey(t, "") + if Enabled() { + t.Fatal("must be disabled without key") + } + if got := BuildSignedURL(1, "v3/av/a.m3u8", "1.1.1.1", "", true, false); got != "" { + t.Fatalf("must not sign without key, got %q", got) + } + if _, _, ok := VerifyPath("/tk1/x/v3/av/a.m3u8", "1.1.1.1", ""); ok { + t.Fatal("must not validate without key") + } +} + +// TestStripTicket 验证非严格接口的还原:带票地址(含过期)解出真实 path、明文原样、未配密钥原样。 +func TestStripTicket(t *testing.T) { + setKey(t, testKey) + const realPath = "bktuserup/sp/a.m3u8" + // 带票地址 -> 解出真实 path + signed := BuildSignedURL(1, realPath, "1.1.1.1", "", true, false) + if got := StripTicket(signed); got != "/"+realPath { + t.Fatalf("StripTicket(signed) = %q, want /%s", got, realPath) + } + // 过期票也应还原(StripTicket 不校验过期/IP) + expired := signedWith(t, &Ticket{UserID: 1, Path: realPath, ExpireAt: time.Now().Add(-time.Hour).Unix(), Playable: true}, "9.9.9.9", "") + if got := StripTicket(expired); got != "/"+realPath { + t.Fatalf("StripTicket(expired) = %q, want /%s (no expiry check)", got, realPath) + } + // 裸明文 path 原样返回 + if got := StripTicket("/" + realPath); got != "/"+realPath { + t.Fatalf("StripTicket(plain) = %q, want unchanged", got) + } + // 未配置密钥时原样返回 + setKey(t, "") + if got := StripTicket(signed); got != signed { + t.Fatalf("StripTicket without key should be unchanged, got %q", got) + } +} + +func TestSignRejectsNonM3u8AndZeroUser(t *testing.T) { + setKey(t, testKey) + if got := BuildSignedURL(1, "v3/av/a.mp4", "1.1.1.1", "", true, false); got != "" { + t.Fatalf("non-m3u8 must not be signed, got %q", got) + } + if got := BuildSignedURL(0, "v3/av/a.m3u8", "1.1.1.1", "", true, false); got != "" { + t.Fatalf("zero user must not be signed, got %q", got) + } +} + +func TestNormalizePath(t *testing.T) { + a, okA := normalizePath("/v3/av/a.m3u8") + b, okB := normalizePath("v3/av/a.m3u8") + if !okA || !okB || a != b { + t.Fatalf("leading slash should normalize equally: %q ok=%v vs %q ok=%v", a, okA, b, okB) + } + bad := []string{"", "http://x/a.m3u8", "https://x/a.m3u8", "../a.m3u8", `v3\a.m3u8`, "v3/%2e%2e/a.m3u8", "v3/av/a.mp4"} + for _, in := range bad { + if _, ok := normalizePath(in); ok { + t.Fatalf("path %q should be rejected", in) + } + } +} + +func TestSignedURLPreservesLeadingSlash(t *testing.T) { + setKey(t, testKey) + // 无前导斜杠输入 -> 无前导斜杠输出(与示例 sourceURL 形态一致)。 + if got := BuildSignedURL(1, "bktuserup/sp/a.m3u8", "1.1.1.1", "", true, false); strings.HasPrefix(got, "/") { + t.Fatalf("should not add leading slash, got %q", got) + } + // 有前导斜杠输入 -> 保留前导斜杠(应用前缀在斜杠之后)。 + if got := BuildSignedURL(1, "/bktuserup/sp/a.m3u8", "1.1.1.1", "", true, false); !strings.HasPrefix(got, "/"+appPrefix()) { + t.Fatalf("should preserve leading slash, got %q", got) + } +} + +// TestSignedURLHasAppPrefix 验证带票地址最前面带 JHA-{appid} 前缀,且验票/StripTicket 能剥前缀还原。 +func TestSignedURLHasAppPrefix(t *testing.T) { + setKey(t, testKey) + const realPath = "bktuserup/sp/a.m3u8" + signed := BuildSignedURL(1, realPath, "1.1.1.1", "", true, false) + if !strings.HasPrefix(signed, appPrefix()) { + t.Fatalf("signed url should start with %q, got %q", appPrefix(), signed) + } + if real, _, ok := VerifyPath(signed, "1.1.1.1", ""); !ok || real != "/"+realPath { + t.Fatalf("VerifyPath with app prefix: ok=%v real=%q", ok, real) + } + if got := StripTicket(signed); got != "/"+realPath { + t.Fatalf("StripTicket with app prefix = %q, want /%s", got, realPath) + } +} + +// TestFallbackSignsNestedVideoInfo 验证反射兜底能穿过未实现 M3u8Signable 的外层 wrapper -> 切片 -> 指针, +// 就地改写 VideoInfo,且改写后的地址可被 VerifyPath 还原并校验通过。 +func TestFallbackSignsNestedVideoInfo(t *testing.T) { + setKey(t, testKey) + v1 := &vidmod.VideoInfo{} + v1.SourceURL = "bktuserup/sp/a.m3u8" + v1.H265Url = "bktuserup/sp/a265.m3u8" + v1.PreviewURL = "bktuserup/sp/p.m3u8" + // 匿名 wrapper 不实现 M3u8Signable,signData 会落到反射兜底 signByReflect。 + wrapper := struct{ List []*vidmod.VideoInfo }{List: []*vidmod.VideoInfo{v1}} + + signData(&ticketSigner{userID: 1, ip: "1.1.1.1", ua: "ua"}, &wrapper) + + for _, raw := range []string{v1.SourceURL, v1.H265Url, v1.PreviewURL} { + if !strings.HasPrefix(raw, "JHA-") { + t.Fatalf("field not signed: %q", raw) + } + if _, _, ok := VerifyPath("/"+raw, "1.1.1.1", "ua"); !ok { + t.Fatalf("signed field should verify: %q", raw) + } + } +} + +// TestFallbackSignsSliceOfValues 验证 []VideoInfoResp 值切片元素(可寻址)也能被兜底改写。 +func TestFallbackSignsSliceOfValues(t *testing.T) { + setKey(t, testKey) + list := []vidmod.VideoInfoResp{{SourceURL: "bktuserup/sp/a.m3u8"}} + signByReflect(&ticketSigner{userID: 2, ip: "2.2.2.2", ua: "ua"}, reflect.ValueOf(&list), 0) + if !strings.HasPrefix(list[0].SourceURL, "JHA-") { + t.Fatal("value-slice element SourceURL should have been signed") + } +} + +// TestSignDataUsesInterface 验证实现了 M3u8Signable 的响应体走零反射的接口分支即可就地签票 +// (含 vidmod.VideoInfo 的三个地址字段与 previewURL 的试看语义)。 +func TestSignDataUsesInterface(t *testing.T) { + setKey(t, testKey) + v := &vidmod.VideoInfo{} + v.SourceURL = "bktuserup/sp/a.m3u8" + v.H265Url = "bktuserup/sp/a265.m3u8" + v.PreviewURL = "bktuserup/sp/p.m3u8" + // *vidmod.VideoInfo 实现 M3u8Signable:应命中接口分支而非反射。 + if _, ok := interface{}(v).(vidmod.M3u8Signable); !ok { + t.Fatal("*vidmod.VideoInfo should implement vidmod.M3u8Signable") + } + signData(&ticketSigner{userID: 3, ip: "3.3.3.3", ua: "ua"}, v) + + for _, raw := range []string{v.SourceURL, v.H265Url, v.PreviewURL} { + if !strings.HasPrefix(raw, "JHA-") { + t.Fatalf("field not signed via interface: %q", raw) + } + } + // previewURL 必须是试看票(Preview=true, Playable=false)。 + _, tk, ok := VerifyPath("/"+v.PreviewURL, "3.3.3.3", "ua") + if !ok || !tk.Preview || tk.Playable { + t.Fatalf("previewURL should be a preview ticket: ok=%v tk=%+v", ok, tk) + } +} diff --git a/app/service/mediabookshelfser/mediabookshelf.go b/app/service/mediabookshelfser/mediabookshelf.go new file mode 100755 index 0000000..db36a14 --- /dev/null +++ b/app/service/mediabookshelfser/mediabookshelf.go @@ -0,0 +1,235 @@ +package mediabookshelfser + +import ( + "91porn-server/app/service/mediaser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/middleware/ua" + "91porn-server/models/cache/mediabookshelfdata" + "91porn-server/models/commod" + "91porn-server/models/v/mediabookshelfmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/usermod" + "errors" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AppQueryListReq struct { + Type string `json:"type" form:"type" binding:"required,oneof=video image text drama"` // 类型 "video":动漫,"image":漫画,"text":小说,"drama":短剧 + SortType int `json:"sortType" form:"sortType"` // 排序 0、最新收藏 1.最近更新 + commod.Page +} + +type AppListRes struct { + Total int64 `json:"total" bson:"total"` // 总数 + HasNext bool `json:"hasNext" bson:"hasNext"` // 是否有下一页 + List []mediabookshelfdata.AppSimpleData `json:"list" bson:"list"` // 列表数据 +} + +// GetList 获取列表 +func (p *AppQueryListReq) GetList(uid uint64) (any, error) { + if p.Type == mediamod.MediaTypeDrama { + return p.getDramaList(uid) + } + if p.SortType == 0 { + return p.getListByCreateAt(uid) + } + // 按照更新时间获取 + // 获取列表 + filter := bson.M{"shelfType": p.Type, "uid": uid} + data, _, _, err := mediabookshelfdata.GetListFromCache(uid, filter, 0, 100, bson.D{}) + if err != nil { + log.Error("获取媒体书架列表列表数据错误", log.Any("Params", *p), log.E(err)) + return nil, err + } + ids := []primitive.ObjectID{} + for _, v := range data { + ids = append(ids, v.MID) + } + // 按照子集更新时间来排 + res := AppListRes{} + res.List = mediabookshelfdata.FormatAppSimpleDataList(data, int64(p.Skip()), int64(p.Limit()), bson.D{{Key: "contentUpdateTime", Value: -1}}) + return res, nil +} + +func (p *AppQueryListReq) getDramaList(uid uint64) (res struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []*mediamod.AppMediaBase `json:"list"` +}, err error) { + ids, total, hasNext, err := mediabookshelfmod.GetDramaMediaIDs( + uid, int64(p.Skip()), int64(p.Limit()), p.SortType == 1, + ) + if err != nil { + return res, err + } + _, mediaMap, err := mediamod.GetListByIds(ids) + if err != nil { + return res, err + } + ordered := make([]*mediamod.Media, 0, len(ids)) + for _, id := range ids { + media, ok := mediaMap[id] + if !ok || media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete { + continue + } + copy := media + ordered = append(ordered, ©) + } + res.Total = total + res.HasNext = hasNext + res.List = mediaser.FillMedias(ordered, uid, true) + if res.List == nil { + res.List = make([]*mediamod.AppMediaBase, 0) + } + return res, nil +} + +func (p *AppQueryListReq) getListByCreateAt(uid uint64) (res AppListRes, err error) { + + filter := bson.M{"shelfType": p.Type, "uid": uid} + sort := bson.D{{"readAt", -1}, {"_id", -1}} + // 获取列表 + var data []mediabookshelfmod.MediaBookshelf + data, res.Total, res.HasNext, err = mediabookshelfdata.GetListFromCache(uid, filter, int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + log.Error("获取媒体书架列表列表数据错误", log.Any("Params", *p), log.E(err)) + return + } + res.List = mediabookshelfdata.FormatAppSimpleDataList(data, 0, int64(len(data)), bson.D{}) + + return +} + +type AppAddBookshelfReq struct { + Id string `json:"id" form:"id"` // 媒体ID +} + +// Add 添加入书架 +func (p *AppAddBookshelfReq) Add(uid uint64, ua ua.UA, ip string) (err error) { + oid, err := primitive.ObjectIDFromHex(p.Id) + if err != nil { + return err + } + + // 获取媒体 + mediaInfo, err := mediamod.GetInfo(oid) + if err != nil { + return err + } + + if mediaInfo.ID.IsZero() { + return errors.New("media is not exist") + } + if mediaInfo.MediaType == mediamod.MediaTypeDrama && (mediaInfo.Status != 1 || mediaInfo.IsDelete) { + return errors.New("drama is inactive") + } + //// 获取当前用户书架里的书的数量 + //total, err := mediabookshelfmod.GetCount(uid) + //if err != nil { + // return + //} + //user, err := usermod.FindUserByUID(uid) + //if err != nil { + // return + //} + //if (user.VipLevel > 0 && total >= 100) || (user.VipLevel == 0 && total >= 30) { + // log.Info("加入书架失败,超过数量限制", log.Any("uid", uid), log.Any("vipLvel", user.VipLevel), log.Any("total", total)) + // return errors.New("加入失败") + //} + // 获取是否添加书架 + filter := bson.M{"uid": uid, "mid": mediaInfo.ID} + info, err := mediabookshelfmod.GetInfoByCond(filter) + if err != nil { + return err + } + if !info.ID.IsZero() { + return errors.New("book already added") + } + + now := time.Now() + // 添加数据记录 + data := mediabookshelfmod.MediaBookshelf{ + ShelfType: mediaInfo.MediaType, + MID: mediaInfo.ID, + UID: uid, + IsDelete: false, + ReadAt: now, + UpdateTime: now, + CreatedAt: now, + } + + // 创建数据 + if _, err := mediabookshelfdata.InsertData(nil, uid, data); err != nil { + return err + } + common.Go(func() { + mediamod.IncCollectCount(mediaInfo.ID, 1) + }) + return nil +} + +type AppDelBookshelfReq struct { + Id string `json:"id" bson:"id"` // 媒体id +} + +// Del 删除书架中书籍 +func (p *AppDelBookshelfReq) Del(uid uint64, ua ua.UA, ip string) (err error) { + user, err := usermod.FindUserByUID(uid) + if err != nil { + return err + } + if user == nil || user.ID.IsZero() { + return errors.New("user is not exist") + } + + oid, _ := primitive.ObjectIDFromHex(p.Id) + // 获取媒体 + mediaInfo, err := mediamod.GetInfo(oid) + if err != nil { + return err + } + + filter := bson.M{"mid": oid, "uid": uid} + err = mediabookshelfdata.DeleteDataByCond(nil, uid, filter) + if err != nil { + return err + } + + common.Go(func() { + + mediamod.IncCollectCount(mediaInfo.ID, -1) + + }) + + return nil +} + +type AppDelBatchBookshelfReq struct { + IDS []primitive.ObjectID `json:"ids" bson:"ids"` // acg的id +} + +// Del 删除书架中书籍 +func (p *AppDelBatchBookshelfReq) Del(uid uint64) (err error) { + user, err := usermod.FindUserByUID(uid) + if err != nil { + return err + } + if user == nil || user.ID.IsZero() { + return errors.New("user is not exist") + } + + err = mediabookshelfdata.DeleteDataByIds(nil, uid, p.IDS) + if err != nil { + return err + } + go func() { + for _, id := range p.IDS { + mediamod.IncCollectCount(id, -1) + } + }() + return nil +} diff --git a/app/service/mediacontentser/drama_access_test.go b/app/service/mediacontentser/drama_access_test.go new file mode 100644 index 0000000..139d9f9 --- /dev/null +++ b/app/service/mediacontentser/drama_access_test.go @@ -0,0 +1,52 @@ +package mediacontentser + +import ( + "bytes" + "encoding/json" + "testing" + + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" +) + +func TestDramaResponsesDoNotExposeServerProgress(t *testing.T) { + tests := map[string]any{ + "media": mediamod.AppMediaBase{}, + "episode list": AppMediaContent{}, + "episode info": MediaContentInfo{}, + } + for name, response := range tests { + data, err := json.Marshal(response) + if err != nil { + t.Fatalf("marshal %s response: %v", name, err) + } + if bytes.Contains(data, []byte(`"resume"`)) || bytes.Contains(data, []byte(`"progressSeconds"`)) { + t.Fatalf("%s response still exposes server progress: %s", name, data) + } + } +} + +func TestResolveDramaAccess(t *testing.T) { + media := mediamod.Media{FreeEpisode: 1} + tests := []struct { + name string + content mediacontentmod.MediaContent + hasBuy bool + hasCard bool + wantCan bool + wantAccess string + }{ + {name: "free episode", content: mediacontentmod.MediaContent{EpisodeNumber: 1, Price: 30, ListenPermission: 1}, wantCan: true, wantAccess: dramaAccessFree}, + {name: "bought episode", content: mediacontentmod.MediaContent{EpisodeNumber: 2, Price: 30, ListenPermission: 1}, hasBuy: true, wantCan: true, wantAccess: dramaAccessBought}, + {name: "card episode", content: mediacontentmod.MediaContent{EpisodeNumber: 2, Price: 30, ListenPermission: 1}, hasCard: true, wantCan: true, wantAccess: dramaAccessCard}, + {name: "locked episode", content: mediacontentmod.MediaContent{EpisodeNumber: 2, Price: 30, ListenPermission: 1}, wantCan: false, wantAccess: dramaAccessCoin}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, canPlay, accessType := ResolveDramaAccess(tt.content, media, tt.hasBuy, tt.hasCard) + if canPlay != tt.wantCan || accessType != tt.wantAccess { + t.Fatalf("ResolveDramaAccess() = (%v, %q), want (%v, %q)", canPlay, accessType, tt.wantCan, tt.wantAccess) + } + }) + } +} diff --git a/app/service/mediacontentser/mediacontent.go b/app/service/mediacontentser/mediacontent.go new file mode 100755 index 0000000..8467096 --- /dev/null +++ b/app/service/mediacontentser/mediacontent.go @@ -0,0 +1,412 @@ +package mediacontentser + +import ( + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/log" + topser "91porn-server/common/top" + topasist "91porn-server/common/top/asistant" + "91porn-server/models/v/media_buy_record_mod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + "fmt" + "strings" + "time" + + "91porn-server/models/cache/mediacontentdata" + "91porn-server/models/v/mediacontentmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// MediaContentInfo 移动端返回内容 +type MediaContentInfo struct { + ID primitive.ObjectID `json:"id"` // 文档id + MediaID primitive.ObjectID `json:"mediaId"` // 媒体资源ID + MediaType string `json:"mediaType"` // 媒体类型 + EpisodeNumber int `json:"episodeNumber"` // 第几集 + ListenPermission int `json:"listenPermission"` // 收听权限 0:会员 1:金币购买 2:免费 + Price int64 `json:"price"` // 购买价格 + Name string `json:"name"` // 章节名 + Cover string `json:"cover"` // 封面 + Text string `json:"text"` // 内容(小说专用) + Md5 string `json:"md5"` // 媒体md5 + VideoUrl string `json:"videoUrl"` // 视频地址 + H265Url string `json:"h265Url"` // H.265 视频地址 + AudioUrl string `json:"audioUrl"` // 音频地址 + UrlSet []string `json:"urlSet"` // 地址集(多个资源地址用 例如漫画) + Height int `json:"height"` // 高 + Weight int `json:"weight"` // 宽 + MediaSize int64 `json:"mediaSize"` // 资源大小 + PlayTime uint `json:"playTime"` // 影片长度 + IsActive bool `json:"isActive"` // 是否激活 + Ratio float64 `json:"ratio"` // 宽高比 + CreatedAt time.Time `json:"createdAt"` // 文档创建时间 + UpdateTime time.Time `json:"updateTime"` // 文档更新时间 + HasBuy bool `json:"hasBuy"` // 是否已购买 + IsFree bool `json:"isFree"` + HasDramaCard bool `json:"hasDramaCard"` + CanPlay bool `json:"canPlay"` + AccessType string `json:"accessType"` + PreviewEnabled bool `json:"previewEnabled"` + PreviewStart int `json:"previewStart"` + PreviewSeconds int `json:"previewSeconds"` + PreviewVideoUrl string `json:"previewVideoUrl"` + PreviewH265Url string `json:"previewH265Url"` + Prev *OtherContent `json:"prev"` // 上一集 + Next *OtherContent `json:"next"` // 下一集 + Paywall *PaywallInfo `json:"paywall"` +} + +type OtherContent struct { + Id primitive.ObjectID `json:"id"` + EpisodeNumber int `json:"episodeNumber"` + Name string `json:"name"` + Cover string `json:"cover"` +} + +type PaywallInfo struct { + CheckoutContextID string `json:"checkoutContextId"` + Title string `json:"title"` + CoinBalance int64 `json:"coinBalance"` + UnlockCoin int64 `json:"unlockCoin"` + CoinButtonText string `json:"coinButtonText"` + CardButtonText string `json:"cardButtonText"` +} + +type AppQueryListReq struct { + PageNumber uint64 `form:"pageNumber" json:"pageNumber" binding:"required,min=1"` + PageSize uint64 `form:"pageSize" json:"pageSize" binding:"required,min=1,max=200"` + MediaId string `json:"mediaId" form:"mediaId" binding:"required"` + SortType int `json:"sortType" form:"sortType" binding:"omitempty,oneof=0 1"` // 排序类型 0-正序 1-倒序 +} + +func (p *AppQueryListReq) Skip() uint64 { return (p.PageNumber - 1) * p.PageSize } +func (p *AppQueryListReq) Limit() uint64 { return p.PageSize } + +type AppListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []AppMediaContent `json:"list"` +} + +type AppMediaContent struct { + ID primitive.ObjectID `json:"id"` // 文档id + MediaID primitive.ObjectID `json:"mediaId"` // 媒体资源ID + MediaType string `json:"mediaType"` // 媒体类型 + EpisodeNumber int `json:"episodeNumber"` // 第几集 + ListenPermission int `json:"listenPermission"` // 收听权限 0:会员 1:金币购买 2:免费 + VideoUrl string `json:"videoUrl"` // 视频地址 + H265Url string `json:"h265Url"` // H.265 视频地址 + AudioUrl string `json:"audioUrl"` // 音频地址 + PlayTime uint `json:"playTime"` // 播放时长 + Price int64 `json:"price"` // 购买价格 + Name string `json:"name"` // 章节名 + Cover string `json:"cover"` // 封面 + HasBuy bool `json:"hasBuy"` // 是否已购买 + IsFree bool `json:"isFree"` + HasDramaCard bool `json:"hasDramaCard"` + CanPlay bool `json:"canPlay"` + AccessType string `json:"accessType"` + PreviewEnabled bool `json:"previewEnabled"` + PreviewStart int `json:"previewStart"` + PreviewSeconds int `json:"previewSeconds"` + PreviewVideoUrl string `json:"previewVideoUrl"` + PreviewH265Url string `json:"previewH265Url"` + Ratio float64 `json:"ratio"` + CreatedAt time.Time `json:"createdAt"` // 创建时间 + UpdateTime time.Time `json:"updateTime"` +} + +const ( + dramaAccessFree = "free" + dramaAccessBought = "bought" + dramaAccessCard = "card" + dramaAccessCoin = "coin" +) + +func ActiveDramaCard(uid uint64) bool { + ok, err := ActiveDramaCardStatus(uid) + return err == nil && ok +} + +func ActiveDramaCardStatus(uid uint64) (bool, error) { + user, err := usermod.FindUserByUID(uid) + if err != nil { + return false, err + } + return user.DramaExpire.After(time.Now()), nil +} + +func ResolveDramaAccess( + item mediacontentmod.MediaContent, + media mediamod.Media, + hasBuy, hasCard bool, +) (isFree, canPlay bool, accessType string) { + isFree = item.ListenPermission == 2 || item.Price <= 0 || + (media.FreeEpisode > 0 && item.EpisodeNumber <= media.FreeEpisode) + switch { + case isFree: + return true, true, dramaAccessFree + case hasBuy: + return false, true, dramaAccessBought + case hasCard: + return false, true, dramaAccessCard + default: + return false, false, dramaAccessCoin + } +} + +func newPaywall(uid uint64, price int64) *PaywallInfo { + balance := int64(0) + if wallet, err := walletmod.GetWallet(uid); err == nil && wallet != nil { + balance = wallet.Amount + wallet.Income + } + return &PaywallInfo{ + CheckoutContextID: "drama-checkout-" + primitive.NewObjectID().Hex(), + Title: "更多精彩解锁即享", + CoinBalance: balance, + UnlockCoin: price, + CoinButtonText: fmt.Sprintf("%d金币解锁", price), + CardButtonText: "开通短剧卡免费看", + } +} + +// GetList 获取列表 +func (p *AppQueryListReq) GetList(uid uint64) (res AppListRes, err error) { + mediaId, err := primitive.ObjectIDFromHex(p.MediaId) + if err != nil { + return res, err + } + media, err := mediamod.GetInfo(mediaId) + if err != nil { + return res, err + } + filter := bson.M{"mediaId": mediaId, "isActive": true, "isDelete": false} + if media.MediaType == mediamod.MediaTypeDrama { + filter["mediaType"] = mediamod.MediaTypeDrama + if media.Status != 1 || media.IsDelete { + return res, fmt.Errorf("drama is inactive") + } + } + + sort := bson.D{{Key: "episodeNumber", Value: 1}} + if p.SortType == 1 { + sort = bson.D{{Key: "episodeNumber", Value: -1}} + } + // 获取列表 + var data []mediacontentmod.MediaContent + data, res.Total, res.HasNext, err = mediacontentdata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + log.Error("AppQueryListReq.GetList mediacontentdata.GetListFromCache fail", log.Any("req", p), log.E(err)) + return res, err + } + contentIds := []primitive.ObjectID{} + // 获取是否已经购买 + for _, item := range data { + contentIds = append(contentIds, item.ID) + } + buyMap, isWholeBuy, err := media_buy_record_mod.IsBuyBatch(uid, mediaId, contentIds) + if err != nil { + log.Error("AppQueryListReq.GetList media_buy_record_mod.IsBuyBatch fail", log.Any("req", p), log.E(err)) + return res, err + } + hasCard := false + if media.MediaType == mediamod.MediaTypeDrama { + hasCard, err = ActiveDramaCardStatus(uid) + if err != nil { + return res, err + } + } + // 组装返回数据 + for _, item := range data { + tmp := AppMediaContent{ + ID: item.ID, + MediaID: item.MediaID, + MediaType: item.MediaType, + EpisodeNumber: item.EpisodeNumber, + ListenPermission: item.ListenPermission, + Price: item.Price, + Name: item.Name, + PlayTime: item.PlayTime, + Cover: item.Cover, + AudioUrl: item.AudioUrl, + VideoUrl: item.VideoUrl, + H265Url: appg.H265URLForApp(item.H265Url), + HasBuy: buyMap[item.ID], + Ratio: item.Ratio, + CreatedAt: item.CreatedAt, + UpdateTime: item.UpdateTime, + } + if strings.TrimSpace(tmp.Name) == "" { + tmp.Name = fmt.Sprintf("第%v集", item.EpisodeNumber) + } + // 是否整本购买 + if isWholeBuy { + tmp.HasBuy = true + } + if item.MediaType == mediamod.MediaTypeDrama { + tmp.HasDramaCard = hasCard + tmp.IsFree, tmp.CanPlay, tmp.AccessType = ResolveDramaAccess(item, media, tmp.HasBuy, hasCard) + tmp.PreviewEnabled = item.PreviewEnabled + tmp.PreviewStart = item.PreviewStart + tmp.PreviewSeconds = item.PreviewSeconds + if !tmp.CanPlay { + if tmp.PreviewEnabled { + tmp.PreviewVideoUrl = tmp.VideoUrl + tmp.PreviewH265Url = tmp.H265Url + } + tmp.AudioUrl = "" + tmp.VideoUrl = "" + tmp.H265Url = "" + } + } + if tmp.Cover == "" && len(item.UrlSet) > 0 { + tmp.Cover = item.UrlSet[0] + } + res.List = append(res.List, tmp) + } + + return res, nil +} + +type AppQueryInfoReq struct { + ID string `json:"id" form:"id"` // id +} + +// GetInfo 获取详情 +func (p *AppQueryInfoReq) GetInfo(uid uint64) (res MediaContentInfo, err error) { + oid, err := primitive.ObjectIDFromHex(p.ID) + if err != nil { + return res, err + } + + var item mediacontentmod.MediaContent + item, err = mediacontentdata.GetInfoFromCache(oid) + if err != nil { + log.Error("AppQueryInfoReq.GetInfo mediacontentdata.GetInfoFromCache fail", log.Any("mediaContentId", p.ID), log.E(err)) + return + } + var media mediamod.Media + if item.MediaType == mediamod.MediaTypeDrama { + media, err = mediamod.GetInfo(item.MediaID) + if err != nil { + return res, err + } + if media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete || !item.IsActive || item.IsDelete { + return res, fmt.Errorf("drama episode is inactive") + } + } + + // 判断是否已经购买 + hasBuy, err := media_buy_record_mod.IsBuy(uid, item.MediaID, item.ID) + if err != nil { + log.Error("AppQueryInfoReq.GetInfo media_buy_record_mod.IsBuy fail", log.Any("mediaContentId", p.ID), log.E(err)) + return + } + res = MediaContentInfo{ + ID: item.ID, + MediaID: item.MediaID, + MediaType: item.MediaType, + EpisodeNumber: item.EpisodeNumber, + ListenPermission: item.ListenPermission, + Price: item.Price, + Name: item.Name, + Cover: item.Cover, + Text: item.Text, + Md5: item.Md5, + VideoUrl: item.VideoUrl, + H265Url: appg.H265URLForApp(item.H265Url), + AudioUrl: item.AudioUrl, + UrlSet: item.UrlSet, + Height: item.Height, + Weight: item.Weight, + MediaSize: item.MediaSize, + PlayTime: item.PlayTime, + IsActive: item.IsActive, + //Ratio: item.Ratio, + Ratio: 1, + CreatedAt: item.CreatedAt, + UpdateTime: item.UpdateTime, + HasBuy: hasBuy, + PreviewEnabled: item.PreviewEnabled, + PreviewStart: item.PreviewStart, + PreviewSeconds: item.PreviewSeconds, + } + if item.MediaType == mediamod.MediaTypeDrama { + res.HasDramaCard, err = ActiveDramaCardStatus(uid) + if err != nil { + return res, err + } + res.IsFree, res.CanPlay, res.AccessType = ResolveDramaAccess(item, media, hasBuy, res.HasDramaCard) + res.Ratio = item.Ratio + if !res.CanPlay { + if res.PreviewEnabled { + res.PreviewVideoUrl = res.VideoUrl + res.PreviewH265Url = res.H265Url + } + res.VideoUrl = "" + res.H265Url = "" + res.AudioUrl = "" + res.UrlSet = []string{} + res.Text = "" + res.Md5 = "" + res.Paywall = newPaywall(uid, item.Price) + } + } + if strings.TrimSpace(res.Name) == "" { + res.Name = fmt.Sprintf("第%v集", item.EpisodeNumber) + } + if res.Cover == "" && len(item.UrlSet) > 0 { + res.Cover = item.UrlSet[0] + } + EpisodeNumbers := []int{} + if item.EpisodeNumber-1 > 0 { + EpisodeNumbers = append(EpisodeNumbers, item.EpisodeNumber-1) + } + + EpisodeNumbers = append(EpisodeNumbers, item.EpisodeNumber+1) + + // 获取上一集下一集 + otherFilter := bson.M{"mediaId": item.MediaID, "episodeNumber": bson.M{"$in": EpisodeNumbers}, "isActive": true, "isDelete": false} + if item.MediaType == mediamod.MediaTypeDrama { + otherFilter["mediaType"] = mediamod.MediaTypeDrama + } + otherContents, _, _, err := mediacontentdata.GetListFromCache(otherFilter, 0, 2, bson.D{}) + if err != nil { + log.Error("AppQueryInfoReq.GetInfo mediacontentdata.GetListData fail", log.Any("mediaContentId", p.ID), log.E(err)) + return + } + for _, v := range otherContents { + if v.Cover == "" && len(v.UrlSet) > 0 { + v.Cover = v.UrlSet[0] + } + if v.EpisodeNumber == item.EpisodeNumber-1 { + // 上一集 + res.Prev = &OtherContent{ + Id: v.ID, + EpisodeNumber: v.EpisodeNumber, + Name: v.Name, + Cover: v.Cover, + } + } + if v.EpisodeNumber == item.EpisodeNumber+1 { + // 下一集 + res.Next = &OtherContent{ + Id: v.ID, + EpisodeNumber: v.EpisodeNumber, + Name: v.Name, + Cover: v.Cover, + } + } + } + // 累加浏览数 + common.Go(func() { + // 增加父级的浏览数量 + mediamod.IncBrowseCount(item.MediaID, 1) + topasist.Incr(topser.TypeMedia(item.MediaType), item.ID.Hex(), 1) + }) + return +} diff --git a/app/service/mediacontentser/mediacontent_test.go b/app/service/mediacontentser/mediacontent_test.go new file mode 100644 index 0000000..9ed0d7d --- /dev/null +++ b/app/service/mediacontentser/mediacontent_test.go @@ -0,0 +1,68 @@ +package mediacontentser + +import ( + "encoding/json" + "testing" +) + +func TestMediaContentInfoMarshalH265URL(t *testing.T) { + tests := []struct { + name string + h265URL string + want string + }{ + { + name: "resource exists", + h265URL: "https://cdn.example.com/h265/index.m3u8", + want: "https://cdn.example.com/h265/index.m3u8", + }, + { + name: "resource missing", + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(MediaContentInfo{ + VideoUrl: "https://cdn.example.com/h264/index.m3u8", + H265Url: tt.h265URL, + }) + if err != nil { + t.Fatalf("marshal media content info: %v", err) + } + + var response map[string]interface{} + if err := json.Unmarshal(data, &response); err != nil { + t.Fatalf("unmarshal response: %v", err) + } + if response["videoUrl"] != "https://cdn.example.com/h264/index.m3u8" { + t.Fatalf("unexpected H.264 URL: %v", response["videoUrl"]) + } + if response["h265Url"] != tt.want { + t.Fatalf("unexpected H.265 URL: %v", response["h265Url"]) + } + }) + } +} + +func TestAppMediaContentMarshalH265URL(t *testing.T) { + data, err := json.Marshal(AppMediaContent{ + VideoUrl: "https://cdn.example.com/h264/index.m3u8", + H265Url: "https://cdn.example.com/h265/index.m3u8", + }) + if err != nil { + t.Fatalf("marshal app media content: %v", err) + } + + var response map[string]interface{} + if err := json.Unmarshal(data, &response); err != nil { + t.Fatalf("unmarshal app media content: %v", err) + } + if response["videoUrl"] != "https://cdn.example.com/h264/index.m3u8" { + t.Fatalf("unexpected H.264 URL: %v", response["videoUrl"]) + } + if response["h265Url"] != "https://cdn.example.com/h265/index.m3u8" { + t.Fatalf("unexpected H.265 URL: %v", response["h265Url"]) + } +} diff --git a/app/service/mediaser/help.go b/app/service/mediaser/help.go new file mode 100644 index 0000000..381addb --- /dev/null +++ b/app/service/mediaser/help.go @@ -0,0 +1,244 @@ +package mediaser + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/mediatagser" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/mediatagmod" + "91porn-server/models/v/usermod" + "strings" + "sync" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func buildDedupTagsAndDetails(tagIDs []primitive.ObjectID, tagMap map[primitive.ObjectID]*mediatagmod.MediaTag) ([]primitive.ObjectID, []mediamod.TagDetail) { + if len(tagIDs) == 0 { + return nil, nil + } + seen := make(map[string]struct{}, len(tagIDs)) + uniqueTagIDs := make([]primitive.ObjectID, 0, len(tagIDs)) + tagDetails := make([]mediamod.TagDetail, 0, len(tagIDs)) + for _, id := range tagIDs { + tag, ok := tagMap[id] + if !ok { + continue + } + name := strings.TrimSpace(tag.Name) + if _, exists := seen[name]; exists { + continue + } + seen[name] = struct{}{} + uniqueTagIDs = append(uniqueTagIDs, tag.ID) + tagDetails = append(tagDetails, mediamod.TagDetail{ + Id: tag.ID, + Name: tag.Name, + }) + } + return uniqueTagIDs, tagDetails +} + +func FillMedias(medias []*mediamod.Media, uid uint64, needDefaultContent bool, dramaCard ...bool) []*mediamod.AppMediaBase { + if len(medias) == 0 { + return nil + } + tagIDs := make([]primitive.ObjectID, 0) + var ( + mIds []primitive.ObjectID + mStatus map[primitive.ObjectID]mediamod.MediaStatus + hasDrama bool + hasDramaCard bool + ) + + for i := range medias { + if medias[i] == nil { + continue + } + mIds = append(mIds, medias[i].ID) + tagIDs = append(tagIDs, medias[i].Tags...) + if medias[i].MediaType == mediamod.MediaTypeDrama { + hasDrama = true + } + } + var wg sync.WaitGroup + wg.Add(1) + common.Go(func() { + defer wg.Done() + mStatus = GetUserStatus(uid, mIds) + }) + if hasDrama { + if len(dramaCard) > 0 { + hasDramaCard = dramaCard[0] + } else { + wg.Add(1) + common.Go(func() { + defer wg.Done() + user, err := usermod.FindUserByUID(uid) + hasDramaCard = err == nil && user != nil && user.DramaExpire.After(time.Now()) + }) + } + } + wg.Wait() + var infos []*mediamod.AppMediaBase + tagMap := mediatagser.GetTagsByIDs2Map(tagIDs) + for i := range medias { + if medias[i] == nil { + continue + } + mid := "" + if !medias[i].MID.IsZero() { + mid = medias[i].MID.Hex() + } + temp := mediamod.AppMediaBase{ + ID: medias[i].ID, + SortCode: medias[i].SortCode, + Status: medias[i].Status, + Title: medias[i].Title, + HorizontalCover: medias[i].HorizontalCover, + VerticalCover: medias[i].VerticalCover, + Tags: make([]primitive.ObjectID, 0), + TagDetails: make([]mediamod.TagDetail, 0), + Summary: medias[i].Summary, + TotalEpisode: medias[i].TotalEpisode, + CurrentEpisode: medias[i].CurrentEpisode, + FreeEpisode: medias[i].FreeEpisode, + MediaType: medias[i].MediaType, + MediaSubType: medias[i].MediaSubType, + Kind: medias[i].Kind, + Price: medias[i].Price, + ContentsPrice: medias[i].ContentsPrice, + Direction: medias[i].Direction, + UpdateStatus: medias[i].UpdateStatus, + Permission: medias[i].Permission, + PermissionIconHide: medias[i].PermissionIconHide, + Style: medias[i].Style, + FreeTime: medias[i].FreeTime, + CountPurchases: medias[i].CountPurchases, + CountBrowse: medias[i].CountBrowse, + CountCollect: medias[i].CountCollect, + CountLike: medias[i].CountLike, + CountDisLike: medias[i].CountDisLike, + CountView: medias[i].CountView, + CountShare: medias[i].CountShare, + HasFollow: medias[i].HasFollow, + MediaStatus: mStatus[medias[i].ID], + CountComment: medias[i].CountComment, + Number: medias[i].Number, + CreatedAt: medias[i].CreatedAt, + UpdateTime: medias[i].UpdateTime, + LatestPublishedAt: medias[i].LatestPublishedAt, + MID: mid, + ModuleName: medias[i].ModuleName, + SectionName: medias[i].SectionName, + SID: medias[i].SID, + SectionSort: medias[i].SectionSort, + LsjId: medias[i].LsjId, + } + if medias[i].MediaType == mediamod.MediaTypeDrama { + temp.HasDramaCard = hasDramaCard + } + if temp.MediaStatus.HasCollected { + temp.CountCollect = temp.CountCollect + 1 // 避免缓存详情的情况,收藏完成之后数字没有任何变化 + } + if needDefaultContent { + temp.DefaultContent = medias[i].DefaultContent + } + if temp.DefaultContent.Cover == "" && len(temp.DefaultContent.UrlSet) > 0 { + temp.DefaultContent.Cover = temp.DefaultContent.UrlSet[0] + } + if strings.TrimSpace(temp.DefaultContent.Name) == "" { + temp.DefaultContent.Name = "第一集" + } + temp.Tags, temp.TagDetails = buildDedupTagsAndDetails(medias[i].Tags, tagMap) + + infos = append(infos, &temp) + } + return infos +} + +func FillNotStatusMedias(medias []*mediamod.Media, needDefaultContent bool) []*mediamod.AppMediaBase { + if len(medias) == 0 { + return []*mediamod.AppMediaBase{} + } + tagIDs := make([]primitive.ObjectID, 0) + var ( + mIds []primitive.ObjectID + ) + + for i := range medias { + mIds = append(mIds, medias[i].ID) + if medias[i] != nil { + tagIDs = append(tagIDs, medias[i].Tags...) + } + } + + infos := []*mediamod.AppMediaBase{} + tagMap := mediatagser.GetTagsByIDs2Map(tagIDs) + for i := range medias { + if medias[i] == nil { + continue + } + temp := mediamod.AppMediaBase{ + ID: medias[i].ID, + Title: medias[i].Title, + HorizontalCover: medias[i].HorizontalCover, + VerticalCover: medias[i].VerticalCover, + Tags: make([]primitive.ObjectID, 0), + TagDetails: make([]mediamod.TagDetail, 0), + Summary: medias[i].Summary, + TotalEpisode: medias[i].TotalEpisode, + CurrentEpisode: medias[i].CurrentEpisode, + FreeEpisode: medias[i].FreeEpisode, + MediaType: medias[i].MediaType, + MediaSubType: medias[i].MediaSubType, + Kind: medias[i].Kind, + Price: medias[i].Price, + ContentsPrice: medias[i].ContentsPrice, + Direction: medias[i].Direction, + UpdateStatus: medias[i].UpdateStatus, + Permission: medias[i].Permission, + CountPurchases: medias[i].CountPurchases, + CountBrowse: medias[i].CountBrowse, + CountCollect: medias[i].CountCollect, + CountLike: medias[i].CountLike, + HasFollow: medias[i].HasFollow, + CountComment: medias[i].CountComment, + Number: medias[i].Number, + CreatedAt: medias[i].CreatedAt, + UpdateTime: medias[i].UpdateTime, + LatestPublishedAt: medias[i].LatestPublishedAt, + } + if temp.MediaStatus.HasCollected { + temp.CountCollect = temp.CountCollect + 1 // 避免缓存详情的情况,收藏完成之后数字没有任何变化 + } + if needDefaultContent { + temp.DefaultContent = medias[i].DefaultContent + } + if temp.DefaultContent.Cover == "" && len(temp.DefaultContent.UrlSet) > 0 { + temp.DefaultContent.Cover = temp.DefaultContent.UrlSet[0] + } + if strings.TrimSpace(temp.DefaultContent.Name) == "" { + temp.DefaultContent.Name = "第一集" + } + temp.Tags, temp.TagDetails = buildDedupTagsAndDetails(medias[i].Tags, tagMap) + + infos = append(infos, &temp) + } + return infos +} + +// IncrCountBrowse 增加播放量 +func IncrCountBrowse(mediaId primitive.ObjectID) { + now := time.Now() + recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) + key := redisconst.AcgBrowseCountListKey(recentMinute.Unix()) + if err := appg.Redis.RPush(key, mediaId.Hex()); err != nil { + return + } + _, _ = appg.Redis.ExpireKey(key, time.Minute*20) +} diff --git a/app/service/mediaser/help_test.go b/app/service/mediaser/help_test.go new file mode 100644 index 0000000..523be40 --- /dev/null +++ b/app/service/mediaser/help_test.go @@ -0,0 +1,38 @@ +package mediaser + +import ( + "reflect" + "testing" + + "91porn-server/models/v/mediamod" + "91porn-server/models/v/mediatagmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestBuildDedupTagsAndDetails(t *testing.T) { + tagID1 := primitive.NewObjectID() + tagID2 := primitive.NewObjectID() + duplicateNameTagID := primitive.NewObjectID() + missingTagID := primitive.NewObjectID() + tagIDs := []primitive.ObjectID{tagID1, tagID2, duplicateNameTagID, missingTagID} + tagMap := map[primitive.ObjectID]*mediatagmod.MediaTag{ + tagID1: {ID: tagID1, Name: "剧情"}, + tagID2: {ID: tagID2, Name: "恋爱"}, + duplicateNameTagID: {ID: duplicateNameTagID, Name: "剧情"}, + } + + gotTagIDs, gotDetails := buildDedupTagsAndDetails(tagIDs, tagMap) + wantTagIDs := []primitive.ObjectID{tagID1, tagID2} + wantDetails := []mediamod.TagDetail{ + {Id: tagID1, Name: "剧情"}, + {Id: tagID2, Name: "恋爱"}, + } + + if !reflect.DeepEqual(gotTagIDs, wantTagIDs) { + t.Fatalf("tag IDs = %#v, want %#v", gotTagIDs, wantTagIDs) + } + if !reflect.DeepEqual(gotDetails, wantDetails) { + t.Fatalf("tag details = %#v, want %#v", gotDetails, wantDetails) + } +} diff --git a/app/service/mediaser/media.go b/app/service/mediaser/media.go new file mode 100755 index 0000000..59fc16f --- /dev/null +++ b/app/service/mediaser/media.go @@ -0,0 +1,1302 @@ +package mediaser + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/mediatagser" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/common/timeutil" + topser "91porn-server/common/top" + topasist "91porn-server/common/top/asistant" + "91porn-server/common/top/dailytop" + "91porn-server/common/top/monthtop" + "91porn-server/common/top/weektop" + "91porn-server/models/cache/mediadata" + "91porn-server/models/cache/mediatagdata" + "91porn-server/models/commod" + "91porn-server/models/s/sectionstatmod" + "91porn-server/models/v/likemod" + "91porn-server/models/v/media_buy_record_mod" + "91porn-server/models/v/mediabookshelfmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/mediatagdimensionmod" + "91porn-server/models/v/mediatagmod" + "91porn-server/models/v/modulesectionmod" + "fmt" + "github.com/golang-module/carbon/v2" + "github.com/vmihailenco/msgpack/v5" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "strconv" + "sync" + "time" +) + +type AppQueryListReq struct { + commod.Page +} + +type AppListRes struct { + HasNext bool `json:"hasNext"` + List []*mediamod.AppMediaBase `json:"list"` +} + +// GetList 获取列表 +func (p *AppQueryListReq) GetList() AppListRes { + var res AppListRes + var err error + filter := bson.M{} + + sort := bson.D{{"_id", -1}} + // 获取列表 + var data []*mediamod.Media + data, res.HasNext, err = mediadata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + return res + } + // 组装返回数据 + for _, item := range data { + tmp := &mediamod.AppMediaBase{ + ID: item.ID, + SortCode: item.SortCode, + Status: item.Status, + Title: item.Title, + Author: item.Anchor, + HorizontalCover: item.HorizontalCover, + VerticalCover: item.VerticalCover, + Tags: item.Tags, + Summary: item.Summary, + TotalEpisode: item.TotalEpisode, + MediaType: item.MediaType, + MediaSubType: item.MediaSubType, + Kind: item.Kind, + PermissionIconHide: item.PermissionIconHide, + Permission: item.Permission, + Price: item.Price, + Direction: item.Direction, + FreeTime: item.FreeTime, + CountComment: item.CountComment, + CountPurchases: item.CountPurchases, + CountBrowse: item.CountBrowse, + CountCollect: item.CountCollect, + CountLike: item.CountLike, + CountView: item.CountView, + Number: item.Number, + CreatedAt: item.CreatedAt, + UpdateTime: item.UpdateTime, + } + res.List = append(res.List, tmp) + } + + return res +} + +type AppQueryInfoReq struct { + ID string `json:"id" form:"id"` // id +} + +// GetInfo 获取详情 +func (p *AppQueryInfoReq) GetInfo(uid uint64) (resp *mediamod.AppMediaBase, err error) { + oid, err := primitive.ObjectIDFromHex(p.ID) + if err != nil { + return resp, err + } + + var item mediamod.Media + item, err = mediadata.GetInfoFromCache(oid) + if err != nil { + log.Error("AppQueryInfoReq.GetInfo mediadata.GetInfoFromCache fail", log.Any("mediaId", p.ID), log.E(err)) + return resp, err + } + if item.ID.IsZero() { + return resp, nil + } + if item.MediaType == mediamod.MediaTypeDrama && (item.Status != 1 || item.IsDelete) { + return nil, fmt.Errorf("drama is inactive") + } + res := FillMedias([]*mediamod.Media{&item}, uid, true) + resp = res[0] + // 累加浏览数 + common.Go(func() { + // 增加父级的浏览数量 + IncrCountBrowse(item.ID) + topasist.Incr(topser.TypeMedia(item.MediaType), item.ID.Hex(), 1) + }) + return resp, nil +} + +// GetUserStatus 获取 用户状态信息 只跟media这张表有关系 +func GetUserStatus(uid uint64, mediaIds []primitive.ObjectID) map[primitive.ObjectID]mediamod.MediaStatus { + mPay := make(map[primitive.ObjectID]bool) + hasAddBookshelf := make(map[primitive.ObjectID]bool) + mLike := make(map[primitive.ObjectID]bool) + + var wg sync.WaitGroup + wg.Add(3) + common.Go(func() { + defer wg.Done() + mPay, _ = media_buy_record_mod.IsWholeBuyBatch(uid, mediaIds) + }) + common.Go(func() { + defer wg.Done() + mLike, _ = likemod.IsLikeByType(uid, []string{constant.LikeTypeText, constant.LikeTypeComics, constant.LikeTypeCartoon, constant.LikeTypeDrama}, mediaIds) + }) + common.Go(func() { + defer wg.Done() + hasAddBookshelf, _ = mediabookshelfmod.HasAddBookshelf(uid, mediaIds) + }) + wg.Wait() + m := map[primitive.ObjectID]mediamod.MediaStatus{} + for _, v := range mediaIds { + info := mediamod.MediaStatus{ + HasPaid: mPay[v], + HasCollected: hasAddBookshelf[v], + HasLiked: mLike[v], + } + m[v] = info + } + return m +} + +// GetLibrary 获取片库 +func GetLibrary() (res []mediamod.AppLibrary, err error) { + var resp []mediamod.AppLibrary + + str, err := appg.Redis.Get(redisconst.MediaLibraryInfo) + if err != nil { + log.Warn(fmt.Sprintf("缓存获取动漫片库信息异常:%v", err)) + } + if str != nil { + if err = msgpack.Unmarshal([]byte(*str), &resp); err == nil { + return resp, nil + } + log.Warn(fmt.Sprintf("解析预售缓存数据异常:%v", err)) + } + + resp = append(resp, mediamod.AppLibrary{ + Key: Video, + Name: "动漫", + }, mediamod.AppLibrary{ + Key: Image, + Name: "漫画", + }) + for i, d := range resp { + if d.Key == Video { + resp[i].Kind = append(resp[i].Kind, mediamod.KindInfo{ + Key: "1", + Name: "哩番", + Cover: "cf230705/image/lm/34n/1la/2ck/7064ad102298311797d9f43b323a2ea4.png", + }, mediamod.KindInfo{ + Key: "2", + Name: "3D", + Cover: "cf230705/image/18b/2zw/1dp/2wu/3b90ad609435c0037f7a8a90ed8e2de2.png", + }, mediamod.KindInfo{ + Key: "3", + Name: "同人动画", + Cover: "cf230705/image/14q/5y/19x/p9/3c48b94311b361f2dcdb006200208751.png", + }) + } + if d.Key == Image { + resp[i].Kind = append(resp[i].Kind, mediamod.KindInfo{ + Key: "4", + Name: "单行本", + Cover: "cf230705/image/2a4/1kk/2hi/2xg/b830c4997f21ad4351ab0880fd2bc1be.png", + }, mediamod.KindInfo{ + Key: "5", + Name: "连载漫画", + Cover: "cf230705/image/p2/1rq/2c3/nx/ce44dfc5406ef4d20cef04a683970f4c.png", + }, mediamod.KindInfo{ + Key: "6", + Name: "韩漫", + Cover: "cf230705/image/1ol/26f/lb/2rq/872dab8f0ccd3c4eba6cba5171630112.png", + }) + } + } + + // 获取 动漫、漫画纬度 + filter := bson.M{"status": 1, "isDelete": false} + op := options.Find().SetSort(bson.D{{"sortCode", -1}}) + tagDimensions, err := mediatagdimensionmod.QueryAllList(filter, op) + if err != nil { + return resp, nil + } + + if len(tagDimensions) > 0 { + for i, m := range resp { + for _, d := range tagDimensions { + if m.Key == "video" && d.Type == 1 { + for j := range resp[i].Kind { + resp[i].Kind[j].Dimension = append(resp[i].Kind[j].Dimension, mediamod.DimensionInfo{ + Key: d.ID.Hex(), + Name: d.Name, + }) + } + } + if m.Key == "image" && d.Type == 2 { + for j := range resp[i].Kind { + resp[i].Kind[j].Dimension = append(resp[i].Kind[j].Dimension, mediamod.DimensionInfo{ + Key: d.ID.Hex(), + Name: d.Name, + }) + } + } + } + } + } + + // 获取所有推荐标签 + tagFilter := bson.M{"active": true, "isDelete": false, "isDiscovery": true} + tagOp := options.Find().SetSort(bson.D{{"sort", -1}}) + mediaTags, err := mediatagmod.QueryAllList(tagFilter, tagOp) + if err != nil { + return resp, nil + } + + if len(mediaTags) > 0 { + for i, m := range resp { + for _, t := range mediaTags { + if m.Key == "video" && t.Type == 1 && (t.Kind == 1 || t.Kind == 2 || t.Kind == 3) { + for j := range resp[i].Kind { + for d, k := range resp[i].Kind[j].Dimension { + kindStr := strconv.Itoa(t.Kind) + if t.Dimension == k.Name && resp[i].Kind[j].Key == kindStr { + resp[i].Kind[j].Dimension[d].Tag = append(resp[i].Kind[j].Dimension[d].Tag, mediamod.TagInfo{ + ID: t.ID.Hex(), + Name: t.Name, + }) + } + } + } + } + if m.Key == "image" && t.Type == 2 && (t.Kind == 4 || t.Kind == 5 || t.Kind == 6) { + for j := range resp[i].Kind { + for d, k := range resp[i].Kind[j].Dimension { + kindStr := strconv.Itoa(t.Kind) + if t.Dimension == k.Name && resp[i].Kind[j].Key == kindStr { + resp[i].Kind[j].Dimension[d].Tag = append(resp[i].Kind[j].Dimension[d].Tag, mediamod.TagInfo{ + ID: t.ID.Hex(), + Name: t.Name, + }) + } + } + } + } + } + } + } + common.Go(func() { + bytes, _ := msgpack.Marshal(resp) + if err = appg.Redis.Set(redisconst.MediaLibraryInfo, bytes, 11*time.Minute); err != nil { + log.Warn(fmt.Sprintf("保存预售缓存数据异常:%v", err)) + } + }) + return resp, nil +} + +func LibrarySearch(in mediamod.AppLibraryReq) (data mediamod.AppElasticSearchLibraryResponse, err error) { + var resp mediamod.AppElasticSearchLibraryResponse + from := int64((in.PageNumber - 1) * (in.PageSize)) + size := int64(in.PageSize + 1) + + must := elastic.A{ + {"term": elastic.M{"mediaType.keyword": HandleMediaType(in.MediaType)}}, + } + + // 历史同步数据可能为同一个业务标签生成多个 ID。片库按标签名合并这些 + // ID,避免搜索结果返回的某个旧 ID 在标签详情页查不到同名内容。 + tagIds := HandleTagIds(expandMediaTagAliases(in.TagIds)) + must = append(must, tagIds...) + + short := HandleLibraryShort(in) + // 根据数据结构组装数据 + query := elastic.M{ + "query": elastic.M{ + "bool": elastic.M{ + "must": must, + }, + }, + "sort": short, + "from": from, + "size": size, + } + + res, err := mediamod.SearchByCondWithTotal(query) + if err != nil { + return resp, nil + } + hasNext := false + if uint64(len(res.Hits)) > in.PageSize { + res.Hits = res.Hits[:in.PageSize] + hasNext = true + } + esMedias := make([]mediamod.ESMedia, len(res.Hits)) + for i := range res.Hits { + esMedias[i] = res.Hits[i].Source + } + + videosIDs := make([]primitive.ObjectID, len(esMedias)) + for i, v := range esMedias { + videosIDs[i] = v.ID + } + if len(videosIDs) > int(size) { + hasNext = true + videosIDs = videosIDs[:size] + } + log.Info(fmt.Sprintf("search media mIds:%v", videosIDs)) + + mediaList, _, err := mediadata.GetListFromCache(in.Filter(videosIDs), 0, int64(in.Limit()), in.Options()) + if err != nil { + return resp, err + } + mediaList = orderMediaByIDs(mediaList, videosIDs) + resp.HasNext = hasNext + + if len(mediaList) <= 0 { + return resp, nil + } + + tagIDs := make([]primitive.ObjectID, 0) + for _, item := range mediaList { + tagIDs = append(tagIDs, item.Tags...) + } + tagMap := mediatagser.GetTagsByIDs2Map(tagIDs) + + for _, item := range mediaList { + tags, tagDetails := buildDedupTagsAndDetails(item.Tags, tagMap) + data := &mediamod.AppMediaBase{ + ID: item.ID, + SortCode: item.SortCode, + Status: item.Status, + Title: item.Title, + Author: item.Anchor, + HorizontalCover: item.HorizontalCover, + VerticalCover: item.VerticalCover, + Tags: tags, + TagDetails: tagDetails, + Summary: item.Summary, + TotalEpisode: item.TotalEpisode, + UpdateStatus: item.UpdateStatus, + MediaType: item.MediaType, + MediaSubType: item.MediaSubType, + Kind: item.Kind, + PermissionIconHide: item.PermissionIconHide, + Permission: item.Permission, + Price: item.Price, + Direction: item.Direction, + FreeTime: item.FreeTime, + CountComment: item.CountComment, + CountPurchases: item.CountPurchases, + CountBrowse: item.CountBrowse, + CountCollect: item.CountCollect, + CountLike: item.CountLike, + CountView: item.CountView, + Number: item.Number, + CreatedAt: item.CreatedAt, + UpdateTime: item.UpdateTime, + } + resp.List = append(resp.List, data) + } + resp.HasNext = hasNext + common.Go(func() { + if len(videosIDs) > 0 { + //HandleDiffVideo(vidMods, videosIDs) + } + }) + return resp, nil +} + +// orderMediaByIDs keeps the final response in the exact order returned by Elasticsearch. +// MongoDB's $in query does not preserve the input ID order, and its copy of a sort field +// can briefly differ from Elasticsearch while indexes are being synchronized. +func orderMediaByIDs(mediaList []*mediamod.Media, ids []primitive.ObjectID) []*mediamod.Media { + mediaByID := make(map[primitive.ObjectID]*mediamod.Media, len(mediaList)) + for _, item := range mediaList { + if item != nil { + mediaByID[item.ID] = item + } + } + + ordered := make([]*mediamod.Media, 0, len(mediaList)) + for _, id := range ids { + if item, ok := mediaByID[id]; ok { + ordered = append(ordered, item) + } + } + return ordered +} + +func HandleShort(key int) elastic.A { + return elastic.A{{mediamod.AppLibrarySortField(key): elastic.M{"order": "desc"}}} +} + +func HandleLibraryShort(in mediamod.AppLibraryReq) elastic.A { + if HandleMediaType(in.MediaType) == mediamod.MediaTypeDrama && in.SortType == 1 { + return elastic.A{ + {"sortCode": elastic.M{"order": "desc"}}, + {"createdAt": elastic.M{"order": "desc"}}, + } + } + return HandleShort(in.SortType) +} + +func HandleTagIds(tagIds []string) elastic.A { + var tagIdsValue = elastic.A{} + var allTagIds []primitive.ObjectID + if len(tagIds) > 0 { + for t := range tagIds { + id, err := primitive.ObjectIDFromHex(tagIds[t]) + if err != nil || id.IsZero() { + continue + } + allTagIds = append(allTagIds, id) + } + } + allTagIds = deduplicateObjectIDs(allTagIds) + + tagIdsValue = append(tagIdsValue, + elastic.M{"terms": elastic.M{"tags.keyword": allTagIds}}, + ) + + return tagIdsValue +} + +func expandMediaTagAliases(tagIDs []string) []string { + expanded := make([]primitive.ObjectID, 0, len(tagIDs)) + for _, value := range tagIDs { + id, err := primitive.ObjectIDFromHex(value) + if err != nil || id.IsZero() { + continue + } + expanded = append(expanded, id) + + tag, tagErr := mediatagdata.GetInfoFromCache(id) + if tagErr != nil || tag.Name == "" { + continue + } + aliases, aliasErr := mediatagdata.GetTagIdsByNameFromCache(tag.Name) + if aliasErr == nil { + expanded = append(expanded, aliases...) + } + } + + expanded = deduplicateObjectIDs(expanded) + result := make([]string, len(expanded)) + for i, id := range expanded { + result[i] = id.Hex() + } + return result +} + +func deduplicateObjectIDs(ids []primitive.ObjectID) []primitive.ObjectID { + seen := make(map[primitive.ObjectID]struct{}, len(ids)) + result := make([]primitive.ObjectID, 0, len(ids)) + for _, id := range ids { + if id.IsZero() { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + result = append(result, id) + } + return result +} + +func Search(uid uint64, in mediamod.AppSearchReq) (resp mediamod.AppElasticSearchResponse, err error) { + var mediaType constant.AcgNewsType + switch in.Kind { + case 1: + mediaType = mediamod.MediaTypeVideo + case 2: + mediaType = mediamod.MediaTypeImage + case 3: + mediaType = mediamod.MediaTypeText + case 4: + mediaType = mediamod.MediaTypeDrama + default: + return resp, nil + } + if in.Kind == 4 { + defer normalizeDramaSearchLists(&resp) + } + + resp, err = searchByKeyWord(uid, in, mediaType) + if err != nil { + return resp, err + } + + // 本项目使用 关键字 搜索tag名 + in.TagName = in.KeyWord + + var tagIDFilter interface{} + var responseTagID primitive.ObjectID + if in.Kind == 4 { + tagIDs, tagErr := mediatagdata.GetTagIdsByNameFromCache(in.TagName) + if tagErr != nil { + log.Error("通过标签搜索短剧错误", log.Any("TagName", in.TagName), log.E(tagErr)) + return resp, tagErr + } + if len(tagIDs) == 0 { + return resp, nil + } + tagIDFilter = bson.M{"$in": tagIDs} + responseTagID = tagIDs[0] + } else { + tag, tagErr := mediatagdata.GetTagByNameFromCache(in.TagName) + if tagErr != nil { + log.Error("通过标签搜索ACG内容错误", log.Any("TagName", in.TagName), log.E(tagErr)) + return resp, tagErr + } + if tag.ID.IsZero() { + return resp, nil + } + tagIDFilter = tag.ID + responseTagID = tag.ID + } + filter := bson.M{ + "mediaType": mediaType, + "tags": tagIDFilter, + "status": 1, + "isDelete": false, + } + sort := bson.D{{Key: "createdAt", Value: -1}} // 默认最新 + switch in.SortType { + case 1: + // 最新 + sort = bson.D{{Key: "createdAt", Value: -1}} + case 2: + //最多观看 + sort = bson.D{{Key: "countBrowse", Value: -1}} + } + if in.Kind == 4 { + sort = in.Options() + } + mediaList, _, err := mediadata.GetListFromCache(filter, 0, 6, sort) + if err != nil { + log.Error("通过标签搜索ACG内容错误", log.Any("filter", filter), log.E(err)) + return + } + if in.Kind == 4 { + resp.TagMediaList = FillMedias(mediaList, uid, true) + } else { + resp.TagMediaList = mapLegacySearchMedias(mediaList, true) + } + resp.TagID = responseTagID.Hex() + + return resp, nil +} + +func normalizeDramaSearchLists(resp *mediamod.AppElasticSearchResponse) { + if resp.List == nil { + resp.List = []*mediamod.AppMediaBase{} + } + if resp.TagMediaList == nil { + resp.TagMediaList = []*mediamod.AppMediaBase{} + } +} + +func searchByKeyWord(uid uint64, in mediamod.AppSearchReq, mediaType string) (resp mediamod.AppElasticSearchResponse, err error) { + esMedias, err := mediamod.DirectSearch(in.KeyWord, mediaType, int64(in.Skip()), int64(in.Limit()+1)) + if err != nil { + log.Error("搜索ACG内容错误", log.E(err)) + return resp, nil + } + esMediasLen := len(esMedias) + if esMediasLen == 0 { + return resp, err + } + if esMediasLen > int(in.PageSize) { + resp.HasNext = true + esMedias = esMedias[:esMediasLen-1] + } + var mIds []primitive.ObjectID + for _, v := range esMedias { + mIds = append(mIds, v.ID) + } + filter := bson.M{"_id": bson.M{"$in": mIds}, "mediaType": mediaType, "status": 1, "isDelete": false} + + sort := in.Options() + keepSearchOrder := in.Kind == 4 && in.SortType != 2 + if keepSearchOrder { + sort = bson.D{} + } + mediaList, _, err := mediadata.GetListFromCache(filter, 0, int64(in.PageSize), sort) + if err != nil { + return resp, err + } + + if len(mediaList) <= 0 { + return resp, nil + } + + if keepSearchOrder { + // MongoDB 的 $in 不保留 ID 顺序,恢复短剧关键词搜索的相关度顺序。 + mediaList = orderMediaByIDs(mediaList, mIds) + } + if in.Kind == 4 { + resp.List = FillMedias(mediaList, uid, true) + } else { + resp.List = mapLegacySearchMedias(mediaList, false) + } + + return resp, nil +} + +func mapLegacySearchMedias(mediaList []*mediamod.Media, tagResult bool) []*mediamod.AppMediaBase { + result := make([]*mediamod.AppMediaBase, 0, len(mediaList)) + for _, item := range mediaList { + data := &mediamod.AppMediaBase{ + ID: item.ID, SortCode: item.SortCode, Status: item.Status, Title: item.Title, + HorizontalCover: item.HorizontalCover, VerticalCover: item.VerticalCover, + Tags: item.Tags, Summary: item.Summary, TotalEpisode: item.TotalEpisode, + UpdateStatus: item.UpdateStatus, MediaType: item.MediaType, Kind: item.Kind, + PermissionIconHide: item.PermissionIconHide, Permission: item.Permission, + Price: item.Price, Direction: item.Direction, FreeTime: item.FreeTime, + CountComment: item.CountComment, CountPurchases: item.CountPurchases, + CountBrowse: item.CountBrowse, CountCollect: item.CountCollect, + CountLike: item.CountLike, CountView: item.CountView, Number: item.Number, + CreatedAt: item.CreatedAt, UpdateTime: item.UpdateTime, + } + if tagResult { + data.Author = item.Anchor + data.MediaSubType = item.MediaSubType + } + result = append(result, data) + } + return result +} + +func HandleMediaType(key string) string { + var moduleTypeValue string + switch key { + case "video": + moduleTypeValue = mediamod.MediaTypeVideo + case "image": + moduleTypeValue = mediamod.MediaTypeImage + case "text": + moduleTypeValue = mediamod.MediaTypeText + case "drama": + moduleTypeValue = mediamod.MediaTypeDrama + default: + moduleTypeValue = mediamod.MediaTypeVideo + } + return moduleTypeValue +} + +func HandleKindType(key string) int { + var kindValue int + switch key { + case "1": + kindValue = mediamod.RiFan + case "2": + kindValue = mediamod.ThreeD + case "3": + kindValue = mediamod.FanAnimation + case "4": + kindValue = mediamod.Offprint + case "5": + kindValue = mediamod.ComicStrip + case "6": + kindValue = mediamod.KoreanCartoon + default: + kindValue = mediamod.RiFan + } + return kindValue +} + +type AppGetTopicReq struct { + SID string `json:"sId" form:"sId"` // 专题ID + Kind int `json:"kind" form:"kind"` // 种类(1、哩番,2、3D,3、同人动画,4、单行本,5、连载漫画,6、韩漫 7、小说 ) + SortType int `form:"sortType" bson:"sortType"` // 排序 1-最新 2-最多观看 3-最多喜欢(收藏) + Type int `json:"type" form:"type"` // 0-默认专题这些页面获取 1-首页获取(猜你喜欢) + commod.Page +} + +func (p *AppGetTopicReq) Filter() bson.M { + var query = bson.M{} + id, _ := primitive.ObjectIDFromHex(p.SID) + if p.Type != 1 && p.SID != "" { + query["sId"] = id + } + if p.Kind > 0 { + query["kind"] = p.Kind + } + if p.Type == 1 { + // 获取专题的模块id + sId, _ := primitive.ObjectIDFromHex(p.SID) + section, _ := modulesectionmod.GetBySectionByID(sId) + query["mId"] = section.SubModuleID + } + query["status"] = 1 + query["isDelete"] = false + return query +} + +func (p *AppGetTopicReq) Options() bson.D { + var sort bson.D + switch p.SortType { + case 1: + // 1、最新 + sort = bson.D{{"createdAt", -1}} + case 2: + // 2、最多观看 + sort = bson.D{{"countBrowse", -1}} + case 3: + // 3 最多喜欢(收藏) + sort = bson.D{{"countCollect", -1}} + case 4: // 4 热评排序 + sort = bson.D{{"hot", -1}, {"createdAt", -1}} + default: + sort = bson.D{{"createdAt", -1}} + } + return sort +} + +type AppGetTopicRes struct { + HasNext bool `json:"hasNext" bson:"hasNext"` // 是否有下一页 + Section AppSection `json:"section" bson:"section"` // 播单详情 + List []*mediamod.AppMediaBase `json:"list" bson:"list"` // 列表 +} + +type AppSection struct { + ID primitive.ObjectID `json:"id" bson:"id"` // ID + Name string `json:"name" bson:"name" ` // 播单名称 + Cover string `json:"cover" bson:"cover"` // 播单封面 + Count int64 `json:"count" bson:"count"` // 作品数量 + Type int `json:"type" bson:"type"` // 播单类型 + LikeCount int64 `json:"likeCount" bson:"likeCount"` // 点赞数量 + CommentCount int64 `json:"commentCount" bson:"commentCount"` // 评论数量 + UserID uint64 `json:"userId" bson:"userId"` // 用户ID + UserName string `json:"userName" bson:"userName"` // 用户名称 + Portrait string `json:"portrait" bson:"portrait"` // 用户头像 + //MediaStatus mediamod.MediaStatus `json:"mediaStatus" bson:"mediaStatus"` // 媒体状态 +} + +func (p *AppGetTopicReq) getTopicListHomePage() (res AppGetTopicRes, err error) { + mediaList, hasNext, err := mediadata.GetListFromCache(p.Filter(), int64(p.Skip()), int64(p.Limit()), p.Options()) + if err != nil { + return res, err + } + res.List = FillNotStatusMedias(mediaList, false) + res.HasNext = hasNext + return +} + +// GetTopicList 获取详情 +func (p *AppGetTopicReq) GetTopicList(uid uint64) (res AppGetTopicRes, err error) { + if p.Type == 1 { + return p.getTopicListHomePage() + } + // 获取播单数据 + sId, _ := primitive.ObjectIDFromHex(p.SID) + section, err := modulesectionmod.GetBySectionByID(sId) + if err != nil { + return res, err + } + + res, err = p.getMediaList(uid, section) + common.Go(func() { + t := time.Now() + date := t.Add(-12 * time.Hour).Local().Format("2006-01-02") + sectionstatmod.UpsertSectionHits(sId, date) + }) + return +} + +// 获取专题相关的acg +func (p *AppGetTopicReq) getMediaList(uid uint64, section modulesectionmod.Section) (res AppGetTopicRes, err error) { + var ( + mediaContentIds []primitive.ObjectID + mStatus map[primitive.ObjectID]mediamod.MediaStatus + ) + + mediaContentIds = append(mediaContentIds, section.ID) + + mediaList, hasNext, err := mediadata.GetListFromCache(p.Filter(), int64(p.Skip()), int64(p.Limit()), p.Options()) + if err != nil { + return res, err + } + res.HasNext = hasNext + + //if len(mediaList) <= 0 { + // var wg sync.WaitGroup + // wg.Add(1) + // common.Go(func() { + // defer wg.Done() + // mStatus = GetUserStatus(uid, mediaContentIds) + // }) + // wg.Wait() + // res.Section.MediaStatus = mStatus[section.ID] + // return res, nil + //} + + for _, m := range mediaList { + mediaContentIds = append(mediaContentIds, m.ID) + } + + //var wg sync.WaitGroup + //wg.Add(1) + //common.Go(func() { + // defer wg.Done() + // mStatus = GetUserStatus(uid, mediaContentIds) + //}) + //wg.Wait() + //res.Section.MediaStatus = mStatus[section.ID] + + for _, item := range mediaList { + data := &mediamod.AppMediaBase{ + ID: item.ID, + SortCode: item.SortCode, + Status: item.Status, + Title: item.Title, + Author: item.Anchor, + HorizontalCover: item.HorizontalCover, + VerticalCover: item.VerticalCover, + UpdateStatus: item.UpdateStatus, + Tags: item.Tags, + Summary: item.Summary, + TotalEpisode: item.TotalEpisode, + MediaType: item.MediaType, + MediaSubType: item.MediaSubType, + Kind: item.Kind, + PermissionIconHide: item.PermissionIconHide, + Permission: item.Permission, + Price: item.Price, + Direction: item.Direction, + FreeTime: item.FreeTime, + CountComment: item.CountComment, + CountPurchases: item.CountPurchases, + CountBrowse: item.CountBrowse, + CountCollect: item.CountCollect, + CountLike: item.CountLike, + CountView: item.CountView, + Number: item.Number, + MediaStatus: mStatus[item.ID], + CreatedAt: item.CreatedAt, + UpdateTime: item.UpdateTime, + } + res.List = append(res.List, data) + } + + return res, nil +} + +type AppRecommendReq struct { + TagId string `json:"tagId" form:"tagId"` // 标签id + MediaType string `json:"mediaType" form:"mediaType"` // 媒体类型 "video":视频,"image":图片,"text":小说 + IsAudio bool `json:"isAudio" form:"isAudio"` // 是否是音频小说 + commod.Page +} + +func (p *AppRecommendReq) Filter(media *mediamod.Media) bson.M { + var query = bson.M{ + "mediaType": media.MediaType, + "isDelete": false, + } + return query +} + +func (p *AppRecommendReq) Options() bson.D { + sort := bson.D{{"_id", -1}, {"createdAt", -1}} + return sort +} + +type AppRecommendRes struct { + HasNext bool `json:"hasNext" bson:"hasNext"` // 是否有下一页 + List []*MediaRecommend `json:"list" bson:"list"` // 推荐动漫/小说详情页列表 +} + +type ComicsRecommend struct { + SimilarRecommend []*mediamod.AppMediaBase `json:"similarRecommend"` // 相似推荐 + HotRecommend []*MediaRecommend `json:"hotRecommend"` // 热门推荐 + DiamondFeatured []*MediaRecommend `json:"diamondFeatured"` // 钻石精选 +} + +type MediaRecommend struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + Title string `json:"title" bson:"title"` // 标题 + HorizontalCover string `json:"horizontalCover" bson:"horizontalCover"` // 横版封面 + VerticalCover string `json:"verticalCover" bson:"verticalCover"` // 竖版封面 + Tags []primitive.ObjectID `json:"tags" bson:"tags"` // 标签 + TagDetails []mediamod.TagDetail `json:"tagDetails" bson:"tagDetails"` // 标签对象 + TotalEpisode int `json:"totalEpisode" bson:"totalEpisode"` // 总集数 + UpdateStatus int `json:"updateStatus" bson:"updateStatus"` // 更新状态 1-连载中 2-已完结 + MediaType string `json:"mediaType" bson:"mediaType"` // 媒体类型 "video":视频,"image":图片,"text":小说 + MediaSubType mediamod.MediaSubType `json:"mediaSubType" bson:"mediaSubType"` // 子类型 ,暂时只有小说使用 0-默认文本小说 1-有声小说 + Permission int `json:"permission" bson:"permission"` // 收听权限 0:会员 1:金币购买 2:免费 + Price int64 `json:"price" bson:"price"` // 价格(整部购买的类型 小说 cos 写真) + Direction int64 `json:"direction" bson:"direction"` // 排版方向 0横 1竖(冗余字段) + CurrentEpisode int `json:"currentEpisode" bson:"currentEpisode"` // 当前更新的集数 + Anchor string `json:"anchor" bson:"anchor"` // 作者 +} + +// GetList 获取推荐列表 +func (p *AppRecommendReq) GetList(uid uint64) (res AppRecommendRes, err error) { + tagId, _ := primitive.ObjectIDFromHex(p.TagId) + // 获取动漫/小说推荐 + filter := bson.M{"mediaType": p.MediaType} + if !tagId.IsZero() { + filter["tags"] = tagId + } + if p.IsAudio { + filter["mediaSubType"] = 1 + } + list, hasNext, err := mediadata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), bson.D{{Key: "countLike", Value: -1}, {Key: "createdAt", Value: -1}}) + if err != nil { + return + } + res.HasNext = hasNext + for _, v := range list { + res.List = append(res.List, p.MediaToMediaRecommend(v)) + } + return res, nil +} + +func (p *AppRecommendReq) MediaToMediaRecommend(media *mediamod.Media) *MediaRecommend { + return &MediaRecommend{ + ID: media.ID, + Title: media.Title, + HorizontalCover: media.HorizontalCover, + VerticalCover: media.VerticalCover, + Tags: media.Tags, + TagDetails: media.TagDetails, + TotalEpisode: media.TotalEpisode, + UpdateStatus: media.UpdateStatus, + MediaType: media.MediaType, + MediaSubType: media.MediaSubType, + Permission: media.Permission, + Price: media.Price, + CurrentEpisode: media.CurrentEpisode, + Direction: media.Direction, + Anchor: media.Anchor, + } +} + +type RankingReq struct { + Type int `json:"type" form:"type"` // 1-日榜 2-周榜 3-月榜 4-总榜 5-年榜 + MediaType string `json:"mediaType" form:"mediaType"` // video 动画 image 漫画 text 小说 + commod.Page +} + +type RankingResp struct { + List []*mediamod.AppMediaBase `json:"list"` + HasNext bool `json:"hasNext"` +} + +const ( + DailyRank = 1 // 日榜 + WeekRank = 2 // 周榜 + MonthRank = 3 // 周榜 + AllRank = 4 // 总榜 + YearRank = 5 // 年榜 180天 +) + +func (q *RankingReq) List() (res RankingResp, err error) { + start := time.Now() + defer func() { + t := time.Now().UnixMilli() - start.UnixMilli() + log.Info("RankList", log.Any("ranking.Typ", q.Type), log.Any("耗时", fmt.Sprintf("%v ms", t))) + }() + switch q.Type { + case DailyRank, WeekRank, MonthRank: + // 日榜 + res, err = q.getRank(q.Type, q.MediaType) + + case AllRank, YearRank: + res, err = q.getAllRank(q.MediaType, q.Type == YearRank) + } + return +} + +func (q *RankingReq) getAllRank(mediaType string, isYear bool) (res RankingResp, err error) { + // 通过id获取media + filter := bson.M{"mediaType": mediaType, "status": 1, "isDelete": false} + if isYear { + filter["createdAt"] = bson.M{"$gte": timeutil.BeginningOfDay(time.Now())} + } + mediaList, hasNext, err := mediadata.GetListFromCache(filter, int64(q.Skip()), int64(q.Limit()), bson.D{{Key: "countBrowse", Value: -1}}) + if err != nil { + return + } + + res.List = FillNotStatusMedias(mediaList, false) + res.HasNext = hasNext + return res, nil +} + +func (q *RankingReq) getRank(rankinType int, mediaType string) (res RankingResp, err error) { + var list []string + var hasNext bool + switch rankinType { + case DailyRank: + list, _, hasNext = dailytop.GetTopByPage(topser.TypeMedia(mediaType), int64(q.Skip()), int64(q.Limit())) + case WeekRank: + list, _, hasNext = weektop.GetTopByPage(topser.TypeMedia(mediaType), int64(q.Skip()), int64(q.Limit())) + case MonthRank: + list, _, hasNext = monthtop.GetTopByPage(topser.TypeMedia(mediaType), int64(q.Skip()), int64(q.Limit())) + default: + return + } + + ids := []primitive.ObjectID{} + for _, v := range list { + id, _ := primitive.ObjectIDFromHex(v) + if id.IsZero() { + continue + } + ids = append(ids, id) + } + // 通过id获取media + mediaList, _, err := mediadata.GetListFromCache(bson.M{"_id": bson.M{"$in": ids}}, 0, int64(q.Limit()), bson.D{}) + if err != nil { + return + } + mediaMap := make(map[primitive.ObjectID]*mediamod.Media) + for _, v := range mediaList { + mediaMap[v.ID] = v + } + for _, id := range ids { + item, ok := mediaMap[id] + if !ok { + topasist.Remove(topser.TypeMedia(mediaType), id.Hex()) + continue + } + if item.Status != 1 { + topasist.Remove(topser.TypeMedia(mediaType), id.Hex()) + } + res.List = append(res.List, FillNotStatusMedias([]*mediamod.Media{item}, false)...) + } + res.HasNext = hasNext + return res, nil + +} + +type HotReq struct { + Type int `json:"type" form:"type"` // 0-最新热播 1-本月最热 2-上月最热 + MediaType string `json:"mediaType" form:"mediaType"` // video 动画(默认不填显示这个) image 漫画 text 小说 + commod.Page +} + +type HotResp struct { + List []*mediamod.AppMediaBase `json:"list"` + HasNext bool `json:"hasNext"` +} + +func (p *HotReq) List() (resp HotResp, err error) { + filter := bson.M{ + "status": 1, + "isDelete": false, + "mediaType": "video", // 默认展示动漫 + } + now := time.Now() + year, month, day := now.Date() + // 获取今日最后一刻的时间,这里不能直接使用now去做筛选条件,不然根据这些筛选条件生成的redis key,会一直变,导致一直没有命中缓存 + now = time.Date(year, month, day, 23, 59, 59, 0, now.Location()) + // 默认最新热播 + start := now.AddDate(0, 0, -7) + end := now + switch p.Type { + case 1: + // 本月最热 + start = now.AddDate(0, -1, 0) + case 2: + // 上月最热 + start = now.AddDate(0, -2, 0) + end = now.AddDate(0, -1, 0) + default: + // 判断是否是本周三,再往前多查7天 + if carbon.Now().DayOfWeek() <= 3 { + start = now.AddDate(0, 0, -14) + } + } + if p.MediaType != "" { + filter["mediaType"] = p.MediaType + } + filter["createdAt"] = bson.M{"$gte": start, "$lt": end} + + mediaList, hasNext, err := mediadata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), bson.D{{Key: "countCollect", Value: -1}}) + if err != nil { + return + } + for _, item := range mediaList { + tmp := &mediamod.AppMediaBase{ + ID: item.ID, + SortCode: item.SortCode, + Status: item.Status, + Title: item.Title, + Author: item.Anchor, + HorizontalCover: item.HorizontalCover, + VerticalCover: item.VerticalCover, + Tags: item.Tags, + Summary: item.Summary, + TotalEpisode: item.TotalEpisode, + MediaType: item.MediaType, + MediaSubType: item.MediaSubType, + Kind: item.Kind, + PermissionIconHide: item.PermissionIconHide, + Permission: item.Permission, + Price: item.Price, + Direction: item.Direction, + FreeTime: item.FreeTime, + CountComment: item.CountComment, + CountPurchases: item.CountPurchases, + CountBrowse: item.CountBrowse, + CountCollect: item.CountCollect, + CountLike: item.CountLike, + CountView: item.CountView, + Number: item.Number, + CreatedAt: item.CreatedAt, + UpdateTime: item.UpdateTime, + } + resp.List = append(resp.List, tmp) + } + resp.HasNext = hasNext + return +} + +type MyBuyReq struct { + MediaType string `json:"mediaType" form:"mediaType"` // video 动画 image 漫画 text 小说 drama 短剧 + commod.Page +} + +type MyBuyResp struct { + List []*mediamod.AppMediaBase `json:"list"` + HasNext bool `json:"hasNext"` + Total int64 `json:"total"` +} + +func (p *MyBuyReq) List(uid uint64) (resp MyBuyResp, err error) { + if p.MediaType == mediamod.MediaTypeDrama { + buyList, total, hasNext, queryErr := media_buy_record_mod.GetMyBuyMediaIDs(uid, p.MediaType, p.Skip(), p.Limit()) + if queryErr != nil { + return resp, queryErr + } + ids := make([]primitive.ObjectID, 0, len(buyList)) + for _, item := range buyList { + ids = append(ids, item.MediaID) + } + _, mediaMap, queryErr := mediamod.GetListByIds(ids) + if queryErr != nil { + return resp, queryErr + } + ordered := make([]*mediamod.Media, 0, len(ids)) + for _, id := range ids { + media, ok := mediaMap[id] + if !ok || media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete { + continue + } + copy := media + ordered = append(ordered, ©) + } + resp.List = FillMedias(ordered, uid, true) + if resp.List == nil { + resp.List = make([]*mediamod.AppMediaBase, 0) + } + resp.Total, resp.HasNext = total, hasNext + return resp, nil + } + res, hasNext, err := media_buy_record_mod.GetMyBuy(uid, p.MediaType, p.Skip(), p.Limit()) + if err != nil { + return + } + ids := []primitive.ObjectID{} + for _, v := range res { + ids = append(ids, v.MediaId) + } + mediaList, _, err := mediamod.GetListByIds(ids) + if err != nil { + return + } + for _, item := range mediaList { + tmp := &mediamod.AppMediaBase{ + ID: item.ID, + SortCode: item.SortCode, + Status: item.Status, + Title: item.Title, + Author: item.Anchor, + HorizontalCover: item.HorizontalCover, + VerticalCover: item.VerticalCover, + Tags: item.Tags, + Summary: item.Summary, + TotalEpisode: item.TotalEpisode, + MediaType: item.MediaType, + MediaSubType: item.MediaSubType, + Kind: item.Kind, + PermissionIconHide: item.PermissionIconHide, + Permission: item.Permission, + Price: item.Price, + Direction: item.Direction, + FreeTime: item.FreeTime, + CountComment: item.CountComment, + CountPurchases: item.CountPurchases, + CountBrowse: item.CountBrowse, + CountCollect: item.CountCollect, + CountLike: item.CountLike, + CountView: item.CountView, + Number: item.Number, + CreatedAt: item.CreatedAt, + UpdateTime: item.UpdateTime, + } + resp.List = append(resp.List, tmp) + } + resp.HasNext = hasNext + return +} + +type ( + AppGetMediaListByTagReq struct { + commod.Page + TagId string `form:"tagId" json:"tagId"` + SortType int `form:"sortType" json:"sortType"` // 排序 1-最新 2-最热 3-最多收藏 + } + AppGetMediaListByTagRep = AppListRes +) + +type TagMediaListReq struct { + TagID string `form:"tagID" json:"tagID" binding:"required"` // 标签id + SortType int `form:"sortType" json:"sortType" binding:"required"` //排序类型 1最多播放,2、最新,3、 最多收藏 " + commod.Page +} + +type TagMediaListResp struct { + HasNext bool `json:"hasNext"` + List []*mediamod.AppMediaBase `json:"list"` +} + +func (p *TagMediaListReq) GetList() (resp TagMediaListResp, err error) { + tagId, _ := primitive.ObjectIDFromHex(p.TagID) + if tagId.IsZero() { + return + } + filter := bson.M{ + "tags": tagId, + "isDelete": false, + "status": 1, + } + + sort := bson.D{{"createdAt", -1}} + switch p.SortType { + case 1: + sort = bson.D{{"countBrowse", -1}, {"createdAt", -1}} + case 2: + sort = bson.D{{"createdAt", -1}} + case 3: + sort = bson.D{{"countCollect", -1}, {"createdAt", -1}} + case 4: + sort = bson.D{{"hot", -1}, {"createdAt", -1}} + } + // 获取列表 + var data []*mediamod.Media + data, resp.HasNext, err = mediadata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + return + } + resp.List = FillNotStatusMedias(data, false) + + return resp, nil +} diff --git a/app/service/mediaser/media_sort_test.go b/app/service/mediaser/media_sort_test.go new file mode 100644 index 0000000..3afc543 --- /dev/null +++ b/app/service/mediaser/media_sort_test.go @@ -0,0 +1,64 @@ +package mediaser + +import ( + "reflect" + "testing" + + "91porn-server/common/elastic" + "91porn-server/models/v/mediamod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestHandleShortSortMapping(t *testing.T) { + tests := []struct { + name string + sortType int + field string + }{ + {name: "默认最新", sortType: 0, field: "updateTime"}, + {name: "精选", sortType: 1, field: "choiceSort"}, + {name: "最新上架", sortType: 2, field: "updateTime"}, + {name: "最热", sortType: 3, field: "countLike"}, + {name: "最多收藏", sortType: 4, field: "countCollect"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + wantElastic := elastic.A{{tt.field: elastic.M{"order": "desc"}}} + if got := HandleShort(tt.sortType); !reflect.DeepEqual(got, wantElastic) { + t.Fatalf("HandleShort(%d) = %#v, want %#v", tt.sortType, got, wantElastic) + } + + wantMongo := bson.D{{Key: tt.field, Value: -1}} + request := mediamod.AppLibraryReq{SortType: tt.sortType} + if got := request.Options(); !reflect.DeepEqual(got, wantMongo) { + t.Fatalf("AppLibraryReq.Options(%d) = %#v, want %#v", tt.sortType, got, wantMongo) + } + }) + } +} + +func TestOrderMediaByIDsPreservesElasticSearchOrder(t *testing.T) { + firstID := primitive.NewObjectID() + secondID := primitive.NewObjectID() + thirdID := primitive.NewObjectID() + + mongoResult := []*mediamod.Media{ + {ID: secondID}, + {ID: thirdID}, + {ID: firstID}, + } + elasticSearchOrder := []primitive.ObjectID{firstID, secondID, thirdID} + + got := orderMediaByIDs(mongoResult, elasticSearchOrder) + if len(got) != len(elasticSearchOrder) { + t.Fatalf("orderMediaByIDs() length = %d, want %d", len(got), len(elasticSearchOrder)) + } + for i, wantID := range elasticSearchOrder { + if got[i].ID != wantID { + t.Fatalf("orderMediaByIDs()[%d].ID = %s, want %s", i, got[i].ID.Hex(), wantID.Hex()) + } + } +} diff --git a/app/service/mediaser/type.go b/app/service/mediaser/type.go new file mode 100644 index 0000000..2908b25 --- /dev/null +++ b/app/service/mediaser/type.go @@ -0,0 +1,9 @@ +package mediaser + +type libraryType string + +const ( + Video = "video" // 动漫 + Image = "image" // 漫画 + AudioBook = "audiobook" // 有声小说 +) diff --git a/app/service/mediatagser/mediatag.go b/app/service/mediatagser/mediatag.go new file mode 100755 index 0000000..dc849a6 --- /dev/null +++ b/app/service/mediatagser/mediatag.go @@ -0,0 +1,204 @@ +package mediatagser + +import ( + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "encoding/json" + "time" + + "91porn-server/models/cache/mediatagdata" + "91porn-server/models/commod" + "91porn-server/models/v/mediatagmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// MediaTagInfo 移动端返回内容 +type MediaTagInfo struct { + ID primitive.ObjectID `json:"id"` // 文档id + Kind int `json:"kind"` // 种类(动漫 漫画) + Dimension string `json:"dimension"` // 维度 + Name string `json:"name"` // 标签名称 + Active bool `json:"active"` // 是都激活 + Sort int `json:"sort"` // 排序码 + IsDelete bool `json:"isDelete"` // 是否删除 + UpdatedAct string `json:"updatedAct"` // 操作用户账号 + CreatedAt time.Time `json:"createdAt"` // 文档创建时间 + UpdateTime time.Time `json:"updateTime"` // 文档更新时间 +} + +type AppQueryListReq struct { + commod.Page +} + +type AppListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []MediaTagInfo `json:"list"` +} + +// GetList 获取列表 +func (p *AppQueryListReq) GetList() AppListRes { + var res AppListRes + var err error + filter := bson.M{} + + sort := bson.D{{"_id", -1}} + // 获取列表 + var data []mediatagmod.MediaTag + data, res.Total, res.HasNext, err = mediatagdata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + return res + } + // 组装返回数据 + for _, item := range data { + tmp := MediaTagInfo{ + ID: item.ID, + Kind: item.Kind, + Dimension: item.Dimension, + Name: item.Name, + Active: item.Active, + Sort: item.Sort, + IsDelete: item.IsDelete, + UpdatedAct: item.UpdatedAct, + CreatedAt: item.CreatedAt, + UpdateTime: item.UpdateTime, + } + res.List = append(res.List, tmp) + } + + return res +} + +type AppQueryInfoReq struct { + ID string `json:"id" form:"id"` // id +} + +// GetInfo 获取详情 +func (p *AppQueryInfoReq) GetInfo() (res MediaTagInfo, err error) { + oid, err := primitive.ObjectIDFromHex(p.ID) + if err != nil { + return res, err + } + + var item mediatagmod.MediaTag + item, err = mediatagdata.GetInfoFromCache(oid) + if err != nil { + return + } + + res = MediaTagInfo{ + ID: item.ID, + Kind: item.Kind, + Dimension: item.Dimension, + Name: item.Name, + Active: item.Active, + Sort: item.Sort, + IsDelete: item.IsDelete, + UpdatedAct: item.UpdatedAct, + CreatedAt: item.CreatedAt, + UpdateTime: item.UpdateTime, + } + + return +} + +// GetTagsByIDs2Map 通过tagid列表获取tag详情列表 +func GetTagsByIDs2Map(ids []primitive.ObjectID) map[primitive.ObjectID]*mediatagmod.MediaTag { + m := make(map[primitive.ObjectID]*mediatagmod.MediaTag) + if len(ids) == 0 { + return m + } + tags, ok := GetTagsByIDs(ids) + if !ok { + return m + } + for i := range tags { + m[tags[i].ID] = tags[i] + } + return m +} + +// GetTagsByIDs 通过tagid列表获取tag详情列表 +func GetTagsByIDs(ids []primitive.ObjectID) ([]*mediatagmod.MediaTag, bool) { + tags, unExistsTid, err := getTagsByIDsFromRedis(ids) + if err != nil || len(unExistsTid) != 0 { + unExistsTInfo, err := mediatagmod.FindTagsByIDs(unExistsTid) + if err != nil { + return nil, false + } + common.Go(func() { + _ = setTagsByIDs2Redis(unExistsTInfo) + }) + tags = append(tags, unExistsTInfo...) + } + return tags, true +} + +func getTagsByIDsFromRedis(ids []primitive.ObjectID) ([]*mediatagmod.MediaTag, []primitive.ObjectID, error) { + sInfo := getTidKey(ids) + uInfo, err := appg.Redis.MGet(sInfo...) + if err != nil { + log.Error("getTagsByIDsFromRedis error", log.Any("ids", ids), log.Any("sInfo", sInfo), log.Any("err", err)) + return nil, ids, err + } + tagInfo := parseTagInfo(uInfo) + unExist := getUnExistTID(ids, tagInfo) + return tagInfo, unExist, nil +} + +func setTagsByIDs2Redis(tags []*mediatagmod.MediaTag) error { + expire := redisconst.MediaTagInfoExpire() + for _, t := range tags { + d, err := json.Marshal(t) + if err != nil { + continue + } + s := redisconst.MediaTagInfoKey(t.ID.Hex()) + _ = appg.Redis.Set(s, string(d), expire) + } + return nil +} + +func getUnExistTID(ids []primitive.ObjectID, info []*mediatagmod.MediaTag) []primitive.ObjectID { + m := make(map[primitive.ObjectID]bool) + var unExist []primitive.ObjectID + for _, i := range info { + m[i.ID] = true + } + for _, i := range ids { + if !m[i] { + unExist = append(unExist, i) + } + } + return unExist +} + +func getTidKey(tids []primitive.ObjectID) []string { + var keys []string + for _, t := range tids { + s := redisconst.MediaTagInfoKey(t.Hex()) + keys = append(keys, s) + } + return keys +} + +func parseTagInfo(data []interface{}) []*mediatagmod.MediaTag { + var infos []*mediatagmod.MediaTag + for _, d := range data { + str, ok := d.(string) + if !ok { + continue + } + var t mediatagmod.MediaTag + err := json.Unmarshal([]byte(str), &t) + if err != nil { + continue + } + infos = append(infos, &t) + } + return infos +} diff --git a/app/service/messageser/message.go b/app/service/messageser/message.go new file mode 100644 index 0000000..901a56f --- /dev/null +++ b/app/service/messageser/message.go @@ -0,0 +1,861 @@ +package messageser + +import ( + "91porn-server/app/appg" + "91porn-server/app/middleware/authuser" + "91porn-server/app/service/imser" + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/services/message" + "91porn-server/common/stderr" + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/v/imusermod" + "91porn-server/models/v/messagemod" + "91porn-server/models/v/sessionmod" + "91porn-server/models/v/sysconfmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "91porn-server/models/v/walletmod" + "encoding/json" + "fmt" + "strconv" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// QueryDynamics 查询动态列表 +func QueryDynamics(uid uint64, req *messagemod.QueryCond) ([]messagemod.MsgDynamicsApp, bool, stderr.Code) { + //now := time.Now() + // 处理用户消息提示状态 + _ = message.CancelTip(uid) + //动态类型消息 + var msgType []string + if req.MsgType == 1 { + msgType = []string{"like_msg", "like_comment_msg"} + } else { + msgType = []string{"comment_msg", "reply_comment_msg"} + } + cond := bson.M{"msgType": bson.M{"$in": msgType}, "takeUid": uid} + mData, hasNext, err := messagemod.QueryDynamics(cond, int(req.PageSize), int(req.PageNumber)) + if err != nil { + return nil, false, stderr.ErrDbQueryError + } + if len(mData) > 0 { + //用户查看自己的动态列表, 则消息接受者是查看用户 + uids := []uint64{} + vid := []primitive.ObjectID{} + for _, m := range mData { + uids = append(uids, m.SendUid) + vid = append(vid, m.ObjId) + } + //查询帖子信息 + vidMap, _ := vidmod.VideoMap(vid) + if len(uids) > 0 { + //查询用户信息 + uData, err := usermod.FindUsersMapByUID(uids) + if err != nil { + return mData, false, stderr.ErrDbQueryError + } + //循环获取组装用户信息 + for i, m := range mData { + if uData[m.SendUid] != nil { + mData[i].SendName = uData[m.SendUid].Name + mData[i].SendAvatar = uData[m.SendUid].Portrait + mData[i].SendGender = uData[m.SendUid].Gender + //mData[i].SuperUser = uData[m.SendUid].GetSuperUser(now) + mData[i].SuperUser = 0 + mData[i].Awards = uData[m.SendUid].Awards + mData[i].VipExpireDate = uData[m.SendUid].VipExpireDate + mData[i].VipLevel = uData[m.SendUid].VipLevel + } + if m.MsgType == messagemod.LikeCommentMsg { + mData[i].LikeCount = m.UserCount + } else if m.MsgType == messagemod.LikeMsg { + if video, ok := vidMap[m.ObjId]; ok { + mData[i].LikeCount = video.LikeCount + mData[i].ImgUrl = video.Cover + } + } + } + } + //获取成功后, 将当前动态全部变为已读 + common.Go(func() { + //更新状态 + messagemod.UpdDynamicsIsReadByUid(nil, uid) + //清除redis + appg.Redis.Del(messagemod.NoRedDynamicNumRedisKey + strconv.FormatUint(uid, 10)) + }) + } + return mData, hasNext, stderr.Success +} + +// 查询未读动态数 +func QueryNoRedDynamicNum(uid uint64) (messagemod.NoReadMsgDynamicsCountApp, stderr.Code) { + var ret = messagemod.NoReadMsgDynamicsCountApp{ + DtCount: 0, + MsgCount: 0, + } + //redis限制, 一分钟一次 + key := messagemod.NoRedDynamicNumRedisKey + strconv.FormatUint(uid, 10) + result, err := appg.Redis.Get(key) + if err != nil || result == nil { + mt := messagemod.GetDynamics() + //包含私信 + mt = append(mt, messagemod.PrivateLetterMsg) + //动态类型消息 + cond := bson.M{"msgType": bson.M{"$in": mt}, "takeUid": uid, "isRead": false} + data, err := messagemod.FindByFilter(cond) + if err != nil { + log.Warn(fmt.Sprintf("messageser QueryNoRedDynamicNum messagemod.FindByFilter error:%+v:", err), log.Any("uid", uid)) + return ret, stderr.ErrDbQueryError + } + if len(data) > 0 { + for _, d := range data { + if d.MsgType == messagemod.PrivateLetterMsg { + ret.MsgCount += 1 + } else { + ret.DtCount += 1 + } + } + } + } else { + err := json.Unmarshal([]byte(*result), &ret) + if err != nil { + log.Warn(fmt.Sprintf("messageser QueryNoRedDynamicNum json.Unmarshal error:%+v:", err), log.Any("uid", uid)) + return ret, stderr.Failure + } + return ret, stderr.Success + } + //将对象转为字符串 + jsonstr, err := json.Marshal(ret) + if err != nil { + log.Warn(fmt.Sprintf("messageser QueryNoRedDynamicNum json.Marshal error:%+v:", err), log.Any("uid", uid)) + return ret, stderr.Success + } + //设置redis缓存 + _ = appg.Redis.Set(key, string(jsonstr), time.Minute*1) + return ret, stderr.Success +} + +// QueryPrivateLetterSession 查询私信会话列表 +func QueryPrivateLetterSession(uid uint64, req *sessionmod.QueryCond) ([]sessionmod.SessionApp, bool, stderr.Code) { + // 处理用户消息提示状态 + _ = message.CancelTip(uid) + //查询当前用户的会话 + cond := bson.M{"$or": []bson.M{{"sendUid": uid}, {"takeUid": uid}}} + sData, hasNext, err := sessionmod.Query(cond, req) + if err != nil { + log.Warn(fmt.Sprintf("messageser Add sessionmod.Query error:%+v:", err), log.Any("uid", uid)) + return nil, false, stderr.ErrDbQueryError + } + if len(sData) > 0 { + var uids []uint64 + for i, m := range sData { + //查询未读条数 + count, err := messagemod.Count(bson.M{"sessionId": m.SessionId, "takeUid": uid, "isRead": false}) + if err != nil { + log.Error(fmt.Sprintf("messageser Add messagemod.Count error:%+v:", err), log.Any("sessionId", m.ID.Hex()), log.Any("uid", uid)) + continue + } + sData[i].NoReadNum = count + //sendId是当前用户,则会话是有当前用户主动发起的,反之,对方用户主动发起 + //注意, 会话列表用来展示对方用户头像姓名, 所有这里取对方用户信息 + if uid == m.SendUid { + uids = append(uids, m.TakeUid) + continue + } + if uid == m.TakeUid { + uids = append(uids, m.SendUid) + } + } + if len(uids) > 0 { + //查询用户信息 + uData, err := usermod.FindUsersMapByUID(uids) + if err != nil { + log.Warn(fmt.Sprintf("messageser Add usermod.FindUsersMapByUID error:%+v:", err), log.Any("uids", uids), log.Any("uid", uid)) + return sData, false, stderr.ErrDbQueryError + } + imByUID, _ := imusermod.IMUserIDMapByUIDs(uids) // uid → imUserId(imusermod 为准) + //循环获取组装用户信息 + for i, m := range sData { + if uid == m.SendUid && uData[m.TakeUid] != nil { + peer := uData[m.TakeUid] + sData[i].UserId = peer.UID + sData[i].PeerImUserID = imByUID[peer.UID] + sData[i].UserName = peer.Name + sData[i].UserAvatar = peer.Portrait + continue + } + if uid == m.TakeUid && uData[m.SendUid] != nil { + peer := uData[m.SendUid] + sData[i].UserId = peer.UID + sData[i].PeerImUserID = imByUID[peer.UID] + sData[i].UserName = peer.Name + sData[i].UserAvatar = peer.Portrait + } + } + } + } + return sData, hasNext, stderr.Success +} + +// DelSession 删除私信会话 +func DelSession(uid uint64, sessionId string) stderr.Code { + //查询会话是否存在 + session, err := sessionmod.FindOneByFilter(bson.M{"sessionId": sessionId}) + if err != nil { + log.Warn(fmt.Sprintf("messageser DelSession sessionmod.FindOneByFilter error:%+v:", err), log.Any("uid", uid), log.Any("sessionId", sessionId)) + return stderr.ErrDbQueryError + } + if session.ID.IsZero() { + log.Warn("messageser DelSession session is null", log.Any("uid", uid), log.Any("sessionId", sessionId)) + return stderr.Failure + } + if session.SendUid != uid && session.TakeUid != uid { + log.Warn("messageser DelSession session.SendUid != uid && session.TakeUid != uid", log.Any("uid", uid), log.Any("sessionId", sessionId)) + return stderr.Failure + } + //删除 + err = sessionmod.Delete(bson.M{"sessionId": sessionId}) + if err != nil { + log.Warn(fmt.Sprintf("messageser DelSession sessionmod.Delete error:%+v:", err), log.Any("uid", uid), log.Any("sessionId", sessionId)) + return stderr.ErrDbDeleteError + } + //删除会话对应的消息 + _, err = messagemod.DeleteMsgBySessionId(sessionId) + if err != nil { + log.Warn(fmt.Sprintf("messageser DelSession messagemod.DeleteMsgBySessionId error:%+v:", err), log.Any("uid", uid), log.Any("sessionId", sessionId)) + return stderr.ErrDbDeleteError + } + return stderr.Success +} + +// GetSessionId 获取sessionId +func GetSessionId(uid, takeUid uint64) string { + //会话id + sessionId := "" + if uid == takeUid { + log.Warn(fmt.Sprintf("messageser GetSessionId uid == takeUid"), log.Any("uid", uid), log.Any("takeUid", takeUid)) + return sessionId + } + //先查询是否有会话信息 + cond := bson.M{"$or": []bson.M{bson.M{"sendUid": uid, "takeUid": takeUid}, bson.M{"sendUid": takeUid, "takeUid": uid}}} + session, err := sessionmod.FindOneByFilter(cond) + if err != nil { + log.Warn(fmt.Sprintf("messageser GetSessionId sessionmod.FindOneByFilter error:%+v:", err), log.Any("uid", uid)) + return sessionId + } + if session.ID.IsZero() { + sessionId = common.UUID() + //新增session + s := &sessionmod.Session{ + SendUid: uid, + TakeUid: takeUid, + SessionId: sessionId, + CreatedAt: time.Now(), + } + err := sessionmod.InsertOne(nil, s) + if err != nil { + log.Warn(fmt.Sprintf("messageser GetSessionId sessionmod.InsertOne error:%+v:", err), log.Any("session", s), log.Any("uid", uid), log.Any("takeUid", takeUid)) + return "" + } + } else { + sessionId = session.SessionId + } + return sessionId +} + +// QueryPrivateLetterMsg 查询私信聊天列表 +func QueryPrivateLetterMsg(uid uint64, req *messagemod.QueryMsgCond) ([]messagemod.MessageApp, bool, stderr.Code) { + //私信类型消息 + msgTypes := [...]string{string(messagemod.PrivateLetterMsg), string(messagemod.OfficialPrivateLetterMsg)} + cond := bson.M{"msgType": bson.M{"$in": msgTypes}, "sessionId": req.SessionId} + mData, hasNext, err := messagemod.Query(cond, int(req.PageSize), int(req.PageNumber)) + if err != nil { + return nil, false, stderr.ErrDbQueryError + } + if len(mData) > 0 { + uids := []uint64{} + for _, m := range mData { + uids = append(uids, m.SendUid) + uids = append(uids, m.TakeUid) + } + if len(uids) > 0 { + //查询用户信息 + uData, err := usermod.FindUsersMapByUID(uids) + if err != nil { + return mData, false, stderr.ErrDbQueryError + } + //循环获取组装用户信息 + for i, m := range mData { + if sendUser := uData[m.SendUid]; sendUser != nil { + mData[i].SendName = sendUser.Name + mData[i].SendAvatar = sendUser.Portrait + } + if takeUser := uData[m.TakeUid]; takeUser != nil { + mData[i].TakeName = takeUser.Name + mData[i].TakeAvatar = takeUser.Portrait + } + peerUID := m.SendUid + if uid == m.SendUid { + peerUID = m.TakeUid + } + if peer := uData[peerUID]; peer != nil { + mData[i].PeerImUserID = imusermod.IMUserIDByUID(peer.UID) + } + } + } + } + //请求列表时,将当前用户当前会话所有未读消息更新为已读 + common.Go(func() { + messagemod.UpdIsReadBySessionIdAndUid(nil, uid, req.SessionId) + }) + return mData, hasNext, stderr.Success +} + +// AddPrivateLetter 新增私信消息 +func AddPrivateLetter(uid uint64, req messagemod.AddMsgReqInfo) stderr.Code { + data, err := usermod.FindUserByUID(uid) + if err != nil || data == nil { + log.Warn(fmt.Sprintf("messageser Add send user is null"), log.Any("uid", uid), log.Any("takeUid", req.TakeUid)) + return stderr.CommentUserInfoErr + } + if data.HasBanned { + return stderr.CommentUserHasBanned + } + if uid == req.TakeUid { + log.Warn(fmt.Sprintf("messageser Add user cant self add msg"), log.Any("uid", uid), log.Any("takeUid", req.TakeUid)) + return stderr.NoSelfAddMsg + } + //校验私信用户是否vip + //if !data.IsVIP(time.Now()) { + // return stderr.Success + //} + ////校验vip是否是充值过的,没有充值则不允许私信 + //if uid > 150000 && !walletmod.IsRechargeUser(nil, uid) { + // log.Warn(fmt.Sprintf("messageser Add user no recharge"), log.Any("uid", uid), log.Any("takeUid", req.TakeUid), log.Any("content", req.Content)) + // return stderr.Success + //} + price := CheckChatPrice(data) + + wallet, err := walletmod.GetWallet(uid) + if err != nil { + return stderr.ErrParamError + } + + if price > 0 && (wallet == nil || wallet.ID.IsZero()) { + return stderr.InsufficientBalance + } + + checkUserBalance, deAmt, deIncome := CheckUserBalance(wallet, price) + if !checkUserBalance { + return stderr.InsufficientBalance + } + //if wallet.Amount+wallet.Income<=0 || wallet.Amount+wallet.Income + + fengjin := false + //查询被私信的用户是否存在 + takeUser, err := usermod.FindUserByUID(req.TakeUid) + if err != nil || takeUser == nil { + fengjin = true + } + //校验用户请求频率,5秒内超过5次,则封禁 + //key := "add-private-letter-" + strconv.FormatUint(uid, 10) + //ret, err := appg.Redis.Get(key) + //if !fengjin && err == nil { + // var i int = 0 + // if ret != nil { + // i, _ = strconv.Atoi(*ret) + // } + // if i > 5 { + // //封禁 + // fengjin = true + // } else { + // i += 1 + // appg.Redis.Set(key, strconv.Itoa(i), time.Second*5) + // } + //} + //检测私信内容是否包含网址 + //if truthutil.CheckStrIsUrl(req.Content) { + // fengjin = true + // //这里直接返回成功,让广告用户误以为成功了,免得一直来麻烦 + // log.Warn("messageser Add checkStrIsUrl true", log.Any("uid", uid), log.Any("takeUid", req.TakeUid), log.Any("content", req.Content)) + // return stderr.Success + //} + if fengjin { + common.Go(func() { + log.Warn("messageser Add takeUser == nil", log.Any("uid", uid), log.Any("takeUid", req.TakeUid), log.Any("content", req.Content)) + //如果用户不存在, 则证明是脚本在刷私信, 封禁该用户 + hasLocked := true + lockReason := "脚本打广告的用户" + doc := usermod.UserSelector{ + HasLocked: &hasLocked, + LockReason: &lockReason, + } + usermod.Update(uid, doc) + //删除token + authuser.RevokeTokenCache(uid) + }) + return stderr.CommentUserHasBanned + } + //会话id + sessionId := "" + //新增私聊信息之前, 先查询是否有会话信息 + cond := bson.M{"$or": []bson.M{bson.M{"sendUid": uid, "takeUid": req.TakeUid}, bson.M{"sendUid": req.TakeUid, "takeUid": uid}}} + session, err := sessionmod.FindOneByFilter(cond) + if err != nil { + log.Warn(fmt.Sprintf("messageser Add sessionmod.FindOneByFilter error:%+v:", err), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + t := time.Now() + if session.ID.IsZero() { + sessionId = common.UUID() + //新增session + s := &sessionmod.Session{ + SendUid: uid, + TakeUid: req.TakeUid, + SessionId: sessionId, + CreatedAt: t, + UpdatedAt: t, + } + err := sessionmod.InsertOne(nil, s) + if err != nil { + log.Warn(fmt.Sprintf("messageser Add sessionmod.InsertOne error:%+v:", err), log.Any("session", s), log.Any("uid", uid), log.Any("takeUid", req.TakeUid)) + return stderr.ErrDbInsertError + } + } else { + sessionId = session.SessionId + } + + if deAmt > 0 || deIncome > 0 { + // 扣除用户余额 + plan := &walletmod.DebitPlan{ + Amount: deAmt, + Income: deIncome, + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + wallet, err := walletmod.Debit(t, plan, uid) + if err != nil { //扣钱 + return err + } + + var txnLogs []txnmod.TransactionLog + if deAmt > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: uid, + Amount: -deAmt, + ActualAmount: float64(-deAmt), + TranType: txnmod.SendMsgDebitAmountGold.Key(), + TranTypeInt: int64(txnmod.SendMsgDebitAmountGoldReturn), + Desc: fmt.Sprintf("用户私信扣除%v个金币", deAmt), + SysType: data.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if deIncome > 0 { + if err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + UID: uid, + Amount: -deIncome, + ActualAmount: float64(-deIncome), + TranType: txnmod.SendMsgDebitIncomeGold.Key(), + TranTypeInt: int64(txnmod.SendMsgDebitIncomeGold), + Desc: fmt.Sprintf("用户私信扣除收益%v个金币", deIncome), + SysType: data.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + }); err != nil { + return stderr.ErrDbQueryError + } + } + + if len(txnLogs) > 0 { + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + log.Warn(fmt.Sprintf("productser BuyVIP Transaction err %s", err.Error())) + return err + } + } + return nil + }); err != nil { + log.Warn(fmt.Sprintf("send meaasge Transaction err %s", err.Error())) + return stderr.ErrDbUpdateError //通知消息 + } + } + + if err = imser.SendPrivateMessage(uid, req.TakeUid, req.Content, req.ImgUrl); err != nil { + imser.LogSDKSendError(uid, req.TakeUid, err) + return stderr.Failure + } + + code := PublicAddMsg(messagemod.PublicAddMsg{ + SendUid: uid, + TakeUid: req.TakeUid, + SessionId: sessionId, + Content: req.Content, + ImgUrl: req.ImgUrl, + MsgType: messagemod.PrivateLetterMsg, + }) + if code != stderr.Success { + return code + } + common.Go(func() { + //更新会话预览消息 + content := req.Content + if len(req.ImgUrl) > 0 && content == "" { + content = "发送了一张图片" + } + err := sessionmod.UpdOneByFilter(nil, bson.M{"sessionId": sessionId}, bson.M{"$set": bson.M{"preContent": content, "updatedAt": t}}) + if err != nil { + log.Warn(fmt.Sprintf("messageser Add messagemod.UpdOneByFilter error:%+v:", err), log.Any("sessionId", sessionId), log.Any("preContent", req.Content), log.Any("uid", uid)) + } + }) + return stderr.Success +} + +// SendIMPrivateLetter 走第三方 IM 通道发送私信: +// 校验发送/接收方 → 校验余额 → 扣费(含 txn log)→ 调用 imser.SendPrivateMessage 走 IM SDK 投递。 +func SendIMPrivateLetter(uid uint64, req messagemod.AddMsgReqInfo) stderr.Code { + sender, err := usermod.FindUserByUID(uid) + if err != nil || sender == nil { + log.Warn("messageser SendIMPrivateLetter sender not found", + log.Any("uid", uid), log.Any("takeUid", req.TakeUid)) + return stderr.CommentUserInfoErr + } + if sender.HasBanned { + return stderr.CommentUserHasBanned + } + if uid == req.TakeUid { + return stderr.NoSelfAddMsg + } + takeUser, err := usermod.FindUserByUID(req.TakeUid) + if err != nil || takeUser == nil { + log.Warn("messageser SendIMPrivateLetter take user not found", + log.Any("uid", uid), log.Any("takeUid", req.TakeUid)) + return stderr.UserIsNotExists + } + + price := CheckChatPrice(sender) + if uid != 100001 && price > 0 { + wallet, err := walletmod.GetWallet(uid) + if err != nil { + return stderr.ErrParamError + } + if wallet == nil || wallet.ID.IsZero() { + return stderr.InsufficientBalance + } + ok, deAmt, deIncome := CheckUserBalance(wallet, price) + if !ok { + return stderr.InsufficientBalance + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + w, err := walletmod.Debit(t, &walletmod.DebitPlan{Amount: deAmt, Income: deIncome}, uid) + if err != nil { + return err + } + var logs []txnmod.TransactionLog + if deAmt > 0 { + logs = append(logs, txnmod.TransactionLog{ + UID: uid, + Amount: -deAmt, + ActualAmount: float64(-deAmt), + TranType: txnmod.SendMsgDebitAmountGold.Key(), + TranTypeInt: int64(txnmod.SendMsgDebitAmountGoldReturn), + Desc: fmt.Sprintf("用户私信(IM)扣除%v个金币", deAmt), + SysType: sender.SysType, + RealAmount: w.RealAmount(), + }) + } + if deIncome > 0 { + logs = append(logs, txnmod.TransactionLog{ + UID: uid, + Amount: -deIncome, + ActualAmount: float64(-deIncome), + TranType: txnmod.SendMsgDebitIncomeGold.Key(), + TranTypeInt: int64(txnmod.SendMsgDebitIncomeGold), + Desc: fmt.Sprintf("用户私信(IM)扣除收益%v个金币", deIncome), + SysType: sender.SysType, + RealAmount: w.RealAmount(), + }) + } + if len(logs) > 0 { + if err = txnmod.InsertManyTransactionLog(t, logs); err != nil { + return err + } + } + return nil + }); err != nil { + log.Warn("messageser SendIMPrivateLetter charge tx fail", + log.Any("uid", uid), log.Any("takeUid", req.TakeUid), log.E(err)) + return stderr.ErrDbUpdateError + } + } + + if err := imser.SendPrivateMessage(uid, req.TakeUid, req.Content, req.ImgUrl); err != nil { + log.Error("messageser SendIMPrivateLetter imser.SendPrivateMessage fail", + log.Any("uid", uid), log.Any("takeUid", req.TakeUid), log.E(err)) + return stderr.Failure + } + return stderr.Success +} + +func CheckChatPrice(u *usermod.User) int64 { + configure, _ := sysconfdata.GetAllFromCache() + if u == nil { + return configure.GetInt(sysconfmod.SendMsgPrice) + } + var price int64 + // 校验私信用户是否vip + isVip := u.IsPaidVIP() + if isVip && u.ChatPrice > 0 { + price = u.ChatPrice + } else if isVip && u.ChatPrice < 0 { + // vip私信价格设置为负数的时候,则说明不需要钱(因为旧的用户数据这个字段都是0,所以都得读默认配置) + price = 0 + } else { + // 非vip用户或者是旧的vip用户的u.ChatPrice字段为0的时候,都是默认读配置 + price = configure.GetInt(sysconfmod.SendMsgPrice) + } + return price +} + +func CheckUserBalance(w *walletmod.Wallet, price int64) (checkStatus bool, deAmt, deIncome int64) { + totalAmt := w.Amount + w.Income + var dAmt, dIncome int64 + if totalAmt < price { + return false, dAmt, dIncome + } + + if totalAmt >= price && w.Amount >= price { + dAmt = price + } + + if totalAmt >= price && w.Amount < price { + dAmt = w.Amount + dIncome = price - w.Amount + } + + return true, dAmt, dIncome + +} + +// ReadMsg 阅读消息 +func ReadMsg(uid uint64, req messagemod.ReadMsgReqInfo) stderr.Code { + if len(req.MsgIds) < 0 { + log.Error(fmt.Sprintf("messageser readMsg req.MsgIds <= 0"), log.Any("uid", uid)) + return stderr.Success + } + var ids []primitive.ObjectID + for _, v := range req.MsgIds { + id, _ := primitive.ObjectIDFromHex(v) + ids = append(ids, id) + } + if len(ids) < 0 { + log.Error(fmt.Sprintf("messageser readMsg ids <= 0"), log.Any("uid", uid)) + return stderr.Success + } + //更新消息已读状态 + err := messagemod.UpdIsRead(nil, uid, ids) + if err != nil { + log.Error(fmt.Sprintf("messageser messagemod.UpdIsRead error:%+v:", err), log.Any("ids", ids), log.Any("uid", uid)) + return stderr.ErrDbUpdateError + } + return stderr.Success +} + +// DynamicMsgUserGroup 同一obj下的相同操作归类相同用户 +func DynamicMsgUserGroup(params messagemod.PublicAddMsg) stderr.Code { + //查询用户信息 + user, err := usermod.FindUserByUID(params.SendUid) + if err != nil { + return stderr.ErrDbQueryError + } + if user == nil || user.HasLocked { + return stderr.UserIsNotExists + } + + filter := bson.M{"takeUid": params.TakeUid, "msgType": params.MsgType, "objId": params.ObjId} + msg, err := messagemod.FindOneByFilter(filter) + if err != nil || msg.ID.IsZero() { + params.UserCount = 1 + params.UserGroup = []messagemod.UserInfo{ + { + Uid: user.UID, + Name: user.Name, + Avatar: user.Portrait, + }, + } + // 发布消息并返回 + return PublicAddMsg(params) + } + + ug := msg.UserGroup + for _, v := range ug { + // 已经有记录直接返回 + if v.Uid == params.SendUid { + return stderr.Success + } + } + // 取前9个用户 + 当前用户 + if len(ug) >= 10 { + ug = ug[:9] + } + ug = append([]messagemod.UserInfo{{ + Uid: user.UID, + Name: user.Name, + Avatar: user.Portrait, + }}, ug...) + + set := bson.M{"userCount": msg.UserCount + 1, "updatedAt": time.Now(), "userGroup": ug} + err = messagemod.UpdOneByFilter(nil, bson.M{"_id": msg.ID}, bson.M{"$set": set}) + if err != nil { + log.Error(fmt.Sprintf("PublicMsgUserGroup error:%+v:", err), log.Any("set", set), log.Any("msgID", msg.ID)) + return stderr.ErrDbQueryError + } + return stderr.Success +} + +// PublicAddMsg 添加消息公共方法 +func PublicAddMsg(prams messagemod.PublicAddMsg) stderr.Code { + //查询用户信息 + user, err := usermod.FindUserByUID(prams.SendUid) + if err != nil { + return stderr.ErrDbQueryError + } + if user == nil || user.HasLocked { + return stderr.UserIsNotExists + } + now := time.Now() + //新增消息 + msg := &messagemod.Message{ + SendUid: prams.SendUid, + TakeUid: prams.TakeUid, + MsgType: prams.MsgType, + Content: prams.Content, + ImgUrl: prams.ImgUrl, + UserCount: prams.UserCount, + UserGroup: prams.UserGroup, + IsRead: false, + CreatedAt: now, + UpdatedAt: now, + } + // 是否加入未读提醒 + needTip := false + switch prams.MsgType { + case messagemod.PrivateLetterMsg: + //私信 + msg.SessionId = prams.SessionId + needTip = true + case messagemod.OfficialPrivateLetterMsg: + //官方私信 + msg.SessionId = prams.SessionId + msg.ContentType = prams.ContentType + needTip = true + case messagemod.FollowMsg: + //关注 + msg.Content = "关注了你" + //needTip = true + case messagemod.LikeMsg: + //点赞 + msg.Content = "点赞了你的帖子" + msg.ObjId = prams.ObjId + msg.ObjName = prams.ObjName + msg.ObjType = prams.ObjType + needTip = true + case messagemod.ForwardMsg: //转发 + msg.Content = "转发了你的帖子" + msg.ObjId = prams.ObjId + msg.ObjName = prams.ObjName + case messagemod.CommentMsg: //评论 + //msg.Content = "评论了你的帖子" + msg.ObjId = prams.ObjId + msg.ObjName = prams.ObjName + msg.ObjCover = prams.ObjCover + msg.ObjType = prams.ObjType + needTip = true + case messagemod.DesireMsg: + msg.Content = "回复了你的工单" + msg.ObjId = prams.ObjId + msg.ObjName = prams.ObjName + //needTip = true + case messagemod.LikeCommentMsg: // 点赞评论 + msg.Content = "点赞了你的评论" + msg.ObjId = prams.ObjId + msg.ObjName = prams.ObjName + msg.ObjType = prams.ObjType + needTip = true + case messagemod.ReplyCommentMsg: // 回复评论 + msg.Content = "回复了你的评论" + msg.ObjId = prams.ObjId + msg.ObjName = prams.ObjName + msg.ObjType = prams.ObjType + //needTip = true + } + _, err = messagemod.InsertOne(nil, msg) + if err != nil { + log.Warn(fmt.Sprintf("messageser Add messagemod.InsertOne error:%+v:", err), log.Any("message", msg)) + return stderr.ErrDbQueryError + } + go func() { + err = usermod.IncDynamic(prams.TakeUid) + }() + // 更新提示状态 + if needTip { + _ = message.SetTip(prams.TakeUid) + } + + return stderr.Success +} + +func HandleOfficialMsg(receiveUId uint64, content string, contentType int) stderr.Code { + var ( + uid uint64 = 100000 // 官方ID + sessionId = "" // 会话ID + ) + //新增私聊信息之前, 先查询是否有会话信息 + cond := bson.M{"$or": []bson.M{bson.M{"sendUid": uid, "takeUid": receiveUId}, bson.M{"sendUid": receiveUId, "takeUid": uid}}} + session, err := sessionmod.FindOneByFilter(cond) + if err != nil { + log.Warn(fmt.Sprintf("messageser Add sessionmod.FindOneByFilter error:%+v:", err), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + t := time.Now() + if session.ID.IsZero() { + sessionId = common.UUID() + //新增session + s := &sessionmod.Session{ + SendUid: uid, + TakeUid: receiveUId, + SessionId: sessionId, + CreatedAt: t, + UpdatedAt: t, + } + err := sessionmod.InsertOne(nil, s) + if err != nil { + log.Warn(fmt.Sprintf("messageser Add sessionmod.InsertOne error:%+v:", err), log.Any("session", s), log.Any("uid", uid), log.Any("takeUid", receiveUId)) + return stderr.ErrDbInsertError + } + } else { + sessionId = session.SessionId + } + code := PublicAddMsg(messagemod.PublicAddMsg{ + SendUid: uid, + TakeUid: receiveUId, + SessionId: sessionId, + Content: content, + ContentType: contentType, + MsgType: messagemod.OfficialPrivateLetterMsg, + }) + if code != stderr.Success { + return code + } + common.Go(func() { + //更新会话预览消息 + err := sessionmod.UpdOneByFilter(nil, bson.M{"sessionId": sessionId}, bson.M{"$set": bson.M{"preContent": content, "updatedAt": t}}) + if err != nil { + log.Warn(fmt.Sprintf("messageser Add messagemod.UpdOneByFilter error:%+v:", err), log.Any("sessionId", sessionId), log.Any("preContent", content), log.Any("uid", uid)) + } + }) + return stderr.Success +} diff --git a/app/service/mineser/mineser.go b/app/service/mineser/mineser.go new file mode 100644 index 0000000..dd1259b --- /dev/null +++ b/app/service/mineser/mineser.go @@ -0,0 +1,144 @@ +package mineser + +import ( + "91porn-server/app/appg" + "91porn-server/app/middleware/authuser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/followmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + "errors" + "fmt" + "strconv" + "time" +) + +// Follow 关注或者取消关注 +func Follow(uid uint64, followUID uint64, isFollow bool, isShort bool) (code stderr.Code, data interface{}) { + if isFollow { + //判断被关注用户是否存在或者被封 + user, err := usermod.FindUserByUID(followUID) + if err != nil { + return stderr.ErrDbQueryError, nil + } + if user == nil || user.HasLocked { + return stderr.UserIsNotExists, "" + } + //关注人数限制 + if user.Follows >= followmod.MaxFollowUsers { + return stderr.Success, nil + } + //判断关注用户是否存在或者被封 + followUser, err := usermod.FindUserByUID(uid) + if err != nil { + return stderr.ErrDbQueryError, nil + } + if followUser == nil || followUser.HasLocked { + return stderr.UserIsNotExists, "" + } + if _, err := followmod.AddFollow(uid, followUID, isShort); err != nil { + return stderr.ErrDbInsertError, "" + } else { //关注成功 + common.Go(func() { + //校验该用户一天关注上限,超过50人则封禁 + key := "user-follow-" + strconv.FormatUint(uid, 10) + ret, _ := appg.Redis.Get(key) + //当前用户24小时内关注次数 + i := 0 + if ret != nil { + i, _ = strconv.Atoi(*ret) + //24小时内关注超过上限,封禁 + if i > followmod.ToDayFollowLimit { + log.Warn("user follow upper limit", log.Any("uid", uid), log.Any("followUID", followUID), log.Any("followCount", i)) + //如果用户不存在, 则证明是脚本在刷私信, 封禁该用户 + hasLocked := true + lockReason := "脚本刷关注用户" + summary := "未知" + portrait := "bf211222/image/121/si/1d8/2nw/d9af01c3203a5d0615319a7c7d8faf72.jpeg" + name := "游客" + strconv.FormatUint(uid, 10) + doc := usermod.UserSelector{ + HasLocked: &hasLocked, + LockReason: &lockReason, + Summary: &summary, + Portrait: &portrait, + Name: &name, + } + usermod.Update(uid, doc) + //删除token + authuser.RevokeTokenCache(uid) + //删除redis + appg.Redis.Del(key) + } + } + if i <= followmod.ToDayFollowLimit { + i += 1 + appg.Redis.Set(key, strconv.Itoa(i), time.Hour*24) + } + }) + common.Go(func() { + // 排行榜 + //fans.Incr(followUID) + _ = usermod.IncFans(followUID) + _ = usermod.IncFollow(uid) + //r := int64(1) + //_ = leaderboardmod.Insert(nil, followUID, leaderboardmod.Inc{FollowCountWeek: &r}) + }) + ////存动态 + //common.Go(func() { + // _ = messageser.PublicAddMsg(messagemod.PublicAddMsg{ + // SendUid: uid, + // TakeUid: followUID, + // MsgType: messagemod.FollowMsg, + // }) + //}) + } + + return stderr.Success, nil + } + if count, err := followmod.CloseFollow(uid, followUID); err != nil { + return stderr.ErrDbDeleteError, "" + } else if count > 0 { + common.Go(func() { + // // 排行榜 + // fans.Decr(followUID) + _ = usermod.DecFans(followUID) + _ = usermod.DecFollow(uid) + // r := int64(-1) + // _ = leaderboardmod.Insert(nil, followUID, leaderboardmod.Inc{FollowCountWeek: &r}) + }) + } + + return stderr.Success, nil +} + +func UseDownloadCount(uid uint64) (code stderr.Code, err error) { + user, err := usermod.FindUserByUID(uid) + if err != nil { + return stderr.ErrDbQueryError, nil + } + if user == nil || user.HasLocked { + return stderr.UserIsNotExists, nil + } + + wallet, err := walletmod.GetWallet(uid) + if err != nil { + return stderr.ErrDbQueryError, nil + } + + if wallet == nil || wallet.ID.IsZero() { + return stderr.CodeEmptyData, nil + } + if wallet.DownloadCount <= 0 { + return stderr.DownloadCountIsNotEnough, errors.New("DownloadCount is not enough") + } + + err = walletmod.CreditDownloadCount(nil, 1, uid) + if err != nil { + log.Error(fmt.Sprintf("UseDownloadCount walletmod.CreditDownloadCount err %s", err.Error())) + return stderr.ErrDbUpdateError, nil + } + + return stderr.Success, nil +} diff --git a/app/service/moduleser/module.go b/app/service/moduleser/module.go new file mode 100644 index 0000000..fc44fe7 --- /dev/null +++ b/app/service/moduleser/module.go @@ -0,0 +1,127 @@ +package moduleser + +import ( + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/models/v/marqueemod" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/modulesectionmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/usermod" + "encoding/json" + "errors" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AnnouncementResp struct { + //跑马灯 + Announcement []string `json:"announcement"` +} + +const ( + repeat = 5 + usualAnnouncements = `首充任意金额即送7天视频VIP,限时充值返利,最高返利10%,累计充值送大礼,最高赠送1688元,添加游戏专属土豆福利群@pfqipai,不定时发放惊喜大礼` + activityAnnouncements = `限时抢购虎虎生威永久卡,金币视频五折折扣,免费赠送50张观影券 ` +) + +// GetModuleAnnouncements 获取模块下的跑马灯 +func GetModuleAnnouncements() (AnnouncementResp, error) { + marq, err := marqueemod.Get() + if err != nil { + return AnnouncementResp{}, err + } + res := AnnouncementResp{Announcement: make([]string, repeat)} + for i := 0; i < repeat; i++ { + res.Announcement[i] = marq.Content + } + return res, nil +} + +func GetPublishTag(uid uint64) (data *tagmod.TagInfoRes, err error) { + user, err := usermod.FindUserByUID(uid) + if err != nil { + return nil, err + } + + if user == nil || user.ID.IsZero() || user.HasLocked || user.HasBanned { + return nil, errors.New("user is null") + } + + str, err := appg.Redis.Get(redisconst.PublishTagInfoCache) + if err != nil { + log.Warn(fmt.Sprintf("用户ID:%d;缓存获取广告列表信息异常:%v", uid, err)) + } + + data = &tagmod.TagInfoRes{} + if str != nil { + if err = json.Unmarshal([]byte(*str), &data); err == nil { + return + } + log.Warn(fmt.Sprintf("用户ID:%d;解析缓存数据异常:%v", uid, err)) + } + + // 获取模块 + moduleConfs, err := moduleconfmod.GetModuleConfByType(moduleconfmod.Community) + if err != nil { + return + } + + var sId []primitive.ObjectID + var tagIds []primitive.ObjectID + for _, i := range moduleConfs { + sId = append(sId, i.ID) + } + + sections, err := modulesectionmod.GetBySectionBySids(sId) + if err != nil { + return + } + + if len(sections) <= 0 { + return + } + for _, s := range sections { + if s.TagIds != nil && len(*s.TagIds) > 0 { + tagIds = append(tagIds, *s.TagIds...) + } + } + + if len(tagIds) <= 0 { + return + } + tags, err := tagmod.FindOneTagByIds(tagIds) + if err != nil { + return + } + + for _, t := range tags { + info := tagmod.TagInfo{ + ID: t.ID, + Description: t.Description, + FollowCount: t.CollCount, + Name: t.TagName, + VidCount: t.VideoCount, + PlayCount: t.TPlayCount, + CoverImg: t.CoverImg, + } + data.List = append(data.List, info) + } + + common.Go(func() { + if data != nil { + d, err := json.Marshal(data) + if err != nil { + return + } + if err = appg.Redis.Set(redisconst.PublishTagInfoCache, d, 15*time.Minute); err != nil { + log.Warn(fmt.Sprintf("用户ID:%d;保存缓存数据异常:%v", uid, err)) + } + } + }) + return +} diff --git a/app/service/modulevideoser/modulevideo.go b/app/service/modulevideoser/modulevideo.go new file mode 100644 index 0000000..faefbf8 --- /dev/null +++ b/app/service/modulevideoser/modulevideo.go @@ -0,0 +1,120 @@ +package modulevideoser + +import ( + "time" + + "91porn-server/common/stderr" + "91porn-server/models/v/modulesectionmod" + "91porn-server/models/v/modulevidmod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var ( + getModuleVideoByID = func(id primitive.ObjectID) (modulevidmod.SectionVideo, error) { + videos, err := modulevidmod.Find(bson.M{"_id": id}) + if err != nil || len(videos) == 0 { + return modulevidmod.SectionVideo{}, err + } + return videos[0], nil + } + getModuleSectionByID = modulesectionmod.GetBySectionByID + updateModuleVideo = modulevidmod.UpdateOne + syncVideoModuleID = func(videoID, moduleID primitive.ObjectID) error { + _, err := vidmod.UpdateOneByID(videoID, bson.M{ + "mId": moduleID.Hex(), + "updatedAt": time.Now(), + }) + return err + } +) + +func AddVideo(in *modulevidmod.SectionVideo) stderr.Code { + section, err := modulesectionmod.GetBySectionByID(in.SectionID) + if err != nil { + return stderr.ErrDbQueryError + } + + if section.ID.IsZero() { + return stderr.CodeEmptyData + } + mId := section.SubModuleID + + videoInfo, err := vidmod.GetVideoInfo(in.VideoID.Hex()) + if err != nil { + return stderr.ErrDbQueryError + } + + if videoInfo.ID.IsZero() { + return stderr.CodeEmptyData + } + + update := bson.M{} + update["mId"] = mId.Hex() + update["updatedAt"] = time.Now() + in.NewsType = videoInfo.NewsType // 冗余一个字段 + in.VideoReviewedAt = videoInfo.ReviewAt // 冗余审核时间,方便排序 + _, err = vidmod.UpdateOneByID(in.VideoID, update) + if err != nil { + return stderr.ErrDbUpdateError + } + + if err := modulevidmod.InsertOne(in); err != nil { + return stderr.ErrDbInsertError + } + return stderr.Success +} + +// UpdateVideo 更新专题视频关系,并在移动专题时同步视频所属亚模块。 +func UpdateVideo(in *modulevidmod.EditSelector) stderr.Code { + if in == nil { + return stderr.ErrParamError + } + + hasID := in.ID != nil && !in.ID.IsZero() + hasSectionAndVideo := in.SectionID != nil && !in.SectionID.IsZero() && in.VideoID != nil && !in.VideoID.IsZero() + if !hasID && !hasSectionAndVideo { + return stderr.ErrParamError + } + + // 只修改排序时不需要更新视频归属。 + if in.SectionID == nil || in.SectionID.IsZero() { + if err := updateModuleVideo(in); err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success + } + + var videoID primitive.ObjectID + if in.VideoID != nil { + videoID = *in.VideoID + } + if videoID.IsZero() { + current, err := getModuleVideoByID(*in.ID) + if err != nil { + return stderr.ErrDbQueryError + } + if current.ID.IsZero() || current.VideoID.IsZero() { + return stderr.CodeEmptyData + } + videoID = current.VideoID + } + + section, err := getModuleSectionByID(*in.SectionID) + if err != nil { + return stderr.ErrDbQueryError + } + if section.ID.IsZero() { + return stderr.CodeEmptyData + } + + if err = updateModuleVideo(in); err != nil { + return stderr.ErrDbUpdateError + } + if err = syncVideoModuleID(videoID, section.SubModuleID); err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success +} diff --git a/app/service/modulevideoser/modulevideo_test.go b/app/service/modulevideoser/modulevideo_test.go new file mode 100644 index 0000000..92552e6 --- /dev/null +++ b/app/service/modulevideoser/modulevideo_test.go @@ -0,0 +1,86 @@ +package modulevideoser + +import ( + "testing" + + "91porn-server/common/stderr" + "91porn-server/models/v/modulesectionmod" + "91porn-server/models/v/modulevidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestUpdateVideoSyncsModuleIDWhenMovingByRelationID(t *testing.T) { + relationID := primitive.NewObjectID() + videoID := primitive.NewObjectID() + sectionID := primitive.NewObjectID() + moduleID := primitive.NewObjectID() + + originalGetVideo := getModuleVideoByID + originalGetSection := getModuleSectionByID + originalUpdateRelation := updateModuleVideo + originalSyncModuleID := syncVideoModuleID + defer func() { + getModuleVideoByID = originalGetVideo + getModuleSectionByID = originalGetSection + updateModuleVideo = originalUpdateRelation + syncVideoModuleID = originalSyncModuleID + }() + + getModuleVideoByID = func(id primitive.ObjectID) (modulevidmod.SectionVideo, error) { + if id != relationID { + t.Fatalf("relation id = %s, want %s", id.Hex(), relationID.Hex()) + } + return modulevidmod.SectionVideo{ID: relationID, VideoID: videoID}, nil + } + getModuleSectionByID = func(id primitive.ObjectID) (modulesectionmod.Section, error) { + if id != sectionID { + t.Fatalf("section id = %s, want %s", id.Hex(), sectionID.Hex()) + } + return modulesectionmod.Section{ID: sectionID, SubModuleID: moduleID}, nil + } + relationUpdated := false + updateModuleVideo = func(in *modulevidmod.EditSelector) error { + relationUpdated = true + return nil + } + moduleIDSynced := false + syncVideoModuleID = func(gotVideoID, gotModuleID primitive.ObjectID) error { + moduleIDSynced = true + if gotVideoID != videoID || gotModuleID != moduleID { + t.Fatalf("sync ids = (%s, %s), want (%s, %s)", gotVideoID.Hex(), gotModuleID.Hex(), videoID.Hex(), moduleID.Hex()) + } + return nil + } + + code := UpdateVideo(&modulevidmod.EditSelector{ID: &relationID, SectionID: §ionID}) + if code != stderr.Success { + t.Fatalf("UpdateVideo() code = %d, want %d", code, stderr.Success) + } + if !relationUpdated || !moduleIDSynced { + t.Fatalf("relationUpdated = %v, moduleIDSynced = %v", relationUpdated, moduleIDSynced) + } +} + +func TestUpdateVideoSortOnlyDoesNotSyncModuleID(t *testing.T) { + relationID := primitive.NewObjectID() + sortCode := 10 + + originalUpdateRelation := updateModuleVideo + originalSyncModuleID := syncVideoModuleID + defer func() { + updateModuleVideo = originalUpdateRelation + syncVideoModuleID = originalSyncModuleID + }() + + updateModuleVideo = func(in *modulevidmod.EditSelector) error { return nil } + syncVideoModuleID = func(videoID, moduleID primitive.ObjectID) error { + t.Fatal("sort-only update must not sync module id") + return nil + } + + code := UpdateVideo(&modulevidmod.EditSelector{ID: &relationID, SortCode: &sortCode}) + if code != stderr.Success { + t.Fatalf("UpdateVideo() code = %d, want %d", code, stderr.Success) + } +} diff --git a/app/service/nakedchatorderser/nakedchatorder.go b/app/service/nakedchatorderser/nakedchatorder.go new file mode 100755 index 0000000..8690d08 --- /dev/null +++ b/app/service/nakedchatorderser/nakedchatorder.go @@ -0,0 +1,68 @@ +package nakedchatorderser + +import ( + "91porn-server/common/log" + "91porn-server/models/cache/nakedchatorderdata" + "91porn-server/models/commod" + "91porn-server/models/v/nakedchatordermod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AppQueryListReq struct { + Status *int `json:"status" form:"status"` // 0-已下单 1-已完成 + commod.Page +} + +type AppListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []*nakedchatordermod.NakedChatOrderInfo `json:"list"` +} + +// GetList 获取列表 +func (p *AppQueryListReq) GetList(uid uint64) AppListRes { + var res AppListRes + var err error + filter := bson.M{ + "uid": uid, + } + if p.Status != nil { + filter["status"] = *p.Status + } + sort := bson.D{{"_id", -1}} + // 获取列表 + var data []nakedchatordermod.NakedChatOrder + data, res.Total, res.HasNext, err = nakedchatordermod.GetList(filter, int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + log.Error("获取裸聊订单列表数据错误", log.Any("Params", *p), log.E(err)) + return res + } + res.List = nakedchatorderdata.FormatAppDataList(data) + + return res +} + +type AppQueryInfoReq struct { + ID string `json:"id" form:"id"` // id +} +type AppQueryInfoRes = *nakedchatordermod.NakedChatOrderInfo + +// GetInfo 获取详情 +func (p *AppQueryInfoReq) GetInfo() (res AppQueryInfoRes, err error) { + oid, err := primitive.ObjectIDFromHex(p.ID) + if err != nil { + return res, err + } + + var item nakedchatordermod.NakedChatOrder + item, err = nakedchatordermod.GetInfo(oid) + if err != nil { + log.Error("获取裸聊订单详情数据错误", log.Any("ID", p.ID), log.E(err)) + return + } + + res = nakedchatorderdata.FormatAppData(item) + return +} diff --git a/app/service/nakedchatser/nakedchat.go b/app/service/nakedchatser/nakedchat.go new file mode 100755 index 0000000..9449beb --- /dev/null +++ b/app/service/nakedchatser/nakedchat.go @@ -0,0 +1,70 @@ +package nakedchatser + +import ( + "91porn-server/common/log" + "91porn-server/models/cache/nakedchatdata" + "91porn-server/models/commod" + "91porn-server/models/v/nakedchatmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AppQueryListReq struct { + Mid string `json:"mid" form:"mid" binding:"required"` // 模块id + commod.Page +} + +type AppListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []*nakedchatmod.NakedChatSimple `json:"list"` +} + +// GetList 获取列表 +func (p *AppQueryListReq) GetList() AppListRes { + var res AppListRes + var err error + mid, _ := primitive.ObjectIDFromHex(p.Mid) + if mid.IsZero() { + return res + } + filter := bson.M{ + "mid": mid, + "status": 1, + } + sort := bson.D{{"sortCode", -1}, {"createdAt", -1}} + // 获取列表 + var data []nakedchatmod.NakedChat + data, res.Total, res.HasNext, err = nakedchatdata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + log.Error("获取裸聊列表数据错误", log.Any("Params", *p), log.E(err)) + return res + } + res.List = nakedchatdata.FormatAppDataList(data) + + return res +} + +type AppQueryInfoReq struct { + ID string `json:"id" form:"id"` // id +} +type AppQueryInfoRes = *nakedchatmod.NakedChatInfo + +// GetInfo 获取详情 +func (p *AppQueryInfoReq) GetInfo() (res AppQueryInfoRes, err error) { + oid, err := primitive.ObjectIDFromHex(p.ID) + if err != nil { + return res, err + } + + var item nakedchatmod.NakedChat + item, err = nakedchatdata.GetInfoFromCache(oid) + if err != nil { + log.Error("获取裸聊详情数据错误", log.Any("ID", p.ID), log.E(err)) + return + } + + res = nakedchatdata.FormatAppData(item) + return +} diff --git a/app/service/notiser/email.go b/app/service/notiser/email.go new file mode 100644 index 0000000..1db95c1 --- /dev/null +++ b/app/service/notiser/email.go @@ -0,0 +1,33 @@ +package notiser + +import ( + "context" + "fmt" + + "91porn-server/app/appg" + "91porn-server/app/service/emailser" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/email" + "91porn-server/common/log" + "91porn-server/common/maths" + "91porn-server/common/stderr" +) + +func SendEamil(ctx context.Context, emailStr string) stderr.Code { + if code := emailser.CheckEmail(ctx, emailStr); code != stderr.Success { + return code + } + code := maths.RandDigits(constant.CaptchaLen) + redisKey := redisconst.GetEmailCaptchaKey(emailStr) + if err := appg.Redis.Set(redisKey, code, redisconst.EmailCaptchaExpire); err != nil { + log.ErrorX(ctx, "save code in redis failed", log.Any("email", emailStr), log.Any("redisKey", redisKey), log.E(err)) + return stderr.ErrNetWorkBusy + } + var DefaultEmailSender string = fmt.Sprintf("%s@%s", "no-reply", appg.Conf.AWS.SES.VerifiedDomain) + if err := email.Client.Send(ctx, DefaultEmailSender, []*string{&emailStr}, "验证码", + fmt.Sprintf("

您的验证码是 %s

", code)); err != nil { + return stderr.ErrNetWorkBusy + } + return stderr.Success +} diff --git a/app/service/notiser/notificationser.go b/app/service/notiser/notificationser.go new file mode 100644 index 0000000..c161ce2 --- /dev/null +++ b/app/service/notiser/notificationser.go @@ -0,0 +1,43 @@ +package notiser + +import ( + "context" + + "91porn-server/app/appg" + "91porn-server/app/service/emailser" + "91porn-server/app/service/smsser" + "91porn-server/common/constant" + "91porn-server/common/stderr" +) + +const ( + SMSMobileBind = 1 + SMSCaptcha = 2 + EmailCaptcha = 3 +) + +// SendCaptcha 发送验证码 +func SendCaptcha(ctx context.Context, mobile, email string, typ int) stderr.Code { + switch typ { + case SMSCaptcha, SMSMobileBind: + return smsser.SendSmsCode(ctx, mobile, typ) + case EmailCaptcha: + return SendEamil(ctx, email) + default: + return stderr.ErrNotSupportedCaptcha + } +} + +// VerifyCaptchaCode 校验验证码 +func VerifyCaptchaCode(ctx context.Context, mobile, email, code string) stderr.Code { + if appg.Conf.Base.Env != constant.ReleaseRunmod { // 测试环境为方便测试不校验验证码 + return stderr.Success + } + if mobile != "" { + return smsser.VerifySmsCode(mobile, code) + } + if email != "" { + return emailser.VerifyCaptchaCode(ctx, email, code) + } + return stderr.ErrNotSupportedCaptcha +} diff --git a/app/service/officialWebsiteser/album.go b/app/service/officialWebsiteser/album.go new file mode 100644 index 0000000..f4055ec --- /dev/null +++ b/app/service/officialWebsiteser/album.go @@ -0,0 +1,161 @@ +package officialWebsiteser + +import ( + "91porn-server/common/stderr" + "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type AlbumListReq struct { + SortType int64 `json:"sortType" form:"sortType"` // 1: 创建时间 目前仅支持一种排序方式,后续增加后再完善这个字段 + + PageNumber int64 `json:"pageNumber" form:"pageNumber"` + PageSize int64 `json:"pageSize" form:"pageSize"` +} + +type AlbumListResp struct { + HasNext bool `json:"hasNext"` + List []*AlbumResp `json:"list"` +} + +type AlbumResp struct { + ID string `json:"id"` + HeroID string `json:"heroID"` + Title string `json:"title"` + SeoSlug string `json:"seoSlug"` + Description string `json:"description"` + Cover string `json:"cover"` + IsHot bool `json:"isHot"` + Tags []string `json:"tags"` +} + +func (a *AlbumResp) FromModel(album *officialWebsitemod.Album) { + if album == nil { + return + } + a.ID = album.ID.Hex() + a.HeroID = album.HeroID.Hex() + a.Title = album.Title + a.SeoSlug = album.SeoSlug + a.Description = album.Description + a.Cover = album.Cover + a.IsHot = album.IsHot + a.Tags = make([]string, 0) + for _, tag := range album.Tags { + a.Tags = append(a.Tags, tag.Name) + } +} + +func AlbumList(req *AlbumListReq) (resp AlbumListResp, err error) { + var sort = bson.D{} + switch req.SortType { // 目前仅支持一种排序方式,后续增加后再完善这个字段 + default: + sort = bson.D{{Key: "createdAt", Value: -1}} + } + + var albumMod = &officialWebsitemod.Album{} + var skip, limit = transPageAndPageNumber(req.PageSize, req.PageNumber) + var ops = options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + albumsData, err := albumMod.FindMany(bson.M{"isActive": true}, ops) + if err != nil { + return + } + + resp = AlbumListResp{ + HasNext: len(albumsData) > int(limit), + List: make([]*AlbumResp, 0), + } + for _, v := range albumsData { + var albumResp = &AlbumResp{} + albumResp.FromModel(&v) + resp.List = append(resp.List, albumResp) + } + if resp.HasNext { + resp.List = resp.List[:len(resp.List)-1] + } + return resp, nil +} + +type AlbumDetailReq struct { + ID string `uri:"id" form:"id"` + PageNumber int64 `json:"pageNumber" form:"pageNumber"` + PageSize int64 `json:"pageSize" form:"pageSize"` +} + +type AlbumDetailResp struct { + AlbumResp + Videos []AlbumDetailVideoResp `json:"videos"` +} + +type AlbumDetailVideoResp struct { + ID string `json:"id"` + Title string `json:"title"` + SeoSlug string `json:"seoSlug"` + Description string `json:"description"` + Cover string `json:"cover"` + Url string `json:"url"` + IsHot bool `json:"isHot"` + Tags []VideoTagResp `json:"tags"` + WatchCount int64 `json:"watchCount"` + LikeCount int64 `json:"likeCount"` + CommentCount int64 `json:"commentCount"` + CollectCount int64 `json:"collectCount"` +} + +func AlbumDetail(req *AlbumDetailReq) (resp AlbumDetailResp, err error) { + if req.ID == "" { + err = stderr.ErrParamError + return + } + + // 兼容 id 与 seoSlug:24 位 hex 按 _id 查,否则按 seoSlug 查 + filter := bson.M{} + if objID, e := primitive.ObjectIDFromHex(req.ID); e == nil { + filter["_id"] = objID + } else { + filter["seoSlug"] = req.ID + } + albumMod := &officialWebsitemod.Album{} + err = albumMod.FindOne(filter) + if err != nil { + return + } + resp.FromModel(albumMod) + + // 组装获取专辑下视频列表 + filter = bson.M{"albumId": albumMod.ID} + var skip, limit = transPageAndPageNumber(req.PageSize, req.PageNumber) + var option = options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}}).SetSkip(skip).SetLimit(limit + 1) + videoMod := &officialWebsitemod.Video{} + var videos []officialWebsitemod.Video + videos, err = videoMod.FindMany(filter, option) + if err != nil { + return + } + for _, video := range videos { + var videoResp = AlbumDetailVideoResp{ + ID: video.ID.Hex(), + Title: video.Title, + SeoSlug: video.SeoSlug, + Description: video.Description, + Cover: video.Cover, + Url: video.Url, + IsHot: video.IsHot, + WatchCount: video.WatchCount, + LikeCount: video.LikeCount, + CommentCount: video.CommentCount, + CollectCount: video.CollectCount, + } + for _, tag := range video.Tags { + videoResp.Tags = append(videoResp.Tags, VideoTagResp{ + ID: tag.ID.Hex(), + Name: tag.Name, + }) + } + resp.Videos = append(resp.Videos, videoResp) + } + return +} diff --git a/app/service/officialWebsiteser/basicData.go b/app/service/officialWebsiteser/basicData.go new file mode 100644 index 0000000..618a6f6 --- /dev/null +++ b/app/service/officialWebsiteser/basicData.go @@ -0,0 +1,225 @@ +package officialWebsiteser + +import ( + "91porn-server/common/stderr" + "91porn-server/models/v/officialWebsitemod" + "91porn-server/models/v/sourcemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type GetBasicDataResp struct { + Title string `json:"title"` + Description string `json:"description"` + //域名信息 + SourceList []*sourcemod.SourceRes `json:"sourceList"` + //接口域名 + Domain []string `json:"domain"` + Banner []Banner `json:"banner"` + BannersDescription string `json:"bannersDescription" bson:"bannersDescription"` + BannersDuration int `json:"bannersDuration" bson:"bannersDuration"` + HeroBanner []BasicDataHeroBanner `json:"heroBanner"` + Masterpiece []BasicDataMasterpiece `json:"masterpiece"` + Business []BasicDataBusiness `json:"business"` + AboutUs BasicDataAboutUs `json:"aboutUs"` + FAQ []BasicDataFAQ `json:"faq"` + HomePageCMS []BasicDataCMSData `json:"homePageCMS"` + RecruitCMS []BasicDataCMSData `json:"recruitCMS"` + Tags []officialWebsitemod.Tag `json:"tags"` +} + +type Banner struct { + Name string `json:"name"` + Description string `json:"description"` + Image string `json:"image"` + PhoneImage string `json:"phoneImage"` + Thumbnail string `json:"thumbnail"` + URL string `json:"url"` +} + +type BasicDataHeroBanner struct { + HeroID string `json:"heroId"` + Name string `json:"name"` + Description string `json:"description"` + Image string `json:"image"` + URL string `json:"url"` + Duration int `json:"duration"` +} + +type BasicDataMasterpiece struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Cover string `json:"cover"` + Type string `json:"type"` +} + +type BasicDataBusiness struct { + Name string `json:"name"` + EnglishName string `json:"englishName"` + Description string `json:"description"` + Cover string `json:"cover"` + Detail []BasicDataBusinessDetail `json:"detail"` +} + +type BasicDataBusinessDetail struct { + Cover string `json:"cover"` + Description string `json:"description"` +} + +type BasicDataAboutUs struct { + Description string `json:"description"` + Companies []BasicDataCompanyInfo `json:"companies"` + Values string `json:"values"` + ValuesImg string `json:"valuesImg"` +} + +type BasicDataCompanyInfo struct { + Name string `json:"name"` + Address string `json:"address"` + Description string `json:"description"` + Contact []BasicDataContactInfo `json:"contact"` +} + +type BasicDataContactInfo struct { + Type string `json:"type"` + Value string `json:"value"` +} + +type SourceResp struct { + ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + IsActive bool `json:"isActive,omitempty" bson:"isActive,omitempty"` + Type string `json:"type,omitempty" bson:"type,omitempty"` + Domain DomainResp `json:"domain,omitempty" bson:"domain,omitempty"` +} + +type DomainResp struct { + Weight int `json:"weight,omitempty" bson:"weight,omitempty"` + Url string `json:"url,omitempty" bson:"url,omitempty"` + Desc string `json:"desc,omitempty" bson:"desc,omitempty"` + Status int `json:"status,omitempty" bson:"status,omitempty"` +} + +type BasicDataFAQ struct { + Question string `json:"question"` + Answer string `json:"answer"` +} + +type BasicDataCMSData struct { + Description string `json:"description"` + Value string `json:"value"` +} + +func GetBasicData() (data GetBasicDataResp, err error) { + basicDataMod := &officialWebsitemod.BasicData{} + err = basicDataMod.FindOne() + if err != nil { + return + } + if basicDataMod.ID == primitive.NilObjectID { + err = stderr.OfficialWebsiteBasicDataNotExist + return + } + + data = GetBasicDataResp{ + Title: basicDataMod.Title, + Description: basicDataMod.Description, + Banner: make([]Banner, 0), + BannersDescription: basicDataMod.BannersDescription, + BannersDuration: basicDataMod.BannersDuration, + HeroBanner: make([]BasicDataHeroBanner, 0), + Masterpiece: make([]BasicDataMasterpiece, 0), + Business: make([]BasicDataBusiness, 0), + AboutUs: BasicDataAboutUs{}, + FAQ: make([]BasicDataFAQ, 0), + HomePageCMS: make([]BasicDataCMSData, 0), + RecruitCMS: make([]BasicDataCMSData, 0), + } + for _, v := range basicDataMod.Banner { + data.Banner = append(data.Banner, Banner{ + Name: v.Name, + Description: v.Description, + Image: v.Image, + PhoneImage: v.PhoneImage, + Thumbnail: v.Thumbnail, + URL: v.URL, + }) + } + for _, v := range basicDataMod.HeroBanner { + data.HeroBanner = append(data.HeroBanner, BasicDataHeroBanner{ + HeroID: v.HeroID.Hex(), + Name: v.Name, + Description: v.Description, + Image: v.Image, + URL: v.URL, + Duration: v.Duration, + }) + } + for _, v := range basicDataMod.Masterpiece { + data.Masterpiece = append(data.Masterpiece, BasicDataMasterpiece{ + ID: v.ID.Hex(), + Name: v.Name, + Description: v.Description, + Cover: v.Cover, + Type: v.Type, + }) + } + for _, v := range basicDataMod.Business { + data.Business = append(data.Business, BasicDataBusiness{ + Name: v.Name, + EnglishName: v.EnglishName, + Description: v.Description, + Cover: v.Cover, + Detail: make([]BasicDataBusinessDetail, 0, len(v.Detail)), + }) + for _, d := range v.Detail { + data.Business[len(data.Business)-1].Detail = append(data.Business[len(data.Business)-1].Detail, BasicDataBusinessDetail{ + Cover: d.Cover, + Description: d.Description, + }) + } + } + data.AboutUs = BasicDataAboutUs{ + Description: basicDataMod.AboutUs.Description, + Values: basicDataMod.AboutUs.Values, + ValuesImg: basicDataMod.AboutUs.ValuesImg, + } + for _, v := range basicDataMod.AboutUs.Companies { + data.AboutUs.Companies = append(data.AboutUs.Companies, BasicDataCompanyInfo{ + Name: v.Name, + Address: v.Address, + Description: v.Description, + Contact: make([]BasicDataContactInfo, 0), + }) + for _, c := range v.Contact { + data.AboutUs.Companies[len(data.AboutUs.Companies)-1].Contact = append(data.AboutUs.Companies[len(data.AboutUs.Companies)-1].Contact, BasicDataContactInfo{ + Type: c.Type, + Value: c.Value, + }) + } + } + for _, v := range basicDataMod.FAQ { + data.FAQ = append(data.FAQ, BasicDataFAQ{ + Question: v.Question, + Answer: v.Answer, + }) + } + for _, v := range basicDataMod.HomePageCMS { + data.HomePageCMS = append(data.HomePageCMS, BasicDataCMSData{ + Description: v.Description, + Value: v.Value, + }) + } + for _, v := range basicDataMod.RecruitCMS { + data.RecruitCMS = append(data.RecruitCMS, BasicDataCMSData{ + Description: v.Description, + Value: v.Value, + }) + } + + data.Tags = basicDataMod.Tags + + // 域名信息(Domain/SourceList)改由 controller 实时获取,不进基础数据大缓存 + + return data, err +} diff --git a/app/service/officialWebsiteser/business.go b/app/service/officialWebsiteser/business.go new file mode 100644 index 0000000..9f6b2eb --- /dev/null +++ b/app/service/officialWebsiteser/business.go @@ -0,0 +1,66 @@ +package officialWebsiteser + +import "91porn-server/models/v/officialWebsitemod" + +type BusinessListReq struct{} + +type BusinessListResp struct { + Total int64 `json:"total"` + ADPlacement []BusinessInfo `json:"adPlacement"` + ContentProduction []BusinessInfo `json:"contentProduction"` + SubRevenue []BusinessInfo `json:"subRevenue"` +} + +type BusinessInfo struct { + Title string `json:"title"` + Description string `json:"description"` + Type string `json:"type"` // 业务类型,如"adPlacement", "contentProduction", "subRevenue" + ServiceOverview string `json:"serviceOverview"` + CollaborationProcess string `json:"collaborationProcess"` + CaseStudies string `json:"caseStudies"` +} + +func BusinessList(req *BusinessListReq) (resp BusinessListResp, err error) { + data, err := (&officialWebsitemod.BusinessData{}).FindMany(nil) + if err != nil { + return + } + resp = BusinessListResp{ + Total: int64(len(data)), + ADPlacement: make([]BusinessInfo, 0), + ContentProduction: make([]BusinessInfo, 0), + SubRevenue: make([]BusinessInfo, 0), + } + for _, v := range data { + switch v.Type { + case "adPlacement": + resp.ADPlacement = append(resp.ADPlacement, BusinessInfo{ + Title: v.Title, + Description: v.Description, + Type: v.Type, + ServiceOverview: v.ServiceOverview, + CollaborationProcess: v.CollaborationProcess, + CaseStudies: v.CaseStudies, + }) + case "contentProduction": + resp.ContentProduction = append(resp.ContentProduction, BusinessInfo{ + Title: v.Title, + Description: v.Description, + Type: v.Type, + ServiceOverview: v.ServiceOverview, + CollaborationProcess: v.CollaborationProcess, + CaseStudies: v.CaseStudies, + }) + case "subRevenue": + resp.SubRevenue = append(resp.SubRevenue, BusinessInfo{ + Title: v.Title, + Description: v.Description, + Type: v.Type, + ServiceOverview: v.ServiceOverview, + CollaborationProcess: v.CollaborationProcess, + CaseStudies: v.CaseStudies, + }) + } + } + return resp, nil +} diff --git a/app/service/officialWebsiteser/common.go b/app/service/officialWebsiteser/common.go new file mode 100644 index 0000000..f80d114 --- /dev/null +++ b/app/service/officialWebsiteser/common.go @@ -0,0 +1,13 @@ +package officialWebsiteser + +func transPageAndPageNumber(pageSize, pageNumber int64) (skip int64, limit int64) { + if pageNumber <= 0 { + pageNumber = 1 + } + if pageSize <= 0 { + pageSize = 20 + } + skip = (pageNumber - 1) * pageSize + limit = pageSize + return skip, limit +} diff --git a/app/service/officialWebsiteser/hero.go b/app/service/officialWebsiteser/hero.go new file mode 100644 index 0000000..d1bec7e --- /dev/null +++ b/app/service/officialWebsiteser/hero.go @@ -0,0 +1,314 @@ +package officialWebsiteser + +import ( + "91porn-server/common" + "91porn-server/models/v/officialWebsitemod" + "sync" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type HeroListReq struct { + SortType int `json:"sortType" form:"sortType"` // 1: 最多粉丝,2: 最新加入,3:后管排序 + + PageNumber int64 `json:"pageNumber" form:"pageNumber"` + PageSize int64 `json:"pageSize" form:"pageSize"` +} + +type HeroListResp struct { + HasNext bool `json:"hasNext"` + List []*HeroResp `json:"list"` +} + +type HeroResp struct { + ID string `json:"id"` + Name string `json:"name"` + SeoSlug string `json:"seoSlug"` + Avatar string `json:"avatar"` + FansCount int64 `json:"fansCount"` + Cover string `json:"cover"` +} + +func HeroList(req *HeroListReq) (resp HeroListResp, err error) { + var sort = bson.M{} + switch req.SortType { + case 2: + sort["createdAt"] = -1 + case 3: + sort["sort"] = -1 + default: + sort["fansCount"] = -1 + } + + // 分页查询时,查询一条多的数据来判断是否有下一页数据 + var skip, limit = transPageAndPageNumber(req.PageSize, req.PageNumber) + var ops = options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + data, err := (&officialWebsitemod.Hero{}).FindMany(bson.M{"isActive": true}, ops) + if err != nil { + return + } + + resp = HeroListResp{ + HasNext: len(data) > int(limit), + List: make([]*HeroResp, 0), + } + for _, v := range data { + hero := &HeroResp{ + ID: v.ID.Hex(), + Name: v.Name, + SeoSlug: v.SeoSlug, + Avatar: v.Avatar, + FansCount: v.FansCount, + Cover: v.Cover, + } + resp.List = append(resp.List, hero) + } + // 如果HasNext为true,说明还有下一页数,去除多查询的一条数据 + if resp.HasNext { + resp.List = resp.List[:len(resp.List)-1] + } + + return +} + +type HeroDetailReq struct { + ID string `uri:"id" binding:"required"` +} + +type HeroDetailResp struct { + ID string `json:"id"` + Name string `json:"name"` + SeoSlug string `json:"seoSlug"` + Avatar string `json:"avatar"` + Height int64 `json:"height"` + Bust int64 `json:"bust"` + Waist int64 `json:"waist"` + Hip int64 `json:"hip"` + Description string `json:"description"` + FansCount int64 `json:"fansCount"` + Cover string `json:"cover"` + Partners []HeroDetailPartner `json:"partners"` + Albums []HeroDetailAlbum `json:"albums"` + Videos []HeroDetailVideo `json:"videos"` + Photograph []Photograph `json:"photograph"` +} + +type HeroDetailAlbum struct { + ID string `json:"id"` + Title string `json:"title"` + SeoSlug string `json:"seoSlug"` + Description string `json:"description"` + Cover string `json:"cover"` + Tags []HeroDetailTag `json:"tags"` + IsHot bool `json:"isHot"` +} + +type HeroDetailVideo struct { + ID string `json:"id"` + Title string `json:"title"` + SeoSlug string `json:"seoSlug"` + Description string `json:"description"` + Cover string `json:"cover"` + Url string `json:"url"` + Tags []HeroDetailTag `json:"tags"` + IsHot bool `json:"isHot"` +} + +type HeroDetailPartner struct { + ID string `json:"id"` + Name string `json:"name"` + Url string `json:"url"` + Logo string `json:"logo"` +} + +type Photograph struct { + ID string `json:"id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Description string `json:"description" bson:"description"` + Cover string `json:"cover" bson:"cover"` + Photos []string `json:"photos" bson:"photos"` +} + +type HeroDetailTag struct { + ID string `json:"id"` + Name string `json:"name"` +} + +func HeroDetail(req *HeroDetailReq) (resp *HeroDetailResp, err error) { + filter := bson.M{"isActive": true} + if id, e := primitive.ObjectIDFromHex(req.ID); e == nil { + filter["_id"] = id + } else { + filter["seoSlug"] = req.ID + } + hero := &officialWebsitemod.Hero{} + err = hero.FindOne(filter) + if err != nil { + return nil, err + } + + resp = &HeroDetailResp{ + ID: hero.ID.Hex(), + Name: hero.Name, + SeoSlug: hero.SeoSlug, + Avatar: hero.Avatar, + Height: hero.Height, + Bust: hero.Bust, + Waist: hero.Waist, + Hip: hero.Hip, + Description: hero.Description, + FansCount: hero.FansCount, + Cover: hero.Cover, + Partners: make([]HeroDetailPartner, 0), + Albums: make([]HeroDetailAlbum, 0), + Videos: make([]HeroDetailVideo, 0), + } + + wg := sync.WaitGroup{} + wg.Add(4) + + // 查询合作伙伴 + common.Go(func() { + defer wg.Done() + resp.Partners, err = getHeroDetailPartners(hero.Partners) + if err != nil { + return + } + }) + + // 查询专辑 + common.Go(func() { + defer wg.Done() + resp.Albums, err = getHeroDetailAlbums(hero.ID) + if err != nil { + return + } + }) + + // 查询视频 + common.Go(func() { + defer wg.Done() + resp.Videos, err = getHeroDetailVideos(hero.ID) + if err != nil { + return + } + }) + common.Go(func() { + defer wg.Done() + resp.Photograph, err = getHeroDetailPhotograph(hero.ID) + }) + wg.Wait() + + return +} + +func getHeroDetailAlbums(heroID primitive.ObjectID) (albums []HeroDetailAlbum, err error) { + var albumMod = &officialWebsitemod.Album{} + var filters = bson.M{"heroID": heroID} + albumsData, err := albumMod.FindMany(filters, options.Find().SetSort(bson.D{{Key: "isHot", Value: -1}, {Key: "createdAt", Value: -1}}).SetLimit(10)) + if err != nil { + return nil, err + } + + albums = make([]HeroDetailAlbum, 0) + for _, v := range albumsData { + tags := make([]HeroDetailTag, 0) + for _, t := range v.Tags { + tags = append(tags, HeroDetailTag{ + ID: t.ID.Hex(), + Name: t.Name, + }) + } + albums = append(albums, HeroDetailAlbum{ + ID: v.ID.Hex(), + Title: v.Title, + SeoSlug: v.SeoSlug, + Description: v.Description, + Cover: v.Cover, + IsHot: v.IsHot, + Tags: tags, + }) + } + + return +} + +func getHeroDetailVideos(heroID primitive.ObjectID) (videos []HeroDetailVideo, err error) { + var videoMod = &officialWebsitemod.Video{} + var filters = bson.M{"heroID": heroID} + videosData, err := videoMod.FindMany(filters, options.Find().SetSort(bson.D{{Key: "isHot", Value: -1}, {Key: "createdAt", Value: -1}}).SetLimit(10)) + if err != nil { + return nil, err + } + + videos = make([]HeroDetailVideo, 0) + for _, v := range videosData { + tags := make([]HeroDetailTag, 0) + for _, t := range v.Tags { + tags = append(tags, HeroDetailTag{ + ID: t.ID.Hex(), + Name: t.Name, + }) + } + videos = append(videos, HeroDetailVideo{ + ID: v.ID.Hex(), + Title: v.Title, + SeoSlug: v.SeoSlug, + Description: v.Description, + Cover: v.Cover, + Url: v.Url, + IsHot: v.IsHot, + Tags: tags, + }) + } + + return +} + +func getHeroDetailPartners(partnerIds []primitive.ObjectID) (partners []HeroDetailPartner, err error) { + var partnerMod = &officialWebsitemod.Partner{} + var filters = bson.M{"_id": bson.M{"$in": partnerIds}, "isActive": true} + partnersData, err := partnerMod.FindMany(filters) + if err != nil { + return nil, err + } + + partners = make([]HeroDetailPartner, 0) + for i := range partnerIds { // 保持合作伙伴的顺序和Hero中的一致,由后管控制 + for _, v := range partnersData { + if v.ID == partnerIds[i] { + partners = append(partners, HeroDetailPartner{ + ID: v.ID.Hex(), + Name: v.Name, + Url: v.Url, + Logo: v.Logo, + }) + } + } + } + + return +} + +func getHeroDetailPhotograph(heroID primitive.ObjectID) (photograph []Photograph, err error) { + var photographMod = &officialWebsitemod.Photograph{} + var filters = bson.M{"heroId": heroID, "isActive": true} + photographData, err := photographMod.FindMany(filters, options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}}).SetLimit(10)) + if err != nil { + return + } + photograph = make([]Photograph, 0) + for _, v := range photographData { + ph := Photograph{ + ID: v.ID.Hex(), + Name: v.Name, + Description: v.Description, + Cover: v.Cover, + Photos: v.Photos, + } + photograph = append(photograph, ph) + } + return +} diff --git a/app/service/officialWebsiteser/job.go b/app/service/officialWebsiteser/job.go new file mode 100644 index 0000000..b2e4825 --- /dev/null +++ b/app/service/officialWebsiteser/job.go @@ -0,0 +1,47 @@ +package officialWebsiteser + +import ( + "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type JobListReq struct { +} + +type JobListResp struct { + Total int64 `json:"total"` + List []JobInfo `json:"list"` +} + +type JobInfo struct { + Title string `json:"title"` + Description string `json:"description"` + Requirements string `json:"requirements"` + Benefits string `json:"benefits"` + JDUrl string `json:"jdUrl"` +} + +func JobList(req *JobListReq) (resp JobListResp, err error) { + var filter = bson.M{"isActive": true} + var jobMod = &officialWebsitemod.Job{} + jobs, err := jobMod.FindMany(filter, options.Find().SetSort(bson.D{{Key: "isHot", Value: -1}, {Key: "sort", Value: -1}, {Key: "createdAt", Value: -1}})) + if err != nil { + return + } + resp = JobListResp{ + Total: int64(len(jobs)), + List: make([]JobInfo, len(jobs)), + } + for i, job := range jobs { + resp.List[i] = JobInfo{ + Title: job.Title, + Description: job.Description, + Requirements: job.Requirements, + Benefits: job.Benefits, + JDUrl: job.JDUrl, + } + } + return resp, nil +} diff --git a/app/service/officialWebsiteser/news.go b/app/service/officialWebsiteser/news.go new file mode 100644 index 0000000..68b0bb4 --- /dev/null +++ b/app/service/officialWebsiteser/news.go @@ -0,0 +1,163 @@ +package officialWebsiteser + +import ( + "91porn-server/models/v/officialWebsitemod" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type NewsListReq struct { + PageNumber int64 `form:"PageNumber"` + PageSize int64 `form:"pageSize"` +} + +type NewsListResp struct { + HasNext bool `json:"hasNext"` + List []*NewsResp `json:"list"` +} + +type NewsResp struct { + ID string `json:"id"` + Title string `json:"title"` + SeoSlug string `json:"seoSlug"` + Description string `json:"description"` + Cover string `json:"cover"` + Url string `json:"url"` + Tags []NewsTagResp `json:"tags"` + CreatedAt time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` +} + +type NewsTagResp struct { + ID string `json:"id,omitempty" bson:"_id,omitempty"` + Name string `json:"name,omitempty" bson:"name,omitempty"` + Type string `json:"type,omitempty" bson:"type,omitempty"` + IsHot bool `json:"isHot,omitempty" bson:"isHot,omitempty"` +} + +func NewsList(req *NewsListReq) (resp *NewsListResp, err error) { + var filter = bson.M{"isActive": true} + var newsMod = &officialWebsitemod.News{} + var skip, limit = transPageAndPageNumber(req.PageSize, req.PageNumber) + data, err := newsMod.FindMany(filter, options.Find().SetSort(bson.D{{Key: "isHot", Value: -1}, {Key: "sort", Value: -1}, {Key: "createdAt", Value: -1}}).SetSkip(skip).SetLimit(limit+1)) + if err != nil { + return nil, err + } + resp = &NewsListResp{ + HasNext: len(data) > int(limit), + List: make([]*NewsResp, 0), + } + for _, v := range data { + newsResp := &NewsResp{ + ID: v.ID.Hex(), + Title: v.Title, + SeoSlug: v.SeoSlug, + Description: v.Description, + Cover: v.Cover, + Url: v.Url, + CreatedAt: v.CreatedAt, + } + for _, tag := range v.Tags { + newsResp.Tags = append(newsResp.Tags, NewsTagResp{ + ID: tag.ID.Hex(), + Name: tag.Name, + Type: tag.Type, + IsHot: tag.IsHot, + }) + } + resp.List = append(resp.List, newsResp) + } + if resp.HasNext { + resp.List = resp.List[:len(resp.List)-1] + } + return +} + +type NewsDetailReq struct { + ID string `uri:"id" binding:"required"` + RelatedPageNumber int64 `form:"RelatedPageNumber"` + RelatedPageSize int64 `form:"RelatedPageSize"` +} + +type NewsDetailResp struct { + ID string `json:"id"` + Title string `json:"title"` + SeoSlug string `json:"seoSlug"` + Description string `json:"description"` + Cover string `json:"cover"` + Url string `json:"url"` + Tags []NewsTagResp `json:"tags"` + Detail string `json:"detail"` + Related []*NewsResp `json:"related"` + CreatedAt time.Time `json:"createdAt"` +} + +func NewsDetail(req *NewsDetailReq) (resp NewsDetailResp, err error) { + // 兼容 id 与 seoSlug:24 位 hex 按 _id 查,否则按 seoSlug 查 + var filter = bson.M{"isActive": true} + if id, e := primitive.ObjectIDFromHex(req.ID); e == nil { + filter["_id"] = id + } else { + filter["seoSlug"] = req.ID + } + var newsMod = &officialWebsitemod.News{} + err = newsMod.FindOne(filter) + if err != nil { + return + } + resp = NewsDetailResp{ + ID: newsMod.ID.Hex(), + Title: newsMod.Title, + SeoSlug: newsMod.SeoSlug, + Description: newsMod.Description, + Cover: newsMod.Cover, + Url: newsMod.Url, + Detail: newsMod.Detail, + CreatedAt: newsMod.CreatedAt, + } + var tagIDs []primitive.ObjectID + for _, tag := range newsMod.Tags { + resp.Tags = append(resp.Tags, NewsTagResp{ + ID: tag.ID.Hex(), + Name: tag.Name, + Type: tag.Type, + IsHot: tag.IsHot, + }) + tagIDs = append(tagIDs, tag.ID) + } + + if len(resp.Tags) > 0 { + // 组装 Related,根据tagIDs搜索,并按照排序 + filter = bson.M{"tags._id": bson.M{"$in": tagIDs}, "isActive": true} + } else { + filter = bson.M{"isActive": true} + } + skip, limit := transPageAndPageNumber(req.RelatedPageSize, req.RelatedPageNumber) + data, err := newsMod.FindMany(filter, options.Find().SetSort(bson.D{{Key: "isHot", Value: -1}, {Key: "sort", Value: -1}, {Key: "createdAt", Value: -1}}).SetSkip(skip).SetLimit(limit)) + if err != nil { + return + } + for _, v := range data { + newsResp := &NewsResp{ + ID: v.ID.Hex(), + Title: v.Title, + SeoSlug: v.SeoSlug, + Description: v.Description, + Cover: v.Cover, + Url: v.Url, + CreatedAt: v.CreatedAt, + } + for _, tag := range v.Tags { + newsResp.Tags = append(newsResp.Tags, NewsTagResp{ + ID: tag.ID.Hex(), + Name: tag.Name, + Type: tag.Type, + IsHot: tag.IsHot, + }) + } + resp.Related = append(resp.Related, newsResp) + } + return +} diff --git a/app/service/officialWebsiteser/partner.go b/app/service/officialWebsiteser/partner.go new file mode 100644 index 0000000..b4e19d0 --- /dev/null +++ b/app/service/officialWebsiteser/partner.go @@ -0,0 +1,37 @@ +package officialWebsiteser + +import "91porn-server/models/v/officialWebsitemod" + +type PartnerListReq struct{} + +type PartnerListResp struct { + Total int64 `json:"total"` + List []PartnerResp `json:"list"` +} + +type PartnerResp struct { + Name string `json:"name"` + Description string `json:"description"` + Logo string `json:"logo"` + Url string `json:"url"` +} + +func PartnerList(req *PartnerListReq) (resp PartnerListResp, err error) { + data, err := (&officialWebsitemod.Partner{}).FindMany(nil) + if err != nil { + return + } + resp = PartnerListResp{ + Total: int64(len(data)), + List: make([]PartnerResp, 0), + } + for _, v := range data { + resp.List = append(resp.List, PartnerResp{ + Name: v.Name, + Description: v.Description, + Logo: v.Logo, + Url: v.Url, + }) + } + return resp, nil +} diff --git a/app/service/officialWebsiteser/recruit.go b/app/service/officialWebsiteser/recruit.go new file mode 100644 index 0000000..a88914d --- /dev/null +++ b/app/service/officialWebsiteser/recruit.go @@ -0,0 +1,97 @@ +package officialWebsiteser + +import ( + "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type RecruitFormReq struct { + Name string `json:"name" form:"name" binding:"required"` + Sex int `json:"sex" form:"sex"` + Age int `json:"age" form:"age"` + Country string `json:"country" form:"country"` + Address string `json:"address" form:"address"` + Contact []ContactInfoReq `json:"contact" form:"contact" binding:"required,dive"` + Description string `json:"description" form:"description"` +} + +type ContactInfoReq struct { + Type string `json:"type" form:"type"` + Value string `json:"value" form:"value"` +} + +type RecruitFormResp struct { + Message string `json:"message"` +} + +func RecruitForm(req *RecruitFormReq) (resp RecruitFormResp, err error) { + if filter, ok := buildRecruitContactFilter(req.Contact); ok { + var exist = &officialWebsitemod.RecruitForm{} + err = exist.FindOne(filter) + if err != nil { + return + } + if exist.ID != primitive.NilObjectID { + resp = RecruitFormResp{ + Message: "您已提交过了,我们会尽快联系您", + } + return resp, nil + } + } + + form := &officialWebsitemod.RecruitForm{ + Name: req.Name, + Sex: req.Sex, + Age: req.Age, + Country: req.Country, + Address: req.Address, + Description: req.Description, + } + for _, v := range req.Contact { + form.Contact = append(form.Contact, officialWebsitemod.ContactInfo{ + Type: v.Type, + Value: v.Value, + }) + } + err = form.Create() + if err != nil { + return + } + resp = RecruitFormResp{ + Message: "提交成功", + } + return resp, nil +} + +func buildRecruitContactFilter(contacts []ContactInfoReq) (officialWebsitemod.M, bool) { + if len(contacts) == 0 { + return nil, false + } + + orConditions := make([]officialWebsitemod.M, 0, len(contacts)) + for _, c := range contacts { + if c.Type == "" || c.Value == "" { + continue + } + orConditions = append(orConditions, officialWebsitemod.M{ + "contact": bson.M{ + "$elemMatch": bson.M{ + "type": c.Type, + "value": c.Value, + }, + }, + }) + } + + if len(orConditions) == 0 { + return nil, false + } + + if len(orConditions) == 1 { + return orConditions[0], true + } + + return officialWebsitemod.M{"$or": orConditions}, true +} diff --git a/app/service/officialWebsiteser/tag.go b/app/service/officialWebsiteser/tag.go new file mode 100644 index 0000000..03527fe --- /dev/null +++ b/app/service/officialWebsiteser/tag.go @@ -0,0 +1,48 @@ +package officialWebsiteser + +import ( + "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type TagListReq struct { + Type string `json:"type" form:"type" binding:"required,oneof=video hero album all"` +} + +type TagListResp struct { + List []*TagResp `json:"list"` +} + +type TagResp struct { + ID string `json:"id"` + Name string `json:"name"` +} + +func TagList(req *TagListReq) (resp TagListResp, err error) { + var filter = bson.M{} + switch req.Type { + case "": + filter = bson.M{} + case "all": + filter = bson.M{} + default: + filter = bson.M{"type": req.Type} + } + filter["isActive"] = true + data, err := (&officialWebsitemod.Tag{}).FindMany(filter, options.Find().SetSort(bson.D{{Key: "isHot", Value: -1}, {Key: "sort", Value: -1}, {Key: "createdAt", Value: -1}})) + if err != nil { + return + } + resp = TagListResp{ + List: make([]*TagResp, 0), + } + for _, v := range data { + resp.List = append(resp.List, &TagResp{ + ID: v.ID.Hex(), + Name: v.Name, + }) + } + return resp, nil +} diff --git a/app/service/officialWebsiteser/video.go b/app/service/officialWebsiteser/video.go new file mode 100644 index 0000000..322ca89 --- /dev/null +++ b/app/service/officialWebsiteser/video.go @@ -0,0 +1,227 @@ +package officialWebsiteser + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/officialWebsitemod" + "91porn-server/models/v/vidmod" + "sync" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type VideoListReq struct { + Type int64 `json:"type" form:"type"` // 1: 全量,2:专辑ID,3:演员ID, 4: TagID + ID string `json:"id" form:"id"` // 专辑ID或者演员ID,根据Type决定 + Sort int64 `json:"sort" form:"sort"` // 1:热度,2:创建时间,3: 播放量,4:点赞量,5:评论量,6:收藏量 + PageNumber int64 `json:"pageNumber" form:"pageNumber"` + PageSize int64 `json:"pageSize" form:"pageSize"` +} + +type VideoListResp struct { + HasNext bool `json:"hasNext"` + List []*VideoResp `json:"list"` +} + +type VideoResp struct { + ID string `json:"id"` + Title string `json:"title"` + SeoSlug string `json:"seoSlug"` + Cover string `json:"cover"` + IsHot bool `json:"isHot"` + Tags []VideoTagResp `json:"tags"` +} + +type VideoTagResp struct { + ID string `json:"id"` + Name string `json:"name"` +} + +func VideoList(req *VideoListReq) (resp VideoListResp, err error) { + var sort interface{} + switch req.Sort { + case 2: + sort = bson.D{{Key: "createdAt", Value: -1}} + case 3: + sort = bson.D{{Key: "watchCount", Value: -1}} + case 4: + sort = bson.D{{Key: "likeCount", Value: -1}} + case 5: + sort = bson.D{{Key: "commentCount", Value: -1}} + case 6: + sort = bson.D{{Key: "collectCount", Value: -1}} + default: + sort = bson.D{{Key: "isHot", Value: -1}, {Key: "createdAt", Value: -1}} + } + var filter = bson.M{} + objID, err := primitive.ObjectIDFromHex(req.ID) + if err != nil && req.Type != 1 { + err = stderr.ErrParamError + return + } + switch req.Type { + case 2: + filter["albumId"] = objID + case 3: + filter["heroId"] = objID + case 4: + filter["tags._id"] = objID + } + filter["isActive"] = true + + var skip, limit = transPageAndPageNumber(req.PageSize, req.PageNumber) + var ops = options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + videosData, err := (&officialWebsitemod.Video{}).FindMany(filter, ops) + if err != nil { + return + } + + resp = VideoListResp{ + HasNext: len(videosData) > int(limit), + List: make([]*VideoResp, 0), + } + for _, v := range videosData { + var videoResp = &VideoResp{ + ID: v.ID.Hex(), + Title: v.Title, + SeoSlug: v.SeoSlug, + Cover: v.Cover, + IsHot: v.IsHot, + Tags: make([]VideoTagResp, 0), + } + for _, tag := range v.Tags { + videoResp.Tags = append(videoResp.Tags, VideoTagResp{ + ID: tag.ID.Hex(), + Name: tag.Name, + }) + } + resp.List = append(resp.List, videoResp) + } + if resp.HasNext { + resp.List = resp.List[:len(resp.List)-1] + } + + return +} + +type VideoDetailReq struct { + ID string `uri:"id" binding:"required"` +} + +type VideoDetailResp struct { + ID string `json:"id"` + Title string `json:"title"` + SeoSlug string `json:"seoSlug"` + Description string `json:"description"` + Cover string `json:"cover"` + Url string `json:"url"` + PlayTime uint `json:"playTime"` // 视频播放时长(秒) + IsHot bool `json:"isHot"` + Tags []VideoTagResp `json:"tags"` + WatchCount int64 `json:"watchCount"` + LikeCount int64 `json:"likeCount"` + CommentCount int64 `json:"commentCount"` + CollectCount int64 `json:"collectCount"` + RelatedVideos []VideoResp `json:"relatedVideos"` + CreatedAt time.Time `json:"createdAt"` +} + +func VideoDetail(req *VideoDetailReq) (resp VideoDetailResp, err error) { + // 兼容 id 与 seoSlug:24 位 hex 按 _id 查,否则按 seoSlug 查 + filter := bson.M{"isActive": true} + if id, e := primitive.ObjectIDFromHex(req.ID); e == nil { + filter["_id"] = id + } else { + filter["seoSlug"] = req.ID + } + video := &officialWebsitemod.Video{} + err = video.FindOne(filter) + if err != nil { + return + } + if video.ID == primitive.NilObjectID { + err = stderr.OfficialWebsiteVideoNotExist + return + } + + resp = VideoDetailResp{ + ID: video.ID.Hex(), + Title: video.Title, + SeoSlug: video.SeoSlug, + Description: video.Description, + Cover: video.Cover, + Url: video.Url, + IsHot: video.IsHot, + Tags: make([]VideoTagResp, 0), + WatchCount: video.WatchCount, + LikeCount: video.LikeCount, + CommentCount: video.CommentCount, + CollectCount: video.CollectCount, + CreatedAt: video.CreatedAt, + } + // 播放时长:官网视频复用了视频库(vidmod)的视频 ID,按 ID 反查视频库获取 + if vid, e := vidmod.GetVideoByID(video.ID); e == nil && vid != nil { + resp.PlayTime = vid.PlayTime + } + for _, tag := range video.Tags { + resp.Tags = append(resp.Tags, VideoTagResp{ + ID: tag.ID.Hex(), + Name: tag.Name, + }) + } + + var tagsID = make([]primitive.ObjectID, 0) + for _, tag := range video.Tags { + tagsID = append(tagsID, tag.ID) + } + var videoTags []officialWebsitemod.Video + var videoNormal []officialWebsitemod.Video + var wg = &sync.WaitGroup{} + wg.Add(2) + common.Go(func() { + defer wg.Done() + if len(tagsID) == 0 { + return + } + filterTags := bson.M{"tags.id": bson.M{"$in": tagsID}, "isActive": true} + videoTags, _ = (&officialWebsitemod.Video{}).FindMany(filterTags, + options.Find().SetSort(bson.D{{Key: "isHot", Value: -1}, {Key: "createdAt", Value: -1}}).SetLimit(10)) + }) + common.Go(func() { + defer wg.Done() + filterNormal := bson.M{"isActive": true} + videoNormal, _ = (&officialWebsitemod.Video{}).FindMany(filterNormal, + options.Find().SetSort(bson.D{{Key: "isHot", Value: -1}, {Key: "createdAt", Value: -1}}).SetLimit(10)) + }) + wg.Wait() + + var relatedVideosData []officialWebsitemod.Video + if len(videoTags) > 0 { + relatedVideosData = videoTags + } else { + relatedVideosData = videoNormal + } + + resp.RelatedVideos = make([]VideoResp, 0) + for _, v := range relatedVideosData { + var videoResp = VideoResp{ + ID: v.ID.Hex(), + Title: v.Title, + SeoSlug: v.SeoSlug, + Cover: v.Cover, + IsHot: v.IsHot, + Tags: make([]VideoTagResp, 0), + } + for _, tag := range v.Tags { + videoResp.Tags = append(videoResp.Tags, VideoTagResp{ + ID: tag.ID.Hex(), + Name: tag.Name, + }) + } + resp.RelatedVideos = append(resp.RelatedVideos, videoResp) + } + return +} diff --git a/app/service/officialser/official.go b/app/service/officialser/official.go new file mode 100644 index 0000000..f8f43e7 --- /dev/null +++ b/app/service/officialser/official.go @@ -0,0 +1,62 @@ +package officialser + +import ( + "91porn-server/app/service/adser" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/officialmod" + "strconv" + "strings" +) + +// QueryAll 查询列表 +func QueryAll(in *officialmod.QueryCond, ver, sysType string) ([]*officialmod.OfficialConfigApp, stderr.Code) { + //应用推荐模块的广告,改到由数据中心拿数据 + if in.Type == "1" { + // 这里不能直接拷贝,可能其它产品的位置ID不一样 + jtAds, err := adser.JtAdvertiseThreeServer() + if err != nil { + log.Error("JtAdvertiseThreeServer error occur", log.E(err)) + return nil, stderr.Failure + } + + list := make([]*officialmod.OfficialConfigApp, 0) + for _, loc := range jtAds { + pos, err := strconv.Atoi(loc.AdvertiseLocationCode) + if err != nil { + continue + } + if pos != 200 && pos != 201 { + continue + } + for _, ad := range loc.AdDetailInfoList { + if pos == 200 { + list = append(list, &officialmod.OfficialConfigApp{ + OfficialName: ad.AdvertiseName, + OfficialDesc: ad.AdvertiseName, + OfficialImg: ad.GetCoverLsj(), + OfficialUrl: strings.ReplaceAll(ad.AdvertiseUrl, "inner://yinseinner/", "yinseinner://"), + Position: 1, + OfficialType: "1", + }) + } + if pos == 201 { + list = append(list, &officialmod.OfficialConfigApp{ + OfficialName: ad.AdvertiseName, + OfficialDesc: ad.AdvertiseName, + OfficialImg: ad.GetCoverLsj(), + OfficialUrl: strings.ReplaceAll(ad.AdvertiseUrl, "inner://yinseinner/", "yinseinner://"), + Position: 2, + OfficialType: "1", + }) + } + } + } + return list, stderr.Success + } + list, err := officialmod.Query(in.Type) + if err != nil { + return nil, stderr.ErrDbQueryError + } + return list, stderr.Success +} diff --git a/app/service/paymentguideser/payment_guide.go b/app/service/paymentguideser/payment_guide.go new file mode 100644 index 0000000..d1e6996 --- /dev/null +++ b/app/service/paymentguideser/payment_guide.go @@ -0,0 +1,349 @@ +package paymentguideser + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + "time" + + "91porn-server/app/proto" + "91porn-server/app/service/sys_config" + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/paymentguidemod" + "91porn-server/models/v/sysconfmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type GuideResp struct { + Show bool `json:"show"` + TotalWatchCount *uint64 `json:"totalWatchCount,omitempty"` + ConfigID string `json:"configId,omitempty"` + ContentVersion string `json:"contentVersion,omitempty"` + Segment string `json:"segment"` + Style string `json:"style,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Cover string `json:"cover,omitempty"` + Videos []vidmod.PaymentGuideVideo `json:"videos,omitempty"` + ProductID string `json:"productId,omitempty"` + DurationSeconds int64 `json:"durationSeconds,omitempty"` + Action *paymentguidemod.Action `json:"action,omitempty"` +} + +type ImpressionReq struct { + ConfigID string `json:"configId" binding:"required"` + Scene string `json:"scene" binding:"required"` + ContentVersion string `json:"contentVersion"` + VideoID string `json:"videoId"` + RequestID string `json:"requestId" binding:"required"` +} + +const videoPlayCountMultiplier = 99 + +var pingGuideScenes = []string{ + paymentguidemod.SceneHomeNewUserFreeTrial, + paymentguidemod.SceneHomeOldUser, + paymentguidemod.SceneVideoPreviewEnd, + paymentguidemod.SceneDiscountCountdown, + paymentguidemod.SceneVideoBack, + paymentguidemod.SceneVIPCenter, + paymentguidemod.SceneVIPContentUpdate, +} + +// impressionLimitedPingGuideScenes contains only scenes whose show state is +// suppressed after the same configuration has already been displayed. The +// discount countdown is intentionally excluded because the App controls its +// once-per-launch behavior and starts a fresh local countdown on every launch. +var impressionLimitedPingGuideScenes = map[string]struct{}{ + paymentguidemod.SceneHomeNewUserFreeTrial: {}, + paymentguidemod.SceneHomeOldUser: {}, + paymentguidemod.SceneVideoPreviewEnd: {}, + paymentguidemod.SceneVideoBack: {}, + paymentguidemod.SceneVIPCenter: {}, +} + +type paymentGuideSwitchState struct { + global bool + home bool +} + +func contentVersion(videos []vidmod.PaymentGuideVideo) string { + ids := make([]string, 0, len(videos)) + for _, video := range videos { + ids = append(ids, video.ID) + } + sum := sha256.Sum256([]byte(strings.Join(ids, ","))) + return hex.EncodeToString(sum[:8]) +} + +func SegmentForUser(user *usermod.User) string { + switch user.GetPaymentStatusPopup() { + case usermod.UserPaymentStatusPopupNewUnpay: + return paymentguidemod.SegmentNewNeverPaid + case usermod.UserPaymentStatusPopupUnder7DayUnpay, + usermod.UserPaymentStatusPopupUnder7DayUnpayNoCountdown, + usermod.UserPaymentStatusPopupOver7DayUnpay: + return paymentguidemod.SegmentOldNeverPaid + case usermod.UserPaymentStatusPopupOver7DayNeedUpgrade: + return paymentguidemod.SegmentPaidUpgrade + case usermod.UserPaymentStatusPopupMaxVIPLevel: + return paymentguidemod.SegmentMaxVIP + case usermod.UserPaymentStatusPopupUnregistered: + return paymentguidemod.SegmentUnregistered + default: + return paymentguidemod.SegmentNormal + } +} + +func hasFreeTrialRemaining(user *usermod.User) bool { + return user != nil && user.WatchCount > 0 +} + +func paymentGuideSwitches() (paymentGuideSwitchState, error) { + values, err := sysconfdata.GetBoolsFromSharedCache( + sysconfmod.VCodePaymentGuideEnabled, + sysconfmod.VCodePaymentGuideHomeEnabled, + ) + if err != nil { + return paymentGuideSwitchState{}, err + } + return paymentGuideSwitchesFromValues(values), nil +} + +func paymentGuideSwitchesFromValues(values map[sysconfmod.VCode]bool) paymentGuideSwitchState { + homeEnabled := true + if configured, exists := values[sysconfmod.VCodePaymentGuideHomeEnabled]; exists { + homeEnabled = configured + } + return paymentGuideSwitchState{ + global: values[sysconfmod.VCodePaymentGuideEnabled], + home: homeEnabled, + } +} + +func (s paymentGuideSwitchState) sceneEnabled(scene string) bool { + if scene == paymentguidemod.SceneHomeNewUser { + return false + } + if !s.global { + return false + } + if scene == paymentguidemod.SceneHomeNewUserFreeTrial || scene == paymentguidemod.SceneHomeOldUser { + return s.home + } + return true +} + +// GetPingGuide resolves every payment-guide scene in two bounded database +// operations: one active-config aggregation and one impression lookup for the +// scenes that are suppressed after display. VIP_CONTENT_UPDATE only exposes +// its enabled state because videos and contentVersion must be evaluated in real +// time by GetGuide. +func GetPingGuide(user *usermod.User) (proto.PaymentGuidePing, error) { + segment := SegmentForUser(user) + totalWatchCount := sys_config.GetTotalWatchCount() + freeTrialRemaining := hasFreeTrialRemaining(user) + switches, err := paymentGuideSwitches() + if err != nil { + return buildPingGuide(paymentGuideSwitchState{}, segment, totalWatchCount, freeTrialRemaining, nil, nil), err + } + if !switches.global { + return buildPingGuide(switches, segment, totalWatchCount, freeTrialRemaining, nil, nil), nil + } + configs, err := paymentguidemod.FindActiveByScenes(pingGuideScenes, segment, time.Now()) + if err != nil { + return buildPingGuide(switches, segment, totalWatchCount, freeTrialRemaining, nil, nil), err + } + + configIDs := make([]primitive.ObjectID, 0, len(impressionLimitedPingGuideScenes)) + for scene, config := range configs { + if _, limited := impressionLimitedPingGuideScenes[scene]; limited && switches.sceneEnabled(scene) { + configIDs = append(configIDs, config.ID) + } + } + var uid uint64 + if user != nil { + uid = user.UID + } + shown, err := paymentguidemod.FindImpressionConfigIDs(uid, configIDs) + if err != nil { + // 展示记录不可用时失败关闭,避免已展示过的普通弹窗重复弹出。 + return buildPingGuide(switches, segment, totalWatchCount, freeTrialRemaining, nil, nil), err + } + return buildPingGuide(switches, segment, totalWatchCount, freeTrialRemaining, configs, shown), nil +} + +func buildPingGuide( + switches paymentGuideSwitchState, + segment string, + totalWatchCount uint64, + freeTrialRemaining bool, + configs map[string]paymentguidemod.PaymentGuide, + shown map[primitive.ObjectID]bool, +) proto.PaymentGuidePing { + resp := proto.PaymentGuidePing{ + Enabled: switches.global, + Segment: segment, + Scenes: make(map[string]proto.PaymentGuidePingScene, len(pingGuideScenes)), + } + for _, scene := range pingGuideScenes { + item := proto.PaymentGuidePingScene{} + if scene == paymentguidemod.SceneHomeNewUserFreeTrial { + item.TotalWatchCount = &totalWatchCount + } + if scene != paymentguidemod.SceneVIPContentUpdate { + show := false + item.Show = &show + } + config, exists := configs[scene] + if !switches.sceneEnabled(scene) || !exists { + resp.Scenes[scene] = item + continue + } + item.Enabled = true + if scene == paymentguidemod.SceneVIPContentUpdate { + resp.Scenes[scene] = item + continue + } + fillPingSceneConfig(&item, config) + show := true + if _, limited := impressionLimitedPingGuideScenes[scene]; limited { + show = !shown[config.ID] + } + if scene == paymentguidemod.SceneHomeNewUserFreeTrial && !freeTrialRemaining { + show = false + } + item.Show = &show + resp.Scenes[scene] = item + } + return resp +} + +func fillPingSceneConfig(item *proto.PaymentGuidePingScene, config paymentguidemod.PaymentGuide) { + item.ConfigID = config.ID.Hex() + item.Style = config.Style + item.Title = config.Title + description := config.Description + cover := config.Cover + productID := config.ProductID + durationSeconds := config.DurationSeconds + action := config.Action + item.Description = &description + item.Cover = &cover + item.ProductID = &productID + item.DurationSeconds = &durationSeconds + item.Action = &action +} + +func GetGuide(uid uint64, scene string) (GuideResp, error) { + if !paymentguidemod.ValidScene(scene) { + return GuideResp{}, fmt.Errorf("unsupported scene: %s", scene) + } + user, err := usermod.FindUserByUID(uid) + if err != nil { + return GuideResp{}, err + } + segment := SegmentForUser(user) + resp := GuideResp{Segment: segment} + if scene == paymentguidemod.SceneHomeNewUserFreeTrial { + totalWatchCount := sys_config.GetTotalWatchCount() + resp.TotalWatchCount = &totalWatchCount + } + switches, err := paymentGuideSwitches() + if err != nil { + return resp, err + } + if !switches.sceneEnabled(scene) { + return resp, nil + } + if scene == paymentguidemod.SceneHomeNewUserFreeTrial && !hasFreeTrialRemaining(user) { + return resp, nil + } + config, err := paymentguidemod.FindActive(scene, segment, time.Now()) + if err != nil || config == nil { + return resp, err + } + if scene == paymentguidemod.SceneVIPContentUpdate { + excludedModuleIDs, queryErr := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), false) + if queryErr != nil { + return GuideResp{}, queryErr + } + resp.Videos, queryErr = vidmod.LatestVIPContent(config.VIPContentVideoLimit(), excludedModuleIDs) + if queryErr != nil { + return GuideResp{}, queryErr + } + if len(resp.Videos) == 0 { + return resp, nil + } + for i := range resp.Videos { + resp.Videos[i].PlayCount = (resp.Videos[i].PlayCount + resp.Videos[i].FakePlayCount) * videoPlayCountMultiplier + } + resp.ContentVersion = contentVersion(resp.Videos) + } + if scene != paymentguidemod.SceneDiscountCountdown { + shown, impressionErr := paymentguidemod.HasImpression(uid, config.ID, scene, resp.ContentVersion) + if impressionErr != nil { + return resp, impressionErr + } + if shown { + resp.Videos = nil + resp.ContentVersion = "" + return resp, nil + } + } + resp.Show = true + resp.ConfigID = config.ID.Hex() + resp.Style = config.Style + resp.Title = config.Title + resp.Description = config.Description + resp.Cover = config.Cover + resp.ProductID = config.ProductID + resp.DurationSeconds = config.DurationSeconds + resp.Action = &config.Action + return resp, nil +} + +func RecordImpression(uid uint64, req ImpressionReq) error { + configID, err := primitive.ObjectIDFromHex(req.ConfigID) + if err != nil { + return fmt.Errorf("invalid configId") + } + if !paymentguidemod.ValidScene(req.Scene) { + return fmt.Errorf("unsupported scene: %s", req.Scene) + } + if _, err = uuid.Parse(req.RequestID); err != nil { + return fmt.Errorf("invalid requestId") + } + req.ContentVersion = strings.TrimSpace(req.ContentVersion) + if req.Scene == paymentguidemod.SceneVIPContentUpdate && req.ContentVersion == "" { + return fmt.Errorf("contentVersion is required") + } + config, err := paymentguidemod.GetByID(configID) + if err != nil { + return err + } + if config.Scene != req.Scene { + return fmt.Errorf("scene does not match config") + } + user, err := usermod.FindUserByUID(uid) + if err != nil { + return err + } + if !config.MatchesSegment(SegmentForUser(user)) { + return fmt.Errorf("config does not match user segment") + } + return paymentguidemod.RecordImpression(paymentguidemod.Impression{ + UID: uid, + ConfigID: configID, + Scene: req.Scene, + ContentVersion: req.ContentVersion, + VideoID: req.VideoID, + RequestID: req.RequestID, + }) +} diff --git a/app/service/paymentguideser/payment_guide_test.go b/app/service/paymentguideser/payment_guide_test.go new file mode 100644 index 0000000..b2efe9c --- /dev/null +++ b/app/service/paymentguideser/payment_guide_test.go @@ -0,0 +1,397 @@ +package paymentguideser + +import ( + "encoding/json" + "strings" + "testing" + "time" + + "91porn-server/models/v/paymentguidemod" + "91porn-server/models/v/sysconfmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestSegmentForUser(t *testing.T) { + now := time.Now() + tests := []struct { + name string + user *usermod.User + want string + }{ + { + name: "new unpaid", + user: &usermod.User{ + CreatedAt: now.Add(-time.Hour), + WatchCount: 3, + }, + want: paymentguidemod.SegmentNewNeverPaid, + }, + { + name: "new user with exhausted free views stays new unpaid", + user: &usermod.User{ + CreatedAt: now.Add(-time.Hour), + WatchCount: 0, + }, + want: paymentguidemod.SegmentNewNeverPaid, + }, + { + name: "expired is old unpaid", + user: &usermod.User{ + CreatedAt: now.AddDate(0, 0, -30), + VipLevel: 2, + VipExpireDate: now.Add(-time.Hour), + }, + want: paymentguidemod.SegmentOldNeverPaid, + }, + { + name: "active paid upgrade", + user: &usermod.User{ + CreatedAt: now.AddDate(0, 0, -30), + VipLevel: 2, + VipExpireDate: now.AddDate(0, 1, 0), + }, + want: paymentguidemod.SegmentPaidUpgrade, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := SegmentForUser(tt.user); got != tt.want { + t.Fatalf("SegmentForUser() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestContentVersion(t *testing.T) { + videos := []vidmod.PaymentGuideVideo{{ID: "video-a"}, {ID: "video-b"}} + got := contentVersion(videos) + if got == "" { + t.Fatal("contentVersion must not be empty") + } + if got != contentVersion(videos) { + t.Fatal("contentVersion must be stable for the same videos") + } + reordered := []vidmod.PaymentGuideVideo{{ID: "video-b"}, {ID: "video-a"}} + if got == contentVersion(reordered) { + t.Fatal("contentVersion must change when video order changes") + } + expanded := append(append([]vidmod.PaymentGuideVideo{}, videos...), vidmod.PaymentGuideVideo{ID: "video-c"}) + if got == contentVersion(expanded) { + t.Fatal("contentVersion must change when configured video count changes") + } +} + +func TestPaymentGuideSwitchesDefaultLegacyHomeToEnabled(t *testing.T) { + legacy := paymentGuideSwitchesFromValues(map[sysconfmod.VCode]bool{ + sysconfmod.VCodePaymentGuideEnabled: true, + }) + if !legacy.global || !legacy.home { + t.Fatalf("legacy switches must keep the home scenes enabled: %+v", legacy) + } + disabled := paymentGuideSwitchesFromValues(map[sysconfmod.VCode]bool{ + sysconfmod.VCodePaymentGuideEnabled: true, + sysconfmod.VCodePaymentGuideHomeEnabled: false, + }) + if !disabled.global || disabled.home { + t.Fatalf("configured home switch must be honored: %+v", disabled) + } +} + +func TestGuideRespUsesVideosWithoutVideoIDs(t *testing.T) { + body, err := json.Marshal(GuideResp{ + Show: true, + Videos: []vidmod.PaymentGuideVideo{{ID: "video-a"}}, + }) + if err != nil { + t.Fatal(err) + } + jsonBody := string(body) + if !strings.Contains(jsonBody, `"videos"`) { + t.Fatalf("response must contain videos: %s", jsonBody) + } + if strings.Contains(jsonBody, `"videoIds"`) { + t.Fatalf("response must not contain videoIds: %s", jsonBody) + } +} + +func TestBuildPingGuide(t *testing.T) { + freeTrialID := primitive.NewObjectID() + homeID := primitive.NewObjectID() + contentID := primitive.NewObjectID() + configs := map[string]paymentguidemod.PaymentGuide{ + paymentguidemod.SceneHomeNewUserFreeTrial: { + ID: freeTrialID, + Scene: paymentguidemod.SceneHomeNewUserFreeTrial, + Style: "BOTTOM_SHEET", + Title: "free trial", + Action: paymentguidemod.Action{Type: "NONE"}, + }, + paymentguidemod.SceneHomeOldUser: { + ID: homeID, + Scene: paymentguidemod.SceneHomeOldUser, + Style: "BOTTOM_SHEET", + Title: "open vip", + ProductID: "product-id", + DurationSeconds: 60, + Action: paymentguidemod.Action{Type: "VIP_PRODUCT", Value: "product-id"}, + }, + paymentguidemod.SceneVIPContentUpdate: { + ID: contentID, + Scene: paymentguidemod.SceneVIPContentUpdate, + }, + } + + resp := buildPingGuide(paymentGuideSwitchState{global: true, home: true}, paymentguidemod.SegmentOldNeverPaid, 3, true, configs, nil) + if !resp.Enabled || resp.Segment != paymentguidemod.SegmentOldNeverPaid { + t.Fatalf("unexpected response header: %+v", resp) + } + if len(resp.Scenes) != len(pingGuideScenes) { + t.Fatalf("scene count = %d, want %d", len(resp.Scenes), len(pingGuideScenes)) + } + freeTrial := resp.Scenes[paymentguidemod.SceneHomeNewUserFreeTrial] + if !freeTrial.Enabled || freeTrial.TotalWatchCount == nil || *freeTrial.TotalWatchCount != 3 || freeTrial.ConfigID != freeTrialID.Hex() { + t.Fatalf("unexpected new-user free-trial scene: %+v", freeTrial) + } + home := resp.Scenes[paymentguidemod.SceneHomeOldUser] + if !home.Enabled || home.Show == nil || !*home.Show || home.ConfigID != homeID.Hex() { + t.Fatalf("unexpected ordinary scene: %+v", home) + } + if home.Action == nil || home.Action.Value != "product-id" { + t.Fatalf("ordinary scene action missing: %+v", home) + } + content := resp.Scenes[paymentguidemod.SceneVIPContentUpdate] + if !content.Enabled || content.Show != nil || content.ConfigID != "" { + t.Fatalf("content-update Ping must expose enabled only: %+v", content) + } + missing := resp.Scenes[paymentguidemod.SceneVideoBack] + if missing.Enabled || missing.Show == nil || *missing.Show { + t.Fatalf("missing ordinary scene must be disabled: %+v", missing) + } +} + +func TestBuildPingGuideSuppressesShownAndGlobalDisabled(t *testing.T) { + configID := primitive.NewObjectID() + configs := map[string]paymentguidemod.PaymentGuide{ + paymentguidemod.SceneVIPCenter: { + ID: configID, + Scene: paymentguidemod.SceneVIPCenter, + Style: "CENTER_POPUP", + Title: "shown", + Description: "description", + Cover: "cover.png", + ProductID: "product-id", + DurationSeconds: 0, + Action: paymentguidemod.Action{Type: "VIP_PRODUCT", Value: "product-id"}, + }, + } + resp := buildPingGuide( + paymentGuideSwitchState{global: true, home: true}, + paymentguidemod.SegmentNormal, + 3, + true, + configs, + map[primitive.ObjectID]bool{configID: true}, + ) + vipCenter := resp.Scenes[paymentguidemod.SceneVIPCenter] + if !vipCenter.Enabled || vipCenter.Show == nil || *vipCenter.Show || vipCenter.ConfigID != configID.Hex() { + t.Fatalf("shown configuration must retain its display payload: %+v", vipCenter) + } + if vipCenter.Description == nil || *vipCenter.Description != "description" || + vipCenter.Cover == nil || *vipCenter.Cover != "cover.png" || + vipCenter.ProductID == nil || *vipCenter.ProductID != "product-id" || + vipCenter.DurationSeconds == nil || *vipCenter.DurationSeconds != 0 || + vipCenter.Action == nil || vipCenter.Action.Value != "product-id" { + t.Fatalf("shown configuration payload is incomplete: %+v", vipCenter) + } + + disabled := buildPingGuide(paymentGuideSwitchState{}, paymentguidemod.SegmentNormal, 3, true, configs, nil) + if disabled.Enabled { + t.Fatal("global-disabled response must be disabled") + } + for scene, item := range disabled.Scenes { + if item.Enabled { + t.Fatalf("scene %s must be disabled", scene) + } + if scene != paymentguidemod.SceneVIPContentUpdate && (item.Show == nil || *item.Show) { + t.Fatalf("ordinary scene %s must explicitly return show=false", scene) + } + } +} + +func TestBuildPingGuideUsesSharedHomeSwitch(t *testing.T) { + freeTrialID := primitive.NewObjectID() + homeOldID := primitive.NewObjectID() + videoBackID := primitive.NewObjectID() + configs := map[string]paymentguidemod.PaymentGuide{ + paymentguidemod.SceneHomeNewUserFreeTrial: {ID: freeTrialID, Scene: paymentguidemod.SceneHomeNewUserFreeTrial}, + paymentguidemod.SceneHomeOldUser: {ID: homeOldID, Scene: paymentguidemod.SceneHomeOldUser}, + paymentguidemod.SceneVideoBack: {ID: videoBackID, Scene: paymentguidemod.SceneVideoBack}, + } + resp := buildPingGuide( + paymentGuideSwitchState{global: true, home: false}, + paymentguidemod.SegmentNormal, + 3, + true, + configs, + nil, + ) + for _, scene := range []string{paymentguidemod.SceneHomeNewUserFreeTrial, paymentguidemod.SceneHomeOldUser} { + item := resp.Scenes[scene] + if item.Enabled || item.Show == nil || *item.Show || item.ConfigID != "" { + t.Fatalf("home scene %s must be disabled by the shared switch: %+v", scene, item) + } + } + videoBack := resp.Scenes[paymentguidemod.SceneVideoBack] + if !videoBack.Enabled || videoBack.Show == nil || !*videoBack.Show || videoBack.ConfigID != videoBackID.Hex() { + t.Fatalf("non-home scene must not be affected by the home switch: %+v", videoBack) + } +} + +func TestBuildPingGuideCountdownIsNotSuppressedByImpression(t *testing.T) { + configID := primitive.NewObjectID() + configs := map[string]paymentguidemod.PaymentGuide{ + paymentguidemod.SceneDiscountCountdown: { + ID: configID, + Scene: paymentguidemod.SceneDiscountCountdown, + Style: "CENTER_POPUP", + Title: "限时优惠", + Description: "前端本地倒计时", + Cover: "countdown.png", + ProductID: "product-id", + DurationSeconds: 0, + Action: paymentguidemod.Action{Type: "VIP_PRODUCT", Value: "product-id"}, + }, + } + resp := buildPingGuide( + paymentGuideSwitchState{global: true, home: true}, + paymentguidemod.SegmentNormal, + 3, + true, + configs, + map[primitive.ObjectID]bool{configID: true}, + ) + countdown := resp.Scenes[paymentguidemod.SceneDiscountCountdown] + if !countdown.Enabled || countdown.Show == nil || !*countdown.Show || countdown.ConfigID != configID.Hex() { + t.Fatalf("countdown must remain displayable on every App launch: %+v", countdown) + } + if countdown.DurationSeconds == nil || *countdown.DurationSeconds != 0 { + t.Fatalf("countdown must return the complete configured card: %+v", countdown) + } +} + +func TestPaymentGuidePingJSONOmitsContentUpdateShow(t *testing.T) { + body, err := json.Marshal(buildPingGuide(paymentGuideSwitchState{}, paymentguidemod.SegmentUnregistered, 3, false, nil, nil)) + if err != nil { + t.Fatal(err) + } + var decoded struct { + Scenes map[string]map[string]interface{} `json:"scenes"` + } + var envelope struct { + Enabled bool `json:"enabled"` + Segment string `json:"segment"` + Scenes map[string]map[string]interface{} `json:"scenes"` + } + if err = json.Unmarshal(body, &envelope); err != nil { + t.Fatal(err) + } + decoded.Scenes = envelope.Scenes + if _, exists := decoded.Scenes[paymentguidemod.SceneHomeNewUser]; exists { + t.Fatalf("legacy HOME_NEW_USER must not be returned in Ping: %s", body) + } + freeTrial := decoded.Scenes[paymentguidemod.SceneHomeNewUserFreeTrial] + if count, exists := freeTrial["totalWatchCount"]; !exists || count != float64(3) { + t.Fatalf("new-user free-trial Ping must include totalWatchCount=3: %s", body) + } + if _, exists := decoded.Scenes[paymentguidemod.SceneVIPContentUpdate]["show"]; exists { + t.Fatalf("VIP_CONTENT_UPDATE Ping must omit show: %s", body) + } + if show, exists := decoded.Scenes[paymentguidemod.SceneHomeOldUser]["show"]; !exists || show != false { + t.Fatalf("home Ping scene must include show=false: %s", body) + } +} + +func TestPaymentGuidePingJSONKeepsConfiguredFieldsWhenShowFalse(t *testing.T) { + configID := primitive.NewObjectID() + body, err := json.Marshal(buildPingGuide( + paymentGuideSwitchState{global: true, home: true}, + paymentguidemod.SegmentNormal, + 3, + true, + map[string]paymentguidemod.PaymentGuide{ + paymentguidemod.SceneVideoBack: { + ID: configID, + Scene: paymentguidemod.SceneVideoBack, + Style: "CENTER_POPUP", + Title: "返回引导", + Description: "", + Cover: "", + ProductID: "", + DurationSeconds: 0, + Action: paymentguidemod.Action{Type: "NONE"}, + }, + }, + map[primitive.ObjectID]bool{configID: true}, + )) + if err != nil { + t.Fatal(err) + } + var envelope struct { + Scenes map[string]map[string]interface{} `json:"scenes"` + } + if err = json.Unmarshal(body, &envelope); err != nil { + t.Fatal(err) + } + item := envelope.Scenes[paymentguidemod.SceneVideoBack] + for _, field := range []string{"configId", "style", "title", "description", "cover", "productId", "durationSeconds", "action"} { + if _, exists := item[field]; !exists { + t.Fatalf("configured show=false scene must include %s: %s", field, body) + } + } + if show, ok := item["show"].(bool); !ok || show { + t.Fatalf("configured scene must preserve show=false: %s", body) + } +} + +func TestBuildPingGuideSuppressesExhaustedNewUserFreeTrialOnly(t *testing.T) { + freeTrialID := primitive.NewObjectID() + videoBackID := primitive.NewObjectID() + resp := buildPingGuide( + paymentGuideSwitchState{global: true, home: true}, + paymentguidemod.SegmentNewNeverPaid, + 3, + false, + map[string]paymentguidemod.PaymentGuide{ + paymentguidemod.SceneHomeNewUserFreeTrial: { + ID: freeTrialID, + Scene: paymentguidemod.SceneHomeNewUserFreeTrial, + Style: "BOTTOM_SHEET", + Title: "free trial", + Action: paymentguidemod.Action{Type: "NONE"}, + }, + paymentguidemod.SceneVideoBack: { + ID: videoBackID, + Scene: paymentguidemod.SceneVideoBack, + Style: "BOTTOM_SHEET", + Title: "video back", + Action: paymentguidemod.Action{Type: "NONE"}, + }, + }, + nil, + ) + + if resp.Segment != paymentguidemod.SegmentNewNeverPaid { + t.Fatalf("segment = %q, want %q", resp.Segment, paymentguidemod.SegmentNewNeverPaid) + } + freeTrial := resp.Scenes[paymentguidemod.SceneHomeNewUserFreeTrial] + if !freeTrial.Enabled || freeTrial.Show == nil || *freeTrial.Show || freeTrial.ConfigID != freeTrialID.Hex() { + t.Fatalf("exhausted free-trial scene must retain config with show=false: %+v", freeTrial) + } + videoBack := resp.Scenes[paymentguidemod.SceneVideoBack] + if !videoBack.Enabled || videoBack.Show == nil || !*videoBack.Show || videoBack.ConfigID != videoBackID.Hex() { + t.Fatalf("free-trial exhaustion must not suppress other matching scenes: %+v", videoBack) + } +} diff --git a/app/service/productser/drama.go b/app/service/productser/drama.go new file mode 100644 index 0000000..65094c5 --- /dev/null +++ b/app/service/productser/drama.go @@ -0,0 +1,196 @@ +package productser + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "strings" + "time" + + "91porn-server/app/appg" + "91porn-server/app/service/mediacontentser" + + "91porn-server/common/db" + "91porn-server/common/log" + redisutil "91porn-server/common/redis" + "91porn-server/common/stderr" + "91porn-server/middleware/ua" + "91porn-server/models/v/media_buy_record_mod" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + dramaBuyRequestTTL = 24 * time.Hour + dramaBuyLockTTL = 2 * time.Minute +) + +var errDramaAlreadyBought = errors.New("drama episode already bought") + +var releaseDramaBuyLockScript = redisutil.NewScript(` +if redis.call('GET', KEYS[1]) ~= ARGV[1] then + return 0 +end +return redis.call('DEL', KEYS[1]) +`) + +type BuyDramaEpisodeResponse struct { + OrderID primitive.ObjectID `json:"orderId"` + MediaID primitive.ObjectID `json:"mediaId"` + ContentID primitive.ObjectID `json:"contentId"` + AccessType string `json:"accessType"` + CanPlay bool `json:"canPlay"` + PaidCoin int64 `json:"paidCoin"` + CoinBalance int64 `json:"coinBalance"` +} + +func BuyDramaEpisode( + uid uint64, + mediaID, contentID primitive.ObjectID, + checkoutContextID, requestID string, + uaInfo ua.UA, + ip string, +) (BuyDramaEpisodeResponse, stderr.Code) { + requestID = strings.TrimSpace(requestID) + checkoutContextID = strings.TrimSpace(checkoutContextID) + if requestID == "" || len(requestID) > 128 || contentID.IsZero() || mediaID.IsZero() || + !strings.HasPrefix(checkoutContextID, "drama-checkout-") { + return BuyDramaEpisodeResponse{}, stderr.ErrParamError + } + media, err := mediamod.GetInfo(mediaID) + if err != nil || media.ID.IsZero() || media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete { + return BuyDramaEpisodeResponse{}, stderr.InvalidProduct + } + content, err := mediacontentmod.GetInfo(contentID, true) + if err != nil || content.ID.IsZero() || content.MediaID != mediaID || content.MediaType != mediamod.MediaTypeDrama || content.IsDelete { + return BuyDramaEpisodeResponse{}, stderr.InvalidProduct + } + if bought, buyErr := media_buy_record_mod.IsBuy(uid, mediaID, contentID); buyErr != nil { + return BuyDramaEpisodeResponse{}, stderr.PayBusy + } else if bought { + return BuyDramaEpisodeResponse{}, stderr.RepeatPurchase + } + hasCard, cardErr := mediacontentser.ActiveDramaCardStatus(uid) + if cardErr != nil { + return BuyDramaEpisodeResponse{}, stderr.PayBusy + } + isFree, canPlay, accessType := mediacontentser.ResolveDramaAccess(content, media, false, hasCard) + if isFree || (canPlay && accessType == "card") { + return BuyDramaEpisodeResponse{}, stderr.RepeatPurchase + } + if content.Price <= 0 { + return BuyDramaEpisodeResponse{}, stderr.InvalidProduct + } + wallet, err := walletmod.GetWallet(uid) + if err != nil || wallet == nil || wallet.ID.IsZero() { + return BuyDramaEpisodeResponse{}, stderr.PayBusy + } + plan := debitPlan(wallet, content.Price) + if plan == nil { + return BuyDramaEpisodeResponse{}, stderr.InsufficientBalance + } + user, err := usermod.FindUserByUID(uid) + if err != nil || user == nil { + return BuyDramaEpisodeResponse{}, stderr.PayBusy + } + if appg.Redis == nil { + return BuyDramaEpisodeResponse{}, stderr.PayBusy + } + requestKey := dramaBuyKey("request", uid, requestID) + requestAcquired, err := appg.Redis.SetNX(requestKey, "1", dramaBuyRequestTTL) + if err != nil { + return BuyDramaEpisodeResponse{}, stderr.PayBusy + } + if !requestAcquired { + if bought, _ := media_buy_record_mod.IsBuy(uid, mediaID, contentID); bought { + return BuyDramaEpisodeResponse{}, stderr.RepeatPurchase + } + return BuyDramaEpisodeResponse{}, stderr.PayBusy + } + lockKey := dramaBuyKey("episode", uid, contentID.Hex()) + lockAcquired, err := appg.Redis.SetNX(lockKey, requestID, dramaBuyLockTTL) + if err != nil || !lockAcquired { + _, _ = appg.Redis.Del(requestKey) + return BuyDramaEpisodeResponse{}, stderr.PayBusy + } + defer releaseDramaBuyLock(lockKey, requestID) + + orderID := primitive.NewObjectID() + createdAt := time.Now() + balanceBefore := wallet.Amount + wallet.Income + balanceAfter := balanceBefore + err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + alreadyBought, checkErr := media_buy_record_mod.IsBuyWithTool(t, uid, mediaID, contentID) + if checkErr != nil { + return checkErr + } + if alreadyBought { + return errDramaAlreadyBought + } + updatedWallet, debitErr := walletmod.Debit(t, plan, uid) + if debitErr != nil { + return debitErr + } + balanceAfter = updatedWallet.Amount + updatedWallet.Income + record := &media_buy_record_mod.MediaBuyRecord{ + ID: orderID, MediaId: mediaID, MediaType: mediamod.MediaTypeDrama, + Uid: uid, Type: 0, ContentId: contentID, Coins: content.Price, + PayMoney: content.Price, CreatedAt: createdAt, UpdateTime: createdAt, + } + if createErr := media_buy_record_mod.Create(t, record); createErr != nil { + return createErr + } + transaction := txnmod.TransactionLog{ + TransNo: orderID, UID: uid, Amount: -content.Price, ActualAmount: float64(-content.Price), + TranType: txnmod.BuyAcg.Key(), TranTypeInt: int64(txnmod.BuyAcg), + Desc: fmt.Sprintf("短剧-单集购买-%s-第%d集", media.Title, content.EpisodeNumber), + DiscDoc: user.DiscDoc, SysType: user.SysType, + RealAmount: walletmod.GetRealAmount(updatedWallet), + } + if logErr := txnmod.InsertTransactionLog(t, &transaction); logErr != nil { + return logErr + } + if countErr := mediamod.IncPurchasesCountWithTool(t, mediaID, 1); countErr != nil { + return countErr + } + if countErr := mediacontentmod.IncCountPurchasesWithTool(t, contentID, 1); countErr != nil { + return countErr + } + return nil + }) + if err != nil { + _, _ = appg.Redis.Del(requestKey) + if errors.Is(err, errDramaAlreadyBought) { + return BuyDramaEpisodeResponse{}, stderr.RepeatPurchase + } + log.Error("buy drama episode transaction failed", log.E(err), log.Any("uid", uid), log.Any("contentID", contentID)) + return BuyDramaEpisodeResponse{}, stderr.BuyFailed + } + return BuyDramaEpisodeResponse{ + OrderID: orderID, MediaID: mediaID, ContentID: contentID, + AccessType: "bought", CanPlay: true, PaidCoin: content.Price, CoinBalance: balanceAfter, + }, stderr.Success +} + +func dramaBuyKey(scope string, uid uint64, value string) string { + sum := sha256.Sum256([]byte(fmt.Sprintf("%s:%d:%s", scope, uid, value))) + return "drama:buy:" + scope + ":" + hex.EncodeToString(sum[:]) +} + +func releaseDramaBuyLock(key, token string) { + if appg.Redis == nil || key == "" || token == "" { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + if _, err := appg.Redis.RunScriptContext(ctx, releaseDramaBuyLockScript, []string{key}, token); err != nil { + log.Error("release drama buy lock failed", log.E(err), log.Any("key", key)) + } +} diff --git a/app/service/productser/dto.go b/app/service/productser/dto.go new file mode 100644 index 0000000..fa13307 --- /dev/null +++ b/app/service/productser/dto.go @@ -0,0 +1,17 @@ +package productser + +import "go.mongodb.org/mongo-driver/bson/primitive" + +type ActivityCouponDetailResponse struct { + OriginalPrice int64 `json:"originalPrice" ` // 原价 + DiscountedPrice int64 `json:"discountedPrice"` // 现价 + CouponList []ActivityCoupon `json:"couponList"` // 优惠卷列表 + IsDiscounted bool `json:"isDiscounted"` // 是否折扣 +} + +type ActivityCoupon struct { + ID primitive.ObjectID `json:"id"` // 优惠卷ID + Name string `json:"name"` // 优惠卷名字 + Count int32 `json:"count"` // 优惠卷数量 + DiscountedPrice int64 `json:"discountedPrice"` // 现价 +} diff --git a/app/service/productser/handler.go b/app/service/productser/handler.go new file mode 100644 index 0000000..de81b04 --- /dev/null +++ b/app/service/productser/handler.go @@ -0,0 +1,1401 @@ +package productser + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/taskser" + "91porn-server/common" + "91porn-server/common/game" + "91porn-server/models/v/advanceordermod" + "91porn-server/models/v/backpackmod" + "91porn-server/models/v/oncetaskmod" + "91porn-server/models/v/signrecordmod" + "errors" + "fmt" + "math" + "time" + + "go.mongodb.org/mongo-driver/bson" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/videocoupon" + "91porn-server/models/v/videodiscountmod" + "91porn-server/models/v/walletmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// NewBuyMeetingCard 新版购买约会卡 +func NewBuyMeetingCard(order rchgordmod.RechargeOrder, p *productmod.Product, desc string, w *walletmod.Wallet) func(*db.MongoTool) error { + var ( + //商品购买记录 + history = prdcthsomod.ProductHistory{ + ID: primitive.NewObjectID(), + UID: order.UID, + ProductID: p.ID, + Name: p.Name, + Amount: p.DiscountedPrice, + ProductType: prdcthsomod.MeetingCard, + DiscDoc: order.DiscDoc, + SysType: order.DevType, + ProductSnapShot: p, + } + // 资金流水记录 + transactionLog = &txnmod.TransactionLog{ + UID: order.UID, + TranType: txnmod.MeetingCard.Key(), + TranTypeInt: int64(txnmod.MeetingCard), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + CurrencyType: txnmod.CurrencyTypeCash, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + RealAmount: w.RealAmount(), + } + ) + return func(t *db.MongoTool) error { + // 保存商品购买记录 + if err := prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + // 保存资金流水记录 + return txnmod.InsertTransactionLog(t, transactionLog) + } +} + +// NewBuyOtherCard 新版购买其他卡/实体商品 +func NewBuyOtherCard(order rchgordmod.RechargeOrder, p *productmod.Product, desc string, w *walletmod.Wallet) func(*db.MongoTool) error { + var ( + //商品购买记录 + history = prdcthsomod.ProductHistory{ + ID: primitive.NewObjectID(), + UID: order.UID, + ProductID: p.ID, + Name: p.Name, + Amount: p.DiscountedPrice, + ProductType: prdcthsomod.OTHER, + DiscDoc: order.DiscDoc, + SysType: order.DevType, + ProductSnapShot: p, + } + // 资金流水记录 + transactionLog = &txnmod.TransactionLog{ + UID: order.UID, + TranType: txnmod.Other.Key(), + TranTypeInt: int64(txnmod.Other), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + CurrencyType: txnmod.CurrencyTypeCash, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + RealAmount: w.RealAmount(), + } + ) + return func(t *db.MongoTool) error { + // 保存商品购买记录 + if err := prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + // 保存资金流水记录 + return txnmod.InsertTransactionLog(t, transactionLog) + } +} + +func NewBuyVIP(order rchgordmod.RechargeOrder, p *productmod.Product, desc string, w *walletmod.Wallet) (func(*db.MongoTool) error, time.Time, error) { + var ( + //商品购买记录 + history = prdcthsomod.ProductHistory{ + ID: primitive.NewObjectID(), + UID: order.UID, + ProductID: p.ID, + Name: p.Name, + Amount: p.DiscountedPrice, + ProductType: prdcthsomod.VIP, + SysType: order.DevType, + ProductSnapShot: p, + IsUpgrade: p.IsUpgrade, + CurrentVipName: p.CurrentVipName, + CurrentVipPrice: p.CurrentVipPrice, + PurchasePrice: p.PurchasePrice, + DiscDoc: order.DiscDoc, + } + sel usermod.UserSelector + creditPlan = walletmod.CreditPlan{} + vipLevel int + payVidDiscount int + vipExpireTime time.Time + ) + // 资金流水记录 + txnLogs := []txnmod.TransactionLog{ + txnmod.TransactionLog{ + UID: order.UID, + TranType: txnmod.BuyVIP.Key(), + TranTypeInt: int64(txnmod.BuyVIP), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + CurrencyType: txnmod.CurrencyTypeCash, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + RealAmount: w.RealAmount(), + }, + } + // 查询用户信息 + u, err := usermod.FindUserByUID(order.UID) + if err != nil { + log.Error(fmt.Sprintf("充值购买会员卡-订单号[%s] 查询用户信息异常 [%v]", order.ID, err)) + return nil, vipExpireTime, err + } + // VIP变更 + if p.Duration > 0 { + var expire time.Time + vipLevel = p.VipLevel + payVidDiscount = p.PayVidDiscount + // VIP未过期 + if u.VipExpireDate.After(time.Now()) { + expire = u.VipExpireDate.AddDate(0, 0, p.Duration) + if u.VipLevel > p.VipLevel { + vipLevel = u.VipLevel + } + if u.PayVidDiscount > 0 && u.PayVidDiscount < p.PayVidDiscount { + payVidDiscount = u.PayVidDiscount + } + } else { + expire = time.Now().AddDate(0, 0, p.Duration) + } + log.Info(fmt.Sprintf("到期时间:%v", expire)) + sel.VipExpireDate = &expire + sel.VipLevel = &vipLevel + sel.PayVidDiscount = &payVidDiscount + vipExpireTime = expire + } + if p.Duration > 0 && p.Name != "" { + sel.VipName = &p.Name + } + // 增加金币视频免费天数 + if p.GoldVideoFreeDay > 0 { + var expire time.Time + if u.GoldVideoFreeExpire.IsZero() || u.GoldVideoFreeExpire.Before(time.Now()) { + expire = time.Now().AddDate(0, 0, p.GoldVideoFreeDay) + } else { + expire = u.GoldVideoFreeExpire.AddDate(0, 0, p.GoldVideoFreeDay) + } + sel.GoldVideoFreeExpire = &expire + // 金币免费限额(只有当前卡片有金币免费免费时间,才会生效) + if p.GoldVideoFreeLimit > u.GoldVideoFreeLimit { + sel.GoldVideoFreeLimit = &p.GoldVideoFreeLimit + } + } + if p.BroadcastDays > 0 { + expire := time.Time{} + if u.BroadcastExpire.IsZero() || u.BroadcastExpire.Before(time.Now()) { + expire = time.Now().AddDate(0, 0, p.BroadcastDays) + } else { + expire = u.BroadcastExpire.AddDate(0, 0, p.BroadcastDays) + } + sel.BroadcastExpire = &expire + } + if p.DramaDays > 0 { + expire := usermod.RenewDramaExpire(u.DramaExpire, time.Now(), p.DramaDays) + sel.DramaExpire = &expire + } + // 赠送金币 + if p.GiveCoin > 0 { + creditPlan.Amount = &p.GiveCoin + } + if p.DownloadCount > 0 { + creditPlan.DownloadCount = &p.DownloadCount + } + if p.LuckyDrawCount > 0 { + creditPlan.LotteryTimes = &p.LuckyDrawCount + } + // 赠送AI免费脱衣次数 + if p.AiUndressCount > 0 { + aiUndressFreeTimes := int64(p.AiUndressCount) + creditPlan.AiUndressFreeTimes = &aiUndressFreeTimes + } + var co int64 = 1 + creditPlan.Consumption = &co + //if p.ChatPrice > 0 { + // sel.ChatPrice = &p.ChatPrice + //} + // 改成是 -1免费 0读配置 大于0则为会员私信价格 + sel.ChatPrice = &p.ChatPrice + return func(t *db.MongoTool) error { + // 钱包变更 + if p.GiveCoin > 0 || p.GiveFruitCoin > 0 || p.DownloadCount > 0 || p.AiUndressCount > 0 || p.LuckyDrawCount > 0 { + w, err := walletmod.Credit(t, creditPlan, order.UID) + if err != nil { + return err + } + if p.LuckyDrawCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + LotteryTimes: p.LuckyDrawCount, + TranType: txnmod.GiveLotteryTimesCount.Key(), + TranTypeInt: int64(txnmod.GiveLotteryTimesCount), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-赠送[%d]次抽奖", p.LuckyDrawCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + }) + } + if p.GiveFruitCoin > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + FruitCoin: p.GiveFruitCoin, + TranType: txnmod.CurrencyGive.Key(), + TranTypeInt: int64(txnmod.CurrencyGive), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-赠送[%d]果币", p.GiveFruitCoin), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + FruitCoinBalance: w.FruitCoin, + }) + } + if p.DownloadCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + DownloadCount: p.DownloadCount, + TranType: txnmod.GiveDownload.Key(), + TranTypeInt: int64(txnmod.GiveDownload), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-赠送[%d]次数", p.DownloadCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + }) + } + + //插入购买会员卡赠送金币流水 + if p.GiveCoin > 0 { + giveLog := txnmod.TransactionLog{ + UID: order.UID, + Amount: p.GiveCoin, + ActualAmount: float64(p.GiveCoin), + TranType: txnmod.VipCardGive.Key(), + TranTypeInt: int64(txnmod.VipCardGive), + TransNo: history.ID, + Desc: fmt.Sprintf("购买%s-赠送金币[%v个]", p.Name, p.GiveCoin), + SysType: u.SysType, + RealAmount: w.RealAmount(), + } + txnLogs = append(txnLogs, giveLog) + } + // 赠送免费次数 + if p.AiUndressCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + Amount: int64(p.AiUndressCount), + ActualAmount: float64(p.AiUndressCount), + TranType: txnmod.VipCardGiveAiUndressFreeCount.Key(), + TranTypeInt: int64(txnmod.VipCardGiveAiUndressFreeCount), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-赠送AI脱衣免费次数[%d次]", p.AiUndressCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: w.RealAmount(), + }) + } + } + // 用户信息变更 + if p.Duration > 0 || p.GoldVideoFreeDay > 0 || p.ChatPrice > 0 || p.BroadcastDays > 0 || p.DramaDays > 0 { + if _, err = usermod.UpdateTrans(t, u.UID, sel); err != nil { + return err + } + } + // 保存商品购买记录 + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + // 保存资金流水记录 + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + return err + } + // 处理购买VIP观影券 + if coupons := HandleGoldVideoCoupon(p, order.UID, videocoupon.GoldVideoCouponSourceVIP); len(coupons) > 0 { + if err = videocoupon.InsertMany(coupons); err != nil { + return err + } + } + common.Go(func() { + _ = taskser.CompleteOnceTask(nil, order.UID, oncetaskmod.OnceTaskTypeUserBuyVip) + }) + return nil + }, vipExpireTime, nil +} + +func BuyAdvanceVIP(order rchgordmod.RechargeOrder, p *productmod.Product, desc string, w *walletmod.Wallet, payMoney int64) (func(*db.MongoTool) error, time.Time, error) { + var ( + //商品购买记录 + history = prdcthsomod.ProductHistory{ + ID: primitive.NewObjectID(), + UID: order.UID, + ProductID: p.ID, + Name: p.Name, + Amount: p.DiscountedPrice, + ProductType: prdcthsomod.AdvanceCard, + SysType: order.DevType, + ProductSnapShot: p, + IsUpgrade: p.IsUpgrade, + CurrentVipName: p.CurrentVipName, + CurrentVipPrice: p.CurrentVipPrice, + PurchasePrice: p.PurchasePrice, + DiscDoc: order.DiscDoc, + } + sel usermod.UserSelector + creditPlan = walletmod.CreditPlan{} + payVidDiscount int + changeStatus int + price = p.AdvanceAmount + vipExpireTime time.Time + ) + // 查询用户信息 + u, err := usermod.FindUserByUID(order.UID) + if err != nil { + log.Error(fmt.Sprintf("充值购买会员卡-订单号[%s] 查询用户信息异常 [%v]", order.ID, err)) + return nil, vipExpireTime, err + } + + data, err := advanceordermod.IsExist(bson.M{"uid": order.UID, "productID": order.ProductID}) + if err != nil { + log.Error(fmt.Sprintf("充值购买预售卡-订单号[%s] 查询预售订单信息异常 [%v]", order.ID, err)) + return nil, vipExpireTime, err + } + if data == nil || data.ID.IsZero() { + log.Error(fmt.Sprintf("充值购买预售卡-订单号[%s] 查询预售订单信息异常 [%v]", order.ID, err)) + return nil, vipExpireTime, errors.New("advance order is null") + } + // 回调的时候,刚好已经处理完成 + if data.Status == advanceordermod.BalanceSUCCESS { + // 处理失败,直接返回成功, + //common.Go(func() { + // OnCallbackFail(payMoney, order, p) + //}) + return func(tool *db.MongoTool) error { + return nil + }, vipExpireTime, nil + } + if data.Status != advanceordermod.AdvanceProcessing && data.Status != advanceordermod.BalanceProcessing { + log.Error(fmt.Sprintf("充值购买预售卡-订单号[%s] 查询预售订单信息状态异常 [%v]", order.ID, data.Status)) + return nil, vipExpireTime, errors.New("advance order status is err") + } + + var duration = p.Duration + var vipLevel = p.VipLevel + var fullPrivilege bool + var tranType = txnmod.BuyAdvanceVIP + if data.Status == advanceordermod.AdvanceProcessing { + duration = p.AdvanceDuration + // 会员持续时间不能超过预付过期时间 + if time.Now().AddDate(0, 0, duration).After(p.AdvanceExpires) { + td := p.AdvanceExpires.Sub(time.Now()) + duration = int(math.Round(td.Hours() / 24)) + if duration < 1 { + duration = 1 + } + } + + vipLevel = p.AdvanceVipLevel + changeStatus = advanceordermod.AdvanceSUCCESS + } + if data.Status == advanceordermod.BalanceProcessing { + changeStatus = advanceordermod.BalanceSUCCESS + price = p.BalanceAmount + fullPrivilege = true + tranType = txnmod.BuyBalanceVIP + } + + // 校验预售金额 + status, _, diff := CheckAdvanceMoney(payMoney, price, order, p) + if !status { + return func(tool *db.MongoTool) error { + return nil + }, vipExpireTime, nil + } + + // 资金流水记录 + txnLogs := []txnmod.TransactionLog{ + { + UID: order.UID, + TranType: tranType.Key(), + TranTypeInt: int64(tranType), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + CurrencyType: txnmod.CurrencyTypeCash, + Amount: -price, + ActualAmount: float64(-price), + RealAmount: w.RealAmount(), + }, + } + + // VIP变更 + if duration > 0 { + var expire time.Time + payVidDiscount = p.PayVidDiscount + // VIP未过期 + if u.VipExpireDate.After(time.Now()) { + if changeStatus == advanceordermod.BalanceSUCCESS { + expire = u.VipExpireDate.AddDate(0, 0, duration) + } + if changeStatus == advanceordermod.AdvanceSUCCESS { + expire = time.Now().AddDate(0, 0, duration) + } + + if u.VipLevel > p.VipLevel { + vipLevel = u.VipLevel + } + if u.PayVidDiscount > 0 && u.PayVidDiscount < p.PayVidDiscount { + payVidDiscount = u.PayVidDiscount + } + } else { + expire = time.Now().AddDate(0, 0, duration) + } + log.Info(fmt.Sprintf("到期时间:%v", expire)) + sel.VipExpireDate = &expire + sel.VipLevel = &vipLevel + sel.PayVidDiscount = &payVidDiscount + vipExpireTime = expire + } + + if p.AllGoldVideoFree && fullPrivilege { + free := true + sel.AllGoldVideoFree = &free + } + if p.Name != "" { + sel.VipName = &p.Name + } + // 增加金币视频免费天数 + if p.GoldVideoFreeDay > 0 && fullPrivilege { + var expire time.Time + if u.GoldVideoFreeExpire.IsZero() || u.GoldVideoFreeExpire.Before(time.Now()) { + if changeStatus == advanceordermod.BalanceSUCCESS { + expire = time.Now().AddDate(0, 0, p.GoldVideoFreeDay) + } + if changeStatus == advanceordermod.AdvanceSUCCESS { + expire = p.EndTime + } + } else { + if changeStatus == advanceordermod.BalanceSUCCESS { + expire = u.GoldVideoFreeExpire.AddDate(0, 0, p.GoldVideoFreeDay) + } + if changeStatus == advanceordermod.AdvanceSUCCESS { + expire = p.EndTime + } + } + sel.GoldVideoFreeExpire = &expire + // 金币免费限额(只有当前卡片有金币免费免费时间,才会生效) + if p.GoldVideoFreeLimit > u.GoldVideoFreeLimit { + sel.GoldVideoFreeLimit = &p.GoldVideoFreeLimit + } + } + if p.BroadcastDays > 0 { + expire := time.Time{} + if u.BroadcastExpire.IsZero() || u.BroadcastExpire.Before(time.Now()) { + expire = time.Now().AddDate(0, 0, p.BroadcastDays) + } else { + expire = u.BroadcastExpire.AddDate(0, 0, p.BroadcastDays) + } + sel.BroadcastExpire = &expire + } + if p.DramaDays > 0 && fullPrivilege { + expire := usermod.RenewDramaExpire(u.DramaExpire, time.Now(), p.DramaDays) + sel.DramaExpire = &expire + } + // 赠送金币 + if p.GiveCoin > 0 { + creditPlan.Amount = &p.GiveCoin + } + if p.DownloadCount > 0 && fullPrivilege { + creditPlan.DownloadCount = &p.DownloadCount + } + // 赠送AI免费脱衣次数 + if p.AiUndressCount > 0 && fullPrivilege { + aiUndressFreeTimes := int64(p.AiUndressCount) + creditPlan.AiUndressFreeTimes = &aiUndressFreeTimes + } + if p.LuckyDrawCount > 0 && fullPrivilege { + creditPlan.LotteryTimes = &p.LuckyDrawCount + } + var co int64 = 1 + creditPlan.Consumption = &co + //if p.ChatPrice > 0 { + // sel.ChatPrice = &p.ChatPrice + //} + sel.ChatPrice = &p.ChatPrice + + return func(t *db.MongoTool) error { + if diff > 0 { + gold := diff / 10 + crPlan := walletmod.CreditPlan{} + crPlan.Amount = &gold + w, err := walletmod.Credit(t, crPlan, order.UID) + if err != nil { + return err + } + txnLogs = append(txnLogs, txnmod.TransactionLog{ + TransNo: order.ID, + UID: order.UID, + Amount: diff, + ActualAmount: float64(diff), + TranType: txnmod.OfficialRech.Key(), + TranTypeInt: int64(txnmod.OfficialRech), + ChannelType: order.RechargeType, + Desc: fmt.Sprintf("官方充值购买[%s],超额转化为[%d]金币", p.Name, diff/10), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + RealAmount: w.RealAmount(), + }) + } + + if changeStatus == advanceordermod.BalanceSUCCESS { + // 钱包变更 + if p.GiveCoin > 0 || p.GiveFruitCoin > 0 || p.DownloadCount > 0 || p.AiUndressCount > 0 || p.LuckyDrawCount > 0 { + w, err := walletmod.Credit(t, creditPlan, order.UID) + if err != nil { + return err + } + if p.GiveFruitCoin > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + FruitCoin: p.GiveFruitCoin, + TranType: txnmod.CurrencyGive.Key(), + TranTypeInt: int64(txnmod.CurrencyGive), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-赠送[%d]果币", p.GiveFruitCoin), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + FruitCoinBalance: w.FruitCoin, + }) + } + if p.DownloadCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + DownloadCount: p.DownloadCount, + TranType: txnmod.GiveDownload.Key(), + TranTypeInt: int64(txnmod.GiveDownload), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-赠送[%d]次数", p.DownloadCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + }) + } + + //插入购买会员卡赠送金币流水 + if p.GiveCoin > 0 { + giveLog := txnmod.TransactionLog{ + UID: order.UID, + Amount: p.GiveCoin, + ActualAmount: float64(p.GiveCoin), + TranType: txnmod.VipCardGive.Key(), + TranTypeInt: int64(txnmod.VipCardGive), + TransNo: history.ID, + Desc: fmt.Sprintf("购买%s-赠送金币[%v个]", p.Name, p.GiveCoin), + SysType: u.SysType, + RealAmount: w.RealAmount(), + } + txnLogs = append(txnLogs, giveLog) + } + // 赠送免费次数 + if p.AiUndressCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + Amount: int64(p.AiUndressCount), + ActualAmount: float64(p.AiUndressCount), + TranType: txnmod.VipCardGiveAiUndressFreeCount.Key(), + TranTypeInt: int64(txnmod.VipCardGiveAiUndressFreeCount), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-赠送AI脱衣免费次数[%d次]", p.AiUndressCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: w.RealAmount(), + }) + } + if p.LuckyDrawCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + LotteryTimes: p.LuckyDrawCount, + TranType: txnmod.GiveLotteryTimesCount.Key(), + TranTypeInt: int64(txnmod.GiveLotteryTimesCount), + TransNo: history.ID, + Desc: fmt.Sprintf("预售卡购买-%v", p.Name) + fmt.Sprintf("-赠送[%d]次数", p.LuckyDrawCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + }) + } + } + } + + // 修改预付订单状态 + err := advanceordermod.Update(t, data.ID, bson.M{"status": changeStatus}) + if err != nil { + return err + } + // 用户信息变更 + if p.Duration > 0 || p.GoldVideoFreeDay > 0 || p.ChatPrice > 0 || p.BroadcastDays > 0 || (p.DramaDays > 0 && fullPrivilege) { + if _, err = usermod.UpdateTrans(t, u.UID, sel); err != nil { + return err + } + } + history.AdvanceOrderStatus = changeStatus + // 保存商品购买记录 + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + // 保存资金流水记录 + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + return err + } + // 处理购买VIP观影券 + if coupons := HandleGoldVideoCoupon(p, order.UID, videocoupon.GoldVideoCouponSourceVIP); len(coupons) > 0 { + if err = videocoupon.InsertMany(coupons); err != nil { + return err + } + } + common.Go(func() { + _ = taskser.CompleteOnceTask(nil, order.UID, oncetaskmod.OnceTaskTypeUserBuyVip) + }) + return nil + }, vipExpireTime, nil +} + +func OnCallbackFail(payMoney int64, order rchgordmod.RechargeOrder, p *productmod.Product) (err error) { + creditPlan := walletmod.CreditPlan{Consumption: &payMoney} + gold := payMoney / 10 + creditPlan.Amount = &gold + + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + w, err := walletmod.Credit(t, creditPlan, order.UID) + if err != nil { + return err + } + //插入一条支出流水 + if err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + TransNo: order.ID, + UID: order.UID, + Amount: gold, + ActualAmount: float64(gold), + TranType: txnmod.OfficialRech.Key(), + TranTypeInt: int64(txnmod.OfficialRech), + ChannelType: order.RechargeType, + Desc: fmt.Sprintf("官方充值购买[%s]实际支付[%d]元;全额转化为[%d]金币", p.Name, payMoney/100, payMoney/10), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + RealAmount: w.RealAmount(), + }); err != nil { + log.Info(fmt.Sprintf("rechargeSer checkAdvanceMoney CallbackAddCoins Trans fail error:%v, order:%+v", err, order)) + return err + } + return nil + }); err != nil { + log.Warn(fmt.Sprintf("productser buyVIP checkAdvanceMoney Transaction err %s", err.Error())) + return err + } + return nil +} + +func CheckAdvanceMoney(payMoney int64, price int64, order rchgordmod.RechargeOrder, p *productmod.Product) (status bool, cs int, diff int64) { + var ( + checkStatus bool + diffAmt = payMoney - price*10 + creditPlan = walletmod.CreditPlan{Consumption: &payMoney} + gold = payMoney / 10 + csm int + ) + if diffAmt >= -10 && diffAmt < 0 { + checkStatus = true + return checkStatus, csm, 0 + } + if diffAmt == 0 { + checkStatus = true + return checkStatus, csm, diffAmt + } + // 超额支付转化金币 + if diffAmt > 0 { + checkStatus = true + return checkStatus, csm, diffAmt + } + + // 小于全额转化金币 + if diffAmt < -10 { + creditPlan.Amount = &gold + csm = rchgordmod.ChanShareCoin + + if err := appg.VideoDB.Trans(func(t *db.MongoTool) error { + w, err := walletmod.Credit(t, creditPlan, order.UID) + if err != nil { + return err + } + //插入一条支出流水 + if err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + TransNo: order.ID, + UID: order.UID, + Amount: gold, + ActualAmount: float64(gold), + TranType: txnmod.OfficialRech.Key(), + TranTypeInt: int64(txnmod.OfficialRech), + ChannelType: order.RechargeType, + Desc: fmt.Sprintf("官方充值购买[%s]实际支付[%d]元;全额转化为[%d]金币", p.Name, payMoney/100, payMoney/10), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + RealAmount: w.RealAmount(), + }); err != nil { + log.Info(fmt.Sprintf("rechargeSer checkAdvanceMoney CallbackAddCoins Trans fail error:%v, order:%+v", err, order)) + return err + } + return nil + }); err != nil { + log.Warn(fmt.Sprintf("productser buyVIP checkAdvanceMoney Transaction err %s", err.Error())) + return checkStatus, csm, diffAmt + } + } + return checkStatus, csm, diffAmt +} + +func BuyGameAdvanceVIP(order rchgordmod.RechargeOrder, p *productmod.Product, desc string, w *walletmod.Wallet) (func(*db.MongoTool) error, time.Time, error) { + var ( + // 商品购买记录 + history = prdcthsomod.ProductHistory{ + ID: primitive.NewObjectID(), + UID: order.UID, + ProductID: p.ID, + Name: p.Name, + Amount: p.DiscountedPrice, + ProductType: prdcthsomod.GameAdvanceCard, + DiscDoc: order.DiscDoc, + SysType: order.DevType, + ProductSnapShot: p, + } + sel usermod.UserSelector + creditPlan = walletmod.CreditPlan{} + vipLevel int + payVidDiscount int + vipExpireTime time.Time + ) + // 获取三方游戏码 + gameCode, err := game.GainTripartiteGameCode(order.UID) + if err != nil { + log.Error(fmt.Sprintf("充值购买游戏预售-订单号[%s] 获取游戏码信息异常 [%v]", order.ID, err)) + return nil, vipExpireTime, err + } + + history.GameCode = gameCode + // 查询用户信息 + u, err := usermod.FindUserByUID(order.UID) + if err != nil { + log.Error(fmt.Sprintf("充值购买会员卡-订单号[%s] 查询用户信息异常 [%v]", order.ID, err)) + return nil, vipExpireTime, err + } + + // 资金流水记录 + txnLogs := []txnmod.TransactionLog{ + { + UID: order.UID, + TranType: txnmod.BuyGameAdvanceVIP.Key(), + TranTypeInt: int64(txnmod.BuyGameAdvanceVIP), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + CurrencyType: txnmod.CurrencyTypeCash, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + RealAmount: w.RealAmount(), + }, + } + goodsList := make([]backpackmod.Backpack, 1) + now := time.Now() + goodsList[0] = backpackmod.Backpack{ + UID: order.UID, + GoodsName: p.Name, + GoodsType: backpackmod.GameCode, + GoodsValue: p.DiscountedPrice, + GoodsOrigin: fmt.Sprintf("充值购买"), + GoodsDesc: gameCode, + Status: backpackmod.Unused, + ExpiredTime: now.AddDate(0, 0, int(p.Duration)), + CreateTime: now, + } + + // 购买游戏是否赠送VIP && VIP变更 + if p.SendGame && p.Duration > 0 { + var expire time.Time + vipLevel = p.VipLevel + payVidDiscount = p.PayVidDiscount + // VIP未过期 + if u.VipExpireDate.After(time.Now()) { + expire = u.VipExpireDate.AddDate(0, 0, p.Duration) + + if u.VipLevel > p.VipLevel { + vipLevel = u.VipLevel + } + if u.PayVidDiscount > 0 && u.PayVidDiscount < p.PayVidDiscount { + payVidDiscount = u.PayVidDiscount + } + } else { + expire = time.Now().AddDate(0, 0, p.Duration) + } + log.Info(fmt.Sprintf("到期时间:%v", expire)) + sel.VipExpireDate = &expire + sel.VipLevel = &vipLevel + sel.PayVidDiscount = &payVidDiscount + vipExpireTime = expire + } + + if p.AllGoldVideoFree { + free := true + sel.AllGoldVideoFree = &free + } + if p.SendGame && p.Name != "" { + sel.VipName = &p.Name + } + + // 增加金币视频免费天数 + if p.GoldVideoFreeDay > 0 { + var expire time.Time + if u.GoldVideoFreeExpire.IsZero() || u.GoldVideoFreeExpire.Before(time.Now()) { + expire = time.Now().AddDate(0, 0, p.GoldVideoFreeDay) + } else { + expire = u.GoldVideoFreeExpire.AddDate(0, 0, p.GoldVideoFreeDay) + } + sel.GoldVideoFreeExpire = &expire + } + + // 赠送金币 + if p.GiveCoin > 0 { + creditPlan.Amount = &p.GiveCoin + } + + if p.DownloadCount > 0 { + creditPlan.DownloadCount = &p.DownloadCount + } + + // 赠送AI免费脱衣次数 + if p.AiUndressCount > 0 { + aiUndressFreeTimes := int64(p.AiUndressCount) + creditPlan.AiUndressFreeTimes = &aiUndressFreeTimes + } + var co int64 = 1 + creditPlan.Consumption = &co + if p.ChatPrice > 0 { + sel.ChatPrice = &p.ChatPrice + } + return func(t *db.MongoTool) error { + // 钱包变更 + if p.GiveCoin > 0 || p.GiveFruitCoin > 0 || p.DownloadCount > 0 || p.AiUndressCount > 0 { + w, err := walletmod.Credit(t, creditPlan, order.UID) + if err != nil { + return err + } + if p.GiveFruitCoin > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + FruitCoin: p.GiveFruitCoin, + TranType: txnmod.CurrencyGive.Key(), + TranTypeInt: int64(txnmod.CurrencyGive), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-赠送[%d]果币", p.GiveFruitCoin), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + FruitCoinBalance: w.FruitCoin, + }) + } + if p.DownloadCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + DownloadCount: p.DownloadCount, + TranType: txnmod.GiveDownload.Key(), + TranTypeInt: int64(txnmod.GiveDownload), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-赠送[%d]次数", p.DownloadCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + }) + } + + //插入购买会员卡赠送金币流水 + if p.GiveCoin > 0 { + giveLog := txnmod.TransactionLog{ + UID: order.UID, + Amount: p.GiveCoin, + ActualAmount: float64(p.GiveCoin), + TranType: txnmod.VipCardGive.Key(), + TranTypeInt: int64(txnmod.VipCardGive), + TransNo: history.ID, + Desc: fmt.Sprintf("购买%s-赠送金币[%v个]", p.Name, p.GiveCoin), + SysType: u.SysType, + RealAmount: w.RealAmount(), + } + txnLogs = append(txnLogs, giveLog) + } + + // 赠送免费次数 + if p.AiUndressCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + Amount: int64(p.AiUndressCount), + ActualAmount: float64(p.AiUndressCount), + TranType: txnmod.VipCardGiveAiUndressFreeCount.Key(), + TranTypeInt: int64(txnmod.VipCardGiveAiUndressFreeCount), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-赠送AI脱衣免费次数[%d次]", p.AiUndressCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: w.RealAmount(), + }) + } + } + + // 用户信息变更 + if p.SendGame && (p.Duration > 0 || p.GoldVideoFreeDay > 0 || p.ChatPrice > 0) { + if _, err = usermod.UpdateTrans(t, u.UID, sel); err != nil { + return err + } + } + + // 保存用户游戏码 + err := backpackmod.AddGoodsMany(t, order.UID, goodsList) + if err != nil { + return err + } + + // 保存商品购买记录 + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + // 保存资金流水记录 + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + return err + } + return nil + }, vipExpireTime, nil +} + +func NewBuyVideoFreeCard(order rchgordmod.RechargeOrder, p *productmod.Product, desc string, w *walletmod.Wallet) (func(*db.MongoTool) error, error) { + var ( + expiration time.Time + now = time.Now() + history = prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: primitive.NewObjectID(), + UID: order.UID, + ProductID: p.ID, + Name: p.Name, + Amount: p.DiscountedPrice, + ProductType: prdcthsomod.VideoFreeCard, + DiscDoc: order.DiscDoc, + SysType: order.DevType, + ProductSnapShot: p, + } + transactionLog = &txnmod.TransactionLog{ + UID: order.UID, + TranType: txnmod.VideoFreeCard.Key(), + TranTypeInt: int64(txnmod.VideoFreeCard), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + CurrencyType: txnmod.CurrencyTypeCash, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + RealAmount: w.RealAmount(), + } + ) + u, err := usermod.FindUserByUID(order.UID) + if err != nil { + return nil, err + } + if u.VideoFreeExpiration != nil && u.VideoFreeExpiration.After(now) { + expiration = u.VideoFreeExpiration.AddDate(0, 0, p.Duration) + } else { + expiration = now.AddDate(0, 0, p.Duration) + } + return func(t *db.MongoTool) error { + // 保存资金流水记录 + if err = txnmod.InsertTransactionLog(t, transactionLog); err != nil { + return err + } + // 保存商品购买记录 + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + // 用户楼凤全免信息更新 + _, err = usermod.UpdateTrans(t, order.UID, usermod.UserSelector{ + VideoFreeExpiration: &expiration, + }) + return err + }, err +} + +func NewBuyVideoDiscountCard(order rchgordmod.RechargeOrder, p *productmod.Product, desc string, w *walletmod.Wallet) ( + func(*db.MongoTool) error, error) { + var ( + videoDiscount int + expiration time.Time + now = time.Now() + history = prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: primitive.NewObjectID(), + UID: order.UID, + ProductID: p.ID, + Name: p.Name, + Amount: p.DiscountedPrice, + ProductType: prdcthsomod.VideoDiscount, + DiscDoc: order.DiscDoc, + SysType: order.DevType, + ProductSnapShot: p, + } + transactionLog = &txnmod.TransactionLog{ + UID: order.UID, + TranType: txnmod.VideoDiscount.Key(), + TranTypeInt: int64(txnmod.VideoDiscount), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + CurrencyType: txnmod.CurrencyTypeCash, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + RealAmount: w.RealAmount(), + } + ) + videoDiscountLog, err := videodiscountmod.GetByUID(order.UID) + if err != nil { + return nil, err + } + if videoDiscountLog.Expiration.After(now) { + if p.VideoDiscount > videoDiscountLog.VideoDiscount { + videoDiscount = p.VideoDiscount + } else { + videoDiscount = videoDiscountLog.VideoDiscount + } + expiration = videoDiscountLog.Expiration.AddDate(0, 0, p.Duration) + } else { + videoDiscount = p.VideoDiscount + expiration = now.AddDate(0, 0, p.Duration) + } + setLd := videodiscountmod.EditSelector{ + UID: &order.UID, + Expiration: &expiration, + VideoDiscount: &videoDiscount, + } + return func(t *db.MongoTool) error { + // 保存资金流水记录 + if err = txnmod.InsertTransactionLog(t, transactionLog); err != nil { + return err + } + // 保存商品购买记录 + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + // 楼凤折扣变更 + return videodiscountmod.Upsert(t, &setLd) + }, nil +} + +func NewBuyCoinMonthCard(order rchgordmod.RechargeOrder, p *productmod.Product, desc string, w *walletmod.Wallet) (func(*db.MongoTool) error, error) { + var ( + creditPlan = walletmod.CreditPlan{} + sel usermod.UserSelector + vipLevel int + payVidDiscount int + history = prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: primitive.NewObjectID(), + UID: order.UID, + ProductID: p.ID, + Name: p.Name, + Amount: p.DiscountedPrice, + ProductType: prdcthsomod.CoinMonthCard, + DiscDoc: order.DiscDoc, + SysType: order.DevType, + ProductSnapShot: p, + } + transactionLog = &txnmod.TransactionLog{ + UID: order.UID, + TranType: txnmod.CoinMonthCard.Key(), + TranTypeInt: int64(txnmod.CoinMonthCard), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + CurrencyType: txnmod.CurrencyTypeCash, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + RealAmount: w.RealAmount(), + } + ) + // 查询用户信息 + u, err := usermod.FindUserByUID(order.UID) + if err != nil { + log.Error(fmt.Sprintf("充值购买会员卡-订单号[%s] 查询用户信息异常 [%v]", order.ID, err)) + return nil, err + } + //vip变更 + if p.Duration > 0 { + var expire time.Time + vipLevel = p.VipLevel + payVidDiscount = p.PayVidDiscount + // VIP未过期 + if u.CoinMouthExpireDate.After(time.Now()) { + expire = u.CoinMouthExpireDate.AddDate(0, 0, p.Duration) + if u.VipLevel > p.VipLevel { + vipLevel = u.VipLevel + } + if u.PayVidDiscount > 0 && u.PayVidDiscount < p.PayVidDiscount { + payVidDiscount = u.PayVidDiscount + } + } else { + expire = time.Now().AddDate(0, 0, p.Duration) + } + log.Info(fmt.Sprintf("到期时间:%v", expire)) + sel.CoinMouthExpireDate = &expire + sel.VipLevel = &vipLevel + sel.PayVidDiscount = &payVidDiscount + } + //赠送金币----首次购买赠送金币 + if p.GiveCoin > 0 { + creditPlan.Amount = &p.GiveCoin + } + + return func(t *db.MongoTool) error { + //钱包变更 + if p.GiveCoin > 0 || p.GiveFruitCoin > 0 { + _, err = walletmod.Credit(t, creditPlan, order.UID) + if err != nil { + return err + } + } + fmt.Println("----------------p", p, "user", sel) + // 用户信息变更 + if p.Duration > 0 || p.GoldVideoFreeDay > 0 { + if _, err = usermod.UpdateTrans(t, u.UID, sel); err != nil { + return err + } + } + // 保存商品购买记录 + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + // 保存资金流水记录 + return txnmod.InsertTransactionLog(t, transactionLog) + }, nil +} + +func NewBuyWhoringCard(order rchgordmod.RechargeOrder, p *productmod.Product, desc string, w *walletmod.Wallet) (func(*db.MongoTool) error, error) { + var ( + // 商品购买记录 + history = prdcthsomod.ProductHistory{ + ID: primitive.NewObjectID(), + UID: order.UID, + ProductID: p.ID, + Name: p.Name, + Amount: p.DiscountedPrice, + ProductType: prdcthsomod.WhoringCard, + DiscDoc: order.DiscDoc, + SysType: order.DevType, + ProductSnapShot: p, + } + sel usermod.UserSelector + creditPlan = walletmod.CreditPlan{} + vipLevel int + payVidDiscount int + record signrecordmod.SignRecord + ) + creditPlan.Consumption = &order.PayMoney + // 资金流水记录 + txnLogs := []txnmod.TransactionLog{ + { + UID: order.UID, + TranType: txnmod.BuyWhoringCard.Key(), + TranTypeInt: int64(txnmod.BuyWhoringCard), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + CurrencyType: txnmod.CurrencyTypeCash, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + RealAmount: w.RealAmount(), + }, + } + // 查询用户信息 + u, err := usermod.FindUserByUID(order.UID) + if err != nil { + log.Error(fmt.Sprintf("充值购买比嫖卡-订单号[%s] 查询用户信息异常 [%v]", order.ID, err)) + return nil, err + } + + // VIP变更 + if p.Duration > 0 { + var expire time.Time + vipLevel = p.VipLevel + payVidDiscount = p.PayVidDiscount + // VIP未过期 + if u.VipExpireDate.After(time.Now()) { + expire = u.VipExpireDate.AddDate(0, 0, p.Duration) + if u.VipLevel > p.VipLevel { + vipLevel = u.VipLevel + } + if u.PayVidDiscount > 0 && u.PayVidDiscount < p.PayVidDiscount { + payVidDiscount = u.PayVidDiscount + } + } else { + expire = time.Now().AddDate(0, 0, p.Duration) + } + log.Info(fmt.Sprintf("到期时间:%v", expire)) + sel.VipExpireDate = &expire + sel.VipLevel = &vipLevel + sel.PayVidDiscount = &payVidDiscount + } + if p.Duration > 0 && p.Name != "" { + sel.VipName = &p.Name + } + if p.SignDays > 0 { + var buyStatus = true + sel.HasWhoringCard = &buyStatus + recTime := []signrecordmod.Record{} + // 获取打卡记录 + nowTime := time.Now() + record.UID = order.UID + record.PID = p.ID + record.UpdateTime = nowTime + record.TotalDays = int64(p.SignDays) + record.CurrentSignDays = 1 + record.ForgetSignDays = 0 + record.RenewalSignDays = 0 + record.SignTime = nowTime + record.EndTime = nowTime.AddDate(0, 0, int(p.SignDays)) + record.CreatedAt = nowTime + record.RecordTime = recTime + } + // 增加金币视频免费天数 + if p.GoldVideoFreeDay > 0 { + var expire time.Time + if u.GoldVideoFreeExpire.IsZero() || u.GoldVideoFreeExpire.Before(time.Now()) { + expire = time.Now().AddDate(0, 0, p.GoldVideoFreeDay) + } else { + expire = u.GoldVideoFreeExpire.AddDate(0, 0, p.GoldVideoFreeDay) + } + sel.GoldVideoFreeExpire = &expire + } + // 赠送金币 + if p.GiveCoin > 0 { + creditPlan.Amount = &p.GiveCoin + } + if p.DownloadCount > 0 { + creditPlan.DownloadCount = &p.DownloadCount + } + + // 赠送AI免费脱衣次数 + if p.AiUndressCount > 0 { + aiUndressFreeTimes := int64(p.AiUndressCount) + creditPlan.AiUndressFreeTimes = &aiUndressFreeTimes + } + + if p.ChatPrice != 0 { + sel.ChatPrice = &p.ChatPrice + } + // 处理购买VIP观影券 + coupons := HandleGoldVideoCoupon(p, order.UID, videocoupon.GoldVideoCouponSourceVIP) + return func(t *db.MongoTool) error { + // 钱包变更 + if p.GiveCoin > 0 || p.GiveFruitCoin > 0 || p.DownloadCount > 0 || p.AiUndressCount > 0 { + w, err := walletmod.Credit(t, creditPlan, order.UID) + if err != nil { + return err + } + if p.GiveFruitCoin > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + FruitCoin: p.GiveFruitCoin, + TranType: txnmod.CurrencyGive.Key(), + TranTypeInt: int64(txnmod.CurrencyGive), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-赠送[%d]果币", p.GiveFruitCoin), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + FruitCoinBalance: w.FruitCoin, + }) + } + if p.DownloadCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + DownloadCount: p.DownloadCount, + TranType: txnmod.GiveDownload.Key(), + TranTypeInt: int64(txnmod.GiveDownload), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-赠送[%d]次数", p.DownloadCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + }) + } + // 赠送免费次数 + if p.AiUndressCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + Amount: int64(p.AiUndressCount), + ActualAmount: float64(p.AiUndressCount), + TranType: txnmod.VipCardGiveAiUndressFreeCount.Key(), + TranTypeInt: int64(txnmod.VipCardGiveAiUndressFreeCount), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-赠送AI脱衣免费次数[%d次]", p.AiUndressCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: w.RealAmount(), + }) + } + // 赠送金币 + if p.GiveCoin > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + Amount: int64(p.GiveCoin), + ActualAmount: float64(p.AiUndressCount), + TranType: txnmod.VipCardGive.Key(), + TranTypeInt: int64(txnmod.VipCardGive), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-赠送金币[%d个]", p.GiveCoin), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: w.RealAmount(), + }) + } + } + // 用户信息变更 + if p.Duration > 0 || p.GoldVideoFreeDay > 0 || p.ChatPrice != 0 || p.SignDays > 0 { + if _, err = usermod.UpdateTrans(t, u.UID, sel); err != nil { + return err + } + } + // 保存商品购买记录 + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + // 保存资金流水记录 + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + return err + } + + // 赠送观影券 + if len(coupons) > 0 { + if err := videocoupon.InsertMany(coupons); err != nil { + return err + } + } + + // 打卡记录 + _, err := signrecordmod.InsertOne(t, record) + if err != nil { + return err + } + return nil + }, nil +} diff --git a/app/service/productser/productser.go b/app/service/productser/productser.go new file mode 100644 index 0000000..3654f64 --- /dev/null +++ b/app/service/productser/productser.go @@ -0,0 +1,3003 @@ +package productser + +import ( + "91porn-server/common/constant/redisconst" + "91porn-server/common/game" + "91porn-server/middleware/ua" + "91porn-server/models/v/advanceordermod" + "91porn-server/models/v/dailytaskmod" + "91porn-server/models/v/discount_area_mod" + "91porn-server/models/v/imgroupmembermod" + "91porn-server/models/v/imgroupmod" + "91porn-server/models/v/media_buy_record_mod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/nakedchatmod" + "91porn-server/models/v/nakedchatordermod" + "encoding/json" + "errors" + "fmt" + "math" + "strconv" + "time" + + "go.mongodb.org/mongo-driver/bson" + + "91porn-server/app/appg" + "91porn-server/app/service/taskser" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/db" + "91porn-server/common/log" + sli "91porn-server/common/slice" + "91porn-server/common/stderr" + "91porn-server/common/timeutil" + "91porn-server/models/commod" + "91porn-server/models/l/payvidlgmod" + "91porn-server/models/v/audiobookmod" + "91porn-server/models/v/backpackmod" + "91porn-server/models/v/newactivity" + "91porn-server/models/v/oncetaskmod" + "91porn-server/models/v/payaudiobookmod" + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/prizemod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/videocoupon" + "91porn-server/models/v/videodiscountmod" + "91porn-server/models/v/vidmod" + "91porn-server/models/v/vipconfigmod" + "91porn-server/models/v/walletmod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Buy 购买商品 +func Buy(uid uint64, productType commod.ProductType, productID, couponID, serviceID primitive.ObjectID, num uint64, userContact, sys string, + ChapterID string, goldVideoCouponNum int, isH5 bool, ua ua.UA, ip string, experimentAttribution VIPExperimentAttribution) stderr.Code { + normalizedAttribution, err := validateVIPExperimentAttribution(uid, productID, experimentAttribution) + if err != nil { + log.Warn("invalid VIP experiment coin purchase attribution", log.Any("uid", uid), log.Any("productID", productID), log.E(err)) + return stderr.ErrParamError + } + switch productType { + case prdcthsomod.VIP, commod.NEWUSERCard: + return BuyVIP(uid, productID, couponID, sys, false, ua, ip, normalizedAttribution) + case prdcthsomod.AdvanceCard: + return BuyAdvanceCard(uid, productID, couponID, sys, false, ua, ip, normalizedAttribution) + case prdcthsomod.GameAdvanceCard: + return BuyGameAdvanceCard(uid, productID, couponID, sys, false, ua, ip, normalizedAttribution) + case prdcthsomod.VIDEO: + return BuyVid(uid, productID, goldVideoCouponNum, ua, ip) + case prdcthsomod.Media: + return BuyMedia(uid, productID, ua, ip) + case prdcthsomod.MeetingCard: + return BuyMeetingCard(uid, productID, sys, ua, ip) + case prdcthsomod.OTHER, commod.PhysicalGoods: + return BuyOtherCard(uid, productID, sys, ua, ip) + case commod.AudioBook: + return BuyAudioBook(uid, productID, ChapterID, ua, ip) + case commod.VideoDiscount: + return BuyVideoDiscountCard(uid, productID, sys, ua, ip) + case commod.VideoFreeCard: + return BuyVideoFreeCard(uid, productID, sys, ua, ip) + case commod.ImGroup: + return BuyImGroup(uid, productID, sys) + case commod.NakedChat: + return BuyNakedChat(uid, productID, num, userContact, sys) + default: + return stderr.ErrParamError + } +} + +func BuyNakedChat(uid uint64, pid primitive.ObjectID, num uint64, userContact, sys string) stderr.Code { + // 获取该裸聊 + info, err := nakedchatmod.GetInfo(pid) + if err != nil { + return stderr.BuyFailed + } + if info.Price <= 0 { + return stderr.BuyFailed + } + var buyNum uint64 + for _, v := range info.Options { + if num == v { + buyNum = v + break + } + } + // 并没有配置该项 + if buyNum == 0 { + return stderr.BuyFailed + } + // 判断钱包余额是否足够, 做出扣款计划 + wallet, err := walletmod.GetWallet(uid) + if err != nil { + return stderr.ErrNetWorkBusy + } + if wallet == nil { + return stderr.InsufficientBalance + } + amount := int64(info.Price * buyNum) + // 扣除金币,加入群聊 + plan := debitPlan(wallet, amount) + if plan == nil { // 余额不足无法做出扣款计划 + return stderr.InsufficientBalance + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + wallet, err = walletmod.Debit(t, plan, uid) + if err != nil && err.Error() != "wallet not found" { + return err + } + orderId, err := nakedchatordermod.Insert(t, nakedchatordermod.NakedChatOrder{ + Nid: info.ID, + Uid: uid, + UserContact: userContact, + Num: buyNum, + Price: info.Price, + Amount: amount, + Remark: fmt.Sprintf("购买裸聊%v分钟", buyNum), + Snapshot: info, + Status: 0, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + if err != nil { + return stderr.BuyFailed + } + var realAmount decimal.Decimal + if wallet != nil { + realAmount = walletmod.GetRealAmount(wallet) + } + tl := txnmod.TransactionLog{TransNo: orderId, + UID: uid, + Amount: -amount, + ActualAmount: float64(-amount), + TranType: txnmod.BuyNakedChat.Key(), + TranTypeInt: int64(txnmod.BuyNakedChat), + Desc: fmt.Sprintf("购买裸聊-%v-%v分钟", info.Title, buyNum), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: realAmount, + } + if err = txnmod.InsertTransactionLog(t, &tl); err != nil { + return err + } + if err = nakedchatmod.IncSaleNum(t, info.ID, 1); err != nil { + return err + } + return nil + }); err != nil { + log.Warn(fmt.Sprintf("productser BuyNakedChat Transaction err [%s]", err.Error())) + return stderr.BuyFailed + } + return stderr.Success +} + +func BuyImGroup(uid uint64, pid primitive.ObjectID, sys string) stderr.Code { + // 获取该群组 + group, err := imgroupmod.GetInfo(pid) + if err != nil { + return stderr.BuyFailed + } + // 判断是否已经加入 + res, err := imgroupmembermod.GetInfoByCond(bson.M{"uid": uid, "groupId": group.GroupId}) + if err != nil { + return stderr.BuyFailed + } + // 已经购买过了 + if !res.ID.IsZero() && res.Status != 2 { + return stderr.BuyFailed + } + if group.Price == 0 { + // 直接写入记录 + _, err = imgroupmembermod.Insert(nil, imgroupmembermod.ImGroupMember{ + GroupId: group.GroupId, + Uid: uid, + Price: group.Price, + Status: 1, + CreatedAt: time.Now(), + }) + if err != nil { + return stderr.BuyFailed + } + return stderr.Success + } + // 判断钱包余额是否足够, 做出扣款计划 + wallet, err := walletmod.GetWallet(uid) + if err != nil { + return stderr.ErrNetWorkBusy + } + if wallet == nil { + return stderr.InsufficientBalance + } + // 扣除金币,加入群聊 + plan := debitPlan(wallet, group.Price) + if plan == nil { // 余额不足无法做出扣款计划 + return stderr.InsufficientBalance + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + wallet, err = walletmod.Debit(t, plan, uid) + if err != nil && err.Error() != "wallet not found" { + return err + } + memberId, err := imgroupmembermod.Insert(t, imgroupmembermod.ImGroupMember{ + GroupId: group.GroupId, + Uid: uid, + Price: group.Price, + Status: 1, + CreatedAt: time.Now(), + }) + if err != nil { + return stderr.BuyFailed + } + var realAmount decimal.Decimal + if wallet != nil { + realAmount = walletmod.GetRealAmount(wallet) + } + tl := txnmod.TransactionLog{TransNo: memberId, + UID: uid, + Amount: -group.Price, + ActualAmount: float64(-group.Price), + TranType: txnmod.JoinGroup.Key(), + TranTypeInt: int64(txnmod.JoinGroup), + Desc: "加入群组付费-" + group.Name, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: realAmount, + } + if err = txnmod.InsertTransactionLog(t, &tl); err != nil { + return err + } + + return nil + }); err != nil { + log.Warn(fmt.Sprintf("productser BuyImGroup Transaction err [%s]", err.Error())) + return stderr.BuyFailed + } + common.Go(func() { + // 群组成员加1 + imgroupmod.IncMemberNum(nil, group.ID, 1) + }) + return stderr.Success +} + +func BuyMedia(uid uint64, mediaId primitive.ObjectID, ua ua.UA, ip string) stderr.Code { + hasBought, err := media_buy_record_mod.IsWholeBuy(uid, mediaId) + if err != nil { + return stderr.ErrNetWorkBusy + } + if hasBought { + return stderr.RepeatPurchase + } + + v, err := mediamod.GetInfo(mediaId) + if err != nil { + return stderr.ErrNetWorkBusy + } + + if v.ID.IsZero() { + return stderr.CodeEmptyData + } + if v.MediaType == mediamod.MediaTypeDrama { + // 短剧只能通过带 contentID 的单集购买入口解锁。 + return stderr.ErrParamError + } + if v.Permission == 0 { + return stderr.Failure + } + if (v.Permission == 1 && v.Price == 0) || v.Permission == 2 { + return stderr.Success + } + + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + + code := buyMedia(*u, v, ua, ip) // 购买动漫类 + if code == stderr.Success { + // 完成日常任务 + taskser.CompleteDailyTask(nil, uid, dailytaskmod.DailyUnlockByGold) + } + return code +} + +// buyMedia 整本购买acg +func buyMedia(u usermod.User, media mediamod.Media, ua ua.UA, ip string) stderr.Code { + /* + 购买ACG时: + - VIP折扣和视频折扣卡不可用 + - 视频抵用券不可用 + - 金币视频抵用券不可用 + */ + // 判断钱包余额是否足够, 做出扣款计划 + wallet, err := walletmod.GetWallet(u.UID) + if err != nil { + return stderr.ErrNetWorkBusy + } + if wallet == nil { + return stderr.InsufficientBalance + } + plan := debitPlan(wallet, media.Price) + if plan == nil { // 余额不足无法做出扣款计划 + return stderr.InsufficientBalance + } + + var orderId = primitive.NewObjectID() + var orderCreatedAt = time.Now() + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + wallet, err = walletmod.Debit(t, plan, u.UID) + if err != nil && err.Error() != "wallet not found" { + return err + } + + pv := &media_buy_record_mod.MediaBuyRecord{ + ID: orderId, + MediaId: media.ID, + MediaType: media.MediaType, + ContentId: primitive.NilObjectID, + Uid: u.UID, + Type: 1, + Coins: media.Price, + PayMoney: media.Price, + CreatedAt: orderCreatedAt, + UpdateTime: time.Now(), + } + + if err = media_buy_record_mod.Create(t, pv); err != nil { + return err + } + var realAmount decimal.Decimal + if wallet != nil { + realAmount = walletmod.GetRealAmount(wallet) + } + tl := txnmod.TransactionLog{TransNo: pv.ID, + UID: u.UID, + Amount: -media.Price, + ActualAmount: float64(-media.Price), + TranType: txnmod.BuyAcg.Key(), + TranTypeInt: int64(txnmod.BuyAcg), + Desc: "ACG-整本购买-" + media.Title, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: realAmount, + } + if err = txnmod.InsertTransactionLog(t, &tl); err != nil { + return err + } + + return nil + }); err != nil { + log.Warn(fmt.Sprintf("productser buyMedia Transaction err [%s]", err.Error())) + return stderr.BuyFailed + } + common.Go(func() { + if err = mediamod.IncPurchasesCount(media.ID, 1); err != nil { + log.Warn("media IncPurchaseCount err", log.Any("mediaId", media.ID), log.E(err)) + } + }) + + return stderr.Success +} + +func checkVipRenew(u *usermod.User, p *productmod.Product) (time.Time, int, int) { + now := time.Now() + var end time.Time + level := p.VipLevel + payVidDiscount := p.PayVidDiscount + d := time.Hour * 24 * time.Duration(p.Duration) + if u.VipExpireDate.After(now) { //renew + end = u.VipExpireDate.Add(d) + if u.VipLevel > p.VipLevel { //当前用户的vip等级比这次购买的大,使用用户的 + level = u.VipLevel + } + //当前用户的折扣比这次购买的大,使用用户的 + if u.PayVidDiscount < payVidDiscount && u.PayVidDiscount > 0 { + payVidDiscount = u.PayVidDiscount + } + } else { + end = now.Add(time.Duration(d)) + } + return end, level, payVidDiscount +} + +// 扣除计划, 返回如何扣除本次支出 +func debitPlan(w *walletmod.Wallet, amt int64) *walletmod.DebitPlan { + l1 := w.Amount - amt // l1: 计算amout扣除以后的剩余值(负数的话则表示不足的值) + if l1 >= 0 { // amount够了 只需要在amount内扣除即可 + return &walletmod.DebitPlan{ + Amount: amt, + } + } + // amount不足时, 先扣除amout, 不足部分在income内扣除 + if w.Income+l1 >= 0 { // income足够支付差额 + return &walletmod.DebitPlan{ + Amount: w.Amount, + Income: -l1, + } + } + return nil //余额不足 +} + +// BuyVIP 购买产品产生的行为 +func BuyVIP(uid uint64, productID, couponID primitive.ObjectID, sys string, isRecharge bool, ua ua.UA, ip string, experimentAttribution VIPExperimentAttribution) stderr.Code { + p, err := productmod.FindProduct(productID, sys) + if err != nil || p == nil { + return stderr.ErrParamError + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + // 检查用户升级 + CheckUserUpgrade(uid, p) + + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return stderr.ErrNetWorkBusy + } + + var price int64 = p.DiscountedPrice + // 使用优惠卷(仅限金币购买) + if !couponID.IsZero() && p.IsAmountPay { + goodsDetail, err := backpackmod.GetGoodsDetail(couponID) + if err != nil { + return stderr.ErrNetWorkBusy + } + if goodsDetail == nil { + return stderr.ErrNetWorkBusy + } + price = decimal.NewFromInt(price).Mul(decimal.NewFromInt(goodsDetail.GoodsValue)).Div(decimal.NewFromInt(10)).IntPart() + } + plan := debitPlan(w, price) + if plan == nil { + return stderr.InsufficientBalance + } + if isRecharge { + plan.Amount -= p.GiveCoin + } + if p.DownloadCount > 0 { + plan.DownloadCount = -p.DownloadCount + } + vipExpire, vipLevel, payVidDiscount := checkVipRenew(u, p) + sel := usermod.UserSelector{VipExpireDate: &vipExpire, VipLevel: &vipLevel, PayVidDiscount: &payVidDiscount, VipName: &p.Name} + if p.GoldVideoFreeDay > 0 { + expire := time.Time{} + if u.GoldVideoFreeExpire.IsZero() || u.GoldVideoFreeExpire.Before(time.Now()) { + expire = time.Now().AddDate(0, 0, p.GoldVideoFreeDay) + } else { + expire = u.GoldVideoFreeExpire.AddDate(0, 0, p.GoldVideoFreeDay) + } + sel.GoldVideoFreeExpire = &expire + if p.GoldVideoFreeLimit > u.GoldVideoFreeLimit { + sel.GoldVideoFreeLimit = &p.GoldVideoFreeLimit + } + } + if p.AiUndressCount > 0 { + plan.AiUndressFreeTimes = int64(-p.AiUndressCount) + } + if p.LuckyDrawCount > 0 { + plan.LotteryTimes = -p.LuckyDrawCount // 负数经 Debit 的 $inc 即为增加抽奖次数 + } + if p.BroadcastDays > 0 { + expire := time.Time{} + if u.BroadcastExpire.IsZero() || u.BroadcastExpire.Before(time.Now()) { + expire = time.Now().AddDate(0, 0, p.BroadcastDays) + } else { + expire = u.BroadcastExpire.AddDate(0, 0, p.BroadcastDays) + } + sel.BroadcastExpire = &expire + } + if p.DramaDays > 0 { + expire := usermod.RenewDramaExpire(u.DramaExpire, time.Now(), p.DramaDays) + sel.DramaExpire = &expire + } + plan.Consumption = 1 + sel.ChatPrice = &p.ChatPrice + var orderId = primitive.NewObjectID() + var orderCreatedAt = time.Now() + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + var err error + discDoc := u.DiscDoc + wallet, err := walletmod.Debit(t, plan, uid) + if err != nil { //扣钱 + return err + } + + history := prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: orderId, + UID: uid, + ProductID: productID, + Name: p.Name, + Amount: plan.Amount, + Income: plan.Income, + ProductType: prdcthsomod.VIP, + SysType: u.SysType, + ProductSnapShot: p, + IsUpgrade: p.IsUpgrade, + CurrentVipName: p.CurrentVipName, + CurrentVipPrice: p.CurrentVipPrice, + PurchasePrice: p.PurchasePrice, + DiscDoc: discDoc, + CreatedAt: orderCreatedAt, + ExperimentID: experimentAttribution.ExperimentID, + ExperimentVariant: experimentAttribution.ExperimentVariant, + SessionID: experimentAttribution.SessionID, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + if err = usermod.UpdateVIP(t, uid, u.VipExpireDate, sel); err != nil { + return err + } + txnLogs := []txnmod.TransactionLog{ + { + UID: uid, + Amount: -price, + ActualAmount: float64(-price), + TranType: txnmod.PayVIP.Key(), + TranTypeInt: int64(txnmod.PayVIP), + TransNo: history.ID, + Desc: "VIP购买-" + p.Name, + DiscDoc: discDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + }, + } + // 判断会员卡是否赠送AI脱衣免费次数 + if p.AiUndressCount > 0 { + aiLog := txnmod.TransactionLog{ + UID: uid, + Amount: int64(p.AiUndressCount), + ActualAmount: float64(p.AiUndressCount), + TranType: txnmod.VipCardGiveAiUndressFreeCount.Key(), + TranTypeInt: int64(txnmod.VipCardGiveAiUndressFreeCount), + TransNo: history.ID, + Desc: fmt.Sprintf("购买%s-赠送AI脱衣免费次数[%v次]", p.Name, p.AiUndressCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + txnLogs = append(txnLogs, aiLog) + } + + //插入购买会员卡赠送金币流水 + if p.GiveCoin > 0 && isRecharge { + giveLog := txnmod.TransactionLog{ + UID: uid, + Amount: p.GiveCoin, + ActualAmount: float64(p.GiveCoin), + TranType: txnmod.VipCardGive.Key(), + TranTypeInt: int64(txnmod.VipCardGive), + TransNo: history.ID, + Desc: "VIP购买-" + p.Name + "-赠送金币", + DiscDoc: discDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + txnLogs = append(txnLogs, giveLog) + if err = txnmod.InsertTransactionLog(t, &giveLog); err != nil { + return err + } + } + if p.DownloadCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: uid, + DownloadCount: p.DownloadCount, + TranType: txnmod.GiveDownload.Key(), + TranTypeInt: int64(txnmod.GiveDownload), + TransNo: history.ID, + Desc: fmt.Sprintf("VIP购买-%v", p.Name) + fmt.Sprintf("-赠送[%d]次数", p.DownloadCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + }) + } + if p.LuckyDrawCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: uid, + LotteryTimes: p.LuckyDrawCount, + TranType: txnmod.GiveLotteryTimesCount.Key(), + TranTypeInt: int64(txnmod.GiveLotteryTimesCount), + TransNo: history.ID, + Desc: fmt.Sprintf("VIP购买-%v", p.Name) + fmt.Sprintf("-赠送[%d]次抽奖", p.LuckyDrawCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + }) + } + + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + log.Warn(fmt.Sprintf("productser BuyVIP Transaction err %s", err.Error())) + return err + } + if !couponID.IsZero() { + if err = backpackmod.UseGoods(t, couponID); err != nil { + return err + } + } + // 金币视频观影券 + if coupons := HandleGoldVideoCoupon(p, uid, videocoupon.GoldVideoCouponSourceVIP); len(coupons) > 0 { + if err = videocoupon.InsertManyTrans(t, coupons); err != nil { + return err + } + } + + common.Go(func() { + _ = taskser.CompleteOnceTask(nil, uid, oncetaskmod.OnceTaskTypeUserBuyVip) + }) + return nil + }); err != nil { + log.Warn(fmt.Sprintf("productser BuyVIP Transaction err %s", err.Error())) + return stderr.BuyFailed //通知消息 + } + + return stderr.Success +} + +// BuyAdvanceCard 购买产品产生的行为 +func BuyAdvanceCard(uid uint64, productID, couponID primitive.ObjectID, sys string, isRecharge bool, ua ua.UA, ip string, experimentAttribution VIPExperimentAttribution) stderr.Code { + var ( + sel usermod.UserSelector + payVidDiscount int + changeStatus int + price int64 + dsc = "金币购买" + ) + + p, err := productmod.FindProduct(productID, sys) + if err != nil || p == nil { + return stderr.ErrParamError + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + // 检查用户VIP升级 + CheckUserUpgrade(uid, p) + + data, err := advanceordermod.IsExist(bson.M{"uid": uid, "productID": productID}) + if err != nil { + log.Error(fmt.Sprintf("金币购买预售卡-productID[%s] 查询预售订单信息异常 [%v]", productID.Hex(), err)) + return stderr.ErrNetWorkBusy + } + + if data == nil || data.ID.IsZero() { + var advanceOrder advanceordermod.AdvanceOrder + prepaidPrivilege := advanceordermod.AdvanceCardPrepaidPrivilege{} + if p.PrepaidPrivilege != nil { + prepaidPrivilege = advanceordermod.AdvanceCardPrepaidPrivilege{ + CoinVideoLimitPerDay: p.PrepaidPrivilege.CoinVideoLimitPerDay, + LuckyDrawLimitPerDay: p.PrepaidPrivilege.LuckyDrawLimitPerDay, + AiUndressLimitPerDay: p.PrepaidPrivilege.AiUndressLimitPerDay, + DownloadLimitPerDay: p.PrepaidPrivilege.DownloadLimitPerDay, + } + } + // 创建预售订单 + now := time.Now() + advanceOrder.UID = u.UID + advanceOrder.CreatedAt = now + advanceOrder.Status = advanceordermod.AdvanceProcessing + advanceOrder.AdvanceAmount = p.AdvanceAmount * 10 + advanceOrder.BalanceAmount = p.BalanceAmount * 10 + advanceOrder.StartTime = p.StartTime + advanceOrder.EndTime = p.EndTime + advanceOrder.PrepaidPrivilege = prepaidPrivilege + advanceOrder.TodayUse = advanceordermod.DayUse{} + advanceOrder.ProductID = p.ID + advanceOrder.TotalAmount = (p.AdvanceAmount + p.BalanceAmount) * 10 + data, err = advanceordermod.CreditOrder(nil, &advanceOrder) + if err != nil { + return stderr.ErrDbInsertError + } + changeStatus = advanceordermod.AdvanceSUCCESS + } else if data.Status == advanceordermod.AdvanceProcessing { + // 有预售订单,且是首付状态 + changeStatus = advanceordermod.AdvanceSUCCESS + } else if (data.Status == advanceordermod.AdvanceSUCCESS && time.Now().After(p.StartTime)) || data.Status == advanceordermod.BalanceProcessing { + // 尾款预付状态 + changeStatus = advanceordermod.BalanceSUCCESS + } else if data.Status == advanceordermod.AdvanceSUCCESS && !time.Now().After(p.StartTime) { + return stderr.AdvanceOrderPayTimeIsErr + } else if data.Status == advanceordermod.BalanceSUCCESS { + return stderr.AdvanceOrderStatusIsErr + } + + w, err := walletmod.GetWallet(uid) + if err != nil { + return stderr.ErrNetWorkBusy + } + if w == nil || w.ID.IsZero() { + return stderr.InsufficientBalance + } + + var fullPrivilege bool + var duration = p.Duration + var vipLevel = p.VipLevel + var tranType = txnmod.BuyAdvanceVIP + if changeStatus == advanceordermod.AdvanceSUCCESS { + duration = p.AdvanceDuration + // 会员持续时间不能超过预付过期时间 + if time.Now().AddDate(0, 0, duration).After(p.AdvanceExpires) { + td := p.AdvanceExpires.Sub(time.Now()) + duration = int(math.Round(td.Hours() / 24)) + if duration < 1 { + duration = 1 + } + } + + vipLevel = p.AdvanceVipLevel + price = p.AdvanceAmount + dsc = "预售预付金币购买" + } + if changeStatus == advanceordermod.BalanceSUCCESS { + price = p.BalanceAmount + dsc = "预售尾款金币购买" + fullPrivilege = true + tranType = txnmod.BuyBalanceVIP + } + + // 使用优惠卷(仅限金币购买) + if !couponID.IsZero() && p.IsAmountPay { + goodsDetail, err := backpackmod.GetGoodsDetail(couponID) + if err != nil { + return stderr.ErrNetWorkBusy + } + if goodsDetail == nil { + return stderr.ErrNetWorkBusy + } + price = decimal.NewFromInt(price).Mul(decimal.NewFromInt(goodsDetail.GoodsValue)).Div(decimal.NewFromInt(10)).IntPart() + } + plan := debitPlan(w, price) + if plan == nil { + return stderr.InsufficientBalance + } + if isRecharge { + plan.Amount -= p.GiveCoin + } + + // VIP变更 + if duration > 0 { + var expire time.Time + payVidDiscount = p.PayVidDiscount + // VIP未过期 + if u.VipExpireDate.After(time.Now()) { + if changeStatus == advanceordermod.BalanceSUCCESS { + expire = u.VipExpireDate.AddDate(0, 0, duration) + } + if changeStatus == advanceordermod.AdvanceSUCCESS { + expire = time.Now().AddDate(0, 0, duration) + } + + if u.VipLevel > p.VipLevel { + vipLevel = u.VipLevel + } + if u.PayVidDiscount > 0 && u.PayVidDiscount < p.PayVidDiscount { + payVidDiscount = u.PayVidDiscount + } + } else { + expire = time.Now().AddDate(0, 0, duration) + } + log.Info(fmt.Sprintf("到期时间:%v", expire)) + sel.VipExpireDate = &expire + sel.VipLevel = &vipLevel + sel.PayVidDiscount = &payVidDiscount + } + + // 增加金币视频免费天数 + if p.GoldVideoFreeDay > 0 && fullPrivilege { + var expire time.Time + if u.GoldVideoFreeExpire.IsZero() || u.GoldVideoFreeExpire.Before(time.Now()) { + if changeStatus == advanceordermod.BalanceSUCCESS { + expire = time.Now().AddDate(0, 0, p.GoldVideoFreeDay) + } + if changeStatus == advanceordermod.AdvanceSUCCESS { + expire = p.EndTime + } + } else { + if changeStatus == advanceordermod.BalanceSUCCESS { + expire = u.GoldVideoFreeExpire.AddDate(0, 0, p.GoldVideoFreeDay) + } + if changeStatus == advanceordermod.AdvanceSUCCESS { + expire = p.EndTime + } + } + sel.GoldVideoFreeExpire = &expire + sel.GoldVideoFreeLimit = &p.GoldVideoFreeLimit + } + if p.BroadcastDays > 0 { + expire := time.Time{} + if u.BroadcastExpire.IsZero() || u.BroadcastExpire.Before(time.Now()) { + expire = time.Now().AddDate(0, 0, p.BroadcastDays) + } else { + expire = u.BroadcastExpire.AddDate(0, 0, p.BroadcastDays) + } + sel.BroadcastExpire = &expire + } + if p.DramaDays > 0 && fullPrivilege { + expire := usermod.RenewDramaExpire(u.DramaExpire, time.Now(), p.DramaDays) + sel.DramaExpire = &expire + } + if p.AllGoldVideoFree && fullPrivilege { + free := true + sel.AllGoldVideoFree = &free + } + + if p.Name != "" { + sel.VipName = &p.Name + } + + if changeStatus == advanceordermod.BalanceSUCCESS && p.AiUndressCount > 0 { + plan.AiUndressFreeTimes = int64(-p.AiUndressCount) + } + if changeStatus == advanceordermod.BalanceSUCCESS && p.DownloadCount > 0 { + plan.DownloadCount = -p.DownloadCount + } + if changeStatus == advanceordermod.BalanceSUCCESS && p.LuckyDrawCount > 0 { + plan.LotteryTimes = -p.LuckyDrawCount + } + plan.Consumption = 1 + sel.ChatPrice = &p.ChatPrice + var orderId = primitive.NewObjectID() + var orderCreatedAt = time.Now() + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + var err error + discDoc := u.DiscDoc + wallet, err := walletmod.Debit(t, plan, uid) + if err != nil { //扣钱 + return err + } + + history := prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: orderId, + UID: uid, + ProductID: productID, + Name: p.Name, + Amount: plan.Amount, + Income: plan.Income, + ProductType: prdcthsomod.AdvanceCard, + AdvanceOrderStatus: changeStatus, // 新增的预售订单状态 + SysType: u.SysType, + ProductSnapShot: p, + IsUpgrade: p.IsUpgrade, + CurrentVipName: p.CurrentVipName, + CurrentVipPrice: p.CurrentVipPrice, + PurchasePrice: p.PurchasePrice, + DiscDoc: discDoc, + CreatedAt: orderCreatedAt, + ExperimentID: experimentAttribution.ExperimentID, + ExperimentVariant: experimentAttribution.ExperimentVariant, + SessionID: experimentAttribution.SessionID, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + if err = usermod.UpdateVIP(t, uid, u.VipExpireDate, sel); err != nil { + return err + } + txnLogs := []txnmod.TransactionLog{ + { + UID: uid, + Amount: -price, + ActualAmount: float64(-price), + TranType: tranType.Key(), + TranTypeInt: int64(tranType), + TransNo: history.ID, + Desc: fmt.Sprintf("%s-%s", dsc, p.Name), + DiscDoc: discDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + }, + } + if changeStatus == advanceordermod.BalanceSUCCESS { + + // 判断会员卡是否赠送AI脱衣免费次数 + if p.AiUndressCount > 0 { + aiLog := txnmod.TransactionLog{ + UID: uid, + Amount: int64(p.AiUndressCount), + ActualAmount: float64(p.AiUndressCount), + TranType: txnmod.VipCardGiveAiUndressFreeCount.Key(), + TranTypeInt: int64(txnmod.VipCardGiveAiUndressFreeCount), + TransNo: history.ID, + Desc: fmt.Sprintf("购买%s-赠送AI脱衣免费次数[%v次]", p.Name, p.AiUndressCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + txnLogs = append(txnLogs, aiLog) + } + + //插入购买会员卡赠送金币流水 + if p.GiveCoin > 0 && isRecharge { + giveLog := txnmod.TransactionLog{ + UID: uid, + Amount: p.GiveCoin, + ActualAmount: float64(p.GiveCoin), + TranType: txnmod.VipCardGive.Key(), + TranTypeInt: int64(txnmod.VipCardGive), + TransNo: history.ID, + Desc: "预售卡购买-" + p.Name + "-赠送金币", + DiscDoc: discDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + txnLogs = append(txnLogs, giveLog) + if err = txnmod.InsertTransactionLog(t, &giveLog); err != nil { + return err + } + } + if p.DownloadCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: uid, + DownloadCount: p.DownloadCount, + TranType: txnmod.GiveDownload.Key(), + TranTypeInt: int64(txnmod.GiveDownload), + TransNo: history.ID, + Desc: fmt.Sprintf("预售卡购买-%v", p.Name) + fmt.Sprintf("-赠送[%d]次数", p.DownloadCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + }) + } + if p.LuckyDrawCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: uid, + LotteryTimes: p.LuckyDrawCount, + TranType: txnmod.GiveLotteryTimesCount.Key(), + TranTypeInt: int64(txnmod.GiveLotteryTimesCount), + TransNo: history.ID, + Desc: fmt.Sprintf("预售卡购买-%v", p.Name) + fmt.Sprintf("-赠送[%d]次数", p.LuckyDrawCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + }) + } + } + // 修改预付订单状态 + err = advanceordermod.Update(t, data.ID, bson.M{"status": changeStatus}) + if err != nil { + log.Warn(fmt.Sprintf("productser Buy AdvanceCard advanceordermod Update Transaction err %s", err.Error())) + return err + } + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + log.Warn(fmt.Sprintf("productser Buy AdvanceCard Transaction err %s", err.Error())) + return err + } + if !couponID.IsZero() { + if err = backpackmod.UseGoods(t, couponID); err != nil { + return err + } + } + // 金币视频观影券 + if coupons := HandleGoldVideoCoupon(p, uid, videocoupon.GoldVideoCouponSourceVIP); len(coupons) > 0 { + if err = videocoupon.InsertManyTrans(t, coupons); err != nil { + return err + } + } + + return nil + }); err != nil { + log.Warn(fmt.Sprintf("productser Buy AdvanceCard Transaction err %s", err.Error())) + return stderr.BuyFailed //通知消息 + } + + return stderr.Success +} + +func BuyGameAdvanceCard(uid uint64, productID, couponID primitive.ObjectID, sys string, isRecharge bool, ua ua.UA, ip string, experimentAttribution VIPExperimentAttribution) stderr.Code { + var ( + sel usermod.UserSelector + vipLevel int + payVidDiscount int + price int64 + dsc = "金币购买" + ) + + p, err := productmod.FindProduct(productID, sys) + if err != nil || p == nil { + return stderr.ErrParamError + } + price = p.DiscountedPrice + + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + + w, err := walletmod.GetWallet(uid) + if err != nil { + return stderr.ErrNetWorkBusy + } + if w == nil || w.ID.IsZero() { + return stderr.InsufficientBalance + } + + plan := debitPlan(w, price) + if plan == nil { + return stderr.InsufficientBalance + } + if isRecharge { + plan.Amount -= p.GiveCoin + } + + // 获取三方游戏码 + gameCode, err := game.GainTripartiteGameCode(uid) + if err != nil { + log.Error(fmt.Sprintf("金币购买游戏预售-uid[%v] 获取游戏码信息异常 [%v]", uid, err)) + return stderr.ErrReqForbidden + } + + // VIP变更 + if p.SendGame && p.Duration > 0 { + var expire time.Time + vipLevel = p.VipLevel + payVidDiscount = p.PayVidDiscount + // VIP未过期 + if u.VipExpireDate.After(time.Now()) { + expire = u.VipExpireDate.AddDate(0, 0, p.Duration) + + if u.VipLevel > p.VipLevel { + vipLevel = u.VipLevel + } + if u.PayVidDiscount > 0 && u.PayVidDiscount < p.PayVidDiscount { + payVidDiscount = u.PayVidDiscount + } + } else { + expire = time.Now().AddDate(0, 0, p.Duration) + } + log.Info(fmt.Sprintf("到期时间:%v", expire)) + sel.VipExpireDate = &expire + sel.VipLevel = &vipLevel + sel.PayVidDiscount = &payVidDiscount + } + + // 增加金币视频免费天数 + if p.SendGame && p.GoldVideoFreeDay > 0 { + var expire time.Time + if u.GoldVideoFreeExpire.IsZero() || u.GoldVideoFreeExpire.Before(time.Now()) { + expire = time.Now().AddDate(0, 0, p.GoldVideoFreeDay) + } else { + expire = u.GoldVideoFreeExpire.AddDate(0, 0, p.GoldVideoFreeDay) + } + sel.GoldVideoFreeExpire = &expire + } + + if p.AllGoldVideoFree { + free := true + sel.AllGoldVideoFree = &free + } + + if p.SendGame && p.Name != "" { + sel.VipName = &p.Name + } + + if p.AiUndressCount > 0 { + plan.AiUndressFreeTimes = int64(-p.AiUndressCount) + } + if p.DownloadCount > 0 { + plan.DownloadCount = -p.DownloadCount + } + if p.LuckyDrawCount > 0 { + plan.LotteryTimes = -p.LuckyDrawCount + } + plan.Consumption = 1 + if p.ChatPrice > 0 { + sel.ChatPrice = &p.ChatPrice + } + + goodsList := make([]backpackmod.Backpack, 1) + now := time.Now() + goodsList[0] = backpackmod.Backpack{ + UID: uid, + GoodsName: p.Name, + GoodsType: backpackmod.GameCode, + GoodsValue: p.DiscountedPrice, + GoodsOrigin: fmt.Sprintf("金币购买"), + GoodsDesc: gameCode, + Status: backpackmod.Unused, + ExpiredTime: now.AddDate(0, 0, int(p.Duration)), + CreateTime: now, + } + var orderId = primitive.NewObjectID() + var orderCreatedAt = time.Now() + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + var err error + discDoc := u.DiscDoc + wallet, err := walletmod.Debit(t, plan, uid) + if err != nil { //扣钱 + return err + } + + history := prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: orderId, + UID: uid, + ProductID: productID, + Name: p.Name, + Amount: plan.Amount, + Income: plan.Income, + ProductType: prdcthsomod.GameAdvanceCard, + DiscDoc: discDoc, + SysType: u.SysType, + GameCode: fmt.Sprintf("AAAA-B-%v", time.Now().Nanosecond()), + ProductSnapShot: p, + CreatedAt: orderCreatedAt, + ExperimentID: experimentAttribution.ExperimentID, + ExperimentVariant: experimentAttribution.ExperimentVariant, + SessionID: experimentAttribution.SessionID, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + if err = usermod.UpdateVIP(t, uid, u.VipExpireDate, sel); err != nil { + return err + } + txnLogs := []txnmod.TransactionLog{ + { + UID: uid, + Amount: -price, + ActualAmount: float64(-price), + TranType: txnmod.BuyGameAdvanceVIP.Key(), + TranTypeInt: int64(txnmod.BuyGameAdvanceVIP), + TransNo: history.ID, + Desc: fmt.Sprintf("%s-%s", dsc, p.Name), + DiscDoc: discDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + }, + } + + // 判断会员卡是否赠送AI脱衣免费次数 + if p.AiUndressCount > 0 { + aiLog := txnmod.TransactionLog{ + UID: uid, + Amount: int64(p.AiUndressCount), + ActualAmount: float64(p.AiUndressCount), + TranType: txnmod.VipCardGiveAiUndressFreeCount.Key(), + TranTypeInt: int64(txnmod.VipCardGiveAiUndressFreeCount), + TransNo: history.ID, + Desc: fmt.Sprintf("购买%s-赠送AI脱衣免费次数[%v次]", p.Name, p.AiUndressCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + txnLogs = append(txnLogs, aiLog) + } + + //插入购买会员卡赠送金币流水 + if p.GiveCoin > 0 && isRecharge { + giveLog := txnmod.TransactionLog{ + UID: uid, + Amount: p.GiveCoin, + ActualAmount: float64(p.GiveCoin), + TranType: txnmod.VipCardGive.Key(), + TranTypeInt: int64(txnmod.VipCardGive), + TransNo: history.ID, + Desc: "游戏预售卡购买-" + p.Name + "-赠送金币", + DiscDoc: discDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + txnLogs = append(txnLogs, giveLog) + if err = txnmod.InsertTransactionLog(t, &giveLog); err != nil { + return err + } + } + if p.DownloadCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: uid, + DownloadCount: p.DownloadCount, + TranType: txnmod.GiveDownload.Key(), + TranTypeInt: int64(txnmod.GiveDownload), + TransNo: history.ID, + Desc: fmt.Sprintf("预售卡购买-%v", p.Name) + fmt.Sprintf("-赠送[%d]次数", p.DownloadCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + }) + } + + // 保存用户游戏码 + err = backpackmod.AddGoodsMany(t, uid, goodsList) + if err != nil { + return err + } + + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + log.Warn(fmt.Sprintf("productser Buy AdvanceCard Transaction err %s", err.Error())) + return err + } + + common.Go(func() { + _ = taskser.CompleteOnceTask(nil, uid, oncetaskmod.OnceTaskTypeUserBuyVip) + }) + return nil + }); err != nil { + log.Warn(fmt.Sprintf("productser Buy AdvanceCard Transaction err %s", err.Error())) + return stderr.BuyFailed //通知消息 + } + + return stderr.Success +} + +func debitPlanAmount(w *walletmod.Wallet, amt int64) *walletmod.DebitPlan { + p := walletmod.DebitPlan{} + l1 := w.Amount - amt + if l1 >= 0 { //amount够了 + p.Amount = amt + return &p + } + // l1 < 0 + return nil +} + +// VIPUp 购买产品产生的行为 +func VIPUp(uid uint64) stderr.Code { + cfg, err := vipconfigmod.FindOne() + if err != nil { + return stderr.ErrNetWorkBusy + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return stderr.ErrNetWorkBusy + } + vipExpire := time.Time{} + var vipUpPrice int64 + vipLevel := 2 + now := time.Now() + if !(u.VipExpireDate.After(now) && u.VipExpireDate.Before(now.AddDate(20, 0, 0))) { + return stderr.ErrInvalidRequest + } + vipExpire = u.VipExpireDate.AddDate(0, 0, 9999) + switch u.VipLevel { + case 1: + vipUpPrice = cfg.VipUpPrice + case 2: + vipUpPrice = cfg.SVipUpPrice + default: + } + plan := debitPlanAmount(w, vipUpPrice) + if plan == nil { + return stderr.InsufficientBalance + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + var err error + discDoc := u.DiscDoc + wallet, err := walletmod.Debit(t, plan, uid) + if err != nil { //扣钱 + return err + } + if err = usermod.UpdateVIP(t, uid, u.VipExpireDate, usermod.UserSelector{VipExpireDate: &vipExpire, VipLevel: &vipLevel}); err != nil { + return err + } + txnLog := txnmod.TransactionLog{UID: uid, + Amount: -vipUpPrice, + ActualAmount: float64(-vipUpPrice), + TranType: txnmod.PayVIP.Key(), + TranTypeInt: int64(txnmod.PayVIP), + Desc: "升级永久VIP", + DiscDoc: discDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + return txnmod.InsertTransactionLog(t, &txnLog) + }); err != nil { + log.Warn(fmt.Sprintf("productser BuyVIP Transaction err %s", err.Error())) + return stderr.BuyFailed + } + return stderr.Success +} + +func BuyVIP_d(t *db.MongoTool, uid uint64, p productmod.Product, giveGold int64) error { + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return errors.New("user is null") + } + //新用户8折 + /*if u.CreatedAt.Add(time.Hour*24).After(time.Now()) && p.ProductType == prdcthsomod.VIP { + p.DiscountedPrice = int64(float64(p.DiscountedPrice) * 0.8) + }*/ + vipExpire, vipLevel, payVidDiscount := checkVipRenew(u, &p) + if err != nil { + return stderr.ErrNetWorkBusy + } + wallet, err := walletmod.GetWallet(uid) + if err != nil { + return err + } + history := prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: primitive.NewObjectID(), + UID: uid, + ProductID: p.ID, + Name: p.Name, + Amount: p.DiscountedPrice, + ProductType: prdcthsomod.VIP, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + ProductSnapShot: &p, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + sel := usermod.UserSelector{VipExpireDate: &vipExpire, VipLevel: &vipLevel, PayVidDiscount: &payVidDiscount} + if p.GoldVideoFreeDay > 0 { + expire := time.Time{} + if u.GoldVideoFreeExpire.IsZero() || u.GoldVideoFreeExpire.Before(time.Now()) { + expire = time.Now().AddDate(0, 0, p.GoldVideoFreeDay) + } else { + expire = u.GoldVideoFreeExpire.AddDate(0, 0, p.GoldVideoFreeDay) + } + sel.GoldVideoFreeExpire = &expire + } + if p.DramaDays > 0 { + expire := usermod.RenewDramaExpire(u.DramaExpire, time.Now(), p.DramaDays) + sel.DramaExpire = &expire + } + if err = usermod.UpdateVIP(t, uid, u.VipExpireDate, sel); err != nil { + return err + } + txnLogs := []txnmod.TransactionLog{txnmod.TransactionLog{UID: uid, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + TranType: txnmod.PayVIP.Key(), + TranTypeInt: int64(txnmod.PayVIP), + TransNo: history.ID, + Desc: "VIP购买-" + p.Name, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + CurrencyType: txnmod.CurrencyTypeCash, + }} + // 插入日志 + if giveGold > 0 { + giveLog := txnmod.TransactionLog{UID: uid, + Amount: giveGold, + ActualAmount: float64(giveGold), + TranType: txnmod.VipCardGive.Key(), + TranTypeInt: int64(txnmod.VipCardGive), + TransNo: history.ID, + Desc: "VIP购买-" + p.Name + "-赠送金币", + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + txnLogs = append(txnLogs, giveLog) + } + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + log.Warn(fmt.Sprintf("productser BuyVIP Transaction err %s", err.Error())) + return err + } + // 赠送观影券 + coupons := HandleGoldVideoCoupon(&p, uid, videocoupon.GoldVideoCouponSourceVIP) + if len(coupons) > 0 { + if err = videocoupon.InsertMany(coupons); err != nil { + return err + } + } + return nil +} + +func publiserTaxLevel(publisherID uint64) (int64, float64, int, int) { + u, err := usermod.FindUserByUID(publisherID) + if err != nil || u == nil { + return 0, 0, 0, 0 + } + return u.TaxLevel, u.VideoDeduction, u.VideoDeductionCount, u.VideoDeductionPayCount +} + +func BuyVid(uid uint64, vid primitive.ObjectID, goldVideoCouponNum int, ua ua.UA, ip string) stderr.Code { + var code stderr.Code + hasBought, err := payvidlgmod.IsPay4Video(uid, vid) + if err != nil { + return stderr.ErrNetWorkBusy + } + if hasBought { + return stderr.RepeatPurchase + } + v, err := vidmod.GetVideoInfo(vid.Hex()) + if err != nil { + return stderr.ErrNetWorkBusy + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + switch v.NewsType { + case vidmod.COVER, vidmod.PIC: + code = buyCoverVid(*u, v, ua, ip) + case vidmod.SEED_LINK: + code = buySeedUrlVid(*u, v, ua, ip) + default: + // 判断视频是否在折扣区 + discountArea, err := discount_area_mod.GetDiscountAreaById(v.DiscountAreaId) + if err != nil { + return stderr.ErrNetWorkBusy + } + // 用户是vip且视频在折扣区内打折,使用折扣区的购买逻辑 + if u.IsVIP(time.Now()) && !discountArea.ID.IsZero() { + // 购买折扣区视频 + code = buyDiscountAreaVideo(*u, v, discountArea, ua, ip) + } else { + // 购买普通视频帖子 + code = buyNormalVid(*u, v, goldVideoCouponNum, ua, ip) + } + } + if code != stderr.Success { + return code + } + // 完成日常任务 + taskser.CompleteDailyTask(nil, uid, dailytaskmod.DailyUnlockByGold) + + return stderr.Success +} + +// 购买种子链接帖子 +func buySeedUrlVid(u usermod.User, v vidmod.VideoModel, ua ua.UA, ip string) stderr.Code { // 购买图片帖子 + // 判断钱包余额是否足够, 做出扣款计划 + wallet, err := walletmod.GetWallet(u.UID) + if err != nil || wallet == nil { + return stderr.ErrNetWorkBusy + } + + var orderId = primitive.NewObjectID() + var orderCreatedAt = time.Now() + + plan := debitPlan(wallet, v.Coins) + if plan == nil { // 余额不足无法做出扣款计划 + return stderr.InsufficientBalance + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + wallet, err = walletmod.Debit(t, plan, u.UID) + if err != nil { + return err + } + + deducated := false + pIncome := decimal.NewFromInt(0) + publisherIncome, _ := pIncome.Float64() + taxAmount := float64(v.Coins) + pv := payvidlgmod.Pay4VidLog{ + ID: orderId, + UID: u.UID, + VideoID: v.ID, + NewsType: v.NewsType, + PlayTime: v.PlayTime, + Coins: v.Coins, + PayMoney: v.Coins, + Tax: 0, + TaxAmount: taxAmount, + Title: v.Title, + PublisherIncome: publisherIncome, + PublisherID: v.PublisherID, + Uniq: payvidlgmod.Unique(u.UID, v.ID), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + IsVideoDeduction: deducated, + CreatedAt: orderCreatedAt, + } + if err = payvidlgmod.InsertVideoPayRecord(t, pv); err != nil { + return err + } + tl := txnmod.TransactionLog{TransNo: pv.ID, + UID: u.UID, + Amount: -v.Coins, + ActualAmount: float64(-v.Coins), + TranType: txnmod.PayVID.Key(), + TranTypeInt: int64(txnmod.PayVID), + Desc: "付费种子链接-" + pv.Title, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + if err = txnmod.InsertTransactionLog(t, &tl); err != nil { + return err + } + + return nil + }); err != nil { + log.Warn(fmt.Sprintf("productser BuyVid Transaction err [%s]", err.Error())) + return stderr.BuyFailed + } + if err = vidmod.IncPurchaseCount(v.ID); err != nil { + log.Warn("productser IncPurchaseCount err", log.Any("vid", v.ID), log.E(err)) + } + + return stderr.Success +} + +// 购买图片帖子 +func buyCoverVid(u usermod.User, v vidmod.VideoModel, ua ua.UA, ip string) stderr.Code { // 购买图片帖子 + /* + 购买图片时: + - VIP折扣和视频折扣卡不可用 + - 视频抵用券不可用 + - 金币视频抵用券不可用 + */ + // 判断钱包余额是否足够, 做出扣款计划 + wallet, err := walletmod.GetWallet(u.UID) + if err != nil || wallet == nil { + return stderr.ErrNetWorkBusy + } + + var orderId = primitive.NewObjectID() + var orderCreatedAt = time.Now() + + plan := debitPlan(wallet, v.Coins) + if plan == nil { // 余额不足无法做出扣款计划 + return stderr.InsufficientBalance + } + + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + wallet, err = walletmod.Debit(t, plan, u.UID) + if err != nil { + return err + } + + deducated := false + pIncome := decimal.NewFromInt(0) + publisherIncome, _ := pIncome.Float64() + taxAmount := float64(v.Coins) + pv := payvidlgmod.Pay4VidLog{ + ID: orderId, + UID: u.UID, + VideoID: v.ID, + PlayTime: v.PlayTime, + Coins: v.Coins, + PayMoney: v.Coins, + NewsType: v.NewsType, + Tax: 0, + TaxAmount: taxAmount, + Title: v.Title, + PublisherIncome: publisherIncome, + PublisherID: v.PublisherID, + Uniq: payvidlgmod.Unique(u.UID, v.ID), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + IsVideoDeduction: deducated, + CreatedAt: orderCreatedAt, + } + if err = payvidlgmod.InsertVideoPayRecord(t, pv); err != nil { + return err + } + tl := txnmod.TransactionLog{TransNo: pv.ID, + UID: u.UID, + Amount: -v.Coins, + ActualAmount: float64(-v.Coins), + TranType: txnmod.PayVID.Key(), + TranTypeInt: int64(txnmod.PayVID), + Desc: "付费图片-" + pv.Title, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + if err = txnmod.InsertTransactionLog(t, &tl); err != nil { + return err + } + // 图片不计入博主收益, 只计入博主图片出售数量 + _ = usermod.IncCoverCount(v.PublisherID) + return nil + }); err != nil { + log.Warn(fmt.Sprintf("productser BuyVid Transaction err [%s]", err.Error())) + return stderr.BuyFailed + } + if err = vidmod.IncPurchaseCount(v.ID); err != nil { + log.Warn("productser IncPurchaseCount err", log.Any("vid", v.ID), log.E(err)) + } + + return stderr.Success +} + +// 购买普通视频帖子 +func buyNormalVid(u usermod.User, v vidmod.VideoModel, goldVideoCouponNum int, ua ua.UA, ip string) stderr.Code { + now := time.Now() + // 是否启用视频抵用券 + videoCoupons, err := videocoupon.GetByUIDAndCouponNum(u.UID, goldVideoCouponNum) + if err != nil { + return stderr.ErrNetWorkBusy + } + chooseCoupon, err := CheckGoldVideoCoupon(int64(goldVideoCouponNum), v.Coins, &u, videoCoupons) + if err != stderr.Success { + return err.(stderr.Code) + } + if u.IsVIP(now) && u.PayVidDiscount > 0 && goldVideoCouponNum <= 0 { + v.Coins = decimal.NewFromInt(int64(u.PayVidDiscount)).Shift(-1). + Mul(decimal.NewFromInt(v.Coins)).Round(0).IntPart() + } + goldVideoFreeLimit := usermod.GetGoldVideoFreeLimit(&u) + if !chooseCoupon { + // 计算用户是否拥有视频折扣卡 + // 如果用户拥有视频折扣卡, 则购买视频时计算折后价 + vDiscLog, err := videodiscountmod.GetByUID(u.UID) + if err != nil { + return stderr.ErrNetWorkBusy + } + // 用户购买了视频免费卡(30金币以下免费) + if v.Coins < goldVideoFreeLimit && (u.GoldVideoFreeExpire.After(now) || (u.VideoFreeExpiration != nil && u.VideoFreeExpiration.After(now))) { + v.Coins = 0 + // 直接返回成功,这个是个金币免费视频,不需要写入购买记录 + return stderr.Success + } else { + // 用户购买了视频折扣卡 + if vDiscLog.Expiration.After(now) && vDiscLog.VideoDiscount > 0 { + discountedCoins := decimal.NewFromInt(int64(vDiscLog.VideoDiscount)).Shift(-1). + Mul(decimal.NewFromInt(v.Coins)).Round(0).IntPart() + if discountedCoins < v.Coins { + v.Coins = discountedCoins // 若折扣后价格高于原价, 则按原价计算 + } + } + } + if u.AllGoldVideoFree && u.GoldVideoFreeExpire.After(now) { + v.Coins = 0 + } + } + + // 判断钱包余额是否足够, 做出扣款计划 + var wallet *walletmod.Wallet + wallet, err = walletmod.GetWallet(u.UID) + if err != nil { + return stderr.ErrNetWorkBusy + } + + var orderId = primitive.NewObjectID() + var orderCreatedAt = time.Now() + + if (wallet == nil || wallet.ID.IsZero()) && v.Coins == 0 { + var p walletmod.CreditPlan + var amount int64 = 0 + p.Amount = &amount + wallet, err = walletmod.Credit(nil, p, u.UID) + if err != nil { + return stderr.ErrNetWorkBusy + } + if wallet == nil || wallet.ID.IsZero() { + return stderr.ErrNetWorkBusy + } + } + plan := debitPlan(wallet, v.Coins) + if goldVideoCouponNum <= 0 && plan == nil { // 当不使用抵用券且无法做出扣款计划时, 说明余额不足 + return stderr.InsufficientBalance + } + taxLevel := constant.DefaultBloggerVideoIncomeTaxLevel + /** TODO: + 默认视频分成为6/4分成 + */ + tl, videoDeduction, videoDeductionCount, videoDeductionPayCount := publiserTaxLevel(v.PublisherID) + if tl > 0 && tl <= 10 { + taxLevel = tl + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + if !chooseCoupon { // 不使用金币视频抵用券, 根据扣款计划做出扣款 + wallet, err = walletmod.Debit(t, plan, u.UID) + if err != nil { + return err + } + + } else { // 使用金币折扣券, 则不扣款, 只进行抵用券扣除操作 + var sel usermod.UserSelector + if u.GoldVideoCoupon != nil && len(u.GoldVideoCoupon) > 0 { // 兼容老数据 + goldVideoCoupon := make([]usermod.UserGoldVideoCoupon, 0) + for _, gvc := range u.GoldVideoCoupon { + if gvc.Gold == goldVideoCouponNum { + gvc.Count -= 1 + } + if gvc.Count > 0 { + goldVideoCoupon = append(goldVideoCoupon, gvc) + } + } + sel.GoldVideoCoupon = &goldVideoCoupon + //更新用户金币视频抵用券 + if _, err = usermod.UpdateTrans(t, u.UID, sel); err != nil { + return err + } + usedCoupon := videocoupon.UserGoldVideoCoupon{ + UID: u.UID, + Num: goldVideoCouponNum, + Source: videocoupon.GoldVideoCouponSourceVIP, + Used: true, + CreatedAt: now, + UpdatedAt: now, + } + // 记录用户使用老版观影券 + if err = videocoupon.InsertOne(usedCoupon); err != nil { + return err + } + } else { // 使用新版观影券 + if err = videocoupon.UseOneCoupon(videoCoupons); err != nil { + return err + } + } + } + // 是否扣量以及税率计算 + deducated := checkVideoDeduction(videoDeduction, videoDeductionCount, videoDeductionPayCount, v.PublisherID, goldVideoCouponNum) + ta := decimal.NewFromInt(v.Coins).Mul(decimal.NewFromInt(taxLevel).Div(decimal.NewFromInt(10))) + pIncome := decimal.NewFromInt(v.Coins).Sub(ta) + publisherIncome, _ := pIncome.Float64() + taxAmount, _ := ta.Float64() + pv := payvidlgmod.Pay4VidLog{ + ID: orderId, + UID: u.UID, + VideoID: v.ID, + PlayTime: v.PlayTime, + NewsType: v.NewsType, + Coins: v.Coins, + PayMoney: v.Coins, + Tax: taxLevel, + TaxAmount: taxAmount, + Title: v.Title, + PublisherIncome: publisherIncome, + PublisherID: v.PublisherID, + Uniq: payvidlgmod.Unique(u.UID, v.ID), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + IsVideoDeduction: deducated, + CreatedAt: orderCreatedAt, + } + if err = payvidlgmod.InsertVideoPayRecord(t, pv); err != nil { + return err + } + tl := txnmod.TransactionLog{TransNo: pv.ID, + UID: u.UID, + Amount: -v.Coins, + ActualAmount: float64(-v.Coins), + TranType: txnmod.PayVID.Key(), + TranTypeInt: int64(txnmod.PayVID), + Desc: "付费视频-" + pv.Title, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + if goldVideoCouponNum > 0 { + tl.Desc = "金币抵用券" + strconv.Itoa(goldVideoCouponNum) + "-购买视频-" + pv.Title + tl.TranType = txnmod.GoldCouplePayVID.Key() + tl.TranTypeInt = int64(txnmod.GoldCouplePayVID) + tl.ActualAmount = 0 + } + if err = txnmod.InsertTransactionLog(t, &tl); err != nil { + return err + } + // 扣量判断 + if deducated { + return nil + } + //此时计算收益 收益分为两个部分 整数部分 10 小数部分 1 + //计算小数部分 + //与当前收益相加 + pw, err := walletmod.GetWallet(v.PublisherID) + if err != nil { + return stderr.ErrNetWorkBusy + } + pwPot, vidIncome := float64(0), float64(0) + if pw != nil { + pwPot = pw.IncomePot + vidIncome = pw.VidIncome + } + income := pIncome.Add(decimal.NewFromFloat(pwPot)) + incomef, _ := pIncome.Add(decimal.NewFromFloat(vidIncome)).Float64() + incomeInt := income.IntPart() + incomePot, _ := income.Sub(decimal.NewFromInt(incomeInt)).Float64() + //增加钱包虚拟货币 + wallet, err = walletmod.CreditIncomeBasePot(t, incomeInt, incomef, incomePot, v.PublisherID) + if err != nil { + return err + } + tl = txnmod.TransactionLog{TransNo: pv.ID, + UID: v.PublisherID, + RechargeId: u.UID, + Amount: v.Coins, + ActualAmount: publisherIncome, + Tax: taxLevel, + TaxAmount: taxAmount, + TranType: txnmod.WorksIncome.Key(), + TranTypeInt: int64(txnmod.WorksIncome), + Desc: "视频收益-" + v.Title, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + + //插入一收益流水 + return txnmod.InsertTransactionLog(t, &tl) + }); err != nil { + log.Warn(fmt.Sprintf("productser BuyVid Transaction err [%s]", err.Error())) + return stderr.BuyFailed + } + if err = vidmod.IncPurchaseCount(v.ID); err != nil { + log.Warn("productser IncPurchaseCount err", log.Any("vid", v.ID), log.E(err)) + } + + return stderr.Success +} + +// 购买折扣专区的视频 +func buyDiscountAreaVideo(u usermod.User, v vidmod.VideoModel, discountArea discount_area_mod.DiscountArea, ua ua.UA, ip string) stderr.Code { + // 视频原价 + originCoins := v.Coins + // 如果是在折扣专区里,按照折扣专区价格来 + if !discountArea.ID.IsZero() { + newPrice := decimal.NewFromInt(int64(discountArea.Discount)).Shift(-2). + Mul(decimal.NewFromInt(originCoins)).Round(0).IntPart() + v.Coins = newPrice + } + if v.Coins == 0 { + // 根本不需要购买 + return stderr.Success + } + // 判断钱包余额是否足够, 做出扣款计划 + var wallet *walletmod.Wallet + wallet, err := walletmod.GetWallet(u.UID) + if err != nil { + return stderr.ErrNetWorkBusy + } + if (wallet == nil || wallet.ID.IsZero()) && v.Coins > 0 { + return stderr.ErrNetWorkBusy + } + var orderId = primitive.NewObjectID() + var orderCreatedAt = time.Now() + + if (wallet == nil || wallet.ID.IsZero()) && v.Coins == 0 { + var p walletmod.CreditPlan + var amount int64 = 0 + p.Amount = &amount + wallet, err = walletmod.Credit(nil, p, u.UID) + if err != nil { + return stderr.ErrNetWorkBusy + } + if wallet == nil || wallet.ID.IsZero() { + return stderr.ErrNetWorkBusy + } + } + plan := debitPlan(wallet, v.Coins) + if plan == nil { // 当无法做出扣款计划时, 说明余额不足 + return stderr.InsufficientBalance + } + taxLevel := constant.DefaultBloggerVideoIncomeTaxLevel + /** TODO: + 默认视频分成为6/4分成 + */ + tl, videoDeduction, videoDeductionCount, videoDeductionPayCount := publiserTaxLevel(v.PublisherID) + if tl > 0 && tl <= 10 { + taxLevel = tl + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + //根据扣款计划做出扣款 + wallet, err = walletmod.Debit(t, plan, u.UID) + if err != nil { + return err + } + // 是否扣量以及税率计算 + deducated := checkVideoDeduction(videoDeduction, videoDeductionCount, videoDeductionPayCount, v.PublisherID, 0) + ta := decimal.NewFromInt(v.Coins).Mul(decimal.NewFromInt(taxLevel).Div(decimal.NewFromInt(10))) + pIncome := decimal.NewFromInt(v.Coins).Sub(ta) + publisherIncome, _ := pIncome.Float64() + taxAmount, _ := ta.Float64() + pv := payvidlgmod.Pay4VidLog{ + ID: orderId, + UID: u.UID, + VideoID: v.ID, + PlayTime: v.PlayTime, + NewsType: v.NewsType, + Coins: v.Coins, + PayMoney: v.Coins, + Tax: taxLevel, + TaxAmount: taxAmount, + Title: v.Title, + PublisherIncome: publisherIncome, + PublisherID: v.PublisherID, + Uniq: payvidlgmod.Unique(u.UID, v.ID), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + IsVideoDeduction: deducated, + CreatedAt: orderCreatedAt, + } + if err = payvidlgmod.InsertVideoPayRecord(t, pv); err != nil { + return err + } + tl := txnmod.TransactionLog{TransNo: pv.ID, + UID: u.UID, + Amount: -v.Coins, + ActualAmount: float64(-v.Coins), + TranType: txnmod.PayVID.Key(), + TranTypeInt: int64(txnmod.PayVID), + Desc: "付费VIP折扣区视频-" + pv.Title, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + + if err = txnmod.InsertTransactionLog(t, &tl); err != nil { + return err + } + // 扣量判断 + if deducated { + return nil + } + //此时计算收益 收益分为两个部分 整数部分 10 小数部分 1 + //计算小数部分 + //与当前收益相加 + pw, err := walletmod.GetWallet(v.PublisherID) + if err != nil { + return stderr.ErrNetWorkBusy + } + pwPot, vidIncome := float64(0), float64(0) + if pw != nil { + pwPot = pw.IncomePot + vidIncome = pw.VidIncome + } + income := pIncome.Add(decimal.NewFromFloat(pwPot)) + incomef, _ := pIncome.Add(decimal.NewFromFloat(vidIncome)).Float64() + incomeInt := income.IntPart() + incomePot, _ := income.Sub(decimal.NewFromInt(incomeInt)).Float64() + //增加钱包虚拟货币 + wallet, err = walletmod.CreditIncomeBasePot(t, incomeInt, incomef, incomePot, v.PublisherID) + if err != nil { + return err + } + tl = txnmod.TransactionLog{TransNo: pv.ID, + UID: v.PublisherID, + RechargeId: u.UID, + Amount: v.Coins, + ActualAmount: publisherIncome, + Tax: taxLevel, + TaxAmount: taxAmount, + TranType: txnmod.WorksIncome.Key(), + TranTypeInt: int64(txnmod.WorksIncome), + Desc: "视频收益-" + v.Title, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + //插入一收益流水 + return txnmod.InsertTransactionLog(t, &tl) + }); err != nil { + log.Warn(fmt.Sprintf("productser BuyVid Transaction err [%s]", err.Error())) + return stderr.BuyFailed + } + if err = vidmod.IncPurchaseCount(v.ID); err != nil { + log.Warn("productser IncPurchaseCount err", log.Any("vid", v.ID), log.E(err)) + } + + return stderr.Success +} + +func makeSerials(giftLeft uint32, quantity int32) (list []uint32) { + list = make([]uint32, quantity+1) + for i := int32(1); i <= quantity; i++ { + list[i] = 300 - giftLeft + uint32(i) + } + return +} + +// 购买嫩模币 +func BuyModel(uid uint64, req newactivity.BuyReq) stderr.Code { + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return stderr.ErrNetWorkBusy + } + //一份嫩模币=10元=100金币 + price := decimal.NewFromInt32(req.Quantity).Mul(decimal.NewFromInt32(100)) + plan := debitPlan(w, price.IntPart()) + if plan == nil { + return stderr.InsufficientBalance + } + var desc string + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + wallet, err := walletmod.Debit(t, plan, uid) + if err != nil { //扣钱 + return err + } + date := timeutil.BeginningOfDay(time.Now()) + if time.Now().Hour() >= 20 { + date = date.Add(24 * time.Hour) + } + giftLeft, location, err := newactivity.UpdateStock(t, req, date) + if err != nil { + return err + } + record := newactivity.SoldRecord{ + Date: date, + ModelId: req.ModelId, + UserId: uint32(uid), + UserName: u.Name, + UserLogo: u.Portrait, + Quantity: req.Quantity, + SoldOut: req.BuyOut, + } + if !req.BuyOut { + record.Serials = makeSerials(giftLeft, req.Quantity) + } + id, err := newactivity.InsertSoldLog(t, &record) + if err != nil { + return err + } + history := prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: primitive.NewObjectID(), + UID: uid, + ProductID: primitive.NilObjectID, + Name: "嫩模币", + Amount: plan.Amount, + Income: plan.Income, + ProductType: prdcthsomod.MODEL, + SysType: u.SysType, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + desc = "参与活动:嫩模之夜," + makeDesc(date, location, req) + txnLog := txnmod.TransactionLog{UID: uid, + Amount: -price.IntPart(), + ActualAmount: float64(-price.IntPart()), + TranType: txnmod.NengModel.Key(), + TranTypeInt: int64(txnmod.NengModel), + TransNo: id, + Desc: desc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + return txnmod.InsertTransactionLog(t, &txnLog) + }); err != nil { + log.Warn(fmt.Sprintf("productser BuyModel Transaction err %s", err.Error())) + return stderr.BuyFailed + } + return stderr.Success +} + +func makeDesc(date time.Time, location string, req newactivity.BuyReq) (desc string) { + desc = "赠送" + date.Format("0102") + "期," + desc += location + strconv.Itoa(int(req.ModelId)) + desc += "号嫩模" + strconv.Itoa(int(req.Quantity)) + "份礼物" + return +} + +// 使用机器人购买嫩模 +func BuyModelFakeUser(uid uint64, req newactivity.BuyReq) stderr.Code { + wallet, err := walletmod.GetWallet(uid) + if err != nil { + return stderr.ErrNetWorkBusy + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + //一份嫩模币=10元=100金币 + price := decimal.NewFromInt32(req.Quantity).Mul(decimal.NewFromInt32(100)) + var desc string + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + date := timeutil.BeginningOfDay(time.Now()) + if time.Now().Hour() >= 20 { + date = date.Add(24 * time.Hour) + } + giftLeft, location, err := newactivity.UpdateStock(t, req, date) + if err != nil { + return err + } + record := newactivity.SoldRecord{ + Date: date, + ModelId: req.ModelId, + UserId: uint32(uid), + UserName: u.Name, + UserLogo: u.Portrait, + Quantity: req.Quantity, + SoldOut: req.BuyOut, + } + if !req.BuyOut { + record.Serials = makeSerials(giftLeft, req.Quantity) + } + id, err := newactivity.InsertSoldLog(t, &record) + if err != nil { + return err + } + history := prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: primitive.NewObjectID(), + UID: uid, + ProductID: primitive.NilObjectID, + Name: "嫩模币", + Amount: price.IntPart(), + Income: 0, + ProductType: prdcthsomod.MODEL, + SysType: u.SysType, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + desc = "参与活动:嫩模之夜," + makeDesc(date, location, req) + txnLog := txnmod.TransactionLog{UID: uid, + Amount: -price.IntPart(), + ActualAmount: float64(-price.IntPart()), + TranType: txnmod.NengModel.Key(), + TranTypeInt: int64(txnmod.NengModel), + TransNo: id, + Desc: desc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + return txnmod.InsertTransactionLog(t, &txnLog) + }); err != nil { + log.Warn(fmt.Sprintf("productser BuyModel Transaction err %s", err.Error())) + return stderr.BuyFailed + } + return stderr.Success +} + +// 扣除指定的游戏币 +func DeductGameCoins(uid uint64, deductCoin int64) stderr.Code { + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return stderr.ErrNetWorkBusy + } + plan := debitPlan(w, deductCoin) + if plan == nil { + return stderr.InsufficientBalance + } + var desc string + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + wallet, err := walletmod.Debit(t, plan, uid) + if err != nil { //扣钱 + return err + } + history := prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: primitive.NewObjectID(), + UID: uid, + ProductID: primitive.NilObjectID, + Name: "游戏币", + Amount: plan.Amount, + Income: plan.Income, + ProductType: prdcthsomod.GAME, + SysType: u.SysType, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + txnLog := txnmod.TransactionLog{UID: uid, + Amount: -deductCoin, + ActualAmount: float64(-deductCoin), + TranType: txnmod.GameCoin.Key(), + TranTypeInt: int64(txnmod.GameCoin), + Desc: desc, + TransNo: history.ID, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + return txnmod.InsertTransactionLog(t, &txnLog) + }); err != nil { + log.Warn(fmt.Sprintf("productser DeductGameCoins Transaction err %s", err.Error())) + return stderr.BuyFailed + } + return stderr.Success +} + +func BuyMeetingCard(uid uint64, productID primitive.ObjectID, sys string, ua ua.UA, ip string) stderr.Code { + p, err := productmod.FindProduct(productID, sys) + if err != nil || p == nil { + return stderr.ErrParamError + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return stderr.ErrNetWorkBusy + } + + var orderId = primitive.NewObjectID() + var orderCreatedAt = time.Now() + + plan := debitPlan(w, p.DiscountedPrice) + if plan == nil { + return stderr.InsufficientBalance + } + if p.GiveCoin > 0 { + plan.Amount = plan.Amount + p.GiveCoin + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + discDoc := u.DiscDoc + wallet, err := walletmod.Debit(t, plan, uid) + if err != nil { //扣钱 + return err + } + + history := prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: orderId, + UID: uid, + ProductID: productID, + Name: p.Name, + Amount: plan.Amount, + Income: plan.Income, + ProductType: prdcthsomod.MeetingCard, + DiscDoc: discDoc, + SysType: u.SysType, + ProductSnapShot: p, + CreatedAt: orderCreatedAt, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + txnLog := txnmod.TransactionLog{ + UID: uid, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + TranType: txnmod.MeetingCard.Key(), + TranTypeInt: int64(txnmod.MeetingCard), + TransNo: history.ID, + Desc: "购买-" + p.Name, + DiscDoc: discDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + + return txnmod.InsertTransactionLog(t, &txnLog) + }); err != nil { + log.Warn(fmt.Sprintf("productser buy meetingcard transaction err %s", err.Error())) + return stderr.BuyFailed + } + + return stderr.Success +} + +func BuyMeetingCard_d(t *db.MongoTool, uid uint64, p productmod.Product) error { + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return errors.New("user is null") + } + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return err + } + history := prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: primitive.NewObjectID(), + UID: uid, + ProductID: p.ID, + Name: p.Name, + Amount: p.DiscountedPrice, + ProductType: prdcthsomod.MeetingCard, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + ProductSnapShot: &p, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + txnLog := txnmod.TransactionLog{ + UID: uid, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + TranType: txnmod.MeetingCard.Key(), + TranTypeInt: int64(txnmod.MeetingCard), + TransNo: history.ID, + Desc: "购买-" + p.Name, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: w.RealAmount(), + CurrencyType: txnmod.CurrencyTypeCash, + } + if err = txnmod.InsertTransactionLog(t, &txnLog); err != nil { + return err + } + return nil +} + +func BuyOtherCard(uid uint64, productID primitive.ObjectID, sys string, ua ua.UA, ip string) stderr.Code { + p, err := productmod.FindProduct(productID, sys) + if err != nil || p == nil { + return stderr.ErrParamError + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return stderr.ErrNetWorkBusy + } + + var orderId = primitive.NewObjectID() + var orderCreatedAt = time.Now() + + plan := debitPlan(w, p.DiscountedPrice) + if plan == nil { + return stderr.InsufficientBalance + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + discDoc := u.DiscDoc + wallet, err := walletmod.Debit(t, plan, uid) + if err != nil { //扣钱 + return err + } + + history := prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: orderId, + UID: uid, + ProductID: productID, + Name: p.Name, + Amount: plan.Amount, + Income: plan.Income, + ProductType: prdcthsomod.OTHER, + DiscDoc: discDoc, + SysType: u.SysType, + ProductSnapShot: p, + CreatedAt: orderCreatedAt, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + txnLog := txnmod.TransactionLog{ + UID: uid, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + TranType: txnmod.Other.Key(), + TranTypeInt: int64(txnmod.Other), + TransNo: history.ID, + Desc: "购买-" + p.Name, + DiscDoc: discDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + + return txnmod.InsertTransactionLog(t, &txnLog) + }); err != nil { + log.Warn(fmt.Sprintf("productser buy otherCard transaction err %s", err.Error())) + return stderr.BuyFailed + } + + return stderr.Success +} + +func BuyOtherCard_d(t *db.MongoTool, uid uint64, p productmod.Product) error { + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return errors.New("user is null") + } + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return err + } + history := prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: primitive.NewObjectID(), + UID: uid, + ProductID: p.ID, + Name: p.Name, + Amount: p.DiscountedPrice, + ProductType: prdcthsomod.OTHER, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + ProductSnapShot: &p, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + txnLog := txnmod.TransactionLog{ + UID: uid, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + TranType: txnmod.Other.Key(), + TranTypeInt: int64(txnmod.Other), + TransNo: history.ID, + Desc: "购买-" + p.Name, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: w.RealAmount(), + CurrencyType: txnmod.CurrencyTypeCash, + } + if err = txnmod.InsertTransactionLog(t, &txnLog); err != nil { + return err + } + return nil +} + +func CountdownTiroCard(uid uint64) (countdownSec int64, err error) { + u, err := usermod.FindUserByUID(uid) + if err != nil { + return + } + if u == nil { + err = errors.New("invalid user") + return + } + var newUserHour int64 = 24 + //查询数据库里面的字段值直接赋予给newUserHour + //data, err := vipconfigmod.FindOne() + //if err != nil { + // return + //} + //if data.TimeConfig > 0 { + // newUserHour = data.TimeConfig + //} + /*data, _ := productmod.FindByProductType(commod.NEWUSERCard) + if len(data) > 0 { + newUserHour = data[0].ShowCountdownTime + }*/ + now := time.Now() + countdownSec = u.CreatedAt.Add(time.Duration(newUserHour)*time.Hour).Unix() - now.Unix() + if countdownSec < 0 { + countdownSec = 0 + } + return +} + +func BuyAudioBook(uid uint64, lid primitive.ObjectID, chapterID string, ua ua.UA, ip string) stderr.Code { + l, err := audiobookmod.GetByID(lid) + if err != nil { + return stderr.ErrNetWorkBusy + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + pab, err := payaudiobookmod.FindByUIDAndObjID(uid, lid) + if err != nil { + return stderr.ErrNetWorkBusy + } + desc := "购买有声小说:" + l.Title + "." + var price int64 = 0 + episodeNumberSet := make([]int, 0) + if chapterID == "" { + desc += "(全购:" + for i := range l.ContentSet { + if l.ContentSet[i].ListenPermission == audiobookmod.ListenPermissionBuy && (!sli.ContainsInt(pab.EpisodeNumberSet, l.ContentSet[i].EpisodeNumber)) { + price += l.ContentSet[i].Price + desc += l.ContentSet[i].Name + "." + episodeNumberSet = append(episodeNumberSet, l.ContentSet[i].EpisodeNumber) + } + } + desc += ")." + } else { + episodeNumber, err := strconv.Atoi(chapterID) + if err != nil { + return stderr.ErrParamError + } + for i := range l.ContentSet { + if l.ContentSet[i].EpisodeNumber == episodeNumber { + if l.ContentSet[i].ListenPermission == audiobookmod.ListenPermissionBuy && !sli.ContainsInt(pab.EpisodeNumberSet, episodeNumber) { + price += l.ContentSet[i].Price + desc += "(单购:" + l.ContentSet[i].Name + ".)" + episodeNumberSet = append(episodeNumberSet, l.ContentSet[i].EpisodeNumber) + } + break + } + + } + } + if price == 0 { + return stderr.RepeatPurchase + } + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return stderr.ErrNetWorkBusy + } + plan := debitPlan(w, price) + if plan == nil { + return stderr.InsufficientBalance + } + pid := l.ID.Hex() + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + wallet, err := walletmod.Debit(t, plan, uid) + if err != nil { //扣钱 + return err + } + txnLog := txnmod.TransactionLog{ + UID: uid, + ProductID: &pid, + Amount: -price, + ActualAmount: float64(-price), + TranType: txnmod.AudioBook.Key(), + TranTypeInt: int64(txnmod.AudioBook), + Desc: desc, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + if err = txnmod.InsertTransactionLog(t, &txnLog); err != nil { + return err + } + return payaudiobookmod.Insert(payaudiobookmod.PayAudioBookHistory{ + UID: uid, + EpisodeNumberSet: episodeNumberSet, + ObjID: lid, + }, t) + }); err != nil { + log.Warn(fmt.Sprintf("productser BuyVIP Transaction err %s", err.Error())) + return stderr.BuyFailed + } + return stderr.Success +} + +func checkVideoDisciountRenew(videoDiscLog videodiscountmod.VideoDiscountLog, p productmod.Product) time.Time { + duration := time.Hour * 24 * time.Duration(p.Duration) // Duration以天为单位 + now := time.Now() + if videoDiscLog.Expiration.After(now) { + return videoDiscLog.Expiration.Add(duration) + } + return now.Add(duration) +} + +func checkVideoFreeCardRenew(user usermod.User, product productmod.Product, t time.Time) time.Time { + duration := time.Hour * 24 * time.Duration(product.Duration) + if user.VideoFreeExpiration != nil && user.VideoFreeExpiration.After(t) { + return user.VideoFreeExpiration.Add(duration) + } + return t.Add(duration) +} + +func BuyVideoFreeCard(uid uint64, pid primitive.ObjectID, sys string, ua ua.UA, ip string) stderr.Code { + p, err := productmod.FindProduct(pid, sys) + if err != nil || p == nil { + return stderr.ErrParamError + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return stderr.ErrNetWorkBusy + } + + var orderId = primitive.NewObjectID() + var orderCreatedAt = time.Now() + + expiration := checkVideoFreeCardRenew(*u, *p, time.Now()) + if err != nil { + return stderr.ErrParamError + } + plan := debitPlan(w, p.DiscountedPrice) + if plan == nil { + return stderr.InsufficientBalance + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + discDoc := u.DiscDoc + wallet, err := walletmod.Debit(t, plan, uid) + if err != nil { //扣钱 + return err + } + + history := prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: orderId, + UID: uid, + ProductID: pid, + Name: p.Name, + Amount: plan.Amount, + Income: plan.Income, + ProductType: prdcthsomod.VideoFreeCard, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + ProductSnapShot: p, + CreatedAt: orderCreatedAt, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + txnLog := txnmod.TransactionLog{ + UID: uid, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + TranType: txnmod.VideoFreeCard.Key(), + TranTypeInt: int64(txnmod.VideoFreeCard), + Desc: "购买-" + p.Name, + DiscDoc: discDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + TransNo: history.ID, + } + if err = txnmod.InsertTransactionLog(t, &txnLog); err != nil { + return err + } + + userSet := usermod.UserSelector{ + VideoFreeExpiration: &expiration, + } + _, _ = usermod.UpdateTrans(t, uid, userSet) + return nil + }); err != nil { + log.Error("BuyVideoFreeCard", log.Any("uid", uid), log.Any("productID", pid), log.Any("sys", sys)) + return stderr.BuyFailed + } + + return stderr.Success +} + +func BuyVideoDiscountCard(uid uint64, pid primitive.ObjectID, sys string, ua ua.UA, ip string) stderr.Code { + p, err := productmod.FindProduct(pid, sys) + if err != nil || p == nil { + return stderr.ErrParamError + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return stderr.ErrNetWorkBusy + } + + var orderId = primitive.NewObjectID() + var orderCreatedAt = time.Now() + + videoDiscountLog, err := videodiscountmod.GetByUID(uid) + if err != nil { + return stderr.ErrNetWorkBusy + } + expiration := checkVideoDisciountRenew(videoDiscountLog, *p) + plan := debitPlan(w, p.DiscountedPrice) + if plan == nil { + return stderr.InsufficientBalance + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + wallet, err := walletmod.Debit(t, plan, uid) // 尝试直接从钱包扣钱 + if err != nil { // 钱包余额不足 + return err + } + + history := prdcthsomod.ProductHistory{ //插入商品购买记录 + ID: orderId, + UID: uid, + ProductID: pid, + Name: p.Name, + Amount: plan.Amount, + Income: plan.Income, + ProductType: prdcthsomod.VideoDiscount, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + ProductSnapShot: p, + CreatedAt: orderCreatedAt, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + txnLog := txnmod.TransactionLog{ + UID: uid, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + TranType: txnmod.VideoDiscount.Key(), + TranTypeInt: int64(txnmod.VideoDiscount), + TransNo: history.ID, + Desc: "购买-" + p.Name, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + if err = txnmod.InsertTransactionLog(t, &txnLog); err != nil { + return err + } + set := videodiscountmod.EditSelector{ + UID: &uid, + Expiration: &expiration, + VideoDiscount: &p.VideoDiscount, + } + + return videodiscountmod.Upsert(t, &set) + }); err != nil { + log.Error("BuyVideoDiscountCard", log.Any("uid", uid), log.Any("productID", pid), log.Any("sys", sys)) + return stderr.BuyFailed + } + + return stderr.Success +} + +func BuyVideoDiscountCard_d(t *db.MongoTool, uid uint64, p productmod.Product) error { + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return stderr.ErrNetWorkBusy + } + videoDiscountLog, err := videodiscountmod.GetByUID(uid) + if err != nil { + return stderr.ErrNetWorkBusy + } + expiration := checkVideoDisciountRenew(videoDiscountLog, p) + discountSet := videodiscountmod.EditSelector{ + UID: &uid, + Expiration: &expiration, + VideoDiscount: &p.VideoDiscount, + } + if err = videodiscountmod.Upsert(t, &discountSet); err != nil { + return err + } + history := prdcthsomod.ProductHistory{ + ID: primitive.NewObjectID(), + UID: uid, + ProductID: p.ID, + Name: p.Name, + Amount: p.DiscountedPrice, + ProductType: productmod.VideoDiscount, + SysType: u.SysType, + DiscDoc: u.DiscDoc, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + txnLog := txnmod.TransactionLog{ + UID: uid, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + TranType: txnmod.VideoDiscount.Key(), + TranTypeInt: int64(txnmod.VideoDiscount), + Desc: "购买-" + p.Name, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: w.RealAmount(), + CurrencyType: txnmod.CurrencyTypeCash, + TransNo: history.ID, + } + return txnmod.InsertTransactionLog(t, &txnLog) +} + +// BuyVideoFreeCard_d 购买视频免费卡,兼容旧版客户端请求 +func BuyVideoFreeCard_d(t *db.MongoTool, uid uint64, p productmod.Product) error { + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + now := time.Now() + expiration := checkVideoFreeCardRenew(*u, p, now) + userSet := usermod.UserSelector{ + VideoFreeExpiration: &expiration, + } + if _, err = usermod.UpdateTrans(t, uid, userSet); err != nil { + return err + } + history := prdcthsomod.ProductHistory{ + ID: primitive.NewObjectID(), + UID: uid, + ProductID: p.ID, + Name: p.Name, + Amount: p.DiscountedPrice, + ProductType: prdcthsomod.VideoFreeCard, + SysType: u.SysType, + DiscDoc: u.DiscDoc, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + wallet, err := walletmod.GetWallet(uid) + if err != nil { + return err + } + txnLog := txnmod.TransactionLog{ + UID: uid, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + TranType: txnmod.VideoFreeCard.Key(), + TranTypeInt: int64(txnmod.VideoFreeCard), + Desc: "购买-" + p.Name, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + CurrencyType: txnmod.CurrencyTypeCash, + TransNo: history.ID, + } + return txnmod.InsertTransactionLog(t, &txnLog) +} + +func DelBroughtHistory(videoID primitive.ObjectID, uid uint64) error { + return payvidlgmod.DelVideoPayRecord(videoID, uid) +} + +func GetCouponDetail(uid uint64, productID string, productType commod.ProductType, sys string) (ActivityCouponDetailResponse, stderr.Code) { + var ( + couponType int + goodsType int + isAmountPay bool + ) + data := ActivityCouponDetailResponse{} + id, err := primitive.ObjectIDFromHex(productID) + if err != nil { + return data, stderr.ErrParamError + } + switch productType { + case commod.VIP, commod.NEWUSERCard: + // 获取商品信息 + productDetail, err := productmod.FindProduct(id, sys) + if err != nil { + return data, stderr.ErrDbQueryError + } + if productDetail.IsAmountPay { + isAmountPay = true + } + data.OriginalPrice = productDetail.OriginalPrice + data.DiscountedPrice = productDetail.DiscountedPrice + couponType = int(prizemod.VIPDiscount) + goodsType = int(backpackmod.VIPDiscount) + default: + return data, stderr.ErrParamError + } + // 获取所有优惠卷 + couponList, err := prizemod.GetCouponList(couponType) + if err != nil { + return data, stderr.ErrDbQueryError + } + // 获取个人拥有的优惠卷 + goodsList, err := backpackmod.GetCouponListByUID(uid, goodsType) + if err != nil { + return data, stderr.ErrDbQueryError + } + // 会员卡(金币购买) + if isAmountPay { + for _, v := range couponList { + couponDetail := ActivityCoupon{} + if productType == commod.VIP { + couponDetail.DiscountedPrice = decimal.NewFromInt(v.Value).Mul(decimal.NewFromInt(data.DiscountedPrice)).Div(decimal.NewFromInt(10)).IntPart() + } else { + couponDetail.DiscountedPrice = decimal.NewFromInt(v.Value).Mul(decimal.NewFromInt(data.OriginalPrice)).Div(decimal.NewFromInt(10)).IntPart() + } + couponDetail.Name = v.Name + for _, gv := range goodsList { + if v.Value == gv.GoodsValue { + if couponDetail.ID.IsZero() { + couponDetail.ID = gv.ID + } + couponDetail.Count++ + } + } + data.CouponList = append(data.CouponList, couponDetail) + } + } + if data.DiscountedPrice < data.OriginalPrice { + data.IsDiscounted = true + } + return data, stderr.Success +} + +// 视频扣量校验 +func checkVideoDeduction(videoDeduction float64, videoDeductionCount, videoDeductionPayCount int, uid uint64, goldVideoCouponNum int) bool { + //使用金币视频抵用券, 则直接返回扣量, 且不计入博主视频的购买数量和扣量次数 + if goldVideoCouponNum > 0 { + log.Info("checkVideoDeduction, goldVideoCouponNum---100%扣量", log.Any("uid", uid)) + return true + } + //根据用户扣量比率videoDeduction 来进行计算. + if videoDeduction == 0 { // 若扣量比例为0 则直接返回不扣量, 且不计入博主视频的购买数量和扣量次数 + log.Info("checkVideoDeduction---不扣量", log.Any("uid", uid)) + return false + } + // 计算是否扣量: + deduct := func() bool { + if videoDeduction >= 10 || videoDeductionCount == 0 { + return true // 若扣量比例为100% 或视频没有扣量次数, 则扣量 + } + // 按比例计算扣量 + deduction := (float64(videoDeductionCount) / (float64(videoDeductionPayCount + 1))) * 10 + if deduction < videoDeduction { // 若当前扣量比例低于配置的扣量比例, 则扣量 + log.Info("checkVideoDeduction---扣量", log.Any("videoDeduction", videoDeduction), log.Any("deduction", deduction), log.Any("uid", uid)) + return true + } + log.Info("checkVideoDeduction---不扣量", log.Any("videoDeduction", videoDeduction), log.Any("deduction", deduction), log.Any("uid", uid)) + return false + }() + //更新博主视频的购买数量和扣量次数 + _ = usermod.IncVideoDeduction(uid, deduct) + return deduct +} + +func GetCoin(uid uint64) (data interface{}, code stderr.Code) { + //获取用户信息 + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return nil, stderr.ErrNetWorkBusy + } + //获取金币月卡产品信息---根据产品类型获取 + p, err := productmod.FindByProductType(commod.CoinMonthCard) + if err != nil || p == nil { + return nil, stderr.ErrParamError + } + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return nil, stderr.ErrNetWorkBusy + } + //检查vip过期时间_判断用户vip是否过期 + if !u.CoinMouthExpireDate.After(time.Now()) { + return nil, stderr.ExpiredVip + } + txnLogs := make([]txnmod.TransactionLog, 0) + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + //金币月卡赠送金币 + if len(p) > 0 && p[0].EveryDayGiveCoin > 0 { + if _, err = walletmod.CreditAmount(t, p[0].EveryDayGiveCoin, uid); err != nil { + return err + } + //插入购买金币月卡怎送金币流水 + giveLog := txnmod.TransactionLog{UID: uid, + Amount: p[0].EveryDayGiveCoin, + ActualAmount: float64(p[0].EveryDayGiveCoin), + TranType: txnmod.VipCardGive.Key(), + TranTypeInt: int64(txnmod.VipCardGive), + TransNo: p[0].ID, + Desc: "金币月卡每日赠送金币", + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: w.RealAmount(), + } + txnLogs = append(txnLogs, giveLog) + if err = txnmod.InsertTransactionLog(t, &giveLog); err != nil { + return err + } + } + return nil + + }); err != nil { + log.Warn(fmt.Sprintf("productser BuyModel Transaction err %s", err.Error())) + return nil, stderr.BuyFailed + } + return "success", stderr.Success +} + +func GetAwVipInfo(uid uint64) (data *productmod.RespVipInfoList, code stderr.Code) { + var res productmod.RespVipInfoList + //获取用户信息 + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return nil, stderr.ErrNetWorkBusy + } + + str, err := appg.Redis.Get(redisconst.GetAwVipInfo) + if err != nil { + log.Warn(fmt.Sprintf("用户ID:%d;缓存获取会员卡列表信息异常:%v", uid, err)) + } + + if str != nil { + if err = json.Unmarshal([]byte(*str), &res); err == nil { + return &res, stderr.Success + } + log.Warn(fmt.Sprintf("用户ID:%d;解析缓存数据异常:%v", uid, err)) + } + productList, err := productmod.FindByVipLevel(productmod.LevelThree) + if err != nil || productList == nil { + return nil, stderr.ErrParamError + } + + if len(productList) <= 0 { + return &res, stderr.Success + } + + if len(productList) > 0 { + for _, p := range productList { + res.List = append(res.List, productmod.ProductList{ + ID: p.ID.Hex(), + Name: p.Name, + }) + } + } + + common.Go(func() { + if res.List != nil || len(res.List) > 0 { + d, err := json.Marshal(res) + if err != nil { + return + } + if err = appg.Redis.Set(redisconst.GetAwVipInfo, d, 5*time.Minute); err != nil { + log.Warn(fmt.Sprintf("用户ID:%d;保存缓存数据异常:%v", uid, err)) + } + } + }) + return &res, stderr.Success +} + +//// GetRecommendVip 获取推荐展示的vip卡 +//func GetRecommendVip() (data *productmod.Product, err error) { +// _, err = cachev2.Classes().CacheTime(redisconst.RecommendVipExpire).Key(redisconst.RecommendVip).ResBind(&data).Cache(productmod.GetRecommendVip) +// if err != nil { +// log.Error("cachev2 productmod.FindOne fail", log.E(err)) +// return +// } +// return +//} diff --git a/app/service/productser/tool.go b/app/service/productser/tool.go new file mode 100644 index 0000000..d081bce --- /dev/null +++ b/app/service/productser/tool.go @@ -0,0 +1,212 @@ +package productser + +import ( + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/videocoupon" + "time" +) + +// formatMoney 生成支付通道支持的金额 +func formatMoney(price int64) int64 { + if price <= 0 { + return 0 + } + + if price <= 300 { + return 300 + } + + if price <= 500 { + return 500 + } + + for i := 1; i < 10; i++ { + if price <= int64(i*1000) { + return int64(i * 1000) + } + } + + return price +} + +func CheckUserUpgradeProducts(uid uint64, vips []productmod.Product) { + // 获取当前用户VIP信息 + user, uErr := usermod.FindUserByUID(uid) + if uErr != nil { + log.Error("CheckUserUpgrade FindUserByUID Error", log.Any("uid", uid)) + return + } + if !user.IsVIP(time.Now()) { + return + } + // 替换产品升级价格 + lastPID, lastVipName, lastAmount := prdcthsomod.GetUserLastVip(uid) + if lastPID.IsZero() || lastAmount < 500 { + return + } + + for i, p := range vips { + vips[i].PurchasePrice = p.DiscountedPrice + if !p.CheckUpgrade(lastPID) { + continue + } + + switch p.ProductType { + case commod.VIP, commod.NEWUSERCard: + if lastAmount >= p.DiscountedPrice { + continue + } + + case commod.AdvanceCard: + if lastAmount >= p.AdvanceAmount { + continue + } + + realPriceAdv := formatMoney(p.AdvanceAmount - lastAmount) + vips[i].AdvanceAmount = realPriceAdv + default: + continue + } + + vips[i].IsUpgrade = true + realPrice := formatMoney(p.DiscountedPrice - lastAmount) + vips[i].DiscountedPrice = realPrice + vips[i].DiscountedPriceAnd = &realPrice + vips[i].DiscountedPriceIos = &realPrice + vips[i].CurrentVipName = lastVipName + vips[i].CurrentVipPrice = lastAmount + } +} + +// CheckUserUpgrade 检查用户当前VIP是否为升级 +func CheckUserUpgrade(uid uint64, product *productmod.Product) { + // 获取当前用户VIP信息 + user, uErr := usermod.FindUserByUID(uid) + if uErr != nil { + log.Error("CheckUserUpgrade FindUserByUID Error", log.Any("uid", uid)) + return + } + if !user.IsVIP(time.Now()) { + return + } + // 替换产品升级价格 + lastPID, lastVipName, lastAmount := prdcthsomod.GetUserLastVip(uid) + if lastPID.IsZero() || lastAmount < 500 { + return + } + + product.PurchasePrice = product.DiscountedPrice + if !product.CheckUpgrade(lastPID) { + return + } + + switch product.ProductType { + case commod.VIP, commod.NEWUSERCard: + if lastAmount >= product.DiscountedPrice { + return + } + + case commod.AdvanceCard: + if lastAmount >= product.AdvanceAmount { + return + } + + realPriceAdv := formatMoney(product.AdvanceAmount - lastAmount) + product.AdvanceAmount = realPriceAdv + default: + return + } + + realPrice := formatMoney(product.DiscountedPrice - lastAmount) + product.IsUpgrade = true + product.DiscountedPrice = realPrice + product.DiscountedPriceAnd = &realPrice + product.DiscountedPriceIos = &realPrice + product.CurrentVipName = lastVipName + product.CurrentVipPrice = lastAmount +} + +// 金币视频抵用券业务处理 +func GoldVideoCoupleHandler(p *productmod.Product, u *usermod.User, sel usermod.UserSelector) usermod.UserSelector { + goldVideoCoupon := make([]usermod.UserGoldVideoCoupon, 0) + //存在 + coupon_bool := true + //处理相同面值优惠券, 券数量相加 + if u.GoldVideoCoupon != nil && len(u.GoldVideoCoupon) > 0 { + goldVideoCoupon = u.GoldVideoCoupon + for index, gvc := range goldVideoCoupon { + if gvc.Gold == p.GoldVideoCouponNum { + goldVideoCoupon[index].Count += p.GoldVideoCouponCount + coupon_bool = false + break + } + } + } + //用户不存在当前面值的优惠券, 则新增 + if coupon_bool { + goldVideoCoupon = append(goldVideoCoupon, usermod.UserGoldVideoCoupon{ + Gold: p.GoldVideoCouponNum, + Count: p.GoldVideoCouponCount, + }) + } + if len(goldVideoCoupon) > 0 { + sel.GoldVideoCoupon = &goldVideoCoupon + } + return sel +} + +// HandleGoldVideoCoupon 处理观影券。source表示观影券来源,如购买VIP。 +func HandleGoldVideoCoupon(p *productmod.Product, uid uint64, source videocoupon.GoldVideoCouponSource) []videocoupon.UserGoldVideoCoupon { + if p.GoldVideoCouponNum <= 0 { + return nil + } + // 该产品设置了附赠观影券 + coupons := make([]videocoupon.UserGoldVideoCoupon, p.GoldVideoCouponCount) + for i := 0; i < p.GoldVideoCouponCount; i++ { + coupons[i] = videocoupon.UserGoldVideoCoupon{ + UID: uid, + Num: p.GoldVideoCouponNum, + Used: false, + Source: source, + } + } + return coupons +} + +// CheckGoldVideoCoupon 校验用户选择的观影券是否符合要求 +// userChoose:用户选择的观影券面值(金币数)。 +// videoCoupons:H5改版重构后的观影券,传入的 UserGoldVideoCoupon 均保证面值与用户的选择相等。 +// 返回bool值: true 表示用户未选择观影券; false 表示用户未选择观影券 +func CheckGoldVideoCoupon(userChoose, videoCoins int64, u *usermod.User, videoCoupons []videocoupon.UserGoldVideoCoupon) (bool, stderr.Code) { + if userChoose <= 0 { // 用户未选择观影券 + return false, stderr.Success + } + if userChoose < videoCoins { + // 用户选择的观影券面值不够 + return false, stderr.GoldVideoCoupleAmountErr + } + // u.GoldVideoCoupon 老版观影券数据结构, 需做兼容并判断观影券金额是否足够 + // videoCoupons 新版(H5改版重构后)观影券数据结构, 无需进行金额判定 + if len(u.GoldVideoCoupon) <= 0 && len(videoCoupons) <= 0 { + // 新老版数据结构皆显示用户无观影券可用 + return false, stderr.GoldVideoCoupleNotExist + } + // 先在老版观影券结构中寻找是否有合适观影券 + for _, coupon := range u.GoldVideoCoupon { + if coupon.Count > 0 && int64(coupon.Gold) >= userChoose { + return true, stderr.Success + } + } + // 若老版观影券结构中未找到, 则从新版观影券结构中寻找 + if len(videoCoupons) > 0 { + // 找到新版观影券 + return true, stderr.Success + } + // 新老版数据结构中皆未找到用户观影券 + return false, stderr.GoldVideoCoupleNotExist +} diff --git a/app/service/productser/vip_experiment_attribution.go b/app/service/productser/vip_experiment_attribution.go new file mode 100644 index 0000000..cb49497 --- /dev/null +++ b/app/service/productser/vip_experiment_attribution.go @@ -0,0 +1,85 @@ +package productser + +import ( + "fmt" + "strings" + + "91porn-server/models/v/vipcardexperimentmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const maxVIPExperimentAttributionLength = 128 + +// VIPExperimentAttribution records the A/B assignment that led to a coin purchase. +type VIPExperimentAttribution struct { + ExperimentID string + ExperimentVariant string + SessionID string +} + +func validateVIPExperimentAttribution( + uid uint64, + productID primitive.ObjectID, + input VIPExperimentAttribution, +) (VIPExperimentAttribution, error) { + attribution, err := normalizeVIPExperimentAttribution(input) + if err != nil || attribution.ExperimentID == "" { + return attribution, err + } + experiment, err := vipcardexperimentmod.FindByExperimentID(attribution.ExperimentID) + if err != nil { + return VIPExperimentAttribution{}, err + } + if experiment == nil { + return VIPExperimentAttribution{}, fmt.Errorf("experiment does not exist") + } + if err = validateVIPExperimentAttributionForExperiment(uid, productID, attribution, experiment); err != nil { + return VIPExperimentAttribution{}, err + } + return attribution, nil +} + +func normalizeVIPExperimentAttribution(input VIPExperimentAttribution) (VIPExperimentAttribution, error) { + input.ExperimentID = strings.TrimSpace(input.ExperimentID) + input.ExperimentVariant = strings.ToUpper(strings.TrimSpace(input.ExperimentVariant)) + input.SessionID = strings.TrimSpace(input.SessionID) + if input.ExperimentID == "" { + return VIPExperimentAttribution{}, nil + } + values := map[string]string{ + "experimentId": input.ExperimentID, + "experimentVariant": input.ExperimentVariant, + "sessionId": input.SessionID, + } + for name, value := range values { + if len(value) > maxVIPExperimentAttributionLength { + return VIPExperimentAttribution{}, fmt.Errorf("%s must not exceed %d characters", name, maxVIPExperimentAttributionLength) + } + } + if input.ExperimentVariant == "" || input.SessionID == "" { + return VIPExperimentAttribution{}, fmt.Errorf("experimentVariant and sessionId are required with experimentId") + } + return input, nil +} + +func validateVIPExperimentAttributionForExperiment( + uid uint64, + productID primitive.ObjectID, + attribution VIPExperimentAttribution, + experiment *vipcardexperimentmod.Experiment, +) error { + config, ok := experiment.ConfigFor(attribution.ExperimentVariant) + if !ok { + return fmt.Errorf("experimentVariant must be A or B") + } + if assigned := experiment.Assign(uid); assigned != attribution.ExperimentVariant { + return fmt.Errorf("experimentVariant does not match user assignment") + } + for _, configuredProductID := range config.ProductIDs { + if configuredProductID == productID { + return nil + } + } + return fmt.Errorf("productID does not belong to experiment variant") +} diff --git a/app/service/productser/vip_experiment_attribution_test.go b/app/service/productser/vip_experiment_attribution_test.go new file mode 100644 index 0000000..4b71792 --- /dev/null +++ b/app/service/productser/vip_experiment_attribution_test.go @@ -0,0 +1,78 @@ +package productser + +import ( + "strings" + "testing" + + "91porn-server/models/v/vipcardexperimentmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestNormalizeVIPExperimentAttribution(t *testing.T) { + attribution, err := normalizeVIPExperimentAttribution(VIPExperimentAttribution{ + ExperimentID: " experiment ", + ExperimentVariant: " a ", + SessionID: " session ", + }) + if err != nil { + t.Fatalf("normalizeVIPExperimentAttribution() error = %v", err) + } + if attribution.ExperimentID != "experiment" || + attribution.ExperimentVariant != vipcardexperimentmod.VariantA || + attribution.SessionID != "session" { + t.Fatalf("normalized attribution = %#v", attribution) + } + + empty, err := normalizeVIPExperimentAttribution(VIPExperimentAttribution{ + ExperimentVariant: vipcardexperimentmod.VariantA, + SessionID: "session", + }) + if err != nil || empty != (VIPExperimentAttribution{}) { + t.Fatalf("empty experiment attribution = %#v, error = %v", empty, err) + } +} + +func TestNormalizeVIPExperimentAttributionRequiresCompleteFields(t *testing.T) { + _, err := normalizeVIPExperimentAttribution(VIPExperimentAttribution{ExperimentID: "experiment"}) + if err == nil { + t.Fatal("expected incomplete attribution to fail") + } + _, err = normalizeVIPExperimentAttribution(VIPExperimentAttribution{ + ExperimentID: strings.Repeat("a", maxVIPExperimentAttributionLength+1), + ExperimentVariant: vipcardexperimentmod.VariantA, + SessionID: "session", + }) + if err == nil { + t.Fatal("expected oversized experimentId to fail") + } +} + +func TestValidateVIPExperimentAttributionForExperiment(t *testing.T) { + productID := primitive.NewObjectID() + experiment := &vipcardexperimentmod.Experiment{ + ExperimentID: "experiment", + TrafficA: 100, + VariantA: vipcardexperimentmod.VariantConfig{ + ProductIDs: []primitive.ObjectID{productID}, + }, + VariantB: vipcardexperimentmod.VariantConfig{ + ProductIDs: []primitive.ObjectID{primitive.NewObjectID()}, + }, + } + attribution := VIPExperimentAttribution{ + ExperimentID: experiment.ExperimentID, + ExperimentVariant: vipcardexperimentmod.VariantA, + SessionID: "session", + } + if err := validateVIPExperimentAttributionForExperiment(123, productID, attribution, experiment); err != nil { + t.Fatalf("valid attribution rejected: %v", err) + } + if err := validateVIPExperimentAttributionForExperiment(123, primitive.NewObjectID(), attribution, experiment); err == nil { + t.Fatal("product outside variant must be rejected") + } + attribution.ExperimentVariant = vipcardexperimentmod.VariantB + if err := validateVIPExperimentAttributionForExperiment(123, productID, attribution, experiment); err == nil { + t.Fatal("variant outside stable assignment must be rejected") + } +} diff --git a/app/service/proxyser/error.go b/app/service/proxyser/error.go new file mode 100644 index 0000000..7a38f90 --- /dev/null +++ b/app/service/proxyser/error.go @@ -0,0 +1,31 @@ +package proxyser + +import "fmt" + +// IllegalInvUserError 非法推广用户 +type IllegalInvUserError struct { + UID uint64 + Parent uint64 +} + +func (i IllegalInvUserError) Error() string { + return fmt.Sprintf("illegal invite uid: %d, parent: %d", i.UID, i.Parent) +} + +// SelfInvError 自我推广错误 +type SelfInvError struct { + uid uint64 +} + +func (s SelfInvError) Error() string { + return fmt.Sprintf("self invitation error! uid: %d", s.uid) +} + +// PromStrInvalidError 推广码无效错误 +type PromStrInvalidError struct { + PromCode string +} + +func (p PromStrInvalidError) Error() string { + return fmt.Sprintf("invalid promCode str: %s", p.PromCode) +} diff --git a/app/service/proxyser/proxyser.go b/app/service/proxyser/proxyser.go new file mode 100644 index 0000000..5b179bb --- /dev/null +++ b/app/service/proxyser/proxyser.go @@ -0,0 +1,476 @@ +package proxyser + +import ( + "91porn-server/app/service/taskser" + "91porn-server/common/timeutil" + "91porn-server/models/v/dailytaskmod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/taskmod" + "errors" + "strconv" + "strings" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/proxymod" + "91porn-server/models/v/proxyrecordmod" + "91porn-server/models/v/sourcemod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var PerformanceConvRat = []int64{100, 25, 15, 10} + +// 推广 +func Invite(inv *proxyrecordmod.InvitationRecord) (err error) { + u, err := usermod.FindUserByUID(inv.UID) + if err != nil || u == nil { + return + } + p := sourcemod.GetRandomPromotionURL() + inv.PromotionCode = u.PromCode + inv.URL = common.BindUrl(p, constant.PromotionField+u.PromCode) + if err = proxyrecordmod.InsertInvitationRecord(inv); err != nil { + return + } + return +} + +// InviteLog 推广绑定记录 +func InviteLog(uid uint64, pageNumber int, pageSize int) (res []*proxymod.InvitationRes, hasNext bool, err error) { + data, hasNext, err := proxymod.GetInvitationByUID(uid, int64(pageNumber), int64(pageSize)) + if err != nil { + return + } + if len(data) <= 0 { + res = make([]*proxymod.InvitationRes, 0) + return + } + res = make([]*proxymod.InvitationRes, len(data)) + for idx, v := range data { + i := proxymod.InvitationRes{} + i.Invitation = *v + i.IsBoughtVIP, err = prdcthsomod.IsBoughtVIP(v.Invitee) + if err != nil { + return + } + res[idx] = &i + } + return +} + +type Invitation = proxymod.Invitation + +// InviteBind 推广绑定 +func InviteBind(uid uint64, parentdisc string, isBind bool) stderr.Code { + now := time.Now() + //查询推广用户 + user, err := usermod.FindUserByUID(uid) + if err != nil { + log.Warn("InviteBind FindUserByUID error", log.E(err), log.Any("uid", uid)) + return stderr.Failure + } + if user == nil { + return stderr.ErrParamError + } + // 通过推广str查询父级用户 + parent, err := findUserByPromotionStr(parentdisc) + if err != nil { + switch err.(type) { + case PromStrInvalidError: + return stderr.InvalidPromotionCode + default: + return stderr.ErrDbQueryError + } + } + // 判断推广是否合法 + if err = canInvite(user, parent); err != nil { + switch err.(type) { + case SelfInvError: + return stderr.SelfPromotionCode + case proxymod.LoopInviteError: + return stderr.PromoteHasBind + default: + return stderr.NotInvitedUsers + } + } + i := Invitation{ + UID: parent.UID, + Invitee: uid, + InviteePortrait: user.Portrait, + InviteCode: parentdisc, + CreatedAt: now, + InviteeName: user.Name, + IsRecharge: false, + } + if isBind { + i.InviteTime = now + } + //invitation表插入记录 + if err := proxymod.InsertOne(i); err != nil { + switch err.(type) { + case proxymod.LoopInviteError: + return stderr.PromoteHasBind + default: + return stderr.ErrDbInsertError + } + } + + common.Go(func() { + // 限制同ip注册不增加vip时间 + if user.RegisterIP != parent.RegisterIP { + // 同一用户, 一天最多增加3个人的分享vip天数 + startTime, endTime := timeutil.EarlyLastDay(time.Now()) + maxCount, _ := proxymod.GetInviteesCount([]uint64{parent.UID}, startTime, endTime) + if maxCount < 3 { + _ = addFreeTime(*parent, 1*24*60*60) + } + log.Info("InviteBind addFreeTime", log.Any("uid", uid), log.Any("parentUid", parent.UID), log.Any("maxCount", maxCount), log.Any("startTime", startTime), log.Any("endTime", endTime)) + } + }) + go func() { + _ = taskser.CompleteDailyTask(nil, parent.UID, dailytaskmod.DailyTaskTypeUserInvite) + maxCount, _ := proxymod.CountByUID(parent.UID) + taskser.CompleteGrowthTask(parent.UID, taskmod.GrowthBuyInviteUser, maxCount) + _ = taskser.ReSignCallBack(parent.UID) + }() + return stderr.Success +} + +// addFreeTime 增加Vip FreeTime +func addFreeTime(user usermod.User, freeSecond int64) error { + freeTime := time.Unix(time.Now().Unix()+freeSecond, 0) + if user.VipExpireDate.After(time.Now()) { + freeTime = time.Unix(user.VipExpireDate.Unix()+freeSecond, 0) + } + upd := usermod.UserSelector{VipExpireDate: &freeTime} + if user.VipLevel == 0 { + l := 1 + upd.VipLevel = &l + } + _, err := usermod.Update(user.UID, upd) + return err +} + +func canInvite(user, parent *usermod.User) error { + if user == nil || parent == nil { + return errors.New("user is nil!") + } + //不能自我推广 + if user.UID == parent.UID { + return SelfInvError{user.UID} + } + if user.CreatedAt.AddDate(0, 0, 1).Before(time.Now()) { + return errors.New("time is expire") + } + if user.CreatedAt.Before(parent.CreatedAt) { + return errors.New("invaild invite") + } + // 推广用户只能是离散节点,不允许闭环推广 + count, err := proxymod.CountByUID(user.UID) + if err != nil { + return err + } + if count != 0 { + return proxymod.LoopInviteError{Parent: parent.UID, UID: user.UID} + } + return nil +} + +func GetInv(uid uint64) (uint64, error) { + return proxymod.GetInvrLv1(uid) +} + +func findUserByPromotionStr(promoCode string) (*usermod.User, error) { + parent, err := usermod.FindUserPromotionCode(promoCode) + if err != nil { + return nil, err + } + if parent == nil { + log.Warn("userser InviteBind fail: invalid promotion code", log.Any("promoCode", promoCode)) + return nil, PromStrInvalidError{promoCode} + } + return parent, nil +} + +// GetInveUsers 获取下级代理 +func GetInveUsers(uid uint64) (invMap map[string][]*usermod.User, err error) { + invMap = make(map[string][]*usermod.User, 0) + var MaxLv = 4 + var flag = 0 + ids := []uint64{uid} + usersChan := make(chan map[string][]*usermod.User, MaxLv) + for i := 1; i <= MaxLv; i++ { + flag += 1 + uids, _, err := proxymod.GetInveNext(ids) + if err != nil || len(ids) <= 0 { + break + } + go func(ids []uint64, lv int, datachan chan map[string][]*usermod.User) { + var users []*usermod.User + m := make(map[string][]*usermod.User) + users, _ = usermod.FindUsersByUID(ids) + m["lv"+strconv.Itoa(lv)] = users + datachan <- m + }(uids, i, usersChan) + ids = uids + } + for i := 0; i < flag; i++ { + um, ok := <-usersChan + if !ok { + break + } + for k, v := range um { + invMap[k] = v + } + } + return +} + +func StatCenterIncPerformance(t *db.MongoTool, uid uint64, proxyUserId uint64, incomeAmount int64, orderId string) error { + u, err := usermod.FindUserByUID(uid) + if err != nil { + if err.Error() == "empty user" { + return nil + } + return errors.New("invalid user") + } + if u == nil { + return nil + } + orderIdV1 := strings.Replace(orderId, "CZ-DSP-", "", -1) + objId, err := primitive.ObjectIDFromHex(orderIdV1) + if err != nil { + log.Error("StatCenterIncPerformance ObjectIDFromHex err", log.E(err)) + return err + } + income := incomeAmount // 金币 + pIncome := income + creditPlan := walletmod.CreditPlan{ + Income: &income, + ProxyIncome: &pIncome, + } + wallet, err := walletmod.Credit(t, creditPlan, uid) + if err != nil { + return err + } + desc := "推广用户充值返现" + transacton := txnmod.TransactionLog{ + UID: uid, + TransNo: objId, + Amount: income, + ActualAmount: float64(income), + TranType: txnmod.ProxyIncome.Key(), + TranTypeInt: int64(txnmod.ProxyIncome), + Desc: desc, + RechargeId: proxyUserId, + RealAmount: walletmod.GetRealAmount(wallet), + } + return txnmod.InsertTransactionLog(t, &transacton) +} + +type WaLiPromoteSettleSyncDataReq struct { + Size int `json:"size"` + ID []primitive.ObjectID `json:"id" bson:"_id,omitempty"` + SetDate []time.Time `json:"setDate" bson:"setDate"` //结算日期 + Money []decimal.Decimal `json:"money" bson:"money"` //收益金额 元 + AppId []int `json:"appId" bson:"appId"` //appid + UID []uint64 `json:"uid" bson:"uid"` //用户id + NoticeStatus []int `json:"noticeStatus" bson:"noticeStatus"` //通知状态 +} + +type WaLiPromoteSettleSyncDataRes struct { + ID []primitive.ObjectID `json:"id" bson:"_id,omitempty"` +} + +func StatCenterIncPerformanceWaLi(req WaLiPromoteSettleSyncDataReq) (res WaLiPromoteSettleSyncDataRes) { + successIDSlice := make([]primitive.ObjectID, 0) + for i := 0; i < req.Size; i++ { + income := req.Money[i].Shift(1).IntPart() // 金币 + pIncome := float64(income) //金币 + cp := walletmod.CreditPlan{ + Income: &income, + WaLiProxyIncome: &pIncome, + } + flag := 1 + _ = appg.VideoDB.Trans(func(t *db.MongoTool) error { + wallet, err := walletmod.Credit(t, cp, req.UID[i]) + if err != nil { + flag = 2 + return err + } + transacton := txnmod.TransactionLog{ + UID: req.UID[i], + UniqueOrder: req.ID[i].Hex(), + Amount: income, + ActualAmount: float64(income), + TranType: txnmod.WaLiProxyIncome.Key(), + TranTypeInt: int64(txnmod.WaLiProxyIncome), + Desc: "游戏推广用户充值返现", + RealAmount: walletmod.GetRealAmount(wallet), + } + if err = txnmod.InsertTransactionLog(t, &transacton); err != nil { + if stderr.IsEqual(err, stderr.InsertExistError) { + return err + } + flag = 2 + return err + } + return nil + }) + if flag == 2 { + continue + } + successIDSlice = append(successIDSlice, req.ID[i]) + } + res.ID = successIDSlice + return +} + +// GetInveUserList 获取下级代理列表 +func GetInveUserList(uid, pageNumber, pageSize uint64) (list []*proxymod.Invitation, total int64, err error) { + //查询用户推广列表 + total, list, err = proxymod.FindManyByApp(uid, pageNumber, pageSize) + if err != nil { + log.Error("HandelProxyRechargeCommission proxymod.FindManyByApp err", log.E(err), log.Any("uid", uid)) + return list, 0, nil + } + if len(list) == 0 { + log.Warn("HandelProxyRechargeCommission len(list) == 0", log.Any("uid", uid)) + return list, 0, nil + } + return list, total, nil +} + +// HandelProxyRechargeCommission 处理代理充值分成 +func HandelProxyRechargeCommission(t *db.MongoTool, orderId string, payMoney int64) { + //return + // 充值订单用户id + var uid uint64 = 0 + // 截取订单号 + orderIdV1 := strings.Replace(orderId, "CZ-91PO-", "", -1) + // 验证充值订单 + order, err := rchgordmod.FindRechargeOrderByID(orderIdV1) + if err != nil { + log.Error("HandelProxyRechargeCommission rchgordmod.FindRechargeOrderByID err", log.E(err), log.Any("orderId", orderId)) + return + } + uid = order.UID + if order.Status != rchgordmod.SUCCESS { + log.Warn("HandelProxyRechargeCommission order.Status != rchgordmod.SUCCESS", log.Any("uid", uid), log.Any("orderId", orderId)) + return + } + // 棋牌订单不参与分成 + if order.ProductType == 1 { + log.Info("HandelProxyRechargeCommission order.ProductType == 1", log.Any("uid", uid), log.Any("orderId", orderId)) + return + } + // 金币订单不参与分成 + if order.Category == 0 { + log.Info("HandelProxyRechargeCommission order.Category == 0", log.Any("uid", uid), log.Any("orderId", orderId)) + return + } + // 查询用户上级id + proxyUserId, err := proxymod.GetInvrLv1(uid) + if err != nil { + log.Error("HandelProxyRechargeCommission proxymod.GetInvrLv1 err", log.E(err), log.Any("uid", uid), log.Any("proxyUserId", proxyUserId), log.Any("orderId", orderId)) + return + } + if proxyUserId == 0 { + log.Warn("HandelProxyRechargeCommission proxyUserId == 0", log.E(err), log.Any("uid", uid), log.Any("proxyUserId", proxyUserId), log.Any("orderId", orderId)) + return + } + // 查询当前订单是否已经分成 + if txnmod.IsProxyExist(proxyUserId, order.ID) { + log.Warn("HandelProxyRechargeCommission txnmod.IsProxyExist == true", log.Any("uid", uid), log.Any("proxyUserId", proxyUserId), log.Any("orderId", orderId)) + return + } + // 查询用户信息 + u, err := usermod.FindUserByUID(proxyUserId) + if err != nil { + log.Error("HandelProxyRechargeCommission usermod.FindUserByUID err", log.E(err), log.Any("uid", uid), log.Any("proxyUserId", proxyUserId), log.Any("orderId", orderId)) + return + } + if u == nil { + log.Warn("HandelProxyRechargeCommission u == nil", log.Any("uid", uid), log.Any("proxyUserId", proxyUserId), log.Any("orderId", orderId)) + return + } + // 查询代理用户钱包 + proxyUserWallet, err := walletmod.GetWallet(proxyUserId) + if err != nil { + log.Error("HandelProxyRechargeCommission walletmod.GetWallet err", log.E(err), log.Any("uid", uid), log.Any("proxyUserId", proxyUserId), log.Any("orderId", orderId)) + return + } + if proxyUserWallet == nil { + log.Warn("HandelProxyRechargeCommission proxyUserWallet == nil", log.Any("uid", uid), log.Any("proxyUserId", proxyUserId), log.Any("orderId", orderId)) + return + } + //分成收益计算,单位:金币个数 + var incomeAmount int64 = 0 + //代理推广用户累计充值 + var proxyConsumption int64 = (proxyUserWallet.ProxyConsumption + payMoney) / 100 + var money int64 = payMoney / 100 + if proxyConsumption < 2000 { + //充值2000元以内, 返利50% + incomeAmount = money * 50 / 10 + } else if proxyConsumption > 2000 && proxyConsumption < 10000 { + //充值2000—10000元以内, 返利60% + incomeAmount = money * 60 / 10 + } else if proxyConsumption > 10000 && proxyConsumption < 30000 { + //充值10000—30000元以内, 返利65% + incomeAmount = money * 65 / 10 + } else if proxyConsumption > 30000 { + //充值大于30000元, 返利70% + incomeAmount = money * 70 / 10 + } + + // 事务处理, 增加流水日志、代理收益 + _ = appg.VideoDB.Trans(func(t *db.MongoTool) error { + //增加收益余额 + pIncome := incomeAmount + creditPlan := walletmod.CreditPlan{ + Income: &incomeAmount, + ProxyIncome: &pIncome, + ProxyConsumption: &payMoney, + } + wallet, err := walletmod.Credit(t, creditPlan, proxyUserId) + if err != nil { + log.Warn("HandelProxyRechargeCommission walletmod.Credit err", log.E(err), log.Any("uid", uid), log.Any("proxyUserId", proxyUserId), log.Any("orderId", orderId)) + return err + } + //新增流水日志 + transacton := txnmod.TransactionLog{ + UID: proxyUserId, + TransNo: order.ID, + Amount: incomeAmount, + ActualAmount: float64(incomeAmount), + TranType: txnmod.ProxyIncome.Key(), + TranTypeInt: int64(txnmod.ProxyIncome), + Desc: "推广用户充值返现", + RechargeId: uid, + RealAmount: walletmod.GetRealAmount(wallet), + } + err = txnmod.InsertTransactionLog(t, &transacton) + if err != nil { + log.Warn("HandelProxyRechargeCommission txnmod.InsertTransactionLog err", log.E(err), log.Any("uid", uid), log.Any("proxyUserId", proxyUserId), log.Any("orderId", orderId)) + return err + } + err = proxymod.UpdateByUid(t, uid, proxyUserId) + if err != nil { + log.Warn("HandelProxyRechargeCommission proxymod.UpdateByUid err", log.E(err), log.Any("uid", uid), log.Any("proxyUserId", proxyUserId), log.Any("orderId", orderId)) + return err + } + return nil + }) + return +} diff --git a/app/service/publishser/publish.go b/app/service/publishser/publish.go new file mode 100644 index 0000000..b8041a0 --- /dev/null +++ b/app/service/publishser/publish.go @@ -0,0 +1,144 @@ +package publishser + +import ( + "fmt" + "strconv" + "time" + + "91porn-server/app/appg" + "91porn-server/common/log" + "91porn-server/common/timeutil" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/videoactivitymod" + "91porn-server/models/v/vidmod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" +) + +func GetWorkTotal(uid uint64) (int64, error) { + return vidmod.GetCountByPublish(uid, nil) +} + +func GetPendingReviewWorkCount(uid uint64) (int64, error) { + status := 0 + return vidmod.GetCountByPublish(uid, &status) +} + +func GetWeekIncomeLeaderboard(limit int) (out []User, err error) { + var ( + weekEndDay = time.Now() + weekStartDay = timeutil.BeginningOfWeek(weekEndDay) + result []struct { + UID uint64 `json:"uid" bson:"_id"` + Income float64 `json:"income" bson:"income"` + } + ) + filter := bson.M{"tranTypeInt": txnmod.WorksIncome, "createdAt": bson.M{"$gte": weekStartDay, "$lt": weekEndDay}} + if err = txnmod.IncomeLeaderboard(&result, filter, limit); err != nil { + log.Error("获取周收益榜异常:", log.E(err)) + return + } + if len(result) == 0 { + return []User{}, nil + } + retMap := make(map[uint64]float64) + uids := make([]uint64, len(result)) + for i, s := range result { + uids[i] = s.UID + retMap[s.UID] = s.Income + } + users, err := usermod.FindUsersByUID(uids) + if err != nil { + log.Error("获取用户信息异常:", log.E(err)) + return + } + for _, s := range result { + for _, user := range users { + if s.UID == user.UID { + out = append(out, User{ID: user.UID, Name: user.Name, Avatar: user.Portrait, Value: decimal.NewFromFloat(retMap[user.UID]).Mul(decimal.NewFromInt(3))}) + break + } + } + } + return +} + +func GetActivityDetails() (out VideoActivity, err error) { + details, err := videoactivitymod.QueryDocumentByStatus(videoactivitymod.Enable) + if err != nil { + return + } + if details.ID.IsZero() { + return + } + out.ID = details.ID + out.EndTime = details.EndTime + out.BackgroundImage = details.BackgroundImage + out.Desc = details.Desc + return +} + +func GetCreatorNumber() (creatorNumber int64, err error) { + result, err := appg.Redis.Get("creatorNumber") + if err != nil { + log.Error(fmt.Sprintf("getCreatorNumber redis获取数据异常:%v", err)) + return + } + if result == nil { + if creatorNumber, err = vidmod.GetCreatorNumber(); err != nil { + log.Error(fmt.Sprintf("getCreatorNumber mongodb获取数据异常:%v", err)) + return + } + if err = appg.Redis.Set("creatorNumber", creatorNumber, 0); err != nil { + log.Error(fmt.Sprintf("getCreatorNumber redis操作异常:%v", err)) + return + } + return + } + if creatorNumber, err = strconv.ParseInt(*result, 10, 64); err != nil { + log.Error(fmt.Sprintf("getCreatorNumber 解析异常:%v", err)) + return + } + return +} + +func GetWeekWorkLeaderboard(limit int) (out []User, err error) { + var ( + weekEndDay = time.Now() + weekStartDay = timeutil.BeginningOfWeek(weekEndDay) + result []struct { + UID uint64 `json:"uid" bson:"_id"` + Count int64 `json:"count" bson:"count"` + } + ) + filter := bson.M{"createdAt": bson.M{"$gte": weekStartDay, "$lt": weekEndDay}} + if err = vidmod.WorkLeaderboard(&result, filter, limit); err != nil { + log.Error("获取周作品榜异常:", log.E(err)) + return + } + if len(result) == 0 { + return []User{}, nil + } + uids := make([]uint64, len(result)) + var retMap = make(map[uint64]int64) + for i, s := range result { + uids[i] = s.UID + retMap[s.UID] = s.Count + } + users, err := usermod.FindUsersByUID(uids) + if err != nil { + log.Error("获取用户信息异常:", log.E(err)) + return + } + for _, s := range result { + for _, user := range users { + if s.UID == user.UID { + out = append(out, User{ID: user.UID, Name: user.Name, Avatar: user.Portrait, Value: decimal.NewFromInt(retMap[user.UID])}) + break + } + } + } + return +} diff --git a/app/service/publishser/type.go b/app/service/publishser/type.go new file mode 100644 index 0000000..0bd5d3a --- /dev/null +++ b/app/service/publishser/type.go @@ -0,0 +1,79 @@ +package publishser + +import ( + "time" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ListType 榜单类型 +type ListType int + +const ( + WeekIncomeLeaderboard ListType = iota // 本周收益榜 + WeekWorkLeaderboard // 本周作品榜 +) + +type CreateVideoPageResp struct { + WorkTotal int64 `json:"workTotal"` // 作品总数 + PendingReviewWorkCount int64 `json:"pendingReviewWorkCount"` // 待审核作品数 + PassWorkCount int64 `json:"passWorkCount"` // 已通过作品数 + WorkCreateCount int64 `json:"workCreateCount"` // 作品创作者总数 + ActivityDetails struct { + ID primitive.ObjectID `json:"id"` // 活动ID + BackgroundImage string `json:"backgroundImage"` // 活动背景图 + EndTime time.Time `json:"endTime"` // 活动结束时间 + } `json:"activityDetails"` // 活动详情 + Leaderboards []struct { + Type ListType `json:"type"` // 榜单类型 + Members []struct { + Name string `json:"name"` // 发布者名称 + Avatar string `json:"avatar"` // 发布者头像 + Value int64 `json:"value"` // 作品收益/作品数量 + } `json:"members"` // 成员列表 + } `json:"leaderboards"` // 排行榜列表 +} + +// DetailsResponse 发布详情返回 +type DetailsResponse struct { + IsFirst bool `json:"isFirst"` // 是否为首次进入 + WorkTotal int64 `json:"workTotal"` // 作品总数 + PendingReviewWorkCount int64 `json:"pendingReviewWorkCount"` // 待审核作品数 + PassWorkCount int64 `json:"passWorkCount"` // 已通过作品数 + WorkCreateCount int64 `json:"workCreateCount"` // 作品创作者总数 + ActivityDetails VideoActivity `json:"activityDetails"` // 活动详情 + Leaderboards []Leaderboard `json:"leaderboards"` // 排行榜列表 +} + +// Leaderboard 排行榜信息 +type Leaderboard struct { + Type ListType `json:"type"` // 榜单类型 + Members []User `json:"members"` // 成员列表 +} + +// User 用户信息 +type User struct { + Name string `json:"name"` // 发布者名称 + Avatar string `json:"avatar"` // 发布者头像 + Value decimal.Decimal `json:"value"` // 作品收益/作品数量 + ID uint64 `json:"id"` // 发布者ID +} + +type VideoActivity struct { + ID primitive.ObjectID `json:"id"` // 活动ID + BackgroundImage string `json:"backgroundImage"` // 活动背景图 + EndTime time.Time `json:"endTime"` // 活动结束时间 + Desc string `json:"desc"` // 活动详情描述 +} + +// WeekLeaderboardReq 周榜请求 +type WeekLeaderboardReq struct { + Type int `form:"type"` // 榜单类型 +} + +// WeekLeaderboardResp 周榜返回 +type WeekLeaderboardResp struct { + Type ListType `json:"type"` // 榜单类型 + Members []User `json:"members"` // 成员列表 +} diff --git a/app/service/questionnreser/questionnreser.go b/app/service/questionnreser/questionnreser.go new file mode 100644 index 0000000..0bb2857 --- /dev/null +++ b/app/service/questionnreser/questionnreser.go @@ -0,0 +1,98 @@ +package questionnreser + +import ( + "time" + + "91porn-server/common/stderr" + "91porn-server/models/v/exchcodemod" + "91porn-server/models/v/idmod" + "91porn-server/models/v/questionnremod" + "91porn-server/models/v/usermod" + "91porn-server/web/service/exchcodeser" +) + +var QuestionnaireGiftGold = []int{50, 100, 200} +var BatchNum = "YS-dzwj" +var Remark = "调查问卷活动奖励兑换码" +var InvalidTime = time.Hour * 24 * 3 + +// 生成活动兑换码 reward 奖励天数 count生成兑换码数 +func makeExchangeCode(reward int, count int) ([]string, error) { + now := time.Now() + exchangeCodes := make([]exchcodemod.ExchCode, count) + codes := make([]string, count) + for i := 0; i < count; i++ { + var exchangeCode = exchcodeser.ProduceExchangeCode(0) + codes[i] = exchangeCode + exchangeCodes[i] = exchcodemod.ExchCode{ + BatchNum: BatchNum, + App: exchcodemod.AppTypeYSVideo, + Code: exchangeCode, + Authority: exchcodemod.AuthorityShortVideoVip, + Status: exchcodemod.StatusUnused, + Reward: reward, + Operator: exchcodemod.SystemOperator, + Remark: Remark, + EffectiveAt: now, + InvalidAt: now.Add(InvalidTime), + CreatedAt: now, + UpdatedAt: now, + } + } + return codes, exchcodemod.BulkWrite(exchangeCodes) +} + +func Submit(q questionnremod.Questionnaire) (code stderr.Code, res questionnremod.SubmitRes) { + code = stderr.Success + qid, err := idmod.GetQuestionnaireId() + if err != nil { + code = stderr.ErrNetWorkBusy + return + } + q.QuestionId = qid + u, err := usermod.FindUserByUID(q.UID) + if err != nil { + code = stderr.ErrNetWorkBusy + return + } + if u == nil { + code = stderr.UserIsNotExists + return + } + //不是vip||vip过期||vip小于试卷允许最小等级||vip越界 + if u.VipLevel < 1 || u.VipExpireDate.Before(time.Now()) || u.VipLevel < q.Level || u.VipLevel > 3 { + code = stderr.ErrAccessForbid + return + } + q.Gold = QuestionnaireGiftGold[q.Level-1] + switch q.Level { + case 1: + q.Codes, _ = makeExchangeCode(7, 1) + case 2: + //30天会员 + q.Codes, _ = makeExchangeCode(30, 1) + case 3: + //30天会员*2 + q.Codes, _ = makeExchangeCode(30, 2) + default: + + } + if err = questionnremod.Insert(q); err != nil { + code = stderr.ErrNetWorkBusy + return + } + res.Gold = q.Gold + res.Codes = q.Codes + res.QuestionId = q.QuestionnaireId + return +} + +func GetByUID(uid uint64) (int, []questionnremod.Questionnaire, error) { + vipLevel := 0 + u, _ := usermod.FindUserByUID(uid) + if u != nil && u.VipExpireDate.After(time.Now()) { + vipLevel = u.VipLevel + } + data, err := questionnremod.GetByUID(uid) + return vipLevel, data, err +} diff --git a/app/service/rankser/rankser.go b/app/service/rankser/rankser.go new file mode 100644 index 0000000..428f5ec --- /dev/null +++ b/app/service/rankser/rankser.go @@ -0,0 +1,241 @@ +package rankser + +import ( + "sync" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/models/s/kwrankmod" + "91porn-server/models/s/statusermod" + "91porn-server/models/v/usermod" +) + +const MaxCount = 30 + +type KWPage struct { + List []kwrankmod.Keyword `json:"list"` + Count int64 `json:"count"` + UpdatedAt string `json:"updatedAt"` + HasNext bool `json:"hasNext"` +} + +// 热搜排行榜 +func GetHotSearchList() (KWPage, error) { + keywords, err := kwrankmod.RecentRanking(MaxCount) + if err != nil { + log.Error("app service rank GetHotSearchList 1 FindHotSearchList err", log.E(err)) + return KWPage{}, err + } + now := time.Now() + return KWPage{ + List: keywords, + Count: int64(len(keywords)), + UpdatedAt: now.Format("2006-01-02 15:04"), + HasNext: false, + }, nil +} + +type RankType = string + +const ( + UserInviteRankType RankType = "userInviteRank" //推广达人 + UserIncomeRankType RankType = "userIncomeRank" //收益达人 + UserUploadRankType RankType = "userUploadRank" //上传达人 + + //VideoTodayRankType RankType = "videoTodayRank" //今日排行(视频) + //VideoLikeRankType RankType = "videoLikeRank" //点赞榜 + //VideoCmtRankType RankType = "videoCmtRank" //评论榜 + //KeywordHotRankType RankType = "keywordHotRank" //热搜榜 +) + +const ( + UserInviteRankMax = 50 + UserIncomeRankMax = 50 + UserUploadRankMax = 50 +) + +type RankMap = map[RankType]interface{} + +func GetRankMap() (RankMap, error) { + rankMap := RankMap{} + wg := sync.WaitGroup{} + wg.Add(3) + var ( + userInviteRank UserRank + userIncomeRank UserRank + userUploadRank UserRank + ) + common.Go(func() { + defer wg.Done() + //收益达人 + rank, err := redisUserIncomeRank() + if err != nil { + log.Warn("redisUserIncomeRank faild", log.E(err)) + } + userIncomeRank = rank + }) + //上传达人 + common.Go(func() { + defer wg.Done() + rank, err := redisUserUploadRank() + if err != nil { + log.Warn("redisUserUploadRank faild", log.E(err)) + } + userUploadRank = rank + }) + wg.Wait() + rankMap[UserInviteRankType] = userInviteRank + rankMap[UserIncomeRankType] = userIncomeRank + rankMap[UserUploadRankType] = userUploadRank + return rankMap, nil +} + +type UBaseInfo = usermod.BaseInfo + +func getExcludedUIDMap() (map[uint64]byte, error) { + excludedUIDMap, err := usermod.GetLockedUIDMap() + if err != nil { + return nil, err + } + if appg.Static != nil { + sysUserList := appg.Static.SysUser + for _, uid := range sysUserList { + excludedUIDMap[uid] = 1 + } + } + //历史遗留的区代不参加排行 + oldAgents := []uint64{115057, 117557, 564993, 1384837, 1384879, 1855799} + for _, agent := range oldAgents { + excludedUIDMap[agent] = 1 + } + return excludedUIDMap, nil +} + +func GetUserIncomeRank() (UserRank, error) { + excludedUIDMap, err := getExcludedUIDMap() + if err != nil { + return UserRank{}, err + } + list, err := statusermod.ListByVidIncome(UserIncomeRankMax + int64(len(excludedUIDMap))) + if err != nil { + return UserRank{}, err + } + listLen := len(list) + uidList := make([]uint64, 0, listLen) + for _, v := range list { + if _, ok := excludedUIDMap[v.UID]; !ok { + uidList = append(uidList, v.UID) + } + } + if len(uidList) > UserIncomeRankMax { + uidList = uidList[:UserIncomeRankMax] + } + uInfos, err := usermod.GetUsersBaseInfo(uidList) + if err != nil { + return UserRank{}, err + } + uInfoMap := make(map[uint64]*UBaseInfo) + for _, info := range uInfos { + uInfoMap[info.UID] = info + } + updatedAt := time.Time{} + if listLen != 0 { + updatedAt = list[0].UpdatedAt + } + userList := make([]User, 0, listLen) + for _, v := range list { + uInfo, ok := uInfoMap[v.UID] + if !ok { + continue + } + userList = append(userList, User{ + UID: uInfo.UID, + Name: uInfo.Name, + Portrait: uInfo.Portrait, + Value: v.Count, + }) + } + rank := UserRank{ + ItemList: userList, + Sort: 1, + Background: "", + UpdatedAt: updatedAt, + } + return rank, nil +} + +func redisUserIncomeRank() (UserRank, error) { + redisKey := redisconst.RankKey(UserIncomeRankType) + var rank UserRank + if err := appg.Redis.Scan(redisKey, &rank); err != nil { + log.Warn("rankser redisUserIncomeRank redis Scan failed", log.E(err)) + return GetUserIncomeRank() + } + return rank, nil +} + +func GetUserUploadRank() (UserRank, error) { + excludedUIDMap, err := getExcludedUIDMap() + if err != nil { + return UserRank{}, err + } + list, err := statusermod.ListByUploadCount(UserUploadRankMax + int64(len(excludedUIDMap))) + if err != nil { + return UserRank{}, err + } + listLen := len(list) + uidList := make([]uint64, 0, listLen) + for _, v := range list { + if _, ok := excludedUIDMap[v.UID]; !ok { + uidList = append(uidList, v.UID) + } + } + if listLen > UserUploadRankMax { + uidList = uidList[:UserUploadRankMax] + } + uInfos, err := usermod.GetUsersBaseInfo(uidList) + if err != nil { + return UserRank{}, err + } + uInfoMap := make(map[uint64]*UBaseInfo) + for _, info := range uInfos { + uInfoMap[info.UID] = info + } + updatedAt := time.Time{} + if len(list) != 0 { + updatedAt = list[0].UpdatedAt + } + userList := make([]User, 0, listLen) + for _, v := range list { + uInfo, ok := uInfoMap[v.UID] + if !ok { + continue + } + userList = append(userList, User{ + UID: uInfo.UID, + Name: uInfo.Name, + Portrait: uInfo.Portrait, + Value: v.Count, + }) + } + rank := UserRank{ + ItemList: userList, + Sort: 1, + Background: "", + UpdatedAt: updatedAt, + } + return rank, nil +} + +func redisUserUploadRank() (UserRank, error) { + redisKey := redisconst.RankKey(UserUploadRankType) + var rank UserRank + if err := appg.Redis.Scan(redisKey, &rank); err != nil { + log.Warn("rankser redisUserUploadRank redis Scan faild", log.E(err)) + return GetUserUploadRank() + } + return rank, nil +} diff --git a/app/service/rankser/type.go b/app/service/rankser/type.go new file mode 100644 index 0000000..61df40c --- /dev/null +++ b/app/service/rankser/type.go @@ -0,0 +1,36 @@ +package rankser + +import ( + "encoding/json" + "time" +) + +type UserRank struct { + ItemList []User `json:"itemList"` + Sort int `json:"sort"` + Background string `json:"background"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func (r UserRank) MarshalBinary() ([]byte, error) { + return json.Marshal(r) +} + +func (r *UserRank) UnmarshalBinary(b []byte) error { + return json.Unmarshal(b, r) +} + +type User struct { + UID uint64 `json:"uid"` //用户id + Name string `json:"name"` //用户名 + Portrait string `json:"portrait"` //头像 + Value int64 `json:"value"` //榜单值 +} + +func (u User) MarshalBinary() ([]byte, error) { + return json.Marshal(u) +} + +func (u *User) UnmarshalBinary(b []byte) error { + return json.Unmarshal(b, u) +} diff --git a/app/service/rechargeser/new_pay.go b/app/service/rechargeser/new_pay.go new file mode 100644 index 0000000..1a66e26 --- /dev/null +++ b/app/service/rechargeser/new_pay.go @@ -0,0 +1,785 @@ +package rechargeser + +import ( + "91porn-server/app/service/productser" + "context" + "errors" + "fmt" + "strconv" + "strings" + "sync" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + + "91porn-server/app/appg" + "91porn-server/app/proto" + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/currencymod" + "91porn-server/models/v/goldcfgmod" + "91porn-server/models/v/goldextramod" + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/productposimod" + "91porn-server/models/v/rchgamtmod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" +) + +// New_CurrencyList 货币列表 +func New_CurrencyList(ctx context.Context, uid uint64, sysType string, t commod.CurrencyType) ([]*proto.CurrencyListResponse, stderr.Code) { + if !isLegalCurrencyType(t) { + log.ErrorX(ctx, "货币类型异常", log.Any("CurrencyType", t)) + return nil, stderr.ErrParamError + } + var ( + wg sync.WaitGroup // 同步控制 + currencys []currencymod.Currency // 货币列表 + moneys []string // 金额区间 + removeRepeat = make(map[int64]struct{}, 0) // 金额去重 + productType int // 产品类型 棋牌:1 站群:0 + err error // 错误 + isFirstPay bool // 棋牌首次充值 + ) + wg.Add(1) + common.Go(func() { + defer wg.Done() + currencys, err = currencymod.List(t) + if err != nil { + log.ErrorX(ctx, "货币列表==>获取列表异常", log.Any("CurrencyType", t), log.E(err)) + } + }) + if t == commod.GameCoin { + wg.Add(1) + productType = 1 + common.Go(func() { + defer wg.Done() + u, err := usermod.FindUserByUID(uid) + if err != nil { + log.Error(fmt.Sprintf("货币列表==>棋牌首次充值查询异常[%v]", err)) + return + } + if u.HasFirstChargeWaLi { + //userLevel = 1 + } else { + isFirstPay = true + } + }) + } + wg.Wait() + if err != nil { + return nil, stderr.ErrNetWorkBusy + } + for _, c := range currencys { + if _, ok := removeRepeat[c.Price]; ok { + continue + } + removeRepeat[c.Price] = struct{}{} + moneys = append(moneys, FenToYuan(c.Price)) + } + + req := rchgutil.GainPayTypeReq{Money: moneys} + bc, err := req.GetPayType() + if err != nil { + log.ErrorX(ctx, "获取支付通道列表异常", log.Any("moneys", moneys), log.Any("sysType", sysType), + log.Any("productType", productType), log.E(err)) + return nil, stderr.ErrNetWorkBusy + } + data := make([]*proto.CurrencyListResponse, 0, len(currencys)) + for _, v := range currencys { + if v.Price < 100 { + continue + } + rechargeType := GetPayChannelDetails_New(v.Price, bc, productType) + if len(rechargeType) <= 0 { // 无支付方式的充值金额不需要展示 + continue + } + c := proto.CurrencyListResponse{ + ID: v.ID, + Amount: v.Coins, + TypeName: v.Name, + Money: v.Price, + GiveVipDays: v.GiveVipDays, + LouFengUnlockTimes: v.LouFengUnlockTimes, + GiveGold: v.GiveGold, + } + if isFirstPay || v.NotFirst || t == commod.FruitCoin || t == commod.Gold { + c.CouponDesc = v.CouponDesc + } + c.RechargeType = rechargeType + data = append(data, &c) + } + return data, stderr.Success +} + +// getChannelDisplayName 获取支付渠道显示名称 +func getChannelDisplayName(pType string) string { + channelNames := map[string]string{ + "wechat": "微信(当前通道拥挤)", + "alipay": "支付宝(官方强烈推荐)", + "union": "银联", + "daichong": "代充", + "usdt": "USDT(官方)", + "quickUnion": "快捷银联", + } + + if name, exists := channelNames[pType]; exists { + return name + } + return "" +} + +func FenToYuan(price int64) string { + return strconv.FormatInt(price/100, 10) +} + +func YuanToFen(money string) int64 { + // 去除前后空格 + money = strings.TrimSpace(money) + if money == "" { + return 0 + } + + // 检查是否为负数 + isNegative := false + if strings.HasPrefix(money, "-") { + isNegative = true + money = money[1:] + } + + // 按小数点分割 + parts := strings.Split(money, ".") + + switch len(parts) { + case 1: + // 只有整数部分,如 "123", "456" + yuan, err := strconv.ParseInt(parts[0], 10, 64) + if err != nil { + log.Warn(fmt.Sprintf("invalid integer part: %v", err)) + return 0 + } + result := yuan * 100 + if isNegative { + result = -result + } + return result + + case 2: + // 有整数和小数部分,如 "123.45", "78.9" + yuan, err := strconv.ParseInt(parts[0], 10, 64) + if err != nil { + log.Warn(fmt.Sprintf("invalid integer part: %v", err)) + return 0 + } + + // 处理小数部分 + decimalPart := parts[1] + if len(decimalPart) > 2 { + // 如果小数部分超过2位,进行四舍五入或截断 + // 这里选择截断,也可以根据需要改为四舍五入 + decimalPart = decimalPart[:2] + } else if len(decimalPart) == 1 { + // 如果只有1位小数,补零 + decimalPart += "0" + } + + fen, err := strconv.ParseInt(decimalPart, 10, 64) + if err != nil { + log.Warn(fmt.Sprintf("invalid decimal part: %v", err)) + return 0 + } + + result := yuan*100 + fen + if isNegative { + result = -result + } + return result + + default: + log.Warn(fmt.Sprintf("invalid money format: %s", money)) + return 0 + } +} + +func GetPayChannelDetails_New(money int64, payTypes []rchgutil.AllPayType, proT int) []rchgamtmod.PayChannelRes { + // 后台系统配置的支付方式展示名称与顺序 + cfg := getPayTypeDisplayConfig() + channels := make([]rchgamtmod.PayChannelRes, 0) + for _, payType := range payTypes { + // 金额匹配检查 + if YuanToFen(payType.Money) != money { + continue + } + + // 处理支持的支付类型 + for _, t := range payType.Types { + // 展示名称优先取后台配置,未配置则回退默认硬编码名称 + channelName := cfg.typeName(t.Type) + if channelName == "" { + continue + } + + channels = append(channels, rchgamtmod.PayChannelRes{ + Type: t.Type, + TypeName: channelName, + Channel: t.Type, // 根据实际情况调整 + }) + } + } + // 按后台配置的顺序排序,未配置顺序的支付方式排在最后 + cfg.sortChannels(channels) + return channels +} + +func New_ProductList(uid uint64, sysType string, newUser bool, proT int) (res []proto.ProductList, err error) { + var data []productmod.Product + var discounts []*goldcfgmod.DiscountConfig + var positionM map[string]string + var positionL []string + var ps map[string]int64 + var ( + goldsError, discountsError, positionError error + ) + wg := sync.WaitGroup{} + wg.Add(3) + //获取金币配置优惠 + common.Go(func() { + defer wg.Done() + discounts, discountsError = goldcfgmod.GetGoldConfigList() + if discountsError != nil { + err = goldsError + return + } + }) + common.Go(func() { + defer wg.Done() + flag := true + + data, goldsError = productmod.FindProductsByProductType(&flag, newUser, sysType) + if goldsError != nil { + err = goldsError + return + } + // 根据用户当前卡进行升级 + productser.CheckUserUpgradeProducts(uid, data) + }) + common.Go(func() { + defer wg.Done() + positionM, ps, positionL, positionError = productposimod.FindNamesWithStatusTrue() + if positionError != nil { + err = positionError + return + } + }) + wg.Wait() + + moneys := GetUniqueMoneys(data) + + // 缓存支付通道信息 + req := rchgutil.GainPayTypeReq{Money: moneys} + bc, err := req.GetPayTypeFromCache() + if err != nil { + log.Warn(fmt.Sprintf("用户ID:%d;rchgutil GetPayType:%v", uid, err)) + } + // 构建结果 + res = make([]proto.ProductList, len(positionL)) + for i := range positionL { + pl := make([]proto.VIPListRes, 0) + res[i] = proto.ProductList{ + ShowType: ps[positionL[i]], + Position: positionM[positionL[i]], + List: pl, + PositionID: positionL[i], + } + } + + for _, v := range data { + chans := GetPayChannelDetails_New(v.DiscountedPrice*10, bc, proT) + if v.ProductType == commod.PhysicalGoods { + v.Name = "" + } + temp := proto.VIPListRes{ + Product: v, + } + if discountsError == nil && len(discounts) > 0 { + for i := 0; i < len(chans); i++ { + for _, d := range discounts { + if chans[i].Type == d.Type && v.DiscountedPrice == d.Amount { + chans[i].IncrAmount = d.IncrAmount + chans[i].IncTax = d.IncTax + } + } + } + } + temp.RechargeType = chans + for k := range res { + if positionM[v.Position] == res[k].Position { + res[k].List = append(res[k].List, temp) + continue + } + } + } + + return +} + +func GetUniqueMoneys(data []productmod.Product) []string { + moneySet := make(map[string]bool) + + for _, p := range data { + money := FenToYuan(p.DiscountedPrice * 10) + moneySet[money] = true + if p.AdvanceAmount > 0 { + advanceAmount := FenToYuan(p.AdvanceAmount * 10) + moneySet[advanceAmount] = true + } + if p.BalanceAmount > 0 { + balanceAmount := FenToYuan(p.BalanceAmount * 10) + moneySet[balanceAmount] = true + } + } + + moneys := make([]string, 0, len(moneySet)) + for money := range moneySet { + moneys = append(moneys, money) + } + + return moneys +} + +// RefundCallBack 退款回调处理逻辑 +func RefundCallBack(ctx context.Context, oid string, tradeNo string) error { + log.InfoX(ctx, "退款回调", log.Any("oid", oid), log.Any("tradeNo", tradeNo)) + // 查询订单 + r, err := rchgordmod.FindRechargeOrderByID(tradeNo) + if err != nil { + log.ErrorX(ctx, "充值回调查询订单异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err)) + return err + } + if r.ID.IsZero() { + log.WarnX(ctx, "充值回调订单不存在", log.Any("oid", oid), log.Any("tradeNo", tradeNo)) + return errors.New("订单不存在") + } + + if r.Status == rchgordmod.REFUND { + log.Warn(fmt.Sprintf("oid:%v, tradeNo:%v, 退款回调重复回调", oid, tradeNo)) + return nil + } + + if r.Status != rchgordmod.SUCCESS { + log.Warn(fmt.Sprintf("oid:%v, tradeNo:%v, 退款回调订单状态异常", oid, tradeNo)) + return nil + } + + var ( + fn func(*db.MongoTool) error + now = time.Now() + desc = rchgordmod.Status(rchgordmod.REFUND).Desc() + remark = "订单退款" + ) + switch r.BuyType { + case commod.BuyGold: + if fn, err = RefundGold(ctx, r); err != nil { + log.ErrorX(ctx, "退款回调购买金币异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err)) + return err + } + + case commod.BuyProduct: + if fn, err = RefundProduct(ctx, r); err != nil { + log.ErrorX(ctx, "退款回调购买商品异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err)) + return err + } + default: // 默认为旧版购买处理 + log.InfoX(ctx, "退款回调旧版本处理", log.Any("oid", oid), log.Any("tradeNo", tradeNo)) + return errors.New("旧版退款回调不再支持, 请使用新版回调方式") + } + + // 新版处理 + if r.BuyType > 0 { + err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + if err = rchgordmod.RefundCallBackModify(ctx, t, r.ID, rchgordmod.EditSelector{ + OID: &oid, + UpdatedAt: now, + RefundAt: &now, + Status: rchgordmod.REFUND, + StatusDesc: &desc, + Remark: &remark, + }); err != nil { + return err + } + return fn(t) + }) + return err + } + if err != nil { + log.Error(fmt.Sprintf("oid:%s,refund callBack err:%v", oid, err)) + return errors.New("退款失败") + } + + return nil +} + +// RefundGold 退款金币 payMoney单位为分 +func RefundGold(ctx context.Context, order rchgordmod.RechargeOrder) (func(*db.MongoTool) error, error) { + payMoney := order.PayMoney + var gold = payMoney / 10 + // 查询金币配置 + currency, err := currencymod.Get(order.ProductID) + if err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值金币-订单号[%s] 产品ID[%v] 查询异常[%v]", order.ID, order.ProductID, err)) + return nil, err + } + // 未匹配到对应配置 + if currency.ID.IsZero() { + log.ErrorX(ctx, fmt.Sprintf("充值金币-订单号[%s] 产品ID[%v] 不存在", order.ID, order.ProductID)) + } else { // 匹配到对应配置 + // 金额不能低于配置金额1元并且有赠送金额 + if (payMoney+100-currency.Price) > 0 && currency.GiveGold > 0 { + gold += currency.GiveGold + } + } + return func(t *db.MongoTool) error { + // 增加金币与总充值金额 + originGold := gold + goldExtra := uint64(0) + if order.GoldExtraID != nil { + extra, err := goldextramod.GetUserGoldExtraByID(t, *order.GoldExtraID) + if err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值退款金币-订单号[%s] 查询金币加购券[%v] 出错: %s", order.ID, *order.GoldExtraID, err.Error())) + return err + } + if extra.Uid != order.UID || extra.Used || extra.Expired.Before(time.Now()) { + log.ErrorX(ctx, fmt.Sprintf("充值退款金币-订单号[%s] 查询金币加购券[%v] 无效", order.ID, *order.GoldExtraID)) + return errors.New("无效的金币加购券") + } + if err := goldextramod.UseGoldExtra(t, order.UID, *order.GoldExtraID); err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值退款金币-订单号[%s] 使用金币加购券[%v] 出错: %s", order.ID, *order.GoldExtraID, err.Error())) + return err + } + goldExtra = extra.Amount + gold += int64(extra.Amount) + } + + gold = -gold + payMoney = -payMoney + w, err := walletmod.Credit(t, walletmod.CreditPlan{ + Amount: &gold, + Consumption: &payMoney, + }, order.UID) + if err != nil { + return err + } + desc := "官方充值-退款" + strconv.FormatInt(gold, 10) + "金币" + if goldExtra != 0 { + desc = "官方充值-退款" + strconv.FormatInt(originGold, 10) + "金币, 消耗金币加购券退款" + strconv.FormatUint(goldExtra, 10) + "金币" + } + return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + TransNo: order.ID, + UID: order.UID, + Amount: gold, + ActualAmount: float64(gold), + TranType: txnmod.OrderRefund.Key(), + TranTypeInt: int64(txnmod.OrderRefund), + ChannelType: order.RechargeType, + Desc: desc, + DiscDoc: order.DiscDoc, + SysType: order.DevType, + RealAmount: w.RealAmount(), + }) + }, nil +} + +// RefundProduct 退款购买商品 +func RefundProduct(ctx context.Context, order rchgordmod.RechargeOrder) (func(*db.MongoTool) error, error) { + payMoney := order.PayMoney + var ( + creditPlan = walletmod.CreditPlan{Consumption: &payMoney} + u *usermod.User + p *productmod.Product + err error + wg sync.WaitGroup + w *walletmod.Wallet + ) + wg.Add(3) + common.Go(func() { + defer wg.Done() + // 查询会员卡信息 + p, err = productmod.FindProduct(order.ProductID, order.DevType) + if err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%v] 产品ID[%v] 查询会员卡信息异常[%v]", order.ID, order.ProductID, err)) + } + }) + common.Go(func() { + defer wg.Done() + // 查询用户信息 + u, err = usermod.FindUserByUID(order.UID) + if err != nil { + log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%s] 用户ID[%d] 查询用户信息异常 [%v]", order.ID, order.UID, err)) + } + }) + common.Go(func() { + defer wg.Done() + var errG error + w, errG = walletmod.GetWallet(order.UID) + if errG != nil { + log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%s] 用户ID[%d] 查询用户钱包异常 [%v]", order.ID, order.UID, err)) + } + }) + wg.Wait() + if p == nil { + log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%v] 产品ID[%v]不存在", order.ID, order.ProductID)) + return nil, fmt.Errorf("充值购买会员卡-订单号[%v] 产品ID[%v]不存在", order.ID, order.ProductID) + } + + if u == nil { + log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%v] 用户ID[%d]不存在", order.ID, order.UID)) + return nil, fmt.Errorf("充值购买会员卡-订单号[%v] 用户ID[%d]不存在", order.ID, order.UID) + } + // 充值金额低于会员卡售价1元 转化为充值对应金额金币 + var ( + disPay = payMoney - p.DiscountedPrice*10 + fn func(t *db.MongoTool) error + ) + if disPay < -100 { + var ( + gold = -(payMoney / 10) + ) + creditPlan.Amount = &gold + return func(t *db.MongoTool) error { + w, err := walletmod.Credit(t, creditPlan, order.UID) + if err != nil { + return err + } + //插入一条支出流水 + if err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + TransNo: order.ID, + UID: order.UID, + Amount: gold, + ActualAmount: float64(gold), + TranType: txnmod.OrderRefund.Key(), + TranTypeInt: int64(txnmod.OrderRefund), + ChannelType: order.RechargeType, + Desc: fmt.Sprintf("退款官方充值购买[%s]实际支付[%d]元;退款[%d]金币", p.Name, payMoney/100, payMoney/10), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + RealAmount: w.RealAmount(), + }); err != nil { + log.ErrorX(ctx, fmt.Sprintf("rechargeSer CallbackAddCoins Trans fail error:%+v order:%+v", err, order)) + return err + } + return nil + }, nil + } + // 购买商品处理 + switch p.ProductType { + case commod.VIP, commod.NEWUSERCard: + if fn, err = RefundBuyVIP(order, p, "退款官方充值-购买[%s]", w); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("退款充值购买会员卡-订单号[%s] 商品ID[%s] 未知商品", order.ID, order.ProductID) + } + // 返回事务函数 + return func(t *db.MongoTool) error { + // 充值金额大于会员卡售价1角 转化为充值对应金额金币 + if disPay > 9 { + var ( + gold = -(disPay / 10) + ) + creditPlan.Amount = &gold + w, err := walletmod.Credit(t, creditPlan, order.UID) + if err != nil { + return err + } + //插入一条支出流水 + if err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + TransNo: order.ID, + UID: order.UID, + Amount: gold, + ActualAmount: float64(gold), + TranType: txnmod.OrderRefund.Key(), + TranTypeInt: int64(txnmod.OrderRefund), + ChannelType: order.RechargeType, + Desc: fmt.Sprintf("退款官方充值购买[%s]实际支付[%d]元;超额部分退款为[%d]金币", p.Name, payMoney/100, disPay), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + RealAmount: w.RealAmount(), + }); err != nil { + log.ErrorX(ctx, fmt.Sprintf("rechargeSer CallbackAddCoins Trans fail error:%+v order:%+v", err, order)) + return err + } + } + if err = fn(t); err != nil { + return err + } + return nil + }, nil +} + +func RefundBuyVIP(order rchgordmod.RechargeOrder, p *productmod.Product, desc string, w *walletmod.Wallet) (func(*db.MongoTool) error, error) { + var ( + //商品购买记录 + history = prdcthsomod.ProductHistory{ + ID: primitive.NewObjectID(), + UID: order.UID, + ProductID: p.ID, + Name: p.Name, + Amount: p.DiscountedPrice, + ProductType: prdcthsomod.VIP, + DiscDoc: order.DiscDoc, + SysType: order.DevType, + ProductSnapShot: p, + } + sel usermod.UserSelector + creditPlan = walletmod.CreditPlan{} + ) + // 资金流水记录 + txnLogs := []txnmod.TransactionLog{ + { + UID: order.UID, + TranType: txnmod.OrderRefundVip.Key(), + TranTypeInt: int64(txnmod.OrderRefundVip), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name), + DiscDoc: order.DiscDoc, + SysType: order.DevType, + CurrencyType: txnmod.CurrencyTypeCash, + Amount: -p.DiscountedPrice, + ActualAmount: float64(-p.DiscountedPrice), + RealAmount: w.RealAmount(), + }, + } + // 查询用户信息 + u, err := usermod.FindUserByUID(order.UID) + if err != nil { + log.Error(fmt.Sprintf("退款充值购买会员卡-订单号[%s] 查询用户信息异常 [%v]", order.ID, err)) + return nil, err + } + // VIP变更 + if p.Duration > 0 { + var expire time.Time + // VIP未过期 + if u.VipExpireDate.After(time.Now()) { + expire = u.VipExpireDate.AddDate(0, 0, -p.Duration) + } else { + expire = time.Now().AddDate(0, 0, -p.Duration) + } + log.Info(fmt.Sprintf("到期时间:%v", expire)) + sel.VipExpireDate = &expire + } + // 增加金币视频免费天数 + if p.GoldVideoFreeDay > 0 { + var expire time.Time + if u.GoldVideoFreeExpire.IsZero() || u.GoldVideoFreeExpire.Before(time.Now()) { + expire = time.Now().AddDate(0, 0, -p.GoldVideoFreeDay) + } else { + expire = u.GoldVideoFreeExpire.AddDate(0, 0, -p.GoldVideoFreeDay) + } + sel.GoldVideoFreeExpire = &expire + } + if p.BroadcastDays > 0 { + expire := time.Time{} + if u.BroadcastExpire.IsZero() || u.BroadcastExpire.Before(time.Now()) { + expire = time.Now().AddDate(0, 0, -p.BroadcastDays) + } else { + expire = u.BroadcastExpire.AddDate(0, 0, -p.BroadcastDays) + } + sel.BroadcastExpire = &expire + } + if p.DramaDays > 0 { + expire := usermod.RenewDramaExpire(u.DramaExpire, time.Now(), -p.DramaDays) + sel.DramaExpire = &expire + } + // 回收赠送金币 + if p.GiveCoin > 0 { + coin := -p.GiveCoin + creditPlan.Amount = &coin + } + + // 回收赠送下载次数 + if p.DownloadCount > 0 { + count := -p.DownloadCount + creditPlan.DownloadCount = &count + } + + // 回收赠送AI免费脱衣次数 + if p.AiUndressCount > 0 { + aiUndressFreeTimes := int64(-p.AiUndressCount) + creditPlan.AiUndressFreeTimes = &aiUndressFreeTimes + } + + return func(t *db.MongoTool) error { + // 钱包变更 + if p.GiveCoin > 0 || p.DownloadCount > 0 || p.AiUndressCount > 0 { + w, err := walletmod.Credit(t, creditPlan, order.UID) + if err != nil { + return err + } + + if p.DownloadCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + DownloadCount: p.DownloadCount, + TranType: txnmod.AdminDebitDownloadCount.Key(), + TranTypeInt: int64(txnmod.AdminDebitDownloadCount), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-回收赠送[%d]次数", p.DownloadCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + }) + } + + // 插入购买会员卡回收赠送金币流水 + if p.GiveCoin > 0 { + giveLog := txnmod.TransactionLog{ + UID: order.UID, + Amount: p.GiveCoin, + ActualAmount: float64(p.GiveCoin), + TranType: txnmod.RefundVipCardGive.Key(), + TranTypeInt: int64(txnmod.RefundVipCardGive), + TransNo: history.ID, + Desc: fmt.Sprintf("购买%s-回收赠送金币[%v个]", p.Name, p.GiveCoin), + SysType: u.SysType, + RealAmount: w.RealAmount(), + } + txnLogs = append(txnLogs, giveLog) + } + + // 回收赠送免费次数 + if p.AiUndressCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: order.UID, + Amount: int64(p.AiUndressCount), + ActualAmount: float64(p.AiUndressCount), + TranType: txnmod.AdminDebitAiUndressFreeTimes.Key(), + TranTypeInt: int64(txnmod.AdminDebitAiUndressFreeTimes), + TransNo: history.ID, + Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-回收赠送AI脱衣免费次数[%d次]", p.AiUndressCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: w.RealAmount(), + }) + } + } + // 用户信息变更 + if p.Duration > 0 || p.GoldVideoFreeDay > 0 || p.BroadcastDays > 0 || p.DramaDays > 0 { + if _, err = usermod.UpdateTrans(t, u.UID, sel); err != nil { + return err + } + } + + // 保存资金流水记录 + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + return err + } + + return nil + }, nil +} diff --git a/app/service/rechargeser/paytype_config.go b/app/service/rechargeser/paytype_config.go new file mode 100644 index 0000000..9efcb82 --- /dev/null +++ b/app/service/rechargeser/paytype_config.go @@ -0,0 +1,59 @@ +package rechargeser + +import ( + "sort" + + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/v/rchgamtmod" + "91porn-server/models/v/sysconfmod" +) + +// payTypeDisplayConfig 支付方式展示配置(来自后台系统配置 GPCodePay) +type payTypeDisplayConfig struct { + nameMap map[string]string // 支付方式 -> 展示名称 + sortIndex map[string]int // 支付方式 -> 顺序(越小越靠前) +} + +// getPayTypeDisplayConfig 读取支付方式展示配置(带缓存)。 +// 读取失败时返回空配置,调用方会回退到默认硬编码名称与上游默认顺序。 +func getPayTypeDisplayConfig() payTypeDisplayConfig { + cfg := payTypeDisplayConfig{} + conf, err := sysconfdata.GetByGpCodeFromCache(sysconfmod.GPCodePay) + if err != nil { + return cfg + } + cfg.nameMap = conf.GetObject(sysconfmod.VCodePayTypeName) + sortList := conf.GetStrSlice(sysconfmod.VCodePayTypeSort) + cfg.sortIndex = make(map[string]int, len(sortList)) + for i, t := range sortList { + if _, ok := cfg.sortIndex[t]; !ok { + cfg.sortIndex[t] = i + } + } + return cfg +} + +// typeName 返回支付方式展示名称,未配置则回退到默认硬编码名称 +func (c payTypeDisplayConfig) typeName(payType string) string { + if name, ok := c.nameMap[payType]; ok && name != "" { + return name + } + return getChannelDisplayName(payType) +} + +// sortChannels 按后台配置的顺序对支付方式排序,未配置顺序的支付方式排在最后(保持相对顺序稳定) +func (c payTypeDisplayConfig) sortChannels(channels []rchgamtmod.PayChannelRes) { + if len(c.sortIndex) == 0 || len(channels) <= 1 { + return + } + const last = 1 << 30 + orderOf := func(t string) int { + if idx, ok := c.sortIndex[t]; ok { + return idx + } + return last + } + sort.SliceStable(channels, func(i, j int) bool { + return orderOf(channels[i].Type) < orderOf(channels[j].Type) + }) +} diff --git a/app/service/rechargeser/rechargeser.go b/app/service/rechargeser/rechargeser.go new file mode 100644 index 0000000..c8dc122 --- /dev/null +++ b/app/service/rechargeser/rechargeser.go @@ -0,0 +1,984 @@ +package rechargeser + +import ( + "91porn-server/app/service/activityclient" + "91porn-server/app/service/adser" + "91porn-server/middleware/ua" + "91porn-server/models/v/advanceordermod" + "context" + "encoding/json" + "errors" + "fmt" + "strconv" + "sync" + "time" + + "go.mongodb.org/mongo-driver/bson" + + "91porn-server/app/appg" + "91porn-server/app/proto" + "91porn-server/app/service" + "91porn-server/app/service/productser" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/currencymod" + "91porn-server/models/v/goldcfgmod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/productposimod" + "91porn-server/models/v/rchgamegoldmod" + "91porn-server/models/v/rchgamtmod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +/* +Recharge 充值 +返回 充值链接、充值模式、错误 +*/ +func Recharge(ctx context.Context, in *RechargeRequest, ua ua.UA, deduct *ActivityDeduct) (string, string, error) { + u, err := usermod.FindUserByUID(in.UID) + if err != nil { + return "", "", err + } + if u == nil { + return "", "", errors.New("invalid user") + } + if u.HasLocked { + return "", "", errors.New("user is banned") + } + if err = in.validateAttribution(u.UID); err != nil { + return "", "", err + } + // 创建订单 + order, advanceOrder, err := in.CreateOrder(u, ua, deduct) + if err != nil { + log.ErrorX(ctx, fmt.Sprintf("用户ID[%d] 支付方式[%s] 购买类型[%d] 产品ID[%v] 产品子ID[%v] 创建订单异常[%v]", + in.UID, in.RechargeType, in.BuyType, in.ProductID, in.ProductChildID, err)) + return "", "", err + } + if err = rchgordmod.InsertRechargeOrder(order); err != nil { + log.ErrorX(ctx, fmt.Sprintf("用户ID[%d] 支付方式[%s] 购买类型[%d] 产品ID[%v] 产品子ID[%v] 保存订单异常[%v]", + in.UID, in.RechargeType, in.BuyType, in.ProductID, in.ProductChildID, err)) + return "", "", err + } + + if advanceOrder == nil || advanceOrder.ID.IsZero() { + advanceOrder.AdvOid = order.ID.Hex() + _, err = advanceordermod.CreditOrder(nil, advanceOrder) + if err != nil { + log.Error("RechargeRequest.CreateOrder 创建预售订单失败", log.Any("advanceOrder", advanceOrder), log.E(err)) + return "", "", err + } + } else if advanceOrder.Status == advanceordermod.AdvanceProcessing { + // 修改订单号和预付款 + err := advanceordermod.Update(nil, advanceOrder.ID, bson.M{"advOid": order.ID.Hex(), "advanceAmount": advanceOrder.AdvanceAmount, "totalAmount": advanceOrder.TotalAmount}) + if err != nil { + log.Error("RechargeRequest.CreateOrder 修改预售订单失败", log.Any("advanceOrder.Status", advanceOrder.Status), log.E(err)) + return "", "", err + } + } else if advanceOrder.Status == advanceordermod.AdvanceSUCCESS || advanceOrder.Status == advanceordermod.BalanceProcessing { + err := advanceordermod.Update(nil, advanceOrder.ID, bson.M{"status": advanceordermod.BalanceProcessing, "balOid": order.ID.Hex()}) + if err != nil { + log.Error("RechargeRequest.CreateOrder 修改预售订单失败", log.Any("advanceOrder.Status", advanceOrder.Status), log.E(err)) + return "", "", err + } + } + + // 下单 + now := time.Now() + res, err := (&rchgutil.Recharge{ + TradeNo: order.ID.Hex(), + Money: rchgutil.FenToYuan(order.Money), + Type: order.RechargeType, + Info: rchgutil.PayInfo{ + PlayerId: strconv.FormatUint(order.UID, 10), + PlayerIp: order.UserIP, + DeviceId: order.DevID, + Tel: order.Tel, + DeviceType: order.DevType, + }, + }).ToPayNew(ctx) + if err != nil { + log.ErrorX(ctx, fmt.Sprintf("用户ID[%d] 支付方式[%s] 购买类型[%d] 产品ID[%v] 产品子ID[%v] 下单请求异常[%v]", + in.UID, in.RechargeType, in.BuyType, in.ProductID, in.ProductChildID, err)) + return "", "", err + } + + log.InfoX(ctx, fmt.Sprintf("用户ID[%d] 支付方式[%s] 购买类型[%d] 产品ID[%v] 产品子ID[%v] 下单返回信息[%v]", + in.UID, in.RechargeType, in.BuyType, in.ProductID, in.ProductChildID, res)) + // 更新订单 + set := rchgordmod.EditSelector{ + Status: rchgordmod.Processing, + ProgressAt: &now, + OID: &res.OID, + Mode: &res.Mode, + //Channel: &res.CID, + Rate: &res.Rebate, + } + if err = rchgordmod.Update(nil, order.ID, set); err != nil { + log.ErrorX(ctx, fmt.Sprintf("用户ID[%d] 支付方式[%s] 购买类型[%d] 产品ID[%v] 产品子ID[%v] 更新订单异常[%v]", + in.UID, in.RechargeType, in.BuyType, in.ProductID, in.ProductChildID, err)) + return "", "", err + } + log.InfoX(ctx, fmt.Sprintf("用户ID[%d] 支付方式[%s] 购买类型[%d] 产品ID[%v] 产品子ID[%v] 【下单成功】", + in.UID, in.RechargeType, in.BuyType, in.ProductID, in.ProductChildID)) + // 会员抵扣券:建单成功异步回调活动服占用(active -> used),携实际抵扣额 + if order.CouponID != "" { + activityclient.NotifyDeductCouponAsync(order.CouponID, order.ID.Hex(), activityclient.DeductCouponStatusUsed, order.DeductAmount) + } + return res.PayUrl, res.Mode, nil +} + +func Recharge_new(ctx context.Context, rchg *rchgordmod.RechargeOrder) (string, string, error) { + u, err := usermod.FindUserByUID(rchg.UID) + if err != nil { + return "", "", err + } + if u == nil { + return "", "", errors.New("invalid user") + } + rchg.DevID = u.DevID + rchg.Name = u.Name + rchg.DiscDoc = u.DiscDoc + if rchg.ProductType == 0 { + if rchg.VipID.IsZero() { + // 金币充值 + g, err := rchgamtmod.GetGoldByID(rchg.ProductID) + if err != nil { + return "", "", err + } + if g == nil { + err = errors.New("invalid productID") + return "", "", err + } + rchg.Amount = g.Coins + rchg.Money = g.Price + } else { + p, err := productmod.FindProduct(rchg.VipID, rchg.DevType) + if err != nil { + return "", "", err + } + if p == nil { + return "", "", errors.New("invalid productID") + } + //新用户8折 + /*if u.CreatedAt.Add(time.Hour*24).After(time.Now()) && p.ProductType == prdcthsomod.VIP && (p.DiscountedPrice == 5000 || p.DiscountedPrice == 10000) { + p.DiscountedPrice = int64(float64(p.DiscountedPrice) * 0.8) + }*/ + rchg.Amount = p.DiscountedPrice + rchg.Money = p.DiscountedPrice * 10 + } + } + if rchg.ProductType == 1 && rchg.Money <= 0 { + return "", "", errors.New("invalid money") + } + now := time.Now() + set := rchgordmod.EditSelector{ + Status: rchgordmod.Processing, + ProgressAt: &now, + ProductType: &rchg.ProductType, + } + statusDesc := rchgordmod.Status(set.Status).Desc() + set.StatusDesc = &statusDesc + channel := "self" + rchg.Channel = channel + if err = rchgordmod.InsertRechargeOrder(rchg); err != nil { + return "", "", err + } + var userLevel int + if rchg.ProductType == 1 && u.HasFirstChargeWaLi { + userLevel = 1 + } + res, err := (&rchgutil.Rchg{ + TransNo: rchg.ID.Hex(), + UID: strconv.FormatUint(u.UID, 10), + DevID: rchg.DevID, + UserIP: rchg.UserIP, + Name: rchg.Name, + Tel: u.Mobile, + DevType: rchg.DevType, + Money: rchg.Money, + Channel: channel, + CreatedAt: now, + PayMethod: rchg.RechargeType, + ProductType: rchg.ProductType, + UserLevel: userLevel, + }).ToPayNew(ctx) + if err != nil { + common.Go(func() { + if appg.Bot == nil { + return + } + //判断是否返回 渠道已经关闭 错误,此时商户将该渠道关闭 + if err == rchgutil.PAY_CHANNEL_CLOSE_ERROR { + return + } + }) + return "", "", err + } + set.OID = &res.OID + payUrl := res.PayUrl + mode := res.Mode + set.Mode = &mode + set.Channel = &res.CID + set.Rate = &(res.Rate) + return payUrl, mode, rchgordmod.Update(nil, rchg.ID, set) +} + +// GetPayChannel根据充值金额选择支付渠道 +func GetPayChannel_new(ctx context.Context, sysType string, proT int) (data []*rchgamtmod.GoldRes, err error) { + var golds []*currencymod.Currency + // var discounts []*goldcfgmod.DiscountConfig + var goldsError error + wg := sync.WaitGroup{} + wg.Add(1) + common.Go(func() { + defer wg.Done() + golds, goldsError = currencymod.GetCurrencyListByRedis(commod.Gold) + if goldsError != nil { + return + } + }) + wg.Wait() + moneysM := make(map[int64]int) + for vsi := range golds { + moneysM[golds[vsi].Price] = 1 + } + moneys := make([]int64, len(moneysM)) + mi := 0 + for k := range moneysM { + moneys[mi] = k + mi++ + } + bc, _ := rchgutil.GetPayType(ctx, moneys, sysType, 0, 0) + data = make([]*rchgamtmod.GoldRes, 0, len(golds)) + for _, v := range golds { + chans := GetPayChannelDetails_new(v.Price, bc, proT) + if len(chans) <= 0 { // 没有支付方式的充值金额不展示 + continue + } + data = append(data, &rchgamtmod.GoldRes{ + ID: v.ID, + RechargeType: chans, + Amount: v.Coins, + TypeName: v.Name, + Money: v.Price, + }) + } + return +} + +func isLegalCurrencyType(t commod.CurrencyType) bool { + return t == commod.Gold || t == commod.FruitCoin || t == commod.GameCoin +} + +// CurrencyList 货币列表(新增果币) +func CurrencyList(ctx context.Context, uid uint64, sysType string, t commod.CurrencyType) ([]*proto.CurrencyListResponse, stderr.Code) { + if !isLegalCurrencyType(t) { + log.ErrorX(ctx, "货币类型异常", log.Any("CurrencyType", t)) + return nil, stderr.ErrParamError + } + var ( + wg sync.WaitGroup // 同步控制 + currencys []currencymod.Currency // 货币列表 + moneys []int64 // 金额区间 + removeRepeat = make(map[int64]struct{}, 0) // 金额去重 + productType int // 产品类型 棋牌:1 站群:0 + err error // 错误 + isFirstPay bool // 棋牌首次充值 + userLevel = 0 + ) + wg.Add(1) + common.Go(func() { + defer wg.Done() + currencys, err = currencymod.List(t) + if err != nil { + log.ErrorX(ctx, "货币列表==>获取列表异常", log.Any("CurrencyType", t), log.E(err)) + } + }) + if t == commod.GameCoin { + wg.Add(1) + productType = 1 + common.Go(func() { + defer wg.Done() + u, err := usermod.FindUserByUID(uid) + if err != nil { + log.Error(fmt.Sprintf("货币列表==>棋牌首次充值查询异常[%v]", err)) + return + } + if u.HasFirstChargeWaLi { + userLevel = 1 + } else { + isFirstPay = true + } + }) + } + wg.Wait() + if err != nil { + return nil, stderr.ErrNetWorkBusy + } + for _, c := range currencys { + if _, ok := removeRepeat[c.Price]; ok { + continue + } + removeRepeat[c.Price] = struct{}{} + moneys = append(moneys, c.Price) + } + bc, err := rchgutil.GetPayType(ctx, moneys, sysType, productType, userLevel) + if err != nil { + log.ErrorX(ctx, "获取支付通道列表异常", log.Any("moneys", moneys), log.Any("sysType", sysType), + log.Any("productType", productType), log.E(err)) + return nil, stderr.ErrNetWorkBusy + } + data := make([]*proto.CurrencyListResponse, 0, len(currencys)) + for _, v := range currencys { + if v.Price < 100 { + continue + } + rechargeType := GetPayChannelDetails_new(v.Price, bc, productType) + if len(rechargeType) <= 0 { // 无支付方式的充值金额不需要展示 + continue + } + c := proto.CurrencyListResponse{ + ID: v.ID, + Amount: v.Coins, + TypeName: v.Name, + Money: v.Price, + GiveVipDays: v.GiveVipDays, + LouFengUnlockTimes: v.LouFengUnlockTimes, + GiveGold: v.GiveGold, + } + if isFirstPay || v.NotFirst || t == commod.FruitCoin || t == commod.Gold { + c.CouponDesc = v.CouponDesc + } + c.RechargeType = rechargeType + data = append(data, &c) + } + return data, stderr.Success +} + +func getChanByte(pType string) string { + switch pType { + case "wechat": //微信 + return "微信【当前通道拥挤】" + case "alipay": //支付宝 + //return "支付宝【🔥限时返利15%金币】" + return "支付宝【官方强烈推荐】" + case "union": //支付宝 + return "银联" + case "daichong": //代充 + return "代充" + case "usdt": + return "usdt" + case "quickUnion": + return "快捷银联" + } + return "" +} + +func GetPayChannelDetails_new(money int64, payTypes []rchgutil.RchgType, proT int) []rchgamtmod.PayChannelRes { + chans := make([]rchgamtmod.PayChannelRes, 0) + for k := range payTypes { + if payTypes[k].Money == money { + if payTypes[k].Alipay { + pName := getChanByte("alipay") + chans = append(chans, rchgamtmod.PayChannelRes{ + Type: "alipay", + TypeName: pName, + Channel: "alipay", + }) + } + if payTypes[k].Wechat { + pName := getChanByte("wechat") + chans = append(chans, rchgamtmod.PayChannelRes{ + Type: "wechat", + TypeName: pName, + Channel: "wechat", + }) + } + if payTypes[k].DaiChong { + pName := getChanByte("daichong") + channel := appg.Conf.DaiChong.H5domain + if proT == 1 { + channel = appg.Conf.DaiChong.H5domainQP + } + chans = append(chans, rchgamtmod.PayChannelRes{ + Type: "daichong", + TypeName: pName, + Channel: channel, + }) + } + if payTypes[k].USDT { + pName := getChanByte("usdt") + chans = append(chans, rchgamtmod.PayChannelRes{ + Type: "usdt", + TypeName: pName, + Channel: "usdt", + }) + } + if payTypes[k].QuickUnion { + pName := getChanByte("quickUnion") + chans = append(chans, rchgamtmod.PayChannelRes{ + Type: "quickUnion", + TypeName: pName, + Channel: "quickUnion", + }) + } + if payTypes[k].Union { + pName := getChanByte("union") + chans = append(chans, rchgamtmod.PayChannelRes{ + Type: "union", + TypeName: pName, + Channel: "union", + }) + } + break + } + } + return chans +} + +func CallbackAddCoins(ctx context.Context, t *db.MongoTool, amount, decPay int64, desc string, + order rchgordmod.RechargeOrder) (err error) { + if err != nil { + return err + } + wallet, err1 := walletmod.Credit(t, walletmod.CreditPlan{Amount: &amount, Consumption: &decPay}, order.UID) + if err1 != nil { + return err1 + } + tt := txnmod.TransactionLog{ + TransNo: order.ID, + UID: order.UID, + Amount: amount, + ActualAmount: float64(amount), + TranType: txnmod.Rchg.Key(), + TranTypeInt: int64(txnmod.Rchg), + ChannelType: order.RechargeType, + Desc: desc, + DiscDoc: order.DiscDoc, + SysType: order.DevType, + RealAmount: walletmod.GetRealAmount(wallet), + } + //插入一条支出流水 + if err = txnmod.InsertTransactionLog(t, &tt); err != nil { + log.ErrorX(ctx, fmt.Sprintf("rechargeSer CallbackAddCoins Trans fail error:%+v order:%+v", err, order)) + return + } + return +} + +// RechargeCallBack 充值成功回调处理逻辑 +func RechargeCallBack(ctx context.Context, oid string, payMoney int64, tradeNo string, code int, paymentAt, successAt time.Time) error { + log.InfoX(ctx, "充值回调", log.Any("oid", oid), log.Any("tradeNo", tradeNo)) + // 查询订单 + r, err := rchgordmod.FindRechargeOrderByID(tradeNo) + if err != nil { + log.ErrorX(ctx, "充值回调查询订单异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err)) + return err + } + if r.ID.IsZero() { + log.WarnX(ctx, "充值回调订单不存在", log.Any("oid", oid), log.Any("tradeNo", tradeNo)) + return errors.New("订单不存在") + } + if r.Status == rchgordmod.SUCCESS { + log.WarnX(ctx, "充值回调重复回调", log.Any("oid", oid), log.Any("tradeNo", tradeNo)) + return nil + } + if r.Status == rchgordmod.REFUND { + log.WarnX(ctx, "充值回调重复回调,订单已退款", log.Any("oid", oid), log.Any("tradeNo", tradeNo)) + return nil + } + if r.Status != rchgordmod.Processing { + log.WarnX(ctx, "充值回调订单状态异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.Any("status", r.Status)) + return errors.New("订单状态异常") + } + if r.ProductType == 1 { + return errors.New("瓦力游戏已下架") + } + // 新版充值---根据购买类型处理 + var ( + fn func(*db.MongoTool) error + now = time.Now() + desc = rchgordmod.Status(rchgordmod.SUCCESS).Desc() + csm int // 分成模式 + ) + + switch r.BuyType { + case commod.BuyGold: + csm = rchgordmod.ChanShareCoin + if fn, _, err = service.BuyGold(ctx, r, payMoney); err != nil { + log.ErrorX(ctx, "充值回调购买金币异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err)) + return err + } + case commod.BuyGameCoin: + if fn, err = service.BuyGameCoin(ctx, r, payMoney); err != nil { + log.ErrorX(ctx, "充值回调购买游戏币异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err)) + return err + } + case commod.BuyFruitCoin: + if fn, err = service.BuyFruitCoin(ctx, r, payMoney); err != nil { + log.ErrorX(ctx, "充值回调购买果币异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err)) + return err + } + case commod.BuyProduct: + if fn, csm, _, err = service.BuyProduct(ctx, r, payMoney); err != nil { + log.ErrorX(ctx, "充值回调购买商品异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err)) + return err + } + default: // 默认为旧版购买处理 + log.InfoX(ctx, "充值回调旧版本处理", log.Any("oid", oid), log.Any("tradeNo", tradeNo)) + return errors.New("旧版回调不再支持, 请使用新版回调方式") + } + // 新版处理 + if r.BuyType > 0 { + err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + if err = rchgordmod.CallBackModifyWithProcessing(ctx, t, r.ID, rchgordmod.EditSelector{ + PayMoney: &payMoney, + OID: &oid, + PaymentAt: &paymentAt, + SuccessAt: &successAt, + Status: rchgordmod.SUCCESS, + StatusDesc: &desc, + ChanShareMod: &csm, + }); err != nil { + return err + } + return fn(t) + }) + if err == nil { + // ABC广告分组测试 + common.Go(func() { + user, _ := usermod.FindUserByUID(r.UID) + if user != nil { + _ = adser.UpsertAdStat(context.Background(), user, time.Now(), 0, 0, payMoney) + } + }) + + // 会员抵扣券:支付成功异步回调活动服核销(used -> verified) + if r.CouponID != "" { + activityclient.NotifyDeductCouponAsync(r.CouponID, r.ID.Hex(), activityclient.DeductCouponStatusVerified, r.DeductAmount) + } + } + return err + } + id, err := primitive.ObjectIDFromHex(tradeNo) + if err != nil { + log.ErrorX(ctx, "ObjectIDFromHex fail", log.Any("tradeNo", tradeNo), log.E(err)) + return err + } + set := rchgordmod.EditSelector{ + PayMoney: &payMoney, + OID: &oid, + PaymentAt: &paymentAt, + } + if stderr.Code(code) != stderr.Success { + set.Status = rchgordmod.FAILURE + desc := rchgordmod.Status(rchgordmod.FAILURE).Desc() + set.StatusDesc = &desc + set.FailureAt = &now + set.SuccessAt = &successAt + return rchgordmod.CallBackModifyWithProcessing(ctx, nil, id, set) + } + if r.ProductID.IsZero() { //人民币购买会员卡 + p, err := productmod.FindProduct(r.VipID, r.DevType) + if err != nil { + return err + } + if p == nil { + return errors.New("invalid vipID") + } + disMoney := payMoney - (p.DiscountedPrice * 10) + if disMoney < -constant.RechargeAmtTolerance { + if payMoney <= 0 { + return nil + } + tdesc := "购买「" + p.Name + "卡」实际支付" + strconv.FormatFloat(float64(payMoney)/100, 'f', 2, 64) + + ",全额转为「" + strconv.FormatInt(payMoney/10, 10) + "」金币" + return appg.VideoDB.Trans(func(t *db.MongoTool) error { + csm := rchgordmod.ChanShareCoin + set.ChanShareMod = &csm + if err = rchgordmod.CallBackModifyWithProcessing(ctx, t, id, set); err != nil { + return err + } + return CallbackAddCoins(ctx, nil, payMoney/10, payMoney, tdesc, r) + }) + } + //查询金币优惠 + localAmount := p.DiscountedPrice + giveGold := false + var gold int64 + disCount, err := goldcfgmod.GetGoldCgfByAmountAndType(ctx, localAmount, r.RechargeType) + if disCount != nil && err == nil { + giveGold = true + if disCount.IncrAmount != 0 { + localAmount = disCount.IncrAmount + gold = disCount.IncrAmount + } else if disCount.IncTax != 0.0 { + incr := decimal.NewFromFloat(disCount.IncTax) + local := decimal.NewFromInt(localAmount) + localAmount = localAmount + local.Mul(incr).IntPart() + } + } + if p.ChanSplitMod == 1 { //正常分成 + csm := rchgordmod.ChanShareModVIP + set.ChanShareMod = &csm + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + set.SuccessAt = &successAt + set.Status = rchgordmod.SUCCESS + desc := rchgordmod.Status(rchgordmod.SUCCESS).Desc() + set.StatusDesc = &desc + if err = rchgordmod.CallBackModifyWithProcessing(ctx, t, id, set); err != nil { + return err + } + creditPlan := walletmod.CreditPlan{ + Consumption: &payMoney, + } + if giveGold { + creditPlan.Amount = &localAmount + } + if _, err = walletmod.Credit(t, creditPlan, r.UID); err != nil { + log.ErrorX(ctx, "walletmod.Credit failed", log.Any("creditPlan", creditPlan), log.Any("uid", r.UID), + log.E(err)) + return err + } + fPayMoney := float32(payMoney) + if disMoney > 9 { + tdesc := "购买「" + p.Name + "卡」实际支付" + strconv.FormatFloat(float64(fPayMoney)/100, 'f', 2, 64) + + ",超额部分转为「" + strconv.FormatInt(disMoney/10, 10) + "」金币" + if err = CallbackAddCoins(ctx, t, disMoney/10, 0, tdesc, r); err != nil { + return err + } + } + if err = RechargeBuy_p(t, r.UID, *p, gold); err != nil { + log.ErrorX(ctx, "RechargeBuy_p failed", log.Any("uid", r.UID), log.Any("product", p), + log.Any("gold", gold), log.E(err)) + return err + } + + return nil + }); err != nil { + log.ErrorX(ctx, "WalletSer RechargeCallBack Trans fail", log.E(err)) + return err + } + + } else { //充值金币 + amount := payMoney / 10 + g, err := rchgamtmod.GetGoldByID(r.ProductID) + if err != nil { + return err + } + if g == nil { + return errors.New("invalid productID") + } + if !r.VipID.IsZero() { + if amount > (g.Coins-10) && amount < g.Coins { + amount = g.Coins + } + } else { + //充值赠送金币 + localAmount := amount + giveGold := false + disCount, err := goldcfgmod.GetGoldCgfByAmountAndType(ctx, localAmount, r.RechargeType) + if disCount != nil && err == nil { + giveGold = true + if disCount.IncrAmount != 0 { + localAmount = disCount.IncrAmount + } else if disCount.IncTax != 0.0 { + incr := decimal.NewFromFloat(disCount.IncTax) + local := decimal.NewFromInt(localAmount) + localAmount = localAmount + local.Mul(incr).IntPart() + } + } + if giveGold { + amount += localAmount + } + } + csm := rchgordmod.ChanShareCoin + set.ChanShareMod = &csm + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + set.SuccessAt = &successAt + set.Status = rchgordmod.SUCCESS + desc := rchgordmod.Status(rchgordmod.SUCCESS).Desc() + set.StatusDesc = &desc + if err = rchgordmod.CallBackModifyWithProcessing(ctx, t, id, set); err != nil { + return err + } + if err = walletmod.CreditConsumption(t, payMoney, r.UID); err != nil { + return err + } + wallet, err := walletmod.CreditAmount(t, amount, r.UID) + if err != nil { + return err + } + tt := txnmod.TransactionLog{ + TransNo: r.ID, + UID: r.UID, + Amount: amount, + ActualAmount: float64(amount), + TranType: txnmod.Rchg.Key(), + TranTypeInt: int64(txnmod.Rchg), + ChannelType: r.RechargeType, + Desc: "充值新增-" + strconv.FormatInt(amount, 10), + DiscDoc: r.DiscDoc, + SysType: r.DevType, + RealAmount: walletmod.GetRealAmount(wallet), + } + //插入一条支出流水 + return txnmod.InsertTransactionLog(t, &tt) + }); err != nil { + log.ErrorX(ctx, "WalletSer RechargeCallBack Trans fail", log.E(err)) + return err + } + } + // ABC广告分组测试 + common.Go(func() { + user, _ := usermod.FindUserByUID(r.UID) + if user != nil { + _ = adser.UpsertAdStat(context.Background(), user, time.Now(), 0, 0, payMoney) + } + }) + // //用户充值成功时做充值扣量 + // executionRechargeDeduction(ctx, tradeNo) + + return nil +} + +func FindProductByTypeNew(ctx context.Context, uid uint64, sysType string, newUser bool, proT int) (res []proto.ProductList, err error) { + redisKey := redisconst.DataCachKey(productmod.RedisSetKey, "FindProductByTypeNew"+sysType+strconv.FormatBool(newUser)) + if !appg.Redis.IsExist(redisKey) { + var data []productmod.Product + var discounts []*goldcfgmod.DiscountConfig + var positionM map[string]string + var positionL []string + var ps map[string]int64 + var ( + goldsError, discountsError, positionError error + ) + wg := sync.WaitGroup{} + wg.Add(3) + //获取金币配置优惠 + common.Go(func() { + defer wg.Done() + discounts, discountsError = goldcfgmod.GetGoldConfigList() + }) + common.Go(func() { + defer wg.Done() + flag := true + data, goldsError = productmod.FindProductsByProductType(&flag, newUser, sysType) + if goldsError != nil { + err = goldsError + return + } + }) + common.Go(func() { + defer wg.Done() + positionM, ps, positionL, positionError = productposimod.FindNamesWithStatusTrue() + if positionError != nil { + err = positionError + return + } + }) + wg.Wait() + moneysM := make(map[int64]int) + for vsi := range data { + moneysM[data[vsi].DiscountedPrice*10] = 1 + } + moneys := make([]int64, len(moneysM)) + mi := 0 + for k := range moneysM { + moneys[mi] = k + mi++ + } + bc, _ := rchgutil.GetPayType(ctx, moneys, sysType, proT, 0) + res = make([]proto.ProductList, len(positionL)) + for i := range positionL { + pl := make([]proto.VIPListRes, 0) + res[i] = proto.ProductList{ + ShowType: ps[positionL[i]], + Position: positionM[positionL[i]], + List: pl, + PositionID: positionL[i], + } + } + for _, v := range data { + price := v.DiscountedPrice * 10 + if v.ProductType == productmod.AdvanceCard { + price = v.AdvanceAmount * 10 + } + chans := GetPayChannelDetails_new(price, bc, proT) + if v.ProductType == commod.PhysicalGoods { + v.Name = "" + } + temp := proto.VIPListRes{ + Product: v, + } + if discountsError == nil && len(discounts) > 0 { + for i := 0; i < len(chans); i++ { + for _, d := range discounts { + if chans[i].Type == d.Type && v.DiscountedPrice == d.Amount { + chans[i].IncrAmount = d.IncrAmount + chans[i].IncTax = d.IncTax + } + } + } + } + temp.RechargeType = chans + for k := range res { + if positionM[v.Position] == res[k].Position { + res[k].List = append(res[k].List, temp) + continue + } + } + } + jsonBytes, err := json.Marshal(res) + if err != nil { + return res, err + } + _ = appg.Redis.Set(redisKey, string(jsonBytes), redisconst.DataCachExpire) + } + str, err := appg.Redis.Get(redisKey) + if err != nil || str == nil { + return + } + if err = json.Unmarshal([]byte(*str), &res); err != nil { + return + } + return +} + +// RechargeBuy 充值回调时购买w +func RechargeBuy_p(t *db.MongoTool, uid uint64, p productmod.Product, giveGold int64) error { + switch p.ProductType { + case productmod.MeetingCard: //约会卡: + return productser.BuyMeetingCard_d(t, uid, p) + case productmod.OTHER, productmod.PHYSICALGOODS: + return productser.BuyOtherCard_d(t, uid, p) + case productmod.VIP, productmod.NewUser: + return productser.BuyVIP_d(t, uid, p, giveGold) + case productmod.VideoFreeCard: + return productser.BuyVideoFreeCard_d(t, uid, p) + case productmod.VideoDiscount: + return productser.BuyVideoDiscountCard_d(t, uid, p) + } + return errors.New("invalid productType") +} + +func GetPayChannel_qp(ctx context.Context, uid uint64, sysType string) (data []*rchgamtmod.GameGoldRes, err error) { + gameGolds, err := rchgamegoldmod.GetGoldListByActiveTrue() + if err != nil { + return + } + moneys := make([]int64, len(gameGolds)) + for i, v := range gameGolds { + moneys[i] = v.Price + } + if len(gameGolds) == 0 { //@todo:兼容旧版本,以后删除 + moneys = []int64{5000, 10000, 28800, 39800, 59600, 100000, 300000, 500000} //单位分 金额50, ... ,5000元 + for _, v := range moneys { + gameGolds = append(gameGolds, &rchgamegoldmod.Gold{ + Coins: v, + Price: v, + }) + } + } + firstPay := true + userLevel := 0 + if u, _ := usermod.FindUserByUID(uid); u != nil { + if u.HasFirstChargeWaLi { + firstPay = false + userLevel = 1 + } + } + bc, err := rchgutil.GetPayType(ctx, moneys, sysType, 1, userLevel) + if err != nil { + return + } + data = make([]*rchgamtmod.GameGoldRes, 0, len(gameGolds)) + for _, v := range gameGolds { + chans := GetPayChannelDetails_new(v.Price, bc, 1) + if len(chans) <= 0 { // 没有支付方式的充值金额不展示 + continue + } + gold := rchgamtmod.GameGoldRes{ + Amount: v.Coins * 100, + TypeName: fmt.Sprintf("%d元充值", v.Price/100), + Money: v.Price, + } + if firstPay || v.NotFirst { + gold.CouponDesc = v.CouponDesc + } + gold.RechargeType = chans + data = append(data, &gold) + } + return +} + +func GetUserTransactionDetails(uid, pageNum, pageSize uint64) (total int64, data []proto.TransactionResp, hasNext bool, err error) { + //获取用户成功的交易记录 + total, rechangeOrders, hasNext, _ := rchgordmod.GetSuccessOrderUidList(uid, pageSize, pageNum) + rechangeOrdersLen := len(rechangeOrders) + goldProductIds := make([]primitive.ObjectID, 0, rechangeOrdersLen) + vipProductIds := make([]primitive.ObjectID, 0, rechangeOrdersLen) + productIdMap := make(map[primitive.ObjectID]primitive.ObjectID) + for _, v := range rechangeOrders { + if _, ok := productIdMap[v.ProductID]; !ok { + if v.BuyType == commod.BuyGold || v.BuyType == commod.BuyGameCoin || v.BuyType == commod.BuyFruitCoin { + goldProductIds = append(goldProductIds, v.ProductID) + } else { + vipProductIds = append(vipProductIds, v.ProductID) + } + productIdMap[v.ProductID] = v.ProductID + } + } + ctx := context.Background() + //根据产品id 查询对应的奖品 + vipProductMap, err := productmod.ListByIDsMap(vipProductIds) + if err != nil { + log.ErrorX(ctx, "ListByIDsMap failed", log.Any("vipProductIds", vipProductIds), log.E(err)) + return + } + //根据产品id 查询金币对应的一个奖品 + goldProductMap, err := currencymod.ListByIdsMap(goldProductIds) + if err != nil { + log.ErrorX(ctx, "ListByIdsMap failed", log.Any("goldProductIds", goldProductIds), log.E(err)) + return + } + data = make([]proto.TransactionResp, len(rechangeOrders)) + for i, v := range rechangeOrders { + resp := proto.TransactionResp{ + OrderId: v.OID, + Money: v.Money, + PaymentAt: v.PaymentAt, + CreatedAt: v.CreatedAt, + PayType: v.RechargeType, + Status: v.Status, + } + if resp.Status == 3 { + resp.Money = v.PayMoney + } + if v.BuyType == commod.BuyProduct { + if p := vipProductMap[v.ProductID]; p != nil { + resp.ProductName = p.Name + } + } else { + if p := goldProductMap[v.ProductID]; p != nil { + resp.ProductName = p.Name + } + } + data[i] = resp + } + return +} diff --git a/app/service/rechargeser/type.go b/app/service/rechargeser/type.go new file mode 100644 index 0000000..f6a3964 --- /dev/null +++ b/app/service/rechargeser/type.go @@ -0,0 +1,251 @@ +package rechargeser + +import ( + "91porn-server/app/service/productser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/middleware/ua" + "91porn-server/models/v/advanceordermod" + "errors" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + + "91porn-server/models/commod" + "91porn-server/models/v/currencymod" + "91porn-server/models/v/goldextramod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/usermod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + // Recommended stable values for reporting. The API also accepts custom values. + OrderSourcePageHomeUserSegment = "HOME_USER_SEGMENT" + OrderSourcePageVideoBottomBanner = "VIDEO_BOTTOM_BANNER" + OrderSourcePageVideoBottomSheet = "VIDEO_BOTTOM_SHEET" + OrderSourcePageVIPCenter = "VIP_CENTER" + OrderSourcePageH5Activity = "H5_ACTIVITY" + OrderSourcePageDramaPaywall = "DRAMA_PAYWALL" + OrderSourcePageUnknown = "UNKNOWN" +) + +// NormalizeOrderSourcePage trims sourcePage and maps an empty value to UNKNOWN. +// Non-empty values are intentionally preserved to support new frontend sources +// without requiring a backend allowlist release. +func NormalizeOrderSourcePage(sourcePage string) string { + normalized := strings.TrimSpace(sourcePage) + if normalized == "" { + return OrderSourcePageUnknown + } + return normalized +} + +// CurrencyListRequest 货币列表请求 +type CurrencyListRequest struct { + Type int `form:"type" binding:"required"` //货币类型 1-金币 2-游戏币 3-果币 +} + +// CurrencyListResponse 货币列表回复 + +// RechargeRequest 充值请求 +type RechargeRequest struct { + UID uint64 `swaggerignore:"true"` + IP string `swaggerignore:"true"` + RechargeType string `json:"rechargeType" binding:"required"` // 充值类型 + ProductID primitive.ObjectID `json:"productID" binding:"required"` // 产品ID 如:会员卡ID + ProductChildID primitive.ObjectID `json:"productChildId"` // 产品子ID 如:服务ID + GoldExtraID *primitive.ObjectID `json:"goldExtraID"` // 金币加购券id(仅仅在购买金币时使用) + BuyType commod.BuyType `json:"buyType" binding:"required"` // 购买类型 + SourcePage string `json:"sourcePage"` // 下单来源页面 + SourceRef string `json:"sourceRef"` // 来源业务对象 + VideoID string `json:"videoId"` // 来源视频ID + ActivityID string `json:"activityId"` // 来源活动ID + ExperimentID string `json:"experimentId"` // A/B实验ID + ExperimentVariant string `json:"experimentVariant"` // A/B实验分组 + SessionID string `json:"sessionId"` // 前端会话ID + MediaID primitive.ObjectID `json:"mediaId"` // 短剧ID + ContentID primitive.ObjectID `json:"contentId"` // 短剧单集ID + CheckoutContextID string `json:"checkoutContextId"` // 短剧付费墙上下文 +} + +// ActivityDeduct 活动服下单附加的会员抵扣券信息。 +// 仅由活动服 HMAC 鉴权入口(CreateRechargeOrder)构造并显式传入,与客户端请求体(RechargeRequest)解耦, +// 客户端普通下单(/api/app/mine/topay)传 nil,无从注入抵扣,杜绝自设 deductAmount 低价购卡。 +type ActivityDeduct struct { + CouponID string // 会员抵扣券ID(活动服券号,不透明串) + DeductAmount int64 // 活动服建议抵扣金额(分),本服再自行校验上限后折价 +} + +func (r *RechargeRequest) CreateOrder(u *usermod.User, ua ua.UA, deduct *ActivityDeduct) (*rchgordmod.RechargeOrder, *advanceordermod.AdvanceOrder, error) { + orderSysType := u.SysType + if strings.TrimSpace(ua.SysType) != "" { + orderSysType = ua.SysType + } + order := &rchgordmod.RechargeOrder{ + UID: u.UID, + DevID: u.DevID, + UserIP: r.IP, + DevType: common.HandleSysType(orderSysType), + DiscDoc: u.DiscDoc, + Name: u.Name, + Tel: u.Mobile, + RechargeType: r.RechargeType, + ProductID: r.ProductID, + Channel: "self", + BuyType: r.BuyType, + Ua: ua, + SourcePage: NormalizeOrderSourcePage(r.SourcePage), + SourceRef: r.SourceRef, + VideoID: r.VideoID, + ActivityID: r.ActivityID, + ExperimentID: r.ExperimentID, + ExperimentVariant: r.ExperimentVariant, + SessionID: r.SessionID, + MediaID: r.MediaID, + ContentID: r.ContentID, + CheckoutContextID: r.CheckoutContextID, + } + var advanceOrder advanceordermod.AdvanceOrder + if r.GoldExtraID != nil && !(*r.GoldExtraID).IsZero() { + if r.BuyType != commod.BuyGold { + return nil, nil, errors.New("金币加购券只能在购买金币时使用") + } + goldExtra, err := goldextramod.GetUserGoldExtraByID(nil, *r.GoldExtraID) + if err != nil { + return nil, nil, err + } + if goldExtra.Uid != u.UID || goldExtra.Used || goldExtra.Expired.Before(time.Now()) { + return nil, nil, errors.New("无效的金币加购券") + } + order.GoldExtraID = r.GoldExtraID + } + if r.RechargeType == "daichong" { + order.RechargeType = "alipay" + } + switch r.BuyType { + case commod.BuyGold, commod.BuyFruitCoin, commod.BuyGameCoin: + // 查询货币信息 + currency, err := currencymod.Get(r.ProductID) + if err != nil { + return nil, nil, err + } + order.Money = currency.Price + order.Amount = currency.Coins + if r.BuyType == commod.BuyGameCoin { + order.ProductType = 1 + } + order.ProductName = currency.Name + case commod.BuyProduct: + // 查询会员卡信息 + product, err := productmod.FindProduct(r.ProductID, order.DevType) + if err != nil { + return nil, nil, err + } + // 用户会员卡升级检查 + productser.CheckUserUpgrade(u.UID, product) + + order.Amount = product.DiscountedPrice + order.Money = product.DiscountedPrice * 10 + order.VipID = r.ProductID + order.ProductName = product.Name + if product.ProductType == commod.AdvanceCard { + data, err := advanceordermod.IsExist(bson.M{"productID": r.ProductID, "uid": u.UID}) + if err != nil { + return nil, nil, errors.New("advance order query IsExist err") + } + if data == nil || data.ID.IsZero() { + prepaidPrivilege := advanceordermod.AdvanceCardPrepaidPrivilege{} + if product.PrepaidPrivilege != nil { + prepaidPrivilege = advanceordermod.AdvanceCardPrepaidPrivilege{ + CoinVideoLimitPerDay: product.PrepaidPrivilege.CoinVideoLimitPerDay, + LuckyDrawLimitPerDay: product.PrepaidPrivilege.LuckyDrawLimitPerDay, + AiUndressLimitPerDay: product.PrepaidPrivilege.AiUndressLimitPerDay, + DownloadLimitPerDay: product.PrepaidPrivilege.DownloadLimitPerDay, + } + } + // 没有创建预售订单,创建预售订单(首付状态) + order.Amount = product.AdvanceAmount + order.Money = product.AdvanceAmount * 10 + // 创建预售订单 + now := time.Now() + advanceOrder.UID = u.UID + advanceOrder.CreatedAt = now + advanceOrder.Status = advanceordermod.AdvanceProcessing + advanceOrder.AdvanceAmount = product.AdvanceAmount * 10 + advanceOrder.BalanceAmount = product.BalanceAmount * 10 + advanceOrder.StartTime = product.StartTime + advanceOrder.EndTime = product.EndTime + advanceOrder.ProductID = product.ID + advanceOrder.TotalAmount = (product.AdvanceAmount + product.BalanceAmount) * 10 + advanceOrder.PrepaidPrivilege = prepaidPrivilege + advanceOrder.TodayUse = advanceordermod.DayUse{} + } else if data.Status == advanceordermod.AdvanceProcessing { + // 有预售订单,且是首付状态 + order.Amount = product.AdvanceAmount + order.Money = product.AdvanceAmount * 10 + + data.AdvanceAmount = product.AdvanceAmount * 10 + data.TotalAmount = (product.AdvanceAmount + product.BalanceAmount) * 10 + advanceOrder = *data + } else if (data.Status == advanceordermod.AdvanceSUCCESS && time.Now().After(product.StartTime)) || data.Status == advanceordermod.BalanceProcessing { + // 尾款预付状态 + order.Amount = product.BalanceAmount + order.Money = product.BalanceAmount * 10 + advanceOrder = *data + } else if data.Status == advanceordermod.AdvanceSUCCESS && !time.Now().After(product.StartTime) { + log.Error("RechargeRequest.CreateOrder 支付预售订单失败,未到尾款支付时间", log.Any("advanceOrder.Status", data.Status), log.Any("product", product), log.E(err)) + return nil, nil, errors.New("advance order status is err") + } else if data.Status == advanceordermod.BalanceSUCCESS { + log.Error("RechargeRequest.CreateOrder 支付预售订单失败,该订单已经预售支付完成", log.Any("advanceOrder.Status", data.Status), log.Any("product", product), log.E(err)) + return nil, nil, errors.New("advance order is paid") + } + advanceOrder.ProductID = product.ID + } + // 会员抵扣券支付抵扣:折后价写入 order.Money,couponId 持久化到订单 + applyDeductCoupon(order, deduct, product) + + switch product.ProductType { + case commod.VIP, commod.MeetingCard, commod.AdvanceCard, commod.GameAdvanceCard, + commod.NEWUSERCard, commod.PhysicalGoods, commod.VideoDiscount, commod.VideoFreeCard, commod.CoinMonthCard: + order.Category = 1 + default: + order.Category = 0 + } + } + return order, &advanceOrder, nil +} + +// minChannelPayFen 支付渠道最低可支付金额(分),抵扣后实付不得低于此值 +const minChannelPayFen = 1 + +// applyDeductCoupon 应用会员抵扣券支付抵扣。 +// deduct 由活动服入口传入(客户端下单为 nil);其 DeductAmount 仅为建议值,不可无条件信任,须自行封顶: +// +// 实际抵扣 = min(DeductAmount, 原价 - 最低实付),折后实付 >= 渠道最低可支付金额。 +// +// 仅普通会员卡(buyType=商品且非预售卡)生效;预售卡分阶段付款不参与抵扣。 +// 折后价写回 order.Money(下单/回调均以此为准),couponId 与实际抵扣额持久化到订单, +// 供支付结果回调向活动服核销时原样回传。 +func applyDeductCoupon(order *rchgordmod.RechargeOrder, deduct *ActivityDeduct, product *productmod.Product) { + if deduct == nil || deduct.CouponID == "" || deduct.DeductAmount <= 0 { + return + } + if product.ProductType == commod.AdvanceCard { + return + } + maxDeduct := order.Money - minChannelPayFen + if maxDeduct <= 0 { + return + } + amount := deduct.DeductAmount + if amount > maxDeduct { + amount = maxDeduct + } + order.Money -= amount + order.CouponID = deduct.CouponID + order.DeductAmount = amount +} diff --git a/app/service/rechargeser/type_attribution_test.go b/app/service/rechargeser/type_attribution_test.go new file mode 100644 index 0000000..ca483fb --- /dev/null +++ b/app/service/rechargeser/type_attribution_test.go @@ -0,0 +1,204 @@ +package rechargeser + +import ( + "strings" + "testing" + + "91porn-server/middleware/ua" + "91porn-server/models/commod" + "91porn-server/models/v/usermod" +) + +func TestNormalizeOrderSourcePage(t *testing.T) { + tests := []struct { + name string + sourcePage string + want string + }{ + {name: "canonical", sourcePage: OrderSourcePageVideoBottomSheet, want: OrderSourcePageVideoBottomSheet}, + {name: "trim arbitrary source", sourcePage: " custom_Page-01 ", want: "custom_Page-01"}, + {name: "empty", sourcePage: "", want: OrderSourcePageUnknown}, + {name: "whitespace", sourcePage: " ", want: OrderSourcePageUnknown}, + {name: "previously unsupported", sourcePage: "OTHER", want: "OTHER"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NormalizeOrderSourcePage(tt.sourcePage); got != tt.want { + t.Fatalf("NormalizeOrderSourcePage(%q) = %q, want %q", tt.sourcePage, got, tt.want) + } + }) + } +} + +func TestRechargeRequestCreateOrderCopiesAttribution(t *testing.T) { + request := &RechargeRequest{ + SourcePage: " custom_source_page ", + SourceRef: "source-ref", + VideoID: "video-id", + ActivityID: "activity-id", + ExperimentID: "experiment-id", + ExperimentVariant: "A", + SessionID: "session-id", + } + + order, _, err := request.CreateOrder(&usermod.User{UID: 123}, ua.UA{}, nil) + if err != nil { + t.Fatalf("CreateOrder() error = %v", err) + } + if order.SourcePage != "custom_source_page" { + t.Fatalf("SourcePage = %q, want custom_source_page", order.SourcePage) + } + if order.SourceRef != request.SourceRef || + order.VideoID != request.VideoID || + order.ActivityID != request.ActivityID || + order.ExperimentID != request.ExperimentID || + order.ExperimentVariant != request.ExperimentVariant || + order.SessionID != request.SessionID { + t.Fatalf("order attribution fields were not copied: %+v", order) + } + + legacyOrder, _, err := (&RechargeRequest{}).CreateOrder(&usermod.User{UID: 456}, ua.UA{}, nil) + if err != nil { + t.Fatalf("legacy CreateOrder() error = %v", err) + } + if legacyOrder.SourcePage != OrderSourcePageUnknown { + t.Fatalf("legacy SourcePage = %q, want %q", legacyOrder.SourcePage, OrderSourcePageUnknown) + } +} + +func TestRechargeRequestCreateOrderUsesCurrentRequestSysType(t *testing.T) { + tests := []struct { + name string + userSysType string + requestSysType string + want string + }{ + {name: "android request overrides historical ios", userSysType: "ios", requestSysType: "android", want: "android"}, + {name: "ios request overrides historical android", userSysType: "android", requestSysType: "IOS-18", want: "ios"}, + {name: "missing request falls back to user", userSysType: "ios", want: "ios"}, + {name: "missing request and user use default", want: "android"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + order, _, err := (&RechargeRequest{}).CreateOrder( + &usermod.User{UID: 123, SysType: tt.userSysType}, + ua.UA{SysType: tt.requestSysType}, + nil, + ) + if err != nil { + t.Fatalf("CreateOrder() error = %v", err) + } + if order.DevType != tt.want { + t.Fatalf("DevType = %q, want %q", order.DevType, tt.want) + } + }) + } +} + +func TestNormalizeAttributionPreservesNonExperimentSession(t *testing.T) { + request := RechargeRequest{ + SourcePage: " H5_ACTIVITY ", + ActivityID: " activity-1 ", + ExperimentVariant: "A", + SessionID: " session-1 ", + } + if err := request.validateAttribution(123); err != nil { + t.Fatalf("validateAttribution() error = %v", err) + } + if request.SourcePage != OrderSourcePageH5Activity { + t.Fatalf("SourcePage = %q, want %q", request.SourcePage, OrderSourcePageH5Activity) + } + if request.ActivityID != "activity-1" { + t.Fatalf("ActivityID = %q", request.ActivityID) + } + if request.ExperimentVariant != "" { + t.Fatalf("ExperimentVariant = %q, want empty without experimentId", request.ExperimentVariant) + } + if request.SessionID != "session-1" { + t.Fatalf("SessionID = %q, want session-1", request.SessionID) + } +} + +func TestValidateAttributionSourcePageLength(t *testing.T) { + request := RechargeRequest{SourcePage: strings.Repeat("页", maxAttributionValueLength)} + if err := request.validateAttribution(123); err != nil { + t.Fatalf("validateAttribution() rejected 128 characters: %v", err) + } + + request.SourcePage = strings.Repeat("页", maxAttributionValueLength+1) + if err := request.validateAttribution(123); err == nil { + t.Fatal("validateAttribution() should reject sourcePage longer than 128 characters") + } +} + +func TestValidateAttributionDramaPaywallGoldRechargeAllowsSourceOnly(t *testing.T) { + request := RechargeRequest{ + BuyType: commod.BuyGold, + SourcePage: OrderSourcePageDramaPaywall, + } + + if err := request.validateAttribution(123); err != nil { + t.Fatalf("validateAttribution() rejected drama paywall gold recharge: %v", err) + } +} + +func TestValidateAttributionDramaPaywallProductAllowsNoContext(t *testing.T) { + request := RechargeRequest{ + BuyType: commod.BuyProduct, + SourcePage: OrderSourcePageDramaPaywall, + } + + if err := request.validateAttribution(123); err != nil { + t.Fatalf("validateAttribution() rejected drama product without context: %v", err) + } +} + +func TestValidateAttributionDramaPaywallProductAllowsPartialContext(t *testing.T) { + request := RechargeRequest{ + BuyType: commod.BuyProduct, + SourcePage: OrderSourcePageDramaPaywall, + SourceRef: "legacy-client-value", + SessionID: "session-1", + CheckoutContextID: "checkout-context-1", + } + + if err := request.validateAttribution(123); err != nil { + t.Fatalf("validateAttribution() rejected partial drama context: %v", err) + } + if request.SourceRef != request.CheckoutContextID { + t.Fatalf("SourceRef = %q, want checkoutContextId %q", request.SourceRef, request.CheckoutContextID) + } +} + +func TestValidateAttributionDramaPaywallSessionDoesNotRequireContext(t *testing.T) { + request := RechargeRequest{ + BuyType: commod.BuyProduct, + SourcePage: OrderSourcePageDramaPaywall, + SourceRef: "legacy-client-value", + SessionID: "session-1", + } + + if err := request.validateAttribution(123); err != nil { + t.Fatalf("validateAttribution() rejected experiment session without drama context: %v", err) + } + if request.SourceRef != "" { + t.Fatalf("SourceRef = %q, want empty without checkoutContextId", request.SourceRef) + } +} + +func TestValidateAttributionDramaPaywallIgnoresOversizedCheckoutContext(t *testing.T) { + request := RechargeRequest{ + BuyType: commod.BuyProduct, + SourcePage: OrderSourcePageDramaPaywall, + CheckoutContextID: strings.Repeat("x", maxAttributionValueLength+1), + } + + if err := request.validateAttribution(123); err != nil { + t.Fatalf("validateAttribution() rejected oversized optional drama context: %v", err) + } + if request.CheckoutContextID != "" || request.SourceRef != "" { + t.Fatalf("optional drama context was not cleared: checkout=%q sourceRef=%q", request.CheckoutContextID, request.SourceRef) + } +} diff --git a/app/service/rechargeser/vip_attribution.go b/app/service/rechargeser/vip_attribution.go new file mode 100644 index 0000000..f5c34d9 --- /dev/null +++ b/app/service/rechargeser/vip_attribution.go @@ -0,0 +1,93 @@ +package rechargeser + +import ( + "fmt" + "strings" + "unicode/utf8" + + "91porn-server/models/commod" + "91porn-server/models/v/vipcardexperimentmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const maxAttributionValueLength = 128 + +func (r *RechargeRequest) normalizeAttribution() { + r.SourcePage = NormalizeOrderSourcePage(r.SourcePage) + r.SourceRef = strings.TrimSpace(r.SourceRef) + r.VideoID = strings.TrimSpace(r.VideoID) + r.ActivityID = strings.TrimSpace(r.ActivityID) + r.ExperimentID = strings.TrimSpace(r.ExperimentID) + r.ExperimentVariant = strings.ToUpper(strings.TrimSpace(r.ExperimentVariant)) + r.SessionID = strings.TrimSpace(r.SessionID) + r.CheckoutContextID = strings.TrimSpace(r.CheckoutContextID) +} + +func (r *RechargeRequest) validateAttribution(uid uint64) error { + r.normalizeAttribution() + if utf8.RuneCountInString(r.SourcePage) > maxAttributionValueLength { + return fmt.Errorf("sourcePage must not exceed %d characters", maxAttributionValueLength) + } + if r.SourcePage == OrderSourcePageDramaPaywall { + if r.BuyType != commod.BuyGold && r.BuyType != commod.BuyProduct { + return fmt.Errorf("buyType must be 1 or 4 for DRAMA_PAYWALL") + } + // sourceRef is the generic order attribution field. For drama paywall + // orders, derive it from the backend-issued checkout context instead of + // requiring clients to submit the same identifier twice. The remaining + // drama fields are attribution-only and must not block a real payment. + if len(r.CheckoutContextID) > maxAttributionValueLength { + r.CheckoutContextID = "" + } + r.SourceRef = r.CheckoutContextID + } + values := map[string]string{ + "sourceRef": r.SourceRef, + "videoId": r.VideoID, + "activityId": r.ActivityID, + "experimentId": r.ExperimentID, + "experimentVariant": r.ExperimentVariant, + "sessionId": r.SessionID, + "checkoutContextId": r.CheckoutContextID, + } + for name, value := range values { + if len(value) > maxAttributionValueLength { + return fmt.Errorf("%s must not exceed %d characters", name, maxAttributionValueLength) + } + } + if r.ExperimentID == "" { + r.ExperimentVariant = "" + return nil + } + if r.ExperimentVariant == "" || r.SessionID == "" { + return fmt.Errorf("experimentVariant and sessionId are required with experimentId") + } + experiment, err := vipcardexperimentmod.FindByExperimentID(r.ExperimentID) + if err != nil { + return err + } + if experiment == nil { + return fmt.Errorf("experiment does not exist") + } + config, ok := experiment.ConfigFor(r.ExperimentVariant) + if !ok { + return fmt.Errorf("experimentVariant must be A or B") + } + if assigned := experiment.Assign(uid); assigned != r.ExperimentVariant { + return fmt.Errorf("experimentVariant does not match user assignment") + } + if !containsObjectID(config.ProductIDs, r.ProductID) { + return fmt.Errorf("productID does not belong to experiment variant") + } + return nil +} + +func containsObjectID(ids []primitive.ObjectID, target primitive.ObjectID) bool { + for _, id := range ids { + if id == target { + return true + } + } + return false +} diff --git a/app/service/recommser/recommser.go b/app/service/recommser/recommser.go new file mode 100644 index 0000000..6aed1ee --- /dev/null +++ b/app/service/recommser/recommser.go @@ -0,0 +1,423 @@ +package recommser + +import ( + "91porn-server/common/redis" + "91porn-server/models/v/recmdtag" + "context" + "encoding/json" + "errors" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" + "math" + "math/rand" + "strconv" + "sync" + "time" + + "91porn-server/app/appg" + "91porn-server/app/proto" + "91porn-server/app/service/adser" + "91porn-server/app/service/shortrecommendser" + "91porn-server/app/service/vidhelpser" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/recommod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/userrecomod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetVidList 获取推荐视频列表 +func GetVidList(uid, size uint64) (code stderr.Code, data recommod.VideoListResp) { + // 无客户端请求ID时不启用业务幂等缓存;该入口保留给内部兼容调用。 + return GetVidListContext(context.Background(), uid, size, "") +} + +// GetVidListContext 获取推荐视频列表,并将请求取消、超时和幂等ID传递到环形队列。 +func GetVidListContext( + ctx context.Context, + uid, size uint64, + requestID string, +) (code stderr.Code, data recommod.VideoListResp) { + if ctx == nil { + ctx = context.Background() + } + if size == 0 { + return stderr.Success, data + } + if !appg.Conf.ShortRecommendEnabled() { + return getRandomVidList(uid, size) + } + fetchCtx, cancel := context.WithTimeout(ctx, appg.Conf.ShortRecommendRequestTimeout()) + defer cancel() + result, err := shortrecommendser.FetchScoped( + fetchCtx, uid, int(size), requestID, "recommend-list", + ) + if err != nil && result.QueueVersion == "" { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + log.Warn("short recommend fetch canceled", + log.E(err), log.Any("uid", uid)) + return stderr.Success, data + } + log.Error("short recommend fetch failed, fallback random set", + log.E(err), log.Any("uid", uid)) + return getRandomVidList(uid, size) + } + if result.QueueVersion == "" { + return getRandomVidList(uid, size) + } + if err != nil && len(result.Videos) == 0 { + log.Error("short recommend fetch failed after queue reservation", + log.E(err), + log.Any("uid", uid), + log.Any("queueVersion", result.QueueVersion), + log.Any("scanned", result.Scanned), + log.Any("batches", result.Batches)) + return stderr.ErrDbQueryError, data + } + if err != nil || result.BudgetExceeded || result.Filtered > 0 { + log.Warn("short recommend fetch completed with filtering", + log.E(err), + log.Any("uid", uid), + log.Any("queueVersion", result.QueueVersion), + log.Any("queueLength", result.QueueLength), + log.Any("requested", size), + log.Any("returned", len(result.Videos)), + log.Any("scanned", result.Scanned), + log.Any("filtered", result.Filtered), + log.Any("batches", result.Batches), + log.Any("budgetExceeded", result.BudgetExceeded)) + } + data.QueueVersion = result.QueueVersion + data.TotalPages = int(math.Ceil(float64(result.QueueLength) / float64(size))) + data.HasNext = result.QueueLength > 0 + data.VInfos = vidhelpser.EncodeVideoInfoForRecommend(uid, result.Videos) + return stderr.Success, data +} + +func getRandomVidList(uid, size uint64) (code stderr.Code, data recommod.VideoListResp) { + totalSize, err := appg.Redis.SCard(redisconst.ShortVideosKey) + if err != nil { + log.Error("GetVidList SCard err", log.E(err)) + return stderr.Success, data + } + if totalSize == 0 { + return stderr.Success, data + } + + if totalSize <= int64(size) { + data.TotalPages = 1 + } else { + data.TotalPages = int(math.Ceil(float64(totalSize) / float64(size))) + } + + sampleSize := int64(size * 5) + if sampleSize > totalSize { + sampleSize = totalSize + } + pushVid, err := redis.Handler.SRandMemberN(redisconst.ShortVideosKey, sampleSize) + if err != nil { + return stderr.Success, data + } + + voidList := common.String2ObjectID(pushVid) + videos, err := vidmod.GetVideosByIDs(voidList) + if err != nil { + return stderr.Success, data + } + excluded, err := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), true) + if err != nil { + log.Error("GetVidList excluded modules err", log.E(err)) + return stderr.Success, data + } + excludedSet := make(map[string]struct{}, len(excluded)) + for _, moduleID := range excluded { + excludedSet[moduleID] = struct{}{} + } + allowedIDs := make([]primitive.ObjectID, 0, size) + for _, video := range videos { + if video == nil { + continue + } + if _, blocked := excludedSet[video.MID]; blocked { + continue + } + allowedIDs = append(allowedIDs, video.ID) + if uint64(len(allowedIDs)) == size { + break + } + } + data.VInfos = vidhelpser.GetVideosByIDs(uid, allowedIDs) + data.HasNext = totalSize > 0 + + return stderr.Success, data +} + +// 获取视频广告 +func GetAd(uid uint64) []*vidmod.VideoInfo { + adVids := make([]*vidmod.VideoInfo, 0) + key := redisconst.VidAdKey + if err := appg.Redis.GetObj(&adVids, key); err != nil { + log.Warn("GetAd getAdNews Redis GetObj err", log.Any("key", key), log.E(err)) + } + if len(adVids) > 0 { + return adVids + } + adnews := vidmod.GetAdVideoIdsList(100) + log.Info("GetAd getAdNews ", log.Any("info", adnews), log.Any("uid", uid)) + if len(adnews) == 0 { + log.Warn("GetAd getAdNews result is null", log.Any("uid", uid)) + return adVids + } + adVids = vidhelpser.GetVideosByIDs(uid, common.String2ObjectID(adnews)) + log.Info("GetAd GetVideosByIDs ", log.Any("adVids", adVids), log.Any("uid", uid)) + common.Go(func() { + b, err := json.Marshal(adVids) + if err != nil { + return + } + _ = appg.Redis.Set(key, b, redisconst.VidAdKeyExpire) + }) + return adVids +} + +func encodeUserList(uids []uint64, mInfo map[uint64]*vidmod.Publisher, mColle map[uint64]int) []recommod.BaseInfo { + datas := make([]recommod.BaseInfo, len(uids)) + for i, v := range uids { + var name, gender, portrait string + var hasLocked, hasBanned bool + if mInfo[v] != nil { + name = mInfo[v].Name + gender = mInfo[v].Gender + portrait = mInfo[v].Portrait + hasLocked = mInfo[v].HasLocked + hasBanned = mInfo[v].HasBanned + } + datas[i] = recommod.BaseInfo{ + UID: v, + Name: name, + Gender: gender, + Portrait: portrait, + HasLocked: hasLocked, + HasBanned: hasBanned, + CollectionCount: mColle[v], + } + } + return datas +} + +// GetUserList 获取推荐用户列表 +func GetUserList(uid uint64, page, size uint64) (code stderr.Code, data interface{}) { + uInfo, _, err := userrecomod.GetUserReco(page, size, false) + if err != nil { + return stderr.ErrDbQueryError, nil + } + uids := make([]uint64, len(uInfo)) + for i, v := range uInfo { + uids[i] = v.UID + } + var wg sync.WaitGroup + wg.Add(2) + mInfo := make(map[uint64]*vidmod.Publisher) + mColle := make(map[uint64]int) + common.Go(func() { + defer wg.Done() + mInfo = vidhelpser.GetUserInfo2Map(uid, uids) + }) + common.Go(func() { + defer wg.Done() + mColle, _ = vidmod.GetHisVideoCount(uids) + }) + wg.Wait() + list := encodeUserList(uids, mInfo, mColle) + data = recommod.UserListResp{List: list, HasNext: true} + return stderr.Success, data +} + +var cachlightVideoResponse *proto.LightVideoRes + +// GetLightVidList 获取轻量视频(无用户uid) +func GetLightVidList(ip string) (code stderr.Code, data interface{}) { + code = stderr.Failure + excludedModules, err := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), true) + if err != nil { + log.Error("GetLightVidList excluded modules err", log.E(err)) + return + } + if cachlightVideoResponse == nil || len(excludedModules) > 0 || rand.Intn(100) == 50 { //随机更新缓存 + hot, err := vidmod.HotVideo() + if err != nil { + return + } + vids := make([]primitive.ObjectID, len(hot)) + for i, v := range hot { + vids[i] = v.ID + } + tags, _, err := tagmod.GetTagListMostPlayed(commod.Page{PageNumber: 1, PageSize: 10, Sort: nil}) + if err != nil { + return + } + resVid := make([]*vidmod.VideoModel, 0) + for i := range tags { + longVid, err := vidmod.VideoListByTagIDAndPlayTime(tags[i].ID, 1, vids) + if err != nil { + continue + } + for _, v := range longVid { + vids = append(vids, v.ID) + } + resVid = append(resVid, longVid...) + shortVid, err := vidmod.VideoListByTagIDAndPlayTime(tags[i].ID, 2, vids) + if err != nil { + continue + } + for _, v := range shortVid { + vids = append(vids, v.ID) + } + resVid = append(resVid, shortVid...) + } + resp, err := adser.AdvertiseThreeServer(0, ip) + if err != nil { + log.Error("AdvertiseThreeServer", log.E(err)) + return + } + ads := make([]*proto.AdsInfo, len(resp.AdvertiseList)) + for i, v := range resp.AdvertiseList { + ads[i] = &proto.AdsInfo{ + ID: strconv.FormatInt(v.Id, 10), + Title: v.Title, + Cover: v.CoverImg, + Href: v.Link, + Position: int(v.LocId), + SortCode: int(v.Sort), + } + } + cachlightVideoResponse = &proto.LightVideoRes{ + HomeVideo: vidhelpser.EncodeVideoInfoNoUIDForRecommend(hot), + ZoneVideo: vidhelpser.EncodeVideoInfoNoUIDForRecommend(resVid), + ADs: ads, + } + } + return stderr.Success, cachlightVideoResponse +} + +func GetShortDiscoverList(uid uint64, req *recmdtag.AppGetShortDiscoverListReq) (resp recmdtag.AppGetShortDiscoverListRep, code stderr.Code, err error) { + + var tmpData struct { + VidList []*vidmod.VideoModel `json:"vidList"` + TagList []*recmdtag.TagInfo `json:"tagList"` + HasNext bool `json:"hasNext"` + Loaded bool `json:"-"` + } + + recommendScene := req.Type != 2 + excludedModules, excludeErr := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), recommendScene) + if excludeErr != nil { + return resp, stderr.ErrDbQueryError, excludeErr + } + useCache := len(excludedModules) == 0 + key := redisconst.GetShortVideoListCacheKey(req.Type, req.TagID, req.PageSize, req.PageNumber) + if useCache { + str, cacheErr := appg.Redis.Get(key) + if cacheErr != nil { + log.Error("service GetShortVideoList get cache error:", log.E(cacheErr)) + } + if str != nil { + if cacheErr = json.Unmarshal([]byte(*str), &tmpData); cacheErr != nil { + log.Error("service GetShortVideoList cache unmarshal error:", log.E(cacheErr)) + } else { + tmpData.Loaded = true + } + } + } + + if !tmpData.Loaded { + // 从数据库查询 + var ( + filter = bson.M{"status": vidmod.CheckPass, "newsType": vidmod.SHORT} + sort bson.D + ) + // 1-最热 2-最新 3-推荐 4 热卖 5 指定tag + switch req.Type { + case 2: // 最新 + if req.PageNumber > 100 { + return resp, stderr.Success, nil + } + filter["reviewAt"] = bson.M{"$gte": time.Now().AddDate(-1, 0, 0)} + sort = bson.D{{Key: "reviewAt", Value: -1}} + case 3: // 推荐 + filter["reviewAt"] = bson.M{"$gte": time.Now().AddDate(0, 0, -30)} + sort = bson.D{{"chosen", -1}, {Key: "likeCount", Value: -1}, {"reviewAt", -1}} + case 4: // 热卖 + filter["coins"] = bson.M{"$gt": 0} + filter["freeArea"] = false + sort = bson.D{{Key: "purchaseCount", Value: -1}} + case 5: // 指定标签 + tid, err := primitive.ObjectIDFromHex(req.TagID) + if err != nil { + return resp, stderr.ErrDbQueryError, err + } + filter["tags"] = tid + sort = bson.D{{Key: "likeCount", Value: -1}, {"reviewAt", -1}} + case 1: // 热门推荐 + fallthrough + default: + if req.PageNumber == 1 { //获取推荐标签列表 + var s, k int64 = 0, 12 + //var t = recmdtag.KindShortVideo + var t = recmdtag.Def + match := recmdtag.TypesMatch{Types: &t} + tags, err := recmdtag.List(recmdtag.Sort_sortCode_N1, &s, &k, match.New()) + + if err != nil { + return resp, stderr.ErrDbQueryError, err + } + for _, tag := range tags { + tmpData.TagList = append(tmpData.TagList, &recmdtag.TagInfo{ + ID: tag.TID, + Name: tag.Name, + }) + } + } + filter["reviewAt"] = bson.M{"$gt": time.Now().AddDate(0, -3, 0)} + sort = bson.D{{Key: "likeCount", Value: -1}, {"reviewAt", -1}} + } + if len(excludedModules) > 0 { + filter["mId"] = bson.M{"$nin": excludedModules} + } + + opt := options.Find().SetSkip(int64(req.Skip())).SetLimit(int64(req.Limit())).SetSort(sort) + tmpData.VidList, tmpData.HasNext, err = vidmod.FindList(filter, opt) + if err != nil { + return resp, stderr.ErrDbQueryError, err + } + + if useCache { + b, marshalErr := json.Marshal(tmpData) + if marshalErr != nil { + log.Error("service GetShortVideoList cache json marshal error:", log.E(marshalErr)) + return resp, stderr.Failure, marshalErr + } + _ = appg.Redis.Set(key, string(b), redisconst.ShortVideoListCacheExpire) + } + } + + if len(tmpData.VidList) > 0 { + if recommendScene { + resp.Videos = vidhelpser.EncodeVideoInfoForRecommend(uid, tmpData.VidList) + } else { + resp.Videos = vidhelpser.EncodeVideoInfo(uid, tmpData.VidList) + } + } + + resp.Tags = tmpData.TagList + resp.HasNext = tmpData.HasNext + + return resp, stderr.Success, nil +} diff --git a/app/service/search/search.go b/app/service/search/search.go new file mode 100644 index 0000000..f2e8833 --- /dev/null +++ b/app/service/search/search.go @@ -0,0 +1,203 @@ +package search + +import ( + "91porn-server/app/proto" + "91porn-server/app/service/searcher" + "91porn-server/app/service/searcher/tagsearcher" + "91porn-server/app/service/searcher/tonesearcher" + "91porn-server/app/service/searcher/usersearcher" + "91porn-server/app/service/searcher/vidhkwsearcher" + "91porn-server/app/service/searcher/vidhotsearcher" + "91porn-server/app/service/searcher/vidpcountsearcher" + "91porn-server/app/service/searcher/vidrichsearcher" + "91porn-server/app/service/searcher/vidsearcher" + "91porn-server/app/service/vidhelpser" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + sli "91porn-server/common/slice" + "91porn-server/models/commod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/vidmod" + "errors" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" + "strings" + "sync" + "time" +) + +type ( + NewsKeywordSearchReq struct { + Realm constant.RealmType `form:"realm" json:"realm" binding:"required"` // SP:长视频 SHORT:短视频 COVER:图文帖子 PIC:图集帖子 SEED_LINK:种子/黄油帖子 + KeyWords []string `form:"keyWords" json:"keyWords" binding:"required"` // 关键字 + SortType int `form:"sortType" json:"sortType"` // 1最多观看 2最新上架 3最多收藏 + commod.Page + } + NewsKeywordSearchRep struct { + List any `json:"list"` // 数据列表(ACG和帖子不同格式) + TagID string `json:"tagID"` // 帖子搜索时附带的tagID + TagVidList []*vidmod.VideoInfo `json:"tagVidList"` // 搜索时tag相关的帖子 + HasNext bool `json:"hasNext"` // 是否还有下一页 + } +) + +func (req *NewsKeywordSearchReq) Search(uid uint64) (rep NewsKeywordSearchRep, err error) { + + var search searcher.Searcher + switch req.Realm { + case constant.SearchComplex: + search = vidrichsearcher.NewVidRichSearcher(uid) + case constant.SearchUser: + search = usersearcher.NewUserSearcher(uid) + case constant.SearchSP, constant.SearchShort, constant.SearchCover, constant.SearchPic: + search = vidsearcher.NewVidSearcher(uid, req.Realm, req.SortType) + case constant.SearchTag: + search = tagsearcher.NewTagSearcher(&uid) + default: + return rep, errors.New("search Realm is invaild") + } + + kw := &searcher.Keyword{Word: strings.Join(req.KeyWords, "|")} + opt := (&searcher.Option{}).SetSkip(req.Skip64()).SetLimit(req.Limit64()) + result, err := search.Search(kw, opt) + if err != nil { + return rep, err + } + + rep.List = result.Data() + rep.HasNext = result.HasNext() + + if req.Realm == constant.SearchSP || req.Realm == constant.SearchShort { + // 额外获取TAG相关信息 + tagId, err := tagmod.GetTagIDByName(req.KeyWords[0]) + if err != nil || tagId.IsZero() { + return rep, nil + } + // 6-最多收藏 + limit := int64(4) + if req.Realm == constant.SearchShort { + limit = 6 + } + filter := bson.M{"tags": tagId, "status": 1, "newsType": req.Realm} + sort := bson.D{{"collectCount", -1}, {"reviewAt", -1}} + opts := options.Find().SetLimit(limit).SetSort(sort) + + tagVideos, _, err := vidmod.FindList(filter, opts) + if err != nil { + return rep, err + } + rep.TagID = tagId.Hex() + rep.TagVidList = vidhelpser.EncodeVideoInfoForSearch(uid, tagVideos) + } + return rep, nil +} + +// GetHome +// 热点视屏 通过hOpt设置 +// 最热视屏 通过hsOpt设置 +// 音色入口 固定7个 +func GetHome(uid uint64, hOpt, hsOpt searcher.Opter) Home { + //热点 + vidHKWSearcher := vidhkwsearcher.NewVidHKWSearcher(uid, (&searcher.Option{}).SetLimit(1)) + //最热视屏 + vidHotSearcher := vidhotsearcher.NewVidHotSearcher(uid) + //音色 + toneSearcher := tonesearcher.NewToneSearcher() + now := time.Now() + var ( + home Home + sumDate = searcher.NewSumDate(now) + hVidRes, hsVidRes, toneRes searcher.Resulter + wg = sync.WaitGroup{} + ) + wg.Add(3) + common.Go(func() { //热点 + defer wg.Done() + res, err := vidHKWSearcher.Search(sumDate, hOpt) + if err != nil { + log.Error("vidHKWSearcher search faild!", log.E(err)) + return + } + hVidRes = res + if res.Count() >= hOpt.Limit() { //查找出来的视屏数量足够 不用替换视屏 + return + } + //查找出来的视屏数量不够,用最多播放视屏替换 + res, err = vidpcountsearcher.NewVidPlayCountSearcher(uid).Search(nil, hOpt) + if err != nil { + log.Error("vidpcountsearcher search faild!", log.E(err)) + return + } + hVidRes = res + }) + common.Go(func() { //最热视屏 + defer wg.Done() + res, err := vidHotSearcher.Search(nil, hsOpt) + if err != nil { + log.Error("vidHotSearcher search faild!", log.E(err)) + //失败 返回原始搜索结果 + return + } + hsVidRes = res + }) + common.Go(func() { //音色 + defer wg.Done() + res, err := toneSearcher.Search(nil, nil) + if err != nil { + log.Error("toneSearcher search faild!", log.E(err)) + //失败 返回原始搜索结果 + return + } + toneRes = res + }) + wg.Wait() + //热点 + if hVidRes != nil { + home.HVidList = hVidRes.Data().([]vidhkwsearcher.VideoRes) + } + //今日最热视屏 + if hsVidRes != nil { + home.HSVidList = hsVidRes.Data().([]vidhotsearcher.VideoRes) + } + //音色 + if toneRes != nil { + home.ToneList = toneRes.Data().([]tonesearcher.ToneRes) + } + return home +} + +func GetWonderTagList(skip, limit int64) ([]vidmod.TagInfo, bool, error) { + limitEx1 := limit + 1 + tags, err := tagmod.WonderTags(skip, limitEx1) + if err != nil { + return nil, false, err + } + hasNext := false + if len(tags) > int(limit) { + hasNext = true + tags = tags[:limit] + } + log.Debug("GetWonderTagList", log.Any("skip", skip), log.Any("limit", limit), log.Any("hasNext", hasNext)) + return tags, hasNext, nil +} + +func GetHotTag() ([]interface{}, error) { + tags, _, err := tagmod.GetTagListMostPlayed(commod.Page{ + PageNumber: 1, + PageSize: 10, + }) + if err != nil { + return nil, err + } + data := make([]interface{}, len(tags)) + for i := range tags { + data[i] = proto.Tag{ + ID: tags[i].ID, + Name: tags[i].TagName, + Hot: tags[i].TPlayCount, + } + } + sli.Shuffle(data) + return data, nil +} diff --git a/app/service/search/type.go b/app/service/search/type.go new file mode 100644 index 0000000..0f0abb4 --- /dev/null +++ b/app/service/search/type.go @@ -0,0 +1,12 @@ +package search + +import ( + "91porn-server/app/service/searcher" + "91porn-server/app/service/searcher/tonesearcher" +) + +type Home struct { + HVidList []searcher.VideoRes `json:"hVidList"` //热点 + HSVidList []searcher.VideoRes `json:"hsVidList"` //最热视屏(最多播放) + ToneList []tonesearcher.ToneRes `json:"toneList"` //音色 +} diff --git a/app/service/searchaccessser/search_access.go b/app/service/searchaccessser/search_access.go new file mode 100644 index 0000000..3c94a49 --- /dev/null +++ b/app/service/searchaccessser/search_access.go @@ -0,0 +1,75 @@ +package searchaccessser + +import ( + "errors" + "fmt" + "time" + + "91porn-server/app/appg" + "91porn-server/common/crypt" +) + +const ( + tokenScope = "video_search_access" + tokenTTL = 10 * time.Minute +) + +// Issue 为搜索结果签发短时效的视频访问凭证。 +func Issue(uid uint64, videoID string, now time.Time) (string, error) { + return issue(appg.Conf.Base.JwtKey, uid, videoID, now) +} + +// Validate 校验搜索结果访问凭证,凭证与用户和视频一一绑定。 +func Validate(token string, uid uint64, videoID string, now time.Time) bool { + return validate(appg.Conf.Base.JwtKey, token, uid, videoID, now) +} + +func issue(secret string, uid uint64, videoID string, now time.Time) (string, error) { + if videoID == "" { + return "", errors.New("video id is empty") + } + return crypt.CreateToken(searchTokenSecret(secret), map[string]interface{}{ + "scope": tokenScope, + "uid": uid, + "vid": videoID, + "iat": now.Unix(), + "exp": now.Add(tokenTTL).Unix(), + }) +} + +func validate(secret, token string, uid uint64, videoID string, now time.Time) bool { + claims, err := crypt.ParseToken(searchTokenSecret(secret), token) + if err != nil { + return false + } + scope, _ := claims["scope"].(string) + vid, _ := claims["vid"].(string) + tokenUID, uidOK := numberClaim(claims["uid"]) + expiresAt, expOK := numberClaim(claims["exp"]) + return scope == tokenScope && + vid == videoID && + uidOK && + tokenUID == int64(uid) && + expOK && + expiresAt >= now.Unix() +} + +func searchTokenSecret(secret string) string { + if secret == "" { + return "" + } + return fmt.Sprintf("%s:%s", secret, tokenScope) +} + +func numberClaim(value interface{}) (int64, bool) { + switch number := value.(type) { + case float64: + return int64(number), true + case int64: + return number, true + case int: + return int64(number), true + default: + return 0, false + } +} diff --git a/app/service/searchaccessser/search_access_test.go b/app/service/searchaccessser/search_access_test.go new file mode 100644 index 0000000..3efeae3 --- /dev/null +++ b/app/service/searchaccessser/search_access_test.go @@ -0,0 +1,51 @@ +package searchaccessser + +import ( + "testing" + "time" +) + +func TestSearchAccessToken(t *testing.T) { + const ( + secret = "unit-test-secret" + uid = uint64(32239213) + videoID = "64b100000000000000000001" + ) + now := time.Now().Truncate(time.Second) + token, err := issue(secret, uid, videoID, now) + if err != nil { + t.Fatalf("issue() error = %v", err) + } + + tests := []struct { + name string + secret string + uid uint64 + video string + now time.Time + wantOK bool + }{ + {name: "valid", secret: secret, uid: uid, video: videoID, now: now.Add(time.Minute), wantOK: true}, + {name: "wrong uid", secret: secret, uid: uid + 1, video: videoID, now: now.Add(time.Minute)}, + {name: "wrong video", secret: secret, uid: uid, video: "64b100000000000000000002", now: now.Add(time.Minute)}, + {name: "wrong secret", secret: "another-secret", uid: uid, video: videoID, now: now.Add(time.Minute)}, + {name: "expired", secret: secret, uid: uid, video: videoID, now: now.Add(tokenTTL + time.Second)}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := validate(test.secret, token, test.uid, test.video, test.now); got != test.wantOK { + t.Fatalf("validate() = %v, want %v", got, test.wantOK) + } + }) + } +} + +func TestIssueRejectsMissingInputs(t *testing.T) { + if _, err := issue("", 1, "64b100000000000000000001", time.Now()); err == nil { + t.Fatal("issue() with empty secret should fail") + } + if _, err := issue("secret", 1, "", time.Now()); err == nil { + t.Fatal("issue() with empty video id should fail") + } +} diff --git a/app/service/searcher/audiobooksearcher/audiobookSearcher.go b/app/service/searcher/audiobooksearcher/audiobookSearcher.go new file mode 100644 index 0000000..53654dc --- /dev/null +++ b/app/service/searcher/audiobooksearcher/audiobookSearcher.go @@ -0,0 +1,76 @@ +package audiobooksearcher + +import ( + "errors" + + "91porn-server/app/appg" + "91porn-server/app/service/searcher" + "91porn-server/models/v/audiobookmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// FictionSearcher FictionSearcher +type FictionSearcher struct { + uid *uint64 +} + +// NewFictionSearcher NewFictionSearcher +func NewFictionSearcher(uid *uint64) searcher.Searcher { + return &FictionSearcher{ + uid: uid, + } +} + +// Search Search +func (t *FictionSearcher) Search(keywrod searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + kw, ok := keywrod.Get().(string) + if !ok { + return nil, errors.New("keyword type error") + } + if kw == "" { //关键字为空,热搜 + audiobook, hasNext, err := audiobookmod.GetNew(opt) + if err != nil { + return nil, err + } + return &Result{dat: audiobook, hasNext: hasNext}, nil + } + if appg.Conf.Elastic.IsActive { + audiobook, _ := esSearch(kw, opt) + hasNext := false + if len(audiobook) > int(opt.Limit()) { + hasNext = true + audiobook = audiobook[:opt.Limit()] + } + ab, err := fillAudioBook(*t.uid, audiobook) + if err != nil { + return nil, err + } + return &Result{dat: ab, hasNext: hasNext}, nil + } + audiobook, hasNext, err := audiobookmod.GetNew(opt) + if err != nil { + return nil, err + } + return &Result{dat: audiobook, hasNext: hasNext}, nil +} + +func fillAudioBook(uid uint64, data []audiobookmod.AudioBookBase) (res []audiobookmod.AudioBookAppRes, err error) { + ids := make([]primitive.ObjectID, len(data)) + for i := range data { + ids[i] = data[i].ID + } + return audiobookmod.GetByIds(ids) +} + +func esSearch(keyword string, opt searcher.Opter) (data []audiobookmod.AudioBookBase, err error) { + audiobook, err := audiobookmod.Search(keyword, opt.Skip(), opt.Limit()+1) + if err != nil { + return + } + data = make([]audiobookmod.AudioBookBase, len(audiobook)) + for i := range audiobook { + data[i] = audiobook[i].Source + } + return +} diff --git a/app/service/searcher/audiobooksearcher/result.go b/app/service/searcher/audiobooksearcher/result.go new file mode 100644 index 0000000..8c3b4c5 --- /dev/null +++ b/app/service/searcher/audiobooksearcher/result.go @@ -0,0 +1,31 @@ +package audiobooksearcher + +import ( + "91porn-server/app/service/searcher" + "91porn-server/models/v/audiobookmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +type Result struct { + dat []audiobookmod.AudioBookAppRes + hasNext bool +} + +func (*Result) Types() searcher.ResultTypes { + return searcher.LouFengSearchResult +} + +func (r *Result) Data() interface{} { + return r.dat +} + +func (r *Result) Count() int64 { + return int64(len(r.dat)) +} + +func (r *Result) HasNext() bool { + return r.hasNext +} diff --git a/app/service/searcher/fictionsearcher/fictionSearcher.go b/app/service/searcher/fictionsearcher/fictionSearcher.go new file mode 100644 index 0000000..722f9a0 --- /dev/null +++ b/app/service/searcher/fictionsearcher/fictionSearcher.go @@ -0,0 +1,63 @@ +package fictionsearcher + +import ( + "errors" + + "91porn-server/app/appg" + "91porn-server/app/service/searcher" + "91porn-server/models/v/fictionmod" +) + +// FictionSearcher FictionSearcher +type FictionSearcher struct { + uid *uint64 +} + +// NewFictionSearcher NewFictionSearcher +func NewFictionSearcher(uid *uint64) searcher.Searcher { + return &FictionSearcher{ + uid: uid, + } +} + +// Search Search +func (t *FictionSearcher) Search(keywrod searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + var fiction []fictionmod.FictionBase + kw, ok := keywrod.Get().(string) + if !ok { + return nil, errors.New("keyword type error") + } + hasNext := false + if kw == "" { //关键字为空,热搜 + fiction, hasNext, err = fictionmod.GetNew(opt) + if err != nil { + return nil, err + } + return &Result{fiction, hasNext}, nil + } + if appg.Conf.Elastic.IsActive { + fiction, err = esSearch(kw, opt) + if len(fiction) > int(opt.Limit()) { + hasNext = true + fiction = fiction[:opt.Limit()] + } + } else { + fiction, hasNext, err = fictionmod.GetNew(opt) + } + if err != nil { + return nil, err + } + return &Result{fiction, hasNext}, nil +} + +func esSearch(keyword string, opt searcher.Opter) (data []fictionmod.FictionBase, err error) { + fiction, err := fictionmod.Search(keyword, opt.Skip(), opt.Limit()+1) + if err != nil { + return + } + data = make([]fictionmod.FictionBase, len(fiction)) + for i := range fiction { + data[i] = fiction[i].Source + } + return +} diff --git a/app/service/searcher/fictionsearcher/result.go b/app/service/searcher/fictionsearcher/result.go new file mode 100644 index 0000000..f0500aa --- /dev/null +++ b/app/service/searcher/fictionsearcher/result.go @@ -0,0 +1,31 @@ +package fictionsearcher + +import ( + "91porn-server/app/service/searcher" + "91porn-server/models/v/fictionmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +type Result struct { + dat []fictionmod.FictionBase + hasNext bool +} + +func (*Result) Types() searcher.ResultTypes { + return searcher.LouFengSearchResult +} + +func (r *Result) Data() interface{} { + return r.dat +} + +func (r *Result) Count() int64 { + return int64(len(r.dat)) +} + +func (r *Result) HasNext() bool { + return r.hasNext +} diff --git a/app/service/searcher/keyword.go b/app/service/searcher/keyword.go new file mode 100644 index 0000000..d57f6e6 --- /dev/null +++ b/app/service/searcher/keyword.go @@ -0,0 +1,91 @@ +package searcher + +import ( + "time" + + "91porn-server/common/timeutil/timerange" + "91porn-server/models/v/tonerecomod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// KeyWorder KeyWorder +type KeyWorder interface { + Get() interface{} +} + +// Keyword Keyword +type Keyword struct { + Word string +} + +func (s *Keyword) Get() interface{} { + return s.Word +} + +// StringList StringList +type StringList struct { + Words []string +} + +func (s *StringList) Get() interface{} { + return s.Words +} + +// ObjectList ObjectList +type ObjIDList struct { + IDList []primitive.ObjectID +} + +func (o *ObjIDList) Get() interface{} { + return o.IDList +} + +// ObjectList ObjectList +type ObjID struct { + ID primitive.ObjectID +} + +func (o *ObjID) Get() interface{} { + return o.ID +} + +// ThemeList ThemeList +type ThemeList struct { + Themes []tonerecomod.ThemeType +} + +func (s *ThemeList) Get() interface{} { + return s.Themes +} + +// TimeRange TimeRange +type TimeRange struct { + Range timerange.TimeRange +} + +func (t *TimeRange) Get() interface{} { + return t.Range +} + +func NewTimeRange(start, end time.Time) KeyWorder { + timeRange := timerange.TimeRange{ + Head: start, + Tail: end, + } + return &TimeRange{timeRange} +} + +// SumDate SumDate +type SumDate struct { + Date time.Time +} + +func (t *SumDate) Get() interface{} { + return t.Date +} + +func NewSumDate(position time.Time) KeyWorder { + sumDate := timerange.LocDayRange(position).Head + return &SumDate{sumDate} +} diff --git a/app/service/searcher/location.go b/app/service/searcher/location.go new file mode 100644 index 0000000..79e6a5b --- /dev/null +++ b/app/service/searcher/location.go @@ -0,0 +1,51 @@ +package searcher + +import ( + "91porn-server/models/v/locmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// LocInfo LocInfo +type LocInfo struct { + ID *primitive.ObjectID `json:"id" bson:"_id"` + Longitude *string `json:"longitude,omitempty"` //经度 + Latitude *string `json:"latitude,omitempty"` //纬度 + Country *string `json:"country,omitempty"` //国家 + CountryCode *string `json:"countryCode,omitempty"` //国家编码 + Province *string `json:"province,omitempty"` //省份 + ProvinceCode *string `json:"provinceCode,omitempty"` //省份编码 + City *string `json:"city,omitempty"` //城市 + CityCode *string `json:"cityCode,omitempty"` //城市编码 + Address *string `json:"address,omitempty"` //地址 + Cover *string `json:"cover,omitempty"` //封面 + Visit *int `json:"visit,omitempty"` //访问人数 +} + +// LocMap LocMap +func LocMap(lidList []ObjectID) (map[ObjectID]*LocInfo, error) { + locMap, err := locmod.GetLocationInfoByIDs(lidList) + if err != nil { + return nil, err + } + m := make(map[ObjectID]*LocInfo, len(locMap)) + for k, v := range locMap { + loc := v + lInfo := LocInfo{ + ID: &loc.ID, + Longitude: &loc.Longitude, + Latitude: &loc.Latitude, + Country: &loc.Country, + CountryCode: &loc.CountryCode, + Province: &loc.Province, + ProvinceCode: &loc.ProvinceCode, + City: &loc.City, + CityCode: &loc.CityCode, + Address: &loc.Address, + Cover: &loc.Cover, + Visit: &loc.Visit, + } + m[k] = &lInfo + } + return m, nil +} diff --git a/app/service/searcher/loufeng.go b/app/service/searcher/loufeng.go new file mode 100644 index 0000000..552093f --- /dev/null +++ b/app/service/searcher/loufeng.go @@ -0,0 +1,83 @@ +package searcher + +import ( + "91porn-server/app/service/walletser" + "91porn-server/common/log" + "91porn-server/models/v/usermod" +) + +// UserInfo UserInfo +type UserInfo struct { + UID *uint64 `json:"uid,omitempty"` + Name *string `json:"name,omitempty"` + Age *int `json:"age,omitempty"` + Gender *string `json:"gender,omitempty"` //性别 + Portrait *string `json:"portrait,omitempty"` //头像 + Region *string `json:"region,omitempty"` //地区 + Summary *string `json:"summary,omitempty"` //简介 + VipLevel *int `json:"vipLevel,omitempty"` //vip等级 + RechargeLevel *int `json:"rechargeLevel" ` //头像显示的vip等级,累计充值决定 + SuperUser *bool `json:"superUser"` //大v + ActiveValue *int `json:"activeValue"` //活跃度 + OfficialCert *bool `json:"officialCert"` //是否官方认证 +} + +// UserRes UserRes +type UserRes struct { + UserInfo `bson:",inline"` + HasFollowed *bool `json:"hasFollowed,omitempty"` //已关注 + FansCount *int64 `json:"fansCount,omitempty"` //粉丝数 +} + +// MapToUserResList MapToUserResList +func MapToUserResList(userList []*usermod.User, uid uint64) []UserRes { + uidList := make([]uint64, len(userList)) + for i, u := range userList { + uidList[i] = u.UID + } + mLevel, err := walletser.GetUidsRchgLevel(uidList) + if err != nil { + log.Error("GetUidsRchgLevel error", log.E(err)) + return []UserRes{} + } + userResList := make([]UserRes, len(userList)) + for i, userp := range userList { + age := userp.Age() + level := mLevel[userp.UID].Level + userInfo := UserInfo{ + Age: &age, + UID: &userp.UID, + Name: &userp.Name, + Gender: &userp.Gender, + Portrait: &userp.Portrait, + Region: &userp.Region, + Summary: &userp.Summary, + VipLevel: &userp.VipLevel, + SuperUser: &userp.SuperUser, + ActiveValue: &userp.ActiveValue, + OfficialCert: &userp.OfficialCert, + RechargeLevel: &level, + } + userResList[i] = UserRes{ + UserInfo: userInfo, + } + } + return userResList +} + +// UserResMap UserResMap +func UserResMap(uidList []uint64, uid uint64) (map[uint64]*UserRes, error) { + userList, err := usermod.FindUsersByUID(uidList) + if err != nil { + return nil, err + } + userResList := MapToUserResList(userList, uid) + m := make(map[uint64]*UserRes, len(userList)) + for _, res := range userResList { + if res.UID != nil { + _res := res + m[*res.UID] = &_res + } + } + return m, nil +} diff --git a/app/service/searcher/opt.go b/app/service/searcher/opt.go new file mode 100644 index 0000000..0a5dc40 --- /dev/null +++ b/app/service/searcher/opt.go @@ -0,0 +1,57 @@ +package searcher + +// Opter Opter +type Opter interface { + SetSkip(v int64) Opter + Skip() int64 + SetLimit(v int64) Opter + Limit() int64 + SetSort(key string, v int64) Opter + Sort() D +} + +type Option struct { + skip *int64 + limit *int64 + sort D +} + +const MaxLimit = 50 + +const MinLimit = 1 + +func (o *Option) Skip() int64 { + if o.skip == nil { + return 0 + } + return *o.skip +} + +func (o *Option) SetSkip(v int64) Opter { + o.skip = &v + return o +} + +func (o *Option) Limit() int64 { + if o.limit == nil { + return MinLimit + } + if *o.limit > MaxLimit { + return MaxLimit + } + return *o.limit +} + +func (o *Option) SetLimit(v int64) Opter { + o.limit = &v + return o +} + +func (o *Option) Sort() D { + return o.sort +} + +func (o *Option) SetSort(key string, v int64) Opter { + o.sort = append(o.sort, E{Key: key, Value: v}) + return o +} diff --git a/app/service/searcher/result.go b/app/service/searcher/result.go new file mode 100644 index 0000000..00aa388 --- /dev/null +++ b/app/service/searcher/result.go @@ -0,0 +1,23 @@ +package searcher + +type ResultTypes = int + +const ( + TagSearchResult ResultTypes = 1 << iota //0 which is 00000001 + ToneSearchResult + UserSearchResult + VidPCountSearchResult + VidRichSearchResult + VidSearchResult + VidTagSearchResult + VidToneSearchResult + VidHotKeywordSearchResult + LouFengSearchResult +) + +type Resulter interface { + Types() ResultTypes + Data() interface{} + Count() int64 + HasNext() bool +} diff --git a/app/service/searcher/search.go b/app/service/searcher/search.go new file mode 100644 index 0000000..bba86c0 --- /dev/null +++ b/app/service/searcher/search.go @@ -0,0 +1,6 @@ +package searcher + +// Searcher Searcher +type Searcher interface { + Search(KeyWorder, Opter) (Resulter, error) +} diff --git a/app/service/searcher/tag.go b/app/service/searcher/tag.go new file mode 100644 index 0000000..4835a64 --- /dev/null +++ b/app/service/searcher/tag.go @@ -0,0 +1,86 @@ +package searcher + +import ( + "strings" + + "91porn-server/models/v/tagmod" +) + +// TagInfo TagInfo +type TagInfo struct { + ID *ObjectID `json:"id,omitempty"` //标签id + Name *string `json:"name,omitempty"` //标签名字 + CoverImg *string `json:"coverImg,omitempty"` //封面图片 + Description *string `json:"description,omitempty"` //文字说明 + PlayCount *int64 `json:"playCount,omitempty"` //播放量 + VidCount *int64 `json:"vidCount,omitempty"` //帖子数量 + FollowCount *int64 `json:"followCount,omitempty"` //关注数量 +} + +// ToTagMap ToTagMap +func ToTagMap(tInfoList []TagInfo) map[ObjectID]TagInfo { + m := make(map[ObjectID]TagInfo, len(tInfoList)) + for _, v := range tInfoList { + if v.ID == nil { + continue + } + m[*v.ID] = v + } + return m +} + +// TagInfoListByTIDList TagInfoListByTIDList +func TagInfoListByTIDList(uid *uint64, tidList []ObjectID) ([]TagInfo, error) { + tMap, err := tagmod.TagMap(tidList) + if err != nil { + return nil, err + } + tagList := make([]tagmod.Tag, len(tMap)) + i := 0 + for _, tag := range tMap { + tagList[i] = tag + i++ + } + tInfoList, err := TagInfoList(uid, tagList) + if err != nil { + return nil, err + } + return tInfoList, nil +} + +// TagInfoList TagInfoList +// +//uid:nil +func TagInfoList(uid *uint64, tagList []tagmod.Tag) ([]TagInfo, error) { + tInfoList := make([]*TagInfo, 0, len(tagList)) + for _, v := range tagList { + _v := v + tagName := strings.TrimSpace(_v.TagName) + if tagName == "" { //临时解决话题搜索返回空tag + continue + } + tInfo := &TagInfo{ + ID: &_v.ID, + Name: &tagName, + CoverImg: &_v.CoverImg, + Description: &_v.Description, + PlayCount: &_v.TPlayCount, //夏沫确认 前端播放量计算方法 + VidCount: &_v.VideoCount, + FollowCount: &_v.CollCount, + } + if uid == nil { + tInfoList = append(tInfoList, tInfo) + continue + } + tInfoList = append(tInfoList, tInfo) + } + tagInfoList := make([]TagInfo, len(tagList)) + for i, tInfo := range tInfoList { + if uid == nil { + tagInfoList[i] = *tInfo + continue + } + tagInfoList[i] = *tInfo + } + return tagInfoList, nil +} diff --git a/app/service/searcher/tagsearcher/result.go b/app/service/searcher/tagsearcher/result.go new file mode 100644 index 0000000..d5b3c4e --- /dev/null +++ b/app/service/searcher/tagsearcher/result.go @@ -0,0 +1,32 @@ +package tagsearcher + +import ( + "91porn-server/app/service/searcher" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +type TagRes = searcher.TagInfo + +type Result struct { + dat []TagRes + hasNext bool +} + +func (*Result) Types() searcher.ResultTypes { + return searcher.TagSearchResult +} + +func (r *Result) Data() interface{} { + return r.dat +} + +func (r *Result) Count() int64 { + return int64(len(r.dat)) +} + +func (r *Result) HasNext() bool { + return r.hasNext +} diff --git a/app/service/searcher/tagsearcher/tagSearcher.go b/app/service/searcher/tagsearcher/tagSearcher.go new file mode 100644 index 0000000..050c18a --- /dev/null +++ b/app/service/searcher/tagsearcher/tagSearcher.go @@ -0,0 +1,87 @@ +package tagsearcher + +import ( + "errors" + + "91porn-server/app/appg" + "91porn-server/app/service/searcher" + "91porn-server/models/v/tagmod" +) + +// TagSearcher TagSearcher +type TagSearcher struct { + uid *uint64 +} + +// NewTagSearcher NewTagSearcher +func NewTagSearcher(uid *uint64) searcher.Searcher { + return &TagSearcher{ + uid: uid, + } +} + +// Search Search +func (t *TagSearcher) Search(keywrod searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + kw, ok := keywrod.Get().(string) + if !ok { + return nil, errors.New("keyword type error") + } + limitEx1 := opt.Limit() + 1 + hasNext := false + if kw == "" { //关键字为空,热搜 + tags, err := tagmod.TagListByFakePlayCount(opt.Skip(), limitEx1) + if err != nil { + return nil, err + } + if len(tags) > int(opt.Limit()) { + hasNext = true + tags = tags[:opt.Limit()] + } + tagResList, err := searcher.TagInfoList(t.uid, tags) + if err != nil { + return nil, err + } + return &Result{tagResList, hasNext}, nil + } + var tags []tagmod.Tag + if appg.Conf.Elastic.IsActive { + tags, err = esSearch(kw, opt) + } else { + tags, err = tagmod.GetTagListByKeyword(kw, opt.Sort(), opt.Skip(), limitEx1) + } + if err != nil { + return nil, err + } + if len(tags) > int(opt.Limit()) { + hasNext = true + tags = tags[:opt.Limit()] + } + tagResList, err := searcher.TagInfoList(t.uid, tags) + if err != nil { + return nil, err + } + return &Result{tagResList, hasNext}, nil +} + +func esSearch(keyword string, opt searcher.Opter) ([]tagmod.Tag, error) { + esTags, err := tagmod.Search(keyword, opt.Skip(), opt.Limit()+1) + if err != nil { + return nil, err + } + if len(esTags) == 0 { + return nil, nil + } + tagsID := make([]ObjectID, len(esTags)) + for i, v := range esTags { + tagsID[i] = v.ID + } + tMap, err := tagmod.FindTagsMapByIDS(tagsID) + if err != nil || tMap == nil { + return nil, err + } + tags := make([]tagmod.Tag, len(tagsID)) + for i, v := range tagsID { + tags[i] = tMap[v] + } + return tags, nil +} diff --git a/app/service/searcher/tonesearcher/result.go b/app/service/searcher/tonesearcher/result.go new file mode 100644 index 0000000..ac9218e --- /dev/null +++ b/app/service/searcher/tonesearcher/result.go @@ -0,0 +1,33 @@ +package tonesearcher + +import "91porn-server/app/service/searcher" + +type ToneRes struct { + Name string `json:"name"` //主题名 + Types string `json:"types"` //类型 + Cover string `json:"cover"` //封面 + PlayCount int64 `json:"playCount"` //播放数 + PayCount int64 `json:"payCount"` //购买数 + Sort int `json:"-"` //排序 +} + +type Result struct { + dat []ToneRes + hasNext bool +} + +func (*Result) Types() searcher.ResultTypes { + return searcher.ToneSearchResult +} + +func (r *Result) Data() interface{} { + return r.dat +} + +func (r *Result) Count() int64 { + return int64(len(r.dat)) +} + +func (r *Result) HasNext() bool { + return r.hasNext +} diff --git a/app/service/searcher/tonesearcher/toneSearcher.go b/app/service/searcher/tonesearcher/toneSearcher.go new file mode 100644 index 0000000..51438a4 --- /dev/null +++ b/app/service/searcher/tonesearcher/toneSearcher.go @@ -0,0 +1,74 @@ +package tonesearcher + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/searcher" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/models/v/tonemod" + "91porn-server/models/v/tonerecomod" + + "go.mongodb.org/mongo-driver/bson" +) + +// M M +type M = bson.M + +type ToneValue = tonemod.ToneValue + +// ToneSearcher ToneSearcher +type ToneSearcher struct { +} + +// NewToneSearcher NewToneSearcher +func NewToneSearcher() searcher.Searcher { + return &ToneSearcher{} +} + +// Search Search +func (t *ToneSearcher) Search(unuse1 searcher.KeyWorder, unuse2 searcher.Opter) (result searcher.Resulter, err error) { + enable := true + enableMatch := tonemod.EnableMatch{Enable: &enable} + max := int64(7) + list, err := tonemod.List(bson.D{{Key: "sortKey", Value: 1}}, 0, max, enableMatch.New()) + if err != nil { + return nil, err + } + toneValue := GetToneValue() + toneResList := make([]ToneRes, len(list)) + for i, v := range list { + toneRes := ToneRes{ + Name: v.Name, + Types: v.Types, + Cover: v.Cover, + Sort: v.SortKey, + } + switch v.Types { + case tonerecomod.OfficialRecom: + toneRes.PlayCount = toneValue.OfficialRecomPlayCount + case tonerecomod.GoldCoinArea: + toneRes.PayCount = toneValue.GoldCoinAreaPayCount + case tonerecomod.LatestUpload: + toneRes.PlayCount = toneValue.LatestUploadPlayCount + case tonerecomod.MostLikes: + toneRes.PlayCount = toneValue.MostLikesPlayCount + case tonerecomod.PlayAtMost: + toneRes.PlayCount = toneValue.PlayAtMostPlayCount + case tonerecomod.MostComments: + toneRes.PlayCount = toneValue.MostCommentsPlayCount + case tonerecomod.FreeArea: + toneRes.PlayCount = toneValue.FreeAreaPlayCount + } + toneResList[i] = toneRes + } + return &Result{toneResList, false}, nil +} + +func GetToneValue() ToneValue { + var toneValue ToneValue + redisKey := redisconst.ToneKey() + if err := appg.Redis.Scan(redisKey, &toneValue); err != nil { + log.Warn("tone Run redis Scan error", log.E(err)) + } + return toneValue +} diff --git a/app/service/searcher/user.go b/app/service/searcher/user.go new file mode 100644 index 0000000..1b264bb --- /dev/null +++ b/app/service/searcher/user.go @@ -0,0 +1,37 @@ +package searcher + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// LouFengInfo LouFengInfo +type LouFengInfo struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title,omitempty"` //标题 + Number int `json:"number" bson:"number,omitempty"` //编号 + Quantity string `json:"quantity" bson:"quantity,omitempty"` //数量 + Age string `json:"age" bson:"age,omitempty"` //年龄 + Cover string `json:"cover" bson:"cover,omitempty"` //封面 + Price string `json:"price" bson:"price"` //价格 单位 元 + BusinessHours string `json:"businessHours" bson:"businessHours,omitempty"` //营业时间 0-24 + ServiceItems []string `json:"serviceItems" bson:"serviceItems,omitempty"` //服务项目 + City string `json:"city" bson:"city,omitempty"` //市 + District string `json:"district" bson:"district,omitempty"` //区,县 + Contact string `json:"contact" bson:"contact,omitempty"` //联系方式 + ContactPrice int64 `json:"contactPrice" bson:"contactPrice,omitempty"` //联系方式价格 单位 金币(角) + Impression string `json:"impression" bson:"impression,omitempty"` //狼友印象 + EnvStar int `json:"envStar" bson:"envStar,omitempty"` //环境评分 + PrettyStar int `json:"prettyStar" bson:"prettyStar,omitempty"` //颜值评分 + OverallRatings int `json:"overallRatings" bson:"overallRatings,omitempty"` //综合评分 + ServiceStar int `json:"serviceStar" bson:"serviceStar,omitempty"` //服务星级 + BusinessDate string `json:"businessDate" bson:"businessDate,omitempty"` //营业月 份 "2020-06" + IsActive bool `json:"isActive " bson:"isActive"` //是否激活 + IsVerify bool `json:"isVerify" bson:"isVerify"` //是否验证 + CountPurchases int64 `json:"countPurchases" bson:"countPurchases"` //购买数 + CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` //浏览数 + CountCollect int64 `json:"countCollect" bson:"countCollect"` //收藏数 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 +} diff --git a/app/service/searcher/usersearcher/result.go b/app/service/searcher/usersearcher/result.go new file mode 100644 index 0000000..9121530 --- /dev/null +++ b/app/service/searcher/usersearcher/result.go @@ -0,0 +1,26 @@ +package usersearcher + +import "91porn-server/app/service/searcher" + +type UserRes = searcher.UserRes + +type Result struct { + dat []searcher.UserRes + hasNext bool +} + +func (*Result) Types() searcher.ResultTypes { + return searcher.UserSearchResult +} + +func (r *Result) Data() interface{} { + return r.dat +} + +func (r *Result) Count() int64 { + return int64(len(r.dat)) +} + +func (r *Result) HasNext() bool { + return r.hasNext +} diff --git a/app/service/searcher/usersearcher/userSearcher.go b/app/service/searcher/usersearcher/userSearcher.go new file mode 100644 index 0000000..2e5bf43 --- /dev/null +++ b/app/service/searcher/usersearcher/userSearcher.go @@ -0,0 +1,105 @@ +package usersearcher + +import ( + "errors" + "strconv" + + "91porn-server/app/appg" + "91porn-server/app/service/searcher" + "91porn-server/models/v/usermod" +) + +// UserSearcher UserSearcher +type UserSearcher struct { + uid uint64 +} + +// NewUserSearcher NewUserSearcher +func NewUserSearcher(uid uint64) searcher.Searcher { + return &UserSearcher{ + uid: uid, + } +} + +// Search Search +func (u *UserSearcher) Search(keyword searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + kw, ok := keyword.Get().(string) + if !ok { + return nil, errors.New("keyword type error") + } + limitEx1 := opt.Limit() + 1 + hasNext := false + if kw == "" { //关键字为空 返回最新用户 + uList, err := usermod.NewUserList(opt.Skip(), limitEx1) + if err != nil { + return nil, err + } + if len(uList) > int(opt.Limit()) { + hasNext = true + uList = uList[:opt.Limit()] + } + userResList := searcher.MapToUserResList(uList, u.uid) + return &Result{userResList, hasNext}, nil + } + userList := make([]*usermod.User, 0, limitEx1) + uid, err := strconv.ParseUint(kw, 10, 64) + var esHasNext bool + if err == nil { //能解析成Int 通过uid搜索 + user, err := usermod.FindUserByUID(uid) + if err != nil { + return nil, err + } + if user != nil { + userList = append(userList, user) + } + } else { //通过用户名搜索 + var uList []*usermod.User + if appg.Conf.Elastic.IsActive { + uList, esHasNext, err = esSearch(kw, opt) + } else { + uList, err = usermod.FindUsersByKeyword(kw, opt.Skip(), limitEx1) + } + if err != nil { + return nil, err + } + userList = append(userList, uList...) + } + if len(userList) == 0 { + return &Result{[]UserRes{}, false}, nil + } + if len(userList) > int(opt.Limit()) { + hasNext = true + userList = userList[:opt.Limit()] + } + if appg.Conf.Elastic.IsActive { + hasNext = esHasNext + } + userResList := searcher.MapToUserResList(userList, u.uid) + return &Result{userResList, hasNext}, nil +} + +func esSearch(keyword string, opt searcher.Opter) (users []*usermod.User, hasNext bool, err error) { + esUsers, err := usermod.Search(keyword, opt.Skip(), opt.Limit()+1) + if err != nil { + return + } + if len(esUsers) == 0 { + return + } + uidList := make([]uint64, len(esUsers)) + for i, v := range esUsers { + uidList[i] = v.Source.UID + } + hasNext = len(esUsers) > int(opt.Limit()) + uMap, err := usermod.FindUsersMapByUID(uidList) + if err != nil || uMap == nil { + return + } + users = make([]*usermod.User, 0, len(esUsers)) + for _, v := range esUsers { + if u, ok := uMap[v.Source.UID]; ok { + users = append(users, u) + } + } + return +} diff --git a/app/service/searcher/video.go b/app/service/searcher/video.go new file mode 100644 index 0000000..b3e19b6 --- /dev/null +++ b/app/service/searcher/video.go @@ -0,0 +1,562 @@ +package searcher + +import ( + "sync" + "time" + + "91porn-server/app/service/searchaccessser" + "91porn-server/app/service/vidhelpser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/models/l/payvidlgmod" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/freeVidmod" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/videodiscountmod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// A A +type A = bson.A + +// M M +type M = bson.M + +// D D +type D = bson.D + +// E E +type E = bson.E + +// ObjectID ObjectID +type ObjectID = primitive.ObjectID + +// VideoInfo VideoInfo +type VideoInfo struct { + ID *ObjectID `json:"id" bson:"_id"` + NewsType *string `json:"newsType"` //帖子类型, VID,视频帖子,COVER + Title *string `json:"title,omitempty"` //视频标题 + Tags []TagInfo `json:"tags"` //视频标签 + SourceID *string `json:"sourceID,omitempty"` //视频在仓库中的资源ID + SourceURL *string `json:"sourceURL,omitempty"` //视频资源地址Path + H265Url *string `json:"h265Url"` // H.265 视频资源地址 + PlayTime *uint `json:"playTime,omitempty"` //影片长度 + Cover *string `json:"cover,omitempty"` //封面大图 + CoverThumb *string `json:"coverThumb,omitempty"` //封⾯小图 + SeriesCover []string `json:"seriesCover"` //封面套图 + Via *string `json:"via,omitempty"` //来源 自拍,上传 + PlayCount *int `json:"playCount,omitempty"` //总的播放量量(假数据) + LikeCount *int `json:"likeCount,omitempty"` //点赞数(假数据) + PayCount *int `json:"payCount,omitempty"` //购买数 + CommentCount *int `json:"commentCount,omitempty"` //评论数(假数据) + ShareCount *int `json:"shareCount,omitempty"` //分享数(假数据) + Coins *int64 `json:"coins,omitempty"` //折扣后价格 + Size *int `json:"size,omitempty"` //文件大小 byte + Status *int `json:"status,omitempty"` //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + Resolution *string `json:"resolution,omitempty"` //分辨率 + Ratio *float64 `json:"ratio,omitempty"` //宽高比 + MimeType *string `json:"mimeType,omitempty"` //视频格式类型 + Actor *string `json:"actor,omitempty"` + FreeTime *int `json:"freeTime,omitempty"` //免费观影时长 + CreatedAt *time.Time `json:"createdAt,omitempty"` //创建时间 + ReviewedAt *time.Time `json:"reviewAt,omitempty"` // 审核时间 + UInfo vidmod.Publisher `json:"publisher,omitempty" bson:"publisher"` //用户信息 + FreeArea *bool `json:"freeArea,omitempty"` //免费专区 + PublisherID *uint64 `json:"-"` //发布者ID(user) + LocationID *primitive.ObjectID `json:"-"` //位置ID + IsTopping *bool `json:"isTopping"` + IsRecommend *bool `json:"isRecommend"` //力荐 + IsChoosen *bool `json:"isChoosen"` //置精 + OriginCoins *int64 `json:"originCoins"` // 视频原价 + SearchAccessToken *string `json:"searchAccessToken,omitempty"` + ShowFreeTrialBadge bool `json:"showFreeTrialBadge" bson:"-"` + FreeTrialRemaining uint64 `json:"freeTrialRemaining" bson:"-"` + CanUseFreeTrial bool `json:"canUseFreeTrial" bson:"-"` +} + +// VideoMap VideoMap +func videoMap(vidList []ObjectID, uid uint64, allowSearchOnly bool) (map[ObjectID]*VideoInfo, error) { + if len(vidList) == 0 { + return make(map[ObjectID]*VideoInfo), nil + } + videoMap, vInfo, err := vidmod.SearchVideoMap(vidList) + if err != nil { + return nil, err + } + blockedModules := make(map[string]struct{}) + if allowSearchOnly { + excludedModules, excludeErr := moduleconfmod.ExcludedSearchModuleIDs() + if excludeErr != nil { + return nil, excludeErr + } + for _, moduleID := range excludedModules { + blockedModules[moduleID] = struct{}{} + } + } else { + moduleIDs := make([]string, 0, len(vInfo)) + for _, video := range vInfo { + if video.MID != "" { + moduleIDs = append(moduleIDs, video.MID) + } + } + blockedModules, err = moduleconfmod.BlockedOutsideSearchModuleIDs(moduleIDs, time.Now()) + if err != nil { + return nil, err + } + } + var ( + vidInfoMap = make(map[ObjectID]*VideoInfo, len(videoMap)) + tidUniqueMap = make(map[ObjectID]byte) + mFree = map[uint64]bool{} + self *usermod.User + videoDiscLog videodiscountmod.VideoDiscountLog + largestDiscount int + wg sync.WaitGroup + mUser map[uint64]*vidmod.Publisher + ) + uids := make([]uint64, len(vInfo)) + for i, v := range vInfo { + uids[i] = v.PublisherID + } + wg.Add(5) + common.Go(func() { + defer wg.Done() + self, err = usermod.FindUserByUID(uid) + if err != nil { + log.Error("VideoMap: usermod.FindUserByUID", log.Any("uid", uid), log.E(err)) + return + } + }) + common.Go(func() { + defer wg.Done() + mUser = vidhelpser.GetUserInfo2Map(uid, uids) + }) + common.Go(func() { + defer wg.Done() + largestDiscount, err = productmod.GetLargestDiscountVIPCard() + if err != nil { + log.Error("VideoMap GetLargestDiscountVIPCard", log.Any("uid", uid), log.E(err)) + } + }) + common.Go(func() { + defer wg.Done() + mFree = freeVidmod.GetFreeVideoMapByUID(uid) + }) + common.Go(func() { + defer wg.Done() + videoDiscLog, err = videodiscountmod.GetByUID(uid) + if err != nil { + log.Error("VideoMap videodiscountmod.GetByUID", log.Any("uid", uid), log.E(err)) + } + }) + wg.Wait() + freeTrialContext := vidhelpser.FreeTrialBadgeContextForUser(uid, self) + for vid, vidInfo := range videoMap { + videoPtr := vidInfo + if _, blocked := blockedModules[videoPtr.MID]; blocked { + continue + } + status := videoPtr.Status + if status == 3 { + status = 1 + } + var uInfo vidmod.Publisher + if mUser[vidInfo.PublisherID] != nil { + uInfo = *mUser[vidInfo.PublisherID] + } else { + //处理作者被删除的情况 + if len(mUser) > 0 { + for i := range mUser { + uInfo = *mUser[i] + break + } + } + } + newsType := vidmod.SP + if len(videoPtr.NewsType) != 0 { + newsType = videoPtr.NewsType + } + discountCoins := vidhelpser.CalcVideoCoins(videoPtr.Coins, videoPtr.PublisherID, self, mFree, videoDiscLog, + largestDiscount, nil) + h265URL := vidhelpser.H265URLForApp(videoPtr) + vInfo := VideoInfo{ + ID: &videoPtr.ID, + NewsType: &newsType, + Title: &videoPtr.Title, //视频标题 + Tags: []TagInfo{}, //视频标签 + SourceID: &videoPtr.SourceID, //视频在仓库中的资源ID + SourceURL: &videoPtr.SourceURL, //视频资源地址Path + H265Url: &h265URL, // H.265 视频资源地址 + PlayTime: &videoPtr.PlayTime, //影片长度 + Cover: &videoPtr.Cover, //封面大图 + CoverThumb: &videoPtr.CoverThumb, //封⾯小图 + SeriesCover: videoPtr.SeriesCover, + Via: &videoPtr.Via, //来源 自拍,上传 + PlayCount: &videoPtr.FakePlayCount, //总的播放量量(假数据) + LikeCount: &videoPtr.FakeLikeCount, //点赞数(假数据) + CommentCount: &videoPtr.FakeCommentCount, //评论数(假数据) + ShareCount: &videoPtr.FakeShareCount, //分享数(假数据) + Size: &videoPtr.Size, //文件大小 byte + Ratio: &videoPtr.Ratio, //分辨率 + Status: &status, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + Resolution: &videoPtr.Resolution, //分辨率 + MimeType: &videoPtr.MimeType, //视频格式类型 + Actor: &videoPtr.Actor, + FreeTime: &videoPtr.FreeTime, //免费观影时长 + PublisherID: &videoPtr.PublisherID, //发布者ID(user) + FreeArea: &videoPtr.FreeArea, //免费专区 + LocationID: &videoPtr.Location, //位置ID + CreatedAt: &videoPtr.CreatedAt, //创建时间 + ReviewedAt: &videoPtr.ReviewAt, + UInfo: uInfo, + IsTopping: &videoPtr.IsTopping, + IsChoosen: &videoPtr.IsChoosen, + IsRecommend: &videoPtr.IsRecommend, + OriginCoins: &videoPtr.Coins, // 定价 + Coins: &discountCoins, // 折扣价 + } + vInfo.ShowFreeTrialBadge, vInfo.FreeTrialRemaining, vInfo.CanUseFreeTrial = freeTrialContext.Fields( + newsType, + videoPtr.Coins, + videoPtr.FreeArea, + videoPtr.PublisherID, + ) + if allowSearchOnly { + token, tokenErr := searchaccessser.Issue(uid, videoPtr.ID.Hex(), time.Now()) + if tokenErr != nil { + return nil, tokenErr + } + vInfo.SearchAccessToken = &token + } + if len(videoPtr.Tags) != 0 { + for _, tid := range videoPtr.Tags { + tidUniqueMap[tid] = 0 + } + } + //fill video Map + vidInfoMap[vid] = &vInfo + } + tidList := make([]ObjectID, len(tidUniqueMap)) + i := 0 + for k := range tidUniqueMap { + tidList[i] = k + i++ + } + //获取所有tagInfo List + tInfoList, err := TagInfoListByTIDList(&uid, tidList) + if err != nil { + return nil, err + } + tInfoMap := ToTagMap(tInfoList) + //vidInfoMap填充tagInfo + for vid, vidInfo := range vidInfoMap { + video := videoMap[vid] + tidList := video.Tags + tInfoList := make([]TagInfo, 0, len(tidList)) + for _, t := range video.Tags { + tInfo := tInfoMap[t] + tInfoList = append(tInfoList, tInfo) + } + vidInfo.Tags = tInfoList + } + return vidInfoMap, nil +} + +// VidStatus VidStatus +type VidStatus struct { + HasPaid *bool `json:"hasPaid,omitempty"` //已支付 +} + +// VidAndStatus VidAndStatus +type VidAndStatus struct { + VID ObjectID + *VidStatus +} + +// VidStatusMap VidStatusMap +func VidStatusMap(vidList []ObjectID, uid uint64) map[ObjectID]VidStatus { + var ( + payUniqList = make([]string, 0, len(vidList)) + payUniqMap = make(map[ObjectID]string, len(vidList)) + ) + for _, v := range vidList { + payUniq := payvidlgmod.Unique(uid, v) + payUniqList = append(payUniqList, payUniq) + payUniqMap[v] = payUniq + } + payStatueMap, err := payvidlgmod.PayStatueMap(payUniqList) + if err != nil { + return map[ObjectID]VidStatus{} + } + vidStatusMap := make(map[ObjectID]VidStatus, len(vidList)) + for _, v := range vidList { + payHash := payUniqMap[v] + payStatue := payStatueMap[payHash] + vidStatusMap[v] = VidStatus{ + HasPaid: &payStatue, + } + } + return vidStatusMap +} + +// Publisher Publisher +type Publisher = UserRes + +// VideoRes VideoRes +type VideoRes struct { + VideoInfo `bson:",inline"` //视屏 + LocInfo `json:"location" bson:"location"` //位置 + VidStatus `json:"vidStatus" bson:"vidStatus"` //用户与视频状态信息 + Publisher `json:"publisher" bson:"publisher"` //发布者 +} + +// GetVideoRes GetVideoRes +func GetVideoRes(vid ObjectID, vInfoMap map[ObjectID]*VideoInfo, publMap map[uint64]*UserRes, locMap map[ObjectID]*LocInfo, statusMap map[ObjectID]VidStatus) VideoRes { + res := VideoRes{} + video := vInfoMap[vid] + if video == nil { + return res + } + res.VideoInfo = *video + if video.PublisherID != nil { + publisher := publMap[*video.PublisherID] + if publisher != nil { + res.Publisher = *publisher + } + } + if video.LocationID != nil { + loc := locMap[*video.LocationID] + if loc != nil { + res.LocInfo = *loc + } + } + status := statusMap[vid] + res.VidStatus = status + return res +} + +func getVideoParentCommentCountMap(vidList []ObjectID) map[ObjectID]int { + res := make(map[ObjectID]int, len(vidList)) + if len(vidList) == 0 { + return res + } + countMap, err := cmtmod.CmtFindParentCountByObjIDs(cmtmod.CmtTypeVideo, vidList) + if err != nil { + log.Error("GetVideoResList CmtFindParentCountByObjIDs error", log.E(err), log.Any("vidList", vidList)) + return res + } + for id, count := range countMap { + res[id] = int(count) + } + return res +} + +// GetVideoResList GetVideoResList +func GetVideoResList(uid uint64, vidList []ObjectID) ([]VideoRes, error) { + return getVideoResList(uid, vidList, false) +} + +// GetVideoResListForSearch 保留当前仅能通过搜索入口访问的视频。 +func GetVideoResListForSearch(uid uint64, vidList []ObjectID) ([]VideoRes, error) { + return getVideoResList(uid, vidList, true) +} + +func getVideoResList(uid uint64, vidList []ObjectID, allowSearchOnly bool) ([]VideoRes, error) { + videoCount := len(vidList) + //vid->video Map + videoMap, err := videoMap(vidList, uid, allowSearchOnly) + if err != nil { + return nil, err + } + commentCountMap := getVideoParentCommentCountMap(vidList) + //Publisher + //发布者ID List + pidList := make([]uint64, 0, videoCount) + for _, video := range videoMap { + if video.PublisherID == nil { + continue + } + pidList = append(pidList, *video.PublisherID) + } + //LocationID List + lidList := make([]ObjectID, 0, videoCount) + for _, v := range videoMap { + if v.LocationID == nil { + continue + } + lidList = append(lidList, *v.LocationID) + } + var ( + wg = sync.WaitGroup{} + publMap = make(map[uint64]*UserRes) + locMap = make(map[ObjectID]*LocInfo) + statusMap = make(map[ObjectID]VidStatus) + ) + //一共3个map,每个携程填充一个Map + wg.Add(3) + common.Go(func() { + //PublisherID->User map + defer wg.Done() + m, err := UserResMap(pidList, uid) + if err != nil { + log.Error("GetVideoResList PublisherID->User error", log.E(err)) + return + } + publMap = m + }) + //Location map + common.Go(func() { + //LocationID->Location map + defer wg.Done() + m, err := LocMap(lidList) + if err != nil { + log.Error("GetVideoResList Location map error", log.E(err)) + return + } + locMap = m + }) + //Video Status map + common.Go(func() { + //Vid->视屏状态 map + defer wg.Done() + statusMap = VidStatusMap(vidList, uid) + }) + wg.Wait() + //组装 []VidInfo + videoResList := make([]VideoRes, 0, videoCount) + for _, vid := range vidList { + info := GetVideoRes(vid, videoMap, publMap, locMap, statusMap) + if info.Publisher.UID == nil { + log.Warn("Publisher UID is invalid") + continue + } + if info.VideoInfo.ID != nil { + commentCount := commentCountMap[*info.VideoInfo.ID] + info.CommentCount = &commentCount + } + info.Publisher.HasFollowed = &videoMap[vid].UInfo.HasFollowed + isFree := true + info.FreeArea = &isFree + videoResList = append(videoResList, info) + } + return videoResList, nil +} + +// RichVidRes RichVidRes +type RichVidRes struct { + VideoInfo `bson:",inline"` //视屏 + LocInfo `json:"location" bson:"location"` //位置 + VidStatus `json:"vidStatus" bson:"vidStatus"` //用户与视频状态信息 + Publisher `json:"publisher,omitempty" bson:"publisher"` //发布者 +} + +// GetRichVideoRes GetRichVideoRes +func GetRichVideoRes(vid ObjectID, vInfoMap map[ObjectID]*VideoInfo, publMap map[uint64]*UserRes, locMap map[ObjectID]*LocInfo, statusMap map[ObjectID]VidStatus) RichVidRes { + res := GetVideoRes(vid, vInfoMap, publMap, locMap, statusMap) + if res.VideoInfo.ID == nil { + return RichVidRes{} + } + richRes := RichVidRes(res) + return richRes +} + +// GetRichVideoResList GetRichVideoResList +func GetRichVideoResList(uid uint64, vidList []ObjectID) ([]RichVidRes, error) { + return getRichVideoResList(uid, vidList, false) +} + +// GetRichVideoResListForSearch 保留当前仅能通过搜索入口访问的视频。 +func GetRichVideoResListForSearch(uid uint64, vidList []ObjectID) ([]RichVidRes, error) { + return getRichVideoResList(uid, vidList, true) +} + +func getRichVideoResList(uid uint64, vidList []ObjectID, allowSearchOnly bool) ([]RichVidRes, error) { + videoCount := len(vidList) + //vid->video Map + videoMap, err := videoMap(vidList, uid, allowSearchOnly) + if err != nil { + return nil, err + } + //Publisher + //发布者ID List + pidList := make([]uint64, 0, videoCount) + for _, video := range videoMap { + if video.PublisherID == nil { + continue + } + pidList = append(pidList, *video.PublisherID) + } + //LocationID List + lidList := make([]ObjectID, 0, videoCount) + for _, v := range videoMap { + if v.LocationID == nil { + continue + } + lidList = append(lidList, *v.LocationID) + } + var ( + wg = sync.WaitGroup{} + publMap = make(map[uint64]*UserRes) + locMap = make(map[ObjectID]*LocInfo) + statusMap = make(map[ObjectID]VidStatus) + ) + //一共4个map,每个携程填充一个Map + wg.Add(3) + common.Go(func() { + //PublisherID->User map + defer wg.Done() + m, err := UserResMap(pidList, uid) + if err != nil { + log.Error("GetVideoResList PublisherID->User error", log.E(err)) + return + } + publMap = m + }) + //Location map + common.Go(func() { + //LocationID->Location map + defer wg.Done() + m, err := LocMap(lidList) + if err != nil { + log.Error("GetVideoResList Location map error", log.E(err)) + return + } + locMap = m + }) + //Video Status map + common.Go(func() { + //Vid->视屏状态 map + defer wg.Done() + statusMap = VidStatusMap(vidList, uid) + }) + wg.Wait() + //组装 []RichVidRes + videoResList := make([]RichVidRes, 0, videoCount) + for _, vid := range vidList { + info := GetRichVideoRes(vid, videoMap, publMap, locMap, statusMap) + if info.Publisher.UID == nil { + continue + } + videoResList = append(videoResList, info) + } + return videoResList, nil +} + +// VideoListByKeyword 通过关键字匹配title和tagName获取VID List +func VideoListByKeyword(keyword string, skip int64, limit int64) ([]vidmod.ObjectID, error) { + tags, err := tagmod.GetTagsByKeyword(keyword, 0, 3) + if err != nil { + return nil, err + } + tidList := make([]primitive.ObjectID, len(tags)) + for i, tag := range tags { + tidList[i] = tag.ID + } + return vidmod.VIDListByKeywordOrTags(keyword, tidList, skip, limit) +} diff --git a/app/service/searcher/vidhkwsearcher/result.go b/app/service/searcher/vidhkwsearcher/result.go new file mode 100644 index 0000000..ad8aec1 --- /dev/null +++ b/app/service/searcher/vidhkwsearcher/result.go @@ -0,0 +1,37 @@ +package vidhkwsearcher + +import ( + "91porn-server/app/service/searcher" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type M = bson.M + +type VideoRes = searcher.VideoRes + +type ObjectID = primitive.ObjectID + +type Video = searcher.VideoInfo + +type Result struct { + dat []VideoRes + hasNext bool +} + +func (*Result) Types() searcher.ResultTypes { + return searcher.VidHotKeywordSearchResult +} + +func (r *Result) Data() interface{} { + return r.dat +} + +func (r *Result) Count() int64 { + return int64(len(r.dat)) +} + +func (r *Result) HasNext() bool { + return r.hasNext +} diff --git a/app/service/searcher/vidhkwsearcher/vidHotKeyWordSearcher.go b/app/service/searcher/vidhkwsearcher/vidHotKeyWordSearcher.go new file mode 100644 index 0000000..a4712ec --- /dev/null +++ b/app/service/searcher/vidhkwsearcher/vidHotKeyWordSearcher.go @@ -0,0 +1,112 @@ +package vidhkwsearcher + +import ( + "91porn-server/common/constant" + "errors" + "fmt" + "time" + + "91porn-server/app/service/searcher" + "91porn-server/app/service/searcher/vidsearcher" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/models/s/kwrankmod" +) + +// VidHKWSearcher VidHKWSearcher +type VidHKWSearcher struct { + uid uint64 + vidOpt searcher.Opter +} + +// NewVidHKWSearcher NewVidHKWSearcher +func NewVidHKWSearcher(uid uint64, vidOpt searcher.Opter) searcher.Searcher { + return &VidHKWSearcher{ + uid: uid, + vidOpt: vidOpt, + } +} + +type pair struct { + Word string + Result searcher.Resulter +} + +const timeout = 3 * time.Second +const gorouCountMax = 20 + +// Search Search +func (v *VidHKWSearcher) Search(unuse searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + limitEx1 := opt.Limit() + 1 + //获取热词 + words, err := kwrankmod.RecentRanking(limitEx1) + if err != nil { + return nil, errors.New("vidhkwsearcher RecentRanking error") + } + //没有热词 + if len(words) == 0 { + return &Result{[]VideoRes{}, false}, nil + } + hasNext := false + if len(words) > int(opt.Limit()) { + words = words[:opt.Limit()] + hasNext = true + } + gorouCount := opt.Limit() + if gorouCount > gorouCountMax { + gorouCount = gorouCountMax + } + ch := make(chan pair, gorouCount) + err_ch := make(chan error) + for _, w := range words { + word := w.KeyWord + common.Go(func() { + k := &searcher.Keyword{Word: word} + result, err := vidsearcher.NewVidSearcher(v.uid, constant.SearchSP, 0).Search(k, v.vidOpt) + if err != nil { + err_ch <- err + return + } + pair := pair{ + Word: word, + Result: result, + } + ch <- pair + }) + } + count := len(words) + resultMap := make(map[string]searcher.Resulter, count) + timer := time.NewTimer(timeout) + for i := 0; i < count; i++ { + timer.Reset(timeout) + select { + case pair := <-ch: + resultMap[pair.Word] = pair.Result + if i == count-1 { + return toResult(words, v.vidOpt.Limit(), resultMap, hasNext), nil + } + case err := <-err_ch: //获取出错前搜索到的视屏 + log.Error(fmt.Sprintf("VidKWSearcher error:%+v\n", err)) + return toResult(words, v.vidOpt.Limit(), resultMap, hasNext), nil + case <-timer.C: //获取超时前搜索到的视屏 + log.Warn(fmt.Sprintf("VidKWSearcher timeout::%+v\n", timeout)) + return toResult(words, v.vidOpt.Limit(), resultMap, hasNext), nil + } + } + return &Result{[]VideoRes{}, false}, nil +} + +func toResult(words []kwrankmod.Keyword, vidCountPer int64, resultMap map[string]searcher.Resulter, hasNext bool) searcher.Resulter { + resList := make([]VideoRes, 0, vidCountPer) + //还原视屏的排序 + for _, w := range words { + word := w.KeyWord + result, ok := resultMap[word] + if !ok { + continue + } + videoList := result.Data().([]VideoRes) + resList = append(resList, videoList...) + } + return &Result{resList, hasNext} +} diff --git a/app/service/searcher/vidhotsearcher/result.go b/app/service/searcher/vidhotsearcher/result.go new file mode 100644 index 0000000..46e8ff9 --- /dev/null +++ b/app/service/searcher/vidhotsearcher/result.go @@ -0,0 +1,40 @@ +package vidhotsearcher + +import "91porn-server/app/service/searcher" + +type VideoRes = searcher.VideoRes + +type RichVidRes = searcher.RichVidRes + +type ResultType = int + +const ( + Video ResultType = iota + RichVido +) + +type Result struct { + dat []VideoRes + richDat []RichVidRes + typ ResultType + hasNext bool +} + +func (*Result) Types() searcher.ResultTypes { + return searcher.VidPCountSearchResult +} + +func (r *Result) Data() interface{} { + if r.typ == Video { + return r.dat + } + return r.richDat +} + +func (r *Result) Count() int64 { + return int64(len(r.dat)) +} + +func (r *Result) HasNext() bool { + return r.hasNext +} diff --git a/app/service/searcher/vidhotsearcher/vidhotsearcher.go b/app/service/searcher/vidhotsearcher/vidhotsearcher.go new file mode 100644 index 0000000..af23bb0 --- /dev/null +++ b/app/service/searcher/vidhotsearcher/vidhotsearcher.go @@ -0,0 +1,117 @@ +package vidhotsearcher + +import ( + "time" + + "91porn-server/app/service/searcher" + "91porn-server/common/log" + "91porn-server/models/s/statvidmod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +// VidHotSearcher VidHotSearcher +type VidHotSearcher struct { + uid uint64 + typ ResultType +} + +// NewVidHotSearcher NewVidHotSearcher +func NewVidHotSearcher(uid uint64) searcher.Searcher { + return &VidHotSearcher{ + uid: uid, + typ: Video, + } +} + +// NewRichVidHotSearcher NewRichVidHotSearcher +func NewRichVidHotSearcher(uid uint64) searcher.Searcher { + return &VidHotSearcher{ + uid: uid, + typ: RichVido, + } +} + +// Search Search +func (v *VidHotSearcher) Search(unuse searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + var ( + vidList []ObjectID + countMap map[ObjectID]int64 + ) + limitEx := opt.Limit() + 1 + if isEarlyMorning1AM(time.Now()) { + vidList, err = vidmod.VIDListByCreatedAt(opt.Skip(), limitEx) + } else { + vidList, countMap, err = statvidmod.GetDailyPlayCountGroup(opt.Skip(), limitEx) + } + if err != nil { + return nil, err + } + if len(vidList) == 0 { + return &Result{ + dat: []VideoRes{}, + richDat: []RichVidRes{}, + typ: v.typ, + hasNext: false, + }, nil + } + hasNext := false + if len(vidList) > int(opt.Limit()) { + vidList = vidList[:opt.Limit()] + hasNext = true + } + if v.typ == Video { + videoResList, err := searcher.GetVideoResList(v.uid, vidList) + if err != nil { + return nil, err + } + setVideoResListPayCount(videoResList, countMap) + return &Result{ + dat: videoResList, + typ: v.typ, + hasNext: hasNext, + }, nil + } else { + richVideoResList, err := searcher.GetRichVideoResList(v.uid, vidList) + if err != nil { + return nil, err + } + setRichVideoResListPayCount(richVideoResList, countMap) + return &Result{ + richDat: richVideoResList, + typ: v.typ, + hasNext: hasNext, + }, nil + } +} + +func setVideoResListPayCount(list []VideoRes, countMap map[ObjectID]int64) { + for i := 0; i < len(list); i++ { + video := list[i] + vid := *video.VideoInfo.ID + playCount := int(countMap[vid]) + video.PlayCount = &playCount + list[i] = video + } +} + +func setRichVideoResListPayCount(list []RichVidRes, countMap map[ObjectID]int64) { + for i := 0; i < len(list); i++ { + video := list[i] + vid := *video.VideoInfo.ID + playCount := int(countMap[vid]) + video.PlayCount = &playCount + list[i] = video + } +} + +//1:00 - 1:10 +func isEarlyMorning1AM(cur time.Time) bool { + start := time.Date(cur.Year(), cur.Month(), cur.Day(), 1, 0, 0, 0, cur.Location()) + end := time.Date(cur.Year(), cur.Month(), cur.Day(), 1, 10, 0, 0, cur.Location()) + log.Info("vidhotsearcher isEarlyMorning1AM", log.Any("start", start.String()), log.Any("end", end.String()), log.Any("cur", cur.String())) + return cur.After(start) && cur.Before(end) +} diff --git a/app/service/searcher/vidpcountsearcher/result.go b/app/service/searcher/vidpcountsearcher/result.go new file mode 100644 index 0000000..e42d00c --- /dev/null +++ b/app/service/searcher/vidpcountsearcher/result.go @@ -0,0 +1,40 @@ +package vidpcountsearcher + +import "91porn-server/app/service/searcher" + +type VideoRes = searcher.VideoRes + +type RichVidRes = searcher.RichVidRes + +type ResultType = int + +const ( + Video ResultType = iota + RichVido +) + +type Result struct { + dat []VideoRes + richDat []RichVidRes + typ ResultType + hasNext bool +} + +func (*Result) Types() searcher.ResultTypes { + return searcher.VidPCountSearchResult +} + +func (r *Result) Data() interface{} { + if r.typ == Video { + return r.dat + } + return r.richDat +} + +func (r *Result) Count() int64 { + return int64(len(r.dat)) +} + +func (r *Result) HasNext() bool { + return r.hasNext +} diff --git a/app/service/searcher/vidpcountsearcher/vidPlayCountSearcher.go b/app/service/searcher/vidpcountsearcher/vidPlayCountSearcher.go new file mode 100644 index 0000000..16cdee1 --- /dev/null +++ b/app/service/searcher/vidpcountsearcher/vidPlayCountSearcher.go @@ -0,0 +1,73 @@ +package vidpcountsearcher + +import ( + "91porn-server/app/service/searcher" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +// VidPlayCountSearcher VidPlayCountSearcher +type VidPlayCountSearcher struct { + uid uint64 + typ ResultType +} + +// NewVidPlayCountSearcher NewVidPlayCountSearcher +func NewVidPlayCountSearcher(uid uint64) searcher.Searcher { + return &VidPlayCountSearcher{ + uid: uid, + typ: Video, + } +} + +// NewRichVidPlayCountSearcher NewRichVidPlayCountSearcher +func NewRichVidPlayCountSearcher(uid uint64) searcher.Searcher { + return &VidPlayCountSearcher{ + uid: uid, + typ: RichVido, + } +} + +// Search Search +func (v *VidPlayCountSearcher) Search(unuse searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + limitEx := opt.Limit() + 1 + vidList, err := vidmod.VIDListByFakePlayCount(opt.Skip(), limitEx) + if err != nil { + return nil, err + } + if len(vidList) == 0 { + return &Result{ + typ: v.typ, + dat: []VideoRes{}, + richDat: []RichVidRes{}, + }, nil + } + hasNext := false + if len(vidList) > int(opt.Limit()) { + hasNext = true + vidList = vidList[:opt.Limit()] + } + if v.typ == Video { + videoResList, err := searcher.GetVideoResList(v.uid, vidList) + if err != nil { + return nil, err + } + return &Result{ + dat: videoResList, + typ: v.typ, + hasNext: hasNext, + }, nil + } + richVideoResList, err := searcher.GetRichVideoResList(v.uid, vidList) + if err != nil { + return nil, err + } + return &Result{ + richDat: richVideoResList, + typ: v.typ, + hasNext: hasNext, + }, nil +} diff --git a/app/service/searcher/vidrichsearcher/result.go b/app/service/searcher/vidrichsearcher/result.go new file mode 100644 index 0000000..f224997 --- /dev/null +++ b/app/service/searcher/vidrichsearcher/result.go @@ -0,0 +1,28 @@ +package vidrichsearcher + +import "91porn-server/app/service/searcher" + +type RichVidRes = searcher.RichVidRes + +type VideoSy = searcher.VideoInfo + +type Result struct { + dat []RichVidRes + hasNext bool +} + +func (*Result) Types() searcher.ResultTypes { + return searcher.VidRichSearchResult +} + +func (r *Result) Data() interface{} { + return r.dat +} + +func (r *Result) Count() int64 { + return int64(len(r.dat)) +} + +func (r *Result) HasNext() bool { + return r.hasNext +} diff --git a/app/service/searcher/vidrichsearcher/vidRichSearcher.go b/app/service/searcher/vidrichsearcher/vidRichSearcher.go new file mode 100644 index 0000000..03d5580 --- /dev/null +++ b/app/service/searcher/vidrichsearcher/vidRichSearcher.go @@ -0,0 +1,76 @@ +package vidrichsearcher + +import ( + "errors" + + "91porn-server/app/appg" + "91porn-server/app/service/searcher" + "91porn-server/app/service/searcher/vidhotsearcher" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +// VidRichSearcher VidRichSearcher +type VidRichSearcher struct { + uid uint64 +} + +// NewVidRichSearcher NewVidRichSearcher +func NewVidRichSearcher(uid uint64) searcher.Searcher { + return &VidRichSearcher{ + uid: uid, + } +} + +// Search Search +func (v *VidRichSearcher) Search(keyword searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + kw, ok := keyword.Get().(string) + if !ok { + return nil, errors.New("keyword type error") + } + if kw == "" { //关键字为空,热搜 + return vidhotsearcher.NewRichVidHotSearcher(v.uid).Search(nil, opt) + } + limitEx1 := opt.Limit() + 1 + var vidList []vidmod.ObjectID + if appg.Conf.Elastic.IsActive { + vidList, err = esSearch(kw, opt) + } else { + vidList, err = searcher.VideoListByKeyword(kw, opt.Skip(), limitEx1) + } + if err != nil { + return nil, err + } + if len(vidList) == 0 { + return &Result{[]RichVidRes{}, false}, nil + } + hasNext := false + if len(vidList) > int(opt.Limit()) { + hasNext = true + vidList = vidList[:opt.Limit()] + } + richVideoResList, err := searcher.GetRichVideoResListForSearch(v.uid, vidList) + if err != nil { + return nil, err + } + return &Result{richVideoResList, hasNext}, nil +} + +func esSearch(keyword string, opt searcher.Opter) ([]vidmod.ObjectID, error) { + esVideos, err := vidmod.Search(keyword, opt.Skip(), opt.Limit()) + if err != nil { + return nil, err + } + esVideosLen := len(esVideos) + if esVideosLen == 0 { + return nil, nil + } + videosID := make([]vidmod.ObjectID, esVideosLen) + for i, v := range esVideos { + videosID[i] = v.ID + } + return videosID, nil +} diff --git a/app/service/searcher/vidsearcher/result.go b/app/service/searcher/vidsearcher/result.go new file mode 100644 index 0000000..b605fc1 --- /dev/null +++ b/app/service/searcher/vidsearcher/result.go @@ -0,0 +1,41 @@ +package vidsearcher + +import ( + "91porn-server/app/service/searcher" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// M M +type M = bson.M + +// A A +type A = bson.A + +type VideoRes = searcher.VideoRes + +type ObjectID = primitive.ObjectID + +type Video = searcher.VideoInfo + +type Result struct { + dat []VideoRes + hasNext bool +} + +func (*Result) Types() searcher.ResultTypes { + return searcher.VidSearchResult +} + +func (r *Result) Data() interface{} { + return r.dat +} + +func (r *Result) Count() int64 { + return int64(len(r.dat)) +} + +func (r *Result) HasNext() bool { + return r.hasNext +} diff --git a/app/service/searcher/vidsearcher/vidSearcher.go b/app/service/searcher/vidsearcher/vidSearcher.go new file mode 100644 index 0000000..7bb78f3 --- /dev/null +++ b/app/service/searcher/vidsearcher/vidSearcher.go @@ -0,0 +1,72 @@ +package vidsearcher + +import ( + "errors" + + "91porn-server/app/appg" + "91porn-server/app/service/searcher" + "91porn-server/app/service/searcher/vidhotsearcher" + "91porn-server/models/v/vidmod" +) + +// VidSearcher VidSearcher +type VidSearcher struct { + uid uint64 + vidType string + sortType int +} + +// NewVidSearcher NewVidSearcher +func NewVidSearcher(uid uint64, vidType string, sortType int) searcher.Searcher { + return &VidSearcher{uid: uid, vidType: vidType, sortType: sortType} +} + +// Search Search +func (v *VidSearcher) Search(keyword searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + kw, ok := keyword.Get().(string) + if !ok { + return nil, errors.New("keyword type error") + } + if kw == "" { //关键字为空,热搜 + return vidhotsearcher.NewVidHotSearcher(v.uid).Search(nil, opt) + } + limitEx1 := opt.Limit() + 1 + var vidList []vidmod.ObjectID + if appg.Conf.Elastic.IsActive { // 使用ES搜索视频 + vidList, err = esSearch(kw, v.vidType, opt, v.sortType) + } else { + vidList, err = searcher.VideoListByKeyword(kw, opt.Skip(), limitEx1) + } + if err != nil { + return nil, err + } + if len(vidList) == 0 { + return &Result{[]VideoRes{}, false}, nil + } + hasNext := false + if len(vidList) > int(opt.Limit()) { + hasNext = true + vidList = vidList[:opt.Limit()] + } + videoResList, err := searcher.GetVideoResListForSearch(v.uid, vidList) + if err != nil { + return nil, err + } + return &Result{videoResList, hasNext}, nil +} + +func esSearch(keyword string, vidType string, opt searcher.Opter, sortType int) ([]vidmod.ObjectID, error) { + esVideos, err := vidmod.DirectSearch(keyword, vidType, sortType, opt.Skip(), opt.Limit()+1) + if err != nil { + return nil, nil + } + esVideosLen := len(esVideos) + if esVideosLen == 0 { + return nil, err + } + videosID := make([]vidmod.ObjectID, esVideosLen) + for i, v := range esVideos { + videosID[i] = v.ID + } + return videosID, nil +} diff --git a/app/service/searcher/vidtagsearcher/result.go b/app/service/searcher/vidtagsearcher/result.go new file mode 100644 index 0000000..3c5511b --- /dev/null +++ b/app/service/searcher/vidtagsearcher/result.go @@ -0,0 +1,34 @@ +package vidtagsearcher + +import ( + "91porn-server/app/service/searcher" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type VideoRes = searcher.VideoRes + +type ObjectID = primitive.ObjectID + +type Video = searcher.VideoInfo + +type Result struct { + dat []VideoRes + hasNext bool +} + +func (*Result) Types() searcher.ResultTypes { + return searcher.VidTagSearchResult +} + +func (r *Result) Data() interface{} { + return r.dat +} + +func (r *Result) Count() int64 { + return int64(len(r.dat)) +} + +func (r *Result) HasNext() bool { + return r.hasNext +} diff --git a/app/service/searcher/vidtagsearcher/vidTagSearcher.go b/app/service/searcher/vidtagsearcher/vidTagSearcher.go new file mode 100644 index 0000000..4da75eb --- /dev/null +++ b/app/service/searcher/vidtagsearcher/vidTagSearcher.go @@ -0,0 +1,63 @@ +package vidtagsearcher + +import ( + "errors" + + "91porn-server/app/service/searcher" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson" +) + +// M M +type M = bson.M + +// A A +type A = bson.A + +// VidTagSearcher VidTagSearcher +type VidTagSearcher struct { + uid uint64 +} + +var ( + videoListByTagID = vidmod.VideoListByTagID + getVideoResListForSearch = searcher.GetVideoResListForSearch +) + +// NewVidWonderSearcher NewVidWonderSearcher +func NewVidTagSearcher(uid uint64) searcher.Searcher { + return &VidTagSearcher{ + uid: uid, + } +} + +// Search Search +func (v *VidTagSearcher) Search(tid searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + tagId, ok := tid.Get().(ObjectID) + if !ok { + return nil, errors.New("keyword type error") + } + limitEx1 := opt.Limit() + 1 + videoList, err := videoListByTagID(tagId, opt.Sort(), opt.Skip(), limitEx1) + if err != nil { + return nil, err + } + if len(videoList) == 0 { + return &Result{[]VideoRes{}, false}, nil + } + hasNext := false + if len(videoList) > int(opt.Limit()) { + hasNext = true + videoList = videoList[:opt.Limit()] + } + vidList := make([]ObjectID, len(videoList)) + for i, video := range videoList { + vidList[i] = video.ID + } + videoResList, err := getVideoResListForSearch(v.uid, vidList) + if err != nil { + return nil, err + } + return &Result{videoResList, hasNext}, nil +} diff --git a/app/service/searcher/vidtagsearcher/vidTagSearcher_test.go b/app/service/searcher/vidtagsearcher/vidTagSearcher_test.go new file mode 100644 index 0000000..7618538 --- /dev/null +++ b/app/service/searcher/vidtagsearcher/vidTagSearcher_test.go @@ -0,0 +1,78 @@ +package vidtagsearcher + +import ( + "reflect" + "testing" + + "91porn-server/app/service/searcher" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestSearchUsesSearchVisibleVideoResults(t *testing.T) { + originalVideoListByTagID := videoListByTagID + originalGetVideoResListForSearch := getVideoResListForSearch + t.Cleanup(func() { + videoListByTagID = originalVideoListByTagID + getVideoResListForSearch = originalGetVideoResListForSearch + }) + + tagID := primitive.NewObjectID() + videoIDs := []primitive.ObjectID{ + primitive.NewObjectID(), + primitive.NewObjectID(), + primitive.NewObjectID(), + } + videoListByTagID = func(gotTagID primitive.ObjectID, sort bson.D, skip, limit int64) ([]vidmod.VideoModel, error) { + if gotTagID != tagID { + t.Fatalf("tagID = %s, want %s", gotTagID.Hex(), tagID.Hex()) + } + if skip != 0 || limit != 3 { + t.Fatalf("skip/limit = %d/%d, want 0/3", skip, limit) + } + return []vidmod.VideoModel{ + {ID: videoIDs[0]}, + {ID: videoIDs[1]}, + {ID: videoIDs[2]}, + }, nil + } + + const uid = uint64(17803584) + searchAccessToken := "search-token" + getVideoResListForSearch = func(gotUID uint64, gotVideoIDs []primitive.ObjectID) ([]searcher.VideoRes, error) { + if gotUID != uid { + t.Fatalf("uid = %d, want %d", gotUID, uid) + } + if !reflect.DeepEqual(gotVideoIDs, videoIDs[:2]) { + t.Fatalf("video IDs = %v, want %v", gotVideoIDs, videoIDs[:2]) + } + return []searcher.VideoRes{ + {VideoInfo: searcher.VideoInfo{ID: &gotVideoIDs[0], SearchAccessToken: &searchAccessToken}}, + {VideoInfo: searcher.VideoInfo{ID: &gotVideoIDs[1], SearchAccessToken: &searchAccessToken}}, + }, nil + } + + option := &searcher.Option{} + option.SetLimit(2) + result, err := NewVidTagSearcher(uid).Search(&searcher.ObjID{ID: tagID}, option) + if err != nil { + t.Fatalf("Search() error = %v", err) + } + if !result.HasNext() { + t.Fatal("Search() hasNext = false, want true") + } + videos, ok := result.Data().([]searcher.VideoRes) + if !ok { + t.Fatalf("Search() data type = %T, want []searcher.VideoRes", result.Data()) + } + if len(videos) != 2 { + t.Fatalf("Search() result length = %d, want 2", len(videos)) + } + for i, video := range videos { + if video.SearchAccessToken == nil || *video.SearchAccessToken != searchAccessToken { + t.Fatalf("video %d searchAccessToken = %v, want %q", i, video.SearchAccessToken, searchAccessToken) + } + } +} diff --git a/app/service/searcher/vidtonesearcher/freeArea.go b/app/service/searcher/vidtonesearcher/freeArea.go new file mode 100644 index 0000000..cf46401 --- /dev/null +++ b/app/service/searcher/vidtonesearcher/freeArea.go @@ -0,0 +1,55 @@ +package vidtonesearcher + +import ( + "91porn-server/app/service/searcher" + "91porn-server/common/log" + "91porn-server/models/v/tonerecomod" + "91porn-server/models/v/vidmod" +) + +type freeAreaSearcher struct { + uid uint64 +} + +func newFreeAreaSearcher(uid uint64) searcher.Searcher { + return &freeAreaSearcher{ + uid: uid, + } +} + +// Search Search +func (f *freeAreaSearcher) Search(unuse searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + limitEx1 := opt.Limit() + 1 + vidList, err := vidmod.GetFreeVidList(opt.Sort(), opt.Skip(), limitEx1) + if err != nil { + return nil, err + } + if len(vidList) == 0 { + return &Result{ + &Resp{ + Theme: tonerecomod.FreeArea, + List: []VideoRes{}, + PayCount: &zero, + }, + }, nil + } + hasNext := false + if len(vidList) > int(opt.Limit()) { + hasNext = true + vidList = vidList[:opt.Limit()] + } + videoInfoList, err := searcher.GetVideoResList(f.uid, vidList) + if err != nil { + return nil, err + } + log.Info("freeArea search result", log.Any("totalVideoCount", len(vidList)), log.Any("respVideoCount", len(videoInfoList))) + freeAreaPlayCount := GetToneValue().FreeAreaPlayCount + return &Result{ + &Resp{ + List: videoInfoList, + Theme: tonerecomod.FreeArea, + PlayCount: &freeAreaPlayCount, + HasNext: hasNext, + }, + }, nil +} diff --git a/app/service/searcher/vidtonesearcher/goldCoinArea.go b/app/service/searcher/vidtonesearcher/goldCoinArea.go new file mode 100644 index 0000000..a512189 --- /dev/null +++ b/app/service/searcher/vidtonesearcher/goldCoinArea.go @@ -0,0 +1,80 @@ +package vidtonesearcher + +import ( + "91porn-server/app/service/searcher" + "91porn-server/common/log" + "91porn-server/models/s/statvidtotalmod" + "91porn-server/models/v/tonerecomod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +type VideoSy = searcher.VideoInfo + +// VidToneSearcher VidToneSearcher +type goldCoinAreaSearcher struct { + uid uint64 +} + +var zero = int64(0) + +// newGoldCoinAreaSearcher newGoldCoinAreaSearcher +func newGoldCoinAreaSearcher(uid uint64) searcher.Searcher { + return &goldCoinAreaSearcher{ + uid: uid, + } +} + +// Search Search +func (g *goldCoinAreaSearcher) Search(unuse searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + limitEx1 := opt.Limit() + 1 + vidList, err := vidmod.GetPayVidList(opt.Sort(), opt.Skip(), limitEx1) + if err != nil { + return nil, err + } + if len(vidList) == 0 { + return &Result{ + &Resp{ + Theme: tonerecomod.GoldCoinArea, + List: []VideoRes{}, + PayCount: &zero, + }, + }, nil + } + hasNext := false + if len(vidList) > int(opt.Limit()) { + hasNext = true + vidList = vidList[:opt.Limit()] + } + videoInfoList, err := searcher.GetVideoResList(g.uid, vidList) + if err != nil { + return nil, err + } + log.Info("goldCoinArea search result", log.Any("totalVideoCount", len(vidList)), log.Any("respVideoCount", len(videoInfoList))) + payCountMap, err := statvidtotalmod.GetPayCountMap(vidList) + if err != nil { + return nil, err + } + videoInfoPCList := make([]VideoRes, 0, len(videoInfoList)) + for _, info := range videoInfoList { + if info.VideoInfo.ID == nil { + continue + } + id := *info.VideoInfo.ID + payCount := int(payCountMap[id]) + info.VideoInfo.PayCount = &payCount + videoInfoPCList = append(videoInfoPCList, info) + } + goldCoinAreaPayCount := GetToneValue().GoldCoinAreaPayCount + return &Result{ + &Resp{ + List: videoInfoPCList, + Theme: tonerecomod.GoldCoinArea, + PayCount: &goldCoinAreaPayCount, + HasNext: hasNext, + }, + }, nil +} diff --git a/app/service/searcher/vidtonesearcher/latestUpload.go b/app/service/searcher/vidtonesearcher/latestUpload.go new file mode 100644 index 0000000..9f9ea14 --- /dev/null +++ b/app/service/searcher/vidtonesearcher/latestUpload.go @@ -0,0 +1,58 @@ +package vidtonesearcher + +import ( + "91porn-server/app/service/searcher" + "91porn-server/models/v/tonerecomod" + "91porn-server/models/v/vidmod" +) + +// latestUpload latestUpload +type latestUploadSearcher struct { + uid uint64 +} + +// latestUpload latestUpload +func newLatestUploadSearcher(uid uint64) searcher.Searcher { + return &latestUploadSearcher{ + uid: uid, + } +} + +// Search Search +func (l *latestUploadSearcher) Search(unuse searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + limit := opt.Limit() + skip := opt.Skip() + limitEx1 := limit + 1 + vidList, err := vidmod.VIDListByCreatedAt(skip, limitEx1) + if err != nil { + return nil, err + } + if len(vidList) == 0 { + return &Result{ + &Resp{ + Theme: tonerecomod.LatestUpload, + List: []VideoRes{}, + PlayCount: &zero, + HasNext: false, + }, + }, nil + } + hasNext := false + if len(vidList) > int(limit) { + hasNext = true + vidList = vidList[:opt.Limit()] + } + videoResList, err := searcher.GetVideoResList(l.uid, vidList) + if err != nil { + return nil, err + } + latestUploadPlayCount := GetToneValue().LatestUploadPlayCount + return &Result{ + &Resp{ + List: videoResList, + Theme: tonerecomod.LatestUpload, + PlayCount: &latestUploadPlayCount, + HasNext: hasNext, + }, + }, nil +} diff --git a/app/service/searcher/vidtonesearcher/mostComments.go b/app/service/searcher/vidtonesearcher/mostComments.go new file mode 100644 index 0000000..89abd40 --- /dev/null +++ b/app/service/searcher/vidtonesearcher/mostComments.go @@ -0,0 +1,57 @@ +package vidtonesearcher + +import ( + "91porn-server/app/service/searcher" + "91porn-server/models/v/tonerecomod" + "91porn-server/models/v/vidmod" +) + +// mostCommentsSearcher mostCommentsSearcher +type mostCommentsSearcher struct { + uid uint64 +} + +// newMostCommentsSearcher newMostCommentsSearcher +func newMostCommentsSearcher(uid uint64) searcher.Searcher { + return &mostCommentsSearcher{ + uid: uid, + } +} + +// Search Search +func (g *mostCommentsSearcher) Search(unuse searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + limit := opt.Limit() + limitEx1 := limit + 1 + vidList, err := vidmod.VIDListSortByFakeCommentCount(opt.Skip(), limitEx1) + if err != nil { + return nil, err + } + if len(vidList) == 0 { + return &Result{ + &Resp{ + Theme: tonerecomod.MostComments, + List: []VideoRes{}, + PlayCount: &zero, + HasNext: false, + }, + }, nil + } + hasNext := false + if len(vidList) > int(limit) { + hasNext = true + vidList = vidList[:opt.Limit()] + } + videoResList, err := searcher.GetVideoResList(g.uid, vidList) + if err != nil { + return nil, err + } + mostCommentsPlayCount := GetToneValue().MostCommentsPlayCount + return &Result{ + &Resp{ + List: videoResList, + Theme: tonerecomod.MostComments, + PlayCount: &mostCommentsPlayCount, + HasNext: hasNext, + }, + }, nil +} diff --git a/app/service/searcher/vidtonesearcher/mostLikes.go b/app/service/searcher/vidtonesearcher/mostLikes.go new file mode 100644 index 0000000..606cb9c --- /dev/null +++ b/app/service/searcher/vidtonesearcher/mostLikes.go @@ -0,0 +1,57 @@ +package vidtonesearcher + +import ( + "91porn-server/app/service/searcher" + "91porn-server/models/v/tonerecomod" + "91porn-server/models/v/vidmod" +) + +// latestUpload latestUpload +type mostLikesSearcher struct { + uid uint64 +} + +// latestUpload latestUpload +func newMostLikesSearcher(uid uint64) searcher.Searcher { + return &mostLikesSearcher{ + uid: uid, + } +} + +// Search Search +func (g *mostLikesSearcher) Search(unuse searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + limit := opt.Limit() + limitEx1 := limit + 1 + vidList, err := vidmod.VIDListByFakeLikeCount(opt.Skip(), limitEx1) + if err != nil { + return nil, err + } + if len(vidList) == 0 { + return &Result{ + &Resp{ + Theme: tonerecomod.MostLikes, + List: []VideoRes{}, + PlayCount: &zero, + HasNext: false, + }, + }, nil + } + hasNext := false + if len(vidList) > int(limit) { + hasNext = true + vidList = vidList[:opt.Limit()] + } + videoResList, err := searcher.GetVideoResList(g.uid, vidList) + if err != nil { + return nil, err + } + mostLikesPlayCount := GetToneValue().MostLikesPlayCount + return &Result{ + &Resp{ + List: videoResList, + Theme: tonerecomod.MostLikes, + PlayCount: &mostLikesPlayCount, + HasNext: hasNext, + }, + }, nil +} diff --git a/app/service/searcher/vidtonesearcher/officialRecom.go b/app/service/searcher/vidtonesearcher/officialRecom.go new file mode 100644 index 0000000..da428fd --- /dev/null +++ b/app/service/searcher/vidtonesearcher/officialRecom.go @@ -0,0 +1,56 @@ +package vidtonesearcher + +import ( + "91porn-server/app/service/searcher" + "91porn-server/models/v/tonerecomod" + "91porn-server/models/v/vidmod" +) + +// VidToneSearcher VidToneSearcher +type officialRecomSearcher struct { + uid uint64 +} + +// officialRecomSearcher officialRecomSearcher +func newOfficialRecomSearcher(uid uint64) searcher.Searcher { + return &officialRecomSearcher{ + uid: uid, + } +} + +// Search Search +func (g *officialRecomSearcher) Search(unuse searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + limitEx1 := opt.Limit() + 1 + vidList, err := vidmod.VIDListToneOfficialRecom(opt.Skip(), limitEx1) + if err != nil { + return nil, err + } + if len(vidList) == 0 { + return &Result{ + &Resp{ + Theme: tonerecomod.OfficialRecom, + List: []VideoRes{}, + PlayCount: &zero, + HasNext: false, + }, + }, nil + } + hasNext := false + if len(vidList) > int(opt.Limit()) { + hasNext = true + vidList = vidList[:opt.Limit()] + } + videoResList, err := searcher.GetVideoResList(g.uid, vidList) + if err != nil { + return nil, err + } + officialRecomPlayCount := GetToneValue().OfficialRecomPlayCount + return &Result{ + &Resp{ + List: videoResList, + Theme: tonerecomod.OfficialRecom, + PlayCount: &officialRecomPlayCount, + HasNext: hasNext, + }, + }, nil +} diff --git a/app/service/searcher/vidtonesearcher/playAtMost.go b/app/service/searcher/vidtonesearcher/playAtMost.go new file mode 100644 index 0000000..4ac9e1d --- /dev/null +++ b/app/service/searcher/vidtonesearcher/playAtMost.go @@ -0,0 +1,57 @@ +package vidtonesearcher + +import ( + "91porn-server/app/service/searcher" + "91porn-server/models/v/tonerecomod" + "91porn-server/models/v/vidmod" +) + +// playAtMostSearcher playAtMostSearcher +type playAtMostSearcher struct { + uid uint64 +} + +// newPlayAtMostSearcher newPlayAtMostSearcher +func newPlayAtMostSearcher(uid uint64) searcher.Searcher { + return &playAtMostSearcher{ + uid: uid, + } +} + +// Search Search +func (g *playAtMostSearcher) Search(unuse searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + limit := opt.Limit() + limitEx1 := limit + 1 + vidList, err := vidmod.VIDListByFakePlayCount(opt.Skip(), limitEx1) + if err != nil { + return nil, err + } + if len(vidList) == 0 { + return &Result{ + &Resp{ + Theme: tonerecomod.PlayAtMost, + List: []VideoRes{}, + PlayCount: &zero, + HasNext: false, + }, + }, nil + } + hasNext := false + if len(vidList) > int(limit) { + hasNext = true + vidList = vidList[:opt.Limit()] + } + videoResList, err := searcher.GetVideoResList(g.uid, vidList) + if err != nil { + return nil, err + } + playAtMostPlayCount := GetToneValue().PlayAtMostPlayCount + return &Result{ + &Resp{ + List: videoResList, + Theme: tonerecomod.PlayAtMost, + PlayCount: &playAtMostPlayCount, + HasNext: hasNext, + }, + }, nil +} diff --git a/app/service/searcher/vidtonesearcher/result.go b/app/service/searcher/vidtonesearcher/result.go new file mode 100644 index 0000000..b00a483 --- /dev/null +++ b/app/service/searcher/vidtonesearcher/result.go @@ -0,0 +1,51 @@ +package vidtonesearcher + +import ( + "91porn-server/app/service/searcher" + + "go.mongodb.org/mongo-driver/bson" +) + +// M M +type M = bson.M + +// A A +type A = bson.A + +type VideoRes = searcher.VideoRes + +type Resp struct { + List []VideoRes `json:"list"` + Theme string `json:"theme"` + PlayCount *int64 `json:"playCount,omitempty"` + PayCount *int64 `json:"payCount,omitempty"` + HasNext bool `json:"hasNext"` +} + +type Result struct { + dat *Resp +} + +const MaxVideoCount = 400 + +func (*Result) Types() searcher.ResultTypes { + return searcher.VidToneSearchResult +} + +func (r *Result) Data() interface{} { + return r.dat +} + +func (r *Result) Count() int64 { + if r.dat == nil { + return 0 + } + return 1 +} + +func (r *Result) HasNext() bool { + if r.dat == nil { + return false + } + return r.dat.HasNext +} diff --git a/app/service/searcher/vidtonesearcher/vidToneSearcher.go b/app/service/searcher/vidtonesearcher/vidToneSearcher.go new file mode 100644 index 0000000..6c0092e --- /dev/null +++ b/app/service/searcher/vidtonesearcher/vidToneSearcher.go @@ -0,0 +1,64 @@ +package vidtonesearcher + +import ( + "fmt" + + "91porn-server/app/appg" + "91porn-server/app/service/searcher" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/models/v/tonemod" + "91porn-server/models/v/tonerecomod" +) + +type ToneValue = tonemod.ToneValue + +// VidToneSearcher VidToneSearcher +type VidToneSearcher struct { + uid uint64 + theme tonerecomod.ThemeType +} + +// NewVidToneSearcher NewVidToneSearcher +func NewVidToneSearcher(theme tonerecomod.ThemeType, uid uint64) searcher.Searcher { + return &VidToneSearcher{ + uid: uid, + theme: theme, + } +} + +// Realm Realm +func (searcher *VidToneSearcher) Theme() string { + return searcher.theme +} + +// Search Search +func (v *VidToneSearcher) Search(keyword searcher.KeyWorder, opt searcher.Opter) (result searcher.Resulter, err error) { + switch v.theme { + case tonerecomod.OfficialRecom: + return newOfficialRecomSearcher(v.uid).Search(nil, opt) + case tonerecomod.GoldCoinArea: + return newGoldCoinAreaSearcher(v.uid).Search(nil, opt) + case tonerecomod.LatestUpload: + return newLatestUploadSearcher(v.uid).Search(nil, opt) + case tonerecomod.MostLikes: + return newMostLikesSearcher(v.uid).Search(nil, opt) + case tonerecomod.PlayAtMost: + return newPlayAtMostSearcher(v.uid).Search(nil, opt) + case tonerecomod.MostComments: + return newMostCommentsSearcher(v.uid).Search(nil, opt) + case tonerecomod.FreeArea: + return newFreeAreaSearcher(v.uid).Search(nil, opt) + default: + return nil, fmt.Errorf("search vidtonesearcher invalid theme: %s", v.theme) + } +} + +func GetToneValue() ToneValue { + var toneValue ToneValue + redisKey := redisconst.ToneKey() + if err := appg.Redis.Scan(redisKey, &toneValue); err != nil { + log.Warn("search vidtonesearcher GetToneValue Scan error", log.E(err)) + } + return toneValue +} diff --git a/app/service/shareser/recommend_share.go b/app/service/shareser/recommend_share.go new file mode 100644 index 0000000..f93ed96 --- /dev/null +++ b/app/service/shareser/recommend_share.go @@ -0,0 +1,130 @@ +package shareser + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + "time" + + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + recommendShareEventIDMaxLength = 128 + recommendShareEventDedupTTL = 7 * 24 * time.Hour + recommendShareDailyDedupTTL = 48 * time.Hour + recommendShareCleanupTimeout = 5 * time.Second +) + +type recommendShareDeduper interface { + SetNXContext( + ctx context.Context, + key string, + value interface{}, + expiration time.Duration, + ) (bool, error) + DelContext(ctx context.Context, keys ...string) (int64, error) +} + +type recommendShareIncrement func(context.Context, primitive.ObjectID) error + +func incrementRecommendShareOnce( + ctx context.Context, + deduper recommendShareDeduper, + uid uint64, + videoID primitive.ObjectID, + eventID string, + now time.Time, +) error { + return incrementRecommendShareOnceWith( + ctx, + deduper, + uid, + videoID, + eventID, + now, + func(ctx context.Context, id primitive.ObjectID) error { + return vidmod.IncrementRecommendInteractionContext( + ctx, + id, + vidmod.RecommendInteractionShare, + ) + }, + ) +} + +func incrementRecommendShareOnceWith( + ctx context.Context, + deduper recommendShareDeduper, + uid uint64, + videoID primitive.ObjectID, + eventID string, + now time.Time, + increment recommendShareIncrement, +) error { + if err := ctx.Err(); err != nil { + return err + } + if deduper == nil { + return fmt.Errorf("recommend share deduper is nil") + } + if uid == 0 || videoID.IsZero() { + return nil + } + eventID = strings.TrimSpace(eventID) + if len(eventID) > recommendShareEventIDMaxLength { + return fmt.Errorf("share eventId exceeds %d characters", recommendShareEventIDMaxLength) + } + + // 无论客户端是否提供 eventId,同一用户、视频、自然日只累计一次推荐分享分, + // 避免客户端通过不断生成新 eventId 刷高权重。eventId 仍用于跨日重试幂等。 + day := now.In(time.FixedZone("CST", 8*60*60)).Format("20060102") + dailyKey := recommendShareDedupKey(uid, videoID, "day:"+day) + dailyAcquired, err := deduper.SetNXContext( + ctx, dailyKey, "1", recommendShareDailyDedupTTL, + ) + if err != nil || !dailyAcquired { + return err + } + + keysToRelease := []string{dailyKey} + if eventID != "" { + eventKey := recommendShareDedupKey(uid, videoID, "event:"+eventID) + eventAcquired, eventErr := deduper.SetNXContext( + ctx, eventKey, "1", recommendShareEventDedupTTL, + ) + if eventErr != nil || !eventAcquired { + releaseRecommendShareKeys(deduper, keysToRelease...) + return eventErr + } + keysToRelease = append(keysToRelease, eventKey) + } + + err = increment(ctx, videoID) + if err != nil { + // 推荐累计失败时释放幂等标记,使同一事件后续重试仍有补偿机会。 + releaseRecommendShareKeys(deduper, keysToRelease...) + return err + } + return nil +} + +func releaseRecommendShareKeys(deduper recommendShareDeduper, keys ...string) { + if deduper == nil || len(keys) == 0 { + return + } + cleanupCtx, cancel := context.WithTimeout( + context.Background(), recommendShareCleanupTimeout, + ) + defer cancel() + _, _ = deduper.DelContext(cleanupCtx, keys...) +} + +func recommendShareDedupKey(uid uint64, videoID primitive.ObjectID, scope string) string { + sum := sha256.Sum256([]byte(fmt.Sprintf("%d:%s:%s", uid, videoID.Hex(), scope))) + return "recommend:short:share-dedup:" + hex.EncodeToString(sum[:]) +} diff --git a/app/service/shareser/recommend_share_test.go b/app/service/shareser/recommend_share_test.go new file mode 100644 index 0000000..e9e1c2c --- /dev/null +++ b/app/service/shareser/recommend_share_test.go @@ -0,0 +1,250 @@ +package shareser + +import ( + "context" + "errors" + "sync" + "testing" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type fakeRecommendShareDeduper struct { + mu sync.Mutex + keys map[string]bool + ttls []time.Duration + err error +} + +func (f *fakeRecommendShareDeduper) SetNXContext( + _ context.Context, + key string, + _ interface{}, + expiration time.Duration, +) (bool, error) { + f.mu.Lock() + defer f.mu.Unlock() + if f.err != nil { + return false, f.err + } + f.ttls = append(f.ttls, expiration) + if f.keys == nil { + f.keys = make(map[string]bool) + } + if f.keys[key] { + return false, nil + } + f.keys[key] = true + return true, nil +} + +func (f *fakeRecommendShareDeduper) DelContext( + _ context.Context, + keys ...string, +) (int64, error) { + f.mu.Lock() + defer f.mu.Unlock() + var removed int64 + for _, key := range keys { + if f.keys[key] { + delete(f.keys, key) + removed++ + } + } + return removed, nil +} + +func TestIncrementRecommendShareOnceDeduplicatesEvent(t *testing.T) { + store := &fakeRecommendShareDeduper{} + id := primitive.NewObjectID() + now := time.Date(2026, 7, 31, 8, 0, 0, 0, time.UTC) + calls := 0 + increment := func(context.Context, primitive.ObjectID) error { + calls++ + return nil + } + for i := 0; i < 2; i++ { + if err := incrementRecommendShareOnceWith( + context.Background(), store, 123, id, "event-1", now, increment, + ); err != nil { + t.Fatal(err) + } + } + if calls != 1 { + t.Fatalf("increment calls = %d, want 1", calls) + } + if len(store.ttls) != 3 || + store.ttls[0] != recommendShareDailyDedupTTL || + store.ttls[1] != recommendShareEventDedupTTL || + store.ttls[2] != recommendShareDailyDedupTTL { + t.Fatalf("dedupe TTL calls = %v", store.ttls) + } +} + +func TestIncrementRecommendShareOnceCapsDifferentEventsPerDay(t *testing.T) { + store := &fakeRecommendShareDeduper{} + id := primitive.NewObjectID() + now := time.Date(2026, 7, 31, 8, 0, 0, 0, time.UTC) + calls := 0 + increment := func(context.Context, primitive.ObjectID) error { + calls++ + return nil + } + for _, eventID := range []string{"event-1", "event-2", "event-3"} { + if err := incrementRecommendShareOnceWith( + context.Background(), store, 123, id, eventID, now, increment, + ); err != nil { + t.Fatal(err) + } + } + if calls != 1 { + t.Fatalf("increment calls = %d, want one per user/video/day", calls) + } +} + +func TestIncrementRecommendShareOnceEventRetryDoesNotConsumeNextDayQuota(t *testing.T) { + store := &fakeRecommendShareDeduper{} + id := primitive.NewObjectID() + first := time.Date(2026, 7, 31, 15, 59, 0, 0, time.UTC) + nextDay := first.Add(2 * time.Minute) + calls := 0 + increment := func(context.Context, primitive.ObjectID) error { + calls++ + return nil + } + for _, step := range []struct { + eventID string + now time.Time + }{ + {eventID: "event-1", now: first}, + {eventID: "event-1", now: nextDay}, + {eventID: "event-2", now: nextDay}, + } { + if err := incrementRecommendShareOnceWith( + context.Background(), store, 123, id, step.eventID, step.now, increment, + ); err != nil { + t.Fatal(err) + } + } + if calls != 2 { + t.Fatalf("increment calls = %d, want one on each day", calls) + } +} + +func TestIncrementRecommendShareOnceLegacyScopesByCSTDay(t *testing.T) { + store := &fakeRecommendShareDeduper{} + id := primitive.NewObjectID() + incremented := 0 + increment := func(context.Context, primitive.ObjectID) error { + incremented++ + return nil + } + first := time.Date(2026, 7, 31, 15, 59, 0, 0, time.UTC) + second := first.Add(2 * time.Minute) + for _, now := range []time.Time{first, first, second} { + if err := incrementRecommendShareOnceWith( + context.Background(), store, 123, id, "", now, increment, + ); err != nil { + t.Fatal(err) + } + } + if incremented != 2 { + t.Fatalf("increment calls = %d, want 2 days", incremented) + } +} + +func TestIncrementRecommendShareOnceRedisFailureDoesNotIncrement(t *testing.T) { + wantErr := errors.New("redis unavailable") + store := &fakeRecommendShareDeduper{err: wantErr} + calls := 0 + err := incrementRecommendShareOnceWith( + context.Background(), + store, + 123, + primitive.NewObjectID(), + "event", + time.Now(), + func(context.Context, primitive.ObjectID) error { + calls++ + return nil + }, + ) + if !errors.Is(err, wantErr) { + t.Fatalf("error = %v, want %v", err, wantErr) + } + if calls != 0 { + t.Fatalf("increment calls = %d, want 0", calls) + } +} + +func TestIncrementRecommendShareOnceReleasesKeyAfterFailure(t *testing.T) { + store := &fakeRecommendShareDeduper{} + id := primitive.NewObjectID() + wantErr := errors.New("injected") + calls := 0 + increment := func(context.Context, primitive.ObjectID) error { + calls++ + if calls == 1 { + return wantErr + } + return nil + } + if err := incrementRecommendShareOnceWith( + context.Background(), store, 123, id, "event", time.Now(), increment, + ); !errors.Is(err, wantErr) { + t.Fatalf("first error = %v, want injected", err) + } + if err := incrementRecommendShareOnceWith( + context.Background(), store, 123, id, "event", time.Now(), increment, + ); err != nil { + t.Fatalf("retry error = %v", err) + } + if calls != 2 { + t.Fatalf("increment calls = %d, want 2", calls) + } +} + +func TestIncrementRecommendShareOnceConcurrent(t *testing.T) { + store := &fakeRecommendShareDeduper{} + id := primitive.NewObjectID() + var mu sync.Mutex + calls := 0 + increment := func(context.Context, primitive.ObjectID) error { + mu.Lock() + calls++ + mu.Unlock() + return nil + } + var wg sync.WaitGroup + for i := 0; i < 32; i++ { + wg.Add(1) + go func() { + defer wg.Done() + if err := incrementRecommendShareOnceWith( + context.Background(), store, 123, id, "event", time.Now(), increment, + ); err != nil { + t.Errorf("increment error = %v", err) + } + }() + } + wg.Wait() + if calls != 1 { + t.Fatalf("increment calls = %d, want 1", calls) + } +} + +func TestIncrementRecommendShareOnceRejectsLongEventID(t *testing.T) { + err := incrementRecommendShareOnceWith( + context.Background(), + &fakeRecommendShareDeduper{}, + 123, + primitive.NewObjectID(), + string(make([]byte, recommendShareEventIDMaxLength+1)), + time.Now(), + func(context.Context, primitive.ObjectID) error { return nil }, + ) + if err == nil { + t.Fatal("expected long eventId error") + } +} diff --git a/app/service/shareser/share.go b/app/service/shareser/share.go new file mode 100644 index 0000000..d80da8f --- /dev/null +++ b/app/service/shareser/share.go @@ -0,0 +1,238 @@ +package shareser + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/versionser" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/sourcemod" + "context" + "fmt" + "math/rand" + "strconv" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/e/sharemod" + "91porn-server/models/v/vidmod" + + "github.com/skip2/go-qrcode" +) + +// GeneratorQrCode 生成二维码 +func GeneratorQrCode(uid uint64, content, videoID string) (code stderr.Code, data interface{}) { + return GeneratorQrCodeContext(context.Background(), uid, content, videoID, "") +} + +// GeneratorQrCodeContext 生成二维码,并按分享事件幂等累计短视频推荐分。 +func GeneratorQrCodeContext( + ctx context.Context, + uid uint64, + content, videoID, eventID string, +) (code stderr.Code, data interface{}) { + return generatorQrCodeContextWithScore( + ctx, + uid, + content, + videoID, + eventID, + func( + ctx context.Context, + uid uint64, + videoID primitive.ObjectID, + eventID string, + now time.Time, + ) error { + return incrementRecommendShareOnce( + ctx, appg.Redis, uid, videoID, eventID, now, + ) + }, + ) +} + +// GeneratorDramaQrCodeContext 生成短剧分享二维码并幂等累计分享互动分。 +func GeneratorDramaQrCodeContext( + ctx context.Context, + uid uint64, + content, mediaID, contentID, eventID string, +) (code stderr.Code, data interface{}) { + id, err := primitive.ObjectIDFromHex(mediaID) + if err != nil { + return stderr.ErrParamError, "" + } + media, err := mediamod.GetInfo(id) + if err != nil || media.ID.IsZero() || media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete { + return stderr.ErrParamError, "" + } + if contentID != "" { + cid, parseErr := primitive.ObjectIDFromHex(contentID) + if parseErr != nil { + return stderr.ErrParamError, "" + } + episode, episodeErr := mediacontentmod.GetInfo(cid, true) + if episodeErr != nil || episode.MediaID != id || episode.MediaType != mediamod.MediaTypeDrama || episode.IsDelete { + return stderr.ErrParamError, "" + } + } + return generatorQrCodeContextWithScore( + ctx, uid, content, mediaID, eventID, + func(ctx context.Context, uid uint64, mediaID primitive.ObjectID, eventID string, now time.Time) error { + return incrementRecommendShareOnceWith( + ctx, appg.Redis, uid, mediaID, eventID, now, + func(_ context.Context, id primitive.ObjectID) error { + return mediamod.IncreaseCountShareByID(id, 1) + }, + ) + }, + ) +} + +type recommendShareScoreFunc func( + context.Context, + uint64, + primitive.ObjectID, + string, + time.Time, +) error + +func generatorQrCodeContextWithScore( + ctx context.Context, + uid uint64, + content, videoID, eventID string, + score recommendShareScoreFunc, +) (code stderr.Code, data interface{}) { + tmp := strconv.FormatUint(uid, 10) + url := content + "&share=true&shareuid=" + tmp + png, err := qrcode.Encode(url, qrcode.Medium, 256) + if err != nil { + e := fmt.Sprintf("GeneratorQrCode error: uid(%d), content(%s), err(%+v)\n", uid, content, err) + log.Error(e) + return stderr.ErrNetWorkBusy, "" + } + if id, parseErr := primitive.ObjectIDFromHex(videoID); parseErr == nil && score != nil { + if incErr := score(ctx, uid, id, eventID, time.Now()); incErr != nil { + // 二维码已经生成,推荐统计是旁路能力;瞬时 Redis/Mongo + // 故障不能破坏旧分享主流程,失败通过日志和监控告警。 + log.Error("increment recommend share failed", log.E(incErr), log.Any("videoID", videoID)) + } + } + return stderr.Success, sharemod.VShareResp{QrCode: png} +} + +// GetShareCnt 获取视频分享次数 +func GetShareCnt(videoID string) (code stderr.Code, data interface{}) { + cnt, err := vidmod.GetShareCount(videoID) + if err != nil { + return stderr.ErrNetWorkBusy, "" + } + data = sharemod.VShareCntResp{VidelID: videoID, Cnt: cnt} + return stderr.Success, data +} + +// Info 获取分享的视频信息以及推荐列表 +func Info(id primitive.ObjectID, sysType string) (*sharemod.List, *stderr.CustomErr) { + video, err := vidmod.GetByIDFromRedis(id.Hex()) + if err != nil { + return nil, stderr.NewCustomErr(stderr.ErrDbQueryError, err.Error(), "查找视频失败") + } + if video.ID.IsZero() { + return nil, stderr.NewCustomErr(stderr.Failure, "视频未找到", "视频未找到") + } + blockedModules, err := moduleconfmod.BlockedOutsideSearchModuleIDs([]string{video.MID}, time.Now()) + if err != nil { + return nil, stderr.NewCustomErr(stderr.ErrDbQueryError, err.Error(), "查找视频失败") + } + if _, blocked := blockedModules[video.MID]; blocked { + return nil, stderr.NewCustomErr(stderr.CodeEmptyData, "视频未找到", "视频未找到") + } + var picUrl, landUrl string + _, pl := sourcemod.PingList() + if len(pl) > 0 { + for _, v := range pl { + if v == nil { + continue + } + if landUrl != "" && picUrl != "" { + break + } + if v.Type == sourcemod.Image && len(v.Domain) > 0 { + picUrl = v.Domain[rand.Intn(len(v.Domain))].Url + continue + } + if v.Type == sourcemod.ShareURL && len(v.Domain) > 0 { + landUrl = v.Domain[rand.Intn(len(v.Domain))].Url + } + } + } + vsis, err := vidmod.GetShareListFromRedis() + if err != nil { + return nil, stderr.NewCustomErr(stderr.ErrDbQueryError, err.Error(), "查询分享视频推荐列表失败") + } + vsis, err = filterShareRecommendations(vsis) + if err != nil { + return nil, stderr.NewCustomErr(stderr.ErrDbQueryError, err.Error(), "查询分享视频推荐列表失败") + } + ver, _, _, _, _, _, err := versionser.AdvVersionAnnounThreeServer("1.0.1", sysType) + if err != nil { + log.Error("查询版本信息失败", log.E(err)) + return nil, stderr.NewCustomErr(stderr.Failure, err.Error(), "查询下载链接失败") + } + if len(ver.DownloadLink) == 0 { + return nil, stderr.NewCustomErr(stderr.Failure, "", "无有效的app下载地址") + } + l := sharemod.List{ + Title: video.Title, + Cover: video.Cover, + SourceUrl: video.SourceURL, + LandUrl: landUrl, + PicUrl: picUrl, + Tags: []string{"暗网幼女", "人兽重口", "校园霸凌", "灌醉迷奸", "处女破处", "捉奸现场", "疯狂孕妇", "黑鬼草人妻", + "缅北禁区", "全家乱操", "黑料吃瓜", "摄像破解", "群P激战", "AI换脸", "SM调教", "反差母狗"}, + DownloadUrl: ver.DownloadLink[0], + List: vsis, + } + return &l, nil +} + +func filterShareRecommendations(items []vidmod.ShareInfo) ([]vidmod.ShareInfo, error) { + if len(items) == 0 { + return items, nil + } + ids := make([]primitive.ObjectID, 0, len(items)) + for _, item := range items { + ids = append(ids, item.ID) + } + videos, err := vidmod.GetVideosByIDs(ids) + if err != nil { + return nil, err + } + excludedModules, err := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), true) + if err != nil { + return nil, err + } + excludedSet := make(map[string]struct{}, len(excludedModules)) + for _, moduleID := range excludedModules { + excludedSet[moduleID] = struct{}{} + } + excludedVideos := make(map[primitive.ObjectID]struct{}) + for _, video := range videos { + if video == nil { + continue + } + if _, excluded := excludedSet[video.MID]; excluded { + excludedVideos[video.ID] = struct{}{} + } + } + filtered := make([]vidmod.ShareInfo, 0, len(items)) + for _, item := range items { + if _, excluded := excludedVideos[item.ID]; excluded { + continue + } + filtered = append(filtered, item) + } + return filtered, nil +} diff --git a/app/service/shareser/share_test.go b/app/service/shareser/share_test.go new file mode 100644 index 0000000..151620e --- /dev/null +++ b/app/service/shareser/share_test.go @@ -0,0 +1,45 @@ +package shareser + +import ( + "context" + "errors" + "testing" + "time" + + "91porn-server/common/stderr" + "91porn-server/models/e/sharemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestGeneratorQrCodeKeepsShareSuccessWhenRecommendScoreFails(t *testing.T) { + scoreCalls := 0 + code, data := generatorQrCodeContextWithScore( + context.Background(), + 123, + "https://example.test/video?id=1", + primitive.NewObjectID().Hex(), + "event-1", + func( + context.Context, + uint64, + primitive.ObjectID, + string, + time.Time, + ) error { + scoreCalls++ + return errors.New("recommend score unavailable") + }, + ) + + if code != stderr.Success { + t.Fatalf("code=%v, want success", code) + } + response, ok := data.(sharemod.VShareResp) + if !ok || len(response.QrCode) == 0 { + t.Fatalf("data=%T %#v, want non-empty QR code", data, data) + } + if scoreCalls != 1 { + t.Fatalf("score calls=%d, want 1", scoreCalls) + } +} diff --git a/app/service/shortrecommendser/fetch.go b/app/service/shortrecommendser/fetch.go new file mode 100644 index 0000000..445b352 --- /dev/null +++ b/app/service/shortrecommendser/fetch.go @@ -0,0 +1,373 @@ +package shortrecommendser + +import ( + "context" + "crypto/sha256" + "errors" + "fmt" + "strings" + "time" + + "91porn-server/app/appg" + recommendqueue "91porn-server/common/shortrecommend" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + defaultMaxBatches = 5 + defaultScanMultiplier = 5 + maxAllowedBatches = 20 + maxAllowedMultiplier = 20 + maxRequestIDLength = 128 + reservationAbortTTL = 2 * time.Second + reservationBusyRetries = 3 + reservationBusyBackoff = 10 * time.Millisecond +) + +// FetchResult 是一次环形队列拉取的结果和可观测指标。 +type FetchResult struct { + Videos []*vidmod.VideoModel + QueueVersion string + QueueLength int + Scanned int + Filtered int + Batches int + BudgetExceeded bool +} + +type fetchOptions struct { + maxBatches int + scanMultiplier int +} + +type reserveFunc func( + context.Context, + uint64, + int, + string, +) (recommendqueue.Reservation, error) + +type fetchDependencies struct { + reserve reserveFunc + commit func( + context.Context, + uint64, + recommendqueue.Reservation, + int, + ) error + abort func( + context.Context, + uint64, + recommendqueue.Reservation, + ) error + excludedModuleIDs func(time.Time, bool) ([]string, error) + findVideos func(context.Context, []primitive.ObjectID) ([]*vidmod.VideoModel, error) + now func() time.Time +} + +// Fetch 从用户当前偏移预留短视频,按当前Mongo状态过滤,并限制补位扫描成本。 +// 只有全部Mongo批次成功后才一次性推进实际扫描前缀;失败会释放预留且不跳内容。 +func Fetch( + ctx context.Context, + uid uint64, + size int, + requestID string, +) (FetchResult, error) { + return FetchScoped(ctx, uid, size, requestID, "short-recommend") +} + +// FetchScoped 将入口和请求大小纳入幂等作用域,防止同一客户端requestID +// 被不同短视频接口或不同请求参数误复用。 +func FetchScoped( + ctx context.Context, + uid uint64, + size int, + requestID, scope string, +) (FetchResult, error) { + if appg.Redis == nil { + return FetchResult{}, fmt.Errorf("short recommend Redis is nil") + } + var err error + requestID, err = scopedRequestID(scope, size, requestID) + if err != nil { + return FetchResult{}, err + } + opts := configuredFetchOptions() + deps := fetchDependencies{ + reserve: func( + ctx context.Context, + uid uint64, + size int, + requestID string, + ) (recommendqueue.Reservation, error) { + return recommendqueue.ReserveCurrentContext( + ctx, appg.Redis, uid, size, requestID, + ) + }, + commit: func( + ctx context.Context, + uid uint64, + reservation recommendqueue.Reservation, + consumed int, + ) error { + return recommendqueue.CommitReservationContext( + ctx, appg.Redis, uid, reservation, consumed, + ) + }, + abort: func( + ctx context.Context, + uid uint64, + reservation recommendqueue.Reservation, + ) error { + return recommendqueue.AbortReservationContext( + ctx, appg.Redis, uid, reservation, + ) + }, + excludedModuleIDs: moduleconfmod.ExcludedVideoModuleIDs, + findVideos: vidmod.GetRecommendVideosByIDsContext, + now: time.Now, + } + return fetch(ctx, uid, size, requestID, opts, deps) +} + +func scopedRequestID(scope string, size int, requestID string) (string, error) { + requestID = strings.TrimSpace(requestID) + if requestID == "" { + return "", nil + } + if len(requestID) > maxRequestIDLength { + return "", fmt.Errorf("short recommend request ID exceeds %d bytes", maxRequestIDLength) + } + sum := sha256.Sum256([]byte(fmt.Sprintf("%s:%d:%s", scope, size, requestID))) + return fmt.Sprintf("%x", sum[:16]), nil +} + +func configuredFetchOptions() fetchOptions { + opts := fetchOptions{ + maxBatches: defaultMaxBatches, + scanMultiplier: defaultScanMultiplier, + } + if appg.Conf == nil { + return opts + } + if configured := appg.Conf.ShortRecommend.MaxBatches; configured > 0 { + opts.maxBatches = clamp(configured, 1, maxAllowedBatches) + } + if configured := appg.Conf.ShortRecommend.ScanMultiplier; configured > 0 { + opts.scanMultiplier = clamp(configured, 1, maxAllowedMultiplier) + } + return opts +} + +func fetch( + ctx context.Context, + uid uint64, + size int, + requestID string, + opts fetchOptions, + deps fetchDependencies, +) (result FetchResult, err error) { + if ctx == nil { + return result, fmt.Errorf("short recommend context must not be nil") + } + if err = ctx.Err(); err != nil { + return result, err + } + if uid == 0 { + return result, fmt.Errorf("anonymous user has no independent queue offset") + } + if size <= 0 { + return result, nil + } + requestID = strings.TrimSpace(requestID) + if len(requestID) > maxRequestIDLength { + return result, fmt.Errorf("short recommend request ID exceeds %d bytes", maxRequestIDLength) + } + opts.maxBatches = clamp(opts.maxBatches, 1, maxAllowedBatches) + opts.scanMultiplier = clamp(opts.scanMultiplier, 1, maxAllowedMultiplier) + scanBudget := size * opts.scanMultiplier + if scanBudget < size { + scanBudget = size + } + if batchBudget := size * opts.maxBatches; scanBudget > batchBudget { + // 预留不会超过本次最多能查询的范围,避免把永远不会扫描的ID + // 从Redis传到App进程。 + scanBudget = batchBudget + } + + excluded, err := deps.excludedModuleIDs(deps.now(), true) + if err != nil { + return result, err + } + excludedSet := make(map[string]struct{}, len(excluded)) + for _, moduleID := range excluded { + excludedSet[moduleID] = struct{}{} + } + + reservation, reserveErr := reserveWithBusyRetry( + ctx, deps.reserve, uid, scanBudget, requestID, + ) + result.QueueVersion = reservation.Version + result.QueueLength = reservation.Length + if reserveErr != nil { + return result, reserveErr + } + if reservation.Version == "" || reservation.Length <= 0 || + reservation.Reserved <= 0 || len(reservation.IDs) == 0 { + return result, nil + } + + committed := false + defer func() { + if committed { + return + } + abortCtx, cancel := context.WithTimeout(context.Background(), reservationAbortTTL) + defer cancel() + _ = deps.abort(abortCtx, uid, reservation) + }() + + result.Videos = make([]*vidmod.VideoModel, 0, size) + seen := make(map[primitive.ObjectID]struct{}, size) + cursor := 0 + for cursor < len(reservation.IDs) && + (reservation.AlreadyCommitted || + (result.Batches < opts.maxBatches && len(result.Videos) < size)) { + batchSize := size + if !reservation.AlreadyCommitted { + batchSize = size - len(result.Videos) + } else if batchSize > 0 { + // 已提交请求的receipt记录的是原实际消费前缀。重试必须扫描并 + // 确认完整前缀,再以原consumed幂等提交,不能因当前状态变化 + // 缩短已确认的消费范围。固定按请求大小分批,避免当前只差一条 + // 有效视频时退化为大量单ID Mongo查询。 + } + if remaining := len(reservation.IDs) - cursor; batchSize > remaining { + batchSize = remaining + } + if batchSize <= 0 { + break + } + end := cursor + batchSize + batchIDs := reservation.IDs[cursor:end] + objectIDs := parseUniqueObjectIDs(batchIDs) + videos, findErr := deps.findVideos(ctx, objectIDs) + if findErr != nil { + result.Videos = nil + return result, findErr + } + result.Batches++ + result.Scanned += len(batchIDs) + cursor = end + byID := make(map[primitive.ObjectID]*vidmod.VideoModel, len(videos)) + for _, video := range videos { + if video == nil { + continue + } + if _, blocked := excludedSet[video.MID]; blocked { + continue + } + byID[video.ID] = video + } + before := len(result.Videos) + for _, rawID := range batchIDs { + if len(result.Videos) >= size { + break + } + id, parseErr := primitive.ObjectIDFromHex(rawID) + if parseErr != nil { + continue + } + if _, exists := seen[id]; exists { + continue + } + video := byID[id] + if video == nil { + continue + } + seen[id] = struct{}{} + result.Videos = append(result.Videos, video) + if len(result.Videos) == size { + break + } + } + result.Filtered += len(batchIDs) - (len(result.Videos) - before) + } + if result.Scanned <= 0 { + result.Videos = nil + return result, fmt.Errorf("short recommend reservation was not scanned") + } + commitErr := deps.commit(ctx, uid, reservation, result.Scanned) + if commitErr != nil && ctx.Err() == nil { + // 同一receipt重试可确认“脚本已执行但响应丢失”的不确定提交, + // 不会再次推进offset。 + commitErr = deps.commit(ctx, uid, reservation, result.Scanned) + } + if commitErr != nil { + result.Videos = nil + return result, commitErr + } + committed = true + result.BudgetExceeded = len(result.Videos) < size && + result.QueueLength > 0 && + result.Scanned < result.QueueLength && + (result.Scanned >= scanBudget || result.Batches >= opts.maxBatches) + return result, nil +} + +func reserveWithBusyRetry( + ctx context.Context, + reserve reserveFunc, + uid uint64, + size int, + requestID string, +) (recommendqueue.Reservation, error) { + var reservation recommendqueue.Reservation + var err error + for attempt := 0; attempt <= reservationBusyRetries; attempt++ { + reservation, err = reserve(ctx, uid, size, requestID) + if !errors.Is(err, recommendqueue.ErrReservationBusy) || + attempt == reservationBusyRetries { + return reservation, err + } + delay := reservationBusyBackoff << attempt + timer := time.NewTimer(delay) + select { + case <-ctx.Done(): + timer.Stop() + return reservation, ctx.Err() + case <-timer.C: + } + } + return reservation, err +} + +func parseUniqueObjectIDs(ids []string) []primitive.ObjectID { + out := make([]primitive.ObjectID, 0, len(ids)) + seen := make(map[primitive.ObjectID]struct{}, len(ids)) + for _, rawID := range ids { + id, err := primitive.ObjectIDFromHex(rawID) + if err != nil { + continue + } + if _, exists := seen[id]; exists { + continue + } + seen[id] = struct{}{} + out = append(out, id) + } + return out +} + +func clamp(value, low, high int) int { + if value < low { + return low + } + if value > high { + return high + } + return value +} diff --git a/app/service/shortrecommendser/fetch_test.go b/app/service/shortrecommendser/fetch_test.go new file mode 100644 index 0000000..1cb1f05 --- /dev/null +++ b/app/service/shortrecommendser/fetch_test.go @@ -0,0 +1,523 @@ +package shortrecommendser + +import ( + "context" + "errors" + "testing" + "time" + + recommendqueue "91porn-server/common/shortrecommend" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestFetchReservesOncePreservesOrderAndCommitsConsumedPrefix(t *testing.T) { + validA := primitive.NewObjectID() + downShelf := primitive.NewObjectID() + excluded := primitive.NewObjectID() + validB := primitive.NewObjectID() + ids := []string{validA.Hex(), downShelf.Hex(), excluded.Hex(), validB.Hex()} + deps := testFetchDependencies( + testReservation(ids, len(ids)), + func(_ context.Context, batch []primitive.ObjectID) ([]*vidmod.VideoModel, error) { + out := make([]*vidmod.VideoModel, 0, len(batch)) + for _, id := range batch { + switch id { + case validA, validB: + out = append(out, &vidmod.VideoModel{ID: id}) + case excluded: + out = append(out, &vidmod.VideoModel{ID: id, MID: "blocked"}) + } + } + return out, nil + }, + ) + deps.excludedModuleIDs = func(time.Time, bool) ([]string, error) { + return []string{"blocked"}, nil + } + reserveCalls, committed, aborted := 0, 0, 0 + originalReserve := deps.reserve + deps.reserve = func( + ctx context.Context, uid uint64, size int, requestID string, + ) (recommendqueue.Reservation, error) { + reserveCalls++ + if size != 10 { + t.Fatalf("reserve size = %d, want scan budget 10", size) + } + return originalReserve(ctx, uid, size, requestID) + } + deps.commit = func( + _ context.Context, + _ uint64, + _ recommendqueue.Reservation, + consumed int, + ) error { + committed = consumed + return nil + } + deps.abort = func( + context.Context, uint64, recommendqueue.Reservation, + ) error { + aborted++ + return nil + } + + result, err := fetch(context.Background(), 7, 2, "request-1", fetchOptions{ + maxBatches: 5, scanMultiplier: 5, + }, deps) + if err != nil { + t.Fatalf("fetch() error = %v", err) + } + if len(result.Videos) != 2 || + result.Videos[0].ID != validA || + result.Videos[1].ID != validB { + t.Fatalf("videos = %#v, want queue ordered valid videos", result.Videos) + } + if reserveCalls != 1 || committed != 4 || aborted != 0 { + t.Fatalf("reserve=%d committed=%d aborted=%d", reserveCalls, committed, aborted) + } + if result.Scanned != 4 || result.Filtered != 2 || result.Batches != 3 { + t.Fatalf("metrics = %+v", result) + } + if result.QueueVersion != "20260731-r1" || result.QueueLength != len(ids) { + t.Fatalf("queue metadata = %+v", result) + } + if result.BudgetExceeded { + t.Fatal("BudgetExceeded = true, want false after a full queue scan") + } +} + +func TestFetchCommitsOnlyBoundedScannedPrefix(t *testing.T) { + ids := make([]string, 30) + for i := range ids { + ids[i] = primitive.NewObjectID().Hex() + } + reservation := testReservation(ids, 100) + deps := testFetchDependencies( + reservation, + func(context.Context, []primitive.ObjectID) ([]*vidmod.VideoModel, error) { + return nil, nil + }, + ) + committed := 0 + deps.commit = func( + _ context.Context, + _ uint64, + _ recommendqueue.Reservation, + consumed int, + ) error { + committed = consumed + return nil + } + + result, err := fetch(context.Background(), 8, 3, "request-2", fetchOptions{ + maxBatches: 2, scanMultiplier: 10, + }, deps) + if err != nil { + t.Fatalf("fetch() error = %v", err) + } + if result.Batches != 2 || result.Scanned != 6 || committed != 6 { + t.Fatalf("committed=%d result=%+v", committed, result) + } + if !result.BudgetExceeded { + t.Fatal("BudgetExceeded = false, want true") + } +} + +func TestFetchMongoFailureAbortsWithoutCommitOrPartialResponse(t *testing.T) { + valid := primitive.NewObjectID() + filtered := primitive.NewObjectID() + failing := primitive.NewObjectID() + wantErr := errors.New("mongo unavailable") + findCalls := 0 + deps := testFetchDependencies( + testReservation( + []string{valid.Hex(), filtered.Hex(), failing.Hex()}, + 10, + ), + func(_ context.Context, ids []primitive.ObjectID) ([]*vidmod.VideoModel, error) { + findCalls++ + if findCalls == 1 { + return []*vidmod.VideoModel{{ID: valid}}, nil + } + return nil, wantErr + }, + ) + commitCalls, abortCalls := 0, 0 + deps.commit = func( + context.Context, uint64, recommendqueue.Reservation, int, + ) error { + commitCalls++ + return nil + } + deps.abort = func( + context.Context, uint64, recommendqueue.Reservation, + ) error { + abortCalls++ + return nil + } + + result, err := fetch(context.Background(), 9, 2, "request-3", fetchOptions{ + maxBatches: 5, scanMultiplier: 5, + }, deps) + if !errors.Is(err, wantErr) { + t.Fatalf("error = %v, want %v", err, wantErr) + } + if result.Videos != nil || commitCalls != 0 || abortCalls != 1 { + t.Fatalf("result=%+v commitCalls=%d abortCalls=%d", result, commitCalls, abortCalls) + } + if result.Scanned != 2 || result.Batches != 1 { + t.Fatalf("successful scan metrics = %+v", result) + } +} + +func TestFetchRetriesUncertainCommitWithSameReservation(t *testing.T) { + id := primitive.NewObjectID() + wantErr := errors.New("connection reset after write") + deps := testFetchDependencies( + testReservation([]string{id.Hex()}, 10), + func(context.Context, []primitive.ObjectID) ([]*vidmod.VideoModel, error) { + return []*vidmod.VideoModel{{ID: id}}, nil + }, + ) + commitCalls := 0 + deps.commit = func( + context.Context, uint64, recommendqueue.Reservation, int, + ) error { + commitCalls++ + if commitCalls == 1 { + return wantErr + } + return nil + } + + result, err := fetch(context.Background(), 10, 1, "request-4", fetchOptions{ + maxBatches: 5, scanMultiplier: 5, + }, deps) + if err != nil { + t.Fatalf("fetch() error = %v", err) + } + if commitCalls != 2 || len(result.Videos) != 1 { + t.Fatalf("commitCalls=%d result=%+v", commitCalls, result) + } +} + +func TestFetchCommitFailureAbortsAndSuppressesVideos(t *testing.T) { + id := primitive.NewObjectID() + wantErr := errors.New("redis unavailable") + deps := testFetchDependencies( + testReservation([]string{id.Hex()}, 10), + func(context.Context, []primitive.ObjectID) ([]*vidmod.VideoModel, error) { + return []*vidmod.VideoModel{{ID: id}}, nil + }, + ) + commitCalls, abortCalls := 0, 0 + deps.commit = func( + context.Context, uint64, recommendqueue.Reservation, int, + ) error { + commitCalls++ + return wantErr + } + deps.abort = func( + context.Context, uint64, recommendqueue.Reservation, + ) error { + abortCalls++ + return nil + } + + result, err := fetch(context.Background(), 11, 1, "request-5", fetchOptions{ + maxBatches: 5, scanMultiplier: 5, + }, deps) + if !errors.Is(err, wantErr) { + t.Fatalf("error = %v, want %v", err, wantErr) + } + if commitCalls != 2 || abortCalls != 1 || result.Videos != nil { + t.Fatalf("commit=%d abort=%d result=%+v", commitCalls, abortCalls, result) + } +} + +func TestFetchReservationErrorCarriesQueueMetadata(t *testing.T) { + deps := testFetchDependencies(recommendqueue.Reservation{}, nil) + deps.reserve = func( + context.Context, uint64, int, string, + ) (recommendqueue.Reservation, error) { + return recommendqueue.Reservation{ + Version: "20260731-r2", + Length: 100, + }, recommendqueue.ErrReservationBusy + } + + result, err := fetch(context.Background(), 12, 1, "", fetchOptions{}, deps) + if !errors.Is(err, recommendqueue.ErrReservationBusy) { + t.Fatalf("error = %v, want ErrReservationBusy", err) + } + if result.QueueVersion != "20260731-r2" || result.QueueLength != 100 { + t.Fatalf("result = %+v", result) + } +} + +func TestFetchRetriesBusyReservationWithinRequest(t *testing.T) { + id := primitive.NewObjectID() + reservation := testReservation([]string{id.Hex()}, 10) + deps := testFetchDependencies( + reservation, + func(context.Context, []primitive.ObjectID) ([]*vidmod.VideoModel, error) { + return []*vidmod.VideoModel{{ID: id}}, nil + }, + ) + reserveCalls := 0 + deps.reserve = func( + context.Context, uint64, int, string, + ) (recommendqueue.Reservation, error) { + reserveCalls++ + if reserveCalls < 3 { + return recommendqueue.Reservation{ + Version: reservation.Version, + Length: reservation.Length, + }, recommendqueue.ErrReservationBusy + } + return reservation, nil + } + + result, err := fetch(context.Background(), 12, 1, "", fetchOptions{}, deps) + if err != nil { + t.Fatalf("fetch() error = %v", err) + } + if reserveCalls != 3 || len(result.Videos) != 1 { + t.Fatalf("reserveCalls=%d result=%+v", reserveCalls, result) + } +} + +func TestReserveBusyRetryHonorsCancellation(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + calls := 0 + _, err := reserveWithBusyRetry( + ctx, + func( + context.Context, uint64, int, string, + ) (recommendqueue.Reservation, error) { + calls++ + cancel() + return recommendqueue.Reservation{}, recommendqueue.ErrReservationBusy + }, + 1, + 20, + "", + ) + if !errors.Is(err, context.Canceled) || calls != 1 { + t.Fatalf("error=%v calls=%d", err, calls) + } +} + +func TestFetchRejectsCanceledContextBeforeDependencies(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + called := false + deps := testFetchDependencies(recommendqueue.Reservation{}, nil) + deps.reserve = func( + context.Context, uint64, int, string, + ) (recommendqueue.Reservation, error) { + called = true + return recommendqueue.Reservation{}, nil + } + + _, err := fetch(ctx, 13, 1, "request-6", fetchOptions{}, deps) + if !errors.Is(err, context.Canceled) { + t.Fatalf("error = %v, want context.Canceled", err) + } + if called { + t.Fatal("dependency called after context cancellation") + } +} + +func TestFetchWithoutClientRequestIDUsesReservationWithoutBusinessReceiptScope(t *testing.T) { + id := primitive.NewObjectID() + deps := testFetchDependencies( + testReservation([]string{id.Hex()}, 1), + func(context.Context, []primitive.ObjectID) ([]*vidmod.VideoModel, error) { + return []*vidmod.VideoModel{{ID: id}}, nil + }, + ) + gotRequestID := "not-called" + originalReserve := deps.reserve + deps.reserve = func( + ctx context.Context, uid uint64, size int, requestID string, + ) (recommendqueue.Reservation, error) { + gotRequestID = requestID + return originalReserve(ctx, uid, size, requestID) + } + + result, err := fetch(context.Background(), 14, 1, "", fetchOptions{ + maxBatches: 5, scanMultiplier: 5, + }, deps) + if err != nil { + t.Fatalf("fetch() error = %v", err) + } + if gotRequestID != "" || len(result.Videos) != 1 { + t.Fatalf("requestID=%q result=%+v", gotRequestID, result) + } +} + +func TestFetchCommittedReceiptReplaysEntireConsumedPrefix(t *testing.T) { + ids := []string{ + primitive.NewObjectID().Hex(), + primitive.NewObjectID().Hex(), + primitive.NewObjectID().Hex(), + } + reservation := testReservation(ids, 10) + reservation.AlreadyCommitted = true + deps := testFetchDependencies( + reservation, + func(_ context.Context, batch []primitive.ObjectID) ([]*vidmod.VideoModel, error) { + out := make([]*vidmod.VideoModel, 0, len(batch)) + for _, id := range batch { + out = append(out, &vidmod.VideoModel{ID: id}) + } + return out, nil + }, + ) + committed := 0 + deps.commit = func( + _ context.Context, + _ uint64, + _ recommendqueue.Reservation, + consumed int, + ) error { + committed = consumed + return nil + } + + result, err := fetch(context.Background(), 15, 1, "request-retry", fetchOptions{ + maxBatches: 1, scanMultiplier: 1, + }, deps) + if err != nil { + t.Fatal(err) + } + if committed != len(ids) || result.Scanned != len(ids) || + len(result.Videos) != 1 { + t.Fatalf("committed=%d result=%+v", committed, result) + } +} + +func TestFetchCommittedReceiptUsesBoundedMongoBatches(t *testing.T) { + const ( + requestSize = 20 + consumed = 100 + ) + ids := make([]string, consumed) + for i := range ids { + ids[i] = primitive.NewObjectID().Hex() + } + reservation := testReservation(ids, consumed) + reservation.AlreadyCommitted = true + batchSizes := make([]int, 0, consumed/requestSize) + deps := testFetchDependencies( + reservation, + func(_ context.Context, batch []primitive.ObjectID) ([]*vidmod.VideoModel, error) { + batchSizes = append(batchSizes, len(batch)) + if len(batchSizes) > 1 { + return nil, nil + } + videos := make([]*vidmod.VideoModel, 0, requestSize-1) + for _, id := range batch[:requestSize-1] { + videos = append(videos, &vidmod.VideoModel{ID: id}) + } + return videos, nil + }, + ) + committed := 0 + deps.commit = func( + _ context.Context, + _ uint64, + _ recommendqueue.Reservation, + value int, + ) error { + committed = value + return nil + } + + result, err := fetch(context.Background(), 16, requestSize, "request-retry", fetchOptions{ + maxBatches: 1, scanMultiplier: 1, + }, deps) + if err != nil { + t.Fatal(err) + } + if len(batchSizes) != consumed/requestSize { + t.Fatalf("Mongo calls=%d batchSizes=%v", len(batchSizes), batchSizes) + } + for _, batchSize := range batchSizes { + if batchSize != requestSize { + t.Fatalf("batchSizes=%v", batchSizes) + } + } + if committed != consumed || result.Scanned != consumed || + len(result.Videos) != requestSize-1 { + t.Fatalf("committed=%d result=%+v", committed, result) + } +} + +func TestScopedRequestIDSeparatesEntryAndRequestSize(t *testing.T) { + first, err := scopedRequestID("recommend-list", 20, "request") + if err != nil { + t.Fatal(err) + } + same, _ := scopedRequestID("recommend-list", 20, " request ") + otherEntry, _ := scopedRequestID("module-short-all", 20, "request") + otherSize, _ := scopedRequestID("recommend-list", 10, "request") + if first == "" || first != same { + t.Fatalf("scoped IDs first=%q same=%q", first, same) + } + if first == otherEntry || first == otherSize || otherEntry == otherSize { + t.Fatalf("scope collision: %q %q %q", first, otherEntry, otherSize) + } + blank, err := scopedRequestID("recommend-list", 20, " ") + if err != nil || blank != "" { + t.Fatalf("blank request ID = %q, %v", blank, err) + } +} + +func testReservation(ids []string, length int) recommendqueue.Reservation { + return recommendqueue.Reservation{ + Version: "20260731-r1", + Length: length, + Offset: 0, + Reserved: len(ids), + IDs: append([]string(nil), ids...), + LeaseToken: "lease", + ReceiptID: "receipt", + } +} + +func testFetchDependencies( + reservation recommendqueue.Reservation, + find func(context.Context, []primitive.ObjectID) ([]*vidmod.VideoModel, error), +) fetchDependencies { + if find == nil { + find = func(context.Context, []primitive.ObjectID) ([]*vidmod.VideoModel, error) { + return nil, nil + } + } + return fetchDependencies{ + reserve: func( + context.Context, uint64, int, string, + ) (recommendqueue.Reservation, error) { + return reservation, nil + }, + commit: func( + context.Context, uint64, recommendqueue.Reservation, int, + ) error { + return nil + }, + abort: func( + context.Context, uint64, recommendqueue.Reservation, + ) error { + return nil + }, + excludedModuleIDs: func(time.Time, bool) ([]string, error) { + return nil, nil + }, + findVideos: find, + now: time.Now, + } +} diff --git a/app/service/signrecordser/signrecord.go b/app/service/signrecordser/signrecord.go new file mode 100755 index 0000000..a66cc64 --- /dev/null +++ b/app/service/signrecordser/signrecord.go @@ -0,0 +1,196 @@ +package signrecordser + +import ( + "91porn-server/app/appg" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/cache/signrecorddata" + "91porn-server/models/commod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/signrecordmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/walletmod" + "errors" + "fmt" + "math" + "time" + + "go.mongodb.org/mongo-driver/mongo/options" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AppQueryListReq struct { + commod.Page +} + +type AppListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []signrecorddata.AppSimpleData `json:"list"` +} + +// GetList 获取列表 +func (p *AppQueryListReq) GetList() AppListRes { + var res AppListRes + var err error + filter := bson.M{} + + sort := bson.D{{"_id", -1}} + // 获取列表 + var data []signrecordmod.SignRecord + data, res.Total, res.HasNext, err = signrecorddata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + log.Error("获取打卡记录表列表数据错误", log.Any("Params", *p), log.E(err)) + return res + } + res.List = signrecorddata.FormatAppSimpleDataList(data) + + return res +} + +type AppQueryInfoReq struct { + ID string `json:"id" form:"id"` // id +} +type AppQueryInfoRes = signrecorddata.AppSimpleData + +// GetInfo 获取详情 +func (p *AppQueryInfoReq) GetInfo() (res AppQueryInfoRes, err error) { + oid, err := primitive.ObjectIDFromHex(p.ID) + if err != nil { + return res, err + } + + var item signrecordmod.SignRecord + item, err = signrecorddata.GetInfoFromCache(oid) + if err != nil { + log.Error("获取打卡记录表详情数据错误", log.Any("ID", p.ID), log.E(err)) + return + } + + res = signrecorddata.FormatAppSimpleData(item) + return +} + +type AppReSignReq struct { + PID string `json:"pid" form:"pid"` // ID +} + +// ReSign 补签 +func (param *AppReSignReq) ReSign(uid uint64) (code stderr.Code, err error) { + var isReturn bool + pid, _ := primitive.ObjectIDFromHex(param.PID) + + filter := bson.M{"_id": pid} + signRecord, err := signrecordmod.GetInfoByCond(filter) + if err != nil { + return stderr.Failure, err + } + + if signRecord == nil || signRecord.ID.IsZero() { + return stderr.Failure, errors.New("signRecord is null") + } + + if signRecord.CurrentSignDays+signRecord.ForgetSignDays > signRecord.TotalDays { + return stderr.Failure, errors.New("reSign days is excess days") + } + + if signRecord.ForgetSignDays <= 0 { + return stderr.Failure, errors.New("reSign no forgetSignDays days") + } + + product, err := productmod.FindOne(nil, bson.M{"_id": signRecord.PID}, options.FindOne()) + if err != nil { + return stderr.ErrDbQueryError, err + } + if product == nil || product.ID.IsZero() { + return stderr.Failure, errors.New("product is null") + } + + // 修改补签次数 + wallet, err := walletmod.GetWallet(uid) + if err != nil { + return stderr.Failure, err + } + + if wallet == nil || wallet.ID.IsZero() { + return stderr.InsufficientBalance, errors.New("account balance is not enough") + } + + // 10/20/40/80/160 + var price uint64 + for p := 0; p < int(signRecord.ForgetSignDays+signRecord.RenewalSignDays); p++ { + if p < int(signRecord.RenewalSignDays) { + continue + } + price += uint64(math.Pow(2, float64(p)) * 10) + } + if int64(price) > wallet.Amount { + return stderr.InsufficientBalance, errors.New("account balance is not enough") + } + + if signRecord.CurrentSignDays+signRecord.ForgetSignDays == signRecord.TotalDays { + isReturn = true + } + newRecord := []signrecordmod.Record{} + cond := bson.M{"_id": signRecord.ID} + update := bson.M{ + "$inc": bson.M{"renewalSignDays": signRecord.ForgetSignDays, "currentSignDays": signRecord.ForgetSignDays, "forgetSignDays": -signRecord.ForgetSignDays}, + "$set": bson.M{"updateTime": time.Now(), "recordTime": newRecord}, + } + if isReturn { + update["$set"] = bson.M{"updateTime": time.Now(), "recordTime": newRecord, "hasReturn": true} + } + if err = appg.VideoDB.Trans(func(tool *db.MongoTool) error { + _, err := signrecordmod.UpdateDataByCond(tool, cond, update) + if err != nil { + return err + } + // 扣除钱包余额 + wall, err := walletmod.DebitAmount(tool, int64(price), uid) + if err != nil { + return err + } + + // 补签打卡扣除金币 + txnLogs := []txnmod.TransactionLog{ + { + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: int64(-price), + ActualAmount: -float64(price), + TranType: txnmod.ReSignDebitAmount.Key(), + TranTypeInt: int64(txnmod.ReSignDebitAmount), + Desc: fmt.Sprintf("%v:%v个", txnmod.ReSignDebitAmount.Key(), int64(price)), + RealAmount: wall.RealAmount(), + }, + } + if isReturn { + amount := *product.DiscountedPriceIos + wallAct, err := walletmod.DebitAmount(tool, -amount, uid) + if err != nil { + return err + } + + tl := txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: amount, + ActualAmount: float64(amount), + TranType: txnmod.SuccessSignReturnAmount.Key(), + TranTypeInt: int64(txnmod.SuccessSignReturnAmount), + Desc: fmt.Sprintf("%v:%v个", txnmod.SuccessSignReturnAmount.Key(), amount), + RealAmount: wallAct.RealAmount(), + } + txnLogs = append(txnLogs, tl) + } + return txnmod.InsertManyTransactionLog(tool, txnLogs) + }); err != nil { + log.Error(fmt.Sprintf("uid:%v, ReSign Trans err:%v", uid, err)) + return stderr.Failure, err + } + + return stderr.Success, nil +} diff --git a/app/service/smsser/smsser.go b/app/service/smsser/smsser.go new file mode 100644 index 0000000..a125ea4 --- /dev/null +++ b/app/service/smsser/smsser.go @@ -0,0 +1,168 @@ +package smsser + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/maths" + "91porn-server/common/redis" + "91porn-server/common/sms" + "91porn-server/common/stderr" + v10 "91porn-server/common/v10" + "91porn-server/common/ysphone" + "91porn-server/models/v/usermod" +) + +type smsCaptchaLimitRule struct { + window time.Duration + maxCount int64 +} + +var smsCaptchaLimitRules = []smsCaptchaLimitRule{ + {window: time.Minute, maxCount: redisconst.MobileCaptchaMaxCountInMinute}, + {window: time.Hour, maxCount: redisconst.MobileCaptchaMaxCountInHour}, + {window: 24 * time.Hour, maxCount: redisconst.MobileCaptchaMaxCountInDay}, +} + +func SendSmsCode(ctx context.Context, mobile string, typ int) stderr.Code { + mobile = strings.TrimSpace(mobile) + mobile = ysphone.FormatPhoneNumber(mobile) + if !v10.IsGlobalizationPhoneNumber(mobile) { + return stderr.ErrMobileInvalid + } + //如果发送的验证码 用户手机绑定 则先校验 当前手机号是否被绑定 + if typ == 1 { + //手机号是否绑定 + if user, _ := usermod.FindUserByMobile(mobile); user != nil { + return stderr.ErrMobileHasBindByOther + } + } + + // 限制同一手机号在 1 分钟/1 小时/24 小时内的发送次数,使用滑动窗口统计。 + rateCode := checkAndRecordSMSCaptchaRate(ctx, mobile) + if rateCode != stderr.Success { + return rateCode + } + + redisKey := redisconst.MobileCaptchaKey(mobile) + redisCode, err := appg.Redis.Get(redisKey) + if err != nil { + log.ErrorX(ctx, "SendCaptcha redis get err", log.Any("mobile", mobile), log.E(err)) + return stderr.ErrNetWorkBusy + } + var code string + if redisCode != nil { + code = *redisCode + } else { + code = maths.RandDigits(constant.CaptchaLen) + } + if err = appg.Redis.Set(redisKey, code, redisconst.MobileCaptchaExpire); err != nil { + log.ErrorX(ctx, "SendCaptcha Save Redis error", log.Any("mobile", mobile), log.E(err)) + return stderr.ErrNetWorkBusy + } + common.Go(func() { + err = sms.Send(sms.FengNiao, mobile, code) + if err != nil { + log.ErrorX(ctx, "SendCaptcha send err", log.Any("mobile", mobile), log.Any("Platform", sms.FengNiao), + log.E(err)) + yunErr := sms.Send(sms.YunPian, mobile, code) + if yunErr != nil { + log.ErrorX(ctx, "SendCaptcha send err", log.Any("mobile", mobile), log.Any("Platform", sms.YunPian), + log.E(err)) + } + } + }) + return stderr.Success +} + +func checkAndRecordSMSCaptchaRate(ctx context.Context, mobile string) stderr.Code { + rateKey := redisconst.SMSCaptchaPhoneNumberKey(mobile) + for retry := 0; retry < 2; retry++ { + ret, err := checkAndRecordSMSCaptchaRateOnce(rateKey) + if err == nil { + return ret + } + if retry == 0 && isRedisWrongTypeError(err) { + _, _ = appg.Redis.Del(rateKey) + continue + } + log.ErrorX(ctx, "checkAndRecordSMSCaptchaRate failed", log.Any("mobile", mobile), log.E(err)) + return stderr.ErrNetWorkBusy + } + return stderr.ErrNetWorkBusy +} + +func checkAndRecordSMSCaptchaRateOnce(rateKey string) (stderr.Code, error) { + now := time.Now().Unix() + dayWindowSec := int64((24 * time.Hour) / time.Second) + + // 先清理 24 小时窗口外的数据,避免集合无界增长。 + _, err := appg.Redis.ZRemRangeByScore(rateKey, "-inf", strconv.FormatInt(now-dayWindowSec, 10)) + if err != nil { + return stderr.ErrNetWorkBusy, err + } + + for _, rule := range smsCaptchaLimitRules { + windowSec := int64(rule.window / time.Second) + start := strconv.FormatInt(now-windowSec+1, 10) + end := strconv.FormatInt(now, 10) + cnt, countErr := appg.Redis.ZCount(rateKey, start, end) + if countErr != nil { + return stderr.ErrNetWorkBusy, countErr + } + if cnt >= rule.maxCount { + return stderr.VisitLimit, nil + } + } + + member := fmt.Sprintf("%d:%d", now, time.Now().UnixNano()) + _, err = appg.Redis.ZAdd(rateKey, redis.Member{Score: float64(now), Member: member}) + if err != nil { + return stderr.ErrNetWorkBusy, err + } + _, err = appg.Redis.ExpireKey(rateKey, 25*time.Hour) + if err != nil { + return stderr.ErrNetWorkBusy, err + } + return stderr.Success, nil +} + +func isRedisWrongTypeError(err error) bool { + return err != nil && strings.Contains(err.Error(), "WRONGTYPE") +} + +func VerifySmsCode(mobile string, code string) stderr.Code { + mobile = strings.TrimSpace(mobile) + if strings.HasPrefix(mobile, "+86122") { // pass the test mobile + log.Info("VerifySmsCode dismiss test mobile", log.Any("mobile", mobile)) + return stderr.Success + } + if !v10.IsGlobalizationPhoneNumber(mobile) { + return stderr.ErrMobileInvalid + } + mobile = ysphone.FormatPhoneNumber(mobile) + redisKey := redisconst.MobileCaptchaKey(mobile) + redisCode, err := appg.Redis.Get(redisKey) + if err != nil { + log.Error("VerifySmsCode redis get err", log.Any("mobile", mobile), log.E(err)) + return stderr.ErrNetWorkBusy + } + if redisCode != nil && *redisCode == code { + log.Info("VerifySmsCode check correct", log.Any("mobile", mobile), log.Any("code", code), log.Any("redisCode", redisCode)) + return stderr.Success + } + if redisCode == nil { + log.Info("VerifySmsCode miss", log.Any("mobile", mobile), log.Any("code", code), log.Any("redisCode", redisCode)) + return stderr.ErrCaptcha + } + log.Info("VerifySmsCode invalid", log.Any("mobile", mobile), log.Any("code", code), log.Any("redisCode", redisCode)) + return stderr.ErrCaptchaInvalid +} diff --git a/app/service/sys_config/freewatch.go b/app/service/sys_config/freewatch.go new file mode 100644 index 0000000..9adec6a --- /dev/null +++ b/app/service/sys_config/freewatch.go @@ -0,0 +1,10 @@ +package sys_config + +import ( + "91porn-server/models/cache/sysconfdata" +) + +// GetTotalWatchCount 读取系统配置 `totalWatchCount`,若未配置则返回默认值。 +func GetTotalWatchCount() uint64 { + return sysconfdata.GetTotalWatchCount() +} diff --git a/app/service/sys_config/paymentpopup.go b/app/service/sys_config/paymentpopup.go new file mode 100644 index 0000000..5d45626 --- /dev/null +++ b/app/service/sys_config/paymentpopup.go @@ -0,0 +1,192 @@ +package sys_config + +import ( + "fmt" + "math/rand" + "time" + + "91porn-server/app/appg" + "91porn-server/app/proto" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/models/v/sysconfmod" + "91porn-server/models/v/usermod" + + "github.com/vmihailenco/msgpack/v5" +) + +// mapUserPaymentStatusPopup 用户付费状态分层 → 需要拉取的配置项集合 +var mapUserPaymentStatusPopup = map[string]map[sysconfmod.VCode]bool{ + usermod.UserPaymentStatusPopupNewUnpay: { + sysconfmod.VCodeUserPaymentStatusPopupNewUserPopup: true, + sysconfmod.VCodeUserPaymentStatusPopupNewUserFloating: true, + sysconfmod.VCodeUserPaymentStatusPopupNewUserPlayPage: true, + sysconfmod.VCodeUserPaymentStatusPopupNewUserMeTab: true, + sysconfmod.VCodeUserPaymentStatusPopupNewUserVipCardID: true, + sysconfmod.VCodeUserPaymentStatusPopupNewUserCountdown: true, + }, + usermod.UserPaymentStatusPopupUnder7DayUnpay: { + sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayPopup: true, + sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayFloating: true, + sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayPlayPage: true, + sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayMeTab: true, + sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayVipCardID: true, + sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayCountdown: true, + }, + usermod.UserPaymentStatusPopupUnder7DayUnpayNoCountdown: { + sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayPopupNoCountdown: true, + sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayFloatingNoCountdown: true, + sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayPlayPageNoCountdown: true, + sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayMeTabNoCountdown: true, + sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayVipCardID: true, + sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayCountdown: true, + }, + usermod.UserPaymentStatusPopupOver7DayUnpay: { + sysconfmod.VCodeUserPaymentStatusPopupUnpayOver7dayPopup: true, + sysconfmod.VCodeUserPaymentStatusPopupUnpayOver7dayFloating: true, + sysconfmod.VCodeUserPaymentStatusPopupUnpayOver7dayPlayPage: true, + sysconfmod.VCodeUserPaymentStatusPopupUnpayOver7dayMeTab: true, + sysconfmod.VCodeUserPaymentStatusPopupUnpayOver7dayVipCardID: true, + sysconfmod.VCodeUserPaymentStatusPopupUnpayOver7dayCountdown: true, + }, + usermod.UserPaymentStatusPopupOver7DayNeedUpgrade: { + sysconfmod.VCodeUserPaymentStatusPopupPayNeedUpgradePopup: true, + sysconfmod.VCodeUserPaymentStatusPopupPayNeedUpgradeFloating: true, + sysconfmod.VCodeUserPaymentStatusPopupPayNeedUpgradePlayPage: true, + sysconfmod.VCodeUserPaymentStatusPopupPayNeedUpgradeMeTab: true, + sysconfmod.VCodeUserPaymentStatusPopupPayNeedUpgradeVipCardID: true, + sysconfmod.VCodeUserPaymentStatusPopupPayNeedUpgradeCountdown: true, + }, + usermod.UserPaymentStatusPopupMaxVIPLevel: { + sysconfmod.VCodeUserPaymentStatusPopupMaxVIPLevel: true, + }, +} + +// SysConfUserPaymentStatusPopup 根据用户付费状态分层,返回对应的分层弹窗配置与分层标识。 +// 结果按分层做 1-3 分钟随机时长的 Redis 缓存。 +func SysConfUserPaymentStatusPopup(user *usermod.User) (res proto.PaymentStatusPopupConfig, userType string, err error) { + if user == nil { + return + } + userType = user.GetPaymentStatusPopup() + userCreateTime := user.CreatedAt + vCodes, ok := mapUserPaymentStatusPopup[userType] + if !ok { + return + } + + // 缓存键使用原始分层标识:有倒计时/无倒计时两个分层的配置项不同,必须分开缓存, + // 否则会互相覆盖,导致下发错误的图片源。 + cacheKey := fmt.Sprintf("%s:%s", redisconst.UserPaymentStatusPopupCache, userType) + + // 读缓存 + str, err := appg.Redis.Get(cacheKey) + if err != nil { + log.Warn(fmt.Sprintf("获取用户分层弹窗缓存数据异常:%v", err)) + } + if str != nil { + if err = msgpack.Unmarshal([]byte(*str), &res); err == nil { + // 缓存按分层(userType)共享、不含 uid,图片配置可共享;但 LastDiscountTime + // 是 per-user 动态值(依赖当前用户注册时间),必须基于当前用户重算, + // 否则会串用到同分层其他用户的时刻。 + if !res.LastDiscountTime.IsZero() { + res.LastDiscountTime = userCreateTime.AddDate(0, 0, countdownDaysOf(userType)).Local() + } + userType = normalizePaymentStatusPopupType(userType) + return + } + log.Warn(fmt.Sprintf("解析用户分层弹窗缓存数据异常:%v", err)) + } + + vCodesKey := make([]sysconfmod.VCode, 0, len(vCodes)) + for vCode := range vCodes { + vCodesKey = append(vCodesKey, vCode) + } + data, err := sysconfmod.BatchGetInfoByVCode(vCodesKey) + if err != nil { + log.Error("获取用户分层弹窗数据失败!", log.E(err)) + res = proto.PaymentStatusPopupConfig{} // 兜底:返回空配置 + } else { // 使用两层 else 是为了让兜底方案也能被缓存,避免故障时每次请求都访问数据库 + for _, d := range data { + switch d.VCode { + case string(sysconfmod.VCodeUserPaymentStatusPopupNewUserPopup), + string(sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayPopup), + string(sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayPopupNoCountdown), + string(sysconfmod.VCodeUserPaymentStatusPopupUnpayOver7dayPopup), + string(sysconfmod.VCodeUserPaymentStatusPopupPayNeedUpgradePopup): + res.Homepage = d.Value + case string(sysconfmod.VCodeUserPaymentStatusPopupNewUserFloating), + string(sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayFloating), + string(sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayFloatingNoCountdown), + string(sysconfmod.VCodeUserPaymentStatusPopupUnpayOver7dayFloating), + string(sysconfmod.VCodeUserPaymentStatusPopupPayNeedUpgradeFloating): + res.HomepageFlot = d.Value + case string(sysconfmod.VCodeUserPaymentStatusPopupNewUserPlayPage), + string(sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayPlayPage), + string(sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayPlayPageNoCountdown), + string(sysconfmod.VCodeUserPaymentStatusPopupUnpayOver7dayPlayPage), + string(sysconfmod.VCodeUserPaymentStatusPopupPayNeedUpgradePlayPage): + res.PlayPage = d.Value + case string(sysconfmod.VCodeUserPaymentStatusPopupNewUserMeTab), + string(sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayMeTab), + string(sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayMeTabNoCountdown), + string(sysconfmod.VCodeUserPaymentStatusPopupUnpayOver7dayMeTab), + string(sysconfmod.VCodeUserPaymentStatusPopupPayNeedUpgradeMeTab), + string(sysconfmod.VCodeUserPaymentStatusPopupMaxVIPLevel): + res.MeTab = d.Value + case string(sysconfmod.VCodeUserPaymentStatusPopupNewUserVipCardID), + string(sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayVipCardID), + string(sysconfmod.VCodeUserPaymentStatusPopupUnpayOver7dayVipCardID), + string(sysconfmod.VCodeUserPaymentStatusPopupPayNeedUpgradeVipCardID): + res.VipCard = d.Value + case string(sysconfmod.VCodeUserPaymentStatusPopupNewUserCountdown), + string(sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayCountdown): + if d.VCode == string(sysconfmod.VCodeUserPaymentStatusPopupNewUserCountdown) && d.Value == "1" { + res.LastDiscountTime = userCreateTime.AddDate(0, 0, 1).Local() + } + if d.VCode == string(sysconfmod.VCodeUserPaymentStatusPopupUnpayUnder7dayCountdown) && d.Value == "1" { + res.LastDiscountTime = userCreateTime.AddDate(0, 0, 2).Local() + } + } + } + } + + common.Go(func() { + // 加入缓存,随机 60-180 秒,避免缓存集中失效 + random := rand.Intn(120) + 60 + b, mErr := msgpack.Marshal(res) + if mErr != nil { + return + } + if sErr := appg.Redis.Set(cacheKey, b, time.Duration(random)*time.Second); sErr != nil { + log.Warn(fmt.Sprintf("设置用户分层弹窗缓存数据异常:%v", sErr)) + } + }) + + userType = normalizePaymentStatusPopupType(userType) + return +} + +// normalizePaymentStatusPopupType 统一对外的分层标识:无倒计时仅作内部区分,对外并入 under_7_day_unpay。 +// 前端通过 LastDiscountTime 是否为零值来判断是否展示倒计时。 +func normalizePaymentStatusPopupType(userType string) string { + if userType == usermod.UserPaymentStatusPopupUnder7DayUnpayNoCountdown { + return usermod.UserPaymentStatusPopupUnder7DayUnpay + } + return userType +} + +// countdownDaysOf 返回该分层倒计时天数(用于命中缓存时按当前用户重算 LastDiscountTime): +// 新用户 1 天,7天内未付款(含无倒计时变体) 2 天,其余分层无倒计时。 +// 需与生成路径 AddDate 的天数保持一致。 +func countdownDaysOf(userType string) int { + switch userType { + case usermod.UserPaymentStatusPopupNewUnpay: + return 1 + case usermod.UserPaymentStatusPopupUnder7DayUnpay, + usermod.UserPaymentStatusPopupUnder7DayUnpayNoCountdown: + return 2 + } + return 0 +} diff --git a/app/service/systemser/config.go b/app/service/systemser/config.go new file mode 100644 index 0000000..5deb554 --- /dev/null +++ b/app/service/systemser/config.go @@ -0,0 +1,14 @@ +package systemser + +import ( + "91porn-server/common/stderr" + "91porn-server/models/v/systemmod" +) + +func List() ([]*systemmod.Config, stderr.Code) { + list, err := systemmod.List() + if err != nil { + return nil, stderr.ErrDbQueryError + } + return list, stderr.Success +} diff --git a/app/service/tagser/list.go b/app/service/tagser/list.go new file mode 100644 index 0000000..aa728a8 --- /dev/null +++ b/app/service/tagser/list.go @@ -0,0 +1,89 @@ +package tagser + +import ( + "encoding/json" + + "91porn-server/app/appg" + "91porn-server/common/constant/redisconst" + "91porn-server/models/v/recmdtag" + "91porn-server/models/v/tagmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type M = bson.M + +type TagGroup struct { + Hot []Tag `json:"hot"` + Newest []Tag `json:"newest"` + Def []Tag `json:"playCount"` +} + +func (t TagGroup) MarshalBinary() ([]byte, error) { + return json.Marshal(t) +} + +func (t *TagGroup) UnmarshalBinary(b []byte) error { + return json.Unmarshal(b, t) +} + +type Tag struct { + ID primitive.ObjectID `json:"id"` + Name string `json:"name"` // 标签名字 前端使用name +} + +type RecmdTagSlice []recmdtag.RecmdTag + +func (r RecmdTagSlice) ToTagsMap() map[recmdtag.RecmdType][]Tag { + m := make(map[recmdtag.RecmdType][]Tag) + for _, v := range r { + m[v.Types] = append(m[v.Types], Tag{ + v.TID, + v.Name, + }) + } + return m +} + +// GetTagGroup +// 每种类型的标签返回limit个 +// "hot": [], 热搜标签 +// "newest": [], 新增标签 +// "playCount": [], 普通标签 +func GetTagGroup(limit int64) TagGroup { + redisKey := redisconst.TagGroup() + tagGroup := TagGroup{} + if err := appg.Redis.Scan(redisKey, &tagGroup); err == nil { + return tagGroup + } + tLimit := limit * 3 + recmdTags, _ := recmdtag.List(recmdtag.Sort_sortCode_N1, nil, &tLimit) + recmdTagSlice := RecmdTagSlice(recmdTags) + tagsMap := recmdTagSlice.ToTagsMap() + defTag := tagsMap[recmdtag.Def] + if len(defTag) < 16 { + randomTags, _ := getRandomTags(16 - int64(len(defTag))) + defTag = append(defTag, randomTags...) + } + tagGroup = TagGroup{ + Hot: tagsMap[recmdtag.Hot], + Newest: tagsMap[recmdtag.New], + Def: defTag, + } + _ = appg.Redis.Set(redisKey, tagGroup, redisconst.TagGroupExpireMax) + return tagGroup +} + +// 随机的 +func getRandomTags(limit int64) ([]Tag, error) { + tags, err := tagmod.GetListRandom(limit) + if err != nil { + return nil, err + } + list := make([]Tag, len(tags)) + for i, v := range tags { + list[i] = Tag{v.ID, v.TagName} + } + return list, nil +} diff --git a/app/service/tagser/tag.go b/app/service/tagser/tag.go new file mode 100644 index 0000000..fbb777e --- /dev/null +++ b/app/service/tagser/tag.go @@ -0,0 +1,313 @@ +package tagser + +import ( + "91porn-server/models/v/collectmod" + "strings" + "sync" + "time" + + "91porn-server/app/service/vidhelpser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/tagconfmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/usertagmod" + "91porn-server/models/v/vidmod" + + "github.com/jinzhu/copier" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID +type VideoRes = *vidmod.VideoInfo +type ListRespInfo = tagmod.ListRespInfo + +// 获取标签数量 +func GetTagsCountByRegexName(content string) (data int64, err error) { + return tagmod.FindTagsCountByRegexName(content) +} + +// 获取专题推荐列表 +func GetTagsList(uid uint64, page commod.Page) TagGroupResp { + var hasNext = false + resp := TagGroupResp{} + skip := (page.PageNumber - 1) * page.PageSize + limit := page.PageSize + 1 + tags, err := tagmod.TagListByFakePlayCount(int64(skip), int64(limit)) + if err != nil { + log.Error("app service tag TagGroupForReco TagGroupForReco err", log.E(err)) + return resp + } + if uint64(len(tags)) > page.PageSize { + hasNext = true + tags = tags[:page.PageSize] + } + tagMap, tidList := convertTagList2Map(tags) + var ( + wg = sync.WaitGroup{} + tidVideoList = make([]TidVideo, 0, len(tidList)) + ) + wg.Add(len(tidList)) + for _, tid := range tidList { + _tid := tid + common.Go(func() { + defer wg.Done() + videoReslist := VideoListSortByTagID(uid, 1, _tid, 0, 5) + if err != nil { + log.Error("VideoListSortByTagID error", log.E(err)) + return + } + tv := TidVideo{ + TID: _tid, + VideoResList: videoReslist, + } + tidVideoList = append(tidVideoList, tv) + }) + } + wg.Wait() + //组装[]ListRespInfo 并恢复标签顺序 + videoListMap := ToTidVideoListMap(tidVideoList) + respInfoList := make([]ListRespInfo, 0, len(tidList)) + for _, tid := range tidList { + tag := tagMap[tid] + if videoResList, ok := videoListMap[tid]; ok { + info := ListRespInfo{ + TagID: tid.Hex(), + TagName: tag.TagName, + TagDesc: tag.TagDesc, + CoverImg: tag.CoverImg, + Description: tag.Description, + TPlayCount: tag.TPlayCount, + VidInfo: videoResList, + } + respInfoList = append(respInfoList, info) + } + } + resp.HasNext = hasNext + resp.List = respInfoList + return resp +} + +type TidVideo struct { + TID ObjectID + VideoResList []VideoRes +} + +func ToTidVideoListMap(tvList []TidVideo) map[ObjectID][]VideoRes { + m := make(map[ObjectID][]VideoRes) + for _, v := range tvList { + m[v.TID] = v.VideoResList + } + return m +} + +func convertTagList2Map(taglist []tagmod.Tag) (map[ObjectID]tagmod.Tag, []ObjectID) { + m := make(map[primitive.ObjectID]tagmod.Tag) + tidList := make([]ObjectID, len(taglist)) + for i, v := range taglist { + m[v.ID] = v + tidList[i] = v.ID + } + return m, tidList +} + +// 根据输入内容获取标签列表(模糊搜索) +func GetRelatedTagsList(content string, page commod.Page) (code stderr.Code, data interface{}, err error) { + stdQuery := commod.StdQuery{ + Page: &commod.PageBy{ + Num: page.PageNumber, + Size: page.PageSize, + }, + Order: &[]commod.OrderBy{{ + Key: "tPlayCount", + Desc: true, + }}, + } + tagsList, err := tagmod.FindTagsByRegexName(content, stdQuery) + if err != nil { + code = stderr.ErrGetTagDataFailed + log.Error("app service tag GetRelatedTagsList FindTagsByRegexName err", log.E(err)) + return + } + list := make([]tagmod.RespInfo, len(tagsList)) + for i, vt := range tagsList { + list[i] = tagmod.RespInfo{ + ID: vt.ID, + Name: vt.TagName, + CoverImg: vt.CoverImg, + Description: vt.Description, + PlayCount: vt.TPlayCount, + } + } + data = list + code = stderr.Success + return +} + +// 点击红心按钮添加标签到用户标签列表中 +func AddToUserTag(uid uint64, tagID ObjectID) (err error) { + uTagMod := usertagmod.UserTag{ + UID: uid, + TagID: tagID, + CreatedAt: time.Now(), + } + if err = usertagmod.UTagInsertOne(&uTagMod); err != nil { + log.Error("app service tag AddToUserTag UTagInsertOne err", log.E(err)) + return + } + return +} + +// 用户删除标签 +func DeleteUserTag(uid uint64, tagID ObjectID) error { + if err := usertagmod.UTagDeleteOne(uid, tagID); err != nil { + log.Error("app service tag DeleteUserTag UTagDeleteOne err", log.E(err)) + return err + } + return nil +} + +// 用户新增一个标签 +func UserAddNewTag(uid uint64, tagName string, coverImg string, description string) (data interface{}, err error) { + tagMod := tagmod.Tag{ + TagName: strings.Trim(tagName, ""), + CoverImg: coverImg, + Description: description, + IsActive: true, + } + id, err := tagmod.InsertOne(&tagMod) + if err != nil { + log.Error("app service tag UserAddNewTag InsertOne err", log.E(err)) + return + } + uTagMod := usertagmod.UserTag{ + UID: uid, + TagID: id, + CreatedAt: time.Now(), + } + if err = usertagmod.UTagInsertOne(&uTagMod); err != nil { + log.Error("app service tag UserAddNewTag UTagInsertOne err", log.E(err)) + return + } + tagData, err := tagmod.FindOneTagByID(id) + if err != nil { + log.Error("app service tag UserAddNewTag FindOneTagByID err", log.E(err)) + return + } + data = tagmod.RespInfo{ + ID: tagData.ID, + Name: tagData.TagName, + CoverImg: tagData.CoverImg, + Description: tagData.Description, + } + return +} + +// GetTagInfo 标签详情 +func GetTagInfo(uid uint64, tagID ObjectID) (code stderr.Code, data tagmod.RespInfo, err error) { + // 获取标签信息 + tagData, err := tagmod.FindOneTagByID(tagID) + if err != nil { + code = stderr.ErrGetTagDataFailed + log.Error("app service tag GetTagInfo FindOneTagByID err", log.E(err)) + return + } + data = tagmod.RespInfo{ + ID: tagData.ID, + Name: tagData.TagName, + CoverImg: tagData.CoverImg, + Description: tagData.TagDesc, + PlayCount: tagData.TPlayCount, + VideoCount: tagData.VideoCount, + } + collects, err := collectmod.CollFindCollectsByUIDAndObjId(uid, tagData.ID) + if err != nil { + code = stderr.ErrDbQueryError + log.Error("app service tag CollFindCollectsByUIDAndObjId err", log.E(err)) + return + } + if !collects.ID.IsZero() { + data.HasCollected = true + } + code = stderr.Success + return +} + +func VideoListSortByTagID(uid uint64, sort int, tid ObjectID, skip, limit uint64) []*vidmod.VideoInfo { + vmodList, err := vidmod.VideoListByTagIDSort(tid, vidmod.SP, sort, skip, limit) + if err != nil { + return nil + } + oids := make([]primitive.ObjectID, len(vmodList)) + for i, v := range vmodList { + oids[i] = v.ID + } + return vidhelpser.GetVideosByIDs(uid, oids) +} + +// 获取常用上传标签 +func GetCommonUsedTagList(content string, pageNumber, pageSize uint64) (data commod.ListResp) { + var hasNext = false + var stdQuery = commod.StdQuery{ + Page: &commod.PageBy{ + CheckNext: true, + Num: pageNumber, + Size: pageSize, + }, + Order: &[]commod.OrderBy{}, + } + tList, err := tagmod.FindPureTags(content, stdQuery) + if err != nil { + log.Error("web service tag GetTagList FindTagsAndCount fail error", log.E(err), log.Any("content", content)) + return commod.ListResp{} + } + if uint64(len(tList)) > pageSize { + hasNext = true + tList = tList[:pageSize] + } + data.List = tList + data.HasNext = hasNext + return +} + +// GetMostPlayedTagList 获取播放量靠前的tag +func GetMostPlayedTagList(req MostPlayedTagListRequest) (resp MostPlayedTagListResponse, err error) { + tags, hasNext, err := tagmod.GetTagListMostPlayed(req.Page) + if err != nil { + return + } + err = copier.Copy(&resp.Tags, &tags) + resp.HasNext = hasNext + return +} + +func GetCommonUsedRecmdTags() (resp AllTagConfResponse, err error) { + tags, err := tagmod.GetCommonRecmdTags() + if err != nil { + return + } + err = copier.Copy(&resp.Tags, tags) + return +} + +// GetAllTagConf 获取所有后台配置标签 +func GetAllTagConf() (resp AllTagConfResponse, err error) { + tagConfs, err := tagconfmod.GetAllTagConf() + if err != nil { + return + } + tagIDs := make([]primitive.ObjectID, len(tagConfs)) + for i, tag := range tagConfs { + tagIDs[i] = tag.TagID + } + tags, err := tagmod.FindTagsByIDS(tagIDs) + if err != nil { + return + } + if err = copier.Copy(&resp.Tags, tags); err != nil { + return + } + return +} diff --git a/app/service/tagser/type.go b/app/service/tagser/type.go new file mode 100644 index 0000000..6b125b6 --- /dev/null +++ b/app/service/tagser/type.go @@ -0,0 +1,35 @@ +package tagser + +import ( + "91porn-server/models/commod" + "91porn-server/models/v/tagmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// MostPlayedTagListRequest 获取播放量靠前的tag +type MostPlayedTagListRequest struct { + commod.Page +} + +// 响应实体 +type TagResp struct { + ID primitive.ObjectID `json:"id"` // 标签id + TagName string `json:"tagName"` // 标签名字 前端使用name +} + +// MostPlayedTagListResponse 播放量靠前的tag列表 +type MostPlayedTagListResponse struct { + Tags []TagResp `json:"tags"` // 标签列表 + HasNext bool `json:"hasNext"` // 是否还有下一页 +} + +// AllTagConfResponse 后台配置的所有标签 +type AllTagConfResponse struct { + Tags []TagResp `json:"tags"` // 标签列表 +} + +type TagGroupResp struct { + List []tagmod.ListRespInfo `json:"list"` // 列表 + HasNext bool `json:"hasNext"` // 是否有下一页 +} diff --git a/app/service/taskser/app.go b/app/service/taskser/app.go new file mode 100644 index 0000000..f7cbcbb --- /dev/null +++ b/app/service/taskser/app.go @@ -0,0 +1,102 @@ +package taskser + +import ( + "time" + + "91porn-server/models/v/dailytaskmod" + "91porn-server/models/v/oncetaskmod" + "91porn-server/models/v/prizemod" + "91porn-server/models/v/taskmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type TaskStatus int + +const ( + UnDone TaskStatus = iota + 1 // 未完成 + Finish // 已完成 + Receive // 已领取 + ReDo // 重做 +) + +// 签到信息返回 +type SignDetailsResponse struct { + List []Config `json:"list"` // 列表 + Value int64 `json:"value"` // 累计签到天数 +} + +type Config struct { + ID primitive.ObjectID `json:"id"` // 文档ID + Title string `json:"title"` // 任务标题 + Desc string `json:"desc"` // 任务描述 + Prizes []prizemod.Prize `json:"prizes"` // 奖品列表 + FinishCondition int64 `json:"finishCondition"` // 达成条件 + FinishValue int64 `json:"finishValue"` // 完成数量 + Status TaskStatus `json:"status"` // 任务状态 1:未完成 2:已完成 3:已领取 + BoonType taskmod.ConfigType `json:"boonType"` // 福利类型 + IsToday bool `json:"isToday"` // 是否是今日任务 +} + +type TaskResponse struct { + JewelBoxDetails struct { + List []Config `json:"list"` // 列表 + Value int64 `json:"value"` // 累计活跃值 + TotalValue int64 `json:"totalValue"` // 总活跃值 + } `json:"jewelBoxDetails"` // 宝箱详情 + TaskList []Config `json:"taskList"` // 任务列表 +} + +type TaskDetailsResponse struct { + TaskList []Config `json:"taskList"` // 任务列表 + Value int64 `json:"value"` // 累计金币/充值金额 +} + +type DailyTaskResponse struct { + ID primitive.ObjectID `json:"id"` // 任务id + Title string `json:"title"` // 任务标题 + Img string `json:"img"` // 任务图标 + Type dailytaskmod.DailyTaskTypeEnum `json:"type"` // 0 每日广告点击; 1 每日邀请 + Desc string `json:"desc"` // 任务说明 + Link string `json:"link"` // 跳转地址 + Status int `json:"status"` // 0 未完成; 1 已完成未领取; 2 已领取 + StartAt time.Time `json:"startAt"` // 场次开始时间;CountdownType=1 时为红包雨场次时间,否则零值 + EndAt time.Time `json:"endAt"` // 场次结束时间;CountdownType=1 时为红包雨场次时间,否则零值 + CountdownType int `json:"countdownType"` // 倒计时类型 0-无 1-红包雨倒计时 +} + +type DailyTaskResponseDetail struct { + Prizes []*prizemod.Prize `json:"prizes"` // 奖品列表, 与签到相同 + FinishCondition uint64 `json:"finishCondition"` // 达成条件. 比如点击广告任务中, 表示3次点击才可以领取奖励 + FinishCount uint64 `json:"finishCount"` // 已完成数量. 比如比如点击广告任务中 2表示已经点击2次广告 + Status int `json:"status"` // 0 未完成; 1 已完成未领取; 2 已领取 +} + +type OnceTaskResponse struct { + ID primitive.ObjectID `json:"id"` // 任务id + Title string `json:"title"` // 任务标题 + Img string `json:"img"` // 任务图标 + Type oncetaskmod.OnceTaskTypeEnum `json:"type"` // 0 每日广告点击; 1 每日邀请 + Prizes []*prizemod.Prize `json:"prizes"` // 奖品列表, 与签到相同 + Desc string `json:"desc"` // 任务说明 + Link string `json:"link"` // 跳转地址 + Status int `json:"status"` // 0 未完成; 1 已完成未领取; 2 已领取 + StartAt time.Time `json:"startAt"` // 场次开始时间;CountdownType=1 时为红包雨场次时间,否则零值 + EndAt time.Time `json:"endAt"` // 场次结束时间;CountdownType=1 时为红包雨场次时间,否则零值 + CountdownType int `json:"countdownType"` // 倒计时类型 0-无 1-红包雨倒计时 +} + +type NewTaskResponse struct { + DailyTasks []DailyTaskResponse `json:"dailyTask"` + OnceTasks []OnceTaskResponse `json:"onceTask"` + GrowthTasks []*GrowthTaskResponse `json:"growthTasks"` +} + +type GrowthTaskResponse struct { + ID primitive.ObjectID `json:"id"` // 任务id + Title string `json:"title"` // 任务标题 + Img string `json:"img"` // 任务图标 + Type int `json:"type" bson:"type"` // 配置类型 1-购买金币 + SubTitle string `json:"subTitle"` // 子标题 + Status int `json:"status"` // 0 未完成; 1 已完成未领取; 2 已领取 +} diff --git a/app/service/taskser/sign.go b/app/service/taskser/sign.go new file mode 100644 index 0000000..2d3f455 --- /dev/null +++ b/app/service/taskser/sign.go @@ -0,0 +1,433 @@ +package taskser + +import ( + "91porn-server/app/appg" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/timeutil" + "91porn-server/models/v/prizemod" + "91porn-server/models/v/signtaskmod" + "91porn-server/models/v/taskrecordmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "encoding/json" + "sort" + "strconv" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 签到 +func Sign(uid uint64, configID string) (code stderr.Code) { + now := time.Now() + id, err := primitive.ObjectIDFromHex(configID) + if err != nil { + return stderr.ErrParamError + } + // 获取用户信息 + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + code = stderr.UserIsNotExists + return + } + // 获取配置列表 + signConfig, err := signtaskmod.GetConfigList() + if err != nil { + return stderr.ErrDbQueryError + } + // 查询当前签到配置 + configInfo, err := signtaskmod.GetConfigByID(id) + if err != nil { + return stderr.ErrDbQueryError + } + // 异常判定 未配置签到福利/签到福利类型不正确 + if configInfo == nil || signConfig == nil || len(signConfig) == 0 { + log.Info("未配置签到福利") + return stderr.ErrInvalidRequest + } + // 查询签到信息 + signRecordMap := make(map[string]bool, 0) + taskInfo, err := taskrecordmod.GetSignRecord(uid) + if err != nil { + return stderr.ErrDbQueryError + } + if taskInfo == nil { + log.Info("未查询到签到任务") + return stderr.ErrDbQueryError + } + for _, taskId := range taskInfo.SignRecord { + signRecordMap[taskId.Hex()] = true + } + // 判断是否重复签到 + if _, ok := signRecordMap[configID]; ok { + log.Info("已签到") + return stderr.HasSigned + } + // 计算相差多少天 + duration := timeutil.BeginningOfDay(now).Sub(taskInfo.SignTime) + diffDays := int64(duration.Hours() / 24) + // 判断此签到是否是当天签到 + if configInfo.FinishCondition-1 != diffDays { + log.Info("条件不匹配") + return stderr.ErrInvalidRequest + } + // 获取奖品列表 + prizes, err := prizemod.GetPrizeListByIDs(configInfo.Prizes) + if err != nil { + return stderr.ErrDbQueryError + } + // 获取额外奖品列表 + signCount := len(signRecordMap) + for _, item := range signConfig { + if item.FinishCondition == int64(signCount)+1 { + extraPrizeIds, e := prizemod.GetPrizeListByIDs(item.ExtraPrizes) + if e != nil { + continue + } + prizes = append(prizes, extraPrizeIds...) + } + } + // 更新签到记录(事务) + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + if err = taskrecordmod.TaskSignUpdate(t, uid, id); err != nil { + return err + } + return nil + }); err != nil { + return stderr.ErrDbTransError + } + // 发放奖励 + for _, v := range prizes { + for i := 0; i < int(v.Count); i++ { + handler, err := prizemod.Run(uid, *v, txnmod.SignBoon) + if err != nil { + log.Error("签到发放奖励handler失败:", log.Any("err", err)) + continue + } + err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + err = handler.Run(t) + if err != nil { + return err + } + err = txnmod.InsertManyTransactionLog(t, handler.GetTransactionLog()) + if err != nil { + return err + } + return nil + }) + if err != nil { + log.Error("签到发放奖励事务失败:", log.Any("err", err)) + continue + } + } + } + return stderr.Success +} + +// 补签 +func ReSign(uid uint64, configID string) (code stderr.Code) { + now := time.Now() + redisKey := "task::resign::" + strconv.FormatUint(uid, 10) + if appg.Redis.Exists(redisKey) { + return stderr.ExceededTimes + } + id, err := primitive.ObjectIDFromHex(configID) + if err != nil { + return stderr.ErrParamError + } + // 获取用户信息 + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + code = stderr.UserIsNotExists + return + } + // 获取配置列表 + signConfig, err := signtaskmod.GetConfigList() + if err != nil { + return stderr.ErrDbQueryError + } + // 查询当前签到配置 + configInfo, err := signtaskmod.GetConfigByID(id) + if err != nil { + return stderr.ErrDbQueryError + } + // 异常判定 未配置签到福利/签到福利类型不正确 + if configInfo == nil || signConfig == nil || len(signConfig) == 0 { + log.Info("未配置签到福利") + return stderr.ErrInvalidRequest + } + //查询签到信息 + signRecordMap := make(map[string]bool, 0) + taskInfo, err := taskrecordmod.GetSignRecord(uid) + if err != nil { + return stderr.ErrDbQueryError + } + if taskInfo == nil { + log.Info("未查询到签到任务") + return stderr.ErrDbQueryError + } + for _, taskId := range taskInfo.SignRecord { + signRecordMap[taskId.Hex()] = true + } + // 判断是否重复签到 + if _, ok := signRecordMap[configID]; ok { + log.Info("已签到") + return stderr.HasSigned + } + // 计算相差多少天 + duration := timeutil.BeginningOfDay(now).Sub(taskInfo.SignTime) + diffDays := int64(duration.Hours() / 24) + // 判断是否在补签范围 + if configInfo.FinishCondition > diffDays { + log.Info("条件不匹配") + return stderr.ErrInvalidRequest + } + + // redis作为倒计时(一天只能补签一次) + seconds := int64(timeutil.BeginningOfTomorrow(now).Sub(now).Seconds()) + err = appg.Redis.Set(redisKey, configID, time.Duration(seconds)*time.Second) + if err != nil { + log.Error(err.Error()) + } + return stderr.Success +} + +// 补签回调 +func ReSignCallBack(uid uint64) (code stderr.Code) { + now := time.Now() + redisKey := "task::resign::" + strconv.FormatUint(uid, 10) + if !appg.Redis.Exists(redisKey) { + return stderr.ErrParamError + } + configID, err := appg.Redis.Get(redisKey) + if err != nil { + log.Error("redis缓存不存在!") + return stderr.ErrParamError + } + if configID == nil { + log.Error("签到配置不存在!") + return stderr.ErrParamError + } + + id, err := primitive.ObjectIDFromHex(*configID) + if err != nil { + return stderr.ErrParamError + } + // 获取用户信息 + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + code = stderr.UserIsNotExists + return + } + // 获取配置列表 + signConfig, err := signtaskmod.GetConfigList() + if err != nil { + return stderr.ErrDbQueryError + } + // 查询当前签到配置 + configInfo, err := signtaskmod.GetConfigByID(id) + if err != nil { + return stderr.ErrDbQueryError + } + // 异常判定 未配置签到福利/签到福利类型不正确 + if configInfo == nil || signConfig == nil || len(signConfig) == 0 { + log.Info("未配置签到福利") + return stderr.ErrInvalidRequest + } + //查询签到信息 + signRecordMap := make(map[string]bool, 0) + taskInfo, err := taskrecordmod.GetSignRecord(uid) + if err != nil { + return stderr.ErrDbQueryError + } + if taskInfo == nil { + log.Info("未查询到签到任务") + return stderr.ErrDbQueryError + } + for _, taskId := range taskInfo.SignRecord { + signRecordMap[taskId.Hex()] = true + } + // 判断是否重复签到 + if _, ok := signRecordMap[*configID]; ok { + log.Info("已签到") + return stderr.HasSigned + } + // 计算相差多少天 + duration := timeutil.BeginningOfDay(now).Sub(taskInfo.SignTime) + diffDays := int64(duration.Hours() / 24) + // 判断是否在补签范围 + if configInfo.FinishCondition > diffDays { + log.Info("条件不匹配") + return stderr.ErrInvalidRequest + } + // 获取奖品列表 + prizes, err := prizemod.GetPrizeListByIDs(configInfo.Prizes) + if err != nil { + return stderr.ErrDbQueryError + } + // 获取额外奖品列表 + signCount := len(signRecordMap) + for _, item := range signConfig { + if item.FinishCondition == int64(signCount) { + extraPrizeIds, e := prizemod.GetPrizeListByIDs(item.ExtraPrizes) + if e != nil { + continue + } + prizes = append(prizes, extraPrizeIds...) + } + } + // 更新签到记录(事务) + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + if err = taskrecordmod.TaskSignUpdate(t, uid, id); err != nil { + return err + } + return nil + }); err != nil { + return stderr.ErrDbTransError + } + // 发放奖励 + for _, v := range prizes { + for i := 0; i < int(v.Count); i++ { + handler, err := prizemod.Run(uid, *v, txnmod.SignBoon) + if err != nil { + log.Error("签到发放奖励handler失败:", log.Any("err", err)) + continue + } + err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + err = handler.Run(t) + if err != nil { + return err + } + err = txnmod.InsertManyTransactionLog(t, handler.GetTransactionLog()) + if err != nil { + return err + } + return nil + }) + if err != nil { + log.Error("签到发放奖励事务失败:", log.Any("err", err)) + continue + } + } + } + return stderr.Success +} + +// 获取签到额外奖励 +func SignExtraPrizes(uid uint64) (out []map[string]interface{}, code stderr.Code) { + out = make([]map[string]interface{}, 0) + taskInfo, err := taskrecordmod.GetSignRecord(uid) + if err != nil { + return out, stderr.ErrDbQueryError + } + signRecords := make([]primitive.ObjectID, 0) + if taskInfo != nil { + signRecords = taskInfo.SignRecord + } + signConfig, err := signtaskmod.GetConfigList() + if err != nil { + return out, stderr.ErrDbQueryError + } + // 获取奖品列表 + prizes, err := prizemod.GetPrizeList() + for _, item := range signConfig { + if len(item.ExtraPrizes) == 0 { + continue + } + extraPrizes := getPrizeList(item.ExtraPrizes, prizes) + for _, prize := range extraPrizes { + b, _ := json.Marshal(&prize) + var extraPrizeMap map[string]interface{} + _ = json.Unmarshal(b, &extraPrizeMap) + days := item.FinishCondition - int64(len(signRecords)) + if days < 0 { + days = 0 + } + extraPrizeMap["finishCondition"] = days + // 设置额外奖励领取状态 + if len(signRecords) >= int(item.FinishCondition) { + extraPrizeMap["extraPrizeStatus"] = true + } else { + extraPrizeMap["extraPrizeStatus"] = false + } + out = append(out, extraPrizeMap) + } + } + sort.Slice(out, func(i, j int) bool { + valI := out[i]["extraPrizeStatus"].(bool) + valJ := out[j]["extraPrizeStatus"].(bool) + return !valI && valJ + }) + return out, stderr.Success +} + +// 获取签到详情 +func GetSignDetails(uid uint64) (out SignDetailsResponse, code stderr.Code) { + now := time.Now() + // 获取配置列表 + signConfig, err := signtaskmod.GetConfigList() + if err != nil { + return out, stderr.ErrDbQueryError + } + // 查询用户签到信息 + signRecordMap := make(map[string]bool, 0) + taskInfo, err := taskrecordmod.GetSignRecord(uid) + if err != nil { + return out, stderr.ErrDbQueryError + } + // 新增用户任务记录 + if taskInfo == nil { + taskInfo, err = taskrecordmod.TaskSignInit(uid) + if err != nil { + return out, stderr.ErrDbQueryError + } + } + // 计算相差多少天 + diffDays := int(timeutil.BeginningOfDay(now).Sub(taskInfo.SignTime).Hours() / 24) + // 判断是否重置数据 + if diffDays >= len(signConfig) { + taskInfo, err = taskrecordmod.TaskSignReset(uid) + if err != nil { + return out, stderr.ErrDbQueryError + } + } + // 构建签到记录map + for _, item := range taskInfo.SignRecord { + signRecordMap[item.String()] = true + } + // 获取奖品列表 + prizes, err := prizemod.GetPrizeList() + if err != nil { + return out, stderr.ErrDbQueryError + } + out.Value = int64(len(taskInfo.SignRecord)) + out.List = make([]Config, len(signConfig)) + for i, v := range signConfig { + taskConfig := Config{ + ID: v.ID, + Prizes: getPrizeList(v.Prizes, prizes), + FinishCondition: v.FinishCondition, + Title: v.Title, + Desc: v.Desc, + Status: UnDone, + } + // 存在表示已经签到,不存在则需要判断是否是补签 + if _, ok := signRecordMap[v.ID.String()]; ok { + // 如果存在,标记为已完成 + taskConfig.Status = Finish + } else { + diffDays = int(timeutil.BeginningOfDay(now).Sub(taskInfo.SignTime).Hours() / 24) + if i < diffDays { + taskConfig.Status = ReDo + } + } + out.List[i] = taskConfig + } + // 当日任务标识 + if diffDays >= 0 && diffDays < len(out.List) { + out.List[diffDays].IsToday = true + } + return out, stderr.Success +} diff --git a/app/service/taskser/task.go b/app/service/taskser/task.go new file mode 100644 index 0000000..a38adaf --- /dev/null +++ b/app/service/taskser/task.go @@ -0,0 +1,1162 @@ +package taskser + +import ( + "91porn-server/models/v/taskrecordmod" + "91porn-server/web/webg" + "errors" + "fmt" + "strconv" + "time" + + "go.mongodb.org/mongo-driver/bson" + + "91porn-server/app/appg" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/timeutil" + "91porn-server/models/l/adsclicklogmod" + "91porn-server/models/l/userdailytasklogmod" + "91porn-server/models/v/dailytaskmod" + "91porn-server/models/v/oncetaskmod" + "91porn-server/models/v/prizemod" + "91porn-server/models/v/proxymod" + "91porn-server/models/v/taskmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +/* +// 获取任务 +func GetTaskList(uid uint64) (out TaskResponse, code stderr.Code) { + var ( + louFengIsReceive bool + now time.Time = time.Now() + consumerFeedbackTask Config = Config{ + BoonType: taskmod.ConsumerFeedback, + Status: UnDone, + } + ) + out.JewelBoxDetails.List = make([]Config, 0) + out.TaskList = make([]Config, 0) + // 获取用户签到信息 + signInfo, err := taskrecordmod.GetByUID(uid) + if err != nil { + code = stderr.ErrDbQueryError + return + } + // 异常判定 + if signInfo == nil { + code = stderr.ErrInvalidRequest + return + } + // 获取奖品列表 + prizes, err := prizemod.GetPrizeList() + if err != nil { + code = stderr.ErrDbQueryError + return + } + // 获取任务配置 + list, err := taskmod.ConfigList() + if err != nil { + code = stderr.ErrDbQueryError + return + } + for _, v := range list { + switch v.Type { + case taskmod.JewelBox: + taskConfig := Config{ + ID: v.ID, + Prizes: getPrizeList(v.Prizes, prizes), + FinishCondition: v.FinishCondition, + Title: v.Title, + Desc: v.Desc, + Status: UnDone, + BoonType: v.Type, + } + // 达到领取条件 + if signInfo.CurrentValue >= v.FinishCondition { + // 已领取 + taskConfig.Status = Finish + if signInfo.HasReceived(v.ID, taskrecordmod.JewelBox) { + taskConfig.Status = Receive + } + taskConfig.FinishValue = v.FinishCondition + } else { + taskConfig.FinishValue = signInfo.CurrentValue + if out.JewelBoxDetails.TotalValue < taskConfig.FinishCondition { + out.JewelBoxDetails.TotalValue = taskConfig.FinishCondition + } + } + out.JewelBoxDetails.List = append(out.JewelBoxDetails.List, taskConfig) + case taskmod.ConsumerFeedback: + consumerFeedbackTask.ID = v.ID + consumerFeedbackTask.Prizes = getPrizeList(v.Prizes, prizes) + consumerFeedbackTask.FinishCondition = v.FinishCondition + consumerFeedbackTask.Title = v.Title + consumerFeedbackTask.Desc = v.Desc + if !louFengIsReceive { + louFengIsReceive = true + // 统计今日楼凤消费 + totalAmount, _, err := txnmod.GetIncomeByType(uid, txnmod.LouFeng, + timeutil.BeginningOfDay(now), timeutil.BeginningOfDay(now.AddDate(0, 0, 1))) + if err != nil { + code = stderr.ErrDbQueryError + return + } + consumerFeedbackTask.FinishValue = -int64(totalAmount) + } + // 任务记录刷新 + if !signInfo.ConsumerFeedbackReceiveTime.After(timeutil.BeginningOfDay(now)) { + signInfo, err = taskrecordmod.ConsumerFeedbackReset(uid) + if err != nil { + code = stderr.ErrDbQueryError + return + } + } + if consumerFeedbackTask.FinishValue >= v.FinishCondition && !signInfo.HasReceived(v.ID, taskrecordmod.ConsumerFeedback) { + consumerFeedbackTask.Status = Finish + } + } + } + if len(out.JewelBoxDetails.List) > 0 && signInfo != nil { + out.JewelBoxDetails.Value = signInfo.CurrentValue + } + if !consumerFeedbackTask.ID.IsZero() { + out.TaskList = append(out.TaskList, consumerFeedbackTask) + } + code = stderr.Success + return +} +*/ + +/* +func GetTaskDetails(uid uint64, boonType int) (out TaskDetailsResponse, code stderr.Code) { + now := time.Now() + // 获取配置列表 + taskDetail, err := taskmod.GetConfigListByType(taskmod.ConfigType(boonType)) + if err != nil { + return out, stderr.ErrDbQueryError + } + // 获取奖品列表 + prizes, err := prizemod.GetPrizeList() + if err != nil { + return out, stderr.ErrDbQueryError + } + //查询用户签到信息 + taskInfo, err := taskrecordmod.GetByUID(uid) + if err != nil { + return out, stderr.ErrDbQueryError + } + // 新增用户任务记录 + if taskInfo == nil { + taskInfo, err = taskrecordmod.AddTaskRecord(uid) + if err != nil { + return out, stderr.ErrDbQueryError + } + } + switch taskmod.ConfigType(boonType) { + case taskmod.ConsumerFeedback: + // 统计今日楼凤消费 + totalAmount, _, err := txnmod.GetIncomeByType(uid, txnmod.LouFeng, + timeutil.BeginningOfDay(now), timeutil.BeginningOfDay(now.AddDate(0, 0, 1))) + if err != nil { + code = stderr.ErrDbQueryError + return + } + out.Value = -int64(totalAmount) + default: + code = stderr.ErrInvalidRequest + return + } + sort.Slice(taskDetail, func(i, j int) bool { // 按照赠送门槛逆序,保证游戏用户能领取游戏充值的游戏币最多的礼物 + return taskDetail[i].FinishCondition > taskDetail[j].FinishCondition + }) + out.TaskList = make([]Config, len(taskDetail)) + for i, v := range taskDetail { + taskConfig := Config{ + ID: v.ID, + Prizes: getPrizeList(v.Prizes, prizes), + FinishCondition: v.FinishCondition, + Title: v.Title, + Desc: v.Desc, + Status: UnDone, + BoonType: v.Type, + } + if out.Value > taskConfig.FinishCondition { // 充值/消费金额满足礼物要求 + switch v.Type { + case taskmod.ConsumerFeedback: // 楼凤消费礼物可以同时领取多个 + taskConfig.Status = Finish + if taskInfo.HasReceived(v.ID, taskrecordmod.ConfigType(v.Type)) { + taskConfig.Status = Receive + } + } + } + out.TaskList[i] = taskConfig + } + return out, stderr.Success +} +*/ + +/* +// 领取福利 +func GetBoon(ctx context.Context, uid uint64, configID string, boonType int) stderr.Code { + var ( + now time.Time = time.Now() + transType txnmod.TransType + ) + id, err := primitive.ObjectIDFromHex(configID) + if err != nil { + return stderr.ErrParamError + } + // 获取任务配置 + configInfo, err := taskmod.GetConfigByID(id) + if err != nil { + return stderr.ErrDbQueryError + } + // 异常判定 配置关闭后请求/配置ID不正确 + if configInfo == nil || configInfo.Type != taskmod.ConfigType(boonType) { + log.InfoX(ctx, "配置关闭后请求/配置ID不正确") + return stderr.ErrInvalidRequest + } + // 获取任务记录 + taskInfo, err := taskrecordmod.GetByUID(uid) + if err != nil { + return stderr.ErrDbQueryError + } + // 异常判定 不满足领取条件用户不会请求该接口 + if taskInfo == nil { + log.InfoX(ctx, "不满足领取条件用户不会请求该接口") + return stderr.ErrInvalidRequest + } + switch taskmod.ConfigType(boonType) { + case taskmod.JewelBox: + // 领取条件判定 + if taskInfo.CurrentValue < configInfo.FinishCondition { + log.InfoX(ctx, "领取条件判定") + return stderr.ErrInvalidRequest + } + transType = txnmod.JewelBoxBoon + case taskmod.ConsumerFeedback: + // 统计今日楼凤消费 + totalAmount, _, err := txnmod.GetIncomeByType(uid, txnmod.LouFeng, + timeutil.BeginningOfDay(now), timeutil.BeginningOfDay(now.AddDate(0, 0, 1))) + if err != nil { + return stderr.ErrDbQueryError + } + if -totalAmount < float64(configInfo.FinishCondition) { + log.InfoX(ctx, "消费回馈(站群)领取条件不足") + return stderr.ErrInvalidRequest + } + transType = txnmod.LouFengConsumerRebate + default: + return stderr.ErrParamError + } + // 异常判定 重复领取 + for _, v := range taskInfo.JewelBoxRecord { + if v == id { + return stderr.ErrDbQueryError + } + } + // 获取奖品列表 + prizes, err := prizemod.GetPrizeListByIDs(configInfo.Prizes) + if err != nil { + return stderr.ErrDbQueryError + } + // 奖励发放 + hanlders := make([]prizemod.PrizeHanlder, 0, len(prizes)) + var transactionLogs []txnmod.TransactionLog + var gameHanlder, addGoldHanlder prizemod.PrizeHanlder + for _, v := range prizes { + hanlder, err := prizemod.Run(uid, *v, transType) + if err != nil { + return stderr.ErrDbQueryError + } + switch v.Type { + case prizemod.Gold: + addGoldHanlder = hanlder + hanlders = append(hanlders, hanlder) + default: + hanlders = append(hanlders, hanlder) + transactionLogs = append(transactionLogs, hanlder.GetTransactionLog()...) + } + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + if err = taskrecordmod.BoonRecordUpdate(t, uid, id, taskrecordmod.ConfigType(boonType)); err != nil { + return err + } + for _, v := range hanlders { + if err = v.Run(t); err != nil { + return err + } + } + if addGoldHanlder != nil { + transactionLogs = append(transactionLogs, addGoldHanlder.GetTransactionLog()...) + } + if len(transactionLogs) > 0 { + if err = txnmod.InsertManyTransactionLog(t, transactionLogs); err != nil { + return err + } + } + // 特殊处理棋牌金币增加 + if gameHanlder != nil { + if err = gameHanlder.Run(t); err != nil { + return err + } + } + return nil + }); err != nil { + return stderr.ErrDbTransError + } + return stderr.Success +} +*/ + +func GetDailyTask(uid uint64) ([]DailyTaskResponse, stderr.Code, string) { + tasks, err := dailytaskmod.GetUserDailyTasks(nil) + if err != nil { + return nil, stderr.ErrDbQueryError, "" + } + tasksLen := len(tasks) + if tasksLen == 0 { + return nil, stderr.Success, "" + } + dtrs := make([]DailyTaskResponse, tasksLen) + for i, task := range tasks { + dtrs[i] = DailyTaskResponse{ + ID: task.ID, + Title: task.Title, + Img: task.Img, + Type: task.Type, + Desc: task.Desc, + Link: task.Link, + Status: 0, + } + today := timeutil.BeginningOfDay(time.Now()) + item, err := taskrecordmod.GetUserRecordByTime(uid, task.ID, today) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;查询用户领取金币任务信息异常:%v", uid, err)) + return nil, stderr.ErrDbQueryError, "" + } + if item != nil && !item.ID.IsZero() { + dtrs[i].Status = item.Status + } + //if task.Type == dailytaskmod.DailyUserLogin || task.Type == dailytaskmod.DailyComment || task.Type == dailytaskmod.DailyPublish || + // task.Type == dailytaskmod.DailyTaskTypeAdsClick || task.Type == dailytaskmod.DailyTaskTypeUserInvite { + // today := timeutil.BeginningOfDay(time.Now()) + // item, err := taskrecordmod.GetUserRecordByTime(uid, task.ID, today) + // if err != nil { + // log.Error(fmt.Sprintf("用户ID:%d;查询用户领取金币任务信息异常:%v", uid, err)) + // return nil, stderr.ErrDbQueryError, "" + // } + // if item != nil && !item.ID.IsZero() { + // dtrs[i].Status = item.Status + // } + //} + } + return dtrs, stderr.Success, "" +} + +func mergePidsAndTrimRepeat(ids1, ids2 []primitive.ObjectID) []primitive.ObjectID { + ids1Len := len(ids1) + ids2Len := len(ids2) + if ids1Len == 0 { + return ids2 + } + if ids2Len == 0 { + return ids1 + } + newIds := make([]primitive.ObjectID, 0, ids1Len+ids2Len) + for _, v := range append(ids1, ids2...) { + found := false + for _, vv := range newIds { + if v == vv { + found = true + break + } + } + if found { + continue + } + newIds = append(newIds, v) + } + return newIds +} + +func CompleteDailyTask(t *db.MongoTool, uid uint64, taskType dailytaskmod.DailyTaskTypeEnum) error { + today := timeutil.BeginOfToday() + cacheKey := redisconst.DailyTaskCacheKey(uid, taskType, today) + redisCli := appg.Redis + if redisCli == nil { + redisCli = webg.Redis + } + if redisCli.Exists(cacheKey) { + return nil // 今日已完成 + } + + // 获取每日任务 + task, err := dailytaskmod.GetUserDailyTaskByType(nil, taskType) + if err != nil { + return stderr.ErrDbQueryError + } + if task == nil || task.ID.IsZero() { + return errors.New("daily task is null") + } + + // 判断今日是否已完成 + item, err := taskrecordmod.GetUserRecordByTime(uid, task.ID, today) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;获取今日任务完成记录异常:%v", uid, err)) + return stderr.ErrDbQueryError + } + if item == nil || item.ID.IsZero() { + var tType taskrecordmod.TaskType + switch taskType { + case dailytaskmod.DailyTaskTypeAdsClick: + tType = taskrecordmod.DailyTaskTypeAdsClick + case dailytaskmod.DailyTaskTypeUserInvite: + tType = taskrecordmod.DailyTaskTypeUserInvite + case dailytaskmod.DailyUserLogin: + tType = taskrecordmod.DailyUserLogin + case dailytaskmod.CommentSetGod: + tType = taskrecordmod.DailyTaskCommentSetGod + case dailytaskmod.DailyDoCollect: + tType = taskrecordmod.DailyDoCollect + case dailytaskmod.DailyPublish: + tType = taskrecordmod.DailyPublish + case dailytaskmod.DailyUnlockByGold: + tType = taskrecordmod.DailyUnlockByGold + case dailytaskmod.DailyComment: + tType = taskrecordmod.DailyComment + default: + return errors.New("不支持的任务类型") + } + + // 更新今日任务记录 + now := time.Now() + filter := bson.M{"uid": uid, "recordTime": today, "taskId": task.ID} + cond := bson.M{ + "uid": uid, + "recordTime": today, + "taskId": task.ID, + "type": taskrecordmod.DailyTask, + "taskType": tType, + "status": taskrecordmod.UnReceived, + "updateTime": now, + "createTime": now, + } + if err := taskrecordmod.InsertTask(nil, filter, cond); err != nil { + return err + } + } + // 缓存标识,以免重复查询 + _ = redisCli.Set(cacheKey, 1, time.Hour*24) + return nil +} + +func GetDailyTaskBoon(uid uint64, taskId primitive.ObjectID) error { + redisKey := redisconst.GetUserDailyTaskRewardLockKey(uid, taskId.String()) + success, err := appg.Redis.Setnx_NewOK(redisKey, "1", redisconst.GetUserDailyTaskRewardLockExpired()) + if err != nil { + return err + } + if !success { + return errors.New("请求过于频繁, 请稍后再试") + } + defer func() { _, _ = appg.Redis.Del(redisKey) }() + task, err := dailytaskmod.GetUserDailyTaskByID(nil, taskId) + if err != nil { + return err + } + var transType txnmod.TransType + switch task.Type { + case dailytaskmod.DailyTaskTypeAdsClick: + transType = txnmod.DailyTaskAdsClick + case dailytaskmod.DailyTaskTypeUserInvite: + transType = txnmod.DailyTaskUserInvite + } + return appg.VideoDB.Trans(func(t *db.MongoTool) error { + prizeIDs, err := getDailyRewards(t, uid, task) + if err != nil { + return err + } + if len(prizeIDs) == 0 { + return nil + } + // 获取奖品 + return getPrizes(t, uid, prizeIDs, transType) + }) +} + +func getDailyRewards(t *db.MongoTool, uid uint64, task *dailytaskmod.DailyTask) ([]primitive.ObjectID, error) { + if task == nil { + return nil, nil + } + taskDetailLen := len(task.Detail) + if taskDetailLen == 0 { + return nil, nil + } + count, err := getUserDailyCountByMongo(uid, task.Type) // 每日已完成数量(已点击次数, 已邀请人数等) + if err != nil { + return nil, err + } + finishCount, err := getUserDailyFinish(t, uid, task.Type) // 每日已领取奖励次数 + if err != nil { + return nil, err + } + if finishCount >= uint64(len(task.Detail)) { + return nil, nil + } + var rewards []primitive.ObjectID + var completeTime uint64 + for i, detail := range task.Detail { + if uint64(i) < finishCount { // 已完成过的不要重复领奖 + count -= detail.FinishCondition + continue + } + if count < detail.FinishCondition { + break + } + count -= detail.FinishCondition + completeTime++ + rewards = append(rewards, detail.Prizes...) + } + if err := userdailytasklogmod.CompleteDailyTask(uid, int64(task.Type), completeTime); err != nil { + return nil, err + } + go func() { + redisKey := redisconst.GetUserDailyTaskRewardTimeKey(uid, int64(task.Type)) + _ = appg.Redis.Set(redisKey, finishCount+completeTime, redisconst.GetUserDailyTaskExpired()) + }() + return rewards, nil +} + +// 获取用户每日数量(广告点击数, 用户邀请数等) +func getUserDailyCount(uid uint64, taskType dailytaskmod.DailyTaskTypeEnum) (uint64, stderr.Code, string) { + count, err := getUserDailyCountByRedis(uid, taskType) + if err == nil { + return count, stderr.Success, "" + } + count, err = getUserDailyCountByMongo(uid, taskType) + if err != nil { + return 0, stderr.ErrDbQueryError, "" + } + return count, stderr.Success, "" +} + +func getUserDailyCountByRedis(uid uint64, taskType dailytaskmod.DailyTaskTypeEnum) (uint64, error) { + redisKey := redisconst.GetUserDailyTaskCountKey(uid, int64(taskType)) + ct, err := appg.Redis.Get(redisKey) + if err != nil { + return 0, err + } + if ct == nil || *ct == "" { + return 0, errors.New("daily task count not found in redis") + } + return strconv.ParseUint(*ct, 10, 0) +} + +func getUserDailyCountByMongo(uid uint64, taskType dailytaskmod.DailyTaskTypeEnum) (uint64, error) { + redisKey := redisconst.GetUserDailyTaskCountKey(uid, int64(taskType)) + switch taskType { + case dailytaskmod.DailyTaskTypeAdsClick: // 每日广告点击 + count, err := adsclicklogmod.GetDailyClickCount(uid) + if err != nil { + return 0, err + } + go func() { _ = appg.Redis.Set(redisKey, count, redisconst.GetUserDailyTaskExpired()) }() + return count, nil + case dailytaskmod.DailyTaskTypeUserInvite: // 每日邀请 + now := time.Now() + count, err := proxymod.CountByUIDAndTime(uid, time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)) + if err != nil { + return 0, err + } + go func() { _ = appg.Redis.Set(redisKey, count, redisconst.GetUserDailyTaskExpired()) }() + return uint64(count), nil + default: + return 0, nil + } +} + +// 获取用户每日完成任务次数 +func getUserDailyFinish(t *db.MongoTool, uid uint64, taskType dailytaskmod.DailyTaskTypeEnum) (uint64, error) { + redisKey := redisconst.GetUserDailyTaskRewardTimeKey(uid, int64(taskType)) + ct, redisErr := appg.Redis.Get(redisKey) + if redisErr == nil && ct != nil && *ct != "" { + if count, err := strconv.ParseUint(*ct, 10, 0); err == nil { + return count, nil + } + } + switch taskType { + case dailytaskmod.DailyTaskTypeAdsClick, dailytaskmod.DailyTaskTypeUserInvite: // 每日广告点击或每日邀请 + finishCount, err := userdailytasklogmod.GetUserFinishCount(uid, taskType) + if err != nil { + return 0, err + } + go func() { _ = appg.Redis.Set(redisKey, finishCount, redisconst.GetUserDailyTaskExpired()) }() + return finishCount, nil + default: + return 0, nil + } +} + +func GetOnceTask(uid uint64) ([]OnceTaskResponse, stderr.Code, string) { + tasks, err := oncetaskmod.GetOnceTaskValid(nil) + if err != nil { + return nil, stderr.ErrDbQueryError, "" + } + tasksLen := len(tasks) + if tasksLen == 0 { + return nil, stderr.Success, "" + } + dtrs := make([]OnceTaskResponse, tasksLen) + for i, task := range tasks { + dtrs[i] = OnceTaskResponse{ + ID: task.ID, + Title: task.Title, + Img: task.Img, + Type: task.Type, + Status: 0, + Desc: task.Desc, + Link: task.Link, + } + if task.Type == oncetaskmod.Download { + dtrs[i].Status, _ = taskrecordmod.IsDownloadTaskComplete(nil, uid, task.ID) + } else { + hasComplete, _ := taskrecordmod.IsOnceTaskComplete(nil, uid, task.ID) + if hasComplete { + dtrs[i].Status = taskrecordmod.Received + } + } + + //taskRecord, err := taskrecordmod.GetUserTaskRecordByUid(uid, taskrecordmod.OnceTask, task.ID) + //if err != nil { + // continue + //} + //if taskRecord == nil || taskRecord.ID.IsZero() { + // continue + //} + //dtrs[i].Status = taskRecord.Status + } + return dtrs, stderr.Success, "" +} + +// GetGrowthTask 获取成长任务 +func GetGrowthTask(uid uint64) ([]*GrowthTaskResponse, stderr.Code, string) { + list, err := taskmod.ConfigList() + if err != nil { + return nil, stderr.ErrDbQueryError, "" + } + res := []*GrowthTaskResponse{} + for _, v := range list { + item := &GrowthTaskResponse{ + ID: v.ID, + Type: int(v.Type), + Title: v.Title, + Img: v.Img, + SubTitle: v.Desc, + } + res = append(res, item) + if len(v.Detail) == 0 { + continue + } + + // 获取任务完成记录 + taskRecord, err := taskrecordmod.GetUserTask(uid, v.ID) + if err != nil { + continue + } + + if taskRecord.Stage < len(v.Detail) { + item.SubTitle = v.Detail[taskRecord.Stage].SubTitle + item.Status = taskRecord.Status + } else { + item.Status = taskrecordmod.Received // 已经超过最后一个阶段,直接展示已领取 + } + } + return res, stderr.Success, "" +} + +func hasFinishOneceTaskByRedis(uid uint64, taskType oncetaskmod.OnceTaskTypeEnum) (bool, error) { + redisKey := redisconst.GetUserOnceTaskCompleteKey(uid, int64(taskType)) + ct, err := appg.Redis.Get(redisKey) + if err != nil { + return false, err + } + if ct == nil || *ct == "" { + return false, errors.New("not found in redis") + } + return *ct == "1", nil +} + +func hasFinishOnceTaskByMongo(uid uint64, taskId primitive.ObjectID) (bool, error) { + u, err := taskrecordmod.GetByUID(uid) + if err != nil { + return false, err + } + if u == nil { + return false, nil + } + for _, v := range u.OnceTask { + if v == taskId { + return true, nil + } + } + return false, nil +} + +func CompleteGrowthTask(uid uint64, configType taskmod.ConfigType, progress int64) { + // 获取该任务 + taskList, err := taskmod.GetConfigListByType(configType) + if err != nil { + log.Error("CompleteGrowthTask taskmod.GetConfigListByType", log.E(err)) + return + } + + if len(taskList) == 0 { + return + } + task := taskList[0] + if len(task.Detail) == 0 { + return + } + // 获取该任务的完成进度 + taskRecord, err := taskrecordmod.GetUserTask(uid, task.ID) + if err != nil { + log.Error("CompleteGrowthTask taskrecordmod.GetUserTaskRecordByUid", log.E(err)) + return + } + // 如果是待领取或者是已领取的情况,则直接返回 + if taskRecord.Status > 0 { + return + } + // 已经没有下一阶段 + if taskRecord.Stage >= len(task.Detail) { + return + } + if progress < int64(task.Detail[taskRecord.Stage].FinishCondition) { + return + } + + taskRecord.Status = taskrecordmod.UnReceived // 修改为待领取状态 + + filter := bson.M{ + "uid": uid, + "taskId": task.ID, + } + updateData := bson.M{ + "$set": bson.M{ + "status": taskRecord.Status, + "updateTime": time.Now(), + }, + "$setOnInsert": bson.M{ + "uid": uid, + "taskId": task.ID, + "createTime": time.Now(), + }, + } + err = taskrecordmod.UpsertOne(filter, updateData) + if err != nil { + log.Error("CompleteGrowthTask taskrecordmod.UpsertOne", log.E(err)) + return + } + return +} + +func CompleteOnceTask(t *db.MongoTool, uid uint64, onceTaskType oncetaskmod.OnceTaskTypeEnum) error { + hasComplete, err := hasFinishOneceTaskByRedis(uid, onceTaskType) + if err == nil && hasComplete { + return nil + } + ot, err := oncetaskmod.GetOnceTaskValidByType(t, onceTaskType) + if err != nil { + if err.Error() == "not found" { + return nil + } + return err + } + if ot == nil { + return nil + } + hasComplete, err = hasFinishOnceTaskByMongo(uid, ot.ID) + if err != nil { + return err + } + if hasComplete { + return nil + } + var transType txnmod.TransType + switch ot.Type { + case oncetaskmod.OnceTaskTypeUserBuyVip: + transType = txnmod.OnceTaskBuyVIP + case oncetaskmod.OnceTaskTypeUserBuyCoin: + transType = txnmod.OnceTaskTypeUserBuyCoin + case oncetaskmod.OnceTaskTypeBindMobile: + transType = txnmod.OnceTaskBindMobile + default: + } + user, err := usermod.FindUserByUIDTrans(t, uid) + if err != nil { + return err + } + if user.ID.IsZero() { + return errors.New("user not found") + } + redisKey := redisconst.GetUserOnceTaskCompleteKey(uid, int64(onceTaskType)) + switch ot.Type { + case oncetaskmod.OnceTaskTypeUserBuyVip: // 购买viP + if !user.IsVIP(time.Now()) { + _ = appg.Redis.Set(redisKey, 0, redisconst.GetUserOnceTaskCompleteExpired()) + return nil + } + if err = taskrecordmod.CompleteOnceTask(t, uid, ot.ID); err != nil { + return err + } + if err = getPrizes(t, uid, ot.Prizes, transType); err != nil { + return err + } + _ = appg.Redis.Set(redisKey, 1, redisconst.GetUserOnceTaskCompleteExpired()) + return nil + case oncetaskmod.OnceTaskTypeBindMobile: // 绑定手机 + if user.Mobile == "" { + _ = appg.Redis.Set(redisKey, 0, redisconst.GetUserOnceTaskCompleteExpired()) + return nil + } + if err = taskrecordmod.CompleteOnceTask(t, uid, ot.ID); err != nil { + return err + } + if err = getPrizes(t, uid, ot.Prizes, transType); err != nil { + return err + } + _ = appg.Redis.Set(redisKey, 1, redisconst.GetUserOnceTaskCompleteExpired()) + return nil + case oncetaskmod.OnceTaskTypeUserBuyCoin: // 购买金币 + if err = taskrecordmod.CompleteOnceTask(t, uid, ot.ID); err != nil { + return err + } + if err = getPrizes(t, uid, ot.Prizes, transType); err != nil { + return err + } + _ = appg.Redis.Set(redisKey, 1, redisconst.GetUserOnceTaskCompleteExpired()) + return nil + default: + return errors.New("unknown type") + } +} + +// 发放奖励 +func getPrizes(t *db.MongoTool, uid uint64, prizeIDs []primitive.ObjectID, transType txnmod.TransType) error { + if len(prizeIDs) == 0 { + return nil + } + // 获取奖品列表 + prizes, err := prizemod.GetPrizeListByIDs(prizeIDs) + if err != nil { + return err + } + if len(prizes) == 0 { + return nil + } + // 奖励发放 + hanlders := make([]prizemod.PrizeHanlder, 0, len(prizes)) + var transactionLogs []txnmod.TransactionLog + var addGoldHanlder prizemod.PrizeHanlder + for _, v := range prizes { + hanlder, err := prizemod.Run(uid, *v, transType) + if err != nil { + return err + } + switch v.Type { + case prizemod.Gold: + addGoldHanlder = hanlder + hanlders = append(hanlders, hanlder) + default: + hanlders = append(hanlders, hanlder) + transactionLogs = append(transactionLogs, hanlder.GetTransactionLog()...) + } + } + for _, v := range hanlders { + if err = v.Run(t); err != nil { + return err + } + } + if addGoldHanlder != nil { + transactionLogs = append(transactionLogs, addGoldHanlder.GetTransactionLog()...) + } + if len(transactionLogs) > 0 { + if err = txnmod.InsertManyTransactionLog(t, transactionLogs); err != nil { + return err + } + } + return nil +} + +func ReceiveTask(uid uint64, in *taskmod.ReceiveTaskReq) (code stderr.Code) { + var ( + transactionLogs []txnmod.TransactionLog + handlers []prizemod.PrizeHanlder + pIds []primitive.ObjectID + rId primitive.ObjectID + ) + user, err := usermod.FindUserByUID(uid) + if err != nil { + return stderr.ErrParamError + } + + if user.ID.IsZero() { + return stderr.UserIsNotExists + } + configID := in.TaskID + id, err := primitive.ObjectIDFromHex(configID) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;领取任务ID:%v;转化ObjectID异常:%v", uid, configID, err)) + return stderr.ErrParamError + } + + if in.Type == 1 { + // 每日任务 + dailyTask, err := dailytaskmod.GetUserDailyTaskByID(nil, id) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;领取任务ID:%v;查询领取任务配置信息异常:%v", uid, configID, err)) + return stderr.ErrDbQueryError + } + + if dailyTask == nil || dailyTask.ID.IsZero() { + log.Warn(fmt.Sprintf("用户ID:%d;领取任务ID:%v;未配置领取福利/领取福利类型不正确", uid, configID)) + return stderr.ErrInvalidRequest + } + // 判断今日是否领取金币 + today := timeutil.BeginningOfDay(time.Now()) + item, err := taskrecordmod.GetUserRecordByTime(uid, dailyTask.ID, today) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;领取任务ID:%v;查询用户领取任务信息异常:%v", uid, configID, err)) + return stderr.ErrDbQueryError + } + + // 异常判定 + if item == nil || item.ID.IsZero() { + log.Warn(fmt.Sprintf("用户ID:%d;领取任务ID:%v;今日已领取", uid, configID)) + return stderr.ErrInvalidRequest + } + + if item.Status == taskrecordmod.Received { + return stderr.UserHasReceivedIntegral + } + + for _, p := range dailyTask.Detail { + pIds = append(pIds, p.Prizes...) + } + rId = item.ID + } + + if in.Type == 2 { + // 一次性任务 + onceTask, err := oncetaskmod.GetTaskByID(nil, id) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;领取任务ID:%v;查询领取任务配置信息异常:%v", uid, configID, err)) + return stderr.ErrDbQueryError + } + + if onceTask == nil || onceTask.ID.IsZero() { + log.Warn(fmt.Sprintf("用户ID:%d;领取任务ID:%v;未配置领取福利/领取福利类型不正确", uid, configID)) + return stderr.ErrInvalidRequest + } + + // 校验是否领取 + task, err := taskrecordmod.GetUserTaskRecordByUid(uid, taskrecordmod.OnceTask, id) + if err != nil { + return stderr.ErrDbQueryError + } + if task == nil || task.ID.IsZero() { + return stderr.UserHasDoTask + } + if task.Status == taskrecordmod.Received { + return stderr.UserHasReceivedIntegral + } + + // 获取任务记录 + pIds = onceTask.Prizes + rId = task.ID + } + if in.Type == 3 { + // 进阶任务 + growthTask, err := taskmod.QueryTaskConfigByCond(bson.M{"_id": id, "status": true}) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;领取任务ID:%v;查询领取任务配置信息异常:%v", uid, configID, err)) + return stderr.ErrDbQueryError + } + + if growthTask == nil || growthTask.ID.IsZero() { + log.Warn(fmt.Sprintf("用户ID:%d;领取任务ID:%v;未配置领取福利/领取福利类型不正确", uid, configID)) + return stderr.ErrInvalidRequest + } + task, err := taskrecordmod.GetUserTask(uid, id) + if err != nil { + return stderr.ErrDbQueryError + } + if task.ID.IsZero() { + return stderr.UserHasDoTask + } + if task.Status == taskrecordmod.Received { + return stderr.UserHasReceivedIntegral + } + // 超过了当前阶段 + if len(growthTask.Detail) <= task.Stage { + return stderr.ErrInvalidRequest + } + // 获取任务记录 + pIds = []primitive.ObjectID{growthTask.Detail[task.Stage].Prize} + + rId = task.ID + } + prizes, err := prizemod.GetPrizeListByIDs(pIds) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;领取积分任务ID:%v;查询奖品列表异常:%v", uid, configID, err)) + return stderr.ErrDbQueryError + } + + if prizes == nil || len(prizes) == 0 { + log.Warn(fmt.Sprintf("用户ID:%d;领取积分任务ID:%v;奖品列表异常", uid, configID)) + return stderr.ErrInvalidRequest + } + + for _, v := range prizes { + handler, err := prizemod.Run(uid, *v, txnmod.ReceiveIntegral) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;领取积分任务ID:%v;奖品派发任务初始化异常:%v", uid, configID, err)) + return stderr.ErrDbQueryError + } + handlers = append(handlers, handler) + } + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + status := taskrecordmod.Received + if in.Type == 3 { + status = 0 + } + if _, err = taskrecordmod.UpdateStatusById(t, rId, status); err != nil { + return err + } + for _, v := range handlers { + if err = v.Run(t); err != nil { + log.Error(fmt.Sprintf("用户ID:%d;领取积分任务ID:%v;奖品派发任务异常:%v", uid, configID, err)) + return err + } + transactionLogs = append(transactionLogs, v.GetTransactionLog()...) + } + if len(transactionLogs) > 0 { + err = txnmod.InsertManyTransactionLog(t, transactionLogs) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;领取积分任务ID:%v;奖品派发资金流水日志插入异常:%v", uid, configID, err)) + return err + } + } + return nil + }); err != nil { + return stderr.ErrDbTransError + } + + log.Info(fmt.Sprintf("用户ID:%d;领取积分任务ID:%v;领取积分成功", uid, in.TaskID)) + return stderr.Success + +} + +func DoTask(uid uint64, in *taskmod.DoTaskReq) (code stderr.Code) { + user, err := usermod.FindUserByUID(uid) + if err != nil { + return stderr.ErrParamError + } + + if user.ID.IsZero() { + return stderr.UserIsNotExists + } + configID := in.TaskID + id, err := primitive.ObjectIDFromHex(configID) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;领取任务ID:%v;转化ObjectID异常:%v", uid, configID, err)) + return stderr.ErrParamError + } + var taskType taskrecordmod.ConfigType + if in.Type == 1 { + // 每日任务 + dailyTask, err := dailytaskmod.GetUserDailyTaskByID(nil, id) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;领取任务ID:%v;查询领取任务配置信息异常:%v", uid, configID, err)) + return stderr.ErrDbQueryError + } + + if dailyTask.ID.IsZero() { + log.Warn(fmt.Sprintf("用户ID:%d;领取任务ID:%v;未配置领取福利/领取福利类型不正确", uid, configID)) + return stderr.ErrInvalidRequest + } + // 判断今日是否领取金币 + today := timeutil.BeginningOfDay(time.Now()) + item, err := taskrecordmod.GetUserRecordByTime(uid, dailyTask.ID, today) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;领取任务ID:%v;查询用户领取任务信息异常:%v", uid, configID, err)) + return stderr.ErrDbQueryError + } + + // 异常判定 + if item != nil && !item.ID.IsZero() { + log.Warn(fmt.Sprintf("用户ID:%d;领取任务ID:%v;今日已领取", uid, configID)) + return stderr.ErrInvalidRequest + } + taskType = taskrecordmod.DailyTask + } + + if in.Type == 2 { + // 一次性任务 + onceTask, err := oncetaskmod.GetTaskByID(nil, id) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;领取任务ID:%v;查询领取任务配置信息异常:%v", uid, configID, err)) + return stderr.ErrDbQueryError + } + + if onceTask == nil || onceTask.ID.IsZero() { + log.Warn(fmt.Sprintf("用户ID:%d;领取任务ID:%v;未配置领取福利/领取福利类型不正确", uid, configID)) + return stderr.ErrInvalidRequest + } + + // 校验是否领取 + task, err := taskrecordmod.GetUserTaskRecordByUid(uid, taskrecordmod.OnceTask, id) + if err != nil { + return stderr.ErrDbQueryError + } + if task != nil && !task.ID.IsZero() { + return stderr.UserHasDoTask + } + // 获取任务记录 + taskType = taskrecordmod.OnceTask + } + + if err = appg.VideoDB.Trans(func(t *db.MongoTool) error { + now := time.Now() + today := timeutil.BeginningOfDay(now) + + // 更新用户记录 + if err = taskrecordmod.InsertOne(taskrecordmod.Task{ + TaskID: id, + Type: taskType, + TaskType: taskrecordmod.Download, + UID: uid, + Status: taskrecordmod.UnReceived, + RecordTime: today, + Count: 1, + UpdateTime: now, + CreateTime: now, + }); err != nil { + return stderr.ErrDbInsertError + } + return nil + }); err != nil { + return stderr.ErrDbTransError + } + + log.Info(fmt.Sprintf("用户ID:%d;任务ID:%v;做任务成功", uid, in.TaskID)) + return stderr.Success + +} diff --git a/app/service/taskser/tool.go b/app/service/taskser/tool.go new file mode 100644 index 0000000..8bb3660 --- /dev/null +++ b/app/service/taskser/tool.go @@ -0,0 +1,20 @@ +package taskser + +import ( + "91porn-server/models/v/prizemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 获取奖品详情列表 通过奖品ID列表 +func getPrizeList(ids []primitive.ObjectID, mPrize map[primitive.ObjectID]prizemod.Prize) []prizemod.Prize { + out := make([]prizemod.Prize, 0, len(ids)) + for _, v := range ids { + item, ok := mPrize[v] + if !ok { + continue + } + out = append(out, item) + } + return out +} diff --git a/app/service/updownloadser/updownloadser.go b/app/service/updownloadser/updownloadser.go new file mode 100644 index 0000000..43402d3 --- /dev/null +++ b/app/service/updownloadser/updownloadser.go @@ -0,0 +1,116 @@ +package updownloadser + +import ( + "context" + "errors" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/models/commod" + "91porn-server/models/v/vidmod" +) + +// SendVidCover2FS 传送图片 +func SendVidCover2FS(id string, ext string, fileData string) (respBody commod.Resp, err error) { + var params = map[string]string{ + "fileData": fileData, + "ext": ext, + } + code, err := httputil.DefaultClientPostJsonWithResp(&respBody, common.BindUrl(appg.Conf.URL.UploadImgUrl, id), nil, params) + log.Info("http method SendVidCover2FS response code ==>", log.Any("statusCode", code), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("file upload wrong ", log.E(err)) + } + return +} + +// SendFile2FS 传送视频文件到Fs +func SendFile2FS(fileID string, fileData string, pos int64, total int64) (respBody commod.Resp, err error) { + respBody = commod.Resp{} + var params = map[string]interface{}{ + "taskId": fileID, + "fileData": fileData, + "pos": pos, + "totalPos": total, + "type": "sp", + } + c, cancle := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancle() + code, err := httputil.DefaultClientPostJsonWithRespWithCtx(c, &respBody, appg.Conf.URL.UploadUrl, nil, params) + log.Info("http method SendFile2FS response code ==>", log.Any("statusCode", code), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("SendFile2FS file upload wrong ", log.E(err)) + } + return +} + +// SendImageToFS 传送图片 +func SendImageToFS(fileName, fileData string) (respBody vidmod.FsSendSingleResp, err error) { + var params = map[string]interface{}{ + "fileData": fileData, + "fileName": fileName, + } + code, err := httputil.DefaultClientPostJsonWithResp(&respBody, appg.Conf.URL.SendSingleFile, nil, params) + log.Info("http method SendImageToFS response code ==>", log.Any("statusCode", code), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("SendImageToFS file upload wrong ", log.E(err)) + } + return +} + +type FileInfo struct { + FileName *string `json:"fileName"` // 文件名称 + FileData *string `json:"fileData"` // 文件存储在那台服务器 + Resize bool `json:"resize"` +} + +type InfoBatch struct { + Batch []*FileInfo `json:"batch"` //Info array +} + +// SendImageToFSBatch 传送图片 +func SendImageToFSBatch(batch InfoBatch) (respBody vidmod.FsSendBatchResp, err error) { + if len(batch.Batch) == 0 { + err = errors.New("no file") + return + } + c, cancle := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancle() + + code, err := httputil.DefaultClientPostJsonWithRespWithCtx(c, &respBody, appg.Conf.URL.SendBatchFile, nil, batch) + log.Info("http method SendImageToFSBatch response code ==>", log.Any("statusCode", code), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("SendImageToFSBatch file upload wrong ", log.E(err)) + } + return +} + +// FsIO 获取io.ready +func FsIO(source, mds string) (data []byte, err error) { + c, cancle := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancle() + _, data, err = httputil.DefaultClientGetBytesWithCtx(c, common.BindUrl(getOriginUrl(mds), source), nil) + if err != nil { + log.ZapLog.Warn("download file error", log.E(err)) + } + return +} + +func getOriginUrl(mds string) string { + switch mds { + case constant.MediaSourcePMS: + return appg.Conf.URL.CdnUrl + case constant.MediaSourceSP: + return appg.Conf.URL.OriginUrl + case constant.MediaSourceLaoSiJi: + return appg.Conf.URL.LaoSiJiOriginUrl + case constant.MediaSourceJH1B: + return appg.Conf.URL.JH1BOriginUrl + default: + return appg.Conf.URL.OriginUrl + } +} diff --git a/app/service/userser/privilegeser.go b/app/service/userser/privilegeser.go new file mode 100644 index 0000000..ba560a5 --- /dev/null +++ b/app/service/userser/privilegeser.go @@ -0,0 +1,42 @@ +package userser + +import ( + "91porn-server/models/v/advanceordermod" + "errors" +) + +const ( + TypeCoinVideo = "coinVideo" + TypeLuckDraw = "luckDraw" + TypeAiUndress = "aiUndress" + TypeDownload = "download" +) + +type ConsumeReq struct { + PrivilegeType string `form:"privilegeType" json:"privilegeType" binding:"required"` // coinVideo:金币视频 luckDraw:抽奖 aiUndress:AI脱衣 download:下载 + Count int64 `form:"count" json:"count" binding:"required"` +} + +// Consume 消费预付权益次数 +func Consume(uid uint64, privilegeType string, count int64) error { + if count < 1 { + return errors.New("debit count illegal") + } + + debitPlan := advanceordermod.DebitPlan{} + switch privilegeType { + case TypeCoinVideo: + debitPlan.CoinVideoCount = &count + //case TypeLuckDraw: + // debitPlan.LuckyDrawCount = &count + //case TypeAiUndress: + // debitPlan.AiUndressCount = &count + case TypeDownload: + debitPlan.DownloadCount = &count + default: + return errors.New("debit type illegal") + } + + err := advanceordermod.Debit(nil, uid, debitPlan) + return err +} diff --git a/app/service/userser/statuser.go b/app/service/userser/statuser.go new file mode 100644 index 0000000..fa005bf --- /dev/null +++ b/app/service/userser/statuser.go @@ -0,0 +1,103 @@ +package userser + +import ( + "context" + "fmt" + "net/http" + "time" + + "91porn-server/app/appg" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/models/commod" +) + +type UserDistrictReq struct { + AppId int32 `form:"appId" json:"appId" binding:"required"` // APPID + DistrictCode string `json:"dc"` // 剪切板信息 dc(渠道) + PromSeqe string `json:"pc"` // 剪切板信息 pc(代理) + IP string `json:"ip"` // IP信息 + SysType string `json:"sysType"` // 操作系统类型 安卓 IOS +} + +type UserDistrictInfo struct { + IsDirect bool `json:"isDirect"` // 是否是直推 + DistrictCode string `json:"districtCode"` // 渠道码 + ParentPromCode string `json:"parentPromCode"` // 全民代理 上级用户推广码 + ParentId uint64 `json:"parentId"` // 全名代理 上级用户 + ParentInviteCount int64 `json:"parentInviteCount"` // 全名代理 上级用户下级用户 +} + +type ShareUrlReq struct { + AppId int32 `from:"appid" json:"appid"` // AppID +} + +// 落地页获取显示的按钮信息 +type ShareUrlResp struct { + Url []string `from:"url" json:"url"` // 分享地址 +} + +func GetUserDistictInfo(dc, pc, ip, sysType string) (dictrictInfo UserDistrictInfo) { + url := appg.Conf.URL.ProductUrl + "/api/stat/userInvite/district" + // 设置默认值 + request := UserDistrictReq{ + AppId: commod.KFK_APPID, + SysType: sysType, + DistrictCode: dc, + PromSeqe: pc, + IP: ip, + } + //限制有渠道码or分享码的就不请求统计中心参与补量查询 + if dc != "" || pc != "" { + if pc != "" { + dictrictInfo.IsDirect = false + dictrictInfo.ParentPromCode = pc + } else { + dictrictInfo.IsDirect = true + dictrictInfo.DistrictCode = dc + } + return + } + // 2秒超时 + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if err := httpRequest(ctx, url, &request, &dictrictInfo); err != nil { + log.Error("GetUserDistictInfo HttpRequest", log.Any("url", url), log.Any("request", request), log.E(err)) + if pc != "" { + dictrictInfo.IsDirect = false + dictrictInfo.ParentPromCode = pc + } else { + dictrictInfo.IsDirect = true + dictrictInfo.DistrictCode = dc + } + return + } + fmt.Println("resp", dictrictInfo) + return dictrictInfo +} + +func httpRequest(ctx context.Context, url string, request interface{}, resp interface{}) error { + code, err := httputil.DefaultClientPostJsonWithRespWithCtx(ctx, resp, url, nil, &request) + if err != nil { + log.Error("HttpRequest err", log.Any("Url", url), log.E(err)) + return err + } + if code != http.StatusOK { + log.Error("HttpRequest err", log.Any("code", code)) + return fmt.Errorf("code err, %d", code) + } + return nil +} + +func UserShareList(ctx context.Context) (urls []string, err error) { + request := ShareUrlReq{ + AppId: commod.KFK_APPID, + } + url := appg.Conf.URL.ProductUrl + "/api/stat/share/list" + resp := &ShareUrlResp{} + if err = httpRequest(ctx, url, &request, resp); err != nil { + log.Error("UserInviteRechargeHandle HttpRequest", log.Any("url", url), log.Any("request", request), log.E(err)) + return nil, err + } + return resp.Url, nil +} diff --git a/app/service/userser/type.go b/app/service/userser/type.go new file mode 100644 index 0000000..92dd33c --- /dev/null +++ b/app/service/userser/type.go @@ -0,0 +1,25 @@ +package userser + +import ( + "91porn-server/models/v/vidmod" +) + +type VideoListResp struct { + List []*vidmod.VideoInfoResp `json:"list"` // 视频列表 + HasNext bool `json:"hasNext"` // 是否还有下一页 + Total int64 `json:"total"` // 视频总数 +} + +type ILikeListRep struct { + List any `json:"list"` // 视频,acg 列表 + HasNext bool `json:"hasNext"` // 是否还有下一页 +} + +// RegisterRequestH5 H5注册请求 +type RegisterRequestH5 struct { + Mobile string `json:"mobile" form:"mobile"` // 手机号 + Email string `json:"email" form:"email"` // 邮箱 + Code string `json:"code" form:"code"` // 验证码 + PassWord string `json:"passWord" form:"passWord"` // 密码 + Account string `json:"account" form:"account"` // 账号 +} diff --git a/app/service/userser/userser.go b/app/service/userser/userser.go new file mode 100644 index 0000000..f672cdc --- /dev/null +++ b/app/service/userser/userser.go @@ -0,0 +1,2044 @@ +package userser + +import ( + "91porn-server/app/appg" + "91porn-server/app/middleware/authuser" + "91porn-server/app/service/adser" + "91porn-server/app/service/imser" + "91porn-server/app/service/mediaser" + "91porn-server/app/service/messageser" + "91porn-server/app/service/notiser" + "91porn-server/app/service/productser" + "91porn-server/app/service/proxyser" + "91porn-server/app/service/sys_config" + "91porn-server/app/service/taskser" + "91porn-server/app/service/vidhelpser" + "91porn-server/app/service/walletser" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/crypt" + "91porn-server/common/datacenter" + "91porn-server/common/db" + "91porn-server/common/log" + sli "91porn-server/common/slice" + "91porn-server/common/stderr" + "91porn-server/common/timeutil" + "91porn-server/common/timeutil/timerange" + "91porn-server/common/version" + "91porn-server/common/ysqr" + "91porn-server/middleware/ua" + "91porn-server/models/cache/aiplazadata" + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/commod" + "91porn-server/models/l/exchlogmod" + "91porn-server/models/l/payvidlgmod" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/v/aiplazamod" + "91porn-server/models/v/dailytaskmod" + "91porn-server/models/v/exchcodemod" + "91porn-server/models/v/faqmod" + "91porn-server/models/v/followmod" + "91porn-server/models/v/idmod" + "91porn-server/models/v/ipblockmod" + "91porn-server/models/v/likemod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/oncetaskmod" + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/proxymod" + "91porn-server/models/v/signrecordmod" + "91porn-server/models/v/sourcemod" + "91porn-server/models/v/sysconfmod" + "91porn-server/models/v/taskmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/videocoupon" + "91porn-server/models/v/videodiscountmod" + "91porn-server/models/v/vidmod" + "91porn-server/models/v/walletmod" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "math" + "math/rand" + "strconv" + "strings" + "sync" + "time" + + "github.com/go-redis/redis" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const firstBindPhoneGold = 3 + +func CheckDevIDSign(devID, devToken string) bool { + if devID == "" || devToken == "" { + return false + } + s := appg.Conf.Base.DevIDSignKey + devID + appg.Conf.Base.DevIDSignKey + sha := sha256.Sum256([]byte(s)) + bsha := base64.StdEncoding.EncodeToString(sha[:]) + return bsha == devToken +} + +// Register 游客注册 +func Register(dc, pc, puc, devID, ip, sysType, devToken, devType, mobile, ver, terminal string, ua ua.UA, tid string) (uid uint64, token string, err error) { + if devType == "aosp:25" { //疑似刷子使用的devType,正常用户已无,封禁注册 + err = errors.New("invalid") + return + } + uid, err = idmod.GetNextUID() + if err != nil { + return + } + //默认用户类型 + userType := usermod.UserUnkown + if terminal == constant.TerminalWeb { //web默认注册为游客 + userType = usermod.UserTourists + } + token, err = authuser.GenToken(&authuser.Claims{UID: uid, Timestamp: time.Now().UnixNano(), Type: uint8(userType)}) + if err != nil { + return + } + sysType = strings.ToLower(sysType) + newPromCode, err := makeOutPromotionCode() + if err != nil { + return + } + portrait := GetPortrait() //设置默认头像 + name := getName() + // 广告用户分组 + adGroup := commod.AdGroupNone + configure, _ := sysconfdata.GetAllFromCache() + adverAbTestSwitch := configure.GetBool(sysconfmod.VCodeAdverAbTestSwitch) + if adverAbTestSwitch && (appg.Conf.Base.Env == "test" || (ua.SysType == constant.SysTypeAndroid && adser.VersionCompare(ver, constant.AdGroupClientVersion))) { + switch uid % 3 { + case 0: + adGroup = commod.AdGroupA + case 1: + adGroup = commod.AdGroupB + default: + adGroup = commod.AdGroupC + } + } + // TODO:根据dc和pc 查询渠道并返回全民代理关系。 + districtInfo := GetUserDistictInfo(dc, pc, ip, sysType) + u := usermod.User{ + UID: uid, + DevID: devID, + Name: name, + Portrait: portrait, + RegisterIP: ip, + WatchCount: sys_config.GetTotalWatchCount(), + VideoDeduction: 3, //默认视频扣量 30% + Token: token, + PromCode: newPromCode, + DevToken: devToken, + VipExpireDate: time.Unix(0, 0), + DevType: devType, + SysType: common.HandleSysType(sysType), + Mobile: mobile, + Ver: ver, + PUC: puc, + DiscDoc: commod.NewDiscDoc(getDiscSeqe(districtInfo.DistrictCode, pc), districtInfo.IsDirect, time.Now()), + LastSysType: common.HandleSysType(sysType), + Type: userType, + Tid: tid, + AdGroup: adGroup, + OriVer: ver, + } + if err = usermod.InsertUser(&u); err != nil { + return + } + if mobile != "" { + go func() { _ = taskser.CompleteOnceTask(nil, uid, oncetaskmod.OnceTaskTypeBindMobile) }() + } + if districtInfo.ParentPromCode != "" { + authPromotionCode(uid, &districtInfo) + } + //Device: ua.SysType, + // DeviceID: CutTo50(ua.DevID), + // UserAgent: ua.UserAgent, + // DeviceModel: ua.DevType, + + if terminal != constant.TerminalWeb { + // 数据中心埋点 + userMsg := &datacenter.UserRegisterMsg{ + UserId: int64(u.UID), + AppId: commod.KFK_APPID, + SysType: u.SysType, + DevType: u.DevType, + Name: u.Name, + DeviceId: u.DevID, + IP: ip, + DistrictCode: u.DistrictCode, + InviteCode: districtInfo.ParentPromCode, + RegisterAt: time.Now(), + UserAgent: ua.BuildID, + } + datacenter.AsyncSendMessage(datacenter.TopicUserRegister, userMsg.Marshal()) + } + redisRegister(uid, sysType, time.Now()) + return +} + +func RandName() string { + return getName() +} + +func getName() string { + const limit = 300 + totalCnt, err := appg.Redis.SCard(redisconst.NameSource) + if err != nil || totalCnt < limit { + if len(appg.Name.FamilyNames) == 0 || len(appg.Name.AfterNames) == 0 { + return "游客" + strconv.FormatInt(time.Now().Unix(), 10) + } + for i := 0; i < limit/100; i++ { + common.Go(func() { + var names []string + for j := 0; j < limit/3; j++ { + length := common.RandInt(2, 4) + if length == 2 { + name := appg.Name.FamilyNames[common.RandInt(0, len(appg.Name.FamilyNames))] + + appg.Name.AfterNames[common.RandInt(0, len(appg.Name.AfterNames))] + names = append(names, name) + + } + if length == 3 { + name := appg.Name.FamilyNames[common.RandInt(0, len(appg.Name.FamilyNames))] + + appg.Name.MiddleNames[common.RandInt(0, len(appg.Name.MiddleNames))] + + appg.Name.AfterNames[common.RandInt(0, len(appg.Name.AfterNames))] + names = append(names, name) + } + } + _, _ = appg.Redis.SAdd(redisconst.NameSource, names) + }) + time.Sleep(100 * time.Millisecond) + } + _, _ = appg.Redis.ExpireKey(redisconst.NameSource, redisconst.NameSourceExpire) + } + name, err := appg.Redis.SPop(redisconst.NameSource) + if name == "" || err != nil { + return "游客" + strconv.FormatInt(time.Now().Unix(), 10) + } + return name +} + +func GetPortrait() string { + var portrait string + if portraitLen := len(appg.Static.Portrait); portraitLen > 0 { + portrait = appg.Static.Portrait[common.RandInt(0, portraitLen)] + } + return portrait +} + +func getDiscSeqe(dc, pc string) commod.DiscSeqe { + var ds commod.DiscSeqe + if dc != "" { + ds.From(dc) + } + ds.PromSeqe = pc + return ds +} + +func redisRegister(uid uint64, sysType string, now time.Time) { + common.Go(func() { //需要使用 common.Go 以监视携程 + recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) + rbk := redisconst.RegistBehaviorKey(uid) + ntf := redisconst.NotifyKey(uid) + redisKey := redisconst.RegistKey(recentMinute) + _, _ = appg.Redis.Piplined(func(pipeliner redis.Pipeliner) error { + pipeliner.SAdd(redisKey, usermod.RegistValue{UID: uid, SysType: sysType}) + pipeliner.Expire(redisKey, redisconst.RegistExpireMax) + return nil + }) + _, _ = appg.Redis.Piplined(func(pipeliner redis.Pipeliner) error { + pipeliner.HMSet(rbk, map[string]interface{}{"default": 1}) + pipeliner.HMSet(ntf, map[string]interface{}{"default": 1}) + pipeliner.Expire(ntf, redisconst.NotifyKeyExpire) + pipeliner.Expire(rbk, redisconst.NewRegisterBehaExpire) + return nil + }) + }) +} + +// 验证推广码 +func authPromotionCode(uid uint64, inviteInfo *UserDistrictInfo) bool { + code := proxyser.InviteBind(uid, inviteInfo.ParentPromCode, false) + // fmt.Println("***authPromotionCode", code) + log.Info("authPromotionCode", log.Any("uid", uid), log.Any("code", code), log.Any("ParentPromCode", inviteInfo.ParentPromCode), log.Any("ParentInviteCount", inviteInfo.ParentInviteCount)) + return code == stderr.Success +} + +// 获取推广吗 +func makeOutPromotionCode() (string, error) { + redisKey := redisconst.PromotionCodeKey() + p, _ := appg.Redis.SPop(redisKey) + if p != "" { + return p, nil + } + return getPromotionCode() +} + +// 获取不重复的推广码 +func getPromotionCode() (string, error) { + promotionCode := common.InvitePromotionCodeGenera() + u, err := usermod.FindUserPromotionCode(promotionCode) + if err != nil { + return "", err + } + if u == nil { + return promotionCode, nil + } + return getPromotionCode() +} + +// MobileBind 绑定手机号码 +func MobileBind(uid uint64, mobile, passWord string) stderr.Code { + //手机号是否绑定 + user, err := usermod.FindUserByMobile(mobile) + if err != nil { + return stderr.Failure + } + if user != nil { + if user.UID == uid { + return stderr.ErrMobileHasBind + } + return stderr.ErrMobileHasBindByOther + } + if err = updateUserMobile(uid, mobile, passWord); err != nil { + log.Warn("updateUserMobile error", log.E(err)) + return stderr.ErrNetWorkBusy + } + go func() { _ = taskser.CompleteOnceTask(nil, uid, oncetaskmod.OnceTaskTypeBindMobile) }() + return stderr.Success +} + +// MobileBind 手机号码登陆 +func MobileLoginOnly(ip, mobile string, ua ua.UA) (*usermod.UserRes, stderr.Code) { + //手机号是否绑定 + user, err := usermod.FindUserByMobile(mobile) + if err != nil { + return nil, stderr.ErrNetWorkBusy + } + if user == nil { + return nil, stderr.ErrLoginMobileNotExist + } + //对马甲账号的合法性做判断,不合法直接视为封禁 + if !IsMaJiaAccount(mobile, user.DevID, user.UID) { + return nil, stderr.ErrAccessForbid + } + if user.HasLocked { + return &usermod.UserRes{User: *user}, stderr.ErrAccessForbid + } + //生产token 刷新token + token, _ := authuser.GenToken(&authuser.Claims{UID: user.UID, Timestamp: time.Now().UnixNano(), Type: uint8(user.Type)}) + if _, err = usermod.UpdateUserToken(user.UID, token); err != nil { + return nil, stderr.ErrNetWorkBusy + } + userInfo, err := Info(user.UID) + if err != nil { + return nil, stderr.ErrNetWorkBusy + } + userInfo.City = vidhelpser.GetLocationByIP(ip) + userInfo.Token = token + authuser.RevokeTokenCache(user.UID) + + return &userInfo, stderr.Success +} + +func lastLogin(uid uint64, token, sys string) error { + _, err := usermod.Update(uid, usermod.UserSelector{ + Token: &token, + LastSysType: &sys, + }) + return err +} + +// 默认为都为非马甲账户 不经过校验,只有是马甲账户时才校验合法, +func IsMaJiaAccount(mobile, devId string, uid uint64) bool { + struid := strconv.FormatUint(uid, 10) + return !(mobile[:len(constant.FakeMobilePrefix)] == constant.FakeMobilePrefix && devId != (usermod.SystemDevIDPrex+struid)) +} + +func updateUserAccount(uid, newUid uint64, account, password string) error { + if account == "" { + return errors.New("empty user account") + } + user, err := usermod.FindUserByUID(uid) + if err != nil { + return err + } + if user.Account == account { + return nil + } + if user.AccountBindAt == nil { + devID := "web-" + user.DevID + ok, err := usermod.BindAccount(uid, newUid, account, password, devID) + if err != nil { + return err + } + if !ok { + return errors.New("bind account failed") + } + } + return nil +} + +func updateUserMobile(uid uint64, mobile, passWord string) error { + if mobile == "" { + return errors.New("mobile is empty") + } + newUser, err := usermod.FindUserByUID(uid) + if err != nil { + return err + } + if newUser.Mobile == mobile { + return nil + } + if newUser.MobileBindAt == nil { //首次绑定 + return appg.VideoDB.Trans(func(t *db.MongoTool) error { + ok, err := usermod.BindMobile(t, uid, mobile, passWord) + if err != nil { + return err + } + if !ok { + return errors.New("mobile bind faild") + } + tl := txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + TranType: txnmod.TranType_WLSysGive.Key(), + TranTypeInt: int64(txnmod.TranType_WLSysGive), + Desc: fmt.Sprintf("绑定手机赠送: %s游戏金币", strconv.Itoa(firstBindPhoneGold)), + DiscDoc: newUser.DiscDoc, + SysType: newUser.SysType, + Money: decimal.NewFromInt(firstBindPhoneGold), + } + return txnmod.InsertTransactionLog(t, &tl) + }) + } + ok, err := usermod.ChangeMobile(uid, mobile) //更换手机号 + if err != nil { + return err + } + if !ok { + return errors.New("mobile change faild") + } + go func() { _ = taskser.CompleteOnceTask(nil, uid, oncetaskmod.OnceTaskTypeBindMobile) }() + return nil +} + +type CutInfo struct { + PromotionCode string `json:"pc"` + DC string `json:"dc"` + PlatformUserCode string `json:"puc"` + Tid string `json:"tid"` +} + +func (this *CutInfo) IsEmpty() bool { + return this.PromotionCode == "" && this.DC == "" +} + +// DeviceLogin () 设备登陆 +func DeviceLogin(ip string, p usermod.DevLoginP, token string, ua ua.UA) (userInfo usermod.UserRes, code stderr.Code, loginType int, err error) { + // iOS 开头的 devType(如 "iOS:25.3.0")统一规范化为 "ios" + if strings.HasPrefix(strings.ToLower(p.DevType), "ios") { + p.DevType = "ios" + } + var u *usermod.User + var isRegister bool + var uid uint64 + if token != "" { + u, _ = usermod.FindUserByToken(token) + if u != nil { + uid = u.UID + loginType = usermod.TokenLogin + } + } + devID := p.DevID + if p.QrCnt != "" { + devID, err = GetDevIDFromQrCnt(p.QrCnt) + if err != nil { + switch err.(type) { + case ysqr.ErrContentInvalid: + code = stderr.ErrQrInvalid + default: + code = stderr.ErrNetWorkBusy + } + return + } + loginType = usermod.QrCodeLogin + } + if u == nil { + u := &usermod.User{} + if devID != "" { + u, _ = usermod.FindUserByDevID(devID) + } else { + code = stderr.ErrDeviceIdAndMobileNil + err = errors.New("devID and mobile is nil") + return + } + if u == nil { + dc, pc, puc, tid := parseCutInfo(p.CutInfos, ip, p.SysType) + ver, err1 := version.New(p.Ver) + if err1 == nil && ver.GTE(version.MustNew(constant.Ver3_6_0)) { + if !CheckDevIDSign(devID, p.DevToken) { + code = stderr.ErrBadDevID + err = errors.New("bad args d") + log.Info("DeviceLogin Register Bad DevID", log.Any("devLogin", p), log.Any("ip", ip)) + return + } + } + // 单 IP 注册总量闸:与 H5Login 共用 register:quota:{ip} 计数,建号前先判定——24h 滚动窗口内 + // 同一真实 IP 注册数超额即拒绝并拉黑。放在设备签名校验之后,避免坏 devID 请求白白消耗配额。 + // 注意:LoginWithDevice 仅在 err != nil 时才读 code,故这里须与其它拒绝分支一样同时置 err。 + if code = checkRegisterQuota(ip); code != stderr.Success { + err = errors.New(code.Msg()) + return + } + log.Info("DeviceLogin Register info", log.Any("devLogin", p), log.Any("ip", ip)) + uid, token, err = Register(dc, pc, puc, devID, ip, p.SysType, p.DevToken, p.DevType, p.Mobile, p.Ver, constant.TerminalAndroid, ua, tid) + if err != nil { + return + } + isRegister = true + } else { + //切换账号 刷新token + token, _ = authuser.GenToken(&authuser.Claims{UID: u.UID, Timestamp: time.Now().UnixNano(), Type: uint8(u.Type)}) + uid = u.UID + } + } + if userInfo, err = Info(uid); err != nil { + return + } + //禁止登陆 + if userInfo.HasLocked { + err = errors.New(stderr.ErrAccessForbid.Msg()) + code = stderr.ErrAccessForbid + return + } + if err = lastLogin(uid, token, p.SysType); err != nil { + return + } + userInfo.City = vidhelpser.GetLocationByIP(ip) + userInfo.Token = token + userInfo.IsRegister = isRegister + authuser.RevokeTokenCache(uid) + + return +} + +func parseCutInfo(cutInfo, ip, sysType string) (dc, pc, puc, tid string) { + var _cutInfo CutInfo //获取推广码/商区 + _ = json.Unmarshal([]byte(cutInfo), &_cutInfo) + log.Info("deviceLogin parseCutInfo info", log.Any("cutInfo", _cutInfo), log.Any("ip", ip), log.Any("systype", sysType)) + dc = _cutInfo.DC + pc = _cutInfo.PromotionCode + puc = _cutInfo.PlatformUserCode + tid = _cutInfo.Tid + return +} + +func GetDevIDFromQrCnt(qrCnt string) (string, error) { + uid, err := ysqr.GetUIDFromQrCnt(ysqr.Login, qrCnt) + if err != nil { + return "", err + } + user, err := usermod.FindUserByUID(uid) + if err != nil { + return "", err + } + if user == nil { + return "", ysqr.ErrContentInvalid{} + } + c := ysqr.Content{ + UID: user.UID, + T: ysqr.Login, + LoginClaims: ysqr.LoginClaims{ + DevID: user.DevID, + }, + } + if qrCnt != c.String(authuser.GetTokenSecret()) { + return "", ysqr.ErrContentInvalid{} + } + return user.DevID, nil +} + +// GetUIDFromQrCnt 校验用户凭证二维码;获取用户uid +func GetUIDFromQrCnt(content string) (uid uint64, err error) { + uid, err = ysqr.GetUIDFromQrCnt(ysqr.Login, content) + if err != nil { + return + } + user, err := usermod.FindUserByUID(uid) + if err != nil { + return + } + if user == nil { + err = ysqr.ErrInvalidUser + return + } + c := ysqr.Content{ + UID: user.UID, + T: ysqr.Login, + LoginClaims: ysqr.LoginClaims{ + DevID: user.DevID, + }, + } + if content != c.String(authuser.GetTokenSecret()) { + err = ysqr.ErrContentIllegal + return + } + return uid, nil +} + +func MyRights(uid uint64) (rights usermod.UserRights, err error) { + coupons, err := videocoupon.GetCouponsByUID(uid) + if err != nil { + return + } + user, err := usermod.FindUserByUID(uid) + if err != nil { + return + } + cards, err := getMyCards(uid) + if err != nil { + return + } + info, err := Info(uid) + if err != nil { + return + } + rights.Cards = cards + rights.UID = uid + rights.GoldVideoCoupons = MergeGoldVideoCoupons(user, coupons) + rights.GoldVideoDiscount = info.PayVidDiscount + rights.VipExpireDate = info.VipExpireDate + rights.GoldVideoFreeExpire = info.GoldVideoFreeExpire + return +} + +func getMyCards(uid uint64) (cards []usermod.Card, err error) { + histories, err := prdcthsomod.FindUserProductHistory(uid) + if err != nil { + return + } + productIDs := make([]primitive.ObjectID, len(histories)) + for i, h := range histories { + productIDs[i] = h.ProductID + } + proucts, err := productmod.FindByProductIDs(productIDs) + if err != nil { + return + } + productMap := make(map[primitive.ObjectID]productmod.Product) + for _, p := range proucts { + productMap[p.ID] = p + } + now := time.Now() + protuctTimeMap := make(map[primitive.ObjectID]time.Duration) + for _, h := range histories { + if h.ProductType != productmod.VIP { + expiration := h.CreatedAt.AddDate(0, 0, productMap[h.ProductID].Duration) + if expiration.After(now) { + duration, ok := protuctTimeMap[h.ProductID] + if !ok { + protuctTimeMap[h.ProductID] = expiration.Sub(now) + } else { + protuctTimeMap[h.ProductID] = duration + expiration.Sub(now) + } + } + } + } + cards = make([]usermod.Card, len(protuctTimeMap)) + i := 0 + for id, d := range protuctTimeMap { + cards[i] = usermod.Card{ + Name: productMap[id].Name, + Expiration: now.Add(d), + } + i++ + } + return +} + +// Info () 我的信息 +func Info(uid uint64) (usermod.UserRes, error) { + var u *usermod.User + var vT int64 + var promoteURL string // 邀请地址 + var inviterCode string // 邀请码 + var buyVidCount int64 // 用户购买的视频数 + var rchgLevel usermod.RechargeLevel // 充值等级 + var videoDiscountLog videodiscountmod.VideoDiscountLog // 视频折扣卡 + var videoCoupons []videocoupon.UserGoldVideoCoupon // 新版观影券 + var whoringCardInfo usermod.WhoringCardInfo // 白嫖卡信息 + var payVidDisc int // 视频折扣 + var isPopUp bool // 是否弹窗 + var totalInvite int64 // 累计邀请人数 + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return usermod.UserRes{}, err + } + correctGoldVideoFreeExpire(u) + if u.SnapVip { + elog, err := exchlogmod.GetLogByUIDAndAuthority(u.UID, exchcodemod.Authority3dPermanentVIP) + if err != nil { + return usermod.UserRes{}, err + } + if elog.CreatedAt.Add(time.Hour * 24).Before(time.Now()) { + s := 0 + u, err = usermod.Update(u.UID, usermod.UserSelector{SnapVip: &s, VipExpireDate: u.OriginVipExpire, VipLevel: u.OriginVipLevel}) + if err != nil { + return usermod.UserRes{}, err + } + } + } + var userInfo usermod.UserRes + if u.HasWhoringCard { + checkTodayLoginSign(uid) + } + var isUpgrade bool // 是否vip升级 + var wg sync.WaitGroup + wg.Add(10) + common.Go(func() { + defer wg.Done() + status := true + // 获取会员卡列表 + productList, _ := productmod.FindProductsByProductType(&status, false, "") + // 根据用户当前卡进行升级 + productser.CheckUserUpgradeProducts(uid, productList) + for _, v := range productList { + if v.IsUpgrade == true { + isUpgrade = true + return + } + } + }) + common.Go(func() { + defer wg.Done() + userInfo.User = *u + if u.VipExpireDate.After(time.Now()) { + userInfo.IsVIP = true + } else { + if u.VipLevel > 0 { + _, err = usermod.UpdateUserVipLevel(uid, 0) + if err != nil { + return + } + } + } + userInfo.Age = u.Age() + userInfo.Token = "" + + userInfo.ShowProxy = true + }) + common.Go(func() { + defer wg.Done() + ivt, err := proxymod.GetInvrLv1(uid) + if err != nil { + return + } + if ivt == 0 { + return + } + u, err := usermod.FindUserByUID(ivt) + if err != nil { + return + } + if u != nil { + inviterCode = u.PromCode + } + }) + common.Go(func() { + defer wg.Done() + promoteURL = sourcemod.GetRandomPromotionURL() + }) + common.Go(func() { + defer wg.Done() + buyVidCount, _ = countPay4VidLogByUID(uid, "SP") + }) + common.Go(func() { + defer wg.Done() + totalInvite, err = proxymod.FindTotal(uid) + }) + common.Go(func() { + defer wg.Done() + videoDiscountLog, err = videodiscountmod.GetByUID(uid) + if videoDiscountLog.Expiration.After(time.Now()) && u.PayVidDiscount > videoDiscountLog.VideoDiscount { + payVidDisc = videoDiscountLog.VideoDiscount + } else { + payVidDisc = u.PayVidDiscount + } + }) + common.Go(func() { + defer wg.Done() + videoCoupons, err = videocoupon.GetCouponsByUID(uid) + if err != nil { + log.Error("Info videocoupon.GetCouponsByUID", log.Any("uid", uid), log.E(err)) + } + }) + //判断用户今日是否领取了金币 + common.Go(func() { + defer wg.Done() + if u.CoinMouthExpireDate.After(time.Now()) { + now := time.Now() + startTime := timeutil.BeginOfTime(now) + endTime := timeutil.BeginningOfTomorrow(now) + filter := bson.M{"uid": uid, "createdAt": bson.M{"$gte": startTime, "$lt": endTime}, "desc": bson.M{"$in": []string{"官方充值-购买[金币月卡]", "金币月卡每日赠送金币"}}} + opts := options.Find() + count, _, _ := txnmod.FindTransactionLogs(filter, opts) + if count < 1 { + isPopUp = true + } + } + }) + common.Go(func() { + defer wg.Done() + resp := []usermod.DetailInfo{} + if u.HasWhoringCard && u.VipExpireDate.After(time.Now()) { + // 获取用户的打卡记录信息 + whoringCardInfo.HasWhoringCard = u.HasWhoringCard + whoringCardInfo.DetailInfo = getSignInfo(uid) + } else { + whoringCardInfo.DetailInfo = resp + } + userInfo.WhoringCard = whoringCardInfo + }) + wg.Wait() + userInfo.IsUpgrade = isUpgrade + userInfo.IsPopUps = isPopUp + userInfo.TotalInvite = totalInvite + userInfo.CollectionCount = vT + userInfo.Follows = u.Follows + userInfo.Fans = u.Fans + userInfo.UpTag = u.UpTag + userInfo.VipName = u.VipName + userInfo.PromoteURL = common.BindUrl(promoteURL, constant.PromotionField+u.PromCode) + userInfo.InviterCode = inviterCode + userInfo.BuyVidCount = buyVidCount + userInfo.RechargeLevel = rchgLevel + userInfo.VideoDiscountExpiration = videoDiscountLog.Expiration + userInfo.GoldVideoCoupon = Modify(videoCoupons, userInfo.GoldVideoCoupon) + userInfo.PayVidDiscount = payVidDisc // 取用户最大视频折扣 + userInfo.GoldVideoFreeExpire = u.GoldVideoFreeExpire + userInfo.BroadcastExpire = u.BroadcastExpire + userInfo.OriVer = u.OriVer + userInfo.AdGroup = u.AdGroup + // 仅当用户参与广告 AB 测(安卓 + 已分组 + 注册版本号 >= AB 测版本号)时才下发展示类型, + // 与广告统计口径 adser.CanRecord 保持一致,避免老版本(注册版本低于门槛)用户误命中。 + if adser.CanRecord(u) { + configure, _ := sysconfdata.GetAllFromCache() + abTestShowTypeMap := configure.GetObject(sysconfmod.VCodeAdverAbTestShowType) + if showType, ok := abTestShowTypeMap[string(u.AdGroup)]; ok { + userInfo.AdverAbTestShowType, _ = strconv.Atoi(showType) + } + } + userInfo.SendMsgPrice = messageser.CheckChatPrice(u) + puc := DLInfo{ + 1, + u.UID, + } + tempM := make(map[string]string) + tempM["puc"], _ = puc.Enc() + b, _ := json.Marshal(tempM) + userInfo.AppStoreCode = string(b) + //webToken + now := time.Now() + expiresAt := now.Add(time.Hour * 24 * 7) + webToken, _ := authuser.GenWebToken(authuser.WebClaims{ + UID: uid, + IssuedAt: now.Unix(), + ExpiresAt: expiresAt.Unix(), + }) + userInfo.WebToken = webToken + + common.Go(func() { + _ = taskser.CompleteDailyTask(nil, uid, dailytaskmod.DailyUserLogin) + // 连续登陆-成长任务 + taskser.CompleteGrowthTask(uid, taskmod.GrowthBuyLogin, userInfo.LoginDays) + }) + + attachImToken(&userInfo) + return userInfo, nil +} + +// attachImToken 给用户信息附加 IM SDK 登录凭据 +func attachImToken(userInfo *usermod.UserRes) { + if userInfo == nil || userInfo.UID == 0 { + return + } + userInfo.ImEnabled = imser.SDKEnabled() + if !userInfo.ImEnabled { + return + } + authInfo, err := imser.GetSDKAuth(userInfo.UID) + if err != nil { + log.Warn("attach im token failed", log.Any("uid", userInfo.UID), log.E(err)) + return + } + userInfo.ImToken = authInfo.ImToken + userInfo.ImUserID = authInfo.ImUserID + userInfo.ImDynamicDomain = authInfo.DynamicConfigDomain + userInfo.ImSocketURL = authInfo.SocketURL +} + +func getSignInfo(uid uint64) (data []usermod.DetailInfo) { + resp := []usermod.DetailInfo{} + // 获取用户的打卡记录信息 + records, err := signrecordmod.GetInfoByUID(uid) + if err != nil { + return resp + } + + if len(records) > 0 { + rts := []usermod.Record{} + for i := range records { + if len(records[i].RecordTime) > 0 { + for _, r := range records[i].RecordTime { + rts = append(rts, usermod.Record{ + SignTime: r.SignTime, + IsSign: r.IsSign, + }) + } + } + var price int64 + if records[i].ForgetSignDays > 0 { + for p := 0; p < int(records[i].ForgetSignDays+records[i].RenewalSignDays); p++ { + if p < int(records[i].RenewalSignDays) { + continue + } + price += int64(math.Pow(2, float64(p)) * 10) + } + } + + resp = append(resp, usermod.DetailInfo{ + ID: records[i].ID, + TotalDays: records[i].TotalDays, + CurrentSignDays: records[i].CurrentSignDays, + RenewalSignDays: records[i].RenewalSignDays, + RenewalSignPrice: price, + ForgetSignDays: records[i].ForgetSignDays, + HasReturn: records[i].HasReturn, + RecordTime: rts, + StartTime: records[i].CreatedAt, + EndTime: records[i].EndTime, + }) + } + } + + return resp +} + +func checkTodayLoginSign(uid uint64) error { + // 获取是否有白嫖卡任务 + signRecord, err := signrecordmod.GetInfoByCond(bson.M{"uid": uid}) + if err != nil { + return err + } + if signRecord == nil || signRecord.ID.IsZero() { + return errors.New("sign record is null") + } + currentTime := time.Now() + + if signRecord.CurrentSignDays == signRecord.TotalDays { + return nil + } + totalDays := signRecord.CurrentSignDays + signRecord.ForgetSignDays + if totalDays >= signRecord.TotalDays { + return nil + } + + // 判断今日是否登录 + today := timeutil.BeginningOfDay(time.Now()) + if signRecord.SignTime.Before(today) && signRecord.EndTime.After(currentTime) { + var isReturn bool + var isReturnAmt int64 + + // 打卡 + update := bson.M{"signTime": currentTime, "currentSignDays": signRecord.CurrentSignDays + 1} + + // 处理是否漏打卡 + duration := today.Sub(signRecord.SignTime) + days := int64(duration.Hours() / 24) + + totalForgetSignDays := signRecord.ForgetSignDays + days + if days > 0 { + // 漏打卡业务 + rec := signRecord.RecordTime + for i := 1; i <= int(days); i++ { + format := currentTime.AddDate(0, 0, -i).Format("2006-01-02") + details := signrecordmod.Record{ + SignTime: format, + IsSign: false, + } + rec = append(rec, details) + } + + update["recordTime"] = rec + update["forgetSignDays"] = totalForgetSignDays + } + if totalForgetSignDays+signRecord.CurrentSignDays+1 > signRecord.TotalDays { + return nil + } + + if signRecord.CurrentSignDays+1 == signRecord.TotalDays { + isReturn = true + update["hasReturn"] = isReturn + // 发放奖励 + product, err := productmod.FindOne(nil, bson.M{"_id": signRecord.PID}, options.FindOne()) + if err != nil { + return err + } + if product == nil || product.ID.IsZero() { + return errors.New("product is null") + } + isReturnAmt = *product.DiscountedPriceIos + } + + if err = appg.VideoDB.Trans(func(tool *db.MongoTool) error { + _, err := signrecordmod.UpdateByCond(tool, bson.M{"_id": signRecord.ID}, update) + if err != nil { + return err + } + + // 补签打卡扣除金币 + var txnLogs []txnmod.TransactionLog + if isReturn { + wallAct, err := walletmod.CreditAmount(tool, isReturnAmt, uid) + if err != nil { + return err + } + tl := txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: isReturnAmt, + ActualAmount: float64(isReturnAmt), + TranType: txnmod.SuccessSignReturnAmount.Key(), + TranTypeInt: int64(txnmod.SuccessSignReturnAmount), + Desc: fmt.Sprintf("%v:%v个", txnmod.SuccessSignReturnAmount.Key(), isReturnAmt), + RealAmount: wallAct.RealAmount(), + } + txnLogs = append(txnLogs, tl) + } + if len(txnLogs) > 0 { + err := txnmod.InsertManyTransactionLog(tool, txnLogs) + if err != nil { + return err + } + } + return nil + }); err != nil { + log.Error(fmt.Sprintf("uid:%v, login sign Trans err:%v", uid, err)) + return err + } + } + + // 如果签到过期 + if signRecord.EndTime.Before(currentTime) && totalDays < signRecord.TotalDays { + // 打卡 + update := bson.M{"signTime": signRecord.EndTime} + + // 处理是否漏打卡 + days := signRecord.TotalDays - signRecord.ForgetSignDays - signRecord.CurrentSignDays + + totalForgetSignDays := signRecord.ForgetSignDays + days + if days > 0 { + // 漏打卡业务 + rec := signRecord.RecordTime + for i := 0; i < int(days); i++ { + format := signRecord.EndTime.AddDate(0, 0, -i).Format("2006-01-02") + details := signrecordmod.Record{ + SignTime: format, + IsSign: false, + } + rec = append(rec, details) + } + + update["recordTime"] = rec + update["forgetSignDays"] = totalForgetSignDays + } + if totalForgetSignDays+signRecord.CurrentSignDays > signRecord.TotalDays { + return nil + } + + _, err := signrecordmod.UpdateByCond(nil, bson.M{"_id": signRecord.ID}, update) + if err != nil { + return err + } + } + + return nil +} + +func correctGoldVideoFreeExpire(user *usermod.User) { + if user.VideoFreeExpiration != nil && user.VideoFreeExpiration.After(user.GoldVideoFreeExpire) { + user.GoldVideoFreeExpire = *user.VideoFreeExpiration + } +} + +// Modify 为非H5端修正数据,合并新版数据 +func Modify(videoCoupons []videocoupon.UserGoldVideoCoupon, oldCoupons []usermod.UserGoldVideoCoupon) []usermod.UserGoldVideoCoupon { + for _, new := range videoCoupons { + if !new.Used { + isNewNum := true + for i, old := range oldCoupons { + if old.Gold == new.Num { + oldCoupons[i].Count++ + isNewNum = false + break + } + } + if isNewNum { + oldCoupons = append(oldCoupons, usermod.UserGoldVideoCoupon{ + Gold: new.Num, + Count: 1, + }) + } + } + } + return oldCoupons +} + +// MergeGoldVideoCoupons 合并新旧版观影券,方便前端展示 +func MergeGoldVideoCoupons(u *usermod.User, coupons []videocoupon.UserGoldVideoCoupon) []usermod.GoldVideoCoupon { + unusedCouponDetails := []usermod.GoldVideoCouponDetail{} + for _, v := range u.GoldVideoCoupon { // 旧版数据均为未使用的 + unusedCouponDetails = append(unusedCouponDetails, usermod.GoldVideoCouponDetail{ + Num: v.Gold, + Count: v.Count, + Source: videocoupon.GoldVideoCouponSourceVIP, + }) + } + unusedCoupons := usermod.GoldVideoCoupon{ + Position: usermod.CouponPositionUnUsed, + Coupons: unusedCouponDetails, + } + usedCoupons := usermod.GoldVideoCoupon{Position: usermod.CouponPositionUsed, + Coupons: []usermod.GoldVideoCouponDetail{}} + for _, v := range coupons { // 新版观影券 + if !v.Used { // 观影券尚未使用 + isNew := true + for i, coupon := range unusedCoupons.Coupons { + if v.Num == coupon.Num { + unusedCoupons.Coupons[i].Count++ + isNew = false + break + } + } + if isNew { + unusedCoupons.Coupons = append(unusedCoupons.Coupons, usermod.GoldVideoCouponDetail{ + Num: v.Num, + Count: 1, + Source: v.Source, + }) + } + } else { // 观影券已被使用 + isNew := true + for i, coupon := range usedCoupons.Coupons { + if v.Num == coupon.Num { + usedCoupons.Coupons[i].Count++ + isNew = false + break + } + } + if isNew { + usedCoupons.Coupons = append(usedCoupons.Coupons, usermod.GoldVideoCouponDetail{ + Num: v.Num, + Count: 1, + Source: v.Source, + }) + } + } + } + ret := []usermod.GoldVideoCoupon{unusedCoupons, usedCoupons} + return ret +} + +// OtherInfo () 他的信息 +func OtherInfo(iID uint64, hID uint64) (userInfo usermod.UserRes, err error) { + var u *usermod.User + //var vT int64 + var buyVidCount int64 //用户购买的视频数 + var rchgLevel usermod.RechargeLevel //充值等级 + var videoDiscountLog videodiscountmod.VideoDiscountLog //视频折扣卡 + var isFollow bool + u, err = usermod.FindUserByUID(hID) + if u == nil || err != nil { + return + } + if u.SnapVip { + var elog exchlogmod.ExchangeLog + elog, err = exchlogmod.GetLogByUIDAndAuthority(u.UID, exchcodemod.Authority3dPermanentVIP) + if err != nil { + return + } + if elog.CreatedAt.Add(time.Hour * 24).Before(time.Now()) { + s := 0 + u, err = usermod.Update(u.UID, usermod.UserSelector{SnapVip: &s, VipExpireDate: u.OriginVipExpire, VipLevel: u.OriginVipLevel}) + if err != nil { + return + } + } + } + var wg sync.WaitGroup + wg.Add(5) + common.Go(func() { + defer wg.Done() + isFollow, _ = followmod.IsFollow(iID, hID) + }) + common.Go(func() { + defer wg.Done() + userInfo.User = *u + if u.VipExpireDate.After(time.Now()) { + userInfo.IsVIP = true + } else { + if u.VipLevel > 0 { + _, err = usermod.UpdateUserVipLevel(hID, 0) + if err != nil { + return + } + } + } + userInfo.Age = u.Age() + userInfo.Token = "" + }) + //common.Go(func() { + // defer wg.Done() + // vT, _ = vidmod.GetPublishedVideoCntByPublisher(hID) + //}) + common.Go(func() { + defer wg.Done() + buyVidCount, _ = countPay4VidLogByUID(hID, "SearchSP") + }) + common.Go(func() { + defer wg.Done() + rchgLevel, _ = walletser.GetRchgLevel(hID) + }) + common.Go(func() { + defer wg.Done() + videoDiscountLog, err = videodiscountmod.GetByUID(iID) + if err != nil { + log.Error("videodiscountmod.GetByUID", log.Any("iID", iID), log.Any("hID", hID), log.E(err)) + } + }) + //common.Go(func() { + // defer wg.Done() + // userInfo.HappinessPlazaCount, _ = vidmod.GetVideoCntByPublisher(hID, vidmod.COVER) + //}) + wg.Wait() + userInfo.Fans = u.Fans + userInfo.Follows = u.Follows + userInfo.UpTag = u.UpTag + userInfo.CollectionCount = u.TotalWorks + userInfo.BuyVidCount = buyVidCount + userInfo.RechargeLevel = rchgLevel + userInfo.VideoDiscountExpiration = videoDiscountLog.Expiration + userInfo.IsFollow = isFollow + return +} + +// BuyVidList () 购买视频列表 +func BuyVidList(uid, oid uint64, newsType string, pageSize int64, pageNumber int64) (data []*vidmod.VideoInfoResp, total int64, hasNext bool, err error) { + if oid == 0 { + oid = uid + } + //var wg sync.WaitGroup + //wg.Add(2) + //common.Go(func() { + //defer wg.Done() + var vl []*payvidlgmod.Pay4VidLog + vl, hasNext, err = payvidlgmod.FindByUID(oid, newsType, pageNumber, pageSize) + if err != nil { + return + } + keyFunc := func(vid string, uid uint64) string { + return fmt.Sprintf("%s-%d", vid, uid) + } + + vlCoinsMap := make(map[string]int64) + for _, v := range vl { + if v != nil { + vlCoinsMap[keyFunc(v.VideoID.Hex(), v.UID)] = v.Coins + } + } + ids := make([]primitive.ObjectID, 0) + for _, v := range vl { + ids = append(ids, v.VideoID) + } + data = vidhelpser.GetVideoListByIDsNoStatus(uid, ids) + if oid == uid { // 仅当用户查看自己的购买列表时才展示购买时的金币数 + for i, v := range data { + if v != nil { + data[i].Coins = vlCoinsMap[keyFunc(v.ID.Hex(), oid)] + } + } + } + //}) + //common.Go(func() { + // defer wg.Done() + // total, _ = countPay4VidLogByUID(oid, newsType) + //}) + //wg.Wait() + return +} + +// GetUserLikeReq +type GetUserLikeReq struct { + commod.Page + UID uint64 `json:"uid" form:"uid"` // 查看那个用户,自己不传 + LikeType string `json:"likeType" form:"likeType"` // 点赞类型: SP/SHORT/COVER/PIC/SEED_LINK 或 video/image/text/drama +} + +// UserLikesVidList 用户喜欢的作品列表 +func UserLikesVidList(uid uint64, req *GetUserLikeReq) (vList any, hasNext bool, err error) { + if req.UID == 0 { + req.UID = uid + } + //var wg sync.WaitGroup + //wg.Add(2) + //common.Go(func() { + // defer wg.Done() + var likeList []likemod.LikeOIDRes + likeList, hasNext, err = likemod.FindLikesByTypeAndUID(req.LikeType, req.UID, int(req.PageNumber), int(req.PageSize)) + var vids []primitive.ObjectID + for _, v := range likeList { + vids = append(vids, v.ObjID) + } + if len(vids) <= 0 { + vList = make([]*vidmod.VideoInfoResp, 0) + return + } + // 区分普通帖子和ACG + switch req.LikeType { + case constant.AiPlaza: + // 获取ai广场帖子 + list, err := aiplazamod.GetAll(bson.M{"_id": bson.M{"$in": vids}}, nil) + if err != nil { + return vList, hasNext, err + } + // 按照点赞时间排序重新排排序 + dataMap := make(map[primitive.ObjectID]aiplazamod.AiPlaza) + for _, v := range list { + dataMap[v.ID] = v + } + infos := []aiplazamod.AiPlaza{} + for _, v := range likeList { + video, ok := dataMap[v.ObjID] + if !ok { + continue + } + infos = append(infos, video) + } + vList = aiplazadata.FormatAppDataList(infos) + case constant.LikeTypeSP, constant.LikeTypeShort, constant.LikeTypeCover, constant.LikeTypePic, constant.LikeTypeSEED_LINK: + videos, err := vidmod.GetVideoListByIDs(vids) + if err != nil { + return vList, hasNext, err + } + //修正用户喜欢的记录 如果此时vList为0 表示用户喜欢的视频 有大量的发生了状态改变,此时需要对记录表进行修正, 删除掉视频状态不为1的喜欢记录 + if len(videos) == 0 { + common.Go(func() { + vidhelpser.FixLikeVideoRecord(uid, vids) + }) + } + // 按照点赞时间排序重新排排序 + videoMap := make(map[primitive.ObjectID]*vidmod.VideoModel) + for _, v := range videos { + videoMap[v.ID] = v + } + infos := []*vidmod.VideoModel{} + for _, v := range likeList { + video, ok := videoMap[v.ObjID] + if !ok { + continue + } + infos = append(infos, video) + } + vList = vidhelpser.NewEncodeVideoInfo(uid, infos) + + case constant.LikeTypeCartoon, constant.LikeTypeComics, constant.LikeTypeText, constant.LikeTypeDrama: + _, m, err := mediamod.GetListByIds(vids) + if err != nil { + return vList, hasNext, err + } + // 按照点赞时间排序重新排排序 + infos := []*mediamod.Media{} + for _, like := range likeList { + if media, ok := m[like.ObjID]; ok { + if req.LikeType == constant.LikeTypeDrama && (media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete) { + continue + } + infos = append(infos, &media) + } + } + if req.LikeType == constant.LikeTypeDrama { + vList = mediaser.FillMedias(infos, uid, true) + } else { + vList = mediaser.FillNotStatusMedias(infos, true) + } + + //case constant.LikeTypeComment: + default: + return + } + + //}) + + //common.Go(func() { + // defer wg.Done() + // total, _ = countLikeVidByUID(uid, constant.LikeTypeSP) + //}) + //wg.Wait() + return vList, hasNext, nil +} + +func countLikeVidByUID(uid uint64, t string) (total int64, err error) { + likes, err := likemod.FindLikeIDSByTypeAndUID(t, uid) + if err != nil { + return + } + ids := make([]primitive.ObjectID, 0) + for _, v := range likes { + ids = append(ids, v.ObjID) + } + if len(ids) < 1 { + return + } + total, err = vidmod.CountPayVidsByUIDS(ids) + if err != nil { + return + } + return +} + +func countPay4VidLogByUID(uid uint64, newsType string) (total int64, err error) { + payvids, err := payvidlgmod.FindManyPay4VidLogByUID(uid, newsType) + if err != nil { + return + } + ids := make([]primitive.ObjectID, 0) + for _, v := range payvids { + ids = append(ids, v.VideoID) + } + if len(ids) < 1 { + return + } + total, err = vidmod.CountPayVidsByUIDS(ids) + if err != nil { + return + } + return +} + +// IVList () 作品 +func IVList(uid uint64, sortType string, pageSize, pageNumber uint64, playTimeType int) (vList []*vidmod.VideoInfoResp, total int64, hasNext bool, err error) { + skip := (pageNumber - 1) * pageSize + limit := pageSize + vModels, total, hasNext, err := vidmod.GetMyWorkList(uid, sortType, skip, limit, playTimeType) + vList = vidhelpser.EncodeVideoInfoBlogger(uid, vModels) + return +} + +// HisVList () 作品 +func HisVList(uid uint64, oid uint64, pageSize, pageNumber uint64, sortType string, playTimeType int) (vList []*vidmod.VideoInfoResp, total int64, hasNext bool, err error) { + var ( + // vModels []*vidmod.VideoModel + skip, limit int64 + status = 1 + ) + skip = int64((pageNumber - 1) * pageSize) + limit = int64(pageSize) + //获取rediskey + redisKey := redisconst.UserCollectionKey(oid, pageNumber, pageSize, playTimeType, sortType) + var data struct { + Vmodel []*vidmod.VideoModel + Total int64 + HasNext bool + } + //尝试从redis读取 + if strP, err1 := appg.Redis.Get(redisKey); strP == nil { //不成功从数据库读取 + data.Vmodel, data.Total, data.HasNext, err = vidmod.GetHisWorkList(oid, skip, limit, &status, sortType, playTimeType) + if err != nil { + return + } + if err1 == nil { + go func() { //写入缓存 + b, err := json.Marshal(data) + if err != nil { + return + } + err = appg.Redis.Set(redisKey, string(b), redisconst.UserCollectionExpire()) + if err != nil { + fmt.Println(err.Error()) + + } + }() + } + } else { + if err = json.Unmarshal([]byte(*strP), &data); err != nil { + return + } + } + total = data.Total + hasNext = data.HasNext + vList = vidhelpser.NewEncodeVideoInfo(uid, data.Vmodel) + return +} + +// FaqList 常见问题列表 +func FaqList(cate string, s commod.StdQuery) (data faqmod.FaqListRes, err error) { + result, total, err := faqmod.FaqList(cate, true, s) + if err != nil { + log.Warn("[METHOD]==>FaqList error", log.E(err)) + return + } + data.Total = total + data.List = result + return +} + +type DLInfo struct { + AppID int `json:"appID"` + UID uint64 `json:"uid"` +} + +func (this *DLInfo) Dec(data string) error { + b, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return err + } + return json.Unmarshal(b, this) +} + +func (this *DLInfo) Enc() (string, error) { + b, err := json.Marshal(*this) + if err != nil { + return "", err + } + resStr := base64.StdEncoding.EncodeToString(b) + return resStr, nil +} + +func checkAccountPassword(ctx context.Context, user *usermod.User, account, pasword string) (code stderr.Code) { + if account == "" || user.UID != 0 { + return stderr.Success + } + u, err := usermod.FindUserByAccount(account) + if err != nil { + log.ErrorX(ctx, "find user by account when register failed", log.Any("account", account), log.E(err)) + return stderr.ErrDbQueryError + } + if u == nil { + code = stderr.ErrInvalidAccount + return + } + *user = *u + return stderr.Success +} + +func checkLoginEmailMobilePassword(ctx context.Context, user *usermod.User, mobile, email, captchaCode, pwd string) ( + code stderr.Code) { + if (mobile == "" && email == "") || user.UID != 0 { + return stderr.Success // if login without both email and mobile then try other ways, so dont't return err + } + if code = notiser.VerifyCaptchaCode(ctx, mobile, email, captchaCode); code != stderr.Success { + return + } + // if login with phone number, check whether the mobile has been registered and whether pasword matches + if mobile != "" { + u, err := usermod.FindUserByMobile(mobile) + if err != nil { + code = stderr.ErrDbQueryError + return + } + if u == nil { + code = stderr.ErrMobileInvalid + return + } + *user = *u + } + return +} + +func H5LoginByQRCode(ctx context.Context, content, ip string) (userInfo usermod.UserRes, code stderr.Code) { + uid, err := GetUIDFromQrCnt(content) + if err != nil { + log.ErrorX(ctx, "get uid from qr code failed", log.Any("content", content), log.E(err)) + code = stderr.ErrQrInvalid + return + } + u, err := usermod.FindUserByUID(uid) + if err != nil { + log.ErrorX(ctx, "find user by uid failed", log.Any("uid", uid), log.E(err)) + code = stderr.UserIsNotExists + return + } + if u == nil || u.UID == 0 { + log.WarnX(ctx, "user invalid") + code = stderr.UserIsNotExists + return + } + //切换账号 刷新token + token, err := authuser.GenToken(&authuser.Claims{UID: u.UID, Timestamp: time.Now().UnixNano(), Type: uint8(u.Type)}) + if err != nil { + code = stderr.ErrInterServerError + return + } + if err = lastLogin(uid, token, constant.SysTypeIOS); err != nil { + return + } + authuser.RevokeTokenCache(uid) + if userInfo, err = Info(uid); err != nil { + return + } + userInfo.Token = token + code = stderr.Success + + return +} + +// H5Login h5登陆 +func H5Login(ctx context.Context, ip string, p usermod.LoginWithH5, ua ua.UA) (userInfo usermod.UserRes, code stderr.Code, err error) { + code = stderr.Success + var u = &usermod.User{} + var uid uint64 + var token string + var isRegister bool + // check and find user by email of mobile if provided + if code = checkLoginEmailMobilePassword(ctx, u, p.Mobile, p.Email, p.Code, p.PassWord); code != stderr.Success { + return + } + // check and find user by account if provided + if code = checkAccountPassword(ctx, u, p.Account, p.PassWord); code != stderr.Success { + return + } + // if login without email and mobile, then check device id + if u.UID == 0 { + user, err := usermod.FindUserByDevID(p.DevID) + if err != nil { + code = stderr.ErrServerUnavailable + return userInfo, code, err + } + if user != nil { + u = user + } + } + // if it's a new user, then check info in clipboard + if u.UID == 0 { + // 单 IP 注册总量闸:先于建号判定。24h 滚动窗口内同一真实 IP 注册数超额直接拒绝并拉黑, + // 堵住"卡着 IPAutoBlock 的 5 分钟速率闸上限、跨窗口累计出成百上千个号"——速率闸过期即清零,管不住总量。 + if code = checkRegisterQuota(ip); code != stderr.Success { + return + } + dc, pc, puc, tid := parseCutInfo(p.CutInfos, ip, p.SysType) + log.Info("DeviceLogin Register info", log.Any("devLogin", p), log.Any("ip", ip)) + devType := "h5" + if p.IsAppStore { + devType = "ios" + } + uid, token, err = Register(dc, pc, puc, p.DevID, ip, p.SysType, "", devType, p.Mobile, "", ua.Terminal, ua, tid) + if err != nil { + return + } + isRegister = true + } else { // user already existed, then refersh its token + uid = u.UID + //切换账号 刷新token + token, _ = authuser.GenToken(&authuser.Claims{UID: u.UID, Timestamp: time.Now().UnixNano(), Type: uint8(u.Type)}) + if err = lastLogin(uid, token, p.SysType); err != nil { + return + } + authuser.RevokeTokenCache(uid) + } + if userInfo, err = Info(uid); err != nil { + return + } + userInfo.Token = token + userInfo.IsRegister = isRegister + + return +} + +// registerQuota* 为"单 IP 注册总量闸"参数:registerQuotaWindow 滚动窗口内,同一真实 IP 最多允许 +// registerQuotaLimit 次注册。区别于 IPAutoBlock 的"每 5 分钟速率闸"——后者窗口过期即清零,拦不住 +// 卡着速率上限、跨窗口长期累计的刷号;总量闸按较长窗口封顶,直接堵累计。上限偏严,大出口 NAT 若误伤再调。 +const ( + registerQuotaWindow = 24 * time.Hour + registerQuotaLimit = 10 +) + +// incrRegisterQuotaScript 原子地对总量闸计数 +1,并在计数首次达到 1 时设置窗口 TTL,返回自增后的计数。 +// 用 Lua 保证 INCR+EXPIRE 原子(避免"加了没设过期"的永久残留);Eval 会回传 Redis 错误,供调用方 fail-closed。 +const incrRegisterQuotaScript = `local c = redis.call('INCR', KEYS[1]) +if c == 1 then + redis.call('EXPIRE', KEYS[1], ARGV[1]) +end +return c` + +// checkRegisterQuota 单 IP 注册"总量闸":registerQuotaWindow(24h)内同一真实 IP 注册数超过 registerQuotaLimit +// 即拒绝并拉黑。与 IPAutoBlock(5 分钟速率闸)互补,专堵"几分钟内爆发 / 卡节奏长期累计"这类速率闸拦不住的刷号。 +// Redis 异常时 fail-closed(拒绝):注册是低频操作,宁可此刻挡下新注册,也不在 Redis 被打抖时放行刷号—— +// appg.Redis.Incr 封装出错会静默返回 0 相当于放行,这里改用 Eval 显式拿 error 兜住。 +func checkRegisterQuota(ip string) stderr.Code { + if !appg.ShouldEnforceIPRateLimit() { + return stderr.Success + } + if ip == "" || appg.Redis == nil { + return stderr.Success + } + key := fmt.Sprintf("register:quota:%s", ip) + res, err := appg.Redis.Eval(incrRegisterQuotaScript, []string{key}, int(registerQuotaWindow/time.Second)) + if err != nil { + log.Warn("register quota eval failed, fail-closed reject", log.Any("ip", ip), log.E(err)) + return stderr.ErrLoginTooFrequently + } + cnt, _ := res.(int64) + if cnt > registerQuotaLimit { + blockRegisterIP(ip, "H5注册24h总量超限自动拉黑") + return stderr.ErrLoginTooFrequently + } + return stderr.Success +} + +// checkRegisterFrequencyAndBlock 注册"速率闸":1 分钟内注册超过 10 次自动拉黑 IP,拦瞬时高频爆发。 +func checkRegisterFrequencyAndBlock(ip string) { + if !appg.ShouldEnforceIPRateLimit() { + return + } + if ip == "" { + return + } + key := fmt.Sprintf("register:freq:%s", ip) + cnt := appg.Redis.Incr(key) + if cnt == 1 { + _, _ = appg.Redis.ExpireKey(key, 1*time.Minute) + } + if cnt > 10 { + blockRegisterIP(ip, "H5注册频率过高自动拉黑") + } +} + +// blockRegisterIP 将 ip 加入注册黑名单:落库 + 刷 Redis 黑名单集合,使其后续请求被 ipblock.IPBlock 中间件 +// 在入口直接拦截(连 handler 都进不来)。供总量闸/频率闸命中时复用。 +func blockRegisterIP(ip, remark string) { + if ip == "" || appg.Redis == nil { + return + } + if err := ipblockmod.Insert(&ipblockmod.IPBlock{ + IP: ip, + Type: ipblockmod.BlockAct(constant.Register), + Remark: remark, + }); err != nil { + log.Error("insert register ip block failed", log.Any("ip", ip), log.E(err)) + } + // 刷新IP黑名单缓存 + blockKey := redisconst.IPBlockKey(constant.Register) + _, _ = appg.Redis.SAdd(blockKey, []interface{}{ip}) + log.Info("auto block register ip", log.Any("ip", ip), log.Any("remark", remark)) +} + +// ResetPassword 手机重置密码 +func ResetPassword(ctx context.Context, encrypted, password string) stderr.Code { + c, err := base64.StdEncoding.DecodeString(encrypted) + if err != nil { + return stderr.Failure + } + baseBytes, err := crypt.AesDecrypt(string(c), appg.Conf.Kefu.AppSecret) + if err != nil { + return stderr.Failure + } + req := usermod.ResetPasswordRequest{} + if err = json.Unmarshal([]byte(baseBytes), &req); err != nil || req.Mobile == "" { + return stderr.Failure + } + return resetPasswordByMobile(ctx, req, password) +} + +func resetPasswordByMobile(ctx context.Context, req usermod.ResetPasswordRequest, password string) stderr.Code { + //手机号是否绑定 + user, err := usermod.FindUserByMobile(req.Mobile) + if err != nil { + return stderr.Failure + } + if user == nil || (user.UID != req.UID) { + return stderr.UserIsNotExists + } + u, err := usermod.Update(user.UID, usermod.UserSelector{PassWord: &password}) + if err != nil || u == nil { + log.ErrorX(ctx, "reset pwd by mobile failed", log.Any("req", req), log.Any("pwd", password), log.E(err)) + return stderr.ErrNetWorkBusy + } + return stderr.Success +} + +// GenResetWorld 生成验证结果字符串 +func GenResetWorld(ctx context.Context, mobile, email string) (stderr.Code, string) { + var user *usermod.User + if mobile != "" { + //手机号是否绑定 + user, err := usermod.FindUserByMobile(mobile) + if err != nil { + return stderr.Failure, "" + } + if user == nil { + return stderr.UserIsNotExists, "" + } + } + mrp := usermod.ResetPasswordRequest{ + Mobile: mobile, + Email: email, + UID: user.UID, + TimeStamp: time.Now().Unix(), + RandNum: rand.Int63n(9999), + } + baseBytes, _ := json.Marshal(mrp) + sign, err := crypt.AesEncrypt(string(baseBytes), appg.Conf.Kefu.AppSecret) + if err != nil { + return stderr.Failure, "" + } + return stderr.Success, base64.StdEncoding.EncodeToString(sign) +} + +// MobileRegister 手机注册 +func MobileRegister(uid uint64, mobile, passWord, sys string) (*usermod.UserRes, stderr.Code) { + //手机号是否绑定 + user, err := usermod.FindUserByMobile(mobile) + if err != nil { + return nil, stderr.Failure + } + if user != nil { + if user.UID == uid { + return nil, stderr.ErrMobileHasBind + } + return nil, stderr.ErrMobileHasBindByOther + } + user, err = usermod.FindUserByUID(uid) + if err != nil { + return nil, stderr.Failure + } + if user.MobileBindAt != nil { + return nil, stderr.ErrInvalidRequest + } + if err = updateUserMobile(uid, mobile, passWord); err != nil { + log.Warn("updateUserMobile error", log.E(err)) + return nil, stderr.ErrNetWorkBusy + } + return refreshToken(uid, sys, uint8(user.Type)) +} + +// RegisterH5 H5注册.手机号、邮箱、账号 +func RegisterH5(ctx context.Context, uid uint64, sys string, req RegisterRequestH5) (*usermod.UserRes, stderr.Code) { + // if code := notiser.VerifyCaptchaCode(ctx, req.Mobile, req.Email, req.Code); code != stderr.Success { + // return nil, code + // } + // if req.Mobile = strings.TrimSpace(req.Mobile); req.Mobile != "" { + // return MobileRegister(uid, req.Mobile, "", sys) + // } + if req.Account = strings.TrimSpace(req.Account); req.Account != "" { + return accountRegister(ctx, uid, req.Account, "", sys) + } + return nil, stderr.ErrInvalidRequest +} + +func accountRegister(ctx context.Context, uid uint64, account, password, sys string) (*usermod.UserRes, stderr.Code) { + // if n, err := filterser.CheckIllegalWords(account); n > 0 || err != nil { + // if err != nil { + // log.ErrorX(ctx, "check illegal account failed", log.Any("account", account), log.E(err)) + // } + // return nil, stderr.ErrAccountHasIllegalContent + // } + user, err := usermod.FindUserByAccount(account) + if err != nil { + return nil, stderr.Failure + } + if user != nil { + if user.UID == uid { + return nil, stderr.ErrAccountHasBinded + } + return nil, stderr.ErrAccountBindedByOther + } + user, err = usermod.FindUserByUID(uid) + if err != nil { + return nil, stderr.Failure + } + if user.AccountBindAt != nil { + return nil, stderr.ErrAccountHasBinded + } + newUid, err := idmod.GetNextUID() //获取新UID + if err != nil { + return nil, stderr.ErrNetWorkBusy + } + if err = updateUserAccount(uid, newUid, account, password); err != nil { + log.ErrorX(ctx, "accountRegister failed", log.Any("uid", uid), log.Any("account", account), + log.Any("passWord", password), log.E(err)) + return nil, stderr.ErrNetWorkBusy + } + return refreshToken(newUid, sys, uint8(user.Type)) +} + +func refreshToken(uid uint64, sys string, userType uint8) (*usermod.UserRes, stderr.Code) { + //切换账号 刷新token + token, _ := authuser.GenToken(&authuser.Claims{UID: uid, Timestamp: time.Now().UnixNano(), Type: uint8(userType)}) + if err := lastLogin(uid, token, sys); err != nil { + return nil, stderr.ErrNetWorkBusy + } + common.Go(func() { + authuser.RevokeTokenCache(uid) + }) + u, err := Info(uid) + if err != nil { + return nil, stderr.Success + } + u.Token = token + return &u, stderr.Success +} + +// HandleUserAwardsExpire 处理用户奖章是否过期 +func HandleUserAwardsExpire(u *usermod.User) (awardsExpire []usermod.AwardsExpireResp) { + var numberIds []int + for _, i := range u.AwardsExpire { + awardExpire := usermod.AwardsExpireResp{ + Number: i.Number, + IsExpire: false, + } + if i.AwardExpire.After(time.Now()) { + awardExpire.IsExpire = true + } + if !sli.ContainsInt(numberIds, i.Number) { + numberIds = append(numberIds, i.Number) + awardsExpire = append(awardsExpire, awardExpire) + continue + } + if len(awardsExpire) > 0 { + for k, v := range awardsExpire { + if v.Number == i.Number && awardExpire.IsExpire { + awardsExpire[k].IsExpire = awardExpire.IsExpire + } + } + } + } + return awardsExpire +} + +type ( + PublishWorkReq struct { + commod.Page + Uid *uint64 `form:"uid" binding:"-"` // (获取他人的)用户ID,存在即获取这个用户的作品 + NewsType *string `form:"newsType" binding:"-"` // (获取他人的)帖子类型 + Status *int `form:"status" binding:"omitempty,oneof=0 1 2"` // (获取自己的)状态: 0:待审核 1:已发布 2:未通过 + SortType *int `form:"sortType" binding:"omitempty,oneof=0 1 2"` // 1-最新 2-最多点赞 + } + PublishWorkRep struct { + List []*vidmod.VideoInfoResp `json:"list"` // 视频列表 + HasNext bool `json:"hasNext"` // 是否还有下一页 + } +) + +// GetUserPublishWorkList 获取用户发布的作品 +func (req *PublishWorkReq) GetUserPublishWorkList(uid uint64) (rep PublishWorkRep, err error) { + var ( + cond bson.M + sort bson.D + opt = options.Find().SetSkip(req.Skip64()).SetLimit(req.Limit64()).SetProjection(bson.M{"richText": 0}) + ) + if req.Uid != nil && *req.Uid > 0 { + // 查询他人 + if req.NewsType == nil { + nt := vidmod.SP + req.NewsType = &nt + } + + cond = bson.M{ + "publisherID": *req.Uid, + "newsType": req.NewsType, + "status": vidmod.CheckPass, + } + sort = bson.D{{Key: "reviewAt", Value: -1}} + } else { + // 查询自己 + if req.Status == nil { + status := vidmod.CheckPass + req.Status = &status + } + + cond = bson.M{ + "publisherID": uid, + "newsType": bson.M{"$in": []string{vidmod.SP, vidmod.SHORT, vidmod.PIC, vidmod.COVER}}, + "status": req.Status, + } + sort = bson.D{{Key: "_id", Value: -1}} + } + if req.SortType != nil && *req.SortType == 1 { + sort = bson.D{{Key: "reviewAt", Value: -1}} + } else if req.SortType != nil && *req.SortType == 2 { + sort = bson.D{{Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}} + } + vModels, hasNext, err := vidmod.FindList(cond, opt.SetSort(sort)) + + vList := vidhelpser.EncodeVideoInfoBlogger(uid, vModels) + rep.List = vList + rep.HasNext = hasNext + + return rep, err +} + +type ( + PublishNumReq struct { + } + PublishNumRep struct { + PassNUm int64 `json:"passNUm"` // 审核通过的数量 + PendingReview int64 `json:"pendingReview"` // 待审核的数量 + } +) + +// GetUserPublishNum 获取用户发布的作品数量 +func (req *PublishNumReq) GetUserPublishNum(uid uint64) (rep PublishNumRep, err error) { + cond := bson.M{ + "publisherID": uid, + "newsType": bson.M{"$in": []string{vidmod.SP, vidmod.SHORT, vidmod.PIC, vidmod.COVER}}, + "status": vidmod.WaitingCheck, + } + + rep.PendingReview, err = vidmod.CountNum(cond) + if err != nil { + return + } + cond = bson.M{ + "publisherID": uid, + "newsType": bson.M{"$in": []string{vidmod.SP, vidmod.SHORT, vidmod.PIC, vidmod.COVER}}, + "status": vidmod.CheckPass, + } + + rep.PassNUm, err = vidmod.CountNum(cond) + if err != nil { + return + } + return rep, err +} + +type PublishWorkDeleteReq struct { + IDs []primitive.ObjectID `form:"ids"` //0:待审核 1:已发布 2:未通过 +} + +// UserPublishWorkDelete 删除我发布但未通过审核的作品[逻辑删除] +func (req *PublishWorkDeleteReq) UserPublishWorkDelete(uid uint64) (err error) { + cond := bson.M{ + "_id": bson.M{"$in": req.IDs}, + "publisherID": uid, + } + updater := bson.M{ + "$set": bson.M{"status": vidmod.IsDeleted}, + } + err = vidmod.UpdateMany(cond, updater) + return err +} diff --git a/app/service/userwatchrecordserver/userwatchrecord.go b/app/service/userwatchrecordserver/userwatchrecord.go new file mode 100644 index 0000000..5a6a8c5 --- /dev/null +++ b/app/service/userwatchrecordserver/userwatchrecord.go @@ -0,0 +1,34 @@ +package userwatchrecordserver + +import ( + "91porn-server/app/service/vidhelpser" + "91porn-server/common/stderr" + "91porn-server/models/v/userwatchrecordmod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func List(uid uint64, req *userwatchrecordmod.ListRequest) (interface{}, stderr.Code) { + list, err := userwatchrecordmod.List(req.Filter(uid), req.Options()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + listLen := len(list) + var out userwatchrecordmod.ListResponse + if listLen > int(req.PageSize) { + out.HasNext = true + list = list[:len(list)-1] + listLen-- + } + if listLen == 0 { + out.WorkList = []*vidmod.VideoInfoResp{} + return out, stderr.Success + } + videoIds := make([]primitive.ObjectID, listLen) + for i, record := range list { + videoIds[i] = record.VideoID + } + out.WorkList = vidhelpser.GetVideoListByIDsNoStatus(uid, videoIds) + return out, stderr.Success +} diff --git a/app/service/versionser/versionV2.go b/app/service/versionser/versionV2.go new file mode 100644 index 0000000..a6f0976 --- /dev/null +++ b/app/service/versionser/versionV2.go @@ -0,0 +1,172 @@ +package versionser + +import ( + "91porn-server/app/appg" + "91porn-server/common/httputil" + "91porn-server/common/localcache" + "91porn-server/common/log" + "91porn-server/models/commod" + "encoding/json" + "fmt" + "net/http" + "time" + + "golang.org/x/sync/singleflight" +) + +const ( + versionCacheTTL = time.Minute + versionFailureCacheTTL = 10 * time.Second + versionLastSuccessTTL = 24 * time.Hour +) + +var versionRequestGroup singleflight.Group + +type PacketType int32 + +const ( + PacketTypeEnterprise PacketType = 1 // 企业签 + PacketTypeShop PacketType = 2 // 商店包 + PacketTypeTestFlight PacketType = 3 // TF包 +) + +type AppAllReq struct { + AppId int32 `json:"appId" binding:"required"` // AppID + SysType string `json:"sysType,omitempty"` // 终端类型 + PktType PacketType `json:"pktType,omitempty"` // 包类型 + CurVersion string `json:"curVersion,omitempty"` // 当前版本 + CurPackageName string `json:"curPackageName,omitempty"` // 包名 +} + +// 返回参数 +type AppAllResp struct { + VersionInfo VersionInfo `json:"versionInfo"` // 版本信息 + AdvList []AdvertiseInfo `json:"advList"` // 广告信息 + AnnouList []AnnouInfo `json:"annouList"` // 公告信息 + IosUrl string `json:"iosLink"` // ios下载链接,网页版使用 + AndroidUrl string `json:"andLink"` // 安卓下载链接,网页版使用 + ShopIosLink string `json:"shopIosLink"` // ios商店包下载链接 +} + +// 版本信息 +type VersionInfo struct { + HasNewVersion bool `json:"hasNewVersion,omitempty"` // 是否存在新版本 + ServerVersion string `json:"serverVersion,omitempty"` // 服务器新版本 + DownloadLink []string `json:"downloadLink,omitempty"` // 下载地址 + Description string `json:"description,omitempty"` // 描述 + IsForceUpdate bool `json:"isForceUpdate,omitempty"` // 是否强制升级 + Size string `json:"size,omitempty"` // 大小 + Md5 string `json:"md5,omitempty"` // md5 +} + +// 广告信息 +type AdvertiseInfo struct { + Id int64 `json:"id"` // 广告id + Title string `json:"title"` // 广告标题 + CoverImg string `json:"coverImg"` // 封面 + RandomImgs []string `json:"randomImgs"` // 随机封面 + RandomNames []string `json:"randomNames"` // 随机名称 + LocId int32 `json:"locId"` // 位置id + JumpType int32 `json:"jumpType"` // 跳转方式 0:外部浏览器跳转 1:内部浏览器跳转 2:app内部跳转 + Link string `json:"link"` // 链接地址 + Sort int64 `json:"sort"` // 排序 + Duration int32 `json:"duration"` // 广告持续时间(弃用) + CoverImgSize string `json:"coverImgSize"` // 封面尺寸 + WatchTime int `json:"watchTime"` // 视频广告时长 +} + +// 公告信息 +type AnnouInfo struct { + //id + ID string `json:"id"` + //名字 + Title string `json:"title"` + //内容 + Content string `json:"content"` + //广告图 + Cover string `json:"cover"` + //链接地址 + Href string `json:"href"` + //类型 + Type int64 `json:"type"` + //时间 + Time string `json:"time"` +} + +// 广告、版本、公告,三合一接口 +func AdvVersionAnnounThreeServer(curVersion, sysType string) (ver VersionInfo, adv []AdvertiseInfo, annou []AnnouInfo, iosUrl, androidUrl, shopIosLink string, err error) { + //版本 + key := "VerAnnInfo:" + sysType + ":" + curVersion + if resp, ok := getVersionCache(key); ok { + ver, annou, iosUrl, androidUrl, shopIosLink = unpackAppAllResp(resp) + return + } + + value, requestErr, _ := versionRequestGroup.Do(key, func() (interface{}, error) { + // 同一版本的并发请求只允许一个访问产品中心;等待者进入后再检查一次缓存。 + if cached, ok := getVersionCache(key); ok { + return cached, nil + } + + resp, fetchErr := fetchAppAllResp(curVersion, sysType) + if fetchErr == nil { + localcache.C.Set(key, resp, versionCacheTTL) + localcache.C.Set(key+":lastSuccess", resp, versionLastSuccessTTL) + return resp, nil + } + + // 下游异常时短暂缓存最后一次成功结果(没有则为空结果),避免每个 Ping + // 都同步等待同一个失败请求;10 秒后自动重试,恢复后能快速拿到新数据。 + fallback, _ := getVersionCache(key + ":lastSuccess") + localcache.C.Set(key, fallback, versionFailureCacheTTL) + return fallback, fetchErr + }) + resp, ok := value.(AppAllResp) + if !ok { + return ver, adv, annou, iosUrl, androidUrl, shopIosLink, fmt.Errorf("invalid version cache type") + } + ver, annou, iosUrl, androidUrl, shopIosLink = unpackAppAllResp(resp) + err = requestErr + //log.Info("AdvVersionAnnounThreeServer response", log.Any("resp", resp)) + return +} + +func getVersionCache(key string) (AppAllResp, bool) { + value, ok := localcache.C.Get(key) + if !ok || value == nil { + return AppAllResp{}, false + } + resp, ok := value.(AppAllResp) + return resp, ok +} + +func unpackAppAllResp(resp AppAllResp) (VersionInfo, []AnnouInfo, string, string, string) { + return resp.VersionInfo, resp.AnnouList, resp.IosUrl, resp.AndroidUrl, resp.ShopIosLink +} + +func fetchAppAllResp(curVersion, sysType string) (AppAllResp, error) { + req := AppAllReq{ + AppId: commod.KFK_APPID, + PktType: PacketTypeEnterprise, + SysType: sysType, + CurVersion: curVersion, + } + bodyStr, err := json.Marshal(req) + if err != nil { + log.Info(fmt.Sprintf("AdvVersionAnnounThreeServer json.Marshal is fail error:%+v/data:%+v", err, req)) + return AppAllResp{}, err + } + + url := appg.Conf.URL.ProductUrl + "/api/stat/all/app" + resp := AppAllResp{} + code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr) + if err != nil { + log.Error("AdvVersionAnnounThreeServer POSTWithJResp ", log.Any("url", url), log.E(err)) + return AppAllResp{}, err + } + if code != http.StatusOK { + log.Error("AdvVersionAnnounThreeServer response status ", log.Any("code", code)) + return AppAllResp{}, fmt.Errorf("response status %d", code) + } + return resp, nil +} diff --git a/app/service/versionser/versionV2_test.go b/app/service/versionser/versionV2_test.go new file mode 100644 index 0000000..c71e186 --- /dev/null +++ b/app/service/versionser/versionV2_test.go @@ -0,0 +1,120 @@ +package versionser + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "sync" + "sync/atomic" + "testing" + "time" + + "91porn-server/app/appg" + "91porn-server/common/localcache" +) + +func TestAdvVersionAnnounThreeServerCoalescesConcurrentMisses(t *testing.T) { + var calls atomic.Int32 + var badRequest atomic.Bool + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls.Add(1) + if r.Method != http.MethodPost || r.URL.Path != "/api/stat/all/app" { + badRequest.Store(true) + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{}`)) + return + } + time.Sleep(50 * time.Millisecond) + _ = json.NewEncoder(w).Encode(AppAllResp{ + VersionInfo: VersionInfo{ServerVersion: "9.9.9"}, + AnnouList: []AnnouInfo{{ID: "announcement-1"}}, + IosUrl: "ios-url", + AndroidUrl: "android-url", + ShopIosLink: "shop-ios-url", + }) + })) + defer server.Close() + + setVersionTestConfig(t, server.URL) + version := "singleflight-test" + deleteVersionTestCache(t, "VerAnnInfo:"+version) + + const concurrency = 20 + start := make(chan struct{}) + errs := make(chan error, concurrency) + var wg sync.WaitGroup + for i := 0; i < concurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + <-start + ver, _, annou, iosURL, androidURL, shopURL, err := AdvVersionAnnounThreeServer(version, "android") + if err == nil && (ver.ServerVersion != "9.9.9" || len(annou) != 1 || iosURL != "ios-url" || androidURL != "android-url" || shopURL != "shop-ios-url") { + err = &unexpectedVersionResponseError{} + } + errs <- err + }() + } + close(start) + wg.Wait() + close(errs) + + for err := range errs { + if err != nil { + t.Fatalf("unexpected result: %v", err) + } + } + if got := calls.Load(); got != 1 { + t.Fatalf("downstream calls = %d, want 1", got) + } + if badRequest.Load() { + t.Fatal("downstream request used an unexpected method or path") + } +} + +func TestAdvVersionAnnounThreeServerCachesFailureFallback(t *testing.T) { + var calls atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + w.WriteHeader(http.StatusMethodNotAllowed) + _, _ = w.Write([]byte(`{}`)) + })) + defer server.Close() + + setVersionTestConfig(t, server.URL) + version := "failure-cache-test" + deleteVersionTestCache(t, "VerAnnInfo:"+version) + + if _, _, _, _, _, _, err := AdvVersionAnnounThreeServer(version, "android"); err == nil { + t.Fatal("first request error = nil, want downstream status error") + } + if _, _, _, _, _, _, err := AdvVersionAnnounThreeServer(version, "android"); err != nil { + t.Fatalf("cached fallback error = %v, want nil", err) + } + if got := calls.Load(); got != 1 { + t.Fatalf("downstream calls = %d, want 1", got) + } +} + +type unexpectedVersionResponseError struct{} + +func (*unexpectedVersionResponseError) Error() string { return "unexpected version response" } + +func setVersionTestConfig(t *testing.T, productURL string) { + t.Helper() + previous := appg.Conf + conf := &appg.GlobalConfig{} + conf.URL.ProductUrl = productURL + appg.Conf = conf + t.Cleanup(func() { appg.Conf = previous }) +} + +func deleteVersionTestCache(t *testing.T, key string) { + t.Helper() + localcache.C.Delete(key) + localcache.C.Delete(key + ":lastSuccess") + t.Cleanup(func() { + localcache.C.Delete(key) + localcache.C.Delete(key + ":lastSuccess") + }) +} diff --git a/app/service/versionser/versionser.go b/app/service/versionser/versionser.go new file mode 100644 index 0000000..af82327 --- /dev/null +++ b/app/service/versionser/versionser.go @@ -0,0 +1,190 @@ +package versionser + +import ( + "91porn-server/common" + "91porn-server/common/crypt" + "91porn-server/common/version" + "91porn-server/models/v/versionmod" +) + +func HandleVersionForList(ver, sysType string) (verlist []*versionmod.VersionBody) { + v1, err := version.New(ver) + if v1 == nil || err != nil { + return + } + sys := common.HandleSysType(sysType) + //首次查看当前版本有没有开启 提升,如果有指定的版本,则返回指定的版本,如果没有指定版本,则返回该平台最新的版本,如果没有打开则按照正常流程走 + vermod, err := versionmod.FindOneVersionByPlatVer(sys, ver) + if vermod != nil && err == nil { + if vermod.IsUpgrade { + if vermod.SpecVersion != "" { + newVersion, _ := versionmod.FindOneVersionByPlatVer(sys, vermod.SpecVersion) + if newVersion != nil { + verlist = append(verlist, &versionmod.VersionBody{ + Code: newVersion.Code, + VersionName: newVersion.VersionName, + Platform: newVersion.Platform, + Description: newVersion.Description, + ForcedUpdate: true, + URL: newVersion.URL}) + return + } else { + return + } + } else { + newVer, _ := versionmod.FindVersion(sys) + verlist = append(verlist, &versionmod.VersionBody{ + Code: newVer.Code, + VersionName: newVer.VersionName, + Platform: newVer.Platform, + Description: newVer.Description, + ForcedUpdate: true, + URL: newVer.URL}) + return + } + } + } + newVer, err := versionmod.FindVersion(sys) + if newVer.VersionName == "" || err != nil { + return + } + v2, _ := version.New(newVer.VersionName) + if v1.GTE(v2) { + return + } + if newVer.VersionRange.Major != "" { + major, _ := version.New(newVer.VersionRange.Major) + if v1.GT(major) { + return + } + } + if newVer.VersionRange.Minor != "" { + minor, _ := version.New(newVer.VersionRange.Minor) + if v1.LT(minor) { + return + } + } + verlist = append(verlist, &versionmod.VersionBody{ + Code: newVer.Code, + VersionName: newVer.VersionName, + Platform: newVer.Platform, + Description: newVer.Description, + ForcedUpdate: newVer.ForcedUpdate, + URL: newVer.URL}) + return +} + +// 当前版本是否在更新范围 +func HandleVersion(ver string, vermod *versionmod.Version) bool { + v1, err := version.New(ver) + if v1 == nil || err != nil { + return false + } + if vermod != nil { + if vermod.VersionRange.Major == "" && vermod.VersionRange.Minor == "" { + return true + } + if vermod.VersionRange.Major != "" { + major, _ := version.New(vermod.VersionRange.Major) + if v1.GT(major) { + return false + } + } + if vermod.VersionRange.Minor != "" { + minor, _ := version.New(vermod.VersionRange.Minor) + if v1.LT(minor) { + return false + } + } + return true + } + return false +} + +func CheckVersionBaseOnBuildId(ver, sysType, buildId string) (versionBody []*versionmod.VersionBody) { + v1, err := version.New(ver) + if v1 == nil || err != nil { + return + } + sys := common.HandleSysType(sysType) + //查看当前版本有没有开启 提升,如果有指定的版本,则返回指定的版本,如果没有指定版本,则返回该平台最新的版本,如果没有打开则按照正常流程走 + vermod, err := versionmod.FindOneVersionByPlatVerBuild(sys, ver, buildId) + if vermod != nil && err == nil { + if vermod.IsUpgrade { + if vermod.SpecVersion != "" { + newVersion, _ := versionmod.FindOneVersionByPlatVerBuild(sys, vermod.SpecVersion, buildId) + if newVersion != nil { + versionBody = append(versionBody, &versionmod.VersionBody{ + Code: newVersion.Code, + VersionName: newVersion.VersionName, + Platform: newVersion.Platform, + Description: newVersion.Description, + ForcedUpdate: true, + URL: newVersion.URL}) + return + } else { + return + } + } + } + } + //分为带buildId的包 和不带buildId的包 + ver1, err := versionmod.FindVersionBaseOnBuildIdAndPlatForm(buildId, sys) + if ver1 == nil || err != nil { + return + } + if ver1.Origin == versionmod.TF { + ver2, err := versionmod.FindVersionBaseOnBuildId(buildId) + if ver2 == nil || err != nil { + return + } + v2, _ := version.New(ver2.VersionName) + if v1.GTE(v2) { + return + } + versionBody = append(versionBody, &versionmod.VersionBody{ + Code: ver2.Code, + VersionName: ver2.VersionName, + Platform: ver2.Platform, + Description: ver2.Description, + ForcedUpdate: ver2.ForcedUpdate, + URL: GetTFUrl(ver2.URL)}) + } else { + versionBody = append(versionBody, &versionmod.VersionBody{ + Code: ver1.Code, + VersionName: ver1.VersionName, + Platform: ver1.Platform, + Description: ver1.Description, + ForcedUpdate: ver1.ForcedUpdate, + URL: ver1.URL}) + } + return +} + +func GetTFUrl(tf string) string { + if tf != "" { + type tfStc struct { + Url string `json:"url"` + Name string `json:"name"` + } + var tfArr []tfStc + _ = crypt.JSON2Struct(tf, &tfArr) + tfArrLen := len(tfArr) + if tfArrLen == 0 { + return tf + } + weight := 100 / tfArrLen + chioce := make([]common.Choice, tfArrLen) + for i := 0; i < len(tfArr); i++ { + chioce[i] = common.Choice{ + Weight: weight, + Item: tfArr[i].Url, + } + } + ch, _ := common.WeightedChoice(chioce) + if v, ok := ch.Item.(string); ok { + return v + } + } + return tf +} diff --git a/app/service/video_gold_coin_ser/video_gold_coin.go b/app/service/video_gold_coin_ser/video_gold_coin.go new file mode 100644 index 0000000..911f137 --- /dev/null +++ b/app/service/video_gold_coin_ser/video_gold_coin.go @@ -0,0 +1,63 @@ +package video_gold_coin_ser + +import ( + "91porn-server/models/commod" + "91porn-server/models/v/advanceordermod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/videogoldcoinmod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func GainVideoGoldCoin(uid uint64, in *videogoldcoinmod.QueryVideoGoldCoinCond) (data videogoldcoinmod.QueryVideoGoldCoinRes, err error) { + var resp videogoldcoinmod.QueryVideoGoldCoinRes + list, total, next, err := videogoldcoinmod.GetVideoList(in.Page, in.Limit, in.Filter(), in.Options()) + if err != nil { + return resp, err + } + resp.List = list + resp.Total = total + resp.HasNext = next + var ( + bStatus bool + advanceAmount, balanceAmount int64 + status int + ) + products, err := productmod.FindByProductType(commod.AdvanceCard) + if err != nil { + return resp, nil + } + + var pId primitive.ObjectID + if products != nil && len(products) > 0 { + for _, p := range products { + pId = p.ID + resp.ActivityDetail.ActivityTime = p.ActivityTime + resp.ActivityDetail.StartTime = p.StartTime + resp.ActivityDetail.EndTime = p.EndTime + advanceAmount = p.AdvanceAmount / 10 + balanceAmount = p.BalanceAmount / 10 + } + } + if pId.IsZero() { + return resp, nil + } + + advanceOrder, err := advanceordermod.IsExist(bson.M{"uid": uid, "productID": pId}) + if err != nil { + return resp, err + } + + if advanceOrder != nil && !advanceOrder.ID.IsZero() { + if advanceOrder.Status == advanceordermod.AdvanceSUCCESS || advanceOrder.Status == advanceordermod.BalanceProcessing { + bStatus = true + } + status = advanceOrder.Status + } + resp.ActivityDetail.BalancePayment = bStatus + resp.ActivityDetail.AdvanceAmount = advanceAmount + resp.ActivityDetail.BalanceAmount = balanceAmount + resp.ActivityDetail.Status = status + resp.ActivityDetail.TotalCount = 1000 + return resp, nil +} diff --git a/app/service/videoactivityserver/video_activity.go b/app/service/videoactivityserver/video_activity.go new file mode 100644 index 0000000..72ecf97 --- /dev/null +++ b/app/service/videoactivityserver/video_activity.go @@ -0,0 +1,76 @@ +package videoactivityserver + +import ( + "91porn-server/app/service/vidhelpser" + "91porn-server/common/stderr" + "91porn-server/models/v/videoactivitymod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func List(uid uint64, req *videoactivitymod.ListRequest) (interface{}, stderr.Code) { + var ( + out videoactivitymod.ListResponse + videoActivity videoactivitymod.VideoActivity + err error + ) + if req.ActivityID == "" { + videoActivity, err = videoactivitymod.QueryDocumentByStatus(videoactivitymod.Enable) + if err != nil { + return out, stderr.ErrDbQueryError + } + req.ActivityID = videoActivity.ID.Hex() + } else { + activityId, err := primitive.ObjectIDFromHex(req.ActivityID) + if err != nil { + return nil, stderr.ErrParamError + } + videoActivity, err = videoactivitymod.QueryDocumentByID(activityId) + if err != nil { + return nil, stderr.ErrDbQueryError + } + } + out.ActivityID = videoActivity.ID + out.ActivityBackgroundImage = videoActivity.BackgroundImage + out.ActivityDesc = videoActivity.Desc + out.ActivityEndTime = videoActivity.EndTime + filter, err := req.Filter() + if err != nil { + return nil, stderr.ErrParamError + } + list, err := vidmod.GetList(filter, req.Options()) + if err != nil { + return out, stderr.ErrDbQueryError + } + listLen := len(list) + if listLen == 0 { + out.WorkList = []*vidmod.VideoInfo{} + return out, stderr.Success + } + if listLen > int(req.PageSize) { + out.HasNext = true + list = list[:len(list)-1] + } + out.WorkList = vidhelpser.EncodeVideoInfo(uid, list) + return out, stderr.Success +} + +func HistoryRecord(req *videoactivitymod.HistoryRecordRequest) (interface{}, stderr.Code) { + var out videoactivitymod.HistoryRecordResponse + list, err := videoactivitymod.QueryAllDocument(req.Filter(), req.Options()) + if err != nil { + return out, stderr.ErrDbQueryError + } + listLen := len(list) + if listLen == 0 { + out.ActivityList = []*videoactivitymod.VideoActivity{} + return out, stderr.Success + } + if listLen > int(req.PageSize) { + out.HasNext = true + list = list[:len(list)-1] + } + out.ActivityList = list + return out, stderr.Success +} diff --git a/app/service/vidhelpser/free_trial.go b/app/service/vidhelpser/free_trial.go new file mode 100644 index 0000000..7e8e4fe --- /dev/null +++ b/app/service/vidhelpser/free_trial.go @@ -0,0 +1,120 @@ +package vidhelpser + +import ( + "time" + + "91porn-server/app/service/sys_config" + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/v/sysconfmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" +) + +// FreeTrialBadgeContext 是一次视频列表组装过程中复用的用户免费观看上下文。 +type FreeTrialBadgeContext struct { + uid uint64 + enabled bool + remaining uint64 + canShow bool + canUse bool +} + +// NewFreeTrialBadgeContext 构造免费观看上下文。 +// remaining 会按系统配置的总次数截断,避免历史异常数据透传给前端。 +func NewFreeTrialBadgeContext(uid, total, remaining uint64, isVIP, enabled bool) FreeTrialBadgeContext { + if total == 0 || uid == 0 { + remaining = 0 + } else if remaining > total { + remaining = total + } + canShow := uid > 0 && total > 0 && !isVIP + return FreeTrialBadgeContext{ + uid: uid, + enabled: enabled, + remaining: remaining, + canShow: canShow, + canUse: canShow && remaining > 0, + } +} + +// LoadFreeTrialBadgeContext 加载用户和后台角标开关。 +func LoadFreeTrialBadgeContext(uid uint64) FreeTrialBadgeContext { + if uid == 0 { + return FreeTrialBadgeContext{} + } + user, err := usermod.FindUserByUID(uid) + if err != nil || user == nil { + return FreeTrialBadgeContext{} + } + return FreeTrialBadgeContextForUser(uid, user) +} + +// FreeTrialBadgeContextForUser 使用已查询的用户构造上下文,避免列表组装重复查询用户。 +func FreeTrialBadgeContextForUser(uid uint64, user *usermod.User) FreeTrialBadgeContext { + if uid == 0 || user == nil { + return FreeTrialBadgeContext{} + } + enabled := false + if value, err := sysconfdata.GetBoolFromSharedCache(sysconfmod.VCodeFreeTrialBadgeEnabled); err == nil { + enabled = value + } + return NewFreeTrialBadgeContext( + uid, + sys_config.GetTotalWatchCount(), + user.WatchCount, + user.IsVIP(time.Now()), + enabled, + ) +} + +// Fields 返回当前视频的角标字段。 +func (c FreeTrialBadgeContext) Fields( + newsType string, + originCoins int64, + freeArea bool, + publisherID uint64, +) (show bool, remaining uint64, canUse bool) { + remaining = c.remaining + eligible := isVIPVideoForFreeTrial(newsType, originCoins, freeArea) && + publisherID != c.uid + canUse = c.canUse && eligible + show = c.enabled && canUse + return +} + +func isVIPVideoForFreeTrial(newsType string, originCoins int64, freeArea bool) bool { + if originCoins != 0 || freeArea { + return false + } + return newsType == vidmod.SP || newsType == vidmod.SHORT +} + +// ApplyFreeTrialBadgeToVideoInfos 设置标准视频列表/详情对象的角标字段。 +func ApplyFreeTrialBadgeToVideoInfos(ctx FreeTrialBadgeContext, videos []*vidmod.VideoInfo) { + for _, video := range videos { + if video == nil { + continue + } + video.ShowFreeTrialBadge, video.FreeTrialRemaining, video.CanUseFreeTrial = ctx.Fields( + video.NewsType, + video.OriginCoins, + video.FreeArea, + video.UInfo.UID, + ) + } +} + +// ApplyFreeTrialBadgeToVideoInfoResps 设置无状态视频列表对象的角标字段。 +func ApplyFreeTrialBadgeToVideoInfoResps(ctx FreeTrialBadgeContext, videos []*vidmod.VideoInfoResp) { + for _, video := range videos { + if video == nil { + continue + } + video.ShowFreeTrialBadge, video.FreeTrialRemaining, video.CanUseFreeTrial = ctx.Fields( + video.NewsType, + video.OriginCoins, + video.FreeArea, + video.UInfo.UID, + ) + } +} diff --git a/app/service/vidhelpser/free_trial_test.go b/app/service/vidhelpser/free_trial_test.go new file mode 100644 index 0000000..48baf81 --- /dev/null +++ b/app/service/vidhelpser/free_trial_test.go @@ -0,0 +1,134 @@ +package vidhelpser + +import ( + "testing" + + "91porn-server/models/v/vidmod" +) + +func TestFreeTrialBadgeContextFields(t *testing.T) { + const ( + uid = uint64(1001) + publisherID = uint64(2002) + ) + + tests := []struct { + name string + ctx FreeTrialBadgeContext + newsType string + originCoins int64 + freeArea bool + publisherID uint64 + wantShow bool + wantRemaining uint64 + wantCanUse bool + }{ + { + name: "eligible vip video", + ctx: NewFreeTrialBadgeContext(uid, 3, 2, false, true), + newsType: vidmod.SP, + publisherID: publisherID, + wantShow: true, + wantRemaining: 2, + wantCanUse: true, + }, + { + name: "switch only hides badge", + ctx: NewFreeTrialBadgeContext(uid, 3, 2, false, false), + newsType: vidmod.SHORT, + publisherID: publisherID, + wantRemaining: 2, + wantCanUse: true, + }, + { + name: "active vip does not use trial", + ctx: NewFreeTrialBadgeContext(uid, 3, 2, true, true), + newsType: vidmod.SP, + publisherID: publisherID, + wantRemaining: 2, + }, + { + name: "coin video", + ctx: NewFreeTrialBadgeContext(uid, 3, 2, false, true), + newsType: vidmod.SP, + originCoins: 10, + publisherID: publisherID, + wantRemaining: 2, + }, + { + name: "free area", + ctx: NewFreeTrialBadgeContext(uid, 3, 2, false, true), + newsType: vidmod.SP, + freeArea: true, + publisherID: publisherID, + wantRemaining: 2, + }, + { + name: "own video", + ctx: NewFreeTrialBadgeContext(uid, 3, 2, false, true), + newsType: vidmod.SP, + publisherID: uid, + wantRemaining: 2, + }, + { + name: "advertisement", + ctx: NewFreeTrialBadgeContext(uid, 3, 2, false, true), + newsType: vidmod.AD_SP, + publisherID: publisherID, + wantRemaining: 2, + }, + { + name: "remaining count is clamped", + ctx: NewFreeTrialBadgeContext(uid, 3, 99, false, true), + newsType: vidmod.SP, + publisherID: publisherID, + wantShow: true, + wantRemaining: 3, + wantCanUse: true, + }, + { + name: "no remaining count hides badge", + ctx: NewFreeTrialBadgeContext(uid, 3, 0, false, true), + newsType: vidmod.SP, + publisherID: publisherID, + wantRemaining: 0, + wantCanUse: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + show, remaining, canUse := tt.ctx.Fields(tt.newsType, tt.originCoins, tt.freeArea, tt.publisherID) + if show != tt.wantShow || remaining != tt.wantRemaining || canUse != tt.wantCanUse { + t.Fatalf( + "Fields() = (%v, %d, %v), want (%v, %d, %v)", + show, + remaining, + canUse, + tt.wantShow, + tt.wantRemaining, + tt.wantCanUse, + ) + } + }) + } +} + +func TestApplyFreeTrialBadgeToVideoInfos(t *testing.T) { + ctx := NewFreeTrialBadgeContext(1001, 3, 2, false, true) + video := &vidmod.VideoInfo{ + VideoBase: vidmod.VideoBase{ + NewsType: vidmod.SP, + OriginCoins: 0, + }, + UInfo: vidmod.Publisher{ + UInfo: vidmod.UInfo{UID: 2002}, + }, + } + + ApplyFreeTrialBadgeToVideoInfos(ctx, []*vidmod.VideoInfo{video}) + + if !video.ShowFreeTrialBadge || !video.CanUseFreeTrial || video.FreeTrialRemaining != 2 { + t.Fatalf("unexpected badge fields: %+v", video.VideoBase) + } +} diff --git a/app/service/vidhelpser/h265.go b/app/service/vidhelpser/h265.go new file mode 100644 index 0000000..dfac24a --- /dev/null +++ b/app/service/vidhelpser/h265.go @@ -0,0 +1,15 @@ +package vidhelpser + +import ( + "91porn-server/app/appg" + "91porn-server/models/v/vidmod" +) + +// H265URLForApp 根据 App 配置决定是否下发已经入库的 H265 播放地址。 +// enableApp 未配置时保持向后兼容;显式配置为 false 时可快速关闭 H265 下发。 +func H265URLForApp(video *vidmod.VideoModel) string { + if video == nil { + return "" + } + return appg.H265URLForApp(video.H265Url) +} diff --git a/app/service/vidhelpser/h265_test.go b/app/service/vidhelpser/h265_test.go new file mode 100644 index 0000000..315f041 --- /dev/null +++ b/app/service/vidhelpser/h265_test.go @@ -0,0 +1,119 @@ +package vidhelpser + +import ( + "encoding/json" + "testing" + + "91porn-server/app/appg" + "91porn-server/models/v/locmod" + "91porn-server/models/v/vidmod" +) + +func TestH265URLForApp(t *testing.T) { + oldConf := appg.Conf + t.Cleanup(func() { + appg.Conf = oldConf + }) + + video := &vidmod.VideoModel{H265Url: "https://cdn.example.com/h265/index.m3u8"} + if got := H265URLForApp(nil); got != "" { + t.Fatalf("nil video returned %q", got) + } + + appg.Conf = nil + if got := H265URLForApp(video); got != video.H265Url { + t.Fatalf("unconfigured switch returned %q", got) + } + + disabled := false + appg.Conf = &appg.GlobalConfig{} + appg.Conf.Hevc.EnableApp = &disabled + if got := H265URLForApp(video); got != "" { + t.Fatalf("disabled switch returned %q", got) + } + + enabled := true + appg.Conf.Hevc.EnableApp = &enabled + if got := H265URLForApp(video); got != video.H265Url { + t.Fatalf("enabled switch returned %q", got) + } +} + +func TestTransfer2InfoIncludesH265URL(t *testing.T) { + video := &vidmod.VideoModel{ + SourceURL: "https://cdn.example.com/h264/index.m3u8", + H265Url: "https://cdn.example.com/h265/index.m3u8", + } + + info := transfer2Info( + video, + vidmod.Publisher{}, + vidmod.VideoStatus{}, + locmod.Location{}, + nil, + vidmod.CommentInfo{}, + 0, + ) + + data, err := json.Marshal(info) + if err != nil { + t.Fatalf("marshal video info: %v", err) + } + + var response map[string]interface{} + if err := json.Unmarshal(data, &response); err != nil { + t.Fatalf("unmarshal video info response: %v", err) + } + if response["sourceURL"] != video.SourceURL { + t.Fatalf("unexpected H.264 URL: %v", response["sourceURL"]) + } + if response["h265Url"] != video.H265Url { + t.Fatalf("unexpected H.265 URL: %v", response["h265Url"]) + } +} + +func TestNewTransfer2InfoIncludesH265URL(t *testing.T) { + video := &vidmod.VideoModel{ + SourceURL: "https://cdn.example.com/h264/index.m3u8", + H265Url: "https://cdn.example.com/h265/index.m3u8", + } + + info := NewTransfer2Info( + video, + vidmod.Publisher{}, + vidmod.VideoStatus{}, + nil, + 0, + ) + + data, err := json.Marshal(info) + if err != nil { + t.Fatalf("marshal video info response: %v", err) + } + + var response map[string]interface{} + if err := json.Unmarshal(data, &response); err != nil { + t.Fatalf("unmarshal video info response: %v", err) + } + if response["sourceURL"] != video.SourceURL { + t.Fatalf("unexpected H.264 URL: %v", response["sourceURL"]) + } + if response["h265Url"] != video.H265Url { + t.Fatalf("unexpected H.265 URL: %v", response["h265Url"]) + } +} + +func TestToVideoBaseInfoIncludesH265URL(t *testing.T) { + video := &vidmod.VideoModel{ + SourceURL: "https://cdn.example.com/h264/index.m3u8", + H265Url: "https://cdn.example.com/h265/index.m3u8", + } + + info := ToVideoBaseInfo(video, nil, 0) + if info.SourceURL != video.SourceURL { + t.Fatalf("unexpected H.264 URL: %q", info.SourceURL) + } + if info.H265Url != video.H265Url { + t.Fatalf("unexpected H.265 URL: %q", info.H265Url) + } +} diff --git a/app/service/vidhelpser/helpser.go b/app/service/vidhelpser/helpser.go new file mode 100644 index 0000000..bee8ba0 --- /dev/null +++ b/app/service/vidhelpser/helpser.go @@ -0,0 +1,1418 @@ +package vidhelpser + +import ( + "91porn-server/app/service/searchaccessser" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/moduleconfmod" + "math" + "sync" + "time" + + "91porn-server/app/appg" + "91porn-server/app/service/walletser" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/maths" + "91porn-server/common/redis" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/l/payvidlgmod" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/v/collectmod" + "91porn-server/models/v/discount_area_mod" + "91porn-server/models/v/followmod" + "91porn-server/models/v/freeVidmod" + "91porn-server/models/v/likemod" + "91porn-server/models/v/locmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/videodiscountmod" + "91porn-server/models/v/vidmod" + "91porn-server/skd/skdg" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// OID 别名 +type OID = primitive.ObjectID + +const ( + //MaxNewestCacheNum 最新帖子最大缓存条数 + MaxNewestCacheNum = 500 + // 视频点赞数放大倍数 + VideoLikeCountMultiplier = 33 + // 视频播放量放大倍数 + VideoPlayCountMultiplier = 99 +) + +// GetLocationByIP 通过ip获取地理位置 +func GetLocationByIP(ip string) string { + r := getRedis() + city, err := r.GetString(redisconst.IpLocationFmt(ip)) + if city != "" && err == nil { + return city + } + city, _ = common.GetLocationByIP(ip) + _ = r.Set(redisconst.IpLocationFmt(ip), city, redisconst.IPLocationExpireMax) + return city +} + +func addUserInfo(srcMap map[uint64]*vidmod.Publisher, add []*usermod.BaseInfoVip) map[uint64]*vidmod.Publisher { + if srcMap == nil { + srcMap = make(map[uint64]*vidmod.Publisher) + } + for _, info := range add { + p := vidmod.Publisher{ + UInfo: *info, + } + srcMap[info.UID] = &p + } + return srcMap +} + +// GetUserInfo2Map 获取用户基本信息到map +func GetUserInfo2Map(uid uint64, uids []uint64) map[uint64]*vidmod.Publisher { + m, unExistsUID, err := getUsersBaseInfoFromRedis(uids) + if err != nil || len(unExistsUID) != 0 { + unExistsUsers, err := getUsersBaseInfoFromMongo(unExistsUID) + if err != nil { + return nil + } + common.Go(func() { + _ = setUsersBaseInfo2Redis(unExistsUsers) + }) + m = addUserInfo(m, unExistsUsers) + } + var wg sync.WaitGroup + var followMap map[uint64]bool + // var rchgLevelMap map[uint64]usermod.RechargeLevel + var userWorks []vidmod.WorkCount + wg.Add(2) + common.Go(func() { + defer wg.Done() + followMap, _ = followmod.IsFollowUsers(uid, uids) + }) + // common.Go(func() { + // defer wg.Done() + // rchgLevelMap, _ = walletser.GetUidsRchgLevel(uids) + // }) + common.Go(func() { + defer wg.Done() + var filter = bson.M{} + filter["publisherID"] = bson.M{"$in": uids} + filter["status"] = vidmod.CheckPass + userWorks, _ = vidmod.TotalWorks(filter) + }) + wg.Wait() + + for u, p := range m { + p.HasFollowed = followMap[u] + // p.RechargeLevel = rchgLevelMap[u].Level + for _, w := range userWorks { + if p.UID == w.Uid { + p.TotalWorks = w.Count + } + } + } + return m +} + +// GetUserInfo2MapNoUID 获取用户基本信息到map +func GetUserInfo2MapNoUID(uids []uint64) map[uint64]*vidmod.Publisher { + m, unExistsUID, err := getUsersBaseInfoFromRedis(uids) + if err != nil || len(unExistsUID) != 0 { + unExistsUsers, err := getUsersBaseInfoFromMongo(unExistsUID) + if err != nil { + return nil + } + common.Go(func() { + _ = setUsersBaseInfo2Redis(unExistsUsers) + }) + m = addUserInfo(m, unExistsUsers) + } + var rchgLevelMap map[uint64]usermod.RechargeLevel + rchgLevelMap, _ = walletser.GetUidsRchgLevel(uids) + for u, p := range m { + p.RechargeLevel = rchgLevelMap[u].Level + } + return m +} + +// GetSimpleUserMapNoUID 获取用户基本信息到map +func GetSimpleUserMapNoUID(uids []uint64) map[uint64]*vidmod.Publisher { + m, unExistsUID, err := getUsersBaseInfoFromRedis(uids) + if err != nil || len(unExistsUID) != 0 { + unExistsUsers, err := getUsersBaseInfoFromMongo(unExistsUID) + if err != nil { + return nil + } + common.Go(func() { + _ = setUsersBaseInfo2Redis(unExistsUsers) + }) + m = addUserInfo(m, unExistsUsers) + } + return m +} + +// GetUserStatus 获取 用户状态信息 +func GetUserStatus(uid uint64, videoIDs []OID) map[OID]vidmod.VideoStatus { + mLike := make(map[primitive.ObjectID]bool) + mPay := make(map[primitive.ObjectID]bool) + mCollect := make(map[primitive.ObjectID]bool) + var wg sync.WaitGroup + wg.Add(3) + common.Go(func() { + defer wg.Done() + mLike, _ = likemod.IsLikeVideos(uid, videoIDs) + }) + common.Go(func() { + defer wg.Done() + mPay, _ = payvidlgmod.IsPay4Videos(uid, videoIDs) + }) + common.Go(func() { + defer wg.Done() + mCollect, _ = collectmod.IsCollectVideos(uid, videoIDs) + }) + wg.Wait() + m := make(map[OID]vidmod.VideoStatus) + for _, v := range videoIDs { + info := vidmod.VideoStatus{ + HasLiked: mLike[v], + HasPaid: mPay[v], + HasCollected: mCollect[v], + TodayRank: 0, + TodayPlayCnt: 0, + } + m[v] = info + } + return m +} + +// GetTagsByIDs 通过tagid列表获取tag详情列表 +func GetTagsByIDs(ids []OID) ([]vidmod.TagInfo, bool) { + tags, unExistsTid, err := getTagsByIDsFromRedis(ids) + if err != nil || len(unExistsTid) != 0 { + unExistsTInfo, err := getTagsByIDsFromMongo(unExistsTid) + if err != nil { + return nil, false + } + common.Go(func() { + _ = setTagsByIDs2Redis(unExistsTInfo) + }) + tags = append(tags, unExistsTInfo...) + } + return tags, true +} + +// GetTagsByIDs2Map 通过tagid列表获取tag详情列表 +func GetTagsByIDs2Map(ids []OID) map[primitive.ObjectID]vidmod.TagInfo { + m := make(map[primitive.ObjectID]vidmod.TagInfo) + tags, ok := GetTagsByIDs(ids) + if !ok { + return m + } + for _, info := range tags { + m[info.ID] = vidmod.TagInfo{ + ID: info.ID, + Name: info.Name, + CoverImg: info.CoverImg, + Description: info.Description, + } + } + return m +} + +// GetPartTags 从内存获取tagInfo +func GetPartTags(ids []OID, mTags map[primitive.ObjectID]vidmod.TagInfo) []vidmod.TagInfo { + infos := make([]vidmod.TagInfo, 0, len(ids)) + for _, i := range ids { + if !mTags[i].ID.IsZero() { + infos = append(infos, mTags[i]) + } + } + return infos +} + +func getVideoParentCommentCountMap(objIDs []primitive.ObjectID) map[primitive.ObjectID]int { + res := make(map[primitive.ObjectID]int, len(objIDs)) + countMap, err := cmtmod.CmtFindParentCountByObjIDs(cmtmod.CmtTypeVideo, objIDs) + if err != nil { + return res + } + for id, cnt := range countMap { + res[id] = int(cnt) + } + return res +} + +// Transfer2Info 将视频数据库模式转为返回模式 +func transfer2Info(v *vidmod.VideoModel, uInfo vidmod.Publisher, vStatus vidmod.VideoStatus, loc locmod.Location, ti []vidmod.TagInfo, com vidmod.CommentInfo, originCoins int64) *vidmod.VideoInfo { + if v.Status == 3 { + v.Status = 1 + } + newType := vidmod.SP + if len(v.NewsType) != 0 { + newType = v.NewsType + } + base := vidmod.VideoBase{ + ID: v.ID, + NewsType: newType, + Title: v.Title, + Content: v.Content, + Tags: ti, + PlayTime: v.PlayTime, + Cover: v.Cover, + CoverThumb: v.CoverThumb, + SeriesCover: v.SeriesCover, + PlayCount: (v.PlayCount + v.FakePlayCount) * VideoPlayCountMultiplier, + LikeCount: v.LikeCount, // 移除点赞数放大倍数,修复各处点赞数不一致问题 + PageViewCount: v.PageViewCount * 3, + CommentCount: v.CommentCount, + ShareCount: v.FakeShareCount, + Coins: v.Coins, + Size: v.Size, + Resolution: v.Resolution, + Ratio: v.Ratio, + PurchaseCount: v.PurchaseCount, + CreatedAt: v.CreatedAt, + ReviewAt: v.ReviewAt, + FreeTime: v.FreeTime, + Status: v.Status, + Reason: v.Reason, + SourceURL: v.SourceURL, + H265Url: H265URLForApp(v), + PreviewURL: v.PreviewURL, + LinkUrl: v.LinkUrl, + IsHideLocation: v.IsHideLocation, + FreeArea: v.FreeArea, + IsTopping: v.IsTopping, + IsChoosen: v.IsChoosen, + IsRecommend: v.IsRecommend, + Rewarded: v.Rewarded.Add(v.FakeRewarded), + OriginCoins: originCoins, + CollectCount: v.CollectCount, + SeedLinkUrl: v.SeedLinkUrl, + SeedSize: v.SeedSize, + SeedPlayTime: v.SeedPlayTime, + RichText: v.RichText, + PreviewStart: v.PreviewStart, + TimeNodeList: v.TimeNodeList, + DownloadAllow: v.DownloadAllow, + ShowType: v.ShowType, + } + if v.Via == "laosiji" { + base.LsjId = v.SourceID + } + locInfo := vidmod.LocInfo{ + ID: loc.ID, + City: loc.City, + Cover: loc.Cover, + Visit: loc.FakeVisit, + CreatedAt: loc.CreatedAt, + } + info := vidmod.VideoInfo{ + VideoBase: base, + UInfo: uInfo, + Location: locInfo, + VidStatus: vStatus, + Comment: com, + } + return &info +} + +// EncodeVideoInfoBlogger 视频上传博主查看自己上传视频(临时解决) +func EncodeVideoInfoBlogger(uid uint64, infos []*vidmod.VideoModel) []*vidmod.VideoInfoResp { + infos = filterOutsideSearch(infos) + infosLen := len(infos) + back := make([]*vidmod.VideoInfoResp, infosLen) + if infosLen == 0 { + return back + } + uids := make([]uint64, infosLen) + videos := make([]primitive.ObjectID, infosLen) + lids := make([]primitive.ObjectID, infosLen) + var tags []primitive.ObjectID + + for i, v := range infos { + uids[i] = v.PublisherID + videos[i] = v.ID + lids[i] = v.Location + tags = append(tags, v.Tags...) + } + var ( + mUser map[uint64]*vidmod.Publisher + mStatus map[OID]vidmod.VideoStatus + mTags map[primitive.ObjectID]vidmod.TagInfo + wg sync.WaitGroup + ) + + wg.Add(3) + common.Go(func() { + defer wg.Done() + mUser = GetUserInfo2Map(uid, uids) + }) + common.Go(func() { + defer wg.Done() + mStatus = GetUserStatus(uid, videos) + }) + common.Go(func() { + defer wg.Done() + mTags = GetTagsByIDs2Map(tags) + }) + wg.Wait() + commentCountMap := getVideoParentCommentCountMap(videos) + for i, v := range infos { + originCoins := v.Coins + var uInfo vidmod.Publisher + if mUser[v.PublisherID] != nil { + uInfo = *mUser[v.PublisherID] + } + back[i] = NewTransfer2Info(v, uInfo, mStatus[v.ID], GetPartTags(v.Tags, mTags), originCoins) + back[i].CommentCount = commentCountMap[v.ID] + if v.Status != vidmod.CheckPass && v.PublisherID == uid { + back[i].PageViewCount = 0 + } + } + return back +} + +// EncodeVideoInfo 封装返回的视频列表信息 +func EncodeVideoInfo(uid uint64, infos []*vidmod.VideoModel) []*vidmod.VideoInfo { + return encodeVideoInfo(uid, filterOutsideSearch(infos)) +} + +// EncodeVideoInfoForRecommend 过滤配置为不进入推荐场景的亚模块内容。 +func EncodeVideoInfoForRecommend(uid uint64, infos []*vidmod.VideoModel) []*vidmod.VideoInfo { + return encodeVideoInfo(uid, filterRecommend(infos)) +} + +// EncodeVideoInfoForSearch 保留搜索专属内容,并给结果签发短时效访问凭证。 +func EncodeVideoInfoForSearch(uid uint64, infos []*vidmod.VideoModel) []*vidmod.VideoInfo { + result := encodeVideoInfo(uid, filterSearch(infos)) + attachVideoInfoSearchTokens(uid, result) + return result +} + +func encodeVideoInfo(uid uint64, infos []*vidmod.VideoModel) []*vidmod.VideoInfo { + if uid == 0 { + return encodeVideoInfoNoUID(infos) + } + infosLen := len(infos) + //now := time.Now() + back := make([]*vidmod.VideoInfo, infosLen) + if infosLen == 0 { + return back + } + uids := make([]uint64, infosLen) + videos := make([]primitive.ObjectID, infosLen) + lids := make([]primitive.ObjectID, infosLen) + discountAreaIds := []primitive.ObjectID{} + var tags []primitive.ObjectID + mids := []primitive.ObjectID{} + for i, v := range infos { + uids[i] = v.PublisherID + videos[i] = v.ID + lids[i] = v.Location + if !v.DiscountAreaId.IsZero() { + discountAreaIds = append(discountAreaIds, v.DiscountAreaId) + } + tags = append(tags, v.Tags...) + mid, _ := primitive.ObjectIDFromHex(v.MID) + if !mid.IsZero() { + mids = append(mids, mid) + } + + } + var mUser map[uint64]*vidmod.Publisher + var mStatus map[OID]vidmod.VideoStatus + var mLocation map[primitive.ObjectID]locmod.Location + var mComment map[primitive.ObjectID]vidmod.CommentInfo + var mTags map[primitive.ObjectID]vidmod.TagInfo + var mFree map[uint64]bool + discountAreaMap := make(map[primitive.ObjectID]*discount_area_mod.DiscountArea) + var self *usermod.User + var videoDiscLog videodiscountmod.VideoDiscountLog + var largestDiscount int + moduleMap := make(map[string]moduleconfmod.ModuleConf) + var wg sync.WaitGroup + wg.Add(9) + common.Go(func() { + defer wg.Done() + moduleList, err := moduleconfmod.FindByIDs(mids) + if err != nil { + log.Error("EncodeVideoInfo: moduleconfmod.FindByIDs fail", log.E(err)) + return + } + for _, v := range moduleList { + moduleMap[v.ID.Hex()] = v + } + }) + common.Go(func() { + defer wg.Done() + // 通过折扣专区id获取专区 + vidDiscountArea, err := discount_area_mod.GetDiscountAreaByIds(discountAreaIds) + if err != nil { + log.Error("EncodeVideoInfo: discount_area_mod.GetDiscountAreaByIds", log.Any("discountAreaIds", discountAreaIds), log.E(err)) + return + } + for _, v := range vidDiscountArea { + discountAreaMap[v.ID] = &v + } + }) + common.Go(func() { + defer wg.Done() + user, findErr := usermod.FindUserByUID(uid) + if findErr != nil { + log.Error("EncodeVideoInfo: usermod.FindUserByUID", log.Any("uid", uid), log.E(findErr)) + return + } + self = user + //if self != nil && !self.IsVIP(now) { + // largestDiscount, err = productmod.GetLargestDiscountVIPCard() + // if err != nil { + // log.Error("GetLargestDiscountVIPCard", log.Any("uid", uid), log.E(err)) + // } + //} + }) + common.Go(func() { + defer wg.Done() + mUser = GetUserInfo2Map(uid, uids) + }) + common.Go(func() { + defer wg.Done() + mStatus = GetUserStatus(uid, videos) + }) + common.Go(func() { + defer wg.Done() + //mLocation, _ = locmod.GetLocationInfoByIDs(lids) + mLocation = make(map[primitive.ObjectID]locmod.Location) + }) + common.Go(func() { + defer wg.Done() + mTags = GetTagsByIDs2Map(tags) + }) + common.Go(func() { + defer wg.Done() + mFree = freeVidmod.GetFreeVideoMapByUID(uid) + }) + common.Go(func() { + defer wg.Done() + discountLog, findErr := videodiscountmod.GetByUID(uid) + if findErr != nil { + log.Error("videodiscountmod.GetByUID", log.Any("uid", uid), log.E(findErr)) + return + } + videoDiscLog = discountLog + }) + wg.Wait() + freeTrialContext := FreeTrialBadgeContextForUser(uid, self) + commentCountMap := getVideoParentCommentCountMap(videos) + for i, v := range infos { + var uInfo vidmod.Publisher + if mUser[v.PublisherID] != nil { + uInfo = *mUser[v.PublisherID] + } else { + //处理作者被删除的情况 + if len(mUser) > 0 { + for i := range mUser { + uInfo = *mUser[i] + break + } + } + } + // 金币帖子 - 图片帖子对所有人原价 + // 非图片帖子再按规则计算折扣价 + originCoins := v.Coins + infos[i].Coins = originCoins + if v.NewsType != vidmod.COVER { + infos[i].Coins = CalcVideoCoins(v.Coins, v.PublisherID, self, mFree, videoDiscLog, largestDiscount, discountAreaMap[v.DiscountAreaId]) + } + info := transfer2Info(v, uInfo, mStatus[v.ID], mLocation[v.Location], GetPartTags(v.Tags, mTags), mComment[v.ID], originCoins) + if v.NewsType != vidmod.COVER { + // 计算折扣专区价格 + info.DiscountAreaPrice = CalcDiscountAreaPrice(originCoins, discountAreaMap[v.DiscountAreaId]) + } + info.ShowFreeTrialBadge, info.FreeTrialRemaining, info.CanUseFreeTrial = freeTrialContext.Fields( + info.NewsType, + originCoins, + v.FreeArea, + v.PublisherID, + ) + info.HappinessPlazaTop = infos[i].HappinessPlazaTop + back[i] = info + module, ok := moduleMap[v.MID] + if ok { + info.VideoTypeId = module.ID.Hex() + info.VideoTypeName = module.ModuleName + } + // 与 comment/list 口径一致:不统计置顶公共评论(该评论不在评论表内)。 + back[i].CommentCount = commentCountMap[info.ID] + } + common.Go(func() { + incVideoPageView(videos) + }) + return back +} + +// NewEncodeVideoInfoNotStatus 封装返回的视频列表信息 +func NewEncodeVideoInfoNotStatus(infos []*vidmod.VideoModel) []*vidmod.VideoInfoResp { + return newEncodeVideoInfoNotStatus(filterOutsideSearch(infos)) +} + +// NewEncodeVideoInfoNotStatusForUser 封装需要用户免费观看状态的无状态视频列表。 +func NewEncodeVideoInfoNotStatusForUser(uid uint64, infos []*vidmod.VideoModel) []*vidmod.VideoInfoResp { + result := newEncodeVideoInfoNotStatus(filterOutsideSearch(infos)) + ApplyFreeTrialBadgeToVideoInfoResps(LoadFreeTrialBadgeContext(uid), result) + return result +} + +// NewEncodeVideoInfoNotStatusForRecommend 过滤配置为不进入推荐场景的亚模块内容。 +func NewEncodeVideoInfoNotStatusForRecommend(infos []*vidmod.VideoModel) []*vidmod.VideoInfoResp { + return newEncodeVideoInfoNotStatus(filterRecommend(infos)) +} + +// NewEncodeVideoInfoNotStatusForSearch 保留搜索专属内容,并给结果签发短时效访问凭证。 +func NewEncodeVideoInfoNotStatusForSearch(uid uint64, infos []*vidmod.VideoModel) []*vidmod.VideoInfoResp { + result := newEncodeVideoInfoNotStatus(filterSearch(infos)) + ApplyFreeTrialBadgeToVideoInfoResps(LoadFreeTrialBadgeContext(uid), result) + attachVideoInfoRespSearchTokens(uid, result) + return result +} + +func newEncodeVideoInfoNotStatus(infos []*vidmod.VideoModel) []*vidmod.VideoInfoResp { + if len(infos) == 0 { + return nil + } + var ( + back []*vidmod.VideoInfoResp + uids []uint64 + videos []primitive.ObjectID + lids []primitive.ObjectID + tags []primitive.ObjectID + + mUser = map[uint64]*vidmod.Publisher{} + mTags = map[primitive.ObjectID]vidmod.TagInfo{} + mStatus = map[OID]vidmod.VideoStatus{} + ) + for _, i := range infos { + uids = append(uids, i.PublisherID) + videos = append(videos, i.ID) + lids = append(lids, i.Location) + tags = append(tags, i.Tags...) + } + + var wg sync.WaitGroup + wg.Add(2) + common.Go(func() { + defer wg.Done() + mUser = GetUserInfo2Map(0, uids) + }) + + common.Go(func() { + defer wg.Done() + mTags = GetTagsByIDs2Map(tags) + }) + + wg.Wait() + commentCountMap := getVideoParentCommentCountMap(videos) + for i, v := range infos { + temp := v + originCoins := temp.Coins + var uInfo vidmod.Publisher + if mUser[temp.PublisherID] != nil { + uInfo = *mUser[temp.PublisherID] + } else { + //处理作者被删除的情况 + if len(mUser) > 0 { + for i := range mUser { + uInfo = *mUser[i] + break + } + } + } + infos[i].Coins = originCoins + info := NewTransfer2Info(temp, uInfo, mStatus[temp.ID], GetPartTags(temp.Tags, mTags), originCoins) + info.CommentCount = commentCountMap[temp.ID] + back = append(back, info) + } + common.Go(func() { + incVideoPageView(videos) + }) + return back +} + +// NewEncodeVideoInfo 封装返回的视频列表信息 +func NewEncodeVideoInfo(uid uint64, infos []*vidmod.VideoModel) []*vidmod.VideoInfoResp { + return newEncodeVideoInfo(uid, filterOutsideSearch(infos)) +} + +func newEncodeVideoInfo(uid uint64, infos []*vidmod.VideoModel) []*vidmod.VideoInfoResp { + var back []*vidmod.VideoInfoResp + if uid == 0 { + return back + } + if len(infos) == 0 { + return back + } + var uids []uint64 + var videos []primitive.ObjectID + var lids []primitive.ObjectID + var tags []primitive.ObjectID + for _, i := range infos { + uids = append(uids, i.PublisherID) + videos = append(videos, i.ID) + lids = append(lids, i.Location) + tags = append(tags, i.Tags...) + + } + var ( + mUser map[uint64]*vidmod.Publisher + mStatus map[OID]vidmod.VideoStatus + mTags map[primitive.ObjectID]vidmod.TagInfo + ) + + var wg sync.WaitGroup + wg.Add(3) + common.Go(func() { + defer wg.Done() + mUser = GetUserInfo2Map(uid, uids) + }) + common.Go(func() { + defer wg.Done() + mStatus = GetUserStatus(uid, videos) + }) + common.Go(func() { + defer wg.Done() + mTags = GetTagsByIDs2Map(tags) + }) + wg.Wait() + freeTrialContext := LoadFreeTrialBadgeContext(uid) + commentCountMap := getVideoParentCommentCountMap(videos) + for i, v := range infos { + temp := v + originCoins := temp.Coins + var uInfo vidmod.Publisher + if mUser[temp.PublisherID] != nil { + uInfo = *mUser[temp.PublisherID] + } else { + //处理作者被删除的情况 + if len(mUser) > 0 { + for i := range mUser { + uInfo = *mUser[i] + break + } + } + } + infos[i].Coins = originCoins + info := NewTransfer2Info(temp, uInfo, mStatus[temp.ID], GetPartTags(v.Tags, mTags), originCoins) + info.ShowFreeTrialBadge, info.FreeTrialRemaining, info.CanUseFreeTrial = freeTrialContext.Fields( + info.NewsType, + originCoins, + temp.FreeArea, + temp.PublisherID, + ) + info.CommentCount = commentCountMap[temp.ID] + back = append(back, info) + } + common.Go(func() { + incVideoPageView(videos) + }) + return back +} + +// CalcVideoCoins 计算折扣后视频金币数,判断了包括用户是否为VIP,以及是否购买了视频折扣卡等状态 +// 注意:调用此函数时,在用户为VIP的情况下,largestDiscount必须设为0! +func CalcVideoCoins(originCoins int64, publisherID uint64, self *usermod.User, mFree map[uint64]bool, + videoDiscLog videodiscountmod.VideoDiscountLog, largestDiscount int, discountArea *discount_area_mod.DiscountArea) (discountedCoins int64) { + if originCoins == 0 { + discountedCoins = 0 + return + } + discountedCoins = math.MaxInt64 + now := time.Now() + // 如果在折扣专区里,直接按照折扣专区的价格来 + if self != nil && self.IsVIP(now) && discountArea != nil { + // 计算折扣专区价格 + discountedCoins = CalcDiscountAreaPrice(originCoins, discountArea) + return + } + //判断当前视频 是否对uid为免费视频 则将金币置为0 + if b, ok := mFree[publisherID]; ok && b { + discountedCoins = 0 + return + } + goldVideoFreeLimit := usermod.GetGoldVideoFreeLimit(self) + // 预售卡全部金币视频免费 + if self != nil && self.AllGoldVideoFree && self.GoldVideoFreeExpire.After(now) { + discountedCoins = 0 + return + } + // 用户购买了视频免费卡(视频金币数少于30) + if originCoins <= goldVideoFreeLimit && self != nil && self.VideoFreeExpiration != nil && self.VideoFreeExpiration.After(now) { + discountedCoins = 0 + return + } + + // 用户购买VIP时赠送了视频免费天数 + if self != nil && originCoins > 0 && originCoins <= goldVideoFreeLimit && (self.GoldVideoFreeExpire.After(now) || + (self.VideoFreeExpiration != nil && self.VideoFreeExpiration.After(now))) { + discountedCoins = 0 + return + } + // 用户购买VIP后赠送视频折扣 + if originCoins > 0 && self != nil && self.IsVIP(now) && self.PayVidDiscount > 0 { + discountedCoins = decimal.NewFromInt(int64(self.PayVidDiscount)).Shift(-1). + Mul(decimal.NewFromInt(originCoins)).Round(0).IntPart() + } + if originCoins > 0 { + if videoDiscLog.VideoDiscount > 0 { // 用户购买了视频折扣卡 + newDiscountedCoins := decimal.NewFromInt(int64(videoDiscLog.VideoDiscount)).Shift(-1). + Mul(decimal.NewFromInt(originCoins)).Round(0).IntPart() + if newDiscountedCoins < discountedCoins { + discountedCoins = newDiscountedCoins + } + } else if largestDiscount > 0 { // 用户未购买视频折扣卡,此时按系统中所有VIP卡最大折扣计算 + newDiscountedCoins := decimal.NewFromInt(int64(largestDiscount)).Shift(-1). + Mul(decimal.NewFromInt(originCoins)).Round(0).IntPart() + if newDiscountedCoins < discountedCoins { + discountedCoins = newDiscountedCoins + } + } + } + if discountedCoins == math.MaxInt64 { + discountedCoins = originCoins + } + return +} + +// CalcDiscountAreaPrice 计算折扣专区的价格 +func CalcDiscountAreaPrice(originCoins int64, discountArea *discount_area_mod.DiscountArea) (price int64) { + price = originCoins + // 判断当前视频是否在vip折扣区 + if discountArea != nil { + price = decimal.NewFromInt(int64(discountArea.Discount)).Shift(-2). + Mul(decimal.NewFromInt(originCoins)).Round(0).IntPart() + } + return +} + +// EncodeVideoInfoNoUID 封装返回的视频列表信息(无用户) +func EncodeVideoInfoNoUID(infos []*vidmod.VideoModel) []*vidmod.VideoInfo { + return encodeVideoInfoNoUID(filterOutsideSearch(infos)) +} + +// EncodeVideoInfoNoUIDForRecommend 过滤配置为不进入推荐场景的亚模块内容。 +func EncodeVideoInfoNoUIDForRecommend(infos []*vidmod.VideoModel) []*vidmod.VideoInfo { + return encodeVideoInfoNoUID(filterRecommend(infos)) +} + +func encodeVideoInfoNoUID(infos []*vidmod.VideoModel) []*vidmod.VideoInfo { + infosLen := len(infos) + back := make([]*vidmod.VideoInfo, infosLen) + if infosLen == 0 { + return back + } + uids := make([]uint64, infosLen) + videos := make([]primitive.ObjectID, infosLen) + lids := make([]primitive.ObjectID, infosLen) + var tags []primitive.ObjectID + for i, v := range infos { + uids[i] = v.PublisherID + videos[i] = v.ID + lids[i] = v.Location + tags = append(tags, v.Tags...) + } + var mUser map[uint64]*vidmod.Publisher + var mLocation map[primitive.ObjectID]locmod.Location + var mComment map[primitive.ObjectID]vidmod.CommentInfo + var mTags map[primitive.ObjectID]vidmod.TagInfo + var wg sync.WaitGroup + wg.Add(3) + common.Go(func() { + defer wg.Done() + mUser = GetUserInfo2MapNoUID(uids) + }) + common.Go(func() { + defer wg.Done() + mLocation, _ = locmod.GetLocationInfoByIDs(lids) + }) + common.Go(func() { + defer wg.Done() + mTags = GetTagsByIDs2Map(tags) + }) + wg.Wait() + commentCountMap := getVideoParentCommentCountMap(videos) + for i, v := range infos { + originCoins := v.Coins + var uInfo vidmod.Publisher + if mUser[v.PublisherID] != nil { + uInfo = *mUser[v.PublisherID] + } + noUserStatus := vidmod.VideoStatus{} + info := transfer2Info(v, uInfo, noUserStatus, mLocation[v.Location], GetPartTags(v.Tags, mTags), + mComment[v.ID], originCoins) + info.CommentCount = commentCountMap[v.ID] + back[i] = info + } + common.Go(func() { + incVideoPageView(videos) + }) + return back +} + +func filterOutsideSearch(infos []*vidmod.VideoModel) []*vidmod.VideoModel { + if len(infos) == 0 { + return infos + } + moduleIDs := make([]string, 0, len(infos)) + for _, video := range infos { + if video != nil && video.MID != "" { + moduleIDs = append(moduleIDs, video.MID) + } + } + blocked, err := moduleconfmod.BlockedOutsideSearchModuleIDs(moduleIDs, time.Now()) + if err != nil { + log.Error("filterOutsideSearch module query failed", log.E(err)) + return []*vidmod.VideoModel{} + } + if len(blocked) == 0 { + return infos + } + filtered := make([]*vidmod.VideoModel, 0, len(infos)) + for _, video := range infos { + if video == nil { + continue + } + if _, exists := blocked[video.MID]; exists { + continue + } + filtered = append(filtered, video) + } + return filtered +} + +func filterRecommend(infos []*vidmod.VideoModel) []*vidmod.VideoModel { + if len(infos) == 0 { + return infos + } + excluded, err := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), true) + if err != nil { + log.Error("filterRecommend module query failed", log.E(err)) + return []*vidmod.VideoModel{} + } + blocked := make(map[string]struct{}, len(excluded)) + for _, moduleID := range excluded { + blocked[moduleID] = struct{}{} + } + if len(blocked) == 0 { + return infos + } + filtered := make([]*vidmod.VideoModel, 0, len(infos)) + for _, video := range infos { + if video == nil { + continue + } + if _, exists := blocked[video.MID]; exists { + continue + } + filtered = append(filtered, video) + } + return filtered +} + +func filterSearch(infos []*vidmod.VideoModel) []*vidmod.VideoModel { + if len(infos) == 0 { + return infos + } + excluded, err := moduleconfmod.ExcludedSearchModuleIDs() + if err != nil { + log.Error("filterSearch module query failed", log.E(err)) + return []*vidmod.VideoModel{} + } + blocked := make(map[string]struct{}, len(excluded)) + for _, moduleID := range excluded { + blocked[moduleID] = struct{}{} + } + if len(blocked) == 0 { + return infos + } + filtered := make([]*vidmod.VideoModel, 0, len(infos)) + for _, video := range infos { + if video == nil { + continue + } + if _, exists := blocked[video.MID]; exists { + continue + } + filtered = append(filtered, video) + } + return filtered +} + +func attachVideoInfoSearchTokens(uid uint64, videos []*vidmod.VideoInfo) { + now := time.Now() + for _, video := range videos { + if video == nil { + continue + } + token, err := searchaccessser.Issue(uid, video.ID.Hex(), now) + if err != nil { + log.Error("issue video search access token failed", log.Any("videoID", video.ID), log.E(err)) + continue + } + video.SearchAccessToken = token + } +} + +func attachVideoInfoRespSearchTokens(uid uint64, videos []*vidmod.VideoInfoResp) { + now := time.Now() + for _, video := range videos { + if video == nil { + continue + } + token, err := searchaccessser.Issue(uid, video.ID.Hex(), now) + if err != nil { + log.Error("issue video search access token failed", log.Any("videoID", video.ID), log.E(err)) + continue + } + video.SearchAccessToken = token + } +} + +func incVideoPageView(vids []primitive.ObjectID) { + now := time.Now() + recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) + key := redisconst.VidPageViewListKey(recentMinute.Unix()) + vidStrs := make([]string, len(vids)) + for i := range vids { + vidStrs[i] = vids[i].Hex() + } + if err := appg.Redis.RPush(key, vidStrs); err != nil { + return + } + _, _ = appg.Redis.ExpireKey(key, time.Minute*20) +} + +func incTagCount(v tagmod.Tag, isValidPlay bool) error { + // 增加总播放量 + tPlayCount := v.TPlayCount + 1 + // 增加有效播放量量&更新播放率 + vPlayCount := v.VPlayCount + var playRating float64 + if isValidPlay { + vPlayCount++ + playRating = maths.Decimal6Bit(float64(vPlayCount) / float64(tPlayCount)) + } else { + playRating = maths.Decimal6Bit(float64(vPlayCount) / float64(tPlayCount)) + } + doc := tagmod.TagUpdateDoc{ + VPlayCount: &vPlayCount, + TPlayCount: &tPlayCount, + PlayRating: &playRating, + UpdatedAt: time.Now(), + } + if err := tagmod.UpdateOneTagByID(v.ID, doc); err != nil { + log.Error("incTagCount error", log.Any("tid", v.ID), log.Any("isValidPlay", isValidPlay), log.E(err)) + return err + } + return nil +} + +// IncreaseTag 播放视频对标签计数 +func IncreaseTag(vidID string, tid primitive.ObjectID, isValidPlay bool) error { + if !tid.IsZero() { + tag, err := tagmod.FindOneTagByID(tid) + if err != nil { + return err + } + _ = incTagCount(tag, isValidPlay) + return err + } + tags, err := vidmod.GetVideoTag(vidID) + if err != nil || len(tags) <= 0 { + log.Error("IncreaseTag error", log.Any("vidID", vidID), log.Any("isValidPlay", isValidPlay), log.E(err)) + return err + } + tagList, err := tagmod.FindTagsByIDS(tags) + if err != nil { + return err + } + for _, v := range tagList { + _ = incTagCount(v, isValidPlay) + } + return nil +} + +// GetVideosByIDs 通过id获取视频列表信息 +func GetVideosByIDs(uid uint64, ids []primitive.ObjectID) []*vidmod.VideoInfo { + if len(ids) == 0 { + log.Warn("GetVideosByIDs error", log.Any("uid", uid), log.Any("ids", ids)) + return nil + } + vInfo, unExistsVid, err := getVideoListByIDsFromRedis(ids) + if err != nil || len(unExistsVid) != 0 { + unExistsVInfo, err := getVideoListByIDsFromMongo(unExistsVid) + if err != nil { + log.Error("GetVideosByIDs getVideoListByIDsFromMongo", log.Any("udi", uid), log.Any("ids", ids), + log.E(err)) + return nil + } + common.Go(func() { + if cacheErr := setVideoListByIDs2Redis(unExistsVInfo); cacheErr != nil { + log.Error("GetVideosByIDs setVideoListByIDs2Redis", log.Any("unExistsVid", unExistsVid), log.Any("uid", uid), + log.E(cacheErr)) + } + }) + vInfo = append(vInfo, unExistsVInfo...) + } + if uid == 0 { // 用户未登录 + return EncodeVideoInfoNoUID(vInfo) + } + return EncodeVideoInfo(uid, vInfo) +} + +// GetVideosByIDsNoCache 通过id直接从MongoDB获取视频列表信息,不读写Redis缓存。 +// 按传入ID顺序返回,保证收藏列表维持收藏时间倒序。 +func GetVideosByIDsNoCache(uid uint64, ids []primitive.ObjectID) []*vidmod.VideoInfo { + if len(ids) == 0 { + return nil + } + vInfo, err := getVideoListByIDsFromMongo(ids) + if err != nil { + log.Error("GetVideosByIDsNoCache getVideoListByIDsFromMongo", log.Any("uid", uid), log.Any("ids", ids), + log.E(err)) + return nil + } + vInfo = orderVideoModelsByIDs(ids, vInfo) + if uid == 0 { + return EncodeVideoInfoNoUID(vInfo) + } + return EncodeVideoInfo(uid, vInfo) +} + +func orderVideoModelsByIDs(ids []primitive.ObjectID, videos []*vidmod.VideoModel) []*vidmod.VideoModel { + videoByID := make(map[primitive.ObjectID]*vidmod.VideoModel, len(videos)) + for _, video := range videos { + if video != nil { + videoByID[video.ID] = video + } + } + ordered := make([]*vidmod.VideoModel, 0, len(videos)) + for _, id := range ids { + if video, ok := videoByID[id]; ok { + ordered = append(ordered, video) + } + } + return ordered +} + +// NewGetVideosByIDs 通过id获取视频列表信息 +func NewGetVideosByIDs(uid uint64, ids []primitive.ObjectID) []*vidmod.VideoInfoResp { + if len(ids) == 0 { + log.Warn("GetVideosByIDs error", log.Any("uid", uid), log.Any("ids", ids)) + return nil + } + vInfo, unExistsVid, err := getVideoListByIDsFromRedis(ids) + if err != nil || len(unExistsVid) != 0 { + unExistsVInfo, err := getVideoListByIDsFromMongo(unExistsVid) + if err != nil { + log.Error("GetVideosByIDs getVideoListByIDsFromMongo", log.Any("udi", uid), log.Any("ids", ids), + log.E(err)) + return nil + } + + common.Go(func() { + if cacheErr := setVideoListByIDs2Redis(unExistsVInfo); cacheErr != nil { + log.Error("GetVideosByIDs setVideoListByIDs2Redis", log.Any("unExistsVid", unExistsVid), log.Any("uid", uid), + log.E(cacheErr)) + } + }) + vInfo = append(vInfo, unExistsVInfo...) + // 将redis中获取不到的写入切片中后得按照原来id顺序的重新排序 + videoMap := make(map[primitive.ObjectID]*vidmod.VideoModel) + for _, v := range vInfo { + videoMap[v.ID] = v + } + videoList := []*vidmod.VideoModel{} + for _, id := range ids { + video, ok := videoMap[id] + if !ok { + continue + } + videoList = append(videoList, video) + } + vInfo = videoList + } + //if uid == 0 { // 用户未登录 + // return EncodeVideoInfoNoUID(vInfo) + //} + return NewEncodeVideoInfo(uid, vInfo) +} + +func getVideoModelByIDs(ids []primitive.ObjectID) []*vidmod.VideoModel { + if len(ids) == 0 { + log.Warn("getVideoModelByIDs error", log.Any("ids", ids)) + return nil + } + vInfo, unExistsVid, err := getVideoListByIDsFromRedis(ids) + if err != nil || len(unExistsVid) != 0 { + unExistsVInfo, err := getVideoListByIDsFromMongoBaseNoStatus(unExistsVid) + if err != nil { + return nil + } + common.Go(func() { + _ = setVideoListByIDs2Redis(unExistsVInfo) + }) + vInfo = append(vInfo, unExistsVInfo...) + } + return vInfo +} + +// GetVideoListByIDsNoStatus 通过id获取视频列表信息 底层不判断视频状态, 由上次业务保证ID的正确性 +func GetVideoListByIDsNoStatus(uid uint64, ids []primitive.ObjectID) []*vidmod.VideoInfoResp { + vInfo := getVideoModelByIDs(ids) + return NewEncodeVideoInfo(uid, vInfo) +} + +func getRedis() *redis.Client { + if appg.Redis != nil { + return appg.Redis + } + if skdg.Redis != nil { + return skdg.Redis + } + return nil +} + +func setCache(recentMinute time.Time) { + common.Go(func() { + ids := getNewsTop200FromMongo(recentMinute) + if len(ids) != 0 { + setNewestNews2Redis(ids, recentMinute) + } + }) +} + +func setCache_old() { + common.Go(func() { + ids := getNewsTop200FromMongo_old() + if len(ids) != 0 { + setNewestNews2Redis_old(ids) + } + }) +} + +func setShortVideoCache() { + common.Go(func() { + ids := getNewsShortVideoTop200FromMongo() + if len(ids) != 0 { + setNewestShortVideo2Redis(ids) + } + }) +} + +// GetNewestNews 获取最新帖子 +// 最新两百条存redis,其他数据库查找 +func GetNewestNews(page, size uint64, reqTime time.Time) ([]*vidmod.VideoModel, bool, error) { + recentMinute := timerange.RecentMinute(reqTime, statrecordmod.FiveMinuteScale) + endpoint := page * size + if endpoint > MaxNewestCacheNum { //大于200条的数据库获取 + return getNewestNewsFromMongo(page, size, recentMinute) + } + ids, err := getNewestNewsFromRedis(page, size, recentMinute) + if err != nil || uint64(len(ids)) < size { //缓存获取失败,数据库获取 + infos, hasNext, err := getNewestNewsFromMongo(page, size, recentMinute) + setCache(recentMinute) + return infos, hasNext, err + } + oids := common.String2ObjectID(ids) + infos := getVideoModelByIDs(oids) + if len(infos) == 0 { + log.Warn("info length error", log.Any("page", page), log.Any("size", size), log.Any("reqTime", reqTime), log.Any("recentMinute", recentMinute), log.Any("ids", ids)) + } + return infos, true, nil +} + +// GetNewestNews 获取最新帖子 +// 最新两百条存redis,其他数据库查找 +func GetNewestNews_old(page, size uint64) ([]*vidmod.VideoModel, bool, error) { + if endpoint := page * size; endpoint > MaxNewestCacheNum { //大于200条的数据库获取 + return getNewestNewsFromMongo_old(page, size) + } + ids, err := getNewestNewsFromRedis_old(page, size) + if err != nil || uint64(len(ids)) < size { //缓存获取失败,数据库获取 + infos, hasNext, err := getNewestNewsFromMongo_old(page, size) + setCache_old() + return infos, hasNext, err + } + oids := common.String2ObjectID(ids) + infos := getVideoModelByIDs(oids) + if len(infos) == 0 { + log.Warn("info length error_old", log.Any("page", page), log.Any("size", size), log.Any("ids", ids)) + } + return infos, true, nil +} + +func GetNewestShortVideo(page, size uint64) ([]*vidmod.VideoModel, bool, error) { + if endpoint := page * size; endpoint > MaxNewestCacheNum { //大于200条的数据库获取 + return getNewestShortVideoFromMongo(page, size) + } + ids, err := getNewestShortVideoFromRedis(page, size) + if err != nil || uint64(len(ids)) < size { //缓存获取失败,数据库获取 + infos, hasNext, err := getNewestShortVideoFromMongo(page, size) + setShortVideoCache() + return infos, hasNext, err + } + oids := common.String2ObjectID(ids) + infos := getVideoModelByIDs(oids) + if len(infos) == 0 { + log.Warn("info length error_old", log.Any("page", page), log.Any("size", size), log.Any("ids", ids)) + } + return infos, true, nil +} + +// IncCommentCount 评论统计次数加加 +func IncCommentCount(id primitive.ObjectID) error { + if err := vidmod.IncCommentCount(id); err != nil { + return err + } + incComment(id) + return nil +} + +// 修正用户喜欢的视频记录 +func FixLikeVideoRecord(uid uint64, oids []primitive.ObjectID) { + back, err := vidmod.GetVideoListByIDsUnPublish(oids) + if err != nil || len(back) == 0 { + return + } + vids := make([]primitive.ObjectID, len(back)) + for i, v := range back { + vids[i] = v.ID + } +} + +// NewTransfer2Info 将视频数据库模式转为返回模式 +func NewTransfer2Info(v *vidmod.VideoModel, uInfo vidmod.Publisher, vStatus vidmod.VideoStatus, tags []vidmod.TagInfo, originCoins int64) *vidmod.VideoInfoResp { + if v.Status == 3 { + v.Status = 1 + } + newType := vidmod.SP + if len(v.NewsType) != 0 { + newType = v.NewsType + } + base := vidmod.VideoInfoResp{ + ID: v.ID, + NewsType: newType, + Title: v.Title, + PlayTime: v.PlayTime, + Cover: v.Cover, + SeriesCover: v.SeriesCover, + PlayCount: (v.PlayCount + v.FakePlayCount) * VideoPlayCountMultiplier, + LikeCount: v.LikeCount, // 移除like数量加倍,修复各处like数不一致问题 + PageViewCount: v.PageViewCount * 3, + CommentCount: v.CommentCount, + Coins: v.Coins, + Size: v.Size, + Resolution: v.Resolution, + Ratio: v.Ratio, + CreatedAt: v.CreatedAt, + ReviewAt: v.ReviewAt, + FreeTime: v.FreeTime, + Status: v.Status, + Reason: v.Reason, + SourceURL: v.SourceURL, + H265Url: H265URLForApp(v), + LinkUrl: v.LinkUrl, + //IsTopping: v.IsTopping, + IsChoosen: v.IsChoosen, + IsRecommend: v.IsRecommend, + Rewarded: v.Rewarded.Add(v.FakeRewarded), + OriginCoins: originCoins, + Chosen: v.Chosen, + DownloadAllow: v.DownloadAllow, + ShowType: v.ShowType, + FreeArea: v.FreeArea, + Tags: tags, + } + if v.LiaoBaTopSort > 0 { + base.IsTopping = true + } + var vipLevel int + if uInfo.VipExpireDate.After(time.Now()) { + vipLevel = uInfo.VipLevel + } + u := vidmod.AppPublisherResp{ + UID: uInfo.UID, + Name: uInfo.Name, + Portrait: uInfo.Portrait, + HasFollowed: uInfo.HasFollowed, + VipLevel: vipLevel, + VipName: uInfo.VipName, + } + base.UInfo = u + base.VidStatus = vStatus + info := base + + return &info +} + +func ToVideoBaseInfo(v *vidmod.VideoModel, ti []vidmod.TagInfo, originCoins int64) *vidmod.VideoBase { + if v.Status == 3 { + v.Status = 1 + } + newType := vidmod.SP + if len(v.NewsType) != 0 { + newType = v.NewsType + } + + base := vidmod.VideoBase{ + ID: v.ID, + NewsType: newType, + Title: v.Title, + Content: v.Content, + Tags: ti, + SourceURL: v.SourceURL, + H265Url: H265URLForApp(v), + PreviewURL: v.PreviewURL, + LinkUrl: v.LinkUrl, + PlayTime: v.PlayTime, + Cover: v.Cover, + CoverThumb: v.CoverThumb, + SeriesCover: v.SeriesCover, + LikeCount: v.LikeCount, // 移除like数量加倍,修复各处like数不一致问题 + PlayCount: (v.PlayCount + v.FakePlayCount) * VideoPlayCountMultiplier, + PurchaseCount: v.PurchaseCount, + CommentCount: v.CommentCount, + ShareCount: v.FakeShareCount, + Coins: v.Coins, + Size: v.Size, + Resolution: v.Resolution, + Ratio: v.Ratio, + Status: v.Status, + Reason: v.Reason, + FreeTime: v.FreeTime, + IsHideLocation: v.IsHideLocation, + FreeArea: v.FreeArea, + CreatedAt: v.CreatedAt, + ReviewAt: v.ReviewAt, + IsTopping: v.IsTopping, + IsRecommend: v.IsRecommend, + IsChoosen: v.IsChoosen, + Rewarded: v.Rewarded.Add(v.FakeRewarded), + OriginCoins: originCoins, + CollectCount: v.CollectCount, + PageViewCount: v.PageViewCount * 3, + SeedLinkUrl: v.SeedLinkUrl, + SeedSize: v.SeedSize, + SeedPlayTime: v.SeedPlayTime, + PreviewStart: v.PreviewStart, + RichText: v.RichText, + TimeNodeList: v.TimeNodeList, + DownloadAllow: v.DownloadAllow, + ShowType: v.ShowType, + } + return &base +} diff --git a/app/service/vidhelpser/helpser_concurrency_test.go b/app/service/vidhelpser/helpser_concurrency_test.go new file mode 100644 index 0000000..39cdaa5 --- /dev/null +++ b/app/service/vidhelpser/helpser_concurrency_test.go @@ -0,0 +1,114 @@ +package vidhelpser + +import ( + "go/ast" + "go/parser" + "go/token" + "path/filepath" + "runtime" + "testing" +) + +func parseHelpserSource(t *testing.T) *ast.File { + t.Helper() + _, testFile, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("resolve test source path") + } + path := filepath.Join(filepath.Dir(testFile), "helpser.go") + file, err := parser.ParseFile(token.NewFileSet(), path, nil, 0) + if err != nil { + t.Fatalf("parse %s: %v", path, err) + } + return file +} + +func findFunction(t *testing.T, file *ast.File, name string) *ast.FuncDecl { + t.Helper() + for _, decl := range file.Decls { + fn, ok := decl.(*ast.FuncDecl) + if ok && fn.Name.Name == name { + return fn + } + } + t.Fatalf("function %s not found", name) + return nil +} + +func TestVideoInfoEncodersAggregatePageViewsOnlyThroughRedis(t *testing.T) { + file := parseHelpserSource(t) + encoders := []string{ + "encodeVideoInfo", + "newEncodeVideoInfoNotStatus", + "newEncodeVideoInfo", + "encodeVideoInfoNoUID", + } + + for _, name := range encoders { + t.Run(name, func(t *testing.T) { + fn := findFunction(t, file, name) + directMongoCalls := 0 + redisAggregateCalls := 0 + ast.Inspect(fn.Body, func(node ast.Node) bool { + call, ok := node.(*ast.CallExpr) + if !ok { + return true + } + switch callee := call.Fun.(type) { + case *ast.Ident: + if callee.Name == "incVideoPageView" { + redisAggregateCalls++ + } + case *ast.SelectorExpr: + pkg, ok := callee.X.(*ast.Ident) + if ok && pkg.Name == "vidmod" && callee.Sel.Name == "IncVideoPageView" { + directMongoCalls++ + } + } + return true + }) + + if directMongoCalls != 0 { + t.Fatalf("direct Mongo page-view increments = %d, want 0", directMongoCalls) + } + if redisAggregateCalls != 1 { + t.Fatalf("Redis page-view aggregate calls = %d, want 1", redisAggregateCalls) + } + }) + } +} + +func TestAsyncClosuresDoNotAssignCapturedErr(t *testing.T) { + file := parseHelpserSource(t) + functions := []string{ + "encodeVideoInfo", + "GetVideosByIDs", + "NewGetVideosByIDs", + } + + for _, name := range functions { + t.Run(name, func(t *testing.T) { + fn := findFunction(t, file, name) + ast.Inspect(fn.Body, func(node ast.Node) bool { + closure, ok := node.(*ast.FuncLit) + if !ok { + return true + } + ast.Inspect(closure.Body, func(node ast.Node) bool { + assign, ok := node.(*ast.AssignStmt) + if !ok || assign.Tok != token.ASSIGN { + return true + } + for _, lhs := range assign.Lhs { + ident, ok := lhs.(*ast.Ident) + if ok && ident.Name == "err" { + t.Errorf("async closure assigns captured err at %s", name) + } + } + return true + }) + return false + }) + }) + } +} diff --git a/app/service/vidhelpser/mongohelp.go b/app/service/vidhelpser/mongohelp.go new file mode 100644 index 0000000..49d7029 --- /dev/null +++ b/app/service/vidhelpser/mongohelp.go @@ -0,0 +1,88 @@ +package vidhelpser + +import ( + "time" + + "91porn-server/models/v/tagmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func getUsersBaseInfoFromMongo(uids []uint64) ([]*usermod.BaseInfoVip, error) { + return usermod.GetUsersBaseInfoWithVip(uids) +} + +func getTagsByIDsFromMongo(tids []primitive.ObjectID) ([]vidmod.TagInfo, error) { + tInfo, err := tagmod.FindTagsByIDS(tids) + if err != nil { + return []vidmod.TagInfo{}, err + } + info := make([]vidmod.TagInfo, len(tInfo)) + for i, v := range tInfo { + info[i] = vidmod.TagInfo{ + ID: v.ID, + Name: v.TagName, + CoverImg: v.CoverImg, + Description: v.Description, + } + } + return info, nil +} + +func getVideoListByIDsFromMongo(vids []primitive.ObjectID) ([]*vidmod.VideoModel, error) { + return vidmod.GetVideoListByIDs(vids) +} + +func getVideoListByIDsFromMongoBaseNoStatus(vids []primitive.ObjectID) ([]*vidmod.VideoModel, error) { + return vidmod.GetVideoListByIDsNoStatus(vids) +} + +func getNewestNewsFromMongo(page, size uint64, recentMinute time.Time) ([]*vidmod.VideoModel, bool, error) { + return vidmod.GetNewestNews(page, size, recentMinute) +} + +func getNewestNewsFromMongo_old(page, size uint64) ([]*vidmod.VideoModel, bool, error) { + return vidmod.GetNewestNews_old(page, size) +} + +func getNewestShortVideoFromMongo(page, size uint64) ([]*vidmod.VideoModel, bool, error) { + return vidmod.GetNewestShortVideo(page, size) +} + +func getNewsTop200FromMongo(recentMinute time.Time) []string { + vModel, _, err := vidmod.GetNewestNews(1, MaxNewestCacheNum, recentMinute) + if err != nil { + return []string{} + } + ids := make([]string, len(vModel)) + for i, v := range vModel { + ids[i] = v.ID.Hex() + } + return ids +} + +func getNewsTop200FromMongo_old() []string { + vModel, _, err := vidmod.GetNewestNews_old(1, MaxNewestCacheNum) + if err != nil { + return []string{} + } + ids := make([]string, len(vModel)) + for i, v := range vModel { + ids[i] = v.ID.Hex() + } + return ids +} + +func getNewsShortVideoTop200FromMongo() []string { + vModel, _, err := vidmod.GetNewestShortVideo(1, MaxNewestCacheNum) + if err != nil { + return []string{} + } + ids := make([]string, len(vModel)) + for i, v := range vModel { + ids[i] = v.ID.Hex() + } + return ids +} diff --git a/app/service/vidhelpser/redishelp.go b/app/service/vidhelpser/redishelp.go new file mode 100644 index 0000000..ed69b9f --- /dev/null +++ b/app/service/vidhelpser/redishelp.go @@ -0,0 +1,317 @@ +package vidhelpser + +import ( + "encoding/json" + "fmt" + "time" + + "91porn-server/app/appg" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func getUIDKey(uids []uint64) []string { + format := redisconst.UserInfoKey() + keys := make([]string, len(uids)) + for i, u := range uids { + keys[i] = fmt.Sprintf(format, u) + } + return keys +} + +func getTidKey(tids []primitive.ObjectID) []string { + format := redisconst.TagInfoKey() + keys := make([]string, len(tids)) + for i, t := range tids { + keys[i] = fmt.Sprintf(format, t.Hex()) + } + return keys +} + +func getVidKey(vids []primitive.ObjectID) []string { + format := redisconst.VideoInfoKey() + keys := make([]string, len(vids)) + for i, v := range vids { + keys[i] = fmt.Sprintf(format, v.Hex()) + } + return keys +} + +func parseUserInfo(data []interface{}) []usermod.BaseInfoVip { + infos := make([]usermod.BaseInfoVip, 0, len(data)) + for _, d := range data { + str, ok := d.(string) + if !ok { + continue + } + var u usermod.BaseInfoVip + if err := json.Unmarshal([]byte(str), &u); err != nil { + continue + } + infos = append(infos, u) + } + return infos +} + +func getPublisher(info []usermod.BaseInfoVip) map[uint64]*vidmod.Publisher { + m := make(map[uint64]*vidmod.Publisher) + for _, u := range info { + p := vidmod.Publisher{ + UInfo: u, + } + m[u.UID] = &p + } + return m +} + +func getUnExistUID(uids []uint64, m map[uint64]*vidmod.Publisher) []uint64 { + ids := make([]uint64, 0, len(uids)) + for _, u := range uids { + if m[u] == nil { + ids = append(ids, u) + } + } + return ids +} + +func getUsersBaseInfoFromRedis(uids []uint64) (map[uint64]*vidmod.Publisher, []uint64, error) { + sInfo := getUIDKey(uids) + uInfo, err := appg.Redis.MGet(sInfo...) + if err != nil { + log.Error("getUsersBaseInfoFromRedis error", log.Any("uids", uids), log.Any("sInfo", sInfo), log.E(err)) + return nil, uids, err + } + userInfo := parseUserInfo(uInfo) + mPublisher := getPublisher(userInfo) + unExist := getUnExistUID(uids, mPublisher) + return mPublisher, unExist, nil +} + +func setUsersBaseInfo2Redis(infos []*usermod.BaseInfoVip) error { + format := redisconst.UserInfoKey() + expire := redisconst.UserInfoExpire() + for _, u := range infos { + d, err := json.Marshal(u) + if err != nil { + continue + } + s := fmt.Sprintf(format, u.UID) + _ = appg.Redis.Set(s, string(d), expire) + } + return nil +} + +func parseTagInfo(data []interface{}) []vidmod.TagInfo { + infos := make([]vidmod.TagInfo, 0, len(data)) + for _, d := range data { + str, ok := d.(string) + if !ok { + continue + } + var t vidmod.TagInfo + if err := json.Unmarshal([]byte(str), &t); err != nil { + continue + } + infos = append(infos, t) + } + return infos +} + +func getUnExistTID(ids []primitive.ObjectID, info []vidmod.TagInfo) []primitive.ObjectID { + m := make(map[primitive.ObjectID]bool) + for _, i := range info { + m[i.ID] = true + } + unExist := make([]primitive.ObjectID, 0, len(ids)) + for _, i := range ids { + if !m[i] { + unExist = append(unExist, i) + } + } + return unExist +} + +func getTagsByIDsFromRedis(ids []primitive.ObjectID) ([]vidmod.TagInfo, []primitive.ObjectID, error) { + sInfo := getTidKey(ids) + uInfo, err := appg.Redis.MGet(sInfo...) + if err != nil { + log.Error("getTagsByIDsFromRedis error", log.Any("ids", ids), log.Any("sInfo", sInfo), log.E(err)) + return nil, ids, err + } + tagInfo := parseTagInfo(uInfo) + unExist := getUnExistTID(ids, tagInfo) + return tagInfo, unExist, nil +} + +func setTagsByIDs2Redis(tags []vidmod.TagInfo) error { + expire := redisconst.TagInfoExpire() + format := redisconst.TagInfoKey() + for _, t := range tags { + d, err := json.Marshal(t) + if err != nil { + continue + } + s := fmt.Sprintf(format, t.ID.Hex()) + _ = appg.Redis.Set(s, string(d), expire) + } + return nil +} + +func parseVideoInfo(data []interface{}) []*vidmod.VideoModel { + infos := make([]*vidmod.VideoModel, 0, len(data)) + for _, d := range data { + str, ok := d.(string) + if !ok { + continue + } + var v vidmod.VideoModel + if err := json.Unmarshal([]byte(str), &v); err != nil { + continue + } + infos = append(infos, &v) + } + return infos +} + +func getUnExistVID(ids []primitive.ObjectID, info []*vidmod.VideoModel) []primitive.ObjectID { + m := make(map[primitive.ObjectID]bool) + for _, i := range info { + m[i.ID] = true + } + unExist := make([]primitive.ObjectID, 0, len(ids)) + for _, i := range ids { + if !m[i] { + unExist = append(unExist, i) + } + } + return unExist +} + +func getVideoListByIDsFromRedis(ids []primitive.ObjectID) ([]*vidmod.VideoModel, []primitive.ObjectID, error) { + sInfo := getVidKey(ids) + uInfo, err := appg.Redis.MGet(sInfo...) + if err != nil { + log.Error("getVideoListByIDsFromRedis error", log.Any("ids", ids), log.Any("sInfo", sInfo), log.E(err)) + return nil, ids, err + } + videoInfo := parseVideoInfo(uInfo) + unExist := getUnExistVID(ids, videoInfo) + return videoInfo, unExist, nil +} + +func setVideoListByIDs2Redis(infos []*vidmod.VideoModel) error { + expire := redisconst.VideoInfoExpire() + format := redisconst.VideoInfoKey() + for _, v := range infos { + d, err := json.Marshal(v) + if err != nil { + continue + } + s := fmt.Sprintf(format, v.ID.Hex()) + _ = appg.Redis.Set(s, string(d), expire) + } + return nil +} + +func getNewestNewsFromRedis(page, size uint64, recentMinute time.Time) ([]string, error) { + key := redisconst.NewestNewsKey(recentMinute) + start := (page - 1) * size + end := page*size - 1 + ids, err := appg.Redis.ZRange(key, int64(start), int64(end)) + if err != nil { + log.Error("getNewestNewsFromRedis error", log.Any("page", page), log.Any("size", size), log.E(err)) + } + return ids, err +} + +func getNewestNewsFromRedis_old(page, size uint64) ([]string, error) { + key := redisconst.NewestNewsKey_old() + start := (page - 1) * size + end := page*size - 1 + ids, err := appg.Redis.ZRange(key, int64(start), int64(end)) + if err != nil { + log.Error("getNewestNewsFromRedis error", log.Any("page", page), log.Any("size", size), log.E(err)) + } + return ids, err +} + +func getNewestShortVideoFromRedis(page, size uint64) ([]string, error) { + key := redisconst.NewestShortVideoKey() + start := (page - 1) * size + end := page*size - 1 + ids, err := appg.Redis.ZRange(key, int64(start), int64(end)) + if err != nil { + log.Error("getNewestNewsFromRedis error", log.Any("page", page), log.Any("size", size), log.E(err)) + } + return ids, err +} + +func news2RedisZ(ids []string) []redis.Member { + if len(ids) == 0 { + return []redis.Member{} + } + members := make([]redis.Member, 0, len(ids)) + for k, v := range ids { + if v != "" { + members = append(members, redis.Member{Score: float64(k), Member: v}) + } + } + return members +} + +func setNewestNews2Redis(ids []string, recentMinute time.Time) { + key := redisconst.NewestNewsKey(recentMinute) + expird := redisconst.NewestNewsExpire() + if _, err := appg.Redis.Del(key); err != nil { + return + } + values := news2RedisZ(ids) + if _, err := appg.Redis.ZAdd(key, values...); err != nil { + return + } + _, _ = appg.Redis.ExpireKeAt(key, time.Now().Add(expird)) +} + +func setNewestNews2Redis_old(ids []string) { + key := redisconst.NewestNewsKey_old() + expird := redisconst.NewestNewsExpire() + if _, err := appg.Redis.Del(key); err != nil { + return + } + values := news2RedisZ(ids) + if _, err := appg.Redis.ZAdd(key, values...); err != nil { + return + } + _, _ = appg.Redis.ExpireKeAt(key, time.Now().Add(expird)) +} + +func setNewestShortVideo2Redis(ids []string) { + key := redisconst.NewestShortVideoKey() + expird := redisconst.NewestNewsExpire() + if _, err := appg.Redis.Del(key); err != nil { + return + } + values := news2RedisZ(ids) + if _, err := appg.Redis.ZAdd(key, values...); err != nil { + return + } + _, _ = appg.Redis.ExpireKeAt(key, time.Now().Add(expird)) +} + +func incComment(vid primitive.ObjectID) { + infos, _, err := getVideoListByIDsFromRedis([]primitive.ObjectID{vid}) + if err != nil || len(infos) == 0 { + return + } + if infos[0] == nil { + return + } + infos[0].CommentCount++ + infos[0].FakeCommentCount++ + _ = setVideoListByIDs2Redis(infos) +} diff --git a/app/service/vidser/acg_sort_test.go b/app/service/vidser/acg_sort_test.go new file mode 100644 index 0000000..86e5eaf --- /dev/null +++ b/app/service/vidser/acg_sort_test.go @@ -0,0 +1,29 @@ +package vidser + +import ( + "reflect" + "testing" + + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" +) + +func TestACGMediaSortLatest(t *testing.T) { + want := bson.D{ + {Key: "latestPublishedAt", Value: -1}, + {Key: "contentUpdateTime", Value: -1}, + {Key: "createdAt", Value: -1}, + {Key: "_id", Value: -1}, + } + if got := acgMediaSort(int(commod.New)); !reflect.DeepEqual(got, want) { + t.Fatalf("acgMediaSort(New) = %#v, want %#v", got, want) + } +} + +func TestACGMediaSortNonLatestKeepsDefault(t *testing.T) { + want := bson.D{{Key: "createdAt", Value: -1}} + if got := acgMediaSort(int(commod.MostHot)); !reflect.DeepEqual(got, want) { + t.Fatalf("acgMediaSort(MostHot) = %#v, want %#v", got, want) + } +} diff --git a/app/service/vidser/dedup.go b/app/service/vidser/dedup.go new file mode 100644 index 0000000..ad1a474 --- /dev/null +++ b/app/service/vidser/dedup.go @@ -0,0 +1,50 @@ +package vidser + +import ( + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 元素去重 +func RemoveRep(slc []*vidmod.VideoModel) []*vidmod.VideoModel { + if len(slc) < 1024 { + // 切片长度小于1024的时候,循环来过滤 + return RemoveRepByLoop(slc) + } + // 大于的时候,通过map来过滤 + return RemoveRepByMap(slc) +} + +// 通过map主键唯一的特性过滤重复元素 +func RemoveRepByMap(slc []*vidmod.VideoModel) []*vidmod.VideoModel { + result := make([]*vidmod.VideoModel, 0, len(slc)) + tempMap := map[primitive.ObjectID]struct{}{} // 存放已添加主键 + for _, e := range slc { + if _, ok := tempMap[e.ID]; ok { // 主键已添加, 则不重复添加 + continue + } + tempMap[e.ID] = struct{}{} + result = append(result, e) + } + return result +} + +// 通过两重循环过滤重复元素 +func RemoveRepByLoop(slc []*vidmod.VideoModel) []*vidmod.VideoModel { + result := make([]*vidmod.VideoModel, 0, len(slc)) // 存放结果 + for i := range slc { + exists := false + for j := range result { + if slc[i].ID == result[j].ID { + exists = true + break + } + } + if exists { + continue + } + result = append(result, slc[i]) + } + return result +} diff --git a/app/service/vidser/discountArea.go b/app/service/vidser/discountArea.go new file mode 100644 index 0000000..3290594 --- /dev/null +++ b/app/service/vidser/discountArea.go @@ -0,0 +1,97 @@ +package vidser + +import ( + "91porn-server/app/service/vidhelpser" + "91porn-server/common/cachev2" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/models/v/discount_area_mod" + "91porn-server/models/v/discount_area_video_mod" + "91porn-server/models/v/vidmod" + "fmt" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +// GetDiscountArea 获取折扣专区 +func GetDiscountArea() (list []discount_area_mod.DiscountArea, err error) { + _, err = cachev2.Classes().CacheTime(redisconst.DiscountAreaExpire).AutoListKey(redisconst.DiscountArea).ResBind(&list).Cache(discount_area_mod.GetAllDiscountArea) + if err != nil { + log.Error(fmt.Sprintf("cachev2 discount_area_mod.GetAllDiscountArea err:%v", err)) + return + } + return list, nil +} + +// GetDiscountAreaVideos 获取折扣专区下的视频 +func GetDiscountAreaVideos(uid uint64, req *DiscountVideoReq) (data *DiscountVideoResp, err error) { + data = &DiscountVideoResp{ + List: []*vidmod.VideoInfo{}, + } + var videos []*vidmod.VideoModel + key := fmt.Sprintf("DiscountAreaVideoList:%v:%v:%v:%v", req.DiscountId, req.SortType, req.PageSize, req.PageNumber) + _, err = cachev2.Classes().CacheTime(2*time.Minute).AutoListKey(key).ResBind(&videos).Cache(discountAreaVideos, req) + if err != nil { + log.Error(fmt.Sprintf("cachev2 discountAreaVideos err:%v", err)) + return + } + + if len(videos) == 0 { + log.Warn(fmt.Sprintf("discountAreaVideos is null, discountAreaId:%v", req.DiscountId)) + return data, nil + } + if len(videos) > int(req.PageSize) { + data.HasNext = true + videos = videos[:req.PageSize] + } + videoList := vidhelpser.EncodeVideoInfo(uid, videos) + data.List = videoList + return +} + +// 获取折扣专区视频列表 +func discountAreaVideos(req *DiscountVideoReq) (list []*vidmod.VideoModel, err error) { + // 按照排序类型查询方式不同 + switch req.SortType { + case 2: + //按照最热查询 + list, err = vidmod.GetVideoListByCond(req.Filter(), req.Options()) + default: + list, err = newDiscountAreaVideos(req) + return + } + if err != nil { + log.Error(fmt.Sprintf("discountAreaVideos err:%v", err)) + return + } + return +} + +func newDiscountAreaVideos(req *DiscountVideoReq) (res []*vidmod.VideoModel, err error) { + // 按照最新添加到折扣专区的顺序排 + discountAreaVideoList, _, err := discount_area_video_mod.GetListByCond(req.Filter(), req.Options()) + if err != nil { + log.Error(fmt.Sprintf("discount_area_video_mod.GetListByCond err:%v", err)) + return + } + vids := []primitive.ObjectID{} + for _, v := range discountAreaVideoList { + vids = append(vids, v.VideoID) + } + // 获取视频 + list, err := vidmod.GetVideoListByIDsNoStatus(vids) + if err != nil { + log.Error(fmt.Sprintf("vidmod.GetVideoListByIDs err:%v", err)) + return + } + // 按照原来的顺序返回(不要使用sort包排序。按照时间排序,会出现问题) + videoMap := make(map[primitive.ObjectID]*vidmod.VideoModel) + for _, v := range list { + videoMap[v.ID] = v + } + for _, v := range discountAreaVideoList { + video := videoMap[v.VideoID] + res = append(res, video) + } + return +} diff --git a/app/service/vidser/free_trial.go b/app/service/vidser/free_trial.go new file mode 100644 index 0000000..577378a --- /dev/null +++ b/app/service/vidser/free_trial.go @@ -0,0 +1,15 @@ +package vidser + +import "91porn-server/app/service/vidhelpser" + +func applyFreeTrialBadgeToSubModule(uid uint64, data *VideoUnderSubModuleResp) { + if data == nil { + return + } + ctx := vidhelpser.LoadFreeTrialBadgeContext(uid) + vidhelpser.ApplyFreeTrialBadgeToVideoInfoResps(ctx, data.AllVideoInfo) + vidhelpser.ApplyFreeTrialBadgeToVideoInfoResps(ctx, data.ChosenVideoInfo) + for i := range data.AllSection { + vidhelpser.ApplyFreeTrialBadgeToVideoInfoResps(ctx, data.AllSection[i].AllVideoInfo) + } +} diff --git a/app/service/vidser/free_watch_consume.go b/app/service/vidser/free_watch_consume.go new file mode 100644 index 0000000..d2ce81b --- /dev/null +++ b/app/service/vidser/free_watch_consume.go @@ -0,0 +1,250 @@ +package vidser + +import ( + "errors" + "fmt" + "strconv" + "time" + + "91porn-server/app/appg" + "91porn-server/common/db" + "91porn-server/common/stderr" + "91porn-server/common/timeutil" + "91porn-server/models/v/useractmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +const freeWatchTransactionRetryLimit = 5 + +type freeWatchConsumeState struct { + isVIP bool + isPublisher bool + freeArea bool + paidVideo bool + viewedToday bool + watchCount uint64 + totalCount uint64 +} + +type freeWatchConsumeDecision struct { + isCan bool + shouldConsume bool + watchCount uint64 +} + +func decideFreeWatchConsume(state freeWatchConsumeState) freeWatchConsumeDecision { + watchCount := state.watchCount + if watchCount > state.totalCount { + watchCount = state.totalCount + } + if state.isVIP || state.isPublisher || state.freeArea || state.paidVideo || state.viewedToday { + return freeWatchConsumeDecision{isCan: true, watchCount: watchCount} + } + if watchCount == 0 { + return freeWatchConsumeDecision{watchCount: 0} + } + return freeWatchConsumeDecision{ + isCan: true, + shouldConsume: true, + watchCount: watchCount, + } +} + +// ConsumeFreeWatch 按现有免费观看视频规则消费次数。 +// 同一用户、同一视频在同一自然日最多消费一次。 +func ConsumeFreeWatch(uid uint64, oid primitive.ObjectID) (vidmod.WatchConsumeResp, error) { + at := time.Now() + day := timeutil.BeginningOfDay(at) + totalCount := TotalFreeWatchCount() + + user, err := usermod.FindUserByUIDForNoCache(uid) + if err != nil { + return vidmod.WatchConsumeResp{}, fmt.Errorf("find user: %w", err) + } + if user == nil { + return vidmod.WatchConsumeResp{}, errors.New("find user: empty user") + } + video, err := vidmod.GetVideoInfo(oid.Hex()) + if err != nil { + return vidmod.WatchConsumeResp{}, fmt.Errorf("find video: %w", err) + } + if video.ID.IsZero() || video.Status != vidmod.CheckPass { + return vidmod.WatchConsumeResp{}, errors.New("video is missing or unavailable") + } + + state := freeWatchConsumeState{ + isVIP: !user.VipExpireDate.Before(at), + isPublisher: video.PublisherID == uid, + freeArea: video.FreeArea, + paidVideo: video.Coins > 0, + watchCount: user.WatchCount, + totalCount: totalCount, + } + decision := decideFreeWatchConsume(state) + if decision.isCan && !decision.shouldConsume { + return watchConsumeResponse(decision, totalCount, false), nil + } + + viewedToday, err := useractmod.IsViewTodayByNoVipTrans(nil, uid, oid, day) + if err != nil { + return vidmod.WatchConsumeResp{}, fmt.Errorf("find daily watch record: %w", err) + } + state.viewedToday = viewedToday + decision = decideFreeWatchConsume(state) + if !decision.shouldConsume { + return watchConsumeResponse(decision, totalCount, false), nil + } + + return consumeFreeWatchTransaction(uid, oid, video, day, at, totalCount) +} + +func consumeFreeWatchTransaction( + uid uint64, + oid primitive.ObjectID, + video vidmod.VideoModel, + day time.Time, + at time.Time, + totalCount uint64, +) (vidmod.WatchConsumeResp, error) { + if appg.VideoDB == nil { + return vidmod.WatchConsumeResp{}, errors.New("video database is unavailable") + } + + var resp vidmod.WatchConsumeResp + transaction := func(t *db.MongoTool) error { + // The transaction callback may be retried, so reset its result each time. + resp = vidmod.WatchConsumeResp{TotalWatchCount: totalCount} + + user, err := usermod.FindUserByUIDTrans(t, uid) + if err != nil { + return fmt.Errorf("find user in transaction: %w", err) + } + if user == nil { + return errors.New("find user in transaction: empty user") + } + viewedToday, err := useractmod.IsViewTodayByNoVipTrans(t, uid, oid, day) + if err != nil { + return fmt.Errorf("find daily watch record in transaction: %w", err) + } + decision := decideFreeWatchConsume(freeWatchConsumeState{ + isVIP: !user.VipExpireDate.Before(at), + isPublisher: video.PublisherID == uid, + freeArea: video.FreeArea, + paidVideo: video.Coins > 0, + viewedToday: viewedToday, + watchCount: user.WatchCount, + totalCount: totalCount, + }) + if !decision.shouldConsume { + resp = watchConsumeResponse(decision, totalCount, false) + return nil + } + + if err := useractmod.UserActInsertTrans(t, useractmod.UserAct{ + VID: oid, + PlayWay: useractmod.IsNoVip, + UID: uid, + DailyDate: day, + CreatedAt: at, + ConsumeKey: freeWatchConsumeKey(uid, oid, day), + }); err != nil { + return err + } + + remaining := decision.watchCount - 1 + updated, err := usermod.UpdateTrans(t, uid, usermod.UserSelector{WatchCount: &remaining}) + if err != nil || updated == nil { + return fmt.Errorf("decrement watch count: %w", err) + } + resp = vidmod.WatchConsumeResp{ + IsCan: true, + WatchCount: remaining, + TotalWatchCount: totalCount, + Consumed: true, + } + return nil + } + + err := runFreeWatchTransactionWithRetry(func() error { + return appg.VideoDB.Trans(transaction) + }) + if err == nil { + if resp.Consumed { + // UpdateTrans clears cache inside the transaction; clear once more after + // commit so a concurrent cache refill cannot retain the old count. + usermod.RefreshCache(uid) + } + return resp, nil + } + if !stderr.IsEqual(err, stderr.InsertExistError) { + return vidmod.WatchConsumeResp{}, err + } + + // A concurrent request inserted the same daily marker first. + user, findErr := usermod.FindUserByUIDForNoCache(uid) + if findErr != nil { + return vidmod.WatchConsumeResp{}, fmt.Errorf("find user after duplicate consume: %w", findErr) + } + if user == nil { + return vidmod.WatchConsumeResp{}, errors.New("find user after duplicate consume: empty user") + } + decision := freeWatchConsumeDecision{ + isCan: true, + watchCount: clampWatchCount(user.WatchCount, totalCount), + } + return watchConsumeResponse(decision, totalCount, false), nil +} + +func runFreeWatchTransactionWithRetry(run func() error) error { + var err error + for attempt := 0; attempt < freeWatchTransactionRetryLimit; attempt++ { + err = run() + if err == nil || + stderr.IsEqual(err, stderr.InsertExistError) || + !isRetryableFreeWatchTransactionError(err) { + return err + } + // Each run starts a fresh Mongo session. The shared transaction helper + // cannot reliably retry wrapped transient errors in-place. + time.Sleep(time.Duration(attempt+1) * 10 * time.Millisecond) + } + return err +} + +func isRetryableFreeWatchTransactionError(err error) bool { + if err == nil { + return false + } + var serverErr mongo.ServerError + if !errors.As(err, &serverErr) { + return false + } + return serverErr.HasErrorLabel("TransientTransactionError") || + serverErr.HasErrorCode(112) || // WriteConflict + serverErr.HasErrorCode(244) || // TransactionAborted + serverErr.HasErrorCode(251) // NoSuchTransaction +} + +func watchConsumeResponse(decision freeWatchConsumeDecision, totalCount uint64, consumed bool) vidmod.WatchConsumeResp { + return vidmod.WatchConsumeResp{ + IsCan: decision.isCan, + WatchCount: decision.watchCount, + TotalWatchCount: totalCount, + Consumed: consumed, + } +} + +func clampWatchCount(watchCount, totalCount uint64) uint64 { + if watchCount > totalCount { + return totalCount + } + return watchCount +} + +func freeWatchConsumeKey(uid uint64, oid primitive.ObjectID, day time.Time) string { + return strconv.FormatUint(uid, 10) + ":" + oid.Hex() + ":" + strconv.FormatInt(day.Unix(), 10) +} diff --git a/app/service/vidser/free_watch_consume_test.go b/app/service/vidser/free_watch_consume_test.go new file mode 100644 index 0000000..a7d321a --- /dev/null +++ b/app/service/vidser/free_watch_consume_test.go @@ -0,0 +1,205 @@ +package vidser + +import ( + "errors" + "fmt" + "testing" + "time" + + "91porn-server/common/stderr" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +func TestDecideFreeWatchConsume(t *testing.T) { + tests := []struct { + name string + state freeWatchConsumeState + wantCan bool + wantConsume bool + wantRemaining uint64 + }{ + { + name: "eligible video consumes", + state: freeWatchConsumeState{watchCount: 3, totalCount: 3}, + wantCan: true, + wantConsume: true, + wantRemaining: 3, + }, + { + name: "already viewed is idempotent", + state: freeWatchConsumeState{viewedToday: true, watchCount: 2, totalCount: 3}, + wantCan: true, + wantRemaining: 2, + }, + { + name: "vip does not consume", + state: freeWatchConsumeState{isVIP: true, watchCount: 2, totalCount: 3}, + wantCan: true, + wantRemaining: 2, + }, + { + name: "publisher does not consume", + state: freeWatchConsumeState{isPublisher: true, watchCount: 2, totalCount: 3}, + wantCan: true, + wantRemaining: 2, + }, + { + name: "free area does not consume", + state: freeWatchConsumeState{freeArea: true, watchCount: 2, totalCount: 3}, + wantCan: true, + wantRemaining: 2, + }, + { + name: "coin video does not consume", + state: freeWatchConsumeState{paidVideo: true, watchCount: 2, totalCount: 3}, + wantCan: true, + wantRemaining: 2, + }, + { + name: "no remaining count denies new video", + state: freeWatchConsumeState{watchCount: 0, totalCount: 3}, + wantCan: false, + }, + { + name: "count is clamped to configured total", + state: freeWatchConsumeState{watchCount: 8, totalCount: 3}, + wantCan: true, + wantConsume: true, + wantRemaining: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := decideFreeWatchConsume(tt.state) + if got.isCan != tt.wantCan || got.shouldConsume != tt.wantConsume || got.watchCount != tt.wantRemaining { + t.Fatalf("decision = %+v, want isCan=%v shouldConsume=%v watchCount=%d", + got, tt.wantCan, tt.wantConsume, tt.wantRemaining) + } + }) + } +} + +func TestFreeWatchConsumeKeyScope(t *testing.T) { + oid := primitive.NewObjectID() + day := time.Date(2026, 7, 28, 0, 0, 0, 0, time.Local) + key := freeWatchConsumeKey(1001, oid, day) + if key != freeWatchConsumeKey(1001, oid, day) { + t.Fatal("same user, video and day must produce the same consume key") + } + if key == freeWatchConsumeKey(1002, oid, day) { + t.Fatal("consume key must be scoped by user") + } + if key == freeWatchConsumeKey(1001, primitive.NewObjectID(), day) { + t.Fatal("consume key must be scoped by video") + } + if key == freeWatchConsumeKey(1001, oid, day.AddDate(0, 0, 1)) { + t.Fatal("consume key must be scoped by day") + } +} + +func TestIsRetryableFreeWatchTransactionError(t *testing.T) { + tests := []struct { + name string + err error + want bool + }{ + { + name: "wrapped transient label", + err: fmt.Errorf("decrement watch count: %w", mongo.CommandError{ + Code: 112, + Labels: []string{"TransientTransactionError"}, + }), + want: true, + }, + { + name: "write conflict code", + err: mongo.CommandError{Code: 112}, + want: true, + }, + { + name: "write exception transient label", + err: mongo.WriteException{ + Labels: []string{"TransientTransactionError"}, + }, + want: true, + }, + { + name: "no such transaction code", + err: fmt.Errorf("retry transaction: %w", mongo.CommandError{Code: 251}), + want: true, + }, + { + name: "ordinary database error", + err: errors.New("database unavailable"), + }, + { + name: "nil", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isRetryableFreeWatchTransactionError(tt.err); got != tt.want { + t.Fatalf("isRetryableFreeWatchTransactionError() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRunFreeWatchTransactionWithRetry(t *testing.T) { + transient := fmt.Errorf("wrapped write conflict: %w", mongo.CommandError{ + Code: 112, + Labels: []string{"TransientTransactionError"}, + }) + + t.Run("eventually succeeds", func(t *testing.T) { + calls := 0 + err := runFreeWatchTransactionWithRetry(func() error { + calls++ + if calls < 3 { + return transient + } + return nil + }) + if err != nil || calls != 3 { + t.Fatalf("err = %v, calls = %d, want nil and 3 calls", err, calls) + } + }) + + t.Run("stops at retry limit", func(t *testing.T) { + calls := 0 + err := runFreeWatchTransactionWithRetry(func() error { + calls++ + return transient + }) + if err == nil || calls != freeWatchTransactionRetryLimit { + t.Fatalf("err = %v, calls = %d, want error and %d calls", + err, calls, freeWatchTransactionRetryLimit) + } + }) + + t.Run("duplicate is handled by caller without retry", func(t *testing.T) { + calls := 0 + err := runFreeWatchTransactionWithRetry(func() error { + calls++ + return stderr.InsertExistError + }) + if !stderr.IsEqual(err, stderr.InsertExistError) || calls != 1 { + t.Fatalf("err = %v, calls = %d, want duplicate and 1 call", err, calls) + } + }) + + t.Run("ordinary error is not retried", func(t *testing.T) { + calls := 0 + ordinary := errors.New("database unavailable") + err := runFreeWatchTransactionWithRetry(func() error { + calls++ + return ordinary + }) + if !errors.Is(err, ordinary) || calls != 1 { + t.Fatalf("err = %v, calls = %d, want ordinary error and 1 call", err, calls) + } + }) +} diff --git a/app/service/vidser/library.go b/app/service/vidser/library.go new file mode 100644 index 0000000..9388025 --- /dev/null +++ b/app/service/vidser/library.go @@ -0,0 +1,409 @@ +package vidser + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/vidhelpser" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models/commod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/mediatagmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/vidmod" + "fmt" + "math/rand" + "sync" + "time" + + "github.com/vmihailenco/msgpack/v5" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func NewsTypeToSting(in string) string { + var libraryTypeName string + switch in { + case constant.SP: + libraryTypeName = "影片" + case constant.SHORT: + libraryTypeName = "抖音" + case constant.Cartoon: + libraryTypeName = "动漫" + case constant.Comics: + libraryTypeName = "漫画" + case constant.PIC: + libraryTypeName = "图集" + case constant.COVER: + libraryTypeName = "帖子" + } + return libraryTypeName +} + +// GetLibrary 获取片库详情 +func GetLibrary(uid uint64) (res vidmod.LibraryData, err error) { + var data vidmod.LibraryData + + // 获取缓存 + str, err := appg.Redis.Get(redisconst.VideoLibraryCache) + if err != nil { + log.Warn(fmt.Sprintf("uid:%v;缓存获取片库信息异常:%v", uid, err)) + } + if str != nil { + if err = msgpack.Unmarshal([]byte(*str), &data); err == nil { + return data, nil + } + log.Warn(fmt.Sprintf("uid:%v;解析片库缓存数据异常:%v", uid, err)) + } + data.OrderBy = append(data.OrderBy, vidmod.SortKey{ + Key: "new", + Name: "最新上架", + }, vidmod.SortKey{ + Key: "playNum", + Name: "最多观看", + }, vidmod.SortKey{ + Key: "love", + Name: "最多收藏", + }) + + data.Canvas = append(data.Canvas, vidmod.SortKey{ + Key: constant.SP, + Name: NewsTypeToSting(constant.SP), + }, vidmod.SortKey{ + Key: constant.SHORT, + Name: NewsTypeToSting(constant.SHORT), + }, vidmod.SortKey{ + Key: constant.Cartoon, + Name: NewsTypeToSting(constant.Cartoon), + }, vidmod.SortKey{ + Key: constant.Comics, + Name: NewsTypeToSting(constant.Comics), + }, vidmod.SortKey{ + Key: constant.PIC, + Name: NewsTypeToSting(constant.PIC), + }, vidmod.SortKey{ + Key: constant.COVER, + Name: NewsTypeToSting(constant.COVER), + }) + + data.PaymentType = append(data.PaymentType, vidmod.SortKey{ + Key: "", + Name: "全部", + }, vidmod.SortKey{ + Key: "vip", + Name: "VIP", + }, vidmod.SortKey{ + Key: "point", + Name: "金币", + }) + + data.TimeType = append( + data.TimeType, vidmod.SortKey{ + Key: "1", + Name: "本月", + }, vidmod.SortKey{ + Key: "2", + Name: "三个月内", + }, vidmod.SortKey{ + Key: "3", + Name: "半年内", + }, vidmod.SortKey{ + Key: "4", + Name: "更久", + }) + + data.VidTags = append(data.VidTags, vidmod.Tag{ + ID: "", + Name: "全部类型", + }) + data.ACGTags = append(data.ACGTags, vidmod.Tag{ + ID: "", + Name: "全部类型", + }) + + wg := sync.WaitGroup{} + wg.Add(2) + common.Go(func() { + defer wg.Done() + // 获取最热标签 + tags, _, err := tagmod.GetLibraryTagList(commod.Page{PageNumber: 1, PageSize: 20}) + if err != nil { + return + } + + if len(tags) > 0 { + for _, t := range tags { + data.VidTags = append(data.VidTags, vidmod.Tag{ + ID: t.ID.Hex(), + Name: t.TagName, + }) + } + } + }) + + common.Go(func() { + defer wg.Done() + // 获取ACG所有推荐标签 + tagFilter := bson.M{"active": true, "isDelete": false, "isDiscovery": true} + tagOp := options.Find().SetLimit(20).SetSort(bson.D{{Key: "sort", Value: -1}}) + mediaTags, err := mediatagmod.QueryAllList(tagFilter, tagOp) + if err != nil { + return + } + + if len(mediaTags) > 0 { + for _, t := range mediaTags { + data.ACGTags = append(data.ACGTags, vidmod.Tag{ + ID: t.ID.Hex(), + Name: t.Name, + }) + } + } + }) + wg.Wait() + + common.Go(func() { + // 加入缓存 随机1-3分钟缓存 + random := rand.Intn(120) + 60 + d, err := msgpack.Marshal(data) + if err != nil { + return + } + if err = appg.Redis.Set(redisconst.VideoLibraryCache, d, time.Duration(random)*time.Second); err != nil { + log.Warn(fmt.Sprintf("uid:%v, 保存缓存数据异常:%v", uid, err)) + } + }) + return data, nil +} + +type LibraryElasticSearchRequest struct { + Keyword LibraryReq `json:"keyword" bson:"keyword" binding:"required"` // 关键词 + UID uint64 `json:"-"` // 用户ID + commod.Page +} + +type LibraryElasticSearchResponse struct { + List []vidmod.ESVideo `json:"list"` // list + HasNext bool `json:"hasNext"` // hasNext + Total int `json:"total"` // total +} + +type LibraryReq struct { + Canvas SortKey `json:"canvas" bson:"canvas"` // 视频分类 + OrderBy SortKey `json:"orderBy" bson:"orderBy"` // 视频排序 + Tags SearchTag `json:"tags" bson:"tags"` // 全部标签 + PaymentType SortKey `json:"paymentType" bson:"paymentType"` // 付费分类 + TimeType SortKey `json:"timeType" bson:"timeType"` // 时间排序 +} + +type LibraryData struct { + Canvas []SortKey `json:"canvas" bson:"canvas"` // 视频分类 + OrderBy []SortKey `json:"orderBy" bson:"orderBy"` // 视频排序 + VidTags []SearchTag `json:"vidTags" bson:"vidTags"` // 全部视频标签 + ACGTags []SearchTag `json:"acgTags" bson:"acgTags"` // 全部ACG标签 + PaymentType []SortKey `json:"paymentType" bson:"paymentType"` // 付费分类 + TimeType []SortKey `json:"timeType" bson:"timeType"` // 时间排序 +} + +type SortKey struct { + Key string `json:"key" bson:"key"` // 健值 + Name string `json:"name" bson:"name"` // 健名称 +} + +type SearchTag struct { + ID string `json:"id" bson:"id"` // 标签ID + Name string `json:"name" bson:"name"` // 标签名称 +} + +type AppElasticSearchLibraryResponse struct { + List []*vidmod.VideoInfoResp `json:"list" bson:"list"` // 视频列表 + AllMediaList []*mediamod.AppMediaBase `json:"allMediaList" bson:"allMediaList"` // ACG列表 + HasNext bool `json:"hasNext" bson:"hasNext"` // 是否下一页 + Total int `json:"total" bson:"total"` // 总数 +} + +func (in *LibraryElasticSearchRequest) CheckSearchType() (AppElasticSearchLibraryResponse, error) { + var ( + data AppElasticSearchLibraryResponse + err error + ) + + switch in.Keyword.Canvas.Key { + case constant.SP, constant.SHORT, constant.PIC, constant.COVER: + data, err = in.VidLibrarySearch() + case constant.Cartoon, constant.Comics: + data, err = in.MediaLibrarySearch() + } + if err != nil { + return data, err + } + return data, nil +} + +func (in *LibraryElasticSearchRequest) VidLibrarySearch() (AppElasticSearchLibraryResponse, error) { + resp := AppElasticSearchLibraryResponse{} + from := in.Skip64() + size := in.Limit64() + + must := elastic.A{ + {"term": elastic.M{"newsType.keyword": in.Keyword.Canvas.Key}}, + } + if in.Keyword.Tags.ID != "" { + tag := elastic.M{"term": elastic.M{"tags.keyword": in.Keyword.Tags.ID}} + must = append(must, tag) + } + timeType := HandleTimeType(in.Keyword.TimeType.Key) + must = append(must, timeType...) + + payType := HandlePayType(in.Keyword.PaymentType.Key) + must = append(must, payType...) + short := HandleShort(in.Keyword.OrderBy.Key) + // 根据数据结构组装数据 + query := elastic.M{ + "query": elastic.M{ + "bool": elastic.M{ + "must": must, + }, + }, + "sort": short, + "from": from, + "size": size + 1, + } + + res, err := vidmod.SearchByCondWithTotal(query) + if err != nil { + return resp, nil + } + + hasNext := false + videosIDs := make([]primitive.ObjectID, 0) + for _, v := range res.Hits { + videosIDs = append(videosIDs, v.Source.ID) + } + + if len(videosIDs) > int(size) { + hasNext = true + videosIDs = videosIDs[:size] + } + + vidMods, err := vidmod.GetVideoListByIDs(videosIDs) + if err != nil { + return resp, nil + } + + var ( + sortVidModels = make([]*vidmod.VideoModel, 0) + vidMap = make(map[primitive.ObjectID]*vidmod.VideoModel) + ) + for _, videoModel := range vidMods { + vidMap[videoModel.ID] = videoModel + } + for _, vid := range videosIDs { + if v, ok := vidMap[vid]; ok { + sortVidModels = append(sortVidModels, v) + } + } + data := vidhelpser.NewEncodeVideoInfoNotStatusForSearch(in.UID, sortVidModels) + + resp.List = data + resp.HasNext = hasNext + resp.Total = res.Total.Value + + // 查询结果数量出现不一致,说名有的帖子不存在了,需要删除 + if len(videosIDs) > 0 && len(videosIDs) != len(vidMods) { + common.Go(func() { + HandleDiffVideo(vidMap, videosIDs) + }) + } + + return resp, nil +} + +func HandleDiffVideo(vidMods map[primitive.ObjectID]*vidmod.VideoModel, videosIDs []primitive.ObjectID) { + var source = elastic.M{} + var diffVid []string + for _, vid := range videosIDs { + if _, ok := vidMods[vid]; !ok { + source[vid.Hex()] = vid.Hex() + diffVid = append(diffVid, vid.Hex()) + } + } + if len(source) > 0 { + log.Info(fmt.Sprintf("search delete videosIDs:%v", diffVid)) + if err := vidmod.DeleteByCond(source); err != nil { + log.Info(fmt.Sprintf("Elastic del data failed, err:%v", err)) + return + } + log.Info(fmt.Sprintf("Elastic del data succsss, count:%v", len(source))) + } +} + +func HandleTimeType(key string) elastic.A { + var timeTypeValue = elastic.A{} + now := time.Now() + switch key { + case "1": // 本月 + timeTypeValue = append(timeTypeValue, + elastic.M{"range": elastic.M{"reviewAt": elastic.M{"gt": now.AddDate(0, -1, 0)}}}, + ) + case "2": // 3月内 + timeTypeValue = append(timeTypeValue, + elastic.M{"range": elastic.M{"reviewAt": elastic.M{"gt": now.AddDate(0, -3, 0), "lt": now.AddDate(0, -1, 0)}}}, + ) + case "3": // 半年内 + timeTypeValue = append(timeTypeValue, + elastic.M{"range": elastic.M{"reviewAt": elastic.M{"gt": now.AddDate(0, -6, 0), "lt": now.AddDate(0, -3, 0)}}}, + ) + case "4": // 更久 + timeTypeValue = append(timeTypeValue, + elastic.M{"range": elastic.M{"reviewAt": elastic.M{"gt": now.AddDate(-5, 0, 0), "lt": now.AddDate(0, -6, 0)}}}, + ) + default: + timeTypeValue = append(timeTypeValue, + elastic.M{"range": elastic.M{"reviewAt": elastic.M{"gt": now.AddDate(-3, -2, 0)}}}, + ) + } + return timeTypeValue +} + +func HandlePayType(key string) elastic.A { + var payTypeValue = elastic.A{} + switch key { + case "free": + payTypeValue = append(payTypeValue, + elastic.M{"term": elastic.M{"status": 3}}, + elastic.M{"term": elastic.M{"coins": 0}}, + ) + case "vip": + payTypeValue = append(payTypeValue, + elastic.M{"term": elastic.M{"status": 1}}, + elastic.M{"term": elastic.M{"coins": 0}}, + ) + case "point": + payTypeValue = append(payTypeValue, + elastic.M{"terms": elastic.M{"status": []int{1, 3}}}, + elastic.M{"range": elastic.M{"coins": elastic.M{"gt": 0}}}, + ) + default: + payTypeValue = append(payTypeValue, elastic.M{"terms": elastic.M{"status": []int{1, 3}}}) + } + return payTypeValue +} + +func HandleShort(key string) elastic.A { + var payTypeValue elastic.A + switch key { + case "playNum": + payTypeValue = elastic.A{{"playCount": elastic.M{"order": "desc"}}} + case "love": + payTypeValue = elastic.A{{"collectCount": elastic.M{"order": "desc"}}} + case "new": + payTypeValue = elastic.A{{"reviewAt": elastic.M{"order": "desc"}}} + default: + payTypeValue = elastic.A{{"fakeLikeCount": elastic.M{"order": "desc"}}, {"reviewAt": elastic.M{"order": "desc"}}} + } + return payTypeValue +} diff --git a/app/service/vidser/m3u8sign.go b/app/service/vidser/m3u8sign.go new file mode 100644 index 0000000..64e351e --- /dev/null +++ b/app/service/vidser/m3u8sign.go @@ -0,0 +1,26 @@ +package vidser + +import "91porn-server/models/v/vidmod" + +// 本文件实现 vidser 视频响应体的 vidmod.M3u8Signable,供 m3u8ticket 零反射签票。 +// 目前仅 /api/app/vid/module/:subModuleID (ModuleVideoList) 需要,故只保留其响应体。 + +// SignM3u8 亚模块下视频:直挂视频、精选视频,以及各专题(AllSection)内的视频都签票。 +func (r VideoUnderSubModuleResp) SignM3u8(s vidmod.M3u8Signer) { + vidmod.SignM3u8Resps(s, r.AllVideoInfo) + vidmod.SignM3u8Resps(s, r.ChosenVideoInfo) + for i := range r.AllSection { + r.AllSection[i].SignM3u8(s) + } +} + +// SignM3u8 专题下视频列表(由 VideoUnderSubModuleResp 内嵌调用)。 +func (r Section) SignM3u8(s vidmod.M3u8Signer) { + vidmod.SignM3u8Resps(s, r.AllVideoInfo) +} + +// 编译期断言。 +var ( + _ vidmod.M3u8Signable = VideoUnderSubModuleResp{} + _ vidmod.M3u8Signable = Section{} +) diff --git a/app/service/vidser/m3u8sign_test.go b/app/service/vidser/m3u8sign_test.go new file mode 100644 index 0000000..821ba7a --- /dev/null +++ b/app/service/vidser/m3u8sign_test.go @@ -0,0 +1,29 @@ +package vidser + +import ( + "testing" + + "91porn-server/models/v/vidmod" +) + +type countingSigner struct{ n int } + +func (c *countingSigner) SignM3u8URL(field *string, playable, preview bool) { c.n++ } + +// 验证亚模块响应体不仅签直挂/精选视频,还会下钻到各专题(AllSection)内的视频—— +// 这正是旧反射兜底会覆盖、改接口后最易漏掉的嵌套层级。 +func TestVideoUnderSubModuleRespSignsNestedSections(t *testing.T) { + c := &countingSigner{} + resp := VideoUnderSubModuleResp{ + AllVideoInfo: []*vidmod.VideoInfoResp{{}}, + ChosenVideoInfo: []*vidmod.VideoInfoResp{{}}, + AllSection: []Section{ + {AllVideoInfo: []*vidmod.VideoInfoResp{{}, {}}}, + }, + } + resp.SignM3u8(c) + // (1 直挂 + 1 精选)*2 字段 + (2 专题内)*2 字段 = 8 + if c.n != 8 { + t.Fatalf("VideoUnderSubModuleResp should offer 8 url fields incl nested sections, got %d", c.n) + } +} diff --git a/app/service/vidser/media_library.go b/app/service/vidser/media_library.go new file mode 100644 index 0000000..ca4236c --- /dev/null +++ b/app/service/vidser/media_library.go @@ -0,0 +1,154 @@ +package vidser + +import ( + "91porn-server/app/service/mediaser" + "91porn-server/common/constant" + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models/v/mediamod" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +func (in *LibraryElasticSearchRequest) MediaLibrarySearch() (AppElasticSearchLibraryResponse, error) { + resp := AppElasticSearchLibraryResponse{} + from := int64((in.PageNumber - 1) * (in.PageSize)) + size := int64(in.PageSize + 1) + + must := elastic.A{ + {"term": elastic.M{"mediaType.keyword": HandleMediaCanvasType(in.Keyword.Canvas.Key)}}, + } + if in.Keyword.Tags.ID != "" { + tag := elastic.M{"term": elastic.M{"tags.keyword": in.Keyword.Tags.ID}} + must = append(must, tag) + } + timeType := HandleMediaTimeType(in.Keyword.TimeType.Key) + must = append(must, timeType...) + + payType := HandleMediaPayType(in.Keyword.PaymentType.Key) + must = append(must, payType...) + short := HandleMediaShort(in.Keyword.OrderBy.Key) + // 根据数据结构组装数据 + query := elastic.M{ + "query": elastic.M{ + "bool": elastic.M{ + "must": must, + }, + }, + "sort": short, + "from": from, + "size": size, + } + + res, err := mediamod.SearchByCondWithTotal(query) + if err != nil { + return resp, nil + } + + if uint64(len(res.Hits)) > in.PageSize { + res.Hits = res.Hits[:in.PageSize] + resp.HasNext = true + } + mIds := make([]primitive.ObjectID, len(res.Hits)) + for i, v := range res.Hits { + if !v.Source.ID.IsZero() { + mIds[i] = v.Source.ID + } + } + // 漫画、动漫、文字小说、有声小说 + filter := bson.M{"_id": bson.M{"$in": mIds}, "status": 1} + op := options.Find().SetSort(bson.D{{Key: "sectionSort", Value: -1}, {Key: "createdAt", Value: -1}}) + mediaBases, err := mediamod.QueryMediaByCond(filter, op) + if err != nil { + log.Error(fmt.Sprintf("mediaLibrarySearch mediamod.QueryMediaByCond err:%v", err)) + return resp, err + } + + if len(mediaBases) == 0 { + log.Warn("mediaLibrarySearch no video info") + return resp, nil + } + + ret := mediaser.FillMedias(mediaBases, in.UID, true) + resp.AllMediaList = ret + resp.Total = res.Total.Value + return resp, nil +} + +func HandleMediaTimeType(key string) elastic.A { + var timeTypeValue = elastic.A{} + now := time.Now() + switch key { + case "1": + timeTypeValue = append(timeTypeValue, + elastic.M{"range": elastic.M{"createdAt": elastic.M{"gt": now.AddDate(0, -1, 0)}}}, + ) + case "2": + timeTypeValue = append(timeTypeValue, + elastic.M{"range": elastic.M{"createdAt": elastic.M{"gt": now.AddDate(0, -3, 0), "lt": now.AddDate(0, -1, 0)}}}, + ) + case "3": + timeTypeValue = append(timeTypeValue, + elastic.M{"range": elastic.M{"createdAt": elastic.M{"gt": now.AddDate(0, -6, 0), "lt": now.AddDate(0, -3, 0)}}}, + ) + case "4": + timeTypeValue = append(timeTypeValue, + elastic.M{"range": elastic.M{"createdAt": elastic.M{"gt": now.AddDate(-5, 0, 0), "lt": now.AddDate(0, -6, 0)}}}, + ) + default: + timeTypeValue = append(timeTypeValue, + elastic.M{"range": elastic.M{"createdAt": elastic.M{"gt": now.AddDate(-3, -2, 0)}}}, + ) + } + return timeTypeValue +} + +func HandleMediaPayType(key string) elastic.A { + var payTypeValue = elastic.A{} + switch key { + case "free": + payTypeValue = append(payTypeValue, + elastic.M{"term": elastic.M{"permission": 2}}, + ) + case "vip": + payTypeValue = append(payTypeValue, + elastic.M{"term": elastic.M{"permission": 0}}, + ) + case "point": + payTypeValue = append(payTypeValue, + elastic.M{"term": elastic.M{"permission": 1}}, + ) + default: + payTypeValue = append(payTypeValue, elastic.M{"terms": elastic.M{"permission": []int{0, 1, 2}}}) + } + return payTypeValue +} + +func HandleMediaShort(key string) elastic.A { + var payTypeValue elastic.A + switch key { + case "playNum": + payTypeValue = elastic.A{{"countBrowse": elastic.M{"order": "desc"}}} + case "love": + payTypeValue = elastic.A{{"countCollect": elastic.M{"order": "desc"}}} + case "new": + payTypeValue = elastic.A{{"createdAt": elastic.M{"order": "desc"}}} + default: + payTypeValue = elastic.A{{"countBrowse": elastic.M{"order": "desc"}}, {"createdAt": elastic.M{"order": "desc"}}} + } + return payTypeValue +} + +func HandleMediaCanvasType(key string) string { + var moduleTypeValue string + switch key { + case constant.Cartoon, constant.Comics: + moduleTypeValue = key + default: + moduleTypeValue = constant.Text + } + return moduleTypeValue +} diff --git a/app/service/vidser/module_refresh.go b/app/service/vidser/module_refresh.go new file mode 100644 index 0000000..7d99508 --- /dev/null +++ b/app/service/vidser/module_refresh.go @@ -0,0 +1,100 @@ +package vidser + +import ( + "bytes" + "crypto/sha256" + "errors" + "sort" + "time" + + "91porn-server/app/service/vidhelpser" + "91porn-server/models/commod" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var ErrRandomRefreshUnsupported = errors.New("the selected sort does not support random refresh") + +func RefreshModuleVideos(uid uint64, subModuleID primitive.ObjectID, req RefreshModuleVideosReq) (RefreshModuleVideosResp, error) { + resp := RefreshModuleVideosResp{ + AllVideoInfo: []*vidmod.VideoInfoResp{}, + RefreshMode: moduleconfmod.RefreshModeRandomTopN, + RefreshToken: req.RefreshToken, + } + subModule, err := moduleconfmod.GetByID(subModuleID) + if err != nil { + return resp, err + } + if !subModule.IsActiveAt(time.Now()) { + return resp, nil + } + subModule.HaiJiaoStyle.EnsureSortRules() + candidateN := 0 + for _, rule := range subModule.HaiJiaoStyle.SortRules { + if rule.Val == commod.SortType(req.ModuleSort) && rule.RefreshMode == moduleconfmod.RefreshModeRandomTopN { + candidateN = rule.RandomCandidateN + break + } + } + if candidateN == 0 { + return resp, ErrRandomRefreshUnsupported + } + + pageSize := req.PageSize + if pageSize <= 0 { + pageSize = 20 + } + if pageSize > candidateN { + pageSize = candidateN + } + filter := bson.M{ + "mId": subModuleID.Hex(), + "status": vidmod.CheckPass, + "newsType": vidmod.SP, + "chosen": false, + } + opt := options.Find(). + SetSort(bson.D{ + {Key: "liaoBaTopSort", Value: -1}, + {Key: "likeCount", Value: -1}, + {Key: "reviewAt", Value: -1}, + {Key: "_id", Value: -1}, + }). + SetLimit(int64(candidateN)). + SetProjection(bson.M{"richText": 0}) + videos, err := vidmod.GetVideoListByCond(filter, opt) + if err != nil { + return resp, err + } + resp.CandidateCount = len(videos) + deterministicOrderVideos(videos, subModuleID.Hex()+":"+req.RefreshToken) + if len(videos) > pageSize { + videos = videos[:pageSize] + } + resp.HasNext = hasNextRefreshCandidate(resp.CandidateCount, len(videos)) + resp.AllVideoInfo = vidhelpser.NewEncodeVideoInfoNotStatusForUser(uid, videos) + if resp.AllVideoInfo == nil { + resp.AllVideoInfo = []*vidmod.VideoInfoResp{} + } + return resp, nil +} + +func hasNextRefreshCandidate(candidateCount, returnedCount int) bool { + return candidateCount > returnedCount +} + +func deterministicOrderVideos(videos []*vidmod.VideoModel, seed string) { + sort.SliceStable(videos, func(i, j int) bool { + left := sha256.Sum256([]byte(seed + ":" + videos[i].ID.Hex())) + right := sha256.Sum256([]byte(seed + ":" + videos[j].ID.Hex())) + cmp := bytes.Compare(left[:], right[:]) + if cmp == 0 { + return videos[i].ID.Hex() < videos[j].ID.Hex() + } + return cmp < 0 + }) +} diff --git a/app/service/vidser/module_refresh_test.go b/app/service/vidser/module_refresh_test.go new file mode 100644 index 0000000..105cf92 --- /dev/null +++ b/app/service/vidser/module_refresh_test.go @@ -0,0 +1,55 @@ +package vidser + +import ( + "reflect" + "testing" + + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestDeterministicOrderVideos(t *testing.T) { + source := []*vidmod.VideoModel{ + {ID: primitive.NewObjectID()}, + {ID: primitive.NewObjectID()}, + {ID: primitive.NewObjectID()}, + {ID: primitive.NewObjectID()}, + } + first := append([]*vidmod.VideoModel(nil), source...) + retry := append([]*vidmod.VideoModel(nil), source...) + deterministicOrderVideos(first, "module:token-a") + deterministicOrderVideos(retry, "module:token-a") + if !reflect.DeepEqual(videoIDs(first), videoIDs(retry)) { + t.Fatal("same refresh token must return the same order") + } +} + +func TestHasNextRefreshCandidate(t *testing.T) { + tests := []struct { + name string + candidateCount int + returnedCount int + want bool + }{ + {name: "candidate pool has remaining videos", candidateCount: 30, returnedCount: 20, want: true}, + {name: "candidate pool exactly fits response", candidateCount: 20, returnedCount: 20, want: false}, + {name: "candidate pool is smaller than page size", candidateCount: 12, returnedCount: 12, want: false}, + {name: "empty candidate pool", candidateCount: 0, returnedCount: 0, want: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasNextRefreshCandidate(tt.candidateCount, tt.returnedCount); got != tt.want { + t.Fatalf("hasNextRefreshCandidate(%d, %d) = %t, want %t", tt.candidateCount, tt.returnedCount, got, tt.want) + } + }) + } +} + +func videoIDs(videos []*vidmod.VideoModel) []primitive.ObjectID { + ids := make([]primitive.ObjectID, len(videos)) + for i := range videos { + ids[i] = videos[i].ID + } + return ids +} diff --git a/app/service/vidser/ranking.go b/app/service/vidser/ranking.go new file mode 100644 index 0000000..a58cd09 --- /dev/null +++ b/app/service/vidser/ranking.go @@ -0,0 +1,236 @@ +package vidser + +import ( + "91porn-server/app/appg" + "91porn-server/app/service/vidhelpser" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/timeutil" + topser "91porn-server/common/top" + topasist "91porn-server/common/top/asistant" + "91porn-server/common/top/dailytop" + "91porn-server/common/top/monthtop" + "91porn-server/common/top/weektop" + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/cache/viddata" + "91porn-server/models/commod" + "91porn-server/models/v/sysconfmod" + "91porn-server/models/v/vidmod" + "encoding/json" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +type RankingReq struct { + Type int `json:"type" form:"type"` // 1-日榜 2-周榜 3-月榜 4-总榜 5-年榜 + NewsType string `json:"newsType" form:"newsType"` // SP-长视频 SHORT-短视频 COVER-帖子 PIC-图集 HOT-热点榜单 + UID uint64 `json:"-" form:"-"` + commod.Page +} + +type RankingResp struct { + List []*vidmod.VideoInfo `json:"list"` + HasNext bool `json:"hasNext"` +} + +const ( + DailyRank = 1 // 日榜 + WeekRank = 2 // 周榜 + MonthRank = 3 // 周榜 + AllRank = 4 // 总榜 + YearRank = 5 // 年榜 +) + +func (q *RankingReq) List() (res RankingResp, err error) { + defer func() { + vidhelpser.ApplyFreeTrialBadgeToVideoInfos(vidhelpser.LoadFreeTrialBadgeContext(q.UID), res.List) + }() + if q.NewsType == "HOT" { + return q.getHotRank() + } + switch q.Type { + case DailyRank, WeekRank, MonthRank: + // 日榜 + res, err = q.getRank(q.Type, q.NewsType) + + case AllRank, YearRank: + res, err = q.getAllRank(q.NewsType, q.Type == YearRank) + } + return +} + +func (q *RankingReq) getHotRank() (res RankingResp, err error) { + // 获取配置的标签 + configure, _ := sysconfdata.GetAllFromCache() + tagStrList := configure.GetStrSlice(sysconfmod.VCodeHotRankingTags) + tagIds := []primitive.ObjectID{} + for _, v := range tagStrList { + tagId, _ := primitive.ObjectIDFromHex(v) + if tagId.IsZero() { + continue + } + tagIds = append(tagIds, tagId) + } + if len(tagIds) == 0 { + return + } + key := redisconst.GetHotRankingListCacheKey(tagIds, q.PageNumber, q.PageSize) + s, err := appg.Redis.Get(key) + if err != nil { + return res, err + } + if s != nil { + err = json.Unmarshal([]byte(*s), &res) + if err == nil { + return res, nil + } + } + defer func() { + b, err := json.Marshal(res) + if err != nil { + log.Error("video getRank json.Marshal(res) error:", log.E(err)) + return + } + ran := time.Duration(common.RandInt(20, 60) + 240) + appg.Redis.Set(key, string(b), time.Second*ran) + }() + + t := timeutil.BeginningOfDay(time.Now()) + filter := bson.M{ + "tags": bson.M{"$in": tagIds}, + "newsType": bson.M{"$in": []string{vidmod.SP, vidmod.SHORT}}, + "status": bson.M{"$in": []int{1, 3}}, + "reviewAt": bson.M{"$gt": t.AddDate(0, 0, -30)}, + } + sorts := bson.D{{Key: "liaoBaTopSort", Value: -1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}} + opts := options.Find().SetLimit(q.Limit64()).SetSkip(q.Skip64()).SetSort(sorts) + vidList, hasNext, err := vidmod.FindList(filter, opts) + if err != nil { + return + } + res.List = vidhelpser.EncodeVideoInfoNoUID(vidList) + res.HasNext = hasNext + + return res, nil +} + +func (q *RankingReq) getAllRank(newsType string, isYear bool) (res RankingResp, err error) { + key := redisconst.GetRankingListCacheKey(q.Type, newsType, q.PageSize, q.PageNumber) + s, err := appg.Redis.Get(key) + if err != nil { + return res, err + } + if s != nil { + err = json.Unmarshal([]byte(*s), &res) + if err == nil { + return res, nil + } + } + defer func() { + b, err := json.Marshal(res) + if err != nil { + log.Error("video getRank json.Marshal(res) error:", log.E(err)) + return + } + ran := time.Duration(common.RandInt(20, 60) + 60) + appg.Redis.Set(key, string(b), time.Second*ran) + }() + + t := timeutil.BeginningOfDay(time.Now()) + t = t.AddDate(-1, -2, 0) + if isYear { + t = t.AddDate(0, -6, 0) + } + + filter := bson.M{ + "newsType": newsType, + "status": bson.M{"$in": []int{1, 3}}, + "reviewAt": bson.M{"$gt": t}, + } + sorts := bson.D{{Key: "collectCount", Value: -1}, {Key: "reviewAt", Value: -1}} + + vidList, hasNext, err := viddata.GetListFromCache(filter, int64(q.Skip()), int64(q.Limit()), sorts) + if err != nil { + return + } + res.List = vidhelpser.EncodeVideoInfoNoUID(vidList) + res.HasNext = hasNext + + return res, nil +} + +func (q *RankingReq) getRank(rankinType int, newsType string) (res RankingResp, err error) { + + var list []string + var hasNext bool + switch rankinType { + case DailyRank: + list, _, hasNext = dailytop.GetTopByPage(topser.TypeVideo(newsType), int64(q.Skip()), int64(q.Limit())) + case WeekRank: + list, _, hasNext = weektop.GetTopByPage(topser.TypeVideo(newsType), int64(q.Skip()), int64(q.Limit())) + case MonthRank: + list, _, hasNext = monthtop.GetTopByPage(topser.TypeVideo(newsType), int64(q.Skip()), int64(q.Limit())) + default: + return + } + periodRankHasContent := len(list) > 0 + if !periodRankHasContent && q.Skip64() > 0 { + periodRankHasContent = q.periodRankHasContent(rankinType, newsType) + } + + ids := []primitive.ObjectID{} + for _, v := range list { + id, _ := primitive.ObjectIDFromHex(v) + if id.IsZero() { + continue + } + ids = append(ids, id) + } + // 通过id获取列表 + vidList, _, err := vidmod.FindList(bson.M{"_id": bson.M{"$in": ids}}, options.Find()) + if err != nil { + return + } + + vidMap := make(map[primitive.ObjectID]*vidmod.VideoModel) + var videoList []*vidmod.VideoModel + for _, v := range vidList { + vidMap[v.ID] = v + } + for _, id := range ids { + item, ok := vidMap[id] + if !ok || (item.Status != 1 && item.Status != 3) { + topasist.Remove(topser.TypeVideo(newsType), id.Hex()) + continue + } + videoList = append(videoList, item) + } + + if len(videoList) == 0 && !periodRankHasContent { + videoList, hasNext, err = vidmod.FindCumulativeRanking(newsType, q.Page) + if err != nil { + return res, err + } + } + + res.List = vidhelpser.EncodeVideoInfoNoUID(videoList) + res.HasNext = hasNext + + return res, nil +} + +func (q *RankingReq) periodRankHasContent(rankingType int, newsType string) bool { + var list []string + switch rankingType { + case DailyRank: + list, _, _ = dailytop.GetTopByPage(topser.TypeVideo(newsType), 0, 1) + case WeekRank: + list, _, _ = weektop.GetTopByPage(topser.TypeVideo(newsType), 0, 1) + case MonthRank: + list, _, _ = monthtop.GetTopByPage(topser.TypeVideo(newsType), 0, 1) + } + return len(list) > 0 +} diff --git a/app/service/vidser/redis.go b/app/service/vidser/redis.go new file mode 100644 index 0000000..7ab0e93 --- /dev/null +++ b/app/service/vidser/redis.go @@ -0,0 +1,48 @@ +package vidser + +import ( + "encoding/json" + "fmt" + "time" + + "91porn-server/app/appg" + "91porn-server/common/log" + "91porn-server/models/v/vidmod" +) + +const UserTopWork = "UserTopWork:%d" + +type WorkList struct { + Works []*vidmod.VideoModel `json:"works"` +} + +// GetUserTopWork 获取用户置顶作品:作品排序前三视频 +func GetUserTopWork(uid uint64) ([]*vidmod.VideoModel, error) { + var ( + w WorkList + key = fmt.Sprintf(UserTopWork, uid) + ) + if err := appg.Redis.GetObj(&w, key); err != nil { + log.Error(fmt.Sprintf("用户[%d] redis获取前三视频异常[%v]", uid, err)) + return nil, err + } + // 从数据库获取 + if len(w.Works) == 0 { + var err error + w.Works, err = vidmod.GetOriginals(uid) + if err != nil { + log.Error(fmt.Sprintf("用户[%d] mongodb获取前三视频异常[%v]", uid, err)) + return nil, err + } + data, err := json.Marshal(&w) + if err != nil { + log.Error(fmt.Sprintf("用户[%d] 序列化前三视频异常[%v]", uid, err)) + return nil, err + } + if err = appg.Redis.Set(key, data, time.Hour*1); err != nil { + log.Error(fmt.Sprintf("用户[%d] redis保存前三视频异常[%v]", uid, err)) + return nil, err + } + } + return w.Works, nil +} diff --git a/app/service/vidser/type.go b/app/service/vidser/type.go new file mode 100644 index 0000000..d38f613 --- /dev/null +++ b/app/service/vidser/type.go @@ -0,0 +1,317 @@ +package vidser + +import ( + "91porn-server/common/cachev2" + "91porn-server/common/log" + "91porn-server/models/v/discount_area_mod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/modulesectionmod" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" + + "91porn-server/models/commod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type VideoUnderSubModule struct { + ID primitive.ObjectID `json:"id"` // 视频ID + PublisherID uint64 `json:"publisherID"` // 上传者ID + Title string `json:"title"` // 视频标题 + SourceID string `json:"sourceID"` // 视频在仓库中的资源ID + SourceURL string `json:"sourceURL"` // 视频资源地址Path + Cover string `json:"cover"` // 封面大图 + CoverThumb string `json:"coverThumb"` // 封面小图 + Tags []primitive.ObjectID `json:"tags"` // 标签 + PlayCount int `json:"playCount"` // 总播放量 + PlayTime uint `json:"playTime"` // 影片长度 + ReviewAt time.Time `json:"reviewAt"` // 审核通过时间 +} + +type VideoUnderSubModuleReq struct { + ModuleSort int `form:"moduleSort,default=1"` // 默认视频排序 1、最新,2、最热/推荐,3、最多播放,4、十分钟以上视频, 5、精华/精选,6、视频 7-最多收藏 8、解锁次数 9、最新热评 + TagId string `json:"tagId" form:"tagId"` // 标签id,社区点击标签刷新数据使用 + commod.Page +} + +func (receive VideoUnderSubModuleReq) Filter(moduleType int, mId *string, TagIds *[]primitive.ObjectID) primitive.M { + now := time.Now().Add(-time.Hour * 24 * 7) + filter := bson.M{} + if mId != nil { + filter["mId"] = mId + } + if TagIds != nil && len(*TagIds) > 0 { + filter["tags"] = bson.M{"$in": TagIds} + } + switch receive.ModuleSort { + case 2: + // 最热 + if moduleType != moduleconfmod.Community { + // 首页本周最热 + filter["reviewAt"] = bson.M{"$gte": now} + } + if moduleType == moduleconfmod.Community { + filter["reviewAt"] = bson.M{"$gte": time.Now().Add(-time.Hour * 24 * 21)} + } + case 3: + // 推荐 + if moduleType == moduleconfmod.Community { + filter["reviewAt"] = bson.M{"$gte": now} + } + case 4: + // 十分钟以上 + filter["newsType"] = vidmod.SP + filter["playTime"] = bson.M{"$gte": 600} + case 5: + // 精选 + filter["reviewAt"] = bson.M{"$gte": time.Now().Add(-time.Hour * 24 * 14)} + case 6: + // 视频 + filter["newsType"] = vidmod.SP + } + filter["status"] = vidmod.CheckPass + return filter +} +func (receive *VideoUnderSubModuleReq) Options(moduleType int) *options.FindOptions { + sort := bson.D{} + switch receive.ModuleSort { + case 1: + // 最新 + if moduleType == moduleconfmod.Community { + sort = bson.D{{"reviewAt", -1}} + } + if moduleType == moduleconfmod.HomePage { + // 改为有数值的置顶 + sort = bson.D{{"liaoBaTopSort", -1}, {"reviewAt", -1}} + } + case 2: + // 本周最热 + sort = bson.D{{"likeCount", -1}, {"reviewAt", -1}} + case 3: + // 推荐 + if moduleType == moduleconfmod.Community { + sort = bson.D{{"liaoBaTopSort", -1}, {"playCount", -1}, {"likeCount", -1}, {"reviewAt", -1}} + } else { + sort = bson.D{{"playCount", -1}, {"reviewAt", -1}} + } + case 4: + // 十分钟以上视频 + sort = bson.D{{"reviewAt", -1}} + case 5: + // 精华排序 + sort = bson.D{{"isChoosen", -1}, {"chosen", -1}, {"commentCount", -1}, {"reviewAt", -1}} + case 6: + // 视频排序 + sort = bson.D{{"reviewAt", -1}} + } + return options.Find(). + SetSort(sort). + SetSkip(int64(receive.Skip())). + SetLimit(int64(receive.Limit() + 1)). + SetProjection(bson.M{"richText": 0}) +} + +// VideoUnderSubModuleMap 专题和其视频的关联关系 +type VideoUnderSubModuleMap struct { + SectionID string `json:"sectionID"` // 专题ID + SectionName string `json:"sectionName"` // 专题名称 + Sort int `json:"-"` // 排序字段,仅仅为了对专题进行排序,不对前端输出 + VideoInfo []*vidmod.VideoInfo `json:"videoInfo"` // 视频详情 + OriginalBloggerInfo OriginalBloggerInfo `json:"originalBloggerInfo"` // 原创博主信息 + ShowType int `json:"showType"` +} + +type VideoUnderSubModuleResp struct { + AllVideoInfo []*vidmod.VideoInfoResp `json:"allVideoInfo" bson:"allVideoInfo"` // 专题下所有视频 + ChosenVideoInfo []*vidmod.VideoInfoResp `json:"chosenVideoInfo" bson:"chosenVideoInfo"` // 精选视频 + AllMediaInfo []*mediamod.AppMediaBase `json:"allMediaInfo" bson:"allMediaInfo"` //所有的acg动漫 + AllSection []Section `json:"allSection" bson:"allSection"` // 所有专题 + HasNext bool `json:"hasNext"` // 是否还有下一页 +} + +type RefreshModuleVideosReq struct { + ModuleSort int `form:"moduleSort" binding:"required"` + PageSize int `form:"pageSize" binding:"omitempty,min=1,max=30"` + RefreshToken string `form:"refreshToken" binding:"required,max=128"` +} + +type RefreshModuleVideosResp struct { + AllVideoInfo []*vidmod.VideoInfoResp `json:"allVideoInfo"` + CandidateCount int `json:"candidateCount"` + HasNext bool `json:"hasNext"` + RefreshMode string `json:"refreshMode"` + RefreshToken string `json:"refreshToken"` +} + +type Section struct { + SectionID string `json:"sectionID" bson:"sectionID"` // 专题ID + SectionName string `json:"sectionName" bson:"sectionName"` // 专题名称 + SectionTitle string `json:"sectionTitle" bson:"sectionTitle"` // 专题的标题 + Hot bool `json:"hot" bson:"hot"` // 是否显示hot标识 + SectionCover string `json:"sectionCover" bson:"sectionCover"` // 亚模块封面 + Sort int `json:"sort" bson:"sort" binding:"required"` // 排序 + ShowType int `json:"showType" bson:"showType,omitempty"` // 展示样式 + AllTags []Tag `json:"allTags" bson:"allTags"` // 所有标签 + AllVideoInfo []*vidmod.VideoInfoResp `json:"allVideoInfo" bson:"allVideoInfo"` // 专题下帖子信息 + AllMediaInfo []*mediamod.AppMediaBase `json:"allMediaInfo" bson:"allMediaInfo"` // 专题下媒体信息 +} + +type Tag struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 标签id + TagName string `json:"tagName" bson:"tagName"` // 标签名字 前端使用name + CoverImg string `json:"coverImg" bson:"coverImg"` // 封面图片 + HotMark string `json:"hotMark" bson:"hotMark"` // 热门标签 + VideoCount int64 `json:"videoCount" bson:"videoCount"` // 使用此标签的视频数量 +} + +// OriginalBloggerInfo 原创博主相关信息(原创模块需要) +type OriginalBloggerInfo struct { + UID uint64 `json:"uid"` // 用户ID + Name string `json:"name"` // 用户姓名 + Portrait string `json:"portrait"` // 头像地址 + OfficialCert bool `json:"officialCert"` // 是否官方认证 + Summary string `json:"summary"` // 用户简介 +} + +type VideoUnderSectionReq struct { + SectionID string `uri:"sectionID" binding:"required"` + SortType string `form:"sortType"` + PlayTimeType int `form:"playTimeType"` +} + +type VideoUnderSectionResp struct { + Videos []*vidmod.VideoInfoResp `json:"videos"` // 视频列表 + Medias []*mediamod.AppMediaBase `json:"medias"` // 动漫/漫画列表 + HasNext bool `json:"hasNext"` // 是否还有下一页 + OriginalBloggerInfo OriginalBloggerInfo `json:"originalBloggerInfo"` // 原创博主信息 +} + +type AllVideosOfModuleReq struct { + SubModuleID string `form:"subModuleID"` + SortType string `form:"sortType"` //hot 热度值排序;watch 最多播放;like 最多点赞(收藏);new 最新视频 + PlayTimeType int `form:"playTimeType"` //0 默认全部 1 长视频 2 短视频 + commod.Page +} + +type AllVideosOfModuleResp struct { + Videos []*vidmod.VideoInfo `json:"videos"` // 视频列表 + HasNext bool `json:"hasNext"` // 是否还有下一页 +} + +type SectionResp struct { + ID primitive.ObjectID `json:"id"` + SectionName string `json:"sectionName"` +} + +type VidRecommandReq struct { + ID string `bson:"id" json:"id"` +} + +type RecommandResp struct { + Videos []*vidmod.VideoInfo `json:"videos"` // 视频列表 + HasNext bool `json:"hasNext"` // 是否还有下一页 +} + +type DiscountVideoReq struct { + DiscountId string `form:"discountId" json:"discountId" binding:"required"` + SortType int `form:"sortType,default=0"` // 排序类型 0-默认 1-最新 2-最热 + commod.Page +} + +func (receive DiscountVideoReq) Filter() primitive.M { + filter := bson.M{} + discountAreaId, err := primitive.ObjectIDFromHex(receive.DiscountId) + if err != nil { + return filter + } + filter["discountAreaId"] = discountAreaId + switch receive.SortType { + case 2: + // 查询的是videoInfo + filter["status"] = vidmod.CheckPass + default: + } + + return filter +} +func (receive *DiscountVideoReq) Options() *options.FindOptions { + sort := bson.D{} + switch receive.SortType { + case 2: + // 最热 + sort = bson.D{{"likeCount", -1}, {"reviewAt", -1}} + default: + // 最新 + sort = bson.D{{"createdAt", -1}} + } + return options.Find().SetSkip(int64(receive.Skip())).SetLimit(int64(receive.Limit() + 1)).SetSort(sort) +} + +type DiscountVideoResp struct { + // 折扣专区 + List []*vidmod.VideoInfo `json:"list"` + HasNext bool `json:"hasNext"` +} + +// 获取折扣专区列表 +type DiscountAreaResp struct { + List []discount_area_mod.DiscountArea +} + +type ( + VideoListAllReq struct { + SortType int `form:"sortType" json:"sortType"` // 列表类型 1:最新 2:热门 3:本周最热 4:本月最热 5:上月最热 + commod.Page + } + VideoListAllResp struct { + Videos []*vidmod.VideoInfo `json:"videos"` // 视频列表 + HasNext bool `json:"hasNext"` // 是否还有下一页 + } +) + +type CommunityRecommendReq struct { + NewsType string `json:"newsType" form:"newsType" binding:"required,oneof=PIC SEED_LINK"` // PIC:套图站热门推荐 SEED_LINK:黄游热门推荐 + SortType int `form:"sortType" json:"sortType"` // 排序:最新:1 最多点赞:2 最多观看:3 最多收藏:7 购买次数:8 + commod.Page +} + +type SectionListReq struct { + Mid string `json:"mid" form:"mid" binding:"required"` // 模块id + commod.Page +} + +type SectionListResp struct { + List []modulesectionmod.Section `json:"list"` // 专题列表 + HasNext bool `json:"hasNext"` // 是否还有下一页 +} + +func (p *SectionListReq) GetList() (resp SectionListResp, err error) { + mid, _ := primitive.ObjectIDFromHex(p.Mid) + if mid.IsZero() { + return + } + active, err := moduleconfmod.CanBrowseModule(mid, time.Now()) + if err != nil || !active { + return resp, err + } + _, err = cachev2.Classes().CacheTime(time.Minute*5).AutoListKey("moduleSections").ResBind(&resp).Cache(p.getList, mid) + if err != nil { + log.Error("SectionListReq.GetList fail", log.E(err)) + return + } + return resp, nil +} + +func (p *SectionListReq) getList(mid primitive.ObjectID) (resp SectionListResp, err error) { + list, hasNext, err := modulesectionmod.GetBySubModuleID(mid, p.Page) + if err != nil { + return + } + resp.List = list + resp.HasNext = hasNext + return +} diff --git a/app/service/vidser/vidser.go b/app/service/vidser/vidser.go new file mode 100644 index 0000000..e2d6eb0 --- /dev/null +++ b/app/service/vidser/vidser.go @@ -0,0 +1,2885 @@ +package vidser + +import ( + "91porn-server/app/service/mediaser" + "91porn-server/app/service/searchaccessser" + "91porn-server/app/service/shortrecommendser" + "91porn-server/app/service/sys_config" + "91porn-server/common/cachev2" + "91porn-server/common/redis" + topser "91porn-server/common/top" + "91porn-server/common/top/weektop" + "91porn-server/models/cache/viddata" + "91porn-server/models/v/mediamod" + "context" + "encoding/json" + "errors" + "fmt" + "math" + "net/http" + "sort" + "strconv" + "strings" + "sync" + "time" + + goredis "github.com/go-redis/redis" + + "github.com/vmihailenco/msgpack/v5" + + "91porn-server/app/appg" + "91porn-server/app/proto" + "91porn-server/app/service/activityclient" + "91porn-server/app/service/searcher" + "91porn-server/app/service/searcher/vidhkwsearcher" + "91porn-server/app/service/searcher/vidhotsearcher" + "91porn-server/app/service/vidhelpser" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/httputil" + "91porn-server/common/log" + sli "91porn-server/common/slice" + "91porn-server/common/stderr" + "91porn-server/common/timeutil" + "91porn-server/models/commod" + "91porn-server/models/l/payvidlgmod" + "91porn-server/models/l/playlgmod" + "91porn-server/models/s/hotstatmod" + "91porn-server/models/s/sectionstatmod" + "91porn-server/models/v/freeVidmod" + "91porn-server/models/v/imagetopmod" + "91porn-server/models/v/locmod" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/modulesectionmod" + "91porn-server/models/v/modulevidmod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/useractmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/userwatchrecordmod" + "91porn-server/models/v/videoactivitymod" + "91porn-server/models/v/videodiscountmod" + "91porn-server/models/v/vidmod" + "91porn-server/models/v/vidpopmod" + "91porn-server/models/v/walletmod" + + "github.com/jinzhu/now" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const ( + h5FirstPlay = "1" + + timeout = 5 * time.Second +) + +var defaultLocation, _ = primitive.ObjectIDFromHex("5dbeb23790b3d9aa3756912f") //默认城市ID 广州 + +func getCityByIP(ip string) string { + return vidhelpser.GetLocationByIP(ip) +} + +// GetLocationList 获取附近位置的视频 +func GetLocationList(uid uint64, ip string, city string, page, size uint64, typ string) (stderr.Code, interface{}) { + if len(city) == 0 { + city = getCityByIP(ip) + } + lid, err := locmod.GetLocationIDByCity(city) + if err != nil { + return stderr.ErrDbQueryError, nil + } + if lid.IsZero() { + return stderr.Success, vidmod.LocVideoResp{List: []*vidmod.VideoInfo{}, HasNext: false} + } + var videos []*vidmod.VideoModel + if typ == vidmod.SP { + if videos, err = vidmod.GetVideoListByLocation(lid, int64(page), int64(size+1)); err != nil { + return stderr.ErrDbQueryError, nil + } + } + if typ == vidmod.COVER { + if videos, err = vidmod.GetImageListByLocation(lid, int64(page), int64(size+1)); err != nil { + return stderr.ErrDbQueryError, nil + } + } + hasNext := false + if uint64(len(videos)) > size { + videos = videos[:size] + hasNext = true + } + return stderr.Success, vidmod.LocVideoResp{List: vidhelpser.EncodeVideoInfo(uid, videos), HasNext: hasNext} +} + +// GetLocationHot 获取热门城市 +func GetLocationHot() (code stderr.Code, data interface{}) { + citys, err := locmod.GetHotCity() + if err != nil { + return stderr.ErrDbQueryError, nil + } + back := make([]locmod.CityResp, len(citys)) + for i, c := range citys { + back[i] = locmod.CityResp{ID: c.ID.Hex(), City: c.City, Province: c.Province, Hot: c.SortKey} + } + return stderr.Success, back +} + +// GetInfo 获取视频详情 +func GetInfo(uid uint64, videoID string) (code stderr.Code, data interface{}) { + return GetInfoWithSearchAccess(uid, videoID, "") +} + +// GetInfoWithSearchAccess 获取视频详情;失效亚模块的视频必须携带搜索结果签发的访问凭证。 +func GetInfoWithSearchAccess(uid uint64, videoID, searchAccessToken string) (code stderr.Code, data interface{}) { + v, err := vidmod.GetVideoInfo(videoID) + if err != nil { + return stderr.ErrDbQueryError, nil + } + blockedModules, err := moduleconfmod.BlockedOutsideSearchModuleIDs([]string{v.MID}, time.Now()) + if err != nil { + return stderr.ErrDbQueryError, nil + } + _, searchOnly := blockedModules[v.MID] + if searchOnly && !searchaccessser.Validate(searchAccessToken, uid, videoID, time.Now()) { + return stderr.CodeEmptyData, nil + } + var infos []*vidmod.VideoInfo + if searchOnly { + infos = vidhelpser.EncodeVideoInfoForSearch(uid, []*vidmod.VideoModel{&v}) + } else { + infos = vidhelpser.EncodeVideoInfo(uid, []*vidmod.VideoModel{&v}) + } + if len(infos) == 0 { + return stderr.UnkonwErr, "" + } + info := infos[0] + if info.NewsType == constant.COVER { + user, _ := usermod.FindUserByUID(uid) + wallet, _ := walletmod.GetWallet(uid) + info.RichText = activityclient.ReplaceActivityDomainInHTML(info.RichText, user, wallet) + } + return stderr.Success, info +} + +// SubmitBase 提交基本信息 +func SubmitBase(uid uint64, req vidmod.SubmitReq, clientIP string) (code stderr.Code, vid primitive.ObjectID) { + if uid == 0 { + uid = req.UID + } + tags := make([]primitive.ObjectID, 0, len(req.Tags)) + for _, t := range req.Tags { + oid, err := primitive.ObjectIDFromHex(t) + if err != nil { + log.Warn("SubmitBase error", log.Any("uid", uid), log.Any("req", req), log.E(err)) + continue + } + if sli.PrimitiveObjIDIsExist(tags, oid) { + continue + } + tags = append(tags, oid) + // 标签视频数计数 + if err = tagmod.IncreaseTagVidCount(oid, 1); err != nil { + log.Warn("SubmitBase2 error", log.Any("uid", uid), log.Any("req", req), log.E(err)) + continue + } + } + l := locmod.Location{ + Longitude: req.Location.Longitude, + Latitude: req.Location.Latitude, + Country: req.Location.Country, + CountryCode: req.Location.CountryCode, + Province: req.Location.Province, + ProvinceCode: req.Location.ProvinceCode, + City: req.Location.City, + CityCode: req.Location.CityCode, + Address: req.Location.Address, + Visit: 0, + } + isHideLocation := false + if len(l.City) == 0 { + l.City = getCityByIP(clientIP) + isHideLocation = true + } + var lid primitive.ObjectID + if err := locmod.InsertLocationInfo(l); err != nil { + lid = defaultLocation + } else { + lid, err = locmod.GetLocationIDByCity(l.City) + if err != nil { + user, userError := usermod.FindUserByUID(uid) + if userError != nil || user.Region == "" { + lid = defaultLocation + } else { + var locErr error + lid, locErr = locmod.GetLocationIDByCity(user.Region) + if locErr != nil || lid.IsZero() { + lid = defaultLocation + } + } + } + } + //为了不影响用户上传体验,对于sourceID为"0000..."情况的视频,允许上传成功,但默认为审核不通过 + var status int + sourceID := req.SourceID + oid, oidErr := primitive.ObjectIDFromHex(req.SourceID) + if (req.NewsType == vidmod.SP || req.NewsType == vidmod.SHORT) && (oidErr != nil || oid.IsZero()) { + oid = primitive.NewObjectID() + sourceID = oid.Hex() + status = 2 + } + series := []string{} + if len(req.SeriesCover) != 0 { + series = req.SeriesCover + } + //视频高度不能低于640 + var width, height int + resolutions := strings.Split(req.Resolution, "*") + if len(resolutions) >= 2 { + width, _ = strconv.Atoi(resolutions[0]) + height, _ = strconv.Atoi(resolutions[1]) + } + if width > 0 && height > 0 && height < 640 { + return stderr.ResolutionNotEnough, primitive.NilObjectID + } + ok, err := vidmod.IsSubmitByPublisherID(uid) + if err != nil { + return stderr.ErrDbQueryError, primitive.NilObjectID + } + if !ok { + appg.Redis.Incr("creatorNumber") + } + v := vidmod.VideoModel{ + ID: oid, + PublisherID: uid, + NewsType: req.NewsType, + Title: req.Title, + Content: req.Content, + Tags: tags, + SourceID: sourceID, + SourceURL: req.SourceURL, + MimeType: req.MimeType, + Activity: []primitive.ObjectID{}, + Filename: req.Filename, + PlayTime: req.PlayTime, + Cover: req.Cover, + CoverThumb: req.CoverThumb, + SeriesCover: series, + SeriesNum: len(series), + Via: req.Via, + Rating: 0, + PlayCount: 0, + LikeCount: 0, + CommentCount: 0, + ShareCount: 0, + Status: status, + Location: lid, + FreeTime: req.FreeTime, + Coins: req.Coins, + Size: req.Size, + Resolution: req.Resolution, + Ratio: req.Ratio, + MD5: req.MD5, + Actor: req.Actor, + IsHideLocation: isHideLocation, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + if req.IsActivity { + videoActivity, err := videoactivitymod.QueryDocumentByStatus(videoactivitymod.Enable) + if err != nil { + return stderr.ErrDbQueryError, primitive.NilObjectID + } + v.ActivityID = videoActivity.ID + } + + if v.PlayTime > 0 || v.NewsType == vidmod.SP { + if v.PlayTime < 300 { + v.NewsType = vidmod.SHORT + } else { + v.NewsType = vidmod.SP + } + } + + if vid, err = vidmod.InsertBase(v); err != nil { + if mongo.IsDuplicateKeyError(err) { + return stderr.ErrUploadExistVideo, primitive.NilObjectID + } + return stderr.ErrNetWorkBusy, primitive.NilObjectID + } + return stderr.Success, vid +} + +// PlayRecord 用户播放行为记录 +// 视频播放、标签播放、地理位置播放++ +// 播放日志记录 +// 播放率统计 +func PlayRecord(uid uint64, req vidmod.PlayReq) (code stderr.Code, data interface{}) { + oid, err := primitive.ObjectIDFromHex(req.VideoID) + if err != nil { + log.Error("PlayRecord error", log.Any("uid", uid), log.Any("req", req), log.E(err)) + return stderr.ErrParamError, nil + } + var tid primitive.ObjectID + if len(req.TagID) != 0 { + tid, err = primitive.ObjectIDFromHex(req.TagID) + if err != nil { + log.Error("PlayRecord tid error", log.Any("uid", uid), log.Any("req", req), log.E(err)) + return stderr.ErrParamError, nil + } + } + now := time.Now() + common.Go(func() { + if req.Publisher <= 0 { + return + } + weektop.Incr(topser.TypeRecommendUser, fmt.Sprintf("%v", req.Publisher), 1) + }) + common.Go(func() { + ok, err := userwatchrecordmod.IsExist(uid, oid) + if err != nil { + return + } + if ok { + if err = userwatchrecordmod.Refresh(uid, oid); err != nil { + return + } + } else { + if err = userwatchrecordmod.Insert(uid, oid); err != nil { + return + } + } + p := playlgmod.PlayLog{ + UID: uid, + VideoID: oid, + Longer: req.Longer, + Progress: req.Progress, + Via: req.Via, + PlayWay: req.PlayWay, + TagID: tid, + Publisher: req.Publisher, + CreatedAt: now, + } + _ = playlgmod.InsertPlayRecord(p) + isValid := req.Longer >= 5 + _ = vidhelpser.IncreaseTag(req.VideoID, tid, isValid) + popCfg, err := vidpopmod.FindVidPopCfg() + if err != nil { + log.Error("vidpopmod.FindVidPopCfg", log.E(err)) + } + _ = vidmod.IncPlayCount(oid, req, popCfg) + }) + common.Go(func() { + _ = setHotPubliser(now, req.Publisher) + }) + return stderr.Success, nil +} + +func setHotPubliser(d time.Time, publishserUID uint64) error { + d = timeutil.BeginningOfDay(d) + reidsKey := redisconst.PlayLeaderboardKey("publiser", d.Unix()) + _, err := appg.Redis.ZIncrBy(reidsKey, 1, strconv.FormatUint(publishserUID, 10)) + return err +} + +// SendBaseInfo2FS 上传基本文件信息到aws +func SendBaseInfo2FS(req vidmod.SubmitReq) (code stderr.Code) { + info := vidmod.BaseVid4Aws{ + ID: req.SourceID, + CheckSum: req.MD5, + Title: req.Title, + Actors: []string{req.Actor}, + AddedTime: time.Now().Format("2006-01-02"), + PlayTime: uint64(req.PlayTime), + Size: req.Size, + Via: "q1", + Filename: req.Filename, + Type: "sp", + } + var tags []string + reqTagsLen := len(req.Tags) + if reqTagsLen > 0 { + tags = make([]string, 0, reqTagsLen) + for _, v := range req.Tags { + tagID, err := primitive.ObjectIDFromHex(v) + if err != nil { + log.Warn("SendBaseInfo2FS error", log.Any("req", req), log.E(err)) + continue + } + tagData, err := tagmod.FindOneTagByID(tagID) + if err != nil { + continue + } + tags = append(tags, tagData.TagName) + } + } + info.Tags = tags + c, cancle := context.WithTimeout(context.Background(), timeout) + defer cancle() + params, _ := common.ToJsonM(info) + respBody := commod.Resp{} + httpStatus, err := httputil.DefaultClientPostJsonWithRespWithCtx(c, &respBody, appg.Conf.URL.FileInfoUrl, nil, params) + log.Info("http method SendBaseInfo2FS response code ==>", log.Any("httpStatus", httpStatus), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("SendBaseInfo2FS POSTJsonWithJResp error", log.Any("req", req), log.E(err)) + return stderr.ErrConnectToFs + } + if respBody.Code != http.StatusOK { + log.Error("SendBaseInfo2FS status error", log.Any("req", req), log.Any("respBody.Code", respBody.Code), log.E(err)) + return stderr.ErrFsServerFile + } + return stderr.Success +} + +// SendCoverInfo2FS 上传基本图集信息到aws +func SendCoverInfo2FS(req vidmod.SubmitReq) (code stderr.Code) { + info := vidmod.CoverInfo4Aws{ + Type: "SP", + Title: req.Title, + Tags: req.Tags, + CoverImg: req.Cover, + SeriesCover: req.SeriesCover, + Status: "", + MimeType: req.MimeType, + Via: "q1", + Topic: []string{}, + NewUpdateAt: time.Now().Format("2006-01-02"), + Desc: "", + } + var tags []string + reqTagsLen := len(req.Tags) + if reqTagsLen > 0 { + tags = make([]string, 0, reqTagsLen) + for _, v := range req.Tags { + tagID, err := primitive.ObjectIDFromHex(v) + if err != nil { + log.Warn("SendCoverInfo2FS error", log.Any("req", req), log.E(err)) + continue + } + tagData, err := tagmod.FindOneTagByID(tagID) + if err != nil { + continue + } + tags = append(tags, tagData.TagName) + } + } + info.Tags = tags + c, cancle := context.WithTimeout(context.Background(), timeout) + defer cancle() + params, _ := common.ToJsonM(info) + respBody := commod.Resp{} + httpStatus, err := httputil.DefaultClientPostJsonWithRespWithCtx(c, &respBody, appg.Conf.URL.CoverInfoUrl, nil, params) + log.Info("http method SendCoverInfo2FS response code ==>", log.Any("httpStatus", httpStatus), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("SendCoverInfo2FS POSTJsonWithJResp error", log.Any("req", req), log.E(err)) + return stderr.ErrConnectToFs + } + if respBody.Code != http.StatusOK { + log.Error("SendCoverInfo2FS status error", log.Any("req", req), log.Any("respBody.Code", respBody.Code), log.E(err)) + return stderr.ErrFsServerFile + } + return stderr.Success +} + +// GetLocation 通过id获取地理位置 +func GetLocation(uid uint64, id string) (code stderr.Code, data interface{}) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Error("GetLocation error", log.Any("uid", uid), log.Any("id", id), log.E(err)) + return stderr.ErrParamError, nil + } + l, err := locmod.GetLocationInfoByID(oid) + if err != nil { + return stderr.ErrDbQueryError, nil + } + if err = locmod.IncVisit(oid); err != nil { + return stderr.ErrDbUpdateError, nil + } + return stderr.Success, vidmod.LocResp{LocInfo: vidmod.LocInfo{ + ID: l.ID, + City: l.City, + Cover: l.Cover, + Visit: l.FakeVisit, + CreatedAt: l.CreatedAt, + }} +} + +// 总数固定10次 看完之后不再恢复 +func GetWatchCount(uid uint64, oid primitive.ObjectID, first string) (isCan bool, watchCount uint64) { + totalCount := TotalFreeWatchCount() + var playWay uint64 = 0 + duration := now.BeginningOfDay() + u, err := usermod.FindUserByUID(uid) + if u == nil || err != nil { + isCan = false + return + } + //是否是vip VIP可免费观看 + isFree := IsFreeWatch(uid) + if isFree { + isCan = true + return + } + info, err := vidmod.GetVideoInfo(oid.Hex()) + if err != nil { + return false, u.WatchCount + } + //判断是否是自己的视频或免费的视频或收费的视频 + if info.PublisherID == uid || info.FreeArea || info.Coins > 0 { + return true, u.WatchCount + } + //当前视频在当天是否观看过 如果观看过 则可以继续观看 + result := useractmod.IsViewTodayByNoVip(uid, oid, duration) + if result { + isCan = true + return + } + watchCount = u.WatchCount + if watchCount > totalCount { + watchCount = totalCount + } + //当观看次数为0时 分两种情况 + if watchCount == 0 { + //判断当天是否观看过 如果没有观看过 则设置为10 + // cnt := useractmod.ActCount(uid, duration) + // if cnt == 0 { + // watchCount = vidmod.TotalWatchCount + // _, _ = usermod.UpdateUserWatchCount(uid, int64(vidmod.TotalWatchCount)) + // } + isCan = false + } + if 0 < watchCount && watchCount <= totalCount { + isCan = true + if first == h5FirstPlay { + return isCan, watchCount + } + _ = useractmod.UserActInsert(useractmod.UserAct{ + VID: oid, + PlayWay: playWay, + UID: uid, + CreatedAt: time.Now(), + DailyDate: duration, + }) + wCount := int64(watchCount - 1) + _, _ = usermod.UpdateUserWatchCount(uid, wCount) + } + return isCan, watchCount +} + +// TotalFreeWatchCount 返回系统配置的免费观看总次数。 +func TotalFreeWatchCount() uint64 { + return sys_config.GetTotalWatchCount() +} + +func FreshCount(uid uint64) uint64 { + u, err := usermod.FindUserByUID(uid) + if u == nil || err != nil { + return 0 + } + return u.WatchCount +} + +func IsFreeWatch(uid uint64) bool { + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return false + } + return !u.VipExpireDate.Before(time.Now()) +} + +func GetHotList(uid uint64, skip, limit int64) (list interface{}, hasNext bool, err error) { + opt := (&searcher.Option{}). + SetSkip(skip). + SetLimit(limit) + hotResult, err := vidhotsearcher.NewVidHotSearcher(uid).Search(nil, opt) + if err != nil { + return nil, false, err + } + return hotResult.Data(), hotResult.HasNext(), nil +} + +func GetHotKWList(uid uint64, skip, limit int64) (list interface{}, hasNext bool, err error) { + //每个关键字平均取视屏 + //如:需要12个视屏 用6个关键字搜素 每个关键字最多查询2个视屏 + const keywordCount = 6 + vidLimitPer := limit / keywordCount + vidOpt := (&searcher.Option{}). + SetSkip(limit * vidLimitPer). + SetLimit(limit) + hkwOpt := (&searcher.Option{}).SetLimit(keywordCount) //搜索排名前keywordCount的关键字 + hkwResult, err := vidhkwsearcher.NewVidHKWSearcher(uid, vidOpt).Search(nil, hkwOpt) + if err != nil { + return nil, false, err + } + if hkwResult.Count() >= limit { //不用补充视屏 + return hkwResult.Data(), hkwResult.HasNext(), nil + } + //热搜视屏代替 + hotOpt := (&searcher.Option{}). + SetSkip(skip). + SetLimit(limit) + hotResult, err := vidhotsearcher.NewVidHotSearcher(uid).Search(nil, hotOpt) + if err != nil { + return nil, false, err + } + return hotResult.Data(), hotResult.HasNext(), nil +} + +// DeleteManyByTTL 删除自己的视频 被删除的视屏归属为officialUID,未审核或审核未通过的视屏通过TTL删除,其余视屏保留 +func DeleteManyByTTL(uid uint64, ids ...primitive.ObjectID) (code stderr.Code, data interface{}) { + //redis清除视频id + mats := []vidmod.Matcher{ + (&vidmod.PublisherIDMatch{PublisherID: &uid}).New(), + (&vidmod.IDInMatch{IDs: ids}).New(), + } + list, err := vidmod.FindMany(mats...) + if err != nil { + return stderr.ErrDbQueryError, nil + } + //removeIDs-转移到官方账号 并置为审核不通过 + //fakeRemoveIDs-审核通过的金币视频,如果有人购买,做逻辑删除 + //deleteIDs-物理删除 + listLen := len(list) + removeIDs, fakeRemoveIDs, deleteIDs := make([]primitive.ObjectID, 0, listLen), make([]primitive.ObjectID, 0, listLen), make([]primitive.ObjectID, 0, listLen) + for _, v := range list { + if v.Status == 1 || v.Status == 3 { //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + if v.Status == 1 && v.Coins > 0 { + isBePay := payvidlgmod.IsVidIfBePay(v.ID) //判断是否当前视频是否购买过 + if isBePay { + fakeRemoveIDs = append(fakeRemoveIDs, v.ID) + } else { + removeIDs = append(removeIDs, v.ID) + } + } else { + removeIDs = append(removeIDs, v.ID) + } + } + if v.Status == 0 || v.Status == 2 { //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + deleteIDs = append(deleteIDs, v.ID) + } + } + rmSetDoc := (&vidmod.SetDoc{}).SetPublisherID(constant.DeleteUID).SetStatus(vidmod.CheckFailure) + logicRmSetDoc := (&vidmod.SetDoc{}).SetPublisherID(constant.DeleteUID).SetStatus(vidmod.IsDeleted) + if _, err = vidmod.ModifyMany(removeIDs, *rmSetDoc); err != nil { + return stderr.ErrDbUpdateError, nil + } + if _, err = vidmod.ModifyMany(fakeRemoveIDs, *logicRmSetDoc); err != nil { + return stderr.ErrDbUpdateError, nil + } + + if len(deleteIDs) != 0 { + _ = vidmod.DelVideo(deleteIDs) + } + return stderr.Success, nil +} + +// GetNewestNewsList 获取最新帖子 +func GetNewestNewsList(uid, page, size uint64, head []*vidmod.VideoModel, reqDate time.Time) ( + code stderr.Code, data interface{}) { + infos, hasNext, err := vidhelpser.GetNewestNews(page, size, reqDate) + if err != nil { + return stderr.ErrDbQueryError, nil + } + if len(head) > 0 { + head = append(head, infos...) + head = RemoveRep(head) + } else { + head = infos + } + newInfos := vidhelpser.EncodeVideoInfo(uid, head) + data = vidmod.NewsListResp{List: newInfos, HasNext: hasNext} + return stderr.Success, data +} + +// GetNewestNewsList 获取最新帖子 +func GetNewestNewsList_old(uid, page, size uint64, head []*vidmod.VideoModel, subType int) (stderr.Code, interface{}) { + var infos []*vidmod.VideoModel + var hasNext bool + var err error + if subType == 1 { // 1 只取短视频 + infos, hasNext, err = vidhelpser.GetNewestShortVideo(page, size) + if err != nil { + return stderr.ErrDbQueryError, nil + } + } else { + infos, hasNext, err = vidhelpser.GetNewestNews_old(page, size) + if err != nil { + return stderr.ErrDbQueryError, nil + } + } + if len(head) > 0 { + head = append(head, infos...) + head = RemoveRep(head) + } else { + head = infos + } + return stderr.Success, vidmod.NewsListResp{List: vidhelpser.EncodeVideoInfo(uid, head), HasNext: hasNext} +} + +// GetHotestNewsList 获取最热帖子 +func GetHotestNewsList(uid uint64, subType int, page, size uint64) (code stderr.Code, data interface{}) { + //优先获取撩吧推荐置顶的 + top := make([]*vidmod.VideoModel, 0) + if page == 1 { + var err error + top, err = vidmod.GetLiaoBaTop() + if err != nil { + return stderr.ErrDbQueryError, nil + } + size = size - uint64(len(top)) + } + //置顶排序 + sort.Sort(vidmod.VideoModelSort(top)) + var ids []primitive.ObjectID + hasNext := false + var err error + if subType == vidmod.TodayRank { + ids, hasNext, err = hotstatmod.GetHotValueToday(page, size) + } + if subType == vidmod.WeekRank { + ids, hasNext, err = hotstatmod.GetHotValueWeek(page, size) + } + if subType == vidmod.MonthRank { + ids, hasNext, err = hotstatmod.GetHotValueMonth(page, size) + } + if err != nil { + return stderr.ErrDbQueryError, nil + } + mInfos, err := vidmod.GetNewsByIDs2M(ids) + if err != nil { + return stderr.ErrDbQueryError, nil + } + topLen := len(top) + sortInfos := make([]*vidmod.VideoModel, 0, len(ids)+topLen) + if topLen > 0 { + sortInfos = append(sortInfos, top...) + } + for _, v := range ids { + if mInfos[v] != nil { + sortInfos = append(sortInfos, mInfos[v]) + } + } + newInfos := vidhelpser.EncodeVideoInfo(uid, sortInfos) + data = vidmod.NewsListResp{List: newInfos, HasNext: hasNext} + return stderr.Success, data +} + +// DoUnlikeVideo 不感兴趣 +func DoUnlikeVideo(uid uint64, vid string) (code stderr.Code, data interface{}) { + oid, err := primitive.ObjectIDFromHex(vid) + if err != nil { + return stderr.ErrParamError, nil + } + fmt.Println(oid) + return stderr.Success, nil +} + +// GetNewsCoinsList 金币专区 +func GetNewsCoinsList(uid uint64, subType int, page, size uint64, head []*vidmod.VideoModel) (stderr.Code, interface{}) { + infos, hasNext, err := vidmod.GetCoinsNews(page, size, subType) + if err != nil { + return stderr.ErrDbQueryError, nil + } + if len(head) > 0 { + head = append(head, infos...) + head = RemoveRep(head) + } else { + head = infos + } + return stderr.Success, vidmod.NewsListResp{List: vidhelpser.EncodeVideoInfo(uid, head), HasNext: hasNext} +} + +// GetOriginalList 新版原创专区 +func GetOriginalList(uid uint64, subType int, page, size uint64) (code stderr.Code, data interface{}) { + /** TODO: + 1.获取博主排序 + 2.获取博主前三则视频 + */ + users, hasNext, err := usermod.OfficialCertList(page, size, subType) + if err != nil { + return stderr.ErrDbQueryError, err + } + var ( + lids []primitive.ObjectID + vids []primitive.ObjectID + wg sync.WaitGroup + m sync.Map + self *usermod.User + mLocation = make(map[primitive.ObjectID]locmod.Location) + mFree = make(map[uint64]bool) + videoDiscLog videodiscountmod.VideoDiscountLog + now = time.Now() + largestDiscount int + ) + wg.Add(len(users)) + for _, v := range users { + go func(u *usermod.User) { + defer wg.Done() + infos, err := GetUserTopWork(u.UID) + if err != nil { + return + } + if len(infos) == 0 { + return + } + var lidOk bool + vidInfos := make([]*vidmod.VideoInfo, len(infos)) + for i, v := range infos { + if !lidOk { + lids = append(lids, v.Location) + lidOk = true + } + vids = append(vids, v.ID) + newType := vidmod.SP + if len(v.NewsType) != 0 { + newType = v.NewsType + } + vidInfos[i] = &vidmod.VideoInfo{ + VideoBase: vidmod.VideoBase{ + ID: v.ID, + NewsType: newType, + Title: v.Title, + PlayTime: v.PlayTime, + Cover: v.Cover, + CoverThumb: v.CoverThumb, + SeriesCover: v.SeriesCover, + PlayCount: v.FakePlayCount, + LikeCount: v.FakeLikeCount, + CommentCount: v.CommentCount, + ShareCount: v.FakeShareCount, + Coins: v.Coins, + Size: v.Size, + Resolution: v.Resolution, + Ratio: v.Ratio, + PurchaseCount: v.PurchaseCount, + CreatedAt: v.CreatedAt, + ReviewAt: v.ReviewAt, + FreeTime: v.FreeTime, + Status: v.Status, + Reason: v.Reason, + SourceURL: v.SourceURL, + H265Url: vidhelpser.H265URLForApp(v), + LinkUrl: v.LinkUrl, + IsHideLocation: v.IsHideLocation, + FreeArea: v.FreeArea, + IsTopping: v.IsTopping, + IsChoosen: v.IsChoosen, + IsRecommend: v.IsRecommend, + Rewarded: v.Rewarded.Add(v.FakeRewarded), + OriginCoins: v.Coins, + }, + UInfo: vidmod.Publisher{ + UInfo: usermod.BaseInfoVip{ + UID: u.UID, + Name: u.Name, + Gender: u.Gender, + Portrait: u.Portrait, + HasLocked: u.HasLocked, + HasBanned: u.HasBanned, + VipLevel: u.VipLevel, + IsVip: u.IsVIP(time.Now()), + SuperUser: u.SuperUser, + ActiveValue: u.ActiveValue, + OfficialCert: u.OfficialCert, + Age: u.Age(), + }, + }, + Location: vidmod.LocInfo{ID: v.Location}, + } + } + m.Store(u.UID, vidInfos) + }(v) + } + wg.Wait() + wg.Add(4) + common.Go(func() { + defer wg.Done() + self, err = usermod.FindUserByUID(uid) + if err != nil { + log.Error("usermod.FindUserByUID", log.Any("uid", uid), log.E(err)) + } + if !self.IsVIP(now) { + largestDiscount, err = productmod.GetLargestDiscountVIPCard() + if err != nil { + log.Error("GetLargestDiscountVIPCard", log.Any("uid", uid), log.E(err)) + } + } + }) + common.Go(func() { + defer wg.Done() + // 查询地理位置 + mLocation, err = locmod.GetLocationInfoByIDs(lids) + if err != nil { + log.Error("usermod.FindUserByUID", log.Any("uid", uid), log.E(err)) + } + }) + common.Go(func() { + defer wg.Done() + mFree = freeVidmod.GetFreeVideoMapByUID(uid) + }) + common.Go(func() { + defer wg.Done() + videoDiscLog, err = videodiscountmod.GetByUID(uid) + if err != nil { + log.Error("videodiscountmod.GetByUID", log.Any("uid", uid), log.E(err)) + } + }) + wg.Wait() + if err != nil { + return stderr.ErrDbQueryError, err + } + //查询购买信息 + mPay, _ := payvidlgmod.IsPay4Videos(uid, vids) + // 排序 + out := make([][]*vidmod.VideoInfo, len(users)) + freeTrialContext := vidhelpser.FreeTrialBadgeContextForUser(uid, self) + for i, v := range users { + if infos, ok := m.Load(v.UID); ok { + for i, info := range infos.([]*vidmod.VideoInfo) { + if item, ok := mLocation[info.Location.ID]; ok { + info.Location = vidmod.LocInfo{ + ID: item.ID, + City: item.City, + Cover: item.Cover, + Visit: item.Visit, + CreatedAt: item.CreatedAt, + } + } + //是否购买 + infos.([]*vidmod.VideoInfo)[i].VidStatus = vidmod.VideoStatus{ + HasPaid: mPay[info.ID], + } + //计算折扣后视频金币数 + infos.([]*vidmod.VideoInfo)[i].Coins = vidhelpser.CalcVideoCoins(info.Coins, v.UID, self, mFree, + videoDiscLog, largestDiscount, nil) + } + vidhelpser.ApplyFreeTrialBadgeToVideoInfos(freeTrialContext, infos.([]*vidmod.VideoInfo)) + out[i] = infos.([]*vidmod.VideoInfo) + } + } + return stderr.Success, vidmod.OriginalInfo{List: out, HasNext: hasNext, Version: vidmod.Version} +} + +// GetHeadNews 获取置顶、力荐、加精 +func GetHeadNews(typ int, lid primitive.ObjectID, isMadou bool) ([]*vidmod.VideoModel, error) { + topping := []*vidmod.VideoModel{} + recomm := []*vidmod.VideoModel{} + chosen := []*vidmod.VideoModel{} + var wg sync.WaitGroup + wg.Add(3) + common.Go(func() { + defer wg.Done() + topping, _ = vidmod.GetTopping(typ, lid, isMadou) + }) + common.Go(func() { + defer wg.Done() + recomm, _ = vidmod.GetReco(typ, lid, isMadou) + }) + common.Go(func() { + defer wg.Done() + chosen, _ = vidmod.GetChosen(typ, lid, isMadou) + }) + wg.Wait() + toppingLen := len(topping) + recommLen := len(recomm) + chosenLen := len(chosen) + head := make([]*vidmod.VideoModel, toppingLen, toppingLen+recommLen+chosenLen) + mFlag := make(map[primitive.ObjectID]bool) + for i, v := range topping { + mFlag[v.ID] = true + head[i] = v + } + for _, v := range recomm { + if !mFlag[v.ID] { + mFlag[v.ID] = true + head = append(head, v) + } + } + for _, v := range chosen { + if !mFlag[v.ID] { + mFlag[v.ID] = true + head = append(head, v) + } + } + return head, nil +} + +func GetVIPVideo(uid, page, size uint64) (stderr.Code, interface{}) { + infos, hasNext, err := vidmod.GetVipList(page, size) + if err != nil { + return stderr.ErrDbQueryError, nil + } + newInfos := vidhelpser.EncodeVideoInfo(uid, infos) + data := vidmod.NewsListResp{List: newInfos, HasNext: hasNext} + return stderr.Success, data +} + +func GetVideosUnderSubModuleNew(subModuleID primitive.ObjectID, uid uint64, req VideoUnderSubModuleReq) (data *VideoUnderSubModuleResp, err error) { + defer func() { + applyFreeTrialBadgeToSubModule(uid, data) + }() + ret := VideoUnderSubModuleResp{ + HasNext: false, + AllVideoInfo: []*vidmod.VideoInfoResp{}, + AllSection: []Section{}, + } + subModule, err := moduleconfmod.GetByID(subModuleID) + if err != nil { + return &ret, err + } + + if subModule.ID.IsZero() { + return &ret, err + } + if !subModule.IsActiveAt(time.Now()) { + return &ret, nil + } + + switch subModule.Type { + case moduleconfmod.HomePage: + cachev2.Classes().CacheTime(time.Minute*5).AutoListKey("91PORN-HomePage").ResBind(&ret).Cache(HandleHomePage, subModule, req) + case moduleconfmod.DeepWeb: + cachev2.Classes().CacheTime(time.Minute*5).AutoListKey("91PORN-DeepWeb").ResBind(&ret).Cache(HandleHaijiaoList, subModule, req) + case moduleconfmod.Cartoon, moduleconfmod.Comics, moduleconfmod.Novel: + cachev2.Classes().CacheTime(time.Minute*5).AutoListKey("91PORN-ACG").ResBind(&ret).Cache(HandleAcg, subModule, req) + case moduleconfmod.Community: //, moduleconfmod.PrivateCircle: + cachev2.Classes().CacheTime(time.Minute*5).AutoListKey("91PORN-Community").ResBind(&ret).Cache(HandleCommunity, subModule, req) + case moduleconfmod.Game, moduleconfmod.Pics: + cachev2.Classes().CacheTime(time.Minute*5).AutoListKey("91PORN-CommunityPicGame").ResBind(&ret).Cache(HandleCommunityPICAndGame, subModule, req) + default: + return + } + return &ret, nil + +} + +func HandleAcg(subModule moduleconfmod.ModuleConf, req VideoUnderSubModuleReq) (data VideoUnderSubModuleResp) { + switch subModule.ShowType { + case moduleconfmod.AllSectionShowType: + // 专题样式 + data = HandleAcgSectionList(subModule, req) + default: + // 海角样式 + data = HandleAcgHaiJiao(subModule, req) + } + return +} + +func HandleAcgHaiJiao(subModule moduleconfmod.ModuleConf, req VideoUnderSubModuleReq) (data VideoUnderSubModuleResp) { + if subModule.Type == moduleconfmod.Cartoon || subModule.Type == moduleconfmod.Comics { + allSection, err := modulesectionmod.GetAllBySubModuleID(subModule.ID) + if err != nil { + return + } + for _, s := range allSection { + sec := Section{ + SectionID: s.ID.Hex(), + SectionName: s.SectionName, + Hot: s.Hot, + } + if s.SectionCover != nil { + sec.SectionCover = *s.SectionCover + } + data.AllSection = append(data.AllSection, sec) + } + } + topIds := []primitive.ObjectID{} + topList := []*mediamod.Media{} + var err error + // 判断当前排序规则是否存在置顶排序 + for _, v := range subModule.HaiJiaoStyle.SortRules { + if v.Val == commod.SortType(req.ModuleSort) && v.Top == true { + // 通过模块id获取acg动漫列表 + topList, _, err = mediamod.GetList(bson.M{ + "mId": subModule.ID, + "sortCode": bson.M{"$gt": 0}, + }, 0, 10, bson.D{ + {Key: "sortCode", Value: -1}, + }) + if err != nil { + log.Error(fmt.Sprintf("HandleAcgHaiJiao mediamod.GetList err:%v", err)) + return + } + for _, vi := range topList { + topIds = append(topIds, vi.ID) + } + break + } + } + filter := bson.M{"mId": subModule.ID, "isDelete": false, "status": 1} + if len(topIds) > 0 { + filter["_id"] = bson.M{"$nin": topIds} + } + sort := acgMediaSort(req.ModuleSort) + switch commod.SortType(req.ModuleSort) { + case commod.New: + // 最新排序已由 acgMediaSort 统一处理。 + case commod.MostHot: + // 最热(热门推荐) + sort = bson.D{{Key: "countLike", Value: -1}, {"createdAt", -1}} + case commod.MostWatch: // 最多观看 + sort = bson.D{{Key: "countBrowse", Value: -1}, {"createdAt", -1}} + case commod.MostCollect: // 最多收藏 + sort = bson.D{{"countCollect", -1}, {"createdAt", -1}} + case commod.HotComment: // 热评 + sort = bson.D{{Key: "hot", Value: -1}, {"createdAt", -1}} + default: + // 最新 + sort = bson.D{{"createdAt", -1}} + } + // 通过模块id获取acg动漫列表 + list, hasNext, err := mediamod.GetList(filter, int64(req.Skip()), int64(req.Limit()), sort) + if err != nil { + log.Error(fmt.Sprintf("HandleAcgHaiJiao mediamod.GetList err:%v", err)) + return + } + if req.PageNumber == 1 && len(topList) > 0 { + list = append(topList, list...) + } + data.HasNext = hasNext + data.AllMediaInfo = mediaser.FillNotStatusMedias(list, false) + return data +} + +func acgMediaSort(moduleSort int) bson.D { + if commod.SortType(moduleSort) == commod.New { + return bson.D{ + {Key: "latestPublishedAt", Value: -1}, + {Key: "contentUpdateTime", Value: -1}, + {Key: "createdAt", Value: -1}, + {Key: "_id", Value: -1}, + } + } + return bson.D{{Key: "createdAt", Value: -1}} +} + +func HandleAcgSectionList(subModule moduleconfmod.ModuleConf, req VideoUnderSubModuleReq) (resp VideoUnderSubModuleResp) { + page := commod.Page{ + PageNumber: req.PageNumber, + PageSize: req.PageSize, + Sort: []commod.Sorts{ + {SortKey: "sort", SortVal: -1}, + {SortKey: "createdAt", SortVal: -1}, + }, + } + sections, hasNext, err := modulesectionmod.GetBySubModuleID(subModule.ID, page) + if err != nil { + log.Error(fmt.Sprintf("HandleCartoonSingleSection modulesectionmod.GetBySubModuleID err:%v", err)) + return resp + } + if len(sections) < 0 { + return resp + } + resp.HasNext = hasNext + wg := &sync.WaitGroup{} + sectionResult := make(chan Section, len(sections)) + for _, v := range sections { + section := v + // 如果是猜你喜欢专题(ACG不存在这个,直接忽略) + if section.ShowType == modulesectionmod.GuessYouLike { + continue + } + wg.Add(1) + common.Go(func() { + defer wg.Done() + num, ok := modulesectionmod.ACGShowTypeHashVale[section.ShowType] + // 兼容旧版本找不到新类型的情况 + if !ok { + // 默认 一大四小 + num = modulesectionmod.ACGShowTypeHashVale[modulesectionmod.FourGrid] + section.ShowType = modulesectionmod.FourGrid + } + secVidMap := Section{ + SectionID: section.ID.Hex(), + SectionTitle: section.SectionTitle, + ShowType: section.ShowType, + SectionName: section.SectionName, + } + if section.Sort != nil { + secVidMap.Sort = *section.Sort + } + if section.SectionCover != nil { + secVidMap.SectionCover = *section.SectionCover + } + //op := options.Find().SetLimit(int64(num)).SetSort(bson.D{{Key: "sortCode", Value: -1}, {Key: "createdAt", Value: -1}}) + op := options.Find().SetLimit(int64(num)).SetSort(bson.D{{Key: "sectionSort", Value: -1}, {Key: "createdAt", Value: -1}}) + mediaBases, err := mediamod.SectionVideosBySectionID(section.ID, op) + if err != nil { + log.Error(fmt.Sprintf("mediamod SectionVideos By SectionID:%v, err:%v", section.ID, err)) + sectionResult <- secVidMap + return + } + + if len(mediaBases) == 0 { + log.Warn("mediamod.SectionVideos no video info") + sectionResult <- secVidMap + return + } + secVidMap.AllMediaInfo = mediaser.FillNotStatusMedias(mediaBases, false) + sectionResult <- secVidMap + }) + } + wg.Wait() + close(sectionResult) + for section := range sectionResult { + resp.AllSection = append(resp.AllSection, section) + } + sort.Slice(resp.AllSection, func(i, j int) bool { + // 正序排 + return resp.AllSection[i].Sort < resp.AllSection[j].Sort + }) + return resp +} + +func HandleHomePage(subModule moduleconfmod.ModuleConf, req VideoUnderSubModuleReq) (data VideoUnderSubModuleResp) { + switch subModule.ShowType { + //case moduleconfmod.NewPageShowType: + // // 首页最新/推荐展示类型 + // return HandleHomePageNew(subModule, req) + case moduleconfmod.HJShowType: + // 海角系 专题+列表 + return HandleHaijiaoList(subModule, req) + case moduleconfmod.AllSectionShowType: + // 专题展示样式 + return HandleSectionList(subModule, req) + default: + return HandleHaijiaoList(subModule, req) + } + return +} + +func HandleHaijiaoList(subModule moduleconfmod.ModuleConf, req VideoUnderSubModuleReq) (data VideoUnderSubModuleResp) { + var ( + videos []*vidmod.VideoModel + topVideos []*vidmod.VideoInfoResp + chosenVideos []*vidmod.VideoInfoResp + ) + // 展示专题 + if subModule.HaiJiaoStyle.SectionStyle > 0 { + allSection, err := modulesectionmod.HomePageGetBySubModuleID(subModule.ID) + if err != nil { + return + } + for _, s := range allSection { + sec := Section{ + SectionID: s.ID.Hex(), + SectionName: s.SectionName, + Hot: s.Hot, + } + if s.SectionCover != nil { + sec.SectionCover = *s.SectionCover + } + data.AllSection = append(data.AllSection, sec) + } + } + + wg := &sync.WaitGroup{} + + if subModule.HaiJiaoStyle.ShowChosenVideo == 1 { + wg.Add(1) + common.Go(func() { + defer wg.Done() + // 获取推荐的数据 + filter := bson.M{"mId": subModule.ID.Hex(), "newsType": vidmod.SP, "status": vidmod.CheckPass, "chosen": true} + sort := bson.D{{"isTopping", -1}, {"chosenDate", -1}} + opt := options.Find(). + SetSort(sort). + SetLimit(20). + SetProjection(bson.M{"richText": 0}) + + // 首页精选视频 + list, err := vidmod.GetVideoListByCond(filter, opt) // 获取视频详情 + if err != nil { + log.Error(fmt.Sprintf("HandleAllList vidmod.GetVideoListByCond err:%v", err)) + return + } + chosenVideos = vidhelpser.NewEncodeVideoInfoNotStatus(list) + }) + } + + topIds := []primitive.ObjectID{} + // 判断当前排序规则是否存在置顶排序 + for _, v := range subModule.HaiJiaoStyle.SortRules { + if v.Val == commod.SortType(req.ModuleSort) && v.Top == true { + // 获取置顶数据 + wg.Add(1) + common.Go(func() { + defer wg.Done() + // 获取置顶的数据 + filter := bson.M{"mId": subModule.ID.Hex(), "liaoBaTopSort": bson.M{"$gt": 0}, "chosen": false} + sort := bson.D{{"liaoBaTopSort", -1}} + opt := options.Find(). + SetSort(sort). + SetLimit(10). + SetProjection(bson.M{"richText": 0}) + + // 首页精选视频 + list, err := vidmod.GetVideoListByCond(filter, opt) // 获取视频详情 + if err != nil { + log.Error(fmt.Sprintf("HandleAllList vidmod.GetVideoListByCond err:%v", err)) + return + } + topVideos = vidhelpser.NewEncodeVideoInfoNotStatus(list) + for _, video := range topVideos { + topIds = append(topIds, video.ID) + } + }) + break + } + } + wg.Wait() + // 首页 + filter := bson.M{ + "mId": subModule.ID.Hex(), + "status": vidmod.CheckPass, + "newsType": vidmod.SP, + "chosen": false, + } + if len(topIds) > 0 { + filter["_id"] = bson.M{"$nin": topIds} + } + sort := bson.D{{"reviewAt", -1}} + switch commod.SortType(req.ModuleSort) { + case commod.New: + // 最新 + sort = bson.D{{"reviewAt", -1}} + case commod.MostHot: + // 最热(热门推荐) + sort = bson.D{{"liaoBaTopSort", -1}, {Key: "likeCount", Value: -1}, {"reviewAt", -1}} + case commod.MostWatch: + // 最多观看 + sort = bson.D{{Key: "playCount", Value: -1}, {"reviewAt", -1}} + case commod.MostCollect: + // 最多收藏 + sort = bson.D{{"collectCount", -1}, {"reviewAt", -1}} + case commod.HotComment: // 热度值 + sort = bson.D{{"hot", -1}, {"reviewAt", -1}} + default: + // 最新 + sort = bson.D{{"reviewAt", -1}} + } + opt := options.Find().SetSort(sort).SetSkip(req.Skip64()).SetLimit(req.Limit64() + 1).SetProjection(bson.M{"richText": 0}) + var err error + videos, err = vidmod.GetVideoListByCond(filter, opt) // 获取视频详情 + if err != nil { + log.Error(fmt.Sprintf("vidmod.GetVideoListByCond err:%v", err)) + return + } + if len(videos) > int(req.Limit()) { + data.HasNext = true + videos = videos[:req.Limit()] + } + data.AllVideoInfo = vidhelpser.NewEncodeVideoInfoNotStatus(videos) + if req.PageNumber == 1 && len(topVideos) > 0 { + // 第一页,直接插入到最前面 + data.AllVideoInfo = append(topVideos, data.AllVideoInfo...) + } + data.ChosenVideoInfo = chosenVideos + return +} + +func HandleSectionList(subModule moduleconfmod.ModuleConf, req VideoUnderSubModuleReq) (resp VideoUnderSubModuleResp) { + page := commod.Page{ + PageNumber: req.PageNumber, + PageSize: req.PageSize, + Sort: []commod.Sorts{ + {SortKey: "sort", SortVal: -1}, + {SortKey: "createdAt", SortVal: -1}, + }, + } + sections, hasNext, err := modulesectionmod.GetBySubModuleID(subModule.ID, page) + if err != nil { + log.Error(fmt.Sprintf("HandleSectionList modulesectionmod.GetBySubModuleID err:%v", err)) + return resp + } + if len(sections) < 0 { + return resp + } + resp.HasNext = hasNext + wg := &sync.WaitGroup{} + sectionResult := make(chan Section, len(sections)) + for _, v := range sections { + section := v + wg.Add(1) + common.Go(func() { + defer wg.Done() + num := modulesectionmod.ShowTypeHashVale[section.ShowType] + if num == 0 { + num = int(req.PageSize) + } + num = num + 2 // 满足ab测需求,额外多返回2个 + secVidMap := Section{ + SectionID: section.ID.Hex(), + SectionTitle: section.SectionTitle, + ShowType: section.ShowType, + SectionName: section.SectionName, + Hot: section.Hot, + } + if section.Sort != nil { + secVidMap.Sort = *section.Sort + } + if section.SectionCover != nil { + secVidMap.SectionCover = *section.SectionCover + } + if section.ShowType == modulesectionmod.GuessYouLike { + sectionResult <- secVidMap + return + } + var ( + sectionVideos []modulevidmod.SectionVideo + videos []*vidmod.VideoModel + ) + opt := options.Find().SetSort(bson.D{{Key: "sortCode", Value: -1}, {Key: "videoReviewedAt", Value: -1}}).SetLimit(int64(num)) + sectionVideos, err = modulevidmod.SectionVideosBySectionID(section.ID, opt) + if err != nil { + log.Error("GetSectionVideosBySectionID", log.Any("sectionID", section.ID), log.E(err)) + return + } + if len(sectionVideos) == 0 { + log.Warn("modulevidmod.SectionVideosBySectionID empty video list", log.Any("sectionID", section.ID)) + return + } + videoIDs := make([]primitive.ObjectID, len(sectionVideos)) + for i, v := range sectionVideos { + videoIDs[i] = v.VideoID + } + // 获取视频详情 + if videos, err = vidmod.GetVideoListByIDs(videoIDs); err != nil { + log.Error("vidmod.GetVideoListByIDs", log.Any("videoIDs", videoIDs), log.E(err)) + return + } + if len(videos) == 0 { + log.Warn("vidmod.GetVideoListByIDs no video info", log.Any("videosIDs", videoIDs)) + return + } + // 根据专题中的重新排序 + sortVideos := []*vidmod.VideoModel{} + videoMap := make(map[primitive.ObjectID]*vidmod.VideoModel) + for _, video := range videos { + videoMap[video.ID] = video + } + for _, video := range sectionVideos { + vi, ok := videoMap[video.VideoID] + if ok { + sortVideos = append(sortVideos, vi) + } + } + videoList := vidhelpser.NewEncodeVideoInfoNotStatus(sortVideos) + secVidMap.AllVideoInfo = videoList + sectionResult <- secVidMap + }) + } + wg.Wait() + close(sectionResult) + for section := range sectionResult { + resp.AllSection = append(resp.AllSection, section) + } + sort.Slice(resp.AllSection, func(i, j int) bool { + // 正序排 + return resp.AllSection[i].Sort < resp.AllSection[j].Sort + }) + return +} + +func HandleCommunity(subModule moduleconfmod.ModuleConf, req VideoUnderSubModuleReq) (data VideoUnderSubModuleResp) { + var ( + videos []*vidmod.VideoModel + videoTagIds []primitive.ObjectID + err error + next bool + ) + + allSection, err := modulesectionmod.GetAllBySubModuleID(subModule.ID) + if err != nil { + return + } + for _, s := range allSection { + var allTag []Tag + if s.Tags != nil && len(*s.Tags) > 0 && s.TagIds != nil && len(*s.TagIds) > 0 { + videoTagIds = append(videoTagIds, *s.TagIds...) + tags, err := tagmod.FindOneTagByIds(*s.TagIds) + if err != nil { + continue + } + if tags != nil && len(tags) > 0 { + for _, t := range tags { + allTag = append(allTag, Tag{ + ID: t.ID, + TagName: t.TagName, + CoverImg: t.CoverImg, + VideoCount: t.VideoCount, + HotMark: t.HotMark, + }) + } + } + } + sec := Section{ + SectionID: s.ID.Hex(), + SectionName: s.SectionName, + AllTags: allTag, + } + if s.SectionCover != nil { + sec.SectionCover = *s.SectionCover + } + data.AllSection = append(data.AllSection, sec) + } + tagId, _ := primitive.ObjectIDFromHex(req.TagId) + if !tagId.IsZero() { + videoTagIds = []primitive.ObjectID{tagId} + } + if len(videoTagIds) == 0 { + return data + } + + var ( + topVideos []*vidmod.VideoModel + now = time.Now() + filter = bson.M{} + sort = bson.D{} + opt = options.Find().SetSkip(req.Skip64()).SetLimit(req.Limit64()).SetProjection(bson.M{"richText": 0}) + ) + + // 社区置顶,第一页查询置顶数据,没有时间限制 + if req.ModuleSort == 7 && req.PageNumber == 1 { + topFilter := bson.M{ + "tags": bson.M{"$in": videoTagIds}, + "status": vidmod.CheckPass, + "liaoBaTopSort": bson.M{"$gt": 0}, + } + topSort := bson.D{ + {"liaoBaTopSort", -1}, + {"reviewAt", -1}, + } + + topOpt := options.Find(). + SetLimit(10). + SetSort(topSort). + SetProjection(bson.M{"richText": 0}) + + topVideos, _, err = vidmod.FindList(topFilter, topOpt) // 获取视频详情 + if err != nil { + log.Error(fmt.Sprintf("vidmod.FindList get top vid err:%v", err)) + return + } + } + + //帖子排序 + //最新:最新上架的 sortType = 1 + //最热:收藏量 10-120天 sortType = 2 + //推荐: 收藏量 14天内 sortType = 7 + //热评:hot排序 1年内 sortType = 9 + + filter = bson.M{"tags": bson.M{"$in": videoTagIds}, "status": vidmod.CheckPass} + switch req.ModuleSort { + case 2: // 精选/最多喜欢 // x天内点赞最多的 + filter["reviewAt"] = bson.M{"$gte": now.AddDate(0, -4, 0), "$lt": now.AddDate(0, -1, 0)} + sort = bson.D{{"collectCount", -1}, {"reviewAt", -1}} + + case 3: // 推荐 + filter["reviewAt"] = bson.M{"$gte": now.Add(-time.Hour * 24 * 30)} + sort = bson.D{{"playCount", -1}, {"reviewAt", -1}} + + case 6: // 社区视频 x天内观看最多(视频) + filter["reviewAt"] = bson.M{"$gte": now.Add(-time.Hour * 24 * 30)} + filter["newsType"] = vidmod.SP + sort = bson.D{{"playCount", -1}, {"reviewAt", -1}} + + case 7: // 推荐/最多收藏 x天内收藏最多 + filter["reviewAt"] = bson.M{"$gte": now.Add(-time.Hour * 24 * 14)} + filter["liaoBaTopSort"] = 0 + sort = bson.D{{"collectCount", -1}, {"reviewAt", -1}} + case 9: // 热评 + filter["reviewAt"] = bson.M{"$gte": now.AddDate(-1, 0, 0)} + sort = bson.D{{"hot", -1}, {"reviewAt", -1}} + + case 1: // 最新 + fallthrough + default: + sort = bson.D{{"reviewAt", -1}} + } + + opt = opt.SetSort(sort) + + videos, next, err = vidmod.FindList(filter, opt) // 获取视频详情 + if err != nil { + log.Error(fmt.Sprintf("vidmod.GetVideoListByCond err:%v", err)) + return + } + if len(topVideos) > 0 { + videos = append(topVideos, videos...) + } + if len(videos) == 0 { + log.Warn(fmt.Sprintf("vidmod.GetVideoListByCond is null, subModuleID:%v", subModule.ID.Hex())) + return + } + + data.HasNext = next + data.AllVideoInfo = vidhelpser.NewEncodeVideoInfoNotStatus(videos) + + // tagId 不为空,找出video的tag, 只返回一个,前端统一 + if !tagId.IsZero() { + var tag vidmod.TagInfo + for _, video := range data.AllVideoInfo { + if tag.ID.IsZero() { + for _, t := range video.Tags { + if t.ID == tagId { + tag = t + } + } + } + video.Tags = append(video.Tags[0:0], tag) + } + } + return +} + +func HandleCommunityPICAndGame(subModule moduleconfmod.ModuleConf, req VideoUnderSubModuleReq) (data VideoUnderSubModuleResp) { + var ( + err error + videos []*vidmod.VideoModel + next bool + ) + var ( + filter = bson.M{} + sort = bson.D{} + someTimeAgo = time.Now().AddDate(-3, 0, 0) // 给个默认时间统一走索引 + opt = options.Find().SetSkip(req.Skip64()).SetLimit(req.Limit64()).SetProjection(bson.M{"richText": 0}) + ) + // 黄油 (本项目没用到) + //最热:根据点赞量排序 sortType = 2 + //最新:根据上架时间排序 sortType = 1 + //畅销:根据解锁次数排序 sortType = 8 + + //图集 + //最多观看:观看量最多 sortType = 3 + //最新发布:根据上架时间排序 sortType = 1 + //最多收藏:根据收藏量排序 sortType = 7 + + filter = bson.M{"mId": subModule.ID.Hex(), "status": vidmod.CheckPass} + switch req.ModuleSort { + case 2: // 精选/最多喜欢 // x天内点赞最多的 + filter["reviewAt"] = bson.M{"$gte": someTimeAgo} + sort = bson.D{{"liaoBaTopSort", -1}, {"likeCount", -1}, {"reviewAt", -1}} + case 3: // 最热/最多播放 x天内观看量 + filter["reviewAt"] = bson.M{"$gte": someTimeAgo} + sort = bson.D{{"playCount", -1}, {"reviewAt", -1}} + + case 7: // 推荐/最多收藏 x天内收藏最多 + filter["reviewAt"] = bson.M{"$gte": someTimeAgo} + sort = bson.D{{"collectCount", -1}, {"reviewAt", -1}} + + case 8: // 购买人数 + filter["reviewAt"] = bson.M{"$gte": someTimeAgo} + sort = bson.D{{"purchaseCount", -1}, {"reviewAt", -1}} + case 1: // 最新 + fallthrough + default: + sort = bson.D{{"reviewAt", -1}} + } + + videos, next, err = vidmod.FindList(filter, opt.SetSort(sort)) // 获取视频详情 + if err != nil { + log.Error(fmt.Sprintf("vidmod.GetVideoListByCond err:%v", err)) + return + } + if len(videos) == 0 { + log.Warn(fmt.Sprintf("vidmod.GetVideoListByCond is null, subModuleID:%v", subModule.ID.Hex())) + return + } + + data.HasNext = next + data.AllVideoInfo = vidhelpser.NewEncodeVideoInfoNotStatus(videos) + return +} + +// GetCommunityRecommend 社区-图集和黄油推荐 +func GetCommunityRecommend(req CommunityRecommendReq) (data *VideoUnderSubModuleResp, err error) { + ret := VideoUnderSubModuleResp{ + HasNext: false, + AllVideoInfo: []*vidmod.VideoInfoResp{}, + AllSection: []Section{}, + } + excluded, err := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), true) + if err != nil { + return &ret, err + } + if len(excluded) > 0 { + result := HandleHotRecommend(req) + return &result, nil + } + expire := time.Minute * 5 + _, err = cachev2.Classes().CacheTime(expire).AutoListKey("CommunityRecommend").ResBind(&ret).Cache(HandleHotRecommend, req) + if err != nil { + return + } + + return &ret, nil +} + +// HandleHotRecommend 社区热门推荐 +func HandleHotRecommend(req CommunityRecommendReq) (resp VideoUnderSubModuleResp) { + var ( + sort bson.D + someTimeAgo = time.Now().AddDate(0, -3, 0) + filter = bson.M{"newsType": req.NewsType, "status": vidmod.CheckPass, "reviewAt": bson.M{"$gte": someTimeAgo}} + opt = options.Find().SetSkip(int64(req.Skip())).SetLimit(int64(req.Limit())).SetProjection(bson.M{"richText": 0}) + ) + excluded, err := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), true) + if err != nil { + return + } + if len(excluded) > 0 { + filter["mId"] = bson.M{"$nin": excluded} + } + + // 黄油 90天内的帖子 + //最热:根据点赞量排序 sortType = 2 + //最新:根据上架时间排序 sortType = 1 + //畅销:根据解锁次数排序 sortType = 8 + //图集 90天内的帖子 + //最多观看:观看量最多 sortType = 3 + //最新发布:根据上架时间排序 sortType = 1 + //最多收藏:根据收藏量排序 sortType = 7 + + switch req.SortType { + case 2: // 精选/最多喜欢 // x天内点赞最多的 + filter["reviewAt"] = bson.M{"$gte": someTimeAgo} + sort = bson.D{{"likeCount", -1}, {"reviewAt", -1}} + + case 3: // 最热/最多播放 x天内观看量 + filter["reviewAt"] = bson.M{"$gte": someTimeAgo} + sort = bson.D{{"playCount", -1}, {"reviewAt", -1}} + + case 7: // 推荐/最多收藏 x天内收藏最多 + filter["reviewAt"] = bson.M{"$gte": someTimeAgo} + sort = bson.D{{"liaoBaTopSort", -1}, {"collectCount", -1}, {"reviewAt", -1}} + + case 8: // 购买人数 + filter["reviewAt"] = bson.M{"$gte": someTimeAgo} + sort = bson.D{{"purchaseCount", -1}, {"reviewAt", -1}} + + case 1: // 最新 + fallthrough + default: + sort = bson.D{{"reviewAt", -1}} + } + + videos, next, err := vidmod.FindList(filter, opt.SetSort(sort)) // 获取视频详情 + if err != nil { + log.Error(fmt.Sprintf("vidmod.GetVideoListByCond err:%v", err)) + return + } + if len(videos) == 0 { + return + } + + resp.HasNext = next + resp.AllVideoInfo = vidhelpser.NewEncodeVideoInfoNotStatusForRecommend(videos) + + return resp +} + +// IncSectionHits 增加专题点击量 +func IncSectionHits(ctx context.Context, sectionID primitive.ObjectID) error { + now := time.Now() + date := now.Add(-12 * time.Hour).Local().Format("2006-01-02") + return sectionstatmod.UpsertSectionHits(sectionID, date) +} + +// GetBloggerVideos 获取某一原创博主的视频列表 +func GetBloggerVideos(uid, bloggerID uint64, page commod.Page) (resp VideoUnderSectionResp, err error) { + videos, hasNext, err := vidmod.GetBloggerVideos(bloggerID, int64(page.Skip()), int64(page.Limit())) + if err != nil { + return + } + resp.HasNext = hasNext + bloggerInfo, err := getOriginalBloggerInfo([]uint64{bloggerID}) + if err != nil { + return + } + resp.OriginalBloggerInfo = bloggerInfo[bloggerID] + resp.Videos = vidhelpser.NewEncodeVideoInfo(uid, videos) + sort.Slice(resp.Videos, func(i, j int) bool { + return resp.Videos[i].CreatedAt.After(resp.Videos[j].CreatedAt) + }) + return +} + +func GetSectionVideos(uid uint64, sectionID primitive.ObjectID, page commod.Page, sortType string, playTimeType int) ( + out VideoUnderSectionResp, err error, +) { + var sectionVideos []modulevidmod.SectionVideo + defer func() { + vidhelpser.ApplyFreeTrialBadgeToVideoInfoResps(vidhelpser.LoadFreeTrialBadgeContext(uid), out.Videos) + }() + section, err := modulesectionmod.GetBySectionByID(sectionID) + if err != nil { + return out, err + } + if section.ID.IsZero() { + return out, errors.New("section not found") + } + active, err := moduleconfmod.CanBrowseModule(section.SubModuleID, time.Now()) + if err != nil { + return out, err + } + if !active { + return out, nil + } + key := redisconst.SectionVideoInfoCacheKey(sectionID.Hex(), sortType, int64(page.PageSize), int64(page.PageNumber)) + str, err := appg.Redis.Get(key) + if err != nil { + log.Warn(fmt.Sprintf("用户ID:%d;缓存获取视频列表信息异常:%v", uid, err)) + } + + if str != nil { + if err = msgpack.Unmarshal([]byte(*str), &out); err == nil { + return out, nil + } + log.Warn(fmt.Sprintf("用户ID:%d;解析缓存数据异常:%v", uid, err)) + } + limit100 := false + skip := page.Skip() + if strings.TrimSpace(section.SectionName) == "最新" { // "最新"栏最多返回100条 + limit100 = true + if skip >= 100 { + return out, nil + } + } + // 默认专题排序 + if sortType == "" && playTimeType == 0 { + opts := options.Find().SetLimit(int64(page.PageSize + 1)).SetSkip(int64((page.PageNumber - 1) * page.PageSize)). + SetSort(bson.D{{Key: "sortCode", Value: -1}, {Key: "videoReviewedAt", Value: -1}}) + sectionVideos, err = modulevidmod.Find(bson.M{"sectionID": sectionID}, opts) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;专题ID:%v;获取专题视频异常:%v", uid, sectionID, err)) + return out, err + } + out.HasNext = false + if uint64(len(sectionVideos)) > page.PageSize { + sectionVideos = sectionVideos[:page.PageSize] + out.HasNext = true + } + } else { + sectionVideos, err = modulevidmod.GetSectionVideosBySectionID(sectionID) + if err != nil { + return out, err + } + } + videoIDs := make([]primitive.ObjectID, len(sectionVideos)) + for i, v := range sectionVideos { + videoIDs[i] = v.VideoID + } + if len(videoIDs) == 0 { + out.Videos = []*vidmod.VideoInfoResp{} + return out, nil + } + cond := bson.M{"_id": bson.M{"$in": videoIDs}, "status": 1} + /* 19porn used sort + 热门推荐:一个月内视频按点赞量排序 支持运营手动设置推荐 + 最新上架:根据发布的时间顺序进行排序,无限下滑 + 最新热评:按照用户评论排序。优先级在置顶排序后边,按天刷新 + */ + var s bson.D + switch sortType { + case "watch": + s = bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}, {Key: "_id", Value: -1}} + case "new", "": + s = bson.D{{Key: "reviewAt", Value: -1}, {Key: "_id", Value: -1}} + case "collect": + s = bson.D{{Key: "collectCount", Value: -1}, {"reviewAt", -1}, {Key: "_id", Value: -1}} + case "like": + s = bson.D{{Key: "likeCount", Value: -1}, {"reviewAt", -1}, {Key: "_id", Value: -1}} + case "comment": // hot comment, sort value , from task timed statistics of 'SKD' server got it + s = bson.D{{Key: "hot", Value: -1}, {"reviewAt", -1}, {Key: "_id", Value: -1}} + default: //兼容旧版,传入参数为其他值 + s = bson.D{{"reviewAt", -1}, {Key: "_id", Value: -1}} + } + videos, total, err := vidmod.GetvideoListByMapCond(cond, s, page.PageNumber, page.PageSize) + if err != nil { + return out, err + } + var newVideos []*vidmod.VideoModel + if playTimeType == 0 && sortType == "" { + newVideos = make([]*vidmod.VideoModel, 0, len(sectionVideos)) + for _, sv := range sectionVideos { + for _, v := range videos { + if sv.VideoID == v.ID { + newVideos = append(newVideos, v) + break + } + } + } + } else { + newVideos = videos + } + if int64(page.PageNumber*page.PageSize) < total { + out.HasNext = true + } + if newVideosLen := uint64(len(newVideos)); limit100 && skip+newVideosLen >= 100 { + out.HasNext = false + newVideos = newVideos[:100-skip] + } + out.Videos = vidhelpser.NewEncodeVideoInfo(uid, newVideos) + + common.Go(func() { + if out.Videos != nil || len(out.Videos) > 0 { + d, err := msgpack.Marshal(out) + if err != nil { + return + } + if err = appg.Redis.Set(key, d, 10*time.Minute); err != nil { + log.Warn(fmt.Sprintf("用户ID:%d;保存缓存数据异常:%v", uid, err)) + } + } + }) + return out, nil +} + +func GetVideosUnderSection(uid uint64, sectionID primitive.ObjectID, page commod.Page, sortType string, playTimeType int) ( + resp VideoUnderSectionResp, err error) { + section, err := modulesectionmod.GetBySectionByID(sectionID) + if err != nil { + return resp, err + } + active, err := moduleconfmod.CanBrowseModule(section.SubModuleID, time.Now()) + if err != nil || !active { + return resp, err + } + sectionVideos, err := modulevidmod.GetSectionVideosBySectionID(sectionID) + if err != nil { + return + } + videoIDs := make([]primitive.ObjectID, len(sectionVideos)) + for i, v := range sectionVideos { + videoIDs[i] = v.VideoID + } + cond := bson.M{"status": 1, "_id": bson.M{"$in": videoIDs}} + switch playTimeType { + case 1: + cond["playTime"] = bson.M{"$gte": 60 * 10} + case 2: + cond["playTime"] = bson.M{"$lt": 60 * 10} + } + var s bson.D + switch sortType { + case "hot": + videos, _, hasNext, err := vidmod.GetHotList(int64((page.PageNumber-1)*page.PageSize), int64(page.PageSize), cond) + vInfos := vidhelpser.NewEncodeVideoInfo(uid, videos) + resp.Videos = vInfos + resp.HasNext = hasNext + return resp, err + case "watch": + s = bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}} + case "like": + s = bson.D{{Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}} + case "new", "": + s = bson.D{{Key: "reviewAt", Value: -1}} + default: //兼容旧版,传入参数为其他值 + s = bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}} + } + videos, total, err := vidmod.GetvideoListByMapCond(cond, s, page.PageNumber, page.PageSize) + if err != nil { + return resp, err + } + videoInfo := vidhelpser.NewEncodeVideoInfo(uid, videos) + resp.Videos = videoInfo + if int64(page.PageNumber*page.PageSize) < total { + resp.HasNext = true + } + resp.OriginalBloggerInfo = OriginalBloggerInfo{} + return +} + +type HotVideoRes struct { + List []*vidmod.VideoInfo `json:"data"` + HasNext bool `json:"hasNext"` +} + +// GetHotVideo 获取最热视频 +func GetHotVideo(page, size int64, t int) (res HotVideoRes, err error) { + key := fmt.Sprintf("hot-videos:page%d:size%d:t%d", page, size, t) + expire := time.Minute * 10 + s, err := appg.Redis.Get(key) + if err != nil { + return + } + if s != nil && *s != "" { + _ = json.Unmarshal([]byte(*s), &res) + } + if len(res.List) == 0 { + start := time.Now().AddDate(0, 0, -7) + end := time.Now() + switch t { + case 1: // 本月最热 + start = time.Now().AddDate(0, -1, 0) + case 2: // 上月最热 + start = time.Now().AddDate(0, -2, 0) + end = time.Now().AddDate(0, -1, 0) + } + + var videos []*vidmod.VideoModel + videos, res.HasNext, err = vidmod.GetHotVideoByOnlineTimeRange(start, end, page, size) + if err != nil { + return HotVideoRes{}, err + } + + res.List = vidhelpser.EncodeVideoInfoNoUID(videos) + common.Go(func() { + b, _ := json.Marshal(res) + _ = appg.Redis.Set(key, string(b), expire) + }) + } + return +} + +func GetHotPublisher(uid uint64) (data []*proto.HotPublisher, err error) { + data = make([]*proto.HotPublisher, 0) + return + key := "getHotpublisher:vidser" + expaire := time.Minute * 10 + s, err := appg.Redis.Get(key) + if err != nil { + return + } + if s != nil && *s != "" { + _ = json.Unmarshal([]byte(*s), &data) + } + if len(data) == 0 { + now := time.Now() + d := timeutil.BeginningOfDay(now) + reidsKey := redisconst.PlayLeaderboardKey("publiser", d.Unix()) + var publiserSli []string + publiserSli, err = appg.Redis.ZRange(reidsKey, 0, 20) + if err != nil { + return + } + //数据量少用头一天的 + if len(publiserSli) < 20 { + reidsKey = redisconst.PlayLeaderboardKey("publiser", d.Add(-time.Hour*24).Unix()) + publiserSli, err = appg.Redis.ZRange(reidsKey, 0, 20) + if err != nil { + return + } + } + publiserUIDs := make([]uint64, len(publiserSli)) + for i := range publiserSli { + publiserUIDs[i], err = strconv.ParseUint(publiserSli[i], 10, 64) + if err != nil { + publiserUIDs[i] = 0 + } + } + data = make([]*proto.HotPublisher, 0, len(publiserUIDs)) + for _, v := range publiserUIDs { + if v == 0 { + continue + } + hotPublisherVid := proto.HotPublisher{ + BaseInfo: usermod.BaseInfo{}, + List: make([]*vidmod.VideoInfo, 0), + } + var u *usermod.User + u, err = usermod.FindUserByUID(v) + if err != nil || u == nil { + return + } + hotPublisherVid.BaseInfo = usermod.BaseInfo{ + UID: u.UID, + Name: u.Name, + Gender: u.Gender, + Portrait: u.Portrait, + HasLocked: u.HasLocked, + HasBanned: u.HasBanned, + } + var vidMods []*vidmod.VideoModel + vidMods, _, _, err = vidmod.GetVideoListByPublishers([]uint64{v}, 1, 4) + if err != nil || len(vidMods) <= 0 { + return + } + hotPublisherVid.List = vidhelpser.EncodeVideoInfo(uid, vidMods) + data = append(data, &hotPublisherVid) + } + common.Go(func() { + b, _ := json.Marshal(data) + _ = appg.Redis.Set(key, string(b), expaire) + }) + } + return +} + +func getOriginalBloggerInfo(uids []uint64) (info map[uint64]OriginalBloggerInfo, err error) { + users, err := usermod.FindUsersByUID(uids) + if err != nil { + return + } + info = make(map[uint64]OriginalBloggerInfo) + for _, user := range users { + info[user.UID] = OriginalBloggerInfo{ + UID: user.UID, + Name: user.Name, + Portrait: user.Portrait, + OfficialCert: user.OfficialCert, + Summary: user.Summary, + } + } + return +} + +// List 新版视频列表 +func List(uid uint64, req *vidmod.AppListReq) (interface{}, stderr.Code) { + var ( + head []*vidmod.VideoModel + err error + ) + data, _ := json.Marshal(req) + log.Info(fmt.Sprintf("新版视频列表请求参数:%s", string(data))) + filter, err := req.Filter(uid) + if err != nil { + log.Error(fmt.Sprintf("获取视频列表 发生错误:%v", err), log.Any("uid", uid), log.Any("params", req)) + return nil, stderr.ErrParamError + } + log.Info("---", log.Any("filter", filter)) + hasNext := false + if req.Type == vidmod.Image && req.Model == vidmod.HappinessPlaza { + head, err = getImageTopList(req.FilterType, filter, req.Options()) + if err != nil { + log.Error(fmt.Sprintf("获取幸福广场列表 发生错误:%v", err), log.Any("uid", uid), log.Any("params", req)) + return nil, stderr.ErrDbQueryError + } + startIndex, endIndex := common.SlicePage(req.PageNumber, req.PageSize, uint64(len(head))) + head = head[startIndex:endIndex] + if len(head) >= int(req.PageSize) { + hasNext = true + } + newInfos := vidhelpser.EncodeVideoInfo(uid, head) + return vidmod.NewsListResp{List: newInfos, HasNext: hasNext}, stderr.Success + } + // 首先从redis缓存内获取相关信息 + redisKey := req.RedisKey(uid) + if redisKey != "" { + ct, err := appg.Redis.Get(redisKey) + if err != nil && ct != nil && *ct != "" { + if err = json.Unmarshal([]byte(*ct), &head); err == nil { + if len(head) > int(req.PageSize) { + hasNext = true + head = head[:req.PageSize] + } + newInfos := vidhelpser.EncodeVideoInfo(uid, head) + return vidmod.NewsListResp{List: newInfos, HasNext: hasNext}, stderr.Success + } + } + } + list, err := vidmod.GetList(filter, req.Options()) + if err != nil { + log.Error(fmt.Sprintf("获取视频列表 发生错误:%v", err), log.Any("uid", uid), log.Any("params", req)) + return nil, stderr.ErrDbQueryError + } + // 设置/刷新redis缓存 + if redisKey != "" { + go func() { + bts, err := json.Marshal(list) + if err != nil { + return + } + _ = appg.Redis.Set(redisKey, string(bts), time.Minute*3) + }() + } + head = list + if len(list) > int(req.PageSize) { + hasNext = true + head = list[:req.PageSize] + } + newInfos := vidhelpser.EncodeVideoInfo(uid, head) + return vidmod.NewsListResp{List: newInfos, HasNext: hasNext}, stderr.Success +} + +func AllVideosOfModule(subModuleID primitive.ObjectID, uid uint64, req AllVideosOfModuleReq) (AllVideosOfModuleResp, error) { + ret := AllVideosOfModuleResp{ + HasNext: false, + Videos: make([]*vidmod.VideoInfo, 0), + } + active, err := moduleconfmod.CanBrowseModule(subModuleID, time.Now()) + if err != nil || !active { + return ret, err + } + sections, err := modulesectionmod.GetAllBySubModuleID(subModuleID) + if err != nil { + return ret, err + } + sectionIds := make([]primitive.ObjectID, len(sections)) + for i, v := range sections { + sectionIds[i] = v.ID + } + sectionVideos, err := modulevidmod.GetVideosBySectionIDs(sectionIds) + if err != nil { + return ret, err + } + vids := make([]primitive.ObjectID, len(sectionVideos)) + for i, v := range sectionVideos { + vids[i] = v.VideoID + } + cond := bson.M{"status": 1, "_id": bson.M{"$in": vids}} + switch req.PlayTimeType { + case 1: + cond["playTime"] = bson.M{"$gte": 60 * 10} + case 2: + cond["playTime"] = bson.M{"$lt": 60 * 10} + } + var sort bson.D + switch req.SortType { + case "hot": + videos, _, hasNext, err := vidmod.GetHotList(int64((req.PageNumber-1)*req.PageSize), int64(req.PageSize), cond) + ret.Videos = vidhelpser.EncodeVideoInfo(uid, videos) + ret.HasNext = hasNext + return ret, err + case "watch": + sort = bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}} + case "like": + sort = bson.D{{Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}} + case "new", "": + sort = bson.D{{Key: "reviewAt", Value: -1}} + default: //兼容旧版,传入参数为其他值 + sort = bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}} + } + videos, total, err := vidmod.GetvideoListByMapCond(cond, sort, req.PageNumber, req.PageSize) + if err != nil { + return ret, err + } + vInfos := vidhelpser.EncodeVideoInfo(uid, videos) + ret.Videos = vInfos + if int64(req.PageNumber*req.PageSize) < total { + ret.HasNext = true + } + return ret, nil +} + +// 获取幸福广场置顶帖子 +func getImageTopList(topType uint, filter1 bson.M, opts *options.FindOptions) ([]*vidmod.VideoModel, error) { + key := fmt.Sprintf(redisconst.ImageTopKey, topType) + //读取缓存 + s, err := appg.Redis.Get(key) + if err != nil { + return nil, err + } + if s != nil && *s != "" { + vidModels := make([]*vidmod.VideoModel, 0) + if err = json.Unmarshal([]byte(*s), &vidModels); err != nil { + return nil, err + } + return vidModels, nil + } + filter := bson.M{} + var opt *options.FindOptions + switch topType { + case 1: + filter["recommendEndTime"] = bson.M{"$gt": time.Now()} + opt = options.Find().SetSort(bson.D{{Key: "recommendSort", Value: -1}}) + case 2: + filter["newEndTime"] = bson.M{"$gt": time.Now()} + opt = options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}}) + default: + filter["newEndTime"] = bson.M{"$gt": time.Now()} + opt = options.Find().SetSort(bson.D{{Key: "newSort", Value: -1}}) + } + imageTops, err := imagetopmod.GetImageTopList(filter, opt) + if err != nil { + log.Error(fmt.Sprintf("获取置顶帖子列表 发生错误:%v", err), log.Any("params", topType)) + return nil, err + } + //查询所有的幸福社区的帖子 + filter1["happinessPlazaTop"] = bson.M{"$ne": 1} + list, _ := vidmod.GetList(filter1, opts) + //根据置顶帖子查询过滤出来视频id + imageTopsLen := len(imageTops) + vidModels := make([]*vidmod.VideoModel, imageTopsLen, imageTopsLen+len(list)) + for i, info := range imageTops { + videoInfo, err := vidmod.GetVideoByID(info.ImageId) + if err != nil { + log.Error(fmt.Sprintf("获取置顶帖子列表 发生错误:%v", err), log.Any("params", topType)) + return nil, err + } + vidModels[i] = videoInfo + } + vidModels = append(vidModels, list...) + //设置缓存 + common.Go(func() { + b, _ := json.Marshal(vidModels) + _ = appg.Redis.Set(key, string(b), redisconst.ImageTopExpire) + }) + return vidModels, nil +} + +func GetValidSections() ([]SectionResp, error) { + sections, err := modulesectionmod.AllSections() + if err != nil { + return nil, err + } + sectionsLen := len(sections) + if sectionsLen == 0 { + return nil, nil + } + sort.Slice(sections, func(i, j int) bool { + if sections[i].Sort == nil { + return true + } + if sections[j].Sort == nil { + return false + } + return *sections[i].Sort < *sections[j].Sort + }) + srs := make([]SectionResp, sectionsLen) + for i := range sections { + srs[i].ID = sections[i].ID + srs[i].SectionName = sections[i].SectionName + } + return srs, nil +} + +func GetVidPlayRecommand(uid uint64, vid primitive.ObjectID) (*RecommandResp, error) { + svs, err := modulevidmod.GetSectionsByVideoId(vid) + if err != nil { + return nil, err + } + svsLen := len(svs) + if svsLen == 0 { + return GetSearchRecommand(uid) + } + sectionIDs := make([]primitive.ObjectID, svsLen) + for i, sv := range svs { + sectionIDs[i] = sv.SectionID + } + svs, err = modulevidmod.GetVideosBySectionIDs(sectionIDs) + if err != nil { + return nil, err + } + svsLen = len(svs) + if svsLen == 0 { + return GetSearchRecommand(uid) + } + vids := make([]primitive.ObjectID, 0, svsLen) + for _, sv := range svs { + if sv.VideoID == vid || sv.VideoID.IsZero() { + continue + } + vids = append(vids, sv.VideoID) + } + if len(vids) == 0 { + return GetSearchRecommand(uid) + } + videos, err := vidmod.GetVideosRecommandInVids(vids) + if err != nil { + return nil, err + } + if len(videos) == 0 { + return GetSearchRecommand(uid) + } + return &RecommandResp{ + Videos: vidhelpser.EncodeVideoInfoForRecommend(uid, videos), + HasNext: false, + }, nil +} + +func GetVidPlayRecommand_new(uid uint64, vid primitive.ObjectID) (data *RecommandResp, err error) { + excludedModules, err := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), true) + if err != nil { + return nil, err + } + useCache := len(excludedModules) == 0 + svs, err := modulevidmod.GetSectionsByVideoId(vid) + if err != nil { + return nil, err + } + svsLen := len(svs) + if svsLen == 0 { + return GetSearchRecommand(uid) + } + sectionIDs := make([]primitive.ObjectID, svsLen) + for i, sv := range svs { + sectionIDs[i] = sv.SectionID + } + //加一个搜索缓存 + var key string + if len(sectionIDs) > 0 { + key = fmt.Sprintf("GetVidPlayRecommand_new%v", sectionIDs[0]) + } + expaire := time.Minute * 5 + if useCache { + s, cacheErr := appg.Redis.Get(key) + if cacheErr != nil { + return nil, cacheErr + } + if s != nil && *s != "" { + if cacheErr = json.Unmarshal([]byte(*s), &data); cacheErr != nil { + return nil, cacheErr + } + return data, nil + } + } + fmt.Println("--------------------1") + var videos []*vidmod.VideoModel + if data == nil { + svs, err = modulevidmod.GetVideosBySectionIDs(sectionIDs) + if err != nil { + return nil, err + } + svsLen = len(svs) + if svsLen == 0 { + return GetSearchRecommand(uid) + } + vids := make([]primitive.ObjectID, 0, svsLen) + fmt.Println("--------------------2") + for _, sv := range svs { + if sv.VideoID == vid || sv.VideoID.IsZero() { + continue + } + vids = append(vids, sv.VideoID) + } + if len(vids) == 0 { + return GetSearchRecommand(uid) + } + fmt.Println("----------------------------vids", vids) + videos, err = vidmod.GetVideosRecommandInVids(vids) + if err != nil { + return nil, err + } + if len(videos) == 0 { + return GetSearchRecommand(uid) + } + fmt.Println("--------------------3") + data = &RecommandResp{Videos: vidhelpser.EncodeVideoInfoForRecommend(uid, videos), HasNext: false} + if useCache { + common.Go(func() { + b, _ := json.Marshal(data) + _ = appg.Redis.Set(key, string(b), expaire) + }) + } + } + return data, nil +} + +func GetSearchRecommand(uid uint64) (*RecommandResp, error) { + videos, err := vidmod.GetSearchRecommand() + if err != nil { + return nil, err + } + return &RecommandResp{ + Videos: vidhelpser.EncodeVideoInfoForRecommend(uid, videos), + HasNext: false, + }, nil +} + +// GetVideosByTagID 通过标签id获取视频列表信息 +func GetVideosByTagID(tid primitive.ObjectID, newsType string, sort int, skip, limit uint64) (list []vidmod.RecoVideoModel, err error) { + redisKey := redisconst.GetTagSortVideoKey(tid.Hex(), newsType, sort, skip, limit) + if !appg.Redis.Exists(redisKey) { + list, err = vidmod.VideoListByTagIDSort(tid, newsType, sort, skip, limit) + if err != nil { + return + } + go func() { + b, _ := json.Marshal(list) + _ = appg.Redis.Set(redisKey, string(b), redisconst.GetTagSortVideoExpire()) + }() + return + } + strP, err := appg.Redis.Get(redisKey) + if err != nil { + return + } + if strP != nil { + err = json.Unmarshal([]byte(*strP), &list) + } else { + err = errors.New("redis GetVideosByTagID get nil") + } + return +} + +func GetRecommendList(uid uint64, tagId string, newsType string, page commod.Page) (list []*vidmod.VideoInfo, hasNext bool, err error) { + tagObjId, _ := primitive.ObjectIDFromHex(tagId) + filter := bson.M{"status": 1, "newsType": newsType} + if !tagObjId.IsZero() { + filter["tags"] = tagObjId + } + res, hasNext, err := viddata.GetListFromCache(filter, int64(page.Skip()), int64(page.Limit()), + bson.D{{Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}}) + if err != nil { + return + } + list = vidhelpser.EncodeVideoInfoForRecommend(uid, res) + return list, hasNext, nil +} + +// -------- 查询亚模块下的所有视屏列表 --------- + +type SubModuleAllVideoListReq struct { + commod.Page + Type int `form:"type"` // 0 最多观看 1 最新上架 3 最多收藏 +} + +func SubModuleAllVideoList(uid uint64, subModuleId primitive.ObjectID, req SubModuleAllVideoListReq) (resp VideoUnderSubModuleResp, err error) { + active, err := moduleconfmod.CanBrowseModule(subModuleId, time.Now()) + if err != nil || !active { + return resp, err + } + expire := time.Minute * 10 + var videos []*vidmod.VideoModel + _, err = cachev2.Classes().CacheTime(expire).AutoListKey("HomePage:MaybeYouLike").ResBind(&videos).Cache(findAllVideoByModuleId, subModuleId, req) + if err != nil || len(videos) == 0 { + return resp, err + } + + if uint64(len(videos)) > req.PageSize { + resp.HasNext = true + videos = videos[:req.PageSize] + } + vInfos := vidhelpser.NewEncodeVideoInfoNotStatusForUser(uid, videos) + resp.AllVideoInfo = vInfos + + return resp, nil +} +func findAllVideoByModuleId(subModuleId primitive.ObjectID, req SubModuleAllVideoListReq) []*vidmod.VideoModel { + subModule, err := moduleconfmod.GetByID(subModuleId) + if err != nil { + return nil + } + if !subModule.IsActiveAt(time.Now()) { + return nil + } + var ( + skip = int64((req.PageNumber - 1) * req.PageSize) + sort bson.D + ) + switch req.Type { + case 1: + sort = bson.D{{"reviewAt", -1}, {"_id", -1}} + case 3: + sort = bson.D{{"collectCount", -1}, {"reviewAt", -1}, {"_id", -1}} + default: // 0 + sort = bson.D{{"playCount", -1}, {"reviewAt", -1}, {"_id", -1}} + } + + filter := bson.M{"mId": subModule.ID.Hex(), "status": 1} + videos, err := vidmod.GetList(filter, options.Find().SetSkip(skip).SetLimit(int64(req.PageSize+1)).SetSort(sort)) + if err != nil { + log.Error("findAllVideoByModuleId vidmod.GetVideosByIDs", log.E(err)) + return nil + } + return videos +} + +type ( + HandleShortVideoUnderSubModuleReq struct { + commod.Page + UID uint64 `json:"-" form:"-"` + Context context.Context `json:"-" form:"-"` + RequestID string `json:"-" form:"-"` + Err error `json:"-" form:"-"` + } + HandleShortVideoUnderSubModuleRep VideoUnderSubModuleResp +) + +func (req *HandleShortVideoUnderSubModuleReq) Do() (data VideoUnderSubModuleResp) { + if req.UID > 0 { + return req.get() + } + excluded, err := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), true) + if err != nil || len(excluded) > 0 { + return req.get() + } + cachev2.Classes().CacheTime(time.Minute * 5).AutoListKey("ShortVideoRecoList").ResBind(&data).Cache(req.get) + return data +} +func (req *HandleShortVideoUnderSubModuleReq) get() (data VideoUnderSubModuleResp) { + if req.UID > 0 && appg.Conf.ShortRecommendEnabled() { + ctx := req.Context + if ctx == nil { + ctx = context.Background() + } + fetchCtx, cancel := context.WithTimeout(ctx, appg.Conf.ShortRecommendRequestTimeout()) + result, fetchErr := shortrecommendser.FetchScoped( + fetchCtx, req.UID, int(req.PageSize), req.RequestID, "module-short-all", + ) + cancel() + if result.QueueVersion != "" { + if fetchErr != nil && len(result.Videos) == 0 { + log.Error("short module recommend fetch failed after reservation", + log.E(fetchErr), + log.Any("uid", req.UID), + log.Any("queueVersion", result.QueueVersion), + log.Any("queueLength", result.QueueLength), + log.Any("scanned", result.Scanned), + log.Any("batches", result.Batches)) + req.Err = fetchErr + return data + } + if fetchErr != nil || result.BudgetExceeded || result.Filtered > 0 { + log.Warn("short module recommend fetch completed with filtering", + log.E(fetchErr), + log.Any("uid", req.UID), + log.Any("queueVersion", result.QueueVersion), + log.Any("queueLength", result.QueueLength), + log.Any("requested", req.PageSize), + log.Any("returned", len(result.Videos)), + log.Any("scanned", result.Scanned), + log.Any("filtered", result.Filtered), + log.Any("batches", result.Batches), + log.Any("budgetExceeded", result.BudgetExceeded)) + } + data.AllVideoInfo = vidhelpser.NewEncodeVideoInfoNotStatusForUser(req.UID, result.Videos) + data.HasNext = result.QueueLength > 0 + return data + } + if fetchErr != nil { + if errors.Is(fetchErr, context.Canceled) || + errors.Is(fetchErr, context.DeadlineExceeded) { + return data + } + log.Error("short module recommend fetch failed, fallback random set", + log.E(fetchErr), log.Any("uid", req.UID)) + } + } + excluded, err := moduleconfmod.ExcludedVideoModuleIDs(time.Now(), true) + if err != nil { + return data + } + excludedSet := make(map[string]struct{}, len(excluded)) + for _, moduleID := range excluded { + excludedSet[moduleID] = struct{}{} + } + totalSize, err := appg.Redis.SCard(redisconst.ShortVideosRecoCacheKey) + if err != nil { + log.Error("GetVidList SCard err", log.E(err)) + return data + } + if totalSize == 0 { + return data + } + + if totalSize <= int64(req.PageSize) { + data.HasNext = false + } else { + data.HasNext = math.Ceil(float64(totalSize)/float64(req.PageSize)) > float64(req.PageNumber) + } + + sampleSize := int64(req.PageSize * 5) + if sampleSize > totalSize { + sampleSize = totalSize + } + pushVid, err := redis.Handler.SRandMemberN(redisconst.ShortVideosRecoCacheKey, sampleSize) + if err != nil { + return data + } + vids := common.String2ObjectID(pushVid) + vList, err := vidmod.GetVideosByIDs(vids) + if err != nil { + return data + } + allowed := make([]*vidmod.VideoModel, 0, req.PageSize) + for _, video := range vList { + if video == nil { + continue + } + if _, blocked := excludedSet[video.MID]; blocked { + continue + } + allowed = append(allowed, video) + if uint64(len(allowed)) == req.PageSize { + break + } + } + data.AllVideoInfo = vidhelpser.NewEncodeVideoInfoNotStatusForUser(req.UID, allowed) + return data +} + +// GetChangeVideos 获取换一批的视频列表 +func GetChangeVideos(in *vidmod.ChangeVideoSectionReq, uid uint64) (resp VideoUnderSectionResp, err error) { + sectionID, err := primitive.ObjectIDFromHex(in.SectionID) + if err != nil { + log.Error(fmt.Sprintf("GetChangeVideos primitive objectIDFromHex err:%v", err)) + return + } + // 获专题信息 + section, err := modulesectionmod.GetBySectionByID(sectionID) + if err != nil { + log.Error(fmt.Sprintf("GetChangeVideos modulesectionmod getBySectionByID err:%v", err)) + return + } + if section.ID.IsZero() { + return + } + active, err := moduleconfmod.CanBrowseModule(section.SubModuleID, time.Now()) + if err != nil || !active { + return resp, err + } + + // 获取专题下视频信息 + filter := bson.M{"sectionID": sectionID} + op := options.Find().SetSort(bson.M{"createdAt": -1}).SetLimit(300) + sectionVideos, err := modulevidmod.Find(filter, op) + if err != nil { + return + } + + if len(sectionVideos) < 0 { + return + } + + var vIds []primitive.ObjectID + for _, v := range sectionVideos { + vIds = append(vIds, v.VideoID) + } + // 根据样式判断随机数据 + num := modulesectionmod.ShowTypeHashVale[section.ShowType] + if len(vIds) < num { + return + } + if num == 0 { + num = 9 + } + rp := common.NewRandomPicker() + pickElements, err := rp.PickElements(vIds, num) + if err != nil { + return + } + + objectIDs := make([]primitive.ObjectID, len(pickElements)) + for i, v := range pickElements { + objectIDs[i] = v.(primitive.ObjectID) + } + + // 获取视频详情 + var videos []*vidmod.VideoModel + if videos, err = vidmod.GetVideoListByIDsPublish(objectIDs); err != nil { + log.Error("vidmod.GetVideoListByIDsPublish", log.Any("videoIDs", vIds), log.E(err)) + return + } + if videos == nil || len(videos) == 0 { + log.Warn("vidmod.GetVideoListByIDsPublish no video info", log.Any("videosIDs", vIds)) + return + } + + resp.Videos = vidhelpser.NewEncodeVideoInfoNotStatusForUser(uid, videos) + return +} + +// GetChangeMedias 获取换一批的视频列表 +func GetChangeMedias(in *vidmod.ChangeVideoSectionReq, uid uint64) (resp VideoUnderSectionResp, err error) { + sectionID, err := primitive.ObjectIDFromHex(in.SectionID) + if err != nil { + log.Error(fmt.Sprintf("GetChangeVideos primitive objectIDFromHex err:%v", err)) + return + } + // 获专题信息 + section, err := modulesectionmod.GetBySectionByID(sectionID) + if err != nil { + log.Error(fmt.Sprintf("GetChangeVideos modulesectionmod getBySectionByID err:%v", err)) + return + } + if section.ID.IsZero() { + return + } + active, err := moduleconfmod.CanBrowseModule(section.SubModuleID, time.Now()) + if err != nil || !active { + return resp, err + } + + // 获取专题下视频信息 + sort := bson.D{{Key: "sectionSort", Value: -1}, {Key: "createdAt", Value: -1}} + list, _, err := mediamod.GetList(bson.M{"sId": section.ID, "isDelete": false, "status": 1}, 0, 200, sort) + + if len(list) < 0 { + return + } + // 根据样式判断随机数据 + num := modulesectionmod.ShowTypeHashVale[section.ShowType] + if len(list) < num { + return + } + if num == 0 { + num = 9 + } + rp := common.NewRandomPicker() + pickElements, err := rp.PickElements(list, num) + if err != nil { + return + } + + mediaList := []*mediamod.Media{} + for _, v := range pickElements { + item, ok := v.(*mediamod.Media) + if !ok { + continue + } + mediaList = append(mediaList, item) + } + + resp.Medias = mediaser.FillNotStatusMedias(mediaList, false) + return +} + +func MostNewModuleVideoList(uid uint64, req vidmod.HomeMostNewModuleVideoListReq) (ret AllVideosOfModuleResp, err error) { + defer func() { + vidhelpser.ApplyFreeTrialBadgeToVideoInfos(vidhelpser.LoadFreeTrialBadgeContext(uid), ret.Videos) + }() + now := time.Now() + excludedModules, excludeErr := moduleconfmod.ExcludedVideoModuleIDs(now, false) + if excludeErr != nil { + return ret, excludeErr + } + useCache := len(excludedModules) == 0 + if useCache { + key := redisconst.GetMostNewModuleVideoListKey(req.SortType, req.PageNumber, req.PageSize) + err = appg.Redis.GetWithJson2Any(key, &ret) + if err == nil { + return + } + if !errors.Is(err, goredis.Nil) { + log.Error("appg.Redis.GetWithJson2Any error", log.E(err)) + } + defer func() { + appg.Redis.SetWithAny2Json(key, ret, redisconst.MostNewModuleVideoListExpire) + }() + } + // 获取暗网标签 + tag, _ := tagmod.FindOneTagByName("暗网") + cond := bson.M{"status": 1, "newsType": vidmod.SP} + if len(excludedModules) > 0 { + cond["mId"] = bson.M{"$nin": excludedModules} + } + if !tag.ID.IsZero() { + cond["tags"] = bson.M{"$ne": tag.ID} + } + var sort bson.D + switch req.SortType { + case 1: // 最新强制 在2年内的帖子 + if req.PageNumber > 100 { + return + } + cond["reviewAt"] = bson.M{"$gte": now.AddDate(-2, 0, 0)} + sort = bson.D{{Key: "reviewAt", Value: -1}} + case 2: // 本周 15天内上架 播放数 + cond["reviewAt"] = bson.M{"$gte": now.AddDate(0, 0, -15)} + sort = bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}} + case 3: // 月度 + cond["reviewAt"] = bson.M{"$lt": now.AddDate(0, 0, -15), "$gte": now.AddDate(0, -1, -15)} + sort = bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}} + case 4: // 年度 + cond["reviewAt"] = bson.M{"$lt": now.AddDate(0, -1, -15), "$gte": now.AddDate(0, -9, 0)} + sort = bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}} + default: + cond["reviewAt"] = bson.M{"$lt": now.AddDate(0, -1, -15), "$gte": now.AddDate(0, -9, 0)} + sort = bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}} + } + opt := options.Find().SetSort(sort).SetSkip(req.Skip64()).SetLimit(req.Limit64()) + videos, next, err := vidmod.FindList(cond, opt) + if err != nil { + return ret, err + } + vInfos := vidhelpser.EncodeVideoInfoNoUID(videos) + ret.Videos = vInfos + ret.HasNext = next + + return ret, nil +} diff --git a/app/service/vipcardexperimentser/service.go b/app/service/vipcardexperimentser/service.go new file mode 100644 index 0000000..912a08e --- /dev/null +++ b/app/service/vipcardexperimentser/service.go @@ -0,0 +1,348 @@ +package vipcardexperimentser + +import ( + "fmt" + "strings" + "time" + + "91porn-server/app/proto" + "91porn-server/models/v/vipcardexperimentmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const maxEventsPerRequest = 50 + +type EventInput struct { + EventID string `json:"eventId"` + EventName string `json:"eventName"` + SessionID string `json:"sessionId"` + OccurredAt time.Time `json:"occurredAt"` + ExperimentID string `json:"experimentId"` + Variant string `json:"variant"` + ProductID primitive.ObjectID `json:"productId"` +} + +type EventsRequest struct { + Events []EventInput `json:"events" binding:"required"` +} + +type EventsResponse struct { + Accepted int `json:"accepted"` + Duplicated int `json:"duplicated"` +} + +// ApplyToProductResponse applies the active experiment to the normal product response. +// No experiment or an unusable configuration keeps the legacy response unchanged. +func ApplyToProductResponse(uid uint64, response *proto.ProductRes, now time.Time) error { + if response == nil { + return nil + } + response.ExperimentStatus = vipcardexperimentmod.StatusDisabled + if uid == 0 { + return nil + } + experiment, err := vipcardexperimentmod.Current(now) + if err != nil || experiment == nil { + return err + } + variant := experiment.Assign(uid) + config, ok := experiment.ConfigFor(variant) + if !ok { + return nil + } + if !applyExperiment(response, experiment.ExperimentID, variant, config) { + return nil + } + otherConfig := experiment.VariantA + if variant == vipcardexperimentmod.VariantA { + otherConfig = experiment.VariantB + } + filterOtherVariantProducts(response, config.ProductIDs, otherConfig.ProductIDs) + return nil +} + +func applyExperiment( + response *proto.ProductRes, + experimentID string, + variant string, + config vipcardexperimentmod.VariantConfig, +) bool { + if !applyVariant(response, config) { + return false + } + response.ExperimentID = experimentID + response.ExperimentStatus = vipcardexperimentmod.StatusActive + response.Variant = variant + response.SkinKey = config.SkinKey + if !config.DefaultProductID.IsZero() { + response.DefaultProductID = config.DefaultProductID.Hex() + } + response.UIConfig = toResponseUIConfig(config.UIConfig) + return true +} + +func toResponseUIConfig(config *vipcardexperimentmod.UIConfig) *proto.VIPCardUIConfig { + if config == nil { + return nil + } + response := &proto.VIPCardUIConfig{ + BackgroundImage: config.BackgroundImage, + BadgeStyles: make([]proto.VIPCardBadgeStyle, len(config.BadgeStyles)), + } + for i, style := range config.BadgeStyles { + response.BadgeStyles[i] = proto.VIPCardBadgeStyle{ + BadgeType: style.BadgeType, + BackgroundColor: style.BackgroundColor, + TextColor: style.TextColor, + } + } + return response +} + +func applyVariant(response *proto.ProductRes, config vipcardexperimentmod.VariantConfig) bool { + productByID := make(map[primitive.ObjectID]proto.VIPListRes) + positionByID := make(map[primitive.ObjectID]int) + for positionIndex := range response.List { + for _, product := range response.List[positionIndex].List { + productByID[product.ID] = product + positionByID[product.ID] = positionIndex + } + } + + badges := make(map[primitive.ObjectID]vipcardexperimentmod.ProductBadge, len(config.ProductBadges)) + for _, badge := range config.ProductBadges { + badges[badge.ProductID] = badge + } + affectedPositions := make(map[int]struct{}) + orderedPositionIndexes := make([]int, 0, len(response.List)) + selectedByPosition := make(map[int][]proto.VIPListRes) + defaultVisible := config.DefaultProductID.IsZero() + for productOrder, productID := range config.ProductIDs { + product, exists := productByID[productID] + if !exists { + continue + } + product.Sort = productOrder + 1 + positionIndex := positionByID[productID] + if _, affected := affectedPositions[positionIndex]; !affected { + affectedPositions[positionIndex] = struct{}{} + orderedPositionIndexes = append(orderedPositionIndexes, positionIndex) + } + if badge, exists := badges[productID]; exists { + product.BadgeType = badge.BadgeType + product.BadgeText = badge.BadgeText + } + if productID == config.DefaultProductID { + defaultVisible = true + } + selectedByPosition[positionIndex] = append(selectedByPosition[positionIndex], product) + } + if len(affectedPositions) == 0 { + return false + } + if !defaultVisible { + return false + } + firstAffectedPosition := len(response.List) + for positionIndex := range affectedPositions { + if positionIndex < firstAffectedPosition { + firstAffectedPosition = positionIndex + } + } + orderedPositions := make([]proto.ProductList, 0, len(orderedPositionIndexes)) + for _, positionIndex := range orderedPositionIndexes { + position := response.List[positionIndex] + position.List = selectedByPosition[positionIndex] + orderedPositions = append(orderedPositions, position) + } + positions := make([]proto.ProductList, 0, len(response.List)) + for positionIndex, position := range response.List { + if positionIndex == firstAffectedPosition { + positions = append(positions, orderedPositions...) + } + if _, affected := affectedPositions[positionIndex]; affected { + continue + } + positions = append(positions, position) + } + response.List = positions + return true +} + +// filterOtherVariantProducts removes products configured exclusively for the +// other experiment variant while preserving positions that do not participate +// in the experiment. This keeps the product response consistent with the +// attribution validation performed when an order is created. +func filterOtherVariantProducts( + response *proto.ProductRes, + currentProductIDs, otherProductIDs []primitive.ObjectID, +) { + if response == nil { + return + } + current := make(map[primitive.ObjectID]struct{}, len(currentProductIDs)) + for _, productID := range currentProductIDs { + current[productID] = struct{}{} + } + blocked := make(map[primitive.ObjectID]struct{}, len(otherProductIDs)) + for _, productID := range otherProductIDs { + if _, shared := current[productID]; !shared { + blocked[productID] = struct{}{} + } + } + if len(blocked) == 0 { + return + } + + positions := make([]proto.ProductList, 0, len(response.List)) + for _, position := range response.List { + products := make([]proto.VIPListRes, 0, len(position.List)) + for _, product := range position.List { + if _, excluded := blocked[product.ID]; excluded { + continue + } + products = append(products, product) + } + if len(products) == 0 { + continue + } + position.List = products + positions = append(positions, position) + } + response.List = positions +} + +func RecordEvents(uid uint64, request EventsRequest, now time.Time) (EventsResponse, error) { + response := EventsResponse{} + if uid == 0 { + return response, fmt.Errorf("authenticated user is required") + } + if len(request.Events) == 0 || len(request.Events) > maxEventsPerRequest { + return response, fmt.Errorf("events must contain 1-%d items", maxEventsPerRequest) + } + experiments := make(map[string]*vipcardexperimentmod.Experiment) + events := make([]*vipcardexperimentmod.AnalyticsEvent, 0, len(request.Events)) + for _, input := range request.Events { + event, experiment, err := validateEvent(uid, input, now, experiments) + if err != nil { + return EventsResponse{}, err + } + events = append(events, event) + experiments[experiment.ExperimentID] = experiment + } + for _, event := range events { + inserted, err := vipcardexperimentmod.InsertEvent(event) + if err != nil { + return EventsResponse{}, err + } + if inserted { + response.Accepted++ + } else { + response.Duplicated++ + } + } + return response, nil +} + +func validateEvent( + uid uint64, + input EventInput, + now time.Time, + experiments map[string]*vipcardexperimentmod.Experiment, +) (*vipcardexperimentmod.AnalyticsEvent, *vipcardexperimentmod.Experiment, error) { + input.EventID = strings.TrimSpace(input.EventID) + input.EventName = strings.ToUpper(strings.TrimSpace(input.EventName)) + input.SessionID = strings.TrimSpace(input.SessionID) + input.ExperimentID = strings.TrimSpace(input.ExperimentID) + input.Variant = strings.ToUpper(strings.TrimSpace(input.Variant)) + if input.EventID == "" || len(input.EventID) > 128 { + return nil, nil, fmt.Errorf("eventId is required and must not exceed 128 characters") + } + if !vipcardexperimentmod.ValidEventName(input.EventName) { + return nil, nil, fmt.Errorf("unsupported eventName: %s", input.EventName) + } + if input.SessionID == "" || len(input.SessionID) > 128 { + return nil, nil, fmt.Errorf("sessionId is required and must not exceed 128 characters") + } + if input.ExperimentID == "" { + return nil, nil, fmt.Errorf("experimentId is required") + } + if len(input.ExperimentID) > 128 { + return nil, nil, fmt.Errorf("experimentId must not exceed 128 characters") + } + if input.OccurredAt.IsZero() || input.OccurredAt.After(now.Add(5*time.Minute)) { + return nil, nil, fmt.Errorf("occurredAt is invalid") + } + experiment := experiments[input.ExperimentID] + var err error + if experiment == nil { + experiment, err = vipcardexperimentmod.FindByExperimentID(input.ExperimentID) + if err != nil { + return nil, nil, err + } + if experiment == nil { + return nil, nil, fmt.Errorf("experiment does not exist") + } + } + if !eventOccurredWithinExperiment(experiment, input.OccurredAt) { + return nil, nil, fmt.Errorf("occurredAt is outside the experiment period") + } + config, ok := experiment.ConfigFor(input.Variant) + if !ok { + return nil, nil, fmt.Errorf("variant must be A or B") + } + if assigned := experiment.Assign(uid); assigned != input.Variant { + return nil, nil, fmt.Errorf("variant does not match user assignment") + } + if input.EventName == vipcardexperimentmod.EventProductImpression { + if input.ProductID.IsZero() { + return nil, nil, fmt.Errorf("productId is required for VIP_PRODUCT_IMPRESSION") + } + if !containsProduct(config.ProductIDs, input.ProductID) { + return nil, nil, fmt.Errorf("productId does not belong to variant") + } + } else { + input.ProductID = primitive.NilObjectID + } + return &vipcardexperimentmod.AnalyticsEvent{ + EventID: input.EventID, + EventName: input.EventName, + UID: uid, + SessionID: input.SessionID, + OccurredAt: input.OccurredAt, + ExperimentID: input.ExperimentID, + Variant: input.Variant, + ProductID: input.ProductID, + }, experiment, nil +} + +func eventOccurredWithinExperiment(experiment *vipcardexperimentmod.Experiment, occurredAt time.Time) bool { + const clockSkew = 5 * time.Minute + startAt := experiment.PublishedAt + if experiment.StartAt != nil && (startAt.IsZero() || experiment.StartAt.After(startAt)) { + startAt = *experiment.StartAt + } + if !startAt.IsZero() && occurredAt.Before(startAt.Add(-clockSkew)) { + return false + } + var endAt *time.Time + if experiment.EndAt != nil { + end := *experiment.EndAt + endAt = &end + } + if experiment.DisabledAt != nil && (endAt == nil || experiment.DisabledAt.Before(*endAt)) { + end := *experiment.DisabledAt + endAt = &end + } + return endAt == nil || !occurredAt.After(endAt.Add(clockSkew)) +} + +func containsProduct(ids []primitive.ObjectID, target primitive.ObjectID) bool { + for _, id := range ids { + if id == target { + return true + } + } + return false +} diff --git a/app/service/vipcardexperimentser/service_test.go b/app/service/vipcardexperimentser/service_test.go new file mode 100644 index 0000000..9fbf62f --- /dev/null +++ b/app/service/vipcardexperimentser/service_test.go @@ -0,0 +1,479 @@ +package vipcardexperimentser + +import ( + "testing" + "time" + + "91porn-server/app/proto" + "91porn-server/models/v/productmod" + "91porn-server/models/v/vipcardexperimentmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestApplyVariantOrdersFiltersAndBadgesProducts(t *testing.T) { + first := primitive.NewObjectID() + second := primitive.NewObjectID() + unaffected := primitive.NewObjectID() + response := proto.ProductRes{List: []proto.ProductList{ + { + Position: "vip", + List: []proto.VIPListRes{ + {Product: productmod.Product{ID: first}}, + {Product: productmod.Product{ID: second}}, + }, + }, + { + Position: "coin", + List: []proto.VIPListRes{ + {Product: productmod.Product{ID: unaffected}}, + }, + }, + }} + config := vipcardexperimentmod.VariantConfig{ + DefaultProductID: second, + ProductIDs: []primitive.ObjectID{second, first}, + ProductBadges: []vipcardexperimentmod.ProductBadge{{ + ProductID: second, + BadgeType: vipcardexperimentmod.BadgeMostPopular, + BadgeText: "最受欢迎", + }}, + } + if !applyVariant(&response, config) { + t.Fatal("applyVariant() = false, want true") + } + vipProducts := response.List[0].List + if len(vipProducts) != 2 || vipProducts[0].ID != second || vipProducts[1].ID != first { + t.Fatalf("VIP product order = %#v", vipProducts) + } + if vipProducts[0].Sort != 1 || vipProducts[1].Sort != 2 { + t.Fatalf("VIP product sort values = %#v", vipProducts) + } + if vipProducts[0].BadgeType != vipcardexperimentmod.BadgeMostPopular || + vipProducts[0].BadgeText != "最受欢迎" { + t.Fatalf("badge not applied: %#v", vipProducts[0]) + } + if len(response.List[1].List) != 1 || response.List[1].List[0].ID != unaffected { + t.Fatalf("unaffected position changed: %#v", response.List[1].List) + } +} + +func TestApplyVariantOrdersIndependentPositionsByFirstConfiguredProduct(t *testing.T) { + firstVIP := primitive.NewObjectID() + secondVIP := primitive.NewObjectID() + presale := primitive.NewObjectID() + unaffected := primitive.NewObjectID() + response := proto.ProductRes{List: []proto.ProductList{ + { + Position: "会员卡", + PositionID: "vip", + List: []proto.VIPListRes{ + {Product: productmod.Product{ID: firstVIP}}, + {Product: productmod.Product{ID: secondVIP}}, + }, + }, + { + Position: "预售卡", + PositionID: "presale", + List: []proto.VIPListRes{ + {Product: productmod.Product{ID: presale}}, + }, + }, + { + Position: "金币", + PositionID: "coin", + List: []proto.VIPListRes{ + {Product: productmod.Product{ID: unaffected}}, + }, + }, + }} + config := vipcardexperimentmod.VariantConfig{ + ProductIDs: []primitive.ObjectID{presale, secondVIP, firstVIP}, + } + + if !applyVariant(&response, config) { + t.Fatal("applyVariant() = false, want true") + } + if len(response.List) != 3 { + t.Fatalf("positions = %#v, want member, presale and coin positions", response.List) + } + if response.List[0].Position != "预售卡" || response.List[0].PositionID != "presale" || + len(response.List[0].List) != 1 || response.List[0].List[0].ID != presale || + response.List[0].List[0].Sort != 1 { + t.Fatalf("presale position = %#v", response.List[0]) + } + wantVIPProducts := []primitive.ObjectID{secondVIP, firstVIP} + if response.List[1].Position != "会员卡" || response.List[1].PositionID != "vip" { + t.Fatalf("member position = %#v", response.List[1]) + } + if len(response.List[1].List) != len(wantVIPProducts) { + t.Fatalf("ordered member products = %#v", response.List[1].List) + } + for index, productID := range wantVIPProducts { + if response.List[1].List[index].ID != productID { + t.Fatalf("ordered member product %d = %s, want %s", index, response.List[1].List[index].ID, productID) + } + if response.List[1].List[index].Sort != index+2 { + t.Fatalf("ordered member product %d sort = %d, want %d", index, response.List[1].List[index].Sort, index+2) + } + } + if response.List[2].Position != "金币" || len(response.List[2].List) != 1 || + response.List[2].List[0].ID != unaffected { + t.Fatalf("unaffected position = %#v", response.List[2]) + } +} + +func TestApplyVariantKeepsInterleavedProductsInOnePosition(t *testing.T) { + firstVIP := primitive.NewObjectID() + secondVIP := primitive.NewObjectID() + presale := primitive.NewObjectID() + response := proto.ProductRes{List: []proto.ProductList{ + { + Position: "会员卡", + PositionID: "vip", + List: []proto.VIPListRes{ + {Product: productmod.Product{ID: firstVIP}}, + {Product: productmod.Product{ID: secondVIP}}, + }, + }, + { + Position: "预售卡", + PositionID: "presale", + List: []proto.VIPListRes{ + {Product: productmod.Product{ID: presale}}, + }, + }, + }} + config := vipcardexperimentmod.VariantConfig{ + ProductIDs: []primitive.ObjectID{secondVIP, presale, firstVIP}, + } + + if !applyVariant(&response, config) { + t.Fatal("applyVariant() = false, want true") + } + if len(response.List) != 2 { + t.Fatalf("positions = %#v, want one member and one presale position", response.List) + } + if response.List[0].PositionID != "vip" || len(response.List[0].List) != 2 || + response.List[0].List[0].ID != secondVIP || response.List[0].List[0].Sort != 1 || + response.List[0].List[1].ID != firstVIP || response.List[0].List[1].Sort != 3 { + t.Fatalf("member position = %#v", response.List[0]) + } + if response.List[1].PositionID != "presale" || len(response.List[1].List) != 1 || + response.List[1].List[0].ID != presale || response.List[1].List[0].Sort != 2 { + t.Fatalf("presale position = %#v", response.List[1]) + } +} + +func TestFilterOtherVariantProductsRemovesExclusivePresalePosition(t *testing.T) { + shared := primitive.NewObjectID() + currentOnly := primitive.NewObjectID() + otherOnlyPresale := primitive.NewObjectID() + unaffected := primitive.NewObjectID() + response := proto.ProductRes{List: []proto.ProductList{ + { + Position: "vip", + List: []proto.VIPListRes{ + {Product: productmod.Product{ID: shared}}, + {Product: productmod.Product{ID: currentOnly}}, + }, + }, + { + Position: "advance-card", + List: []proto.VIPListRes{ + {Product: productmod.Product{ID: otherOnlyPresale}}, + }, + }, + { + Position: "coin", + List: []proto.VIPListRes{ + {Product: productmod.Product{ID: unaffected}}, + }, + }, + }} + + filterOtherVariantProducts( + &response, + []primitive.ObjectID{shared, currentOnly}, + []primitive.ObjectID{shared, otherOnlyPresale}, + ) + + if len(response.List) != 2 { + t.Fatalf("positions = %#v, want vip and unaffected coin positions", response.List) + } + if response.List[0].Position != "vip" || len(response.List[0].List) != 2 { + t.Fatalf("current variant products changed: %#v", response.List[0]) + } + if response.List[1].Position != "coin" || len(response.List[1].List) != 1 || + response.List[1].List[0].ID != unaffected { + t.Fatalf("unaffected position changed: %#v", response.List[1]) + } +} + +func TestApplyVariantFallsBackWhenDefaultProductIsUnavailable(t *testing.T) { + available := primitive.NewObjectID() + missingDefault := primitive.NewObjectID() + response := proto.ProductRes{List: []proto.ProductList{{ + Position: "vip", + List: []proto.VIPListRes{{ + Product: productmod.Product{ID: available}, + }}, + }}} + original := response.List[0].List + config := vipcardexperimentmod.VariantConfig{ + DefaultProductID: missingDefault, + ProductIDs: []primitive.ObjectID{available, missingDefault}, + } + if applyVariant(&response, config) { + t.Fatal("applyVariant() = true, want false") + } + if len(response.List[0].List) != len(original) { + t.Fatalf("legacy response must remain unchanged: %#v", response.List[0].List) + } +} + +func TestApplyVariantAllowsMissingDefaultProduct(t *testing.T) { + first := primitive.NewObjectID() + second := primitive.NewObjectID() + response := proto.ProductRes{List: []proto.ProductList{{ + Position: "vip", + List: []proto.VIPListRes{ + {Product: productmod.Product{ID: first}}, + {Product: productmod.Product{ID: second}}, + }, + }}} + config := vipcardexperimentmod.VariantConfig{ + ProductIDs: []primitive.ObjectID{second, first}, + } + if !applyVariant(&response, config) { + t.Fatal("applyVariant() = false, want true") + } + products := response.List[0].List + if len(products) != 2 || products[0].ID != second || products[1].ID != first { + t.Fatalf("VIP product order = %#v", products) + } +} + +func TestApplyExperimentOmitsMissingDefaultProduct(t *testing.T) { + productID := primitive.NewObjectID() + response := proto.ProductRes{List: []proto.ProductList{{ + Position: "vip", + List: []proto.VIPListRes{{ + Product: productmod.Product{ID: productID}, + }}, + }}} + config := vipcardexperimentmod.VariantConfig{ + ProductIDs: []primitive.ObjectID{productID}, + } + + if !applyExperiment( + &response, + "optional-default-product", + vipcardexperimentmod.VariantA, + config, + ) { + t.Fatal("applyExperiment() = false, want true") + } + if response.DefaultProductID != "" { + t.Fatalf("DefaultProductID = %q, want empty", response.DefaultProductID) + } +} + +func TestApplyExperimentIncludesUIConfig(t *testing.T) { + productID := primitive.NewObjectID() + uiConfig := &vipcardexperimentmod.UIConfig{ + BackgroundImage: "vip-card-skin-a.png", + BadgeStyles: []vipcardexperimentmod.BadgeStyle{{ + BadgeType: vipcardexperimentmod.BadgeNewUser, + BackgroundColor: "#F04432", + TextColor: "#FFFFFF", + }}, + } + config := vipcardexperimentmod.VariantConfig{ + SkinKey: "vip-card-skin-a", + DefaultProductID: productID, + ProductIDs: []primitive.ObjectID{productID}, + UIConfig: uiConfig, + } + response := proto.ProductRes{List: []proto.ProductList{{ + Position: "vip", + List: []proto.VIPListRes{{ + Product: productmod.Product{ID: productID}, + }}, + }}} + + if !applyExperiment( + &response, + "vip-card-test", + vipcardexperimentmod.VariantA, + config, + ) { + t.Fatal("applyExperiment() = false, want true") + } + if response.ExperimentID != "vip-card-test" || + response.ExperimentStatus != vipcardexperimentmod.StatusActive || + response.Variant != vipcardexperimentmod.VariantA || + response.SkinKey != "vip-card-skin-a" || + response.DefaultProductID != productID.Hex() { + t.Fatalf("experiment metadata = %#v", response) + } + if response.UIConfig == nil || + response.UIConfig.BackgroundImage != "vip-card-skin-a.png" || + len(response.UIConfig.BadgeStyles) != 1 { + t.Fatalf("UI config = %#v", response.UIConfig) + } + + uiConfig.BackgroundImage = "changed.png" + uiConfig.BadgeStyles[0].BackgroundColor = "#000000" + if response.UIConfig.BackgroundImage != "vip-card-skin-a.png" || + response.UIConfig.BadgeStyles[0].BackgroundColor != "#F04432" { + t.Fatalf("response UI config aliases stored config: %#v", response.UIConfig) + } +} + +func TestApplyExperimentOmitsUIConfigForLegacyExperiment(t *testing.T) { + productID := primitive.NewObjectID() + response := proto.ProductRes{List: []proto.ProductList{{ + Position: "vip", + List: []proto.VIPListRes{{ + Product: productmod.Product{ID: productID}, + }}, + }}} + config := vipcardexperimentmod.VariantConfig{ + DefaultProductID: productID, + ProductIDs: []primitive.ObjectID{productID}, + } + + if !applyExperiment( + &response, + "legacy-experiment", + vipcardexperimentmod.VariantA, + config, + ) { + t.Fatal("applyExperiment() = false, want true") + } + if response.UIConfig != nil { + t.Fatalf("legacy UI config = %#v, want nil", response.UIConfig) + } +} + +func TestApplyExperimentFailureDoesNotExposeExperimentUI(t *testing.T) { + available := primitive.NewObjectID() + missingDefault := primitive.NewObjectID() + response := proto.ProductRes{List: []proto.ProductList{{ + Position: "vip", + List: []proto.VIPListRes{{ + Product: productmod.Product{ID: available}, + }}, + }}} + config := vipcardexperimentmod.VariantConfig{ + SkinKey: "vip-card-skin-a", + DefaultProductID: missingDefault, + ProductIDs: []primitive.ObjectID{available, missingDefault}, + UIConfig: &vipcardexperimentmod.UIConfig{ + BackgroundImage: "vip-card-skin-a.png", + }, + } + + if applyExperiment( + &response, + "unusable-experiment", + vipcardexperimentmod.VariantA, + config, + ) { + t.Fatal("applyExperiment() = true, want false") + } + if response.ExperimentID != "" || + response.Variant != "" || + response.SkinKey != "" || + response.DefaultProductID != "" || + response.UIConfig != nil { + t.Fatalf("failed experiment leaked metadata: %#v", response) + } + if len(response.List) != 1 || + len(response.List[0].List) != 1 || + response.List[0].List[0].ID != available { + t.Fatalf("failed experiment changed product list: %#v", response.List) + } +} + +func TestApplyToProductResponseSkipsAnonymousUser(t *testing.T) { + response := proto.ProductRes{} + if err := ApplyToProductResponse(0, &response, time.Now()); err != nil { + t.Fatalf("ApplyToProductResponse() error = %v", err) + } + if response.ExperimentStatus != vipcardexperimentmod.StatusDisabled || + response.ExperimentID != "" || response.UIConfig != nil { + t.Fatalf("anonymous response contains experiment config: %#v", response) + } +} + +func TestValidateEventChecksStableAssignmentAndProduct(t *testing.T) { + productID := primitive.NewObjectID() + experiment := &vipcardexperimentmod.Experiment{ + ExperimentID: "experiment", + TrafficA: 100, + TrafficB: 0, + VariantA: vipcardexperimentmod.VariantConfig{ + DefaultProductID: productID, + ProductIDs: []primitive.ObjectID{productID}, + }, + } + now := time.Now() + input := EventInput{ + EventID: "event-1", + EventName: vipcardexperimentmod.EventProductImpression, + SessionID: "session-1", + OccurredAt: now, + ExperimentID: experiment.ExperimentID, + Variant: vipcardexperimentmod.VariantA, + ProductID: productID, + } + event, _, err := validateEvent( + 123, + input, + now, + map[string]*vipcardexperimentmod.Experiment{experiment.ExperimentID: experiment}, + ) + if err != nil { + t.Fatalf("validateEvent() error = %v", err) + } + if event.UID != 123 || event.ProductID != productID { + t.Fatalf("event = %#v", event) + } + + input.Variant = vipcardexperimentmod.VariantB + if _, _, err = validateEvent( + 123, + input, + now, + map[string]*vipcardexperimentmod.Experiment{experiment.ExperimentID: experiment}, + ); err == nil { + t.Fatal("expected mismatched assignment to fail") + } +} + +func TestEventOccurredWithinExperiment(t *testing.T) { + now := time.Now() + start := now.Add(-time.Hour) + end := now.Add(time.Hour) + experiment := &vipcardexperimentmod.Experiment{ + PublishedAt: start, + EndAt: &end, + } + if !eventOccurredWithinExperiment(experiment, now) { + t.Fatal("event during experiment must be accepted") + } + if eventOccurredWithinExperiment(experiment, start.Add(-10*time.Minute)) { + t.Fatal("event before experiment must be rejected") + } + if eventOccurredWithinExperiment(experiment, end.Add(10*time.Minute)) { + t.Fatal("event after experiment must be rejected") + } + + disabled := now.Add(-time.Minute) + experiment.DisabledAt = &disabled + if eventOccurredWithinExperiment(experiment, now.Add(10*time.Minute)) { + t.Fatal("event after disable must be rejected") + } +} diff --git a/app/service/walletser/inviteuser.go b/app/service/walletser/inviteuser.go new file mode 100644 index 0000000..33210f2 --- /dev/null +++ b/app/service/walletser/inviteuser.go @@ -0,0 +1,330 @@ +package walletser + +import ( + "91porn-server/common/constant/redisconst" + "encoding/json" + "fmt" + "strconv" + "sync" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/timeutil" + "91porn-server/models/commod" + "91porn-server/models/v/proxymod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + + "github.com/shopspring/decimal" +) + +// UserInviteAmountInfo 用户账户信息 +type UserInviteAmountInfo struct { + TotalAmount string `json:"totalAmount" bson:"totalAmount"` // 可提现收益 + TotalIncomeAmount string `json:"totalIncomeAmount" bson:"totalIncomeAmount"` // 累计收益总额 + TotalPayUserCount string `json:"totalPayUserCount" bson:"totalPayUserCount"` // 累计付费用户 + TotalInviteUserCount string `json:"totalInviteUserCount" bson:"totalInviteUserCount"` // 累计推广用户 + TodayInviteUserCount string `json:"todayInviteUserCount" bson:"todayInviteUserCount"` // 今日推广用户 + MonthInviteUserCount string `json:"monthInviteUserCount" bson:"monthInviteUserCount"` // 当月推广用户 + TodayIncomeAmount string `json:"todayIncomeAmount" bson:"todayIncomeAmount"` // 今日推广收益金币 + MonthIncomeAmount string `json:"monthIncomeAmount" bson:"monthIncomeAmount"` // 当月推广收益金币 +} + +type VideoIncomeListRes struct { + TotalVideoAmount string `json:"totalVideoAmount"` + TodayTodayAmount string `json:"todayTodayAmount"` + YesterdayAmount string `json:"yesterdayAmount"` + MounthAmount string `json:"mounthAmount"` + HasNext bool `json:"hasNext"` + List []VideoIncomeInfo `json:"list"` +} + +type VideoIncomeInfo struct { + Title string `json:"Title"` // 金币 + IncomeAmount string `json:"incomeAmount"` // 收入金币 + IncomeType int `json:"incomeType"` // 收入类型 + IncomeTime time.Time `json:"incomeTime"` // 时间 +} + +func GetUserAmount(uid uint64) (resp *UserInviteAmountInfo, err error) { + data := UserInviteAmountInfo{} + str, err := appg.Redis.Get(redisconst.InviteCacheKey(uid)) + if err != nil { + return + } + if str != nil { + if err = json.Unmarshal([]byte(*str), &data); err == nil { + return &data, nil + } + log.Warn(fmt.Sprintf("用户ID:%d;解析缓存数据异常:%v", uid, err)) + } + + var ( + income string + todayInviteAmount float64 + monthInviteAmount float64 + proxyTotalIncome float64 + monthInviteUserCount int64 + toadyInviteUserCount int64 + totalInviteUserCount int64 + totalPayUserCount int64 + ) + nowTime := time.Now() + startD, endD := timeutil.EarlyLastDay(nowTime) + monthStartTime, _ := timeutil.EarlyLastMonth(nowTime) + var wg sync.WaitGroup + wg.Add(7) + common.Go(func() { + defer wg.Done() + w, _ := walletmod.GetWallet(uid) + if w == nil { + return + } + income = (decimal.NewFromInt(w.Income).Add(decimal.NewFromFloat(w.IncomePot))).Div(decimal.NewFromInt(10)).String() + //vidTotalIncome = w.VidIncome + proxyTotalIncome = w.ProxyIncome + }) + common.Go(func() { + defer wg.Done() + totalInviteUserCount, err = proxymod.CountByUID(uid) + if err != nil { + return + } + }) + common.Go(func() { + defer wg.Done() + totalPayUserCount, err = proxymod.CountPayUserByUID(uid) + if err != nil { + return + } + }) + common.Go(func() { + defer wg.Done() + monthInviteUserCount, err = proxymod.CountByUIDAndTime(uid, monthStartTime) + if err != nil { + return + } + }) + common.Go(func() { + defer wg.Done() + toadyInviteUserCount, err = proxymod.CountByUIDAndTime(uid, startD) + if err != nil { + return + } + }) + + common.Go(func() { + defer wg.Done() + // 今日成功推广金额 + todayInviteAmount, _, _ = txnmod.GetIncomeByType(uid, txnmod.ProxyIncome, startD, endD) + }) + common.Go(func() { + defer wg.Done() + // 当月成功推广金额 + monthInviteAmount, _, _ = txnmod.GetIncomeByType(uid, txnmod.ProxyIncome, monthStartTime, endD) + }) + + wg.Wait() + data.TotalAmount = income + data.TotalIncomeAmount = strconv.FormatFloat(proxyTotalIncome/10, 'f', 2, 64) + data.TotalPayUserCount = strconv.FormatInt(totalPayUserCount, 10) + data.TotalInviteUserCount = strconv.FormatInt(totalInviteUserCount, 10) + data.TodayInviteUserCount = strconv.FormatInt(toadyInviteUserCount, 10) + data.MonthInviteUserCount = strconv.FormatInt(monthInviteUserCount, 10) + data.TodayIncomeAmount = strconv.FormatFloat(todayInviteAmount/10, 'f', 2, 64) + data.MonthIncomeAmount = strconv.FormatFloat(monthInviteAmount/10, 'f', 2, 64) + + // 加入缓存 + common.Go(func() { + dataInfo, err := json.Marshal(&data) + if err != nil { + log.Warn(fmt.Sprintf("用户ID:%d;json序列化缓存数据异常:%v", uid, err)) + return + } + if err = appg.Redis.Set(redisconst.InviteCacheKey(uid), dataInfo, 2*time.Minute); err != nil { + log.Warn(fmt.Sprintf("用户ID:%d;保存缓存数据异常:%v", uid, err)) + } + }) + return &data, nil +} + +func GetVideoIncomelist(uid, pageNumber, pageSize uint64) (resp VideoIncomeListRes, err error) { + var vidIncomeToday float64 + var vidIncomeMonth float64 + var vidTotalIncome float64 + var vidYesterdayIncome float64 + now := time.Now() + resp = VideoIncomeListRes{ + List: make([]VideoIncomeInfo, 0), + } + startM, endM := timeutil.EarlyLastMonth(now) + startD, endD := timeutil.EarlyLastDay(now) + startY, endY := timeutil.YesterdayDay(now) + var wg sync.WaitGroup + wg.Add(5) + common.Go(func() { //1 + defer wg.Done() + w, _ := walletmod.GetWallet(uid) + if w == nil { + return + } + vidTotalIncome = w.VidIncome + }) + common.Go(func() { //2 + defer wg.Done() + vidIncomeToday, _, _ = txnmod.GetIncomeByType(uid, txnmod.WorksIncome, startD, endD) + + }) + common.Go(func() { //3 + defer wg.Done() + vidIncomeMonth, _, _ = txnmod.GetIncomeByType(uid, txnmod.WorksIncome, startM, endM) + + }) + common.Go(func() { //4 + defer wg.Done() + vidYesterdayIncome, _, _ = txnmod.GetIncomeByType(uid, txnmod.WorksIncome, startY, endY) + + }) + common.Go(func() { //5 + defer wg.Done() + videoList, hasNext, _ := txnmod.WorksIncomebills(uid, pageNumber, pageSize) + resp.HasNext = hasNext + for _, videotran := range videoList { + tmp := VideoIncomeInfo{ + Title: videotran.Desc, + IncomeAmount: strconv.FormatFloat(videotran.ActualAmount, 'f', 2, 64), + IncomeTime: videotran.CreatedAt, + } + resp.List = append(resp.List, tmp) + } + }) + wg.Wait() + resp.TotalVideoAmount = strconv.FormatFloat(vidTotalIncome, 'f', 2, 64) + resp.TodayTodayAmount = strconv.FormatFloat(vidIncomeToday, 'f', 2, 64) + resp.YesterdayAmount = strconv.FormatFloat(vidYesterdayIncome, 'f', 2, 64) + resp.MounthAmount = strconv.FormatFloat(vidIncomeMonth, 'f', 2, 64) + return resp, nil +} + +type UserInviteIncomeListRes struct { + //总邀请数 + TotalInvites int64 `json:"totalInvites"` + //今日邀请 + TodayInvites int64 `json:"todayInvites"` + //总邀请充值 + TotalInviteAmount float64 `json:"totalInviteAmount"` + //今日充值 + TodayInviteAmount float64 `json:"todayInviteAmount"` + //列表总数 + Total int64 `json:"total"` + //是否还有下一页 + HasNext bool `json:"hasNext"` + //列表 + List []UserInviteIncomeInfo `json:"list"` +} + +type UserInviteIncomeInfo struct { + // 充值用户 + UserName string `json:"userName"` + // 收入金币 + IncomeAmount float64 `json:"incomeAmount" bson:"incomeAmount"` + // 充值时间 + RechargeAt time.Time `json:"rechargeAt"` +} + +func GetInviteIncomelist(uid, pageNumber, pageSize uint64) (resp UserInviteIncomeListRes, err error) { + //分页查询收益详情 + txnList, hasNext, _ := txnmod.BuyProductLogWithPage(uid, txnmod.ProxyIncome, commod.Page{ + PageNumber: pageNumber, + PageSize: pageSize, + }) + resp.HasNext = hasNext + txnListLen := len(txnList) + uids := make([]uint64, txnListLen) + for i, tran := range txnList { + uids[i] = tran.RechargeId + } + resList := make([]UserInviteIncomeInfo, 0) + if len(uids) > 0 { + //批量查询用户信息 + m, uErr := usermod.FindUsersMapByUID(uids) + if uErr == nil && len(m) > 0 { + resList = make([]UserInviteIncomeInfo, txnListLen) + for i, tran := range txnList { + tmp := UserInviteIncomeInfo{ + IncomeAmount: tran.ActualAmount / 10, + RechargeAt: tran.CreatedAt, + } + if m[tran.RechargeId] != nil { + tmp.UserName = m[tran.RechargeId].Name + } + resList[i] = tmp + } + } + } + //从缓存获取其他数据 + redisKey := "Get-Invite-Income-" + strconv.FormatUint(uid, 10) + str, err := appg.Redis.Get(redisKey) + if err != nil { // redis 错误不向上报告 + log.Error("GetInviteIncomelist redisc.Get", log.Any("uid", uid), log.Any("redisKey", redisKey), log.E(err)) + } + if str != nil { + if err = json.Unmarshal([]byte(*str), &resp); err == nil { + resp.List = resList + resp.HasNext = hasNext + return resp, nil + } + // json.Unmarshal的错误不向上报告,而是尝试去DB获取数据 + log.Error("GetInviteIncomelist json.Unmarshal", log.Any("uid", uid), log.Any("redisKey", redisKey), log.E(err)) + } + resp.List = resList + resp.HasNext = hasNext + now := time.Now() + resp = UserInviteIncomeListRes{ + List: make([]UserInviteIncomeInfo, 0), + } + startD, endD := timeutil.EarlyLastDay(now) + var wg sync.WaitGroup + wg.Add(4) + common.Go(func() { //1 + defer wg.Done() + w, _ := walletmod.GetWallet(uid) + if w == nil { + return + } + resp.TotalInviteAmount = w.ProxyIncome / 10 + }) + common.Go(func() { //2 + defer wg.Done() + //成功推广数 + resp.TotalInvites, _ = proxymod.CountByUID(uid) + //成功推广数 = 总数 + resp.Total = resp.TotalInvites + }) + common.Go(func() { //3 + defer wg.Done() + //今日成功推广数 + resp.TodayInvites, _ = proxymod.CountByUIDAndTime(uid, startD) + }) + common.Go(func() { //4 + defer wg.Done() + //今日成功推广金额 + resp.TodayInviteAmount, _, _ = txnmod.GetIncomeByType(uid, txnmod.ProxyIncome, startD, endD) + resp.TodayInviteAmount = resp.TodayInviteAmount / 10 + }) + wg.Wait() + common.Go(func() { + jsonBytes, err := json.Marshal(resp) + if err != nil { + log.Error("GetInviteIncomelist json.Marshal", log.Any("uid", uid), log.E(err)) + return + } + if err := appg.Redis.Set(redisKey, string(jsonBytes), 15*time.Minute); err != nil { + log.Error("GetInviteIncomelist redisc.Set", log.Any("uid", uid), log.Any("redisKey", redisKey), log.E(err)) + } + }) + return resp, nil +} diff --git a/app/service/walletser/store.go b/app/service/walletser/store.go new file mode 100644 index 0000000..d53cc01 --- /dev/null +++ b/app/service/walletser/store.go @@ -0,0 +1,146 @@ +package walletser + +import ( + "91porn-server/app/appg" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/walletmod" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func debitPlan(w *walletmod.Wallet, amt int64) *walletmod.DebitPlan { + p := walletmod.DebitPlan{} + l1 := w.Amount - amt + if l1 >= 0 { //amount够了 + p.Amount = amt + return &p + } else { // l1 < 0 + //amount 扣完 + l2 := w.Income + l1 + if l2 >= 0 { + p.Amount = w.Amount + p.Income = -l1 + return &p + } else { + return nil //余额不足 + } + } +} + +const ( + StoreBuyGoods = 1 + StoreBuyNudeChat = 2 + StorePublishWish = 3 + StoreGoodsOrderRefund = 4 + StoreNudeChatOrderRefund = 5 + StoreWishRefund = 6 + StoreWishEdit = 7 +) + +// StoreDeductBalance 商城相关扣款 +func StoreDeductBalance(uid uint64, amount int64, orderId primitive.ObjectID, source int, desc string) (balance int64, code stderr.Code) { + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return 0, stderr.ErrNetWorkBusy + } + plan := debitPlan(w, amount) + if plan == nil { + return 0, stderr.InsufficientBalance + } + var tranType txnmod.TransType + switch source { + case StoreBuyGoods: + tranType = txnmod.StoreBuyGoods + case StoreBuyNudeChat: + tranType = txnmod.StoreBuyNudeChat + case StorePublishWish: + tranType = txnmod.StorePublishWish + case StoreWishEdit: + tranType = txnmod.StoreWishEdit + default: + return 0, stderr.ErrParamError + } + err = appg.VideoDB.Trans(func(t *db.MongoTool) (err error) { + wallet, err := walletmod.Debit(t, plan, uid) + if err != nil { + log.Error("StoreDeductBalance walletmod.Debit fail", log.Any("uid", uid), log.Any("orderId", orderId), log.Any("amount", amount), log.Any("source", source)) + return + } + // 写入流水 + txnLog := &txnmod.TransactionLog{UID: uid, + Amount: -amount, + ActualAmount: float64(-amount), + TranType: tranType.Key(), + TranTypeInt: int64(tranType), + TransNo: orderId, + Desc: desc, + //DiscDoc: discDoc, + //SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + err = txnmod.InsertTransactionLog(t, txnLog) + if err != nil { + log.Error("StoreDeductBalance txnmod.InsertTransactionLog fail", log.Any("uid", uid), log.Any("orderId", orderId), log.Any("amount", amount), log.Any("source", source)) + return + } + balance = wallet.Amount + wallet.Income + return nil + }) + if err != nil { + log.Error("StoreDeductBalance fail", log.Any("uid", uid), log.Any("orderId", orderId), log.Any("amount", amount), log.Any("source", source)) + return 0, stderr.Failure //通知消息 + } + return balance, stderr.Success +} + +// StoreRefund 商城相关退款 +func StoreRefund(uid uint64, amount int64, orderId primitive.ObjectID, source int, desc string) (balance int64, code stderr.Code) { + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return 0, stderr.ErrNetWorkBusy + } + var tranType txnmod.TransType + switch source { + case StoreGoodsOrderRefund: + tranType = txnmod.StoreGoodsOrderRefund + case StoreNudeChatOrderRefund: + tranType = txnmod.StoreNudeChatOrderRefund + case StoreWishRefund: + tranType = txnmod.StoreWishRefund + default: + return 0, stderr.ErrParamError + } + err = appg.VideoDB.Trans(func(t *db.MongoTool) (err error) { + wallet, err := walletmod.CreditAmount(t, amount, uid) + if err != nil { + log.Error("StoreRefund walletmod.CreditAmount fail", log.Any("uid", uid), log.Any("orderId", orderId), log.Any("amount", amount), log.Any("source", source)) + return + } + // 写入流水 + txnLog := &txnmod.TransactionLog{UID: uid, + Amount: amount, + ActualAmount: float64(amount), + TranType: tranType.Key(), + TranTypeInt: int64(tranType), + TransNo: orderId, + Desc: desc, + //DiscDoc: discDoc, + //SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + err = txnmod.InsertTransactionLog(t, txnLog) + if err != nil { + log.Error("StoreRefund txnmod.InsertTransactionLog fail", log.Any("uid", uid), log.Any("orderId", orderId), log.Any("amount", amount), log.Any("source", source)) + return + } + balance = wallet.Amount + wallet.Income + return nil + }) + if err != nil { + log.Error("StoreRefund fail", log.Any("uid", uid), log.Any("orderId", orderId), log.Any("amount", amount), log.Any("source", source)) + return 0, stderr.Failure //通知消息 + } + return balance, stderr.Success +} diff --git a/app/service/walletser/type.go b/app/service/walletser/type.go new file mode 100644 index 0000000..ea18a35 --- /dev/null +++ b/app/service/walletser/type.go @@ -0,0 +1,30 @@ +package walletser + +import ( + "91porn-server/models/commod" + "91porn-server/models/v/txnmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type FruitCoinBillCond struct { + commod.Page +} + +func (cond *FruitCoinBillCond) Query(uid uint64) primitive.M { + return bson.M{ + "uid": uid, + "tranTypeInt": bson.M{"$in": []txnmod.TransType{ + txnmod.BuyNudeChatService, txnmod.FruitCoinRecharge, txnmod.NudeChatConsumption, txnmod.NudeChatRefund, + txnmod.FruitCoinRecharge, txnmod.CurrencyGive, txnmod.AdminCreditFruitCoin, txnmod.AdminDebitFruitCoin, + txnmod.VipCardGive, txnmod.OfficialRech, + }}, + "fruitCoin": bson.M{"$ne": 0, "$exists": true}, + } +} +func (cond *FruitCoinBillCond) Options() *options.FindOptions { + return options.Find().SetLimit(int64(cond.PageSize)).SetSkip(int64((cond.PageNumber - 1) * cond.PageSize)). + SetSort(bson.D{{Key: "createdAt", Value: -1}}) +} diff --git a/app/service/walletser/walletser.go b/app/service/walletser/walletser.go new file mode 100644 index 0000000..2d44b5f --- /dev/null +++ b/app/service/walletser/walletser.go @@ -0,0 +1,317 @@ +package walletser + +import ( + "fmt" + "strconv" + "sync" + "time" + + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/timeutil" + "91porn-server/models/v/proxymod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + + "github.com/shopspring/decimal" +) + +func GetBills(uid uint64, t int64) (data []txnmod.BillsRes, hasNext bool, timeline int64, err error) { + data = make([]txnmod.BillsRes, 0) + if t == 0 { + trans, queryErr := txnmod.LastLedgerTime(uid, time.Now()) + if queryErr != nil { + return nil, false, 0, queryErr + } + if trans.ID.IsZero() { + return + } + t = trans.CreatedAt.Unix() + } + start, end := timeutil.EarlyLastMonth(time.Unix(t, 0)) + var navErr error + var wg sync.WaitGroup + wg.Add(2) + common.Go(func() { + defer wg.Done() + br := txnmod.BillsRes{ + Month: start.Format("2006-01"), + } + br.List, err = txnmod.FindLedgerByTime(uid, start, end) + if err != nil { + return + } + if len(br.List) > 0 { + for _, v := range br.List { + switch v.TranTypeInt { + case int64(txnmod.WithdrawTransfer): + br.Withdraw += -v.Amount + br.WithdrawStr = strconv.Itoa(int(br.Withdraw)) + if br.WithdrawStr == "" { + br.WithdrawStr = "0" + } + case int64(txnmod.WorksIncome), int64(txnmod.ProxyIncome): + br.Income += v.Amount + if br.IncomeStr == "" { + br.IncomeStr = "0" + } + incomeDeci, _ := decimal.NewFromString(br.IncomeStr) + actualIncomeDeci := decimal.NewFromFloat(v.ActualAmount) + br.IncomeStr = incomeDeci.Add(actualIncomeDeci).String() + } + } + } + if len(br.List) > 0 { + data = append(data, br) + } + }) + common.Go(func() { + defer wg.Done() + trans, queryErr := txnmod.LastLedgerTime(uid, start) + if queryErr != nil { + navErr = queryErr + return + } + if !trans.ID.IsZero() { + timeline = trans.CreatedAt.Unix() + hasNext = true + } + + }) + wg.Wait() + if err == nil { + err = navErr + } + return +} + +type rechargeLevel struct { + Min int + Max int + Level int +} + +var rchgLevel = []rechargeLevel{ + {Min: 0, Max: 4900, Level: 0}, + {Min: 5000, Max: 9900, Level: 1}, + {Min: 10000, Max: 19900, Level: 2}, + {Min: 20000, Max: 29900, Level: 3}, + {Min: 30000, Max: 49900, Level: 4}, + {Min: 50000, Max: 99900, Level: 5}, + {Min: 100000, Max: 199900, Level: 6}, + {Min: 200000, Max: 499900, Level: 7}, + {Min: 500000, Max: 1000000, Level: 8}, +} + +func calRchgLevel(amount int) (level int, cur int, limit int) { + l := len(rchgLevel) + if l == 0 { + return + } + min := rchgLevel[0].Min + max := rchgLevel[l-1].Max + if amount < min { + return rchgLevel[0].Level, 0, (rchgLevel[0].Max - rchgLevel[0].Min) + } + if amount > max { + return rchgLevel[l-1].Level, (rchgLevel[l-1].Max - rchgLevel[l-1].Min), (rchgLevel[l-1].Max - rchgLevel[l-1].Min) + } + for _, v := range rchgLevel { + if amount >= v.Min && amount <= v.Max { + return v.Level, (amount - v.Min), (v.Max - v.Min) + } + } + return +} + +// GetRchgLevel 获取用户充值等级 +func GetRchgLevel(uid uint64) (usermod.RechargeLevel, error) { + wallet, err := walletmod.GetWallet(uid) + if err != nil { + return usermod.RechargeLevel{}, err + } + level, cur, limit := calRchgLevel(int(wallet.Consumption)) + info := usermod.RechargeLevel{ + Level: level, + Current: cur / 10, + Limit: limit / 10, + } + return info, err +} + +// GetUidsRchgLevel 获取用户充值等级 +func GetUidsRchgLevel(uids []uint64) (map[uint64]usermod.RechargeLevel, error) { + mInfo := make(map[uint64]usermod.RechargeLevel) + mWallet, err := walletmod.GetWalletMap(uids) + if err != nil { + return mInfo, err + } + for _, v := range mWallet { + level, cur, limit := calRchgLevel(int(v.Consumption)) + info := usermod.RechargeLevel{ + Level: level, + Current: cur / 10, + Limit: limit / 10, + } + mInfo[v.UID] = info + } + return mInfo, err +} + +func GetIncome(uid uint64) (interface{}, error) { + var income string + var vidIncomeToday float64 + var vidIncomeMonth float64 + var vidTotalIncome float64 + var proxyIncomeToday float64 + var proxyIncomeMonth float64 + var InviteeTotalCount int64 + var proxyTotalIncome float64 + var proxyTotalPer int64 + var InviteedTodayCount int64 + var InviteedMonthCount int64 + var proxyPerMonth int64 + var proxyPerToday int64 + var data []txnmod.AgentIncomeRes + now := time.Now() + startM, endM := timeutil.EarlyLastMonth(now) + startD, endD := timeutil.EarlyLastDay(now) + var wg sync.WaitGroup + wg.Add(8) + common.Go(func() { //1 + defer wg.Done() + w, _ := walletmod.GetWallet(uid) + if w == nil { + return + } + income = decimal.NewFromInt(w.Income).Add(decimal.NewFromFloat(w.IncomePot)).String() + vidTotalIncome = w.VidIncome + proxyTotalIncome = w.ProxyIncome + proxyTotalPer = w.Performance + }) + common.Go(func() { //2 + defer wg.Done() + InviteeTotalCount, _ = proxymod.NextProxyCount(uid, time.Unix(0, 0), now) + }) + common.Go(func() { //3 + defer wg.Done() + InviteedMonthCount, _ = proxymod.NextProxyCount(uid, startM, endM) + }) + common.Go(func() { //4 + defer wg.Done() + InviteedTodayCount, _ = proxymod.NextProxyCount(uid, startD, endD) + + }) + common.Go(func() { //5 + defer wg.Done() + vidIncomeToday, _, _ = txnmod.GetIncomeByType(uid, txnmod.WorksIncome, startD, endD) + }) + common.Go(func() { //6 + defer wg.Done() + vidIncomeMonth, _, _ = txnmod.GetIncomeByType(uid, txnmod.WorksIncome, startM, endM) + + }) + common.Go(func() { //7 + defer wg.Done() + proxyIncomeMonth, proxyPerMonth, _ = txnmod.GetIncomeByType(uid, txnmod.ProxyIncome, startM, endM) + }) + common.Go(func() { //8 + defer wg.Done() + data, _ = txnmod.GetProxyIncomeForToday(uid, startD, endD) + }) + wg.Wait() + m := make(map[string]interface{}) + m["income"] = income + m["vidIncomeToday"] = strconv.FormatFloat(vidIncomeToday, 'f', 2, 64) + m["vidIncomeMonth"] = strconv.FormatFloat(vidIncomeMonth, 'f', 2, 64) + m["vidIncomeTotal"] = strconv.FormatFloat(vidTotalIncome, 'f', 2, 64) + m["proxyIncomeTotal"] = strconv.FormatFloat(proxyTotalIncome, 'f', 2, 64) + m["proxyPer"] = strconv.FormatInt(proxyTotalPer, 10) + m["inviteeCountTotal"] = strconv.FormatInt(InviteeTotalCount, 10) + m["proxyIncomeLv1"] = "0" + m["proxyIncomeLv2"] = "0" + m["proxyIncomeLv3"] = "0" + m["proxyIncomeLv4"] = "0" + m["proxyIncomeLv5"] = "0" + for _, v := range data { + proxyIncomeToday += v.TotalMoney + proxyPerToday += v.TotalPerformance + m["proxyIncomeLv"+strconv.Itoa(v.AgentLevel)] = strconv.FormatFloat(v.TotalMoney, 'f', 2, 64) + } + m["proxyPerMonth"] = strconv.FormatInt(proxyPerMonth, 10) + m["proxyIncomeMonth"] = strconv.FormatFloat(proxyIncomeMonth, 'f', 2, 64) + m["inviteeCountMonth"] = strconv.FormatInt(InviteedMonthCount, 10) + m["proxyPerToday"] = strconv.FormatInt(proxyPerToday, 10) + m["proxyIncomeToday"] = strconv.FormatFloat(proxyIncomeToday, 'f', 2, 64) + m["inviteeCountToday"] = strconv.FormatInt(InviteedTodayCount, 10) + return m, nil +} + +func GetBills1(uid uint64, year int, month int, pageNumber, pageSize uint64) (res txnmod.Bills1Res, err error) { + start, end := timeutil.GetTimeByMonthAndYear(month, year) + if pageNumber > 1 { + res.List, res.HasNext, err = txnmod.Ibills(uid, pageNumber, pageSize, start, end) + return + } + var incomeErr, expenditureErr, listErr, navigationErr error + var wg sync.WaitGroup + wg.Add(4) + common.Go(func() { //1 + defer wg.Done() + i, queryErr := txnmod.Income(uid, start, end) + incomeErr = queryErr + res.Income = strconv.FormatFloat(i, 'f', 2, 64) + }) + common.Go(func() { //2 + defer wg.Done() + e, queryErr := txnmod.Expenditure(uid, start, end) + expenditureErr = queryErr + res.Expenditure = strconv.FormatFloat(e, 'f', 2, 64) + }) + common.Go(func() { //3 + defer wg.Done() + start, end := timeutil.GetTimeByMonthAndYear(month, year) + res.List, res.HasNext, listErr = txnmod.Ibills(uid, pageNumber, pageSize, start, end) + }) + common.Go(func() { //4 + defer wg.Done() + t, queryErr := txnmod.LastLedgerTime(uid, start) + navigationErr = queryErr + if !t.ID.IsZero() { + res.HasNextMonth = true + res.Year = t.CreatedAt.Year() + res.Month = int(t.CreatedAt.Month()) + } + }) + wg.Wait() + for _, queryErr := range []error{incomeErr, expenditureErr, listErr, navigationErr} { + if queryErr != nil { + return txnmod.Bills1Res{}, queryErr + } + } + if len(res.List) > 0 { + for i, tlog := range res.List { + if tlog.TranTypeInt == int64(txnmod.GoldCouplePayVID) { + res.List[i].ActualAmount = 0 + res.List[i].Amount = 0 + } + } + } + return +} + +// FruitCoinBill 果币账单 +func FruitCoinBill(uid uint64, in *FruitCoinBillCond) (interface{}, stderr.Code) { + var data = map[string]interface{}{ + "list": []interface{}{}, + } + list, err := txnmod.QueryAll(in.Query(uid), in.Options()) + if err != nil { + log.Error(fmt.Sprintf("查询果币账单列表异常[%v]", err)) + return data, stderr.ErrDbQueryError + } + data["list"] = list + return data, stderr.Success +} diff --git a/app/service/withdrawser/withdrawser.go b/app/service/withdrawser/withdrawser.go new file mode 100644 index 0000000..408a199 --- /dev/null +++ b/app/service/withdrawser/withdrawser.go @@ -0,0 +1,267 @@ +package withdrawser + +import ( + "errors" + "fmt" + "strconv" + "sync" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/crypt" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + "91porn-server/models/v/wdchannmod" + "91porn-server/models/v/wdordmod" + "91porn-server/models/v/wdtaxmod" +) + +// WithDraw 提现 +func WithDraw(wd *wdordmod.WithdrawOrder) (err error) { + now := time.Now() + u, err := usermod.FindUserByUID(wd.UID) + if err != nil || u == nil { + return + } + wt, err := WithdrawType(u.GetSuperUser(now), wd.PayType) + if err != nil { + return err + } + if wd.ProductType == 1 { + return errors.New("瓦力游戏已下架") + } + + w, err := walletmod.GetWallet(wd.UID) + if err != nil { + return err + } + if wd.Name == "" { + wd.Name = u.Name + } + if wd.DevID == "" { + wd.DevID = u.DevID + } + wd.DeviceType = u.SysType + wd.DiscDoc = u.DiscDoc + switch wd.WithdrawType { + case wdordmod.AgentWithdraw: //代理提现 + wd.PayMoney = (wd.Money * (int64)(100-wt.CoinTax)) / 100 + if wd.Money > w.Money { + return errors.New("Insufficient balance") + } + //开启事务 + return appg.VideoDB.Trans(func(t *db.MongoTool) error { + wd.Status = wdordmod.DEFAULT + if _, err = wdordmod.InsertWithdrawOrder(t, wd); err != nil { + return err + } + _, err = walletmod.DebitMoney(t, wd.Money, wd.UID) + return err + }) + default: //wdordmod.GoldWithdraw 金币提现 + wd.Amount = wd.Money / 10 + wd.PayMoney = (wd.Money * (int64)(100-wt.CoinTax)) / 100 + if wd.Amount > w.Income { + return errors.New("Insufficient balance") + } + //开启事务 + return appg.VideoDB.Trans(func(t *db.MongoTool) error { + wd.Status = wdordmod.DEFAULT + if _, err = wdordmod.InsertWithdrawOrder(t, wd); err != nil { + return err + } + wallet, err := walletmod.DebitIncome(t, wd.Amount, wd.UID) + if err != nil { + return err + } + //插入收支流水 + tt := txnmod.TransactionLog{ + TransNo: wd.ID, + UID: wd.UID, + Amount: -wd.Amount, + TranType: txnmod.WithdrawTransfer.Key(), + TranTypeInt: int64(txnmod.WithdrawTransfer), + ChannelType: wd.PayType, + Desc: "用户提现-" + strconv.FormatInt(wd.Amount, 10), + DiscDoc: wd.DiscDoc, + SysType: wd.DeviceType, + ActualAmount: -float64(wd.Amount), + RealAmount: walletmod.GetRealAmount(wallet), + } + //插入一条支出流水 + return txnmod.InsertTransactionLog(t, &tt) + }) + } +} + +// WithdrawType 获取提现方式 +func WithdrawType(superUser bool, wType string) (res wdchannmod.WithdrawChannelRes, err error) { + var wg sync.WaitGroup + wg.Add(2) + common.Go(func() { + defer wg.Done() + wc, err := wdchannmod.GetWithdrawChannel(wType) + if err != nil { + return + } + res.WithdrawChannel = *wc + }) + common.Go(func() { + defer wg.Done() + wt, err := wdtaxmod.GetWithDrawTariff() + if err != nil { + return + } + if superUser { + res.WithdrawTariff.CoinTax = wt.SuperUserCoinTax + } else { + res.WithdrawTariff.CoinTax = wt.CoinTax + } + if wt.GameTax <= 0 { + wt.GameTax = 2 + } + res.WithdrawTariff.GameTax = wt.GameTax + }) + wg.Wait() + return +} + +// GetWithdrawCfg 获取提现配置方式 +func GetWithdrawCfg() (res wdchannmod.WithdrawChannelAppRes) { + var wd []*wdchannmod.AppWithdrawChannels + var wt wdtaxmod.WithdrawTariff + var wg sync.WaitGroup + wg.Add(2) + common.Go(func() { + defer wg.Done() + str, err := appg.Redis.Get(redisconst.WithdrawCfgCacheKey()) + if str != nil && err == nil { + _ = crypt.JSON2Struct(*str, &wd) + } else { + wd, err = wdchannmod.GetActiveWithdrawChannels() + if wd != nil && err == nil { + normalstr, _ := crypt.StructToStrNormal(&wd) + _ = appg.Redis.Set(redisconst.WithdrawCfgCacheKey(), normalstr, redisconst.WithdrawCfgExpire) + } + } + }) + common.Go(func() { + defer wg.Done() + str, err := appg.Redis.Get(redisconst.WithdrawTariffCacheKey()) + if str != nil && err == nil { + _ = crypt.JSON2Struct(*str, &wt) + } else { + wt, err = wdtaxmod.GetWithDrawTariff() + if err == nil { + normalstr, _ := crypt.StructToStrNormal(wt) + _ = appg.Redis.Set(redisconst.WithdrawTariffCacheKey(), normalstr, redisconst.WithdrawTariffExpire) + } + } + }) + wg.Wait() + res.Channels = wd + res.CashTax = wt.CashTax + res.CoinTax = wt.CoinTax + return +} + +// ExchgCallBack 提现回调 +func ExchgCallBack(tradeNo string, oid string, money int64, status string, mark string) error { + w, err := wdordmod.FindWithdrawOrderByID(tradeNo) + if err != nil { + return err + } + if w.Status == wdordmod.SUCCESS || w.Status == wdordmod.FAILURE { + return nil + } + if w.Status != wdordmod.PROCESSING && w.Status != wdordmod.UNKNOWN { + log.Error(fmt.Sprintf("Wtihdraw status error ,order:%+v:", w)) + return errors.New("Wtihdraw status error") + } + if w.ProductType == 1 { + return errors.New("瓦力游戏已下架") + } + now := time.Now() + set := wdordmod.EditSelector{ + OID: &oid, + } + switch status { + case "paid": //已经打款 + //修改订单状态 + set.PayMoney = &money + set.SuccessAt = &now + set.ReceivedAt = &now + set.Status = wdordmod.SUCCESS + set.StatusDesc = wdordmod.SUCCESSStatus + set.SuccessAt = &now + if err = wdordmod.UpdateWithPROCESSINGAndUNKNOWN(nil, tradeNo, set); err != nil { + return err + } + if w.PayType == wdordmod.TypeBankCard { + _, err = usermod.Update(w.UID, usermod.UserSelector{BankActName: &w.ActName}) + return err + } + return nil + case "refused": //拒绝 + return appg.VideoDB.Trans(func(t *db.MongoTool) error { + switch w.WithdrawType { + case wdordmod.GoldWithdraw: + var wallet *walletmod.Wallet + wallet, err = walletmod.CreditIncome(t, w.Amount, w.UID) + if err != nil { + return err + } + //插入收支流水 + tk := txnmod.TransactionLog{ + TransNo: w.ID, + UID: w.UID, + Amount: w.Amount, + TranType: txnmod.WithdrawRefund.Key(), + TranTypeInt: int64(txnmod.WithdrawRefund), + ChannelType: w.PayType, + Desc: "用户提现失败,退款" + strconv.FormatInt(w.Amount, 10), + DiscDoc: w.DiscDoc, + SysType: w.DeviceType, + ActualAmount: float64(w.Amount), + RealAmount: walletmod.GetRealAmount(wallet), + } + //插入一条支出流水 + if err = txnmod.InsertTransactionLog(t, &tk); err != nil { + return err + } + case wdordmod.AgentWithdraw: + if err = walletmod.CreditMoney(t, w.Money, w.UID); err != nil { + return err + } + case wdordmod.DiscWithdraw: + // err = discWirhdrawRefund(t, w, "提现被平台拒绝") + if err != nil { + log.Error("discWirhdrawRefund failed", log.E(err), log.Any("witOrd", w)) + return err + } + default: + return errors.New("invalid type") + } + set.Status = wdordmod.FAILURE + set.StatusDesc = mark + set.FailureAt = &now + if err = wdordmod.UpdateWithPROCESSINGAndUNKNOWN(t, tradeNo, set); err != nil { + return err + } + //棋牌usdt兑换拒绝增加usdt可提现额度 + if w.ProductType == 1 && w.PayType == wdordmod.TypeUsdt { + if _, err = walletmod.IncUsdtAmount(w.UID, money); err != nil { + return err + } + } + return nil + }) + default: + return nil + } +} diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..f0ff300 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,109 @@ + + + + + + + Document + + + + + +
+
+
+ 充值问题 + +
+
+ 提现问题 + +
+
+
+

1.如何充值音币

+

1)直播页面打开礼物面板,可以快速充值

+

2)在“我的-更多-钱包”页面中进行充值

+

2.为什么提示“充值失败”/“充值遇到问题”?

+

1)请检查付款时网络链接是否正常

+

2)请检查付款账号余额是否大于购买的音币数量

+

3.付款成功,但音币没到账?

+

+ 1)由于网络原因可能导致音币到账延迟,滞后90分钟内属于正常现象,请耐心等候 +

+

+ 2)如果付款成功且超过48小时音币仍未到账,请在【消息】-【在线客服】进行反馈,会有工作人员在工作时间帮你核查,反馈时请提供以下界面截图,缺一不可 +

+

+ (1)、(支付成功后的订单详情页)截图,需包含消费时间、金额、收款对象 +

+

+ (2)、个人主页(我)-右上角(三)-(钱包)-(充值)-(充值记录)页截图 +

+

+ (3)、如你是苹果用户、则需要额外提供你的购买记录:前往手机【设置】-【iTunes + store与APP store】-点击最上方【Apple id】-查看【Apple + id】-【购买记录】,找到您没有到账的对应订单,将订单详情页截图给我们 +

+

4.每日提现限额

+

提现每笔最小100元,提现每天最多10000元

+
+
+

1.如何提现?

+

+ 前往提现页面:“个人主页-右上角-钱包”,点击我的收益开始提现,提现前需在音色平台的账户中绑定手机号,否则无法进入提现操作 +

+

1)支持支付宝 / 银行卡

+

+ 2)按照要求绑定支付宝账号 / + 绑定银行卡以及验证信息,请确保提供的信息准确无误,以免绑定失败 +

+

3)绑定信息验证成功后,即可选择提现金额,并完成提现

+

2.显示“提现失败”

+

+ 1)请检查要提现金额是否大于可提现金额,且是否符合提现的最低&最高限额 +

+

2)银行卡提现时,需确认绑定的银行卡可正常使用

+

+ 3)支付宝提现失败时,需确认当前绑定的支付宝账号是否正确以及支付宝账号是否可用 +

+

3.显示“提现成功”但没到账

+

+ 1)请检查要提现金额是否大于可提现金额,且是否符合提现的最低&最高限额 +

+

+ 2)高峰期提现人数多会导致网络拥堵,到账之后72小时内属于正常现象,请耐心等候,如72小时后仍未到账,请在【消息】-【在线客服】中将问题反馈给我们,反馈时请提供APP内的提现记录 +

+

4.提现有手续费吗?

+

金币提现暂无手续费

+
+
+ + + + + diff --git a/app/templates/sources/img/right.png b/app/templates/sources/img/right.png new file mode 100644 index 0000000000000000000000000000000000000000..0d5bb72dd1bf3ea3e2397b3b11ad521d1555b02a GIT binary patch literal 718 zcmV;<0x|uGP)Px%h)G02R5%fhRnKb^K@{HIm?o)+=Af6wlah-Fg5pg`@S^pg2ySl)l%&yX^$)05 zK|N}cB_+9(wjds?@y8*73WC&=w}3}46^S60#JGvy7k1al%%s}~!+Y<0KW5(BnYU)8 zQaNLq=9*y`7sli9`(!e?TqqQNKtB{&@Tw#53f3{mw+Dm4Yb2dNR5Y9{?0!*nMxx8m zH$gEGpCwxe=xB^LdcEFr4X!{|JRZLX^qXQaj3P=|)!N)G8V49Tg4xVX*LB%kg~9~1 zAjt*-?kiq~F+UoOULna@**2AcMjZ7iPVq!`MB*F>EC%km^_5!mGH2KLgw3HU$bfKG_O(UJ)_MIQDnTvaTU`Pqa-n5i?;C z!RstRbb`Dei^X!qV)2uN|L=i6P+0d=Fn0D3@>!K!{$X2Z{iYycCdayevwEd7Et$oWTfx#58g%V16d%SzW@LL07*qoM6N<$f-Hd;d++)c=a.touches[d],b.push({x:c.pageX,y:c.pageY});return b}return[{x:a.pageX,y:a.pageY}]},b.getDistance=function(a,b){var c=b.x-a.x,d=b.y-a.y;return Math.sqrt(c*c+d*d)},b.getFingers=function(a){return a.touches?a.touches.length:1},b.calScale=function(a,b){if(a.length>=2&&b.length>=2){var c=this.getDistance(a[1],a[0]),d=this.getDistance(b[1],b[0]);return d/c}return 1},b.getAngle=function(a,b){return 180*Math.atan2(b.y-a.y,b.x-a.x)/Math.PI},b.getAngle180=function(a,b){var c=Math.atan(-1*(b.y-a.y)/(b.x-a.x))*(180/Math.PI);return 0>c?c+180:c},b.getDirectionFromAngle=function(a){var b={up:-45>a&&a>-135,down:a>=45&&135>a,left:a>=135||-135>=a,right:a>=-45&&45>=a};for(var c in b)if(b[c])return c;return null},b.getXYByElement=function(a){for(var b=0,c=0;a.offsetParent;)b+=a.offsetLeft,c+=a.offsetTop,a=a.offsetParent;return{left:b,top:c}},b.reset=function(){h=i=j=null,q=o=k=l=!1,m=!1,f={},t=!1},b.isTouchMove=function(a){return"touchmove"===a.type||"mousemove"===a.type},b.isTouchEnd=function(a){return"touchend"===a.type||"mouseup"===a.type||"touchcancel"===a.type},b.env=function(){var a={},b=navigator.userAgent,c=b.match(/(Android)[\s\/]+([\d\.]+)/),d=b.match(/(iPad|iPhone|iPod)\s+OS\s([\d_\.]+)/),e=b.match(/(Windows\s+Phone)\s([\d\.]+)/),f=/WebKit\/[\d.]+/i.test(b),g=d?navigator.standalone?f:/Safari/i.test(b)&&!/CriOS/i.test(b)&&!/MQQBrowser/i.test(b):!1;return c&&(a.android=!0,a.version=c[2]),d&&(a.ios=!0,a.version=d[2].replace(/_/g,"."),a.ios7=/^7/.test(a.version),"iPad"===d[1]?a.ipad=!0:"iPhone"===d[1]?(a.iphone=!0,a.iphone5=568==screen.height):"iPod"===d[1]&&(a.ipod=!0)),e&&(a.wp=!0,a.version=e[2],a.wp8=/^8/.test(a.version)),f&&(a.webkit=!0),g&&(a.safari=!0),a}();var c={proxyid:0,proxies:[],trigger:function(a,b,c){c=c||{};var d,e={bubbles:!0,cancelable:!0,detail:c};try{"undefined"!=typeof CustomEvent?(d=new CustomEvent(b,e),a&&a.dispatchEvent(d)):(d=document.createEvent("CustomEvent"),d.initCustomEvent(b,!0,!0,c),a&&a.dispatchEvent(d))}catch(f){console.warn("Touch.js is not supported by environment.")}},bind:function(a,c,d){a.listeners=a.listeners||{},a.listeners[c]?a.listeners[c].push(d):a.listeners[c]=[d];var e=function(a){b.env.ios7&&b.forceReflow(),a.originEvent=a;for(var c in a.detail)"type"!==c&&(a[c]=a.detail[c]);a.startRotate=function(){t=!0};var e=d.call(a.target,a);"undefined"==typeof e||e||(a.stopPropagation(),a.preventDefault())};d.proxy=d.proxy||{},d.proxy[c]?d.proxy[c].push(this.proxyid++):d.proxy[c]=[this.proxyid++],this.proxies.push(e),a.addEventListener&&a.addEventListener(c,e,!1)},unbind:function(a,b,c){if(c){var d=c.proxy[b];d&&d.length&&d.forEach(function(){a.removeEventListener&&a.removeEventListener(b,this.proxies[this.proxyid],!1)})}else{var e=a.listeners[b];e&&e.length&&e.forEach(function(c){a.removeEventListener(b,c,!1)})}},delegate:function(a,c,d,e){var f=function(c){var f,g;c.originEvent=c;for(var h in c.detail)"type"!==h&&(c[h]=c.detail[h]);c.startRotate=function(){t=!0};var i=b.getSelector(a)+" "+d,j=b.matchSelector(c.target,i),k=b.matchSelector(c.target,i+" "+c.target.nodeName);if(!j&&k){for(b.env.ios7&&b.forceReflow(),f=c.target;!b.matchSelector(f,i);)f=f.parentNode;g=e.call(c.target,c),"undefined"==typeof g||g||(c.stopPropagation(),c.preventDefault())}else b.env.ios7&&b.forceReflow(),(j||k)&&(g=e.call(c.target,c),"undefined"==typeof g||g||(c.stopPropagation(),c.preventDefault()))};e.proxy=e.proxy||{},e.proxy[c]?e.proxy[c].push(this.proxyid++):e.proxy[c]=[this.proxyid++],this.proxies.push(f),a.listeners=a.listeners||{},a.listeners[c]?a.listeners[c].push(f):a.listeners[c]=[f],a.addEventListener&&a.addEventListener(c,f,!1)},undelegate:function(a,b,c,d){if(d){var e=d.proxy[b];e.length&&e.forEach(function(){a.removeEventListener&&a.removeEventListener(b,this.proxies[this.proxyid],!1)})}else{var f=a.listeners[b];f.forEach(function(c){a.removeEventListener(b,c,!1)})}}},d={tap:!0,doubleTap:!0,tapMaxDistance:10,hold:!0,tapTime:200,holdTime:650,maxDoubleTapInterval:300,swipe:!0,swipeTime:300,swipeMinDistance:18,swipeFactor:5,drag:!0,pinch:!0,minScaleRate:0,minRotationAngle:0},e={TOUCH_START:"touchstart",TOUCH_MOVE:"touchmove",TOUCH_END:"touchend",TOUCH_CANCEL:"touchcancel",MOUSE_DOWN:"mousedown",MOUSE_MOVE:"mousemove",MOUSE_UP:"mouseup",CLICK:"click",PINCH_START:"pinchstart",PINCH_END:"pinchend",PINCH:"pinch",PINCH_IN:"pinchin",PINCH_OUT:"pinchout",ROTATION_LEFT:"rotateleft",ROTATION_RIGHT:"rotateright",ROTATION:"rotate",SWIPE_START:"swipestart",SWIPING:"swiping",SWIPE_END:"swipeend",SWIPE_LEFT:"swipeleft",SWIPE_RIGHT:"swiperight",SWIPE_UP:"swipeup",SWIPE_DOWN:"swipedown",SWIPE:"swipe",DRAG:"drag",DRAGSTART:"dragstart",DRAGEND:"dragend",HOLD:"hold",TAP:"tap",DOUBLE_TAP:"doubletap"},f={start:null,move:null,end:null},g=0,h=null,i=null,j=null,k=!1,l=!1,m=!1,n={},o=!1,p=null,q=!1,r=null,s=1,t=!1,u=[],v=0,w=0,x=0,y=null,z={getAngleDiff:function(a){for(var c=parseInt(v-b.getAngle180(a[0],a[1]),10),d=0;Math.abs(c-w)>90&&d++<50;)0>w?c-=180:c+=180;return w=parseInt(c,10)},pinch:function(a){var g=a.target;if(d.pinch){if(!o)return;if(b.getFingers(a)<2&&!b.isTouchEnd(a))return;var h=b.calScale(f.start,f.move),i=this.getAngleDiff(f.move),j={type:"",originEvent:a,scale:h,rotation:i,direction:i>0?"right":"left",fingersCount:b.getFingers(a)};if(l?b.isTouchMove(a)?(j.fingerStatus="move",c.trigger(g,e.PINCH,j)):b.isTouchEnd(a)&&(j.fingerStatus="end",c.trigger(g,e.PINCH_END,j),b.reset()):(l=!0,j.fingerStatus="start",c.trigger(g,e.PINCH_START,j)),Math.abs(1-h)>d.minScaleRate){var k=b.simpleClone(j),m=1e-11;h>s?(s=h-m,c.trigger(g,e.PINCH_OUT,k,!1)):s>h&&(s=h+m,c.trigger(g,e.PINCH_IN,k,!1)),b.isTouchEnd(a)&&(s=1)}if(Math.abs(i)>d.minRotationAngle){var n,p=b.simpleClone(j);n=i>0?e.ROTATION_RIGHT:e.ROTATION_LEFT,c.trigger(g,n,p,!1),c.trigger(g,e.ROTATION,j)}}},rotateSingleFinger:function(a){var d=a.target;if(t&&b.getFingers(a)<2){if(!f.move)return;if(u.length<2){var g=b.getXYByElement(d);u=[{x:g.left+d.offsetWidth/2,y:g.top+d.offsetHeight/2},f.move[0]],v=parseInt(b.getAngle180(u[0],u[1]),10)}var h=[u[0],f.move[0]],i=this.getAngleDiff(h),j={type:"",originEvent:a,rotation:i,direction:i>0?"right":"left",fingersCount:b.getFingers(a)};b.isTouchMove(a)?j.fingerStatus="move":(b.isTouchEnd(a)||"mouseout"===a.type)&&(j.fingerStatus="end",c.trigger(d,e.PINCH_END,j),b.reset());var k=i>0?e.ROTATION_RIGHT:e.ROTATION_LEFT;c.trigger(d,k,j),c.trigger(d,e.ROTATION,j)}},swipe:function(a){var h=a.target;if(o&&f.move&&!(b.getFingers(a)>1)){var i=Date.now(),j=i-g,l=b.getDistance(f.start[0],f.move[0]),p={x:f.move[0].x-n.left,y:f.move[0].y-n.top},q=b.getAngle(f.start[0],f.move[0]),r=b.getDirectionFromAngle(q),s=j/1e3,t=10*(10-d.swipeFactor)*s*s,u={type:e.SWIPE,originEvent:a,position:p,direction:r,distance:l,distanceX:f.move[0].x-f.start[0].x,distanceY:f.move[0].y-f.start[0].y,x:f.move[0].x-f.start[0].x,y:f.move[0].y-f.start[0].y,angle:q,duration:j,fingersCount:b.getFingers(a),factor:t};if(d.swipe){var v=function(){var a=e;switch(r){case"up":c.trigger(h,a.SWIPE_UP,u);break;case"down":c.trigger(h,a.SWIPE_DOWN,u);break;case"left":c.trigger(h,a.SWIPE_LEFT,u);break;case"right":c.trigger(h,a.SWIPE_RIGHT,u)}};k?b.isTouchMove(a)?(u.fingerStatus=u.swipe="move",c.trigger(h,e.SWIPING,u),j>d.swipeTime&&jd.swipeMinDistance&&(v(),c.trigger(h,e.SWIPE,u,!1))):(b.isTouchEnd(a)||"mouseout"===a.type)&&(u.fingerStatus=u.swipe="end",c.trigger(h,e.SWIPE_END,u),d.swipeTime>j&&l>d.swipeMinDistance&&(v(),c.trigger(h,e.SWIPE,u,!1))):(u.fingerStatus=u.swipe="start",k=!0,c.trigger(h,e.SWIPE_START,u))}d.drag&&(m?b.isTouchMove(a)?(u.fingerStatus=u.swipe="move",c.trigger(h,e.DRAG,u)):b.isTouchEnd(a)&&(u.fingerStatus=u.swipe="end",c.trigger(h,e.DRAGEND,u)):(u.fingerStatus=u.swipe="start",m=!0,c.trigger(h,e.DRAGSTART,u)))}},tap:function(a){var h=a.target;if(d.tap){var i=Date.now(),j=i-g,k=b.getDistance(f.start[0],f.move?f.move[0]:f.start[0]);clearTimeout(p);var l=function(){if(y&&d.doubleTap&&g-xa)return!0}return!1}();if(l)return clearTimeout(r),void c.trigger(h,e.DOUBLE_TAP,{type:e.DOUBLE_TAP,originEvent:a,position:f.start[0]});if(d.tapMaxDistancej&&b.getFingers(a)<=1&&(q=!0,x=i,y=f.start[0],r=setTimeout(function(){c.trigger(h,e.TAP,{type:e.TAP,originEvent:a,fingersCount:b.getFingers(a),position:y})},d.tapTime))}},hold:function(a){var e=a.target;d.hold&&(clearTimeout(p),p=setTimeout(function(){if(f.start){var g=b.getDistance(f.start[0],f.move?f.move[0]:f.start[0]);d.tapMaxDistance=2&&(v=parseInt(b.getAngle180(f.start[0],f.start[1]),10)),g=Date.now(),h=a,n={};var d=c.getBoundingClientRect(),e=document.documentElement;n={top:d.top+(window.pageYOffset||e.scrollTop)-(e.clientTop||0),left:d.left+(window.pageXOffset||e.scrollLeft)-(e.clientLeft||0)},z.hold(a);break;case"touchmove":case"mousemove":if(!o||!f.start)return;f.move=b.getPosOfEvent(a),b.getFingers(a)>=2?z.pinch(a):t?z.rotateSingleFinger(a):z.swipe(a);break;case"touchend":case"touchcancel":case"mouseup":case"mouseout":if(!o)return;j=a,l?z.pinch(a):t?z.rotateSingleFinger(a):k?z.swipe(a):z.tap(a),b.reset(),v=0,w=0,a.touches&&1===a.touches.length&&(o=!0,t=!0)}},B=function(){function a(a){b.hasTouch||(a=b.getPCevts(a)),j.forEach(function(b){c.delegate(b,a,h,g[a])})}function d(a){b.hasTouch||(a=b.getPCevts(a)),j.forEach(function(b){c.bind(b,a,g[a])})}var e,f,g,h,i=arguments;if(i.length<2||i>4)return console.error("unexpected arguments!");var j="string"===b.getType(i[0])?document.querySelectorAll(i[0]):i[0];if(j=j.length?Array.prototype.slice.call(j):[j],3===i.length&&"string"===b.getType(i[1]))return e=i[1].split(" "),f=i[2],void e.forEach(function(a){b.hasTouch||(a=b.getPCevts(a)),j.forEach(function(b){c.bind(b,a,f)})});if(3!==i.length||"object"!==b.getType(i[1]))if(2!==i.length||"object"!==b.getType(i[1])){if(4===i.length&&"object"===b.getType(i[2]))return e=i[1].split(" "),f=i[3],void e.forEach(function(a){b.hasTouch||(a=b.getPCevts(a)),j.forEach(function(b){c.bind(b,a,f)})});if(4===i.length){var k=j[0];return e=i[1].split(" "),h=i[2],f=i[3],void e.forEach(function(a){b.hasTouch||(a=b.getPCevts(a)),c.delegate(k,a,h,f)})}}else{g=i[1];for(var l in g)d(l)}else{g=i[1],h=i[2];for(var m in g)a(m)}},C=function(){var a,d,e=arguments;if(e.length<1||e.length>4)return console.error("unexpected arguments!");var f="string"===b.getType(e[0])?document.querySelectorAll(e[0]):e[0];if(f=f.length?Array.prototype.slice.call(f):[f],1===e.length||2===e.length)return void f.forEach(function(d){a=e[1]?e[1].split(" "):Object.keys(d.listeners),a.length&&a.forEach(function(a){b.hasTouch||(a=b.getPCevts(a)),c.unbind(d,a),c.undelegate(d,a)})});if(3===e.length&&"function"===b.getType(e[2]))return d=e[2],void f.forEach(function(f){a=e[1].split(" "),a.forEach(function(a){b.hasTouch||(a=b.getPCevts(a)),c.unbind(f,a,d)})});if(3===e.length&&"string"===b.getType(e[2])){var g=e[2];return void f.forEach(function(d){a=e[1].split(" "),a.forEach(function(a){b.hasTouch||(a=b.getPCevts(a)),c.undelegate(d,a,g)})})}return 4===e.length?(d=e[3],void f.forEach(function(f){a=e[1].split(" "),a.forEach(function(a){b.hasTouch||(a=b.getPCevts(a)),c.undelegate(f,a,g,d)})})):void 0},D=function(a,d,e){var f=arguments;b.hasTouch||(d=b.getPCevts(d));var g="string"===b.getType(f[0])?document.querySelectorAll(f[0]):f[0];g=g.length?Array.prototype.call(g):[g],g.forEach(function(a){c.trigger(a,d,e)})};a();var E={};return E.on=E.bind=E.live=B,E.off=E.unbind=E.die=C,E.config=d,E.trigger=D,E}); \ No newline at end of file diff --git a/app/templates/sources/style/animate.css b/app/templates/sources/style/animate.css new file mode 100644 index 0000000..adf4608 --- /dev/null +++ b/app/templates/sources/style/animate.css @@ -0,0 +1,3609 @@ +@charset "UTF-8"; + +/*! + * animate.css -https://daneden.github.io/animate.css/ + * Version - 3.7.2 + * Licensed under the MIT license - http://opensource.org/licenses/MIT + * + * Copyright (c) 2019 Daniel Eden + */ + +@-webkit-keyframes bounce { + from, + 20%, + 53%, + 80%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 40%, + 43% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -30px, 0); + transform: translate3d(0, -30px, 0); + } + + 70% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -15px, 0); + transform: translate3d(0, -15px, 0); + } + + 90% { + -webkit-transform: translate3d(0, -4px, 0); + transform: translate3d(0, -4px, 0); + } +} + +@keyframes bounce { + from, + 20%, + 53%, + 80%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 40%, + 43% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -30px, 0); + transform: translate3d(0, -30px, 0); + } + + 70% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -15px, 0); + transform: translate3d(0, -15px, 0); + } + + 90% { + -webkit-transform: translate3d(0, -4px, 0); + transform: translate3d(0, -4px, 0); + } +} + +.bounce { + -webkit-animation-name: bounce; + animation-name: bounce; + -webkit-transform-origin: center bottom; + transform-origin: center bottom; +} + +@-webkit-keyframes flash { + from, + 50%, + to { + opacity: 1; + } + + 25%, + 75% { + opacity: 0; + } +} + +@keyframes flash { + from, + 50%, + to { + opacity: 1; + } + + 25%, + 75% { + opacity: 0; + } +} + +.flash { + -webkit-animation-name: flash; + animation-name: flash; +} + +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ + +@-webkit-keyframes pulse { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +@keyframes pulse { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +.pulse { + -webkit-animation-name: pulse; + animation-name: pulse; +} + +@-webkit-keyframes rubberBand { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 30% { + -webkit-transform: scale3d(1.25, 0.75, 1); + transform: scale3d(1.25, 0.75, 1); + } + + 40% { + -webkit-transform: scale3d(0.75, 1.25, 1); + transform: scale3d(0.75, 1.25, 1); + } + + 50% { + -webkit-transform: scale3d(1.15, 0.85, 1); + transform: scale3d(1.15, 0.85, 1); + } + + 65% { + -webkit-transform: scale3d(0.95, 1.05, 1); + transform: scale3d(0.95, 1.05, 1); + } + + 75% { + -webkit-transform: scale3d(1.05, 0.95, 1); + transform: scale3d(1.05, 0.95, 1); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +@keyframes rubberBand { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 30% { + -webkit-transform: scale3d(1.25, 0.75, 1); + transform: scale3d(1.25, 0.75, 1); + } + + 40% { + -webkit-transform: scale3d(0.75, 1.25, 1); + transform: scale3d(0.75, 1.25, 1); + } + + 50% { + -webkit-transform: scale3d(1.15, 0.85, 1); + transform: scale3d(1.15, 0.85, 1); + } + + 65% { + -webkit-transform: scale3d(0.95, 1.05, 1); + transform: scale3d(0.95, 1.05, 1); + } + + 75% { + -webkit-transform: scale3d(1.05, 0.95, 1); + transform: scale3d(1.05, 0.95, 1); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +.rubberBand { + -webkit-animation-name: rubberBand; + animation-name: rubberBand; +} + +@-webkit-keyframes shake { + from, + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 10%, + 30%, + 50%, + 70%, + 90% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + + 20%, + 40%, + 60%, + 80% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } +} + +@keyframes shake { + from, + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 10%, + 30%, + 50%, + 70%, + 90% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + + 20%, + 40%, + 60%, + 80% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } +} + +.shake { + -webkit-animation-name: shake; + animation-name: shake; +} + +@-webkit-keyframes headShake { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + + 6.5% { + -webkit-transform: translateX(-6px) rotateY(-9deg); + transform: translateX(-6px) rotateY(-9deg); + } + + 18.5% { + -webkit-transform: translateX(5px) rotateY(7deg); + transform: translateX(5px) rotateY(7deg); + } + + 31.5% { + -webkit-transform: translateX(-3px) rotateY(-5deg); + transform: translateX(-3px) rotateY(-5deg); + } + + 43.5% { + -webkit-transform: translateX(2px) rotateY(3deg); + transform: translateX(2px) rotateY(3deg); + } + + 50% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} + +@keyframes headShake { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + + 6.5% { + -webkit-transform: translateX(-6px) rotateY(-9deg); + transform: translateX(-6px) rotateY(-9deg); + } + + 18.5% { + -webkit-transform: translateX(5px) rotateY(7deg); + transform: translateX(5px) rotateY(7deg); + } + + 31.5% { + -webkit-transform: translateX(-3px) rotateY(-5deg); + transform: translateX(-3px) rotateY(-5deg); + } + + 43.5% { + -webkit-transform: translateX(2px) rotateY(3deg); + transform: translateX(2px) rotateY(3deg); + } + + 50% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} + +.headShake { + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + -webkit-animation-name: headShake; + animation-name: headShake; +} + +@-webkit-keyframes swing { + 20% { + -webkit-transform: rotate3d(0, 0, 1, 15deg); + transform: rotate3d(0, 0, 1, 15deg); + } + + 40% { + -webkit-transform: rotate3d(0, 0, 1, -10deg); + transform: rotate3d(0, 0, 1, -10deg); + } + + 60% { + -webkit-transform: rotate3d(0, 0, 1, 5deg); + transform: rotate3d(0, 0, 1, 5deg); + } + + 80% { + -webkit-transform: rotate3d(0, 0, 1, -5deg); + transform: rotate3d(0, 0, 1, -5deg); + } + + to { + -webkit-transform: rotate3d(0, 0, 1, 0deg); + transform: rotate3d(0, 0, 1, 0deg); + } +} + +@keyframes swing { + 20% { + -webkit-transform: rotate3d(0, 0, 1, 15deg); + transform: rotate3d(0, 0, 1, 15deg); + } + + 40% { + -webkit-transform: rotate3d(0, 0, 1, -10deg); + transform: rotate3d(0, 0, 1, -10deg); + } + + 60% { + -webkit-transform: rotate3d(0, 0, 1, 5deg); + transform: rotate3d(0, 0, 1, 5deg); + } + + 80% { + -webkit-transform: rotate3d(0, 0, 1, -5deg); + transform: rotate3d(0, 0, 1, -5deg); + } + + to { + -webkit-transform: rotate3d(0, 0, 1, 0deg); + transform: rotate3d(0, 0, 1, 0deg); + } +} + +.swing { + -webkit-transform-origin: top center; + transform-origin: top center; + -webkit-animation-name: swing; + animation-name: swing; +} + +@-webkit-keyframes tada { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 10%, + 20% { + -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + } + + 30%, + 50%, + 70%, + 90% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + } + + 40%, + 60%, + 80% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +@keyframes tada { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 10%, + 20% { + -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + } + + 30%, + 50%, + 70%, + 90% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + } + + 40%, + 60%, + 80% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +.tada { + -webkit-animation-name: tada; + animation-name: tada; +} + +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ + +@-webkit-keyframes wobble { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 15% { + -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + } + + 30% { + -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + } + + 45% { + -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + } + + 60% { + -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + } + + 75% { + -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes wobble { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 15% { + -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + } + + 30% { + -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + } + + 45% { + -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + } + + 60% { + -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + } + + 75% { + -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.wobble { + -webkit-animation-name: wobble; + animation-name: wobble; +} + +@-webkit-keyframes jello { + from, + 11.1%, + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 22.2% { + -webkit-transform: skewX(-12.5deg) skewY(-12.5deg); + transform: skewX(-12.5deg) skewY(-12.5deg); + } + + 33.3% { + -webkit-transform: skewX(6.25deg) skewY(6.25deg); + transform: skewX(6.25deg) skewY(6.25deg); + } + + 44.4% { + -webkit-transform: skewX(-3.125deg) skewY(-3.125deg); + transform: skewX(-3.125deg) skewY(-3.125deg); + } + + 55.5% { + -webkit-transform: skewX(1.5625deg) skewY(1.5625deg); + transform: skewX(1.5625deg) skewY(1.5625deg); + } + + 66.6% { + -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg); + transform: skewX(-0.78125deg) skewY(-0.78125deg); + } + + 77.7% { + -webkit-transform: skewX(0.390625deg) skewY(0.390625deg); + transform: skewX(0.390625deg) skewY(0.390625deg); + } + + 88.8% { + -webkit-transform: skewX(-0.1953125deg) skewY(-0.1953125deg); + transform: skewX(-0.1953125deg) skewY(-0.1953125deg); + } +} + +@keyframes jello { + from, + 11.1%, + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 22.2% { + -webkit-transform: skewX(-12.5deg) skewY(-12.5deg); + transform: skewX(-12.5deg) skewY(-12.5deg); + } + + 33.3% { + -webkit-transform: skewX(6.25deg) skewY(6.25deg); + transform: skewX(6.25deg) skewY(6.25deg); + } + + 44.4% { + -webkit-transform: skewX(-3.125deg) skewY(-3.125deg); + transform: skewX(-3.125deg) skewY(-3.125deg); + } + + 55.5% { + -webkit-transform: skewX(1.5625deg) skewY(1.5625deg); + transform: skewX(1.5625deg) skewY(1.5625deg); + } + + 66.6% { + -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg); + transform: skewX(-0.78125deg) skewY(-0.78125deg); + } + + 77.7% { + -webkit-transform: skewX(0.390625deg) skewY(0.390625deg); + transform: skewX(0.390625deg) skewY(0.390625deg); + } + + 88.8% { + -webkit-transform: skewX(-0.1953125deg) skewY(-0.1953125deg); + transform: skewX(-0.1953125deg) skewY(-0.1953125deg); + } +} + +.jello { + -webkit-animation-name: jello; + animation-name: jello; + -webkit-transform-origin: center; + transform-origin: center; +} + +@-webkit-keyframes heartBeat { + 0% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 14% { + -webkit-transform: scale(1.3); + transform: scale(1.3); + } + + 28% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 42% { + -webkit-transform: scale(1.3); + transform: scale(1.3); + } + + 70% { + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@keyframes heartBeat { + 0% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 14% { + -webkit-transform: scale(1.3); + transform: scale(1.3); + } + + 28% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 42% { + -webkit-transform: scale(1.3); + transform: scale(1.3); + } + + 70% { + -webkit-transform: scale(1); + transform: scale(1); + } +} + +.heartBeat { + -webkit-animation-name: heartBeat; + animation-name: heartBeat; + -webkit-animation-duration: 1.3s; + animation-duration: 1.3s; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; +} + +@-webkit-keyframes bounceIn { + from, + 20%, + 40%, + 60%, + 80%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + + 0% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + + 20% { + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + + 40% { + -webkit-transform: scale3d(0.9, 0.9, 0.9); + transform: scale3d(0.9, 0.9, 0.9); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(1.03, 1.03, 1.03); + transform: scale3d(1.03, 1.03, 1.03); + } + + 80% { + -webkit-transform: scale3d(0.97, 0.97, 0.97); + transform: scale3d(0.97, 0.97, 0.97); + } + + to { + opacity: 1; + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +@keyframes bounceIn { + from, + 20%, + 40%, + 60%, + 80%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + + 0% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + + 20% { + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + + 40% { + -webkit-transform: scale3d(0.9, 0.9, 0.9); + transform: scale3d(0.9, 0.9, 0.9); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(1.03, 1.03, 1.03); + transform: scale3d(1.03, 1.03, 1.03); + } + + 80% { + -webkit-transform: scale3d(0.97, 0.97, 0.97); + transform: scale3d(0.97, 0.97, 0.97); + } + + to { + opacity: 1; + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +.bounceIn { + -webkit-animation-duration: 0.75s; + animation-duration: 0.75s; + -webkit-animation-name: bounceIn; + animation-name: bounceIn; +} + +@-webkit-keyframes bounceInDown { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -3000px, 0); + transform: translate3d(0, -3000px, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(0, 25px, 0); + transform: translate3d(0, 25px, 0); + } + + 75% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + + 90% { + -webkit-transform: translate3d(0, 5px, 0); + transform: translate3d(0, 5px, 0); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes bounceInDown { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -3000px, 0); + transform: translate3d(0, -3000px, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(0, 25px, 0); + transform: translate3d(0, 25px, 0); + } + + 75% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + + 90% { + -webkit-transform: translate3d(0, 5px, 0); + transform: translate3d(0, 5px, 0); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.bounceInDown { + -webkit-animation-name: bounceInDown; + animation-name: bounceInDown; +} + +@-webkit-keyframes bounceInLeft { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + + 0% { + opacity: 0; + -webkit-transform: translate3d(-3000px, 0, 0); + transform: translate3d(-3000px, 0, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(25px, 0, 0); + transform: translate3d(25px, 0, 0); + } + + 75% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + + 90% { + -webkit-transform: translate3d(5px, 0, 0); + transform: translate3d(5px, 0, 0); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes bounceInLeft { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + + 0% { + opacity: 0; + -webkit-transform: translate3d(-3000px, 0, 0); + transform: translate3d(-3000px, 0, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(25px, 0, 0); + transform: translate3d(25px, 0, 0); + } + + 75% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + + 90% { + -webkit-transform: translate3d(5px, 0, 0); + transform: translate3d(5px, 0, 0); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.bounceInLeft { + -webkit-animation-name: bounceInLeft; + animation-name: bounceInLeft; +} + +@-webkit-keyframes bounceInRight { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + + from { + opacity: 0; + -webkit-transform: translate3d(3000px, 0, 0); + transform: translate3d(3000px, 0, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(-25px, 0, 0); + transform: translate3d(-25px, 0, 0); + } + + 75% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } + + 90% { + -webkit-transform: translate3d(-5px, 0, 0); + transform: translate3d(-5px, 0, 0); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes bounceInRight { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + + from { + opacity: 0; + -webkit-transform: translate3d(3000px, 0, 0); + transform: translate3d(3000px, 0, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(-25px, 0, 0); + transform: translate3d(-25px, 0, 0); + } + + 75% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } + + 90% { + -webkit-transform: translate3d(-5px, 0, 0); + transform: translate3d(-5px, 0, 0); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.bounceInRight { + -webkit-animation-name: bounceInRight; + animation-name: bounceInRight; +} + +@-webkit-keyframes bounceInUp { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + + from { + opacity: 0; + -webkit-transform: translate3d(0, 3000px, 0); + transform: translate3d(0, 3000px, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + + 75% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + + 90% { + -webkit-transform: translate3d(0, -5px, 0); + transform: translate3d(0, -5px, 0); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes bounceInUp { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + + from { + opacity: 0; + -webkit-transform: translate3d(0, 3000px, 0); + transform: translate3d(0, 3000px, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + + 75% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + + 90% { + -webkit-transform: translate3d(0, -5px, 0); + transform: translate3d(0, -5px, 0); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.bounceInUp { + -webkit-animation-name: bounceInUp; + animation-name: bounceInUp; +} + +@-webkit-keyframes bounceOut { + 20% { + -webkit-transform: scale3d(0.9, 0.9, 0.9); + transform: scale3d(0.9, 0.9, 0.9); + } + + 50%, + 55% { + opacity: 1; + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + + to { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } +} + +@keyframes bounceOut { + 20% { + -webkit-transform: scale3d(0.9, 0.9, 0.9); + transform: scale3d(0.9, 0.9, 0.9); + } + + 50%, + 55% { + opacity: 1; + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + + to { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } +} + +.bounceOut { + -webkit-animation-duration: 0.75s; + animation-duration: 0.75s; + -webkit-animation-name: bounceOut; + animation-name: bounceOut; +} + +@-webkit-keyframes bounceOutDown { + 20% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} + +@keyframes bounceOutDown { + 20% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} + +.bounceOutDown { + -webkit-animation-name: bounceOutDown; + animation-name: bounceOutDown; +} + +@-webkit-keyframes bounceOutLeft { + 20% { + opacity: 1; + -webkit-transform: translate3d(20px, 0, 0); + transform: translate3d(20px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} + +@keyframes bounceOutLeft { + 20% { + opacity: 1; + -webkit-transform: translate3d(20px, 0, 0); + transform: translate3d(20px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} + +.bounceOutLeft { + -webkit-animation-name: bounceOutLeft; + animation-name: bounceOutLeft; +} + +@-webkit-keyframes bounceOutRight { + 20% { + opacity: 1; + -webkit-transform: translate3d(-20px, 0, 0); + transform: translate3d(-20px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} + +@keyframes bounceOutRight { + 20% { + opacity: 1; + -webkit-transform: translate3d(-20px, 0, 0); + transform: translate3d(-20px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} + +.bounceOutRight { + -webkit-animation-name: bounceOutRight; + animation-name: bounceOutRight; +} + +@-webkit-keyframes bounceOutUp { + 20% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, 20px, 0); + transform: translate3d(0, 20px, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} + +@keyframes bounceOutUp { + 20% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, 20px, 0); + transform: translate3d(0, 20px, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} + +.bounceOutUp { + -webkit-animation-name: bounceOutUp; + animation-name: bounceOutUp; +} + +@-webkit-keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.fadeIn { + -webkit-animation-name: fadeIn; + animation-name: fadeIn; +} + +@-webkit-keyframes fadeInDown { + from { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes fadeInDown { + from { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.fadeInDown { + -webkit-animation-name: fadeInDown; + animation-name: fadeInDown; +} + +@-webkit-keyframes fadeInDownBig { + from { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes fadeInDownBig { + from { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.fadeInDownBig { + -webkit-animation-name: fadeInDownBig; + animation-name: fadeInDownBig; +} + +@-webkit-keyframes fadeInLeft { + from { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes fadeInLeft { + from { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.fadeInLeft { + -webkit-animation-name: fadeInLeft; + animation-name: fadeInLeft; +} + +@-webkit-keyframes fadeInLeftBig { + from { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes fadeInLeftBig { + from { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.fadeInLeftBig { + -webkit-animation-name: fadeInLeftBig; + animation-name: fadeInLeftBig; +} + +@-webkit-keyframes fadeInRight { + from { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes fadeInRight { + from { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.fadeInRight { + -webkit-animation-name: fadeInRight; + animation-name: fadeInRight; +} + +@-webkit-keyframes fadeInRightBig { + from { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes fadeInRightBig { + from { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.fadeInRightBig { + -webkit-animation-name: fadeInRightBig; + animation-name: fadeInRightBig; +} + +@-webkit-keyframes fadeInUp { + from { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.fadeInUp { + -webkit-animation-name: fadeInUp; + animation-name: fadeInUp; +} + +@-webkit-keyframes fadeInUpBig { + from { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes fadeInUpBig { + from { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.fadeInUpBig { + -webkit-animation-name: fadeInUpBig; + animation-name: fadeInUpBig; +} + +@-webkit-keyframes fadeOut { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +.fadeOut { + -webkit-animation-name: fadeOut; + animation-name: fadeOut; +} + +@-webkit-keyframes fadeOutDown { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} + +@keyframes fadeOutDown { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} + +.fadeOutDown { + -webkit-animation-name: fadeOutDown; + animation-name: fadeOutDown; +} + +@-webkit-keyframes fadeOutDownBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} + +@keyframes fadeOutDownBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} + +.fadeOutDownBig { + -webkit-animation-name: fadeOutDownBig; + animation-name: fadeOutDownBig; +} + +@-webkit-keyframes fadeOutLeft { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} + +@keyframes fadeOutLeft { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} + +.fadeOutLeft { + -webkit-animation-name: fadeOutLeft; + animation-name: fadeOutLeft; +} + +@-webkit-keyframes fadeOutLeftBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} + +@keyframes fadeOutLeftBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} + +.fadeOutLeftBig { + -webkit-animation-name: fadeOutLeftBig; + animation-name: fadeOutLeftBig; +} + +@-webkit-keyframes fadeOutRight { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} + +@keyframes fadeOutRight { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} + +.fadeOutRight { + -webkit-animation-name: fadeOutRight; + animation-name: fadeOutRight; +} + +@-webkit-keyframes fadeOutRightBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} + +@keyframes fadeOutRightBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} + +.fadeOutRightBig { + -webkit-animation-name: fadeOutRightBig; + animation-name: fadeOutRightBig; +} + +@-webkit-keyframes fadeOutUp { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} + +@keyframes fadeOutUp { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} + +.fadeOutUp { + -webkit-animation-name: fadeOutUp; + animation-name: fadeOutUp; +} + +@-webkit-keyframes fadeOutUpBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} + +@keyframes fadeOutUpBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} + +.fadeOutUpBig { + -webkit-animation-name: fadeOutUpBig; + animation-name: fadeOutUpBig; +} + +@-webkit-keyframes flip { + from { + -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, -360deg); + transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, -360deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + + 40% { + -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + + 50% { + -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + 80% { + -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95) translate3d(0, 0, 0) rotate3d(0, 1, 0, 0deg); + transform: perspective(400px) scale3d(0.95, 0.95, 0.95) translate3d(0, 0, 0) rotate3d(0, 1, 0, 0deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + to { + -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, 0deg); + transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, 0deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } +} + +@keyframes flip { + from { + -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, -360deg); + transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, -360deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + + 40% { + -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + + 50% { + -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + 80% { + -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95) translate3d(0, 0, 0) rotate3d(0, 1, 0, 0deg); + transform: perspective(400px) scale3d(0.95, 0.95, 0.95) translate3d(0, 0, 0) rotate3d(0, 1, 0, 0deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + to { + -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, 0deg); + transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, 0deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } +} + +.animated.flip { + -webkit-backface-visibility: visible; + backface-visibility: visible; + -webkit-animation-name: flip; + animation-name: flip; +} + +@-webkit-keyframes flipInX { + from { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + opacity: 0; + } + + 40% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + 60% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + + 80% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + + to { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} + +@keyframes flipInX { + from { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + opacity: 0; + } + + 40% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + 60% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + + 80% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + + to { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} + +.flipInX { + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; + -webkit-animation-name: flipInX; + animation-name: flipInX; +} + +@-webkit-keyframes flipInY { + from { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + opacity: 0; + } + + 40% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + 60% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + opacity: 1; + } + + 80% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + } + + to { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} + +@keyframes flipInY { + from { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + opacity: 0; + } + + 40% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + 60% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + opacity: 1; + } + + 80% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + } + + to { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} + +.flipInY { + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; + -webkit-animation-name: flipInY; + animation-name: flipInY; +} + +@-webkit-keyframes flipOutX { + from { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + + 30% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + opacity: 1; + } + + to { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + opacity: 0; + } +} + +@keyframes flipOutX { + from { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + + 30% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + opacity: 1; + } + + to { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + opacity: 0; + } +} + +.flipOutX { + -webkit-animation-duration: 0.75s; + animation-duration: 0.75s; + -webkit-animation-name: flipOutX; + animation-name: flipOutX; + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; +} + +@-webkit-keyframes flipOutY { + from { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + + 30% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + opacity: 1; + } + + to { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + opacity: 0; + } +} + +@keyframes flipOutY { + from { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + + 30% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + opacity: 1; + } + + to { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + opacity: 0; + } +} + +.flipOutY { + -webkit-animation-duration: 0.75s; + animation-duration: 0.75s; + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; + -webkit-animation-name: flipOutY; + animation-name: flipOutY; +} + +@-webkit-keyframes lightSpeedIn { + from { + -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg); + transform: translate3d(100%, 0, 0) skewX(-30deg); + opacity: 0; + } + + 60% { + -webkit-transform: skewX(20deg); + transform: skewX(20deg); + opacity: 1; + } + + 80% { + -webkit-transform: skewX(-5deg); + transform: skewX(-5deg); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes lightSpeedIn { + from { + -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg); + transform: translate3d(100%, 0, 0) skewX(-30deg); + opacity: 0; + } + + 60% { + -webkit-transform: skewX(20deg); + transform: skewX(20deg); + opacity: 1; + } + + 80% { + -webkit-transform: skewX(-5deg); + transform: skewX(-5deg); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.lightSpeedIn { + -webkit-animation-name: lightSpeedIn; + animation-name: lightSpeedIn; + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; +} + +@-webkit-keyframes lightSpeedOut { + from { + opacity: 1; + } + + to { + -webkit-transform: translate3d(100%, 0, 0) skewX(30deg); + transform: translate3d(100%, 0, 0) skewX(30deg); + opacity: 0; + } +} + +@keyframes lightSpeedOut { + from { + opacity: 1; + } + + to { + -webkit-transform: translate3d(100%, 0, 0) skewX(30deg); + transform: translate3d(100%, 0, 0) skewX(30deg); + opacity: 0; + } +} + +.lightSpeedOut { + -webkit-animation-name: lightSpeedOut; + animation-name: lightSpeedOut; + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; +} + +@-webkit-keyframes rotateIn { + from { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, -200deg); + transform: rotate3d(0, 0, 1, -200deg); + opacity: 0; + } + + to { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + opacity: 1; + } +} + +@keyframes rotateIn { + from { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, -200deg); + transform: rotate3d(0, 0, 1, -200deg); + opacity: 0; + } + + to { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + opacity: 1; + } +} + +.rotateIn { + -webkit-animation-name: rotateIn; + animation-name: rotateIn; +} + +@-webkit-keyframes rotateInDownLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + opacity: 1; + } +} + +@keyframes rotateInDownLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + opacity: 1; + } +} + +.rotateInDownLeft { + -webkit-animation-name: rotateInDownLeft; + animation-name: rotateInDownLeft; +} + +@-webkit-keyframes rotateInDownRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + opacity: 1; + } +} + +@keyframes rotateInDownRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + opacity: 1; + } +} + +.rotateInDownRight { + -webkit-animation-name: rotateInDownRight; + animation-name: rotateInDownRight; +} + +@-webkit-keyframes rotateInUpLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + opacity: 1; + } +} + +@keyframes rotateInUpLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + opacity: 1; + } +} + +.rotateInUpLeft { + -webkit-animation-name: rotateInUpLeft; + animation-name: rotateInUpLeft; +} + +@-webkit-keyframes rotateInUpRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -90deg); + transform: rotate3d(0, 0, 1, -90deg); + opacity: 0; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + opacity: 1; + } +} + +@keyframes rotateInUpRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -90deg); + transform: rotate3d(0, 0, 1, -90deg); + opacity: 0; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + opacity: 1; + } +} + +.rotateInUpRight { + -webkit-animation-name: rotateInUpRight; + animation-name: rotateInUpRight; +} + +@-webkit-keyframes rotateOut { + from { + -webkit-transform-origin: center; + transform-origin: center; + opacity: 1; + } + + to { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, 200deg); + transform: rotate3d(0, 0, 1, 200deg); + opacity: 0; + } +} + +@keyframes rotateOut { + from { + -webkit-transform-origin: center; + transform-origin: center; + opacity: 1; + } + + to { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, 200deg); + transform: rotate3d(0, 0, 1, 200deg); + opacity: 0; + } +} + +.rotateOut { + -webkit-animation-name: rotateOut; + animation-name: rotateOut; +} + +@-webkit-keyframes rotateOutDownLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } +} + +@keyframes rotateOutDownLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } +} + +.rotateOutDownLeft { + -webkit-animation-name: rotateOutDownLeft; + animation-name: rotateOutDownLeft; +} + +@-webkit-keyframes rotateOutDownRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} + +@keyframes rotateOutDownRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} + +.rotateOutDownRight { + -webkit-animation-name: rotateOutDownRight; + animation-name: rotateOutDownRight; +} + +@-webkit-keyframes rotateOutUpLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} + +@keyframes rotateOutUpLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} + +.rotateOutUpLeft { + -webkit-animation-name: rotateOutUpLeft; + animation-name: rotateOutUpLeft; +} + +@-webkit-keyframes rotateOutUpRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 90deg); + transform: rotate3d(0, 0, 1, 90deg); + opacity: 0; + } +} + +@keyframes rotateOutUpRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 90deg); + transform: rotate3d(0, 0, 1, 90deg); + opacity: 0; + } +} + +.rotateOutUpRight { + -webkit-animation-name: rotateOutUpRight; + animation-name: rotateOutUpRight; +} + +@-webkit-keyframes hinge { + 0% { + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + + 20%, + 60% { + -webkit-transform: rotate3d(0, 0, 1, 80deg); + transform: rotate3d(0, 0, 1, 80deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + + 40%, + 80% { + -webkit-transform: rotate3d(0, 0, 1, 60deg); + transform: rotate3d(0, 0, 1, 60deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + opacity: 1; + } + + to { + -webkit-transform: translate3d(0, 700px, 0); + transform: translate3d(0, 700px, 0); + opacity: 0; + } +} + +@keyframes hinge { + 0% { + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + + 20%, + 60% { + -webkit-transform: rotate3d(0, 0, 1, 80deg); + transform: rotate3d(0, 0, 1, 80deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + + 40%, + 80% { + -webkit-transform: rotate3d(0, 0, 1, 60deg); + transform: rotate3d(0, 0, 1, 60deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + opacity: 1; + } + + to { + -webkit-transform: translate3d(0, 700px, 0); + transform: translate3d(0, 700px, 0); + opacity: 0; + } +} + +.hinge { + -webkit-animation-duration: 2s; + animation-duration: 2s; + -webkit-animation-name: hinge; + animation-name: hinge; +} + +@-webkit-keyframes jackInTheBox { + from { + opacity: 0; + -webkit-transform: scale(0.1) rotate(30deg); + transform: scale(0.1) rotate(30deg); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + } + + 50% { + -webkit-transform: rotate(-10deg); + transform: rotate(-10deg); + } + + 70% { + -webkit-transform: rotate(3deg); + transform: rotate(3deg); + } + + to { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@keyframes jackInTheBox { + from { + opacity: 0; + -webkit-transform: scale(0.1) rotate(30deg); + transform: scale(0.1) rotate(30deg); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + } + + 50% { + -webkit-transform: rotate(-10deg); + transform: rotate(-10deg); + } + + 70% { + -webkit-transform: rotate(3deg); + transform: rotate(3deg); + } + + to { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } +} + +.jackInTheBox { + -webkit-animation-name: jackInTheBox; + animation-name: jackInTheBox; +} + +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ + +@-webkit-keyframes rollIn { + from { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes rollIn { + from { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + } + + to { + opacity: 1; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.rollIn { + -webkit-animation-name: rollIn; + animation-name: rollIn; +} + +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ + +@-webkit-keyframes rollOut { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + } +} + +@keyframes rollOut { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + } +} + +.rollOut { + -webkit-animation-name: rollOut; + animation-name: rollOut; +} + +@-webkit-keyframes zoomIn { + from { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + + 50% { + opacity: 1; + } +} + +@keyframes zoomIn { + from { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + + 50% { + opacity: 1; + } +} + +.zoomIn { + -webkit-animation-name: zoomIn; + animation-name: zoomIn; +} + +@-webkit-keyframes zoomInDown { + from { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} + +@keyframes zoomInDown { + from { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} + +.zoomInDown { + -webkit-animation-name: zoomInDown; + animation-name: zoomInDown; +} + +@-webkit-keyframes zoomInLeft { + from { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} + +@keyframes zoomInLeft { + from { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} + +.zoomInLeft { + -webkit-animation-name: zoomInLeft; + animation-name: zoomInLeft; +} + +@-webkit-keyframes zoomInRight { + from { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} + +@keyframes zoomInRight { + from { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} + +.zoomInRight { + -webkit-animation-name: zoomInRight; + animation-name: zoomInRight; +} + +@-webkit-keyframes zoomInUp { + from { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} + +@keyframes zoomInUp { + from { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} + +.zoomInUp { + -webkit-animation-name: zoomInUp; + animation-name: zoomInUp; +} + +@-webkit-keyframes zoomOut { + from { + opacity: 1; + } + + 50% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + + to { + opacity: 0; + } +} + +@keyframes zoomOut { + from { + opacity: 1; + } + + 50% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + + to { + opacity: 0; + } +} + +.zoomOut { + -webkit-animation-name: zoomOut; + animation-name: zoomOut; +} + +@-webkit-keyframes zoomOutDown { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + to { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} + +@keyframes zoomOutDown { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + to { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} + +.zoomOutDown { + -webkit-animation-name: zoomOutDown; + animation-name: zoomOutDown; +} + +@-webkit-keyframes zoomOutLeft { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0); + transform: scale(0.1) translate3d(-2000px, 0, 0); + -webkit-transform-origin: left center; + transform-origin: left center; + } +} + +@keyframes zoomOutLeft { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0); + transform: scale(0.1) translate3d(-2000px, 0, 0); + -webkit-transform-origin: left center; + transform-origin: left center; + } +} + +.zoomOutLeft { + -webkit-animation-name: zoomOutLeft; + animation-name: zoomOutLeft; +} + +@-webkit-keyframes zoomOutRight { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: scale(0.1) translate3d(2000px, 0, 0); + transform: scale(0.1) translate3d(2000px, 0, 0); + -webkit-transform-origin: right center; + transform-origin: right center; + } +} + +@keyframes zoomOutRight { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: scale(0.1) translate3d(2000px, 0, 0); + transform: scale(0.1) translate3d(2000px, 0, 0); + -webkit-transform-origin: right center; + transform-origin: right center; + } +} + +.zoomOutRight { + -webkit-animation-name: zoomOutRight; + animation-name: zoomOutRight; +} + +@-webkit-keyframes zoomOutUp { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + to { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} + +@keyframes zoomOutUp { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + + to { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} + +.zoomOutUp { + -webkit-animation-name: zoomOutUp; + animation-name: zoomOutUp; +} + +@-webkit-keyframes slideInDown { + from { + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes slideInDown { + from { + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.slideInDown { + -webkit-animation-name: slideInDown; + animation-name: slideInDown; +} + +@-webkit-keyframes slideInLeft { + from { + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes slideInLeft { + from { + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.slideInLeft { + -webkit-animation-name: slideInLeft; + animation-name: slideInLeft; +} + +@-webkit-keyframes slideInRight { + from { + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes slideInRight { + from { + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.slideInRight { + -webkit-animation-name: slideInRight; + animation-name: slideInRight; +} + +@-webkit-keyframes slideInUp { + from { + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes slideInUp { + from { + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.slideInUp { + -webkit-animation-name: slideInUp; + animation-name: slideInUp; +} + +@-webkit-keyframes slideOutDown { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} + +@keyframes slideOutDown { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} + +.slideOutDown { + -webkit-animation-name: slideOutDown; + animation-name: slideOutDown; +} + +@-webkit-keyframes slideOutLeft { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} + +@keyframes slideOutLeft { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} + +.slideOutLeft { + -webkit-animation-name: slideOutLeft; + animation-name: slideOutLeft; +} + +@-webkit-keyframes slideOutRight { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} + +@keyframes slideOutRight { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} + +.slideOutRight { + -webkit-animation-name: slideOutRight; + animation-name: slideOutRight; +} + +@-webkit-keyframes slideOutUp { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} + +@keyframes slideOutUp { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} + +.slideOutUp { + -webkit-animation-name: slideOutUp; + animation-name: slideOutUp; +} + +.animated { + -webkit-animation-duration: 1s; + animation-duration: 1s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} + +.animated.infinite { + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; +} + +.animated.delay-1s { + -webkit-animation-delay: 1s; + animation-delay: 1s; +} + +.animated.delay-2s { + -webkit-animation-delay: 2s; + animation-delay: 2s; +} + +.animated.delay-3s { + -webkit-animation-delay: 3s; + animation-delay: 3s; +} + +.animated.delay-4s { + -webkit-animation-delay: 4s; + animation-delay: 4s; +} + +.animated.delay-5s { + -webkit-animation-delay: 5s; + animation-delay: 5s; +} + +.animated.fast { + -webkit-animation-duration: 800ms; + animation-duration: 800ms; +} + +.animated.faster { + -webkit-animation-duration: 500ms; + animation-duration: 500ms; +} + +.animated.slow { + -webkit-animation-duration: 2s; + animation-duration: 2s; +} + +.animated.slower { + -webkit-animation-duration: 3s; + animation-duration: 3s; +} + +@media (print), (prefers-reduced-motion: reduce) { + .animated { + -webkit-animation-duration: 1ms !important; + animation-duration: 1ms !important; + -webkit-transition-duration: 1ms !important; + transition-duration: 1ms !important; + -webkit-animation-iteration-count: 1 !important; + animation-iteration-count: 1 !important; + } +} \ No newline at end of file diff --git a/app/templates/sources/style/common.css b/app/templates/sources/style/common.css new file mode 100644 index 0000000..977ee66 --- /dev/null +++ b/app/templates/sources/style/common.css @@ -0,0 +1,77 @@ +@import 'page.css'; + +/* @font-face { + font-family: 'PingFang'; + src: url('./fonts/PingFang.ttc'); +} */ +/* 通用样式 */ +* { + padding: 0; + margin: 0; + color: rgba(255, 255, 255, 255); + text-decoration: none; + box-sizing: border-box; + font-family: 'sans-serif'; + -webkit-touch-callout: none; /*系统默认菜单被禁用*/ + -webkit-user-select: none; /*webkit浏览器*/ + -khtml-user-select: none; /*早期浏览器*/ + -moz-user-select: none; /*火狐*/ + -ms-user-select: none; /*IE10*/ + user-select: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-tap-highlight-color: transparent; + -moz-touch-callout: none; + -ms-touch-callout: none; + touch-callout: none; +} + +body, +html { + height: 100%; +} + +.container { + display: flex; + justify-content: space-between; + align-items: center; + overflow-y: hidden; + max-height: 100vh; + flex-wrap: nowrap; +} + +.box { + min-width: 100%; + min-height: 100vh; + font-size: 16px; + font-family: 'PingFang'; + background: rgb(0, 0, 0); + flex: 1; + overflow-y: hidden; + align-self: flex-start; +} + +.box-row { + margin-top: 25px; + height: 22px; + /* background: red; */ + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; +} + +.span1 { + width: 90%; + text-indent: 1em; +} + +.span3 { + width: 90%; + text-indent: 1em; +} + +img { + width: 8px; + height: 15px; + margin-right: 16px; +} diff --git a/app/templates/sources/style/page.css b/app/templates/sources/style/page.css new file mode 100644 index 0000000..c240cfa --- /dev/null +++ b/app/templates/sources/style/page.css @@ -0,0 +1,44 @@ +/* @font-face { + font-family: 'PingFang'; + src: url('../fonts/PingFang.ttc'); +} */ +/* 通用样式 */ +* { + padding: 0; + margin: 0; +} + +.page { + position: absolute; + width: 100%; + z-index: 100; + left: 0; + top: 0; + background: rgb(0, 0, 0); + display: none; + padding: 0 16px; +} + +h3 { + font-size: 16px; + color: rgba(255, 255, 255, 1); + font-family: 'PingFang'; + margin-bottom: 15px; + height: 22px; +} + +p { + font-size: 14px; + color: rgba(255, 255, 255, 0.5); + font-family: 'PingFang'; + margin-bottom: 10px; + line-height: 20px; +} + +.p1 { + margin-top: 29px; +} + +.p2 { + margin-bottom: 30px; +} diff --git a/app/templates/sources/style/reset.css b/app/templates/sources/style/reset.css new file mode 100644 index 0000000..123b567 --- /dev/null +++ b/app/templates/sources/style/reset.css @@ -0,0 +1,136 @@ +/* http://meyerweb.com/eric/tools/css/reset/ */ +/* v1.0 | 20080212 */ + +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +font, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-size: 100%; + vertical-align: baseline; + background: transparent; +} + +body { + line-height: 1; +} + +ol, +ul { + list-style: none; +} + +blockquote, +q { + quotes: none; +} + +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ''; + content: none; +} + +/* remember to define focus styles! */ +:focus { + outline: 0; +} + +/* remember to highlight inserts somehow! */ +ins { + text-decoration: none; +} + +del { + text-decoration: line-through; +} + +/* tables still need 'cellspacing="0"' in the markup */ +table { + border-collapse: collapse; + border-spacing: 0; +} + +* { + box-sizing: border-box; + font-family: 'sans-serif'; + -webkit-touch-callout: none; /*系统默认菜单被禁用*/ + -webkit-user-select: none; /*webkit浏览器*/ + -khtml-user-select: none; /*早期浏览器*/ + -moz-user-select: none; /*火狐*/ + -ms-user-select: none; /*IE10*/ + user-select: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-tap-highlight-color: transparent; +} + +input, +textarea { + -webkit-user-select: auto; /*webkit浏览器*/ + margin: 0px; + padding: 0px; + outline: none; +} diff --git a/app/templates/sources/util/rem.js b/app/templates/sources/util/rem.js new file mode 100644 index 0000000..d908c62 --- /dev/null +++ b/app/templates/sources/util/rem.js @@ -0,0 +1,39 @@ +/** + * 动态设置rem + */ + +var BASE_SIZE = 20; // 基本字体大小 +var MAX_SIZE = 50; // 最大字体大小 +var DESIGN_WIDTH = 720; // iphone 6 +var MIN_DESIGN_WIDTH = 320; +var htmlEl = document.querySelector('html'); +var fontSize = 20; + +var setFontSize = function (size) { + htmlEl.style.fontSize = size + 'px'; + fontSize = size; +}; + +var calc = function () { + var device_width = document.body.clientWidth || window.innerWidth; + var scale = device_width / DESIGN_WIDTH; + var distSize = scale * BASE_SIZE; + if (distSize > MAX_SIZE) { + distSize = MAX_SIZE; + } + if (device_width < MIN_DESIGN_WIDTH) return; + setFontSize(Math.round(distSize)); +}; + +window.addEventListener('resize', calc); + +document.addEventListener('DOMContentLoaded', calc); + +window.rem = function (size) { + return size / BASE_SIZE + 'rem'; +}; + +window.renderPx = function (size) { + return Math.round((size / BASE_SIZE) * fontSize); +}; + diff --git a/app/templates/sources/util/tools.js b/app/templates/sources/util/tools.js new file mode 100644 index 0000000..fbe3242 --- /dev/null +++ b/app/templates/sources/util/tools.js @@ -0,0 +1,31 @@ +var os = (function () { + var ua = navigator.userAgent, + isWindowsPhone = /(?:Windows Phone)/.test(ua), + isSymbian = /(?:SymbianOS)/.test(ua) || isWindowsPhone, + isAndroid = /(?:Android)/.test(ua), + isFireFox = /(?:Firefox)/.test(ua), + isChrome = /(?:Chrome|CriOS)/.test(ua), + isTablet = /(?:iPad|PlayBook)/.test(ua) || (isAndroid && !/(?:Mobile)/.test(ua)) || (isFireFox && /(?:Tablet)/.test(ua)), + isPhone = /(?:iPhone)/.test(ua) && !isTablet, + isWx = /MicroMessenger/.test(ua), + isPc = !isPhone && !isAndroid && !isSymbian; + return { + isTablet: isTablet, + isPhone: isPhone, + isAndroid: isAndroid, + isPc: isPc, + isWx: isWx + }; +})(); +var parseQuery = function (url) { + let query = {}; + if (url.indexOf("?") !== -1) { + url.split("?")[1] + .split("&") + .forEach(entry => { + let _query = entry.split("="); + query[_query[0]] = _query[1]; + }); + } + return query; +}; \ No newline at end of file diff --git a/common/PromotionCodeUtil.go b/common/PromotionCodeUtil.go new file mode 100644 index 0000000..4f0a66f --- /dev/null +++ b/common/PromotionCodeUtil.go @@ -0,0 +1,22 @@ +package common + +import "math/rand" + +/** + * 生成邀请码 + */ +var ( + //所有的字符 + allChars = []string{"2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"} + //默认长度 + defaultLen = 6 +) + +func InvitePromotionCodeGenera() string { + result := "" + for i := 0; i < defaultLen; i++ { + index := rand.Intn(len(allChars)) + result += allChars[index] + } + return result +} diff --git a/common/aiMate/aiMate.go b/common/aiMate/aiMate.go new file mode 100644 index 0000000..672c382 --- /dev/null +++ b/common/aiMate/aiMate.go @@ -0,0 +1,258 @@ +package aiMate + +import ( + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + + "91porn-server/common/httputil" + "91porn-server/common/log" +) + +const ( + req_doman = "https://www.gpt4novel.com" //请求域名 + access_doman = "https://www.saibolaopo.com/tg-login" //访问域名 + get_token_url = "/api/dzmm/get-jwt-for-organization-subaccount" //获取用户登陆token + transfer_points_url = "/api/dzmm/transfer-all-user-credit-to-organization" //转出用户所有积分 + get_points_url = "/api/dzmm/get-organization-user-balance" //获取用户积分 + get_transactions_url = "/api/dzmm/get-user-credit-transactions" //获取用户消费记录 + access_key = "c1c5f394-1a6a-4834-8ee8-cb2fb6325969" //密钥 + organization_id = "65443ba4-480a-4c62-8276-f5380bccc739" //组织id +) + +// 获取token请求参数 +type GetTokenReq struct { + OrganizationId string `json:"organizationId"` // 组织id + OrganizationExternalId string `json:"organizationExternalId"` // 用户唯一id,uuidv4 + First_name string `json:"first_name"` // 用户名 + Avatar_url string `json:"avatar_url"` // 用户头像 + Bio string `json:"bio"` // 简介 + CreditToUser float64 `json:"creditToUser"` // 转移给用户的积分数 + Sign string `json:"sign"` // sign +} + +// 获取token返回参数 +type GetTokenResp struct { + StatusCode int `json:"statusCode"` // 状态码 + Message string `json:"message"` // 描述 + Error string `json:"error"` // 错误 + Jwt string `json:"jwt"` // jwt +} + +// 获取用户访问Ai女友的链接 +func GetUserAccessAiUrl(uid uint64, uuid, name string, amount float64) (access_url string, err error) { + var ( + //请求参数 + req = GetTokenReq{ + OrganizationId: organization_id, + OrganizationExternalId: uuid, + First_name: "91PORN-" + name, + Bio: "AiMate", + Avatar_url: "https://example.com/avatar.jpg", + CreditToUser: amount, + } + //返回参数 + resp = GetTokenResp{} + //加密字符串 + signStr = "avatar_url=" + req.Avatar_url + "&bio=" + req.Bio + "&creditToUser=" + strconv.FormatFloat(req.CreditToUser, 'f', -1, 64) + "&first_name=" + req.First_name + "&organizationExternalId=" + req.OrganizationExternalId + "&organizationId=" + req.OrganizationId + access_key + ) + //参数加密 + h := md5.New() + h.Write([]byte(signStr)) + cipherStr := h.Sum(nil) + req.Sign = hex.EncodeToString(cipherStr) + //请求 + url := req_doman + get_token_url + bodyStr, _ := json.Marshal(req) + code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr) + if err != nil { + log.Error("GetUserAccessAiUrl POSTWithJResp ", log.Any("url", url), log.Any("uid:", uid), log.E(err)) + return + } + if code != http.StatusCreated { + log.Error("GetUserAccessAiUrl response status ", log.Any("code", code), log.Any("uid", uid)) + if strings.Contains(resp.Message, "积分不足") { + err = fmt.Errorf("insufficient organization points") + return + } + err = fmt.Errorf("response status err") + return + } + //访问链接 + access_url = access_doman + "?token=" + resp.Jwt + log.Info("GetUserAccessAiUrl success", log.Any("uid", uid)) + return +} + +// 划转积分请求参数 +type TransferIntegralReq struct { + OrganizationId string `json:"organizationId"` // 组织id + SourceAccountId string `json:"sourceAccountId"` // 用户唯一id,uuidv4 + Note string `json:"note"` // 备注 + Sign string `json:"sign"` // sign +} + +// 划转积分返回参数 +type TransferIntegralResp struct { + StatusCode int `json:"statusCode"` // 状态码 + Message string `json:"message"` // 描述 + Error string `json:"error"` // 错误 + NewBalance float64 `json:"newBalance"` // 最新余额 + TransferredAmount float64 `json:"transferredAmount"` // 划转余额 +} + +// 用户划转积分,从第三方转出用户所有积分 +func UserAiTransferIntegral(uid uint64, uuid string) (transferredAmount float64, err error) { + var ( + //请求参数 + req = TransferIntegralReq{ + SourceAccountId: uuid, + OrganizationId: organization_id, + Note: "transfer-user-all-integral", + } + //返回参数 + resp = TransferIntegralResp{} + //加密字符串 + signStr = "note=" + req.Note + "&organizationId=" + req.OrganizationId + "&sourceAccountId=" + req.SourceAccountId + access_key + ) + //参数加密 + h := md5.New() + h.Write([]byte(signStr)) + cipherStr := h.Sum(nil) + req.Sign = hex.EncodeToString(cipherStr) + //请求 + url := req_doman + transfer_points_url + bodyStr, _ := json.Marshal(req) + code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr) + if err != nil { + log.Error("UserAiTransferIntegral POSTWithJResp ", log.Any("url", url), log.Any("uid:", uid), log.E(err)) + return + } + if code != http.StatusCreated { + log.Error("UserAiTransferIntegral response status ", log.Any("code", code), log.Any("uid", uid)) + err = fmt.Errorf("response status err") + return + } + log.Info("UserAiTransferIntegral success", log.Any("uid", uid)) + transferredAmount = resp.TransferredAmount + return +} + +// 获取用户余额请求参数 +type GetUserAiBalanceReq struct { + OrganizationId string `json:"organizationId"` // 组织id + ExternalUserId string `json:"externalUserId"` // 用户唯一id,uuidv4 + Sign string `json:"sign"` // sign +} + +// 获取用户余额返回参数 +type GetUserAiBalanceResp struct { + StatusCode int `json:"statusCode"` // 状态码 + Message string `json:"message"` // 描述 + Error string `json:"error"` // 错误 + Balance float64 `json:"balance"` // 余额 +} + +// 获取用户余额 +func GetUserAiBalance(uid uint64, uuid string) (balance float64, err error) { + var ( + //请求参数 + req = GetUserAiBalanceReq{ + OrganizationId: organization_id, + ExternalUserId: uuid, + } + //返回参数 + resp = GetUserAiBalanceResp{} + //加密字符串 + signStr = "externalUserId=" + req.ExternalUserId + "&organizationId=" + req.OrganizationId + access_key + ) + //参数加密 + h := md5.New() + h.Write([]byte(signStr)) + cipherStr := h.Sum(nil) + req.Sign = hex.EncodeToString(cipherStr) + //请求 + url := req_doman + get_points_url + bodyStr, _ := json.Marshal(req) + code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr) + if err != nil { + log.Error("GetUserAiBalanceReq POSTWithJResp ", log.Any("url", url), log.Any("uid:", uid), log.E(err)) + return + } + if code != http.StatusCreated && code != 0 { + log.Error("GetUserAiBalanceReq response status ", log.Any("code", code), log.Any("uid", uid)) + err = fmt.Errorf("response status err") + return + } + log.Info("GetUserAiBalanceReq success", log.Any("uid", uid)) + balance = resp.Balance + return +} + +// 获取用户交易记录请求参数 +type GetUserTransactionsReq struct { + OrganizationId string `json:"organizationId"` // 组织id + ExternalUserId string `json:"externalUserId"` // 用户唯一id,uuidv4 + Sign string `json:"sign"` // sign +} + +// 获取用户交易记录返回参数 +type GetUserTransactionsResp struct { + StatusCode int `json:"statusCode"` // 状态码 + Message string `json:"message"` // 描述 + Error string `json:"error"` // 错误 + TotalCount int `json:"totalCount"` // 总条数 + Transactions []TransactionsInfo `json:"transactions"` // 交易列表 +} + +// 用户交易记录对象 +type TransactionsInfo struct { + OrderId int `json:"orderId"` // 订单Id + Credit_delta string `json:"credit_delta"` // 此次操作积分数 + Post_credit string `json:"post_credit"` // 操作后剩余积分数 + Created_at string `json:"created_at"` // 操作时间 + Operation_type string `json:"operation_type"` // 操作类型 +} + +// 获取用户交易记录 +func GetUserTransactions(uid uint64, uuid string) (totalCount int, list []TransactionsInfo, err error) { + var ( + //请求参数 + req = GetUserTransactionsReq{ + OrganizationId: organization_id, + ExternalUserId: uuid, + } + //返回参数 + resp = GetUserTransactionsResp{} + //加密字符串 + signStr = "externalUserId=" + req.ExternalUserId + "&organizationId=" + req.OrganizationId + access_key + ) + log.Info("GetUserTransactionsReq req", log.Any("signStr:", signStr), log.Any("uid:", uid)) + //参数加密 + h := md5.New() + h.Write([]byte(signStr)) + cipherStr := h.Sum(nil) + req.Sign = hex.EncodeToString(cipherStr) + log.Info("GetUserTransactionsReq req", log.Any("params:", req), log.Any("uid:", uid)) + //请求 + url := req_doman + get_transactions_url + bodyStr, _ := json.Marshal(req) + code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr) + if err != nil { + log.Error("GetUserTransactionsReq POSTWithJResp ", log.Any("url", url), log.Any("uid:", uid), log.E(err)) + return + } + if code != http.StatusCreated && code != 0 { + log.Error("GetUserTransactionsReq response status ", log.Any("code", code), log.Any("uid:", uid), log.Any("resp", resp)) + err = fmt.Errorf("response status err") + return + } + log.Info("GetUserTransactionsReq http", log.Any("resp:", resp), log.Any("uid:", uid)) + totalCount = resp.TotalCount + list = resp.Transactions + return +} diff --git a/common/aiService/aiService.go b/common/aiService/aiService.go new file mode 100644 index 0000000..b125f1a --- /dev/null +++ b/common/aiService/aiService.go @@ -0,0 +1,396 @@ +package aiService + +import ( + "91porn-server/common" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/common/redis" + "encoding/json" + "errors" + "reflect" + "strings" + "time" + + "github.com/fatih/structs" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AiService struct { + c *Conf +} + +type Conf struct { + AppId int + Url string + Redis *redis.Client +} + +type Option func(c *Conf) + +func NewAiService(opts ...Option) *AiService { + c := &Conf{} + for _, opt := range opts { + opt(c) + } + return &AiService{ + c: c, + } +} + +func Redis(r *redis.Client) Option { + return func(c *Conf) { + c.Redis = r + } +} + +func AppId(appId int) Option { + return func(c *Conf) { + c.AppId = appId + } +} + +func Url(url string) Option { + return func(c *Conf) { + c.Url = url + } +} + +type msg struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` +} + +func (m *AiService) getParams(p interface{}) (params map[string]interface{}) { + req := structs.New(p) + fields := req.Fields() + params = make(map[string]interface{}) + for _, field := range fields { + jsonTag := field.Tag("json") + tagList := strings.Split(jsonTag, ",") + key := field.Name() + omitempty := "" + if len(tagList) > 0 { + key = tagList[0] + } + if len(tagList) > 1 { + omitempty = tagList[1] + } + kind := reflect.TypeOf(field.Value()).Kind() + // 如果是0值忽略或者空指针,不需要传 + if field.IsZero() && (kind == reflect.Ptr || omitempty == "omitempty") { + continue + } + if kind == reflect.Ptr { + // 通过指针取值 + params[key] = reflect.ValueOf(field.Value()).Elem().Interface() + } else { + params[key] = field.Value() + } + } + params["appId"] = m.c.AppId + return +} + +func (s *AiService) getTemplateList(p *TemplateListReq) (result TemplateListResp, err error) { + var retMsg msg + path := "/api/ai/template/all" + params := s.getParams(p) + code, err := httputil.DefaultClientGetWithResp(&retMsg, common.BindUrl(s.c.Url, path), nil, params) + if err != nil { + log.Error("AiService GetTemplateList fail", log.Any("params", params), log.E(err)) + err = errors.New("AiService GetTemplateList fail") + return + } + if code != 200 { + log.Error("AiService GetTemplateList fail", log.Any("params", params), log.Any("code", code)) + err = errors.New("AiService GetTemplateList fail") + return + } + + if retMsg.Code != 200 { + log.Error("AiService GetTemplateList fail", log.Any("params", params), log.Any("retMsg", retMsg)) + err = errors.New("AiService GetTemplateList fail") + return + } + + b, err := json.Marshal(retMsg.Data) + if err != nil { + log.Error("AiService GetTemplateList json.Marshal fail", log.Any("params", params), log.E(err)) + err = errors.New("AiService GetTemplateList json.Marshal fail") + return + } + + if err = json.Unmarshal(b, &result); err != nil { + log.Error("AiService GetTemplateList json.Unmarshal fail", log.Any("params", params), log.Any("data", string(b)), log.E(err)) + err = errors.New("AiService GetTemplateList json.Unmarshal fail") + return + } + + return +} + +type TemplateDetailReq struct { + Ids []primitive.ObjectID `json:"ids" binding:"required"` +} + +func (s *AiService) getTemplateDetail(p *TemplateDetailReq) (result TemplateDetailResp, err error) { + var retMsg msg + path := "/api/ai/template/detail" + params := s.getParams(p) + code, err := httputil.DefaultClientPostJsonWithResp(&retMsg, common.BindUrl(s.c.Url, path), nil, params) + if err != nil { + log.Error("AiService GetTemplateDetail fail", log.Any("params", params), log.E(err)) + err = errors.New("AiService GetTemplateDetail fail") + return + } + if code != 200 { + log.Error("AiService GetTemplateDetail fail", log.Any("params", params), log.Any("code", code)) + err = errors.New("AiService GetTemplateDetail fail") + return + } + + if retMsg.Code != 200 { + log.Error("AiService GetTemplateDetail fail", log.Any("params", params), log.Any("retMsg", retMsg)) + err = errors.New("AiService GetTemplateDetail fail") + return + } + + b, err := json.Marshal(retMsg.Data) + if err != nil { + log.Error("AiService GetTemplateDetail json.Marshal fail", log.Any("params", params), log.E(err)) + err = errors.New("AiService GetTemplateDetail json.Marshal fail") + return + } + + if err = json.Unmarshal(b, &result); err != nil { + log.Error("AiService GetTemplateDetail json.Unmarshal fail", log.Any("params", params), log.Any("data", string(b)), log.E(err)) + err = errors.New("AiService GetTemplateDetail json.Unmarshal fail") + return + } + + return +} + +func (s *AiService) Sync() (err error) { + if s.c.Redis == nil { + return errors.New("redis is nil") + } + resp, err := s.getTemplateList(&TemplateListReq{}) + if err != nil { + log.Error("AI Template sync fail", log.E(err)) + return + } + for _, template := range resp.TemplateList { + b, err := json.Marshal(template) + if err != nil { + continue + } + // 将模版数据写入redis + s.c.Redis.Set(AiTemplateKey(template.ID), string(b), time.Hour*24*7) + } + var imageCategoryList []Category + var videoCategoryList []Category + for _, v := range resp.CategoryList { + if v.Type == 0 { + imageCategoryList = append(imageCategoryList, v) + } else { + videoCategoryList = append(videoCategoryList, v) + } + } + // 将数据写入写入redis + b0, err := json.Marshal(imageCategoryList) + if err != nil { + log.Error("AI Template json.Marshal(imageCategoryList) fail", log.E(err)) + return + } + b1, err := json.Marshal(videoCategoryList) + if err != nil { + log.Error("AI Template json.Marshal(videoCategoryList) fail", log.E(err)) + return + } + err = s.c.Redis.Set(AiCategoryKey(0), string(b0), time.Hour*24*7) + if err != nil { + log.Error("AI Template Redis.Set(AiCategoryKey(0) fail", log.E(err)) + return + } + err = s.c.Redis.Set(AiCategoryKey(1), string(b1), time.Hour*24*7) + if err != nil { + log.Error("AI Template Redis.Set(AiCategoryKey(1) fail", log.E(err)) + return + } + return nil +} + +func (s *AiService) GetTemplateList(types int, categoryId string) (categoryList []*Category, queryCategoryId string, templateList []*Template, err error) { + if s.c.Redis == nil { + err = errors.New("redis nil") + return + } + // 获取分类列表 + val, err := s.c.Redis.Get(AiCategoryKey(types)) + if err != nil { + return + } + if val == nil { + return + } + err = json.Unmarshal([]byte(*val), &categoryList) + if err != nil { + return + } + if len(categoryList) == 0 { + return + } + if categoryId == "" { + queryCategoryId = categoryList[0].ID.Hex() + } else { + queryCategoryId = categoryId + } + templateIds := []primitive.ObjectID{} + for _, v := range categoryList { + if v.ID.Hex() == queryCategoryId { + templateIds = v.TemplateIds + break + } + } + keys := []string{} + for _, templateId := range templateIds { + keys = append(keys, AiTemplateKey(templateId)) + } + if len(keys) == 0 { + return + } + // 通过id查找这个模版 + res, err := s.c.Redis.MGet(keys...) + if err != nil { + return + } + for _, v := range res { + templateVal, ok := v.(string) + if !ok || templateVal == "" { + continue + } + item := Template{} + err = json.Unmarshal([]byte(templateVal), &item) + if err != nil { + continue + } + if item.ModuleType != types { + continue + } + templateList = append(templateList, &item) + } + return categoryList, queryCategoryId, templateList, nil +} + +func (s *AiService) GetTemplate(id primitive.ObjectID) (template Template, err error) { + if id.IsZero() { + return + } + if s.c.Redis == nil { + err = errors.New("redis nil") + return + } + // 从缓存中获取数据 + val, err := s.c.Redis.Get(AiTemplateKey(id)) + if err != nil { + return + } + if val != nil && *val != "" { + err = json.Unmarshal([]byte(*val), &template) + if err == nil { + return template, nil + } + } + // 如果获取不到,尝试从ai服务获取 + res, err := s.getTemplateDetail(&TemplateDetailReq{ + Ids: []primitive.ObjectID{id}, + }) + if err != nil { + return + } + if len(res.List) == 0 { + return + } + template = res.List[0] + common.Go(func() { + b, err := json.Marshal(template) + if err != nil { + return + } + // 将模版数据写入redis + s.c.Redis.Set(AiTemplateKey(template.ID), string(b), time.Hour*48) + }) + return template, nil +} + +func (s *AiService) GetTemplateByIds(ids []primitive.ObjectID) (templateList []Template, err error) { + if len(ids) == 0 { + return + } + if s.c.Redis == nil { + err = errors.New("redis nil") + return + } + + keys := []string{} + for _, id := range ids { + keys = append(keys, AiTemplateKey(id)) + } + // 从缓存中获取数据 + list, err := s.c.Redis.MGet(keys...) + if err != nil { + return + } + noFoundKey := []int{} + for k, v := range list { + if v == nil { + noFoundKey = append(noFoundKey, k) + continue + } + val, ok := v.(string) + if !ok || val == "" { + noFoundKey = append(noFoundKey, k) + continue + } + item := Template{} + err = json.Unmarshal([]byte(val), &item) + if err != nil { + noFoundKey = append(noFoundKey, k) + continue + } + templateList = append(templateList, item) + } + noFoundIds := []primitive.ObjectID{} + for _, k := range noFoundKey { + noFoundIds = append(noFoundIds, ids[k]) + } + if len(noFoundIds) == 0 { + return + } + // 如果获取不到,尝试从ai服务获取 + res, err := s.getTemplateDetail(&TemplateDetailReq{ + Ids: noFoundIds, + }) + if err != nil { + return + } + if len(res.List) == 0 { + return + } + templateList = append(templateList, res.List...) + common.Go(func() { + // 将查到的写入redis + for _, template := range res.List { + b, err := json.Marshal(template) + if err != nil { + return + } + // 将模版数据写入redis + s.c.Redis.Set(AiTemplateKey(template.ID), string(b), time.Hour*48) + } + + }) + return templateList, nil +} diff --git a/common/aiService/rediskey.go b/common/aiService/rediskey.go new file mode 100644 index 0000000..f215c30 --- /dev/null +++ b/common/aiService/rediskey.go @@ -0,0 +1,20 @@ +package aiService + +import ( + "fmt" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + aiCategoryKey = "aiCategory:%v" + aiTemplateKey = "aiTemplate:%v" + aiCategoryTemplateKey = "aiCategoryTemplate:%v" +) + +func AiTemplateKey(id primitive.ObjectID) string { + return fmt.Sprintf(aiTemplateKey, id.Hex()) +} + +func AiCategoryKey(types int) string { + return fmt.Sprintf(aiCategoryKey, types) +} diff --git a/common/aiService/type.go b/common/aiService/type.go new file mode 100644 index 0000000..d875c22 --- /dev/null +++ b/common/aiService/type.go @@ -0,0 +1,46 @@ +package aiService + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +// 获取模版列表 +type TemplateListReq struct { +} + +type TemplateListResp struct { + CategoryList []Category `json:"categoryList"` // 分类列表 + TemplateList []*Template `json:"templateList"` // 模版列表 + +} + +type TemplateDetailResp struct { + List []Template `json:"list"` +} + +type Template struct { + ID primitive.ObjectID `json:"id" bson:"id"` + Title string `json:"title" bson:"title"` + Cover string `json:"cover" bson:"cover"` //封面 + M3u8Url string `json:"m3u8Url" bson:"m3u8_url"` // m3u8 地址 + Coin uint `json:"coin" bson:"coin"` // 价格(金币) + ModuleType int `json:"moduleType" bson:"module_type"` //换脸模版类型 0 图片 1 视频 + VipCoin uint `json:"vipCoin" bson:"vipCoin"` + CategoryId primitive.ObjectID `json:"categoryId" bson:"categoryId"` // 分类id + CreatedAt time.Time `json:"createdAt,omitempty" bson:"createdAt"` //创建时间 + UsedCount uint64 `json:"usedCount" bson:"usedCount"` // 模版使用次数 +} + +// Category 模版分类表 +type Category struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` // 分类名 + Type int `json:"type" bson:"type"` // 0 图片 1 视频 + SortCode int `json:"sortCode" bson:"sortCode"` // 排序号 + AppId int `json:"appId" bson:"appId"` // 绑定appId + Status int `json:"status" bson:"status"` // 0-不可用 1-可用 + UpdateTime time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间 + CreateTime time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + TemplateIds []primitive.ObjectID `json:"templateIds"` +} diff --git a/common/bank/bank.go b/common/bank/bank.go new file mode 100644 index 0000000..9dbc2f4 --- /dev/null +++ b/common/bank/bank.go @@ -0,0 +1,96 @@ +package bank + +import ( + "encoding/json" + "fmt" + + "91porn-server/common/httputil" + "91porn-server/common/log" +) + +// Bank 充值订单请求体 +type Bank struct { + CardNo string `json:"cardNo" form:"cardNo"` + CardBinCheck string `json:"cardBinCheck" from:"cardBinCheck"` +} + +type MsgModel struct { + CardType string `json:"cardType"` + Bank string `json:"bank"` + Key string `json:"key"` + Messages []Message `json:"messages"` + Validated bool `json:"validated"` + Stat string `json:"stat"` +} + +type Message struct { + ErrorCodes string `json:"errorCodes"` + Name string `json:"name"` +} + +type BankType struct { + BankName string `form:"bankName" json:"bankName" bson:"bankName"` //银行名字 + BankCode string `form:"bankCode" json:"bankCode" bson:"bankCode"` //银行代码 + Icon string `form:"icon" json:"icon" bson:"icon"` + Img string `form:"img" json:"img" bson:"img" ` +} + +// GetBankCardInfo 银行卡信息获取 +func (b *Bank) GetBankCardCode() (msg MsgModel, err error) { + b.Fill() + code, err := httputil.DefaultClientGetWithResp(&msg, b.GetURL(), b.GetHeader(), b.GetQueryParam()) + if err != nil { + log.Error(fmt.Sprintf("bank GetBankCardInfo Http.Get fail error:%+v/,data:%+v;", err, b)) + return + } + if code != 200 { + log.Error(fmt.Sprintf("bank GetBankCardInfo Http.Get fail error:%+v/,data:%+v;", err, msg)) + return + } + return +} + +// GetBankCardInfo 银行卡信息获取 +func (b *Bank) GetBankCardName(code string) (msg BankType, err error) { + return +} + +// Fill 补全配置信息和签名信息 +func (b *Bank) Fill() { +} + +// GetBody 获取请求body +func (b *Bank) GetBody() interface{} { + jsonStr, err := json.Marshal(b) + if err != nil { + log.Info(fmt.Sprintf("Bank GetBody json.Marshal is fail error:%+v/data:%+v", err, b)) + } + return jsonStr +} + +// GetBody 获取请求body +func (b *Bank) GetQueryParam() map[string]string { + j, err := json.Marshal(b) + if err != nil { + return nil + } + m := make(map[string]string) + if err := json.Unmarshal(j, &m); err != nil { + return nil + } + return m +} + +// GetHeader 获取请求头信息 +func (b *Bank) GetHeader() map[string]string { + header := make(map[string]string, 0) + return header +} + +// GetURL 获取请求地址 +func (bank *Bank) GetURL() string { + return "https://ccdcapi.alipay.com/validateAndCacheCardInfo.json" +} + +func (b *Bank) GetBankCardInfo() { +} diff --git a/common/cache/cache.go b/common/cache/cache.go new file mode 100644 index 0000000..db57b7a --- /dev/null +++ b/common/cache/cache.go @@ -0,0 +1,34 @@ +package cache + +import ( + "time" + + cache "github.com/robfig/go-cache" +) + +// Cache 服务单机本地缓存 +type Cache struct { + Expiration time.Duration + CleanInterval time.Duration + Cli *cache.Cache +} + +// New 创建缓存客户端 +func (c *Cache) New() { + c.Cli = cache.New(c.Expiration, c.CleanInterval) +} + +// Set 设置键值 +func (c *Cache) Set(key string, value interface{}, expiration time.Duration) { + c.Cli.Set(key, value, expiration) +} + +// Get 获取值 +func (c *Cache) Get(key string) (result interface{}, exists bool) { + return c.Cli.Get(key) +} + +// Add 添加值 +func (c *Cache) Add(key string, value interface{}, expiration time.Duration) error { + return c.Cli.Add(key, value, expiration) +} diff --git a/common/cachev2/base.go b/common/cachev2/base.go new file mode 100644 index 0000000..ee5084d --- /dev/null +++ b/common/cachev2/base.go @@ -0,0 +1,445 @@ +package cachev2 + +import ( + "crypto/md5" + "fmt" + "math" + "math/rand" + "runtime" + "strings" + "time" + + "go.uber.org/zap" +) + +const ( + // 默认的缓存时间 + defaultCacheTime = 1800 + // 空数据的缓存时间 + emptyDataCacheTime = 300 + + // 数据类型 + dataTypeList = "list" + dataTypeInfo = "info" +) + +// BaseCache 缓存类定义 +// 集成到类黑料框架文件: +// +// copy common/cachev2/* +// common/redis/redis.go +ScanKeys方法 +// web/main.go | app/main.go (+初始化缓存操作中间件) +// +// Usage: +// +// cache object: cachev2.Classes().CacheTime(30*time.Minute).Key("cache-key").ResBind(&object).Cache(user_func, params...) +// cache base type: cachev2.Classes().CacheTime(2*time.Hour).Key("cache-key").Cache(user_func, params...) +// auto cache key(list): cachev2.Classes().CacheTime(600*time.Second).AutoListKey("table-name").Cache(user_func, params...) +// auto cache key(info): cachev2.Classes().CacheTime(600*time.Second).AutoInfoKey("table-name", "id-string").Cache(user_func, params...) +// +// 清理缓存: +// +// clear cache key: cachev2.Classes().FussyClear("key-prefix*") +// clear auto cache(just list cache): cachev2.Classes().Table("table-name").AutoClear(true, nil) +// clear auto cache(just info cache): cachev2.Classes().Table("table-name").AutoClear(false, &idString) +// clear auto cache(both of list and info): cachev2.Classes().Table("table-name").AutoClear(true, &idString) +type BaseCache struct { + // 缓存驱动器 + driver Driver + // 缓存时间 s + ttl int64 + // 是否随机时间 + random bool + // 缓存随机时间 s + randomTTL int + // 是否需要刷新缓存 + refresh bool + // 缓存key + key string + // 自动生成缓存key + autoKey bool + // 表名 + table string + // 数据类型 + dataType string + // 数据id + dataID string + // 缓存对象 + object interface{} + // 日志驱动 + logger *zap.Logger +} + +// basic 缓存基础 +var basic = &BaseCache{} + +// Init 初始化 +func Init(d Driver, l *zap.Logger) { + basic.driver = d + basic.logger = l +} + +// Classes 获取操作类 +func Classes() *BaseCache { + c := &BaseCache{ + driver: basic.driver, + ttl: defaultCacheTime, + random: true, + logger: basic.logger, + } + + return c +} + +// New 新建缓存操作类 +func New(d Driver) *BaseCache { + rand.Seed(time.Now().UnixNano()) + rnd := rand.Intn(30) + c := &BaseCache{ + driver: d, + ttl: defaultCacheTime, + random: true, + randomTTL: rnd, + logger: basic.logger, + } + + return c +} + +// Key 设置缓存键 +func (c *BaseCache) Key(k string) *BaseCache { + c.key = k + return c +} + +// AutoListKey 自动生成列表存储键 +func (c *BaseCache) AutoListKey(table string) *BaseCache { + c.autoKey = true + c.table = table + c.dataType = dataTypeList + return c +} + +// AutoInfoKey 自动生成单条数据键 +func (c *BaseCache) AutoInfoKey(table, id string) *BaseCache { + c.autoKey = true + c.table = table + c.dataID = id + c.dataType = dataTypeInfo + return c +} + +// ResBind 设置缓存对象 +func (c *BaseCache) ResBind(o interface{}) *BaseCache { + c.object = o + return c +} + +// CacheTime 设定缓存时间 +func (c *BaseCache) CacheTime(t time.Duration) *BaseCache { + c.ttl = int64(t.Seconds()) + c.random = true + return c +} + +// CacheStair 阶梯式缓存 +func (c *BaseCache) CacheStair(td time.Duration) *BaseCache { + second := int64(td.Seconds()) + t := time.Now() + timeSecond := int64(math.Min(86400, math.Max(60, float64(second)))) + zeroTime := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()).Unix() + over := t.Unix() - zeroTime + + c.ttl = timeSecond - over%timeSecond + c.random = false + return c +} + +// Refresh 刷新缓存 +func (c *BaseCache) Refresh() *BaseCache { + c.refresh = true + return c +} + +// Cache 缓存并返回[对象]数据 +func (c *BaseCache) Cache(fn interface{}, p ...interface{}) (data interface{}, err error) { + // 操作完成后需要重置缓存对象 + defer c.reset() + + // 自动缓存key + if c.autoKey { + c.key, err = c.generalKey(false, p...) + if err != nil { + return + } + } + + if c.key == "" { + return nil, fmt.Errorf("缓存键未设置") + } + + var e error + if c.refresh { + _, _ = c.Delete(c.key) + } + // 获取缓存 + ok, d := c.getCache(c.key) + // 存在缓在则进行反序列编码 + if ok { + data, e = unSerialJson(d, c.object) + if e == nil { + //fmt.Println("data from cache") + return + } + + c.LogError("unSerialJson error occur:", zap.Any("data", d), zap.Error(err)) + } + + // 不存在缓存则获取锁 + lockKey := c.key + "_processing" + ok, e = c.driver.SetNX(lockKey, 1, 5*time.Second) + if !ok || e != nil { + //fmt.Println("未获取到锁,等待中:", lockKey) + i := 0 + expire := false + var d string + for expire == false { + time.Sleep(200 * time.Millisecond) + // 重新尝试获取缓存 + ok, d = c.getCache(c.key) + i++ + expire = i >= 25 || ok + //fmt.Printf("第 %d 次尝试获取缓存,key: %s \n", i, lockKey) + } + + if ok { + data, e = unSerialJson(d, c.object) + if e == nil { + //fmt.Println("data from cache2") + return + } + } + } + + //fmt.Println("已获取到cache锁:", lockKey) + // 释放数据请求锁 + defer func(driver Driver, key string) { + _, _ = driver.Del(key) + }(c.driver, lockKey) + + // 请求原始数据 + data, err = funcInvoke(fn, p...) + if err != nil && err.Error() != "record not found" { + c.LogError("acquire source data error occur:", zap.Error(err)) + return nil, fmt.Errorf("获取原数据错误:%s", err.Error()) + } + + // 缓存原始数据后释放锁 + d, e = serialJson(data) + if e == nil { + //fmt.Println("data form origin") + // object 设置 + if c.object != nil { + _, e = unSerialJson(d, c.object) + if e != nil { + c.LogError("unmarshal data to object error occur:", zap.Error(e)) + return data, fmt.Errorf("数据映射失败:%s", e.Error()) + } + } + e = c.setCache(c.key, d) + if e != nil && c.logger != nil { + c.LogError("set cache data error occur:", zap.Any("data", data), zap.Error(e)) + } + } else { + c.LogError("serialJson data error occur:", zap.Any("data", data), zap.Error(e)) + } + + return +} + +// generalKey 自动生成缓存键 +func (c *BaseCache) generalKey(forFuzzyClear bool, p ...interface{}) (string, error) { + var id, key string + keyStr, e := serialJson(p) + if e != nil { + return "", fmt.Errorf("此类参数不支持自动生成key") + } + switch c.dataType { + case dataTypeList: + if forFuzzyClear { + id = "*" + } else { + id = fmt.Sprintf("%x", md5.Sum([]byte(keyStr))) + } + key = fmt.Sprintf("table-%s:list-%s", c.table, id) + case dataTypeInfo: + if forFuzzyClear { + id = c.dataID + ":*" + } else { + id = fmt.Sprintf("%s:%x", c.dataID, md5.Sum([]byte(keyStr))) + } + key = fmt.Sprintf("table-%s:info-%s", c.table, id) + } + + return key, nil +} + +// getCache 获取缓存 +func (c *BaseCache) getCache(key string) (ok bool, data string) { + if !c.driver.IsExist(key) { + return + } + var err error + res, err := c.driver.Get(key) + if err != nil { + c.LogError("get cache error:", zap.Any("cache key", key), zap.Error(err)) + return + } + if res == nil { + return + } + + return true, *res +} + +// setCache 设置缓存 +func (c *BaseCache) setCache(key string, data string) error { + if data == "" || data == "null" { + c.ttl = emptyDataCacheTime + } else if c.random { + if c.randomTTL == 0 { + rand.Seed(time.Now().UnixNano()) + rnd := rand.Intn(30) + c.ttl += int64(rnd) + } else { + c.ttl += int64(c.randomTTL) + } + } + + return c.driver.Set(key, data, time.Duration(c.ttl)*time.Second) +} + +// Delete 清除指定缓存 +func (c *BaseCache) Delete(key ...string) (int64, error) { + c.refresh = false + count, err := c.driver.Del(key...) + if err != nil { + c.LogError("delete cache key error", zap.Any("key", key), zap.Error(err)) + return count, err + } + + return count, nil +} + +// DeleteCurrent 清除当前缓存 +func (c *BaseCache) DeleteCurrent(p ...interface{}) (int64, error) { + var err error + if c.key == "" { + c.key, err = c.generalKey(false, p...) + if err != nil { + return 0, err + } + } + return c.Delete(c.key) +} + +// FussyClear 模糊匹配删除 +func (c *BaseCache) FussyClear(match string) (int64, error) { + go func() { + var err error + var keys []string + keys, err = c.driver.ScanKeys(match) + if err != nil { + c.LogError("scan cache key error", zap.Any("match", match), zap.Error(err)) + return + } + + // 分批删除 + var batch = 10 + for i := 0; i < len(keys); i += batch { + if i+batch >= len(keys) { + _, err = c.Delete(keys[i:]...) + } else { + _, err = c.Delete(keys[i : i+batch]...) + } + if err != nil { + return + } + } + }() + return 1, nil +} + +// Table 设置表名 +func (c *BaseCache) Table(table string) *BaseCache { + c.table = table + return c +} + +// AutoClear 清理自动生成的列表/详情缓存 +func (c *BaseCache) AutoClear(clearList bool, id *string) (count int64, err error) { + // 判断是否设置表 + //if c.table == "" { + // return 0, fmt.Errorf("清理表缓存失败,未设置表名") + //} + // + //var tc int64 + //// 先清理列表缓存 + //if clearList { + // c.dataType = dataTypeList + // key, _ := c.generalKey(true) + // tc, err = c.FussyClear(key) + // count += tc + // if err != nil { + // return + // } + //} + //// 清理详情数据 + //if id != nil { + // c.dataType = dataTypeInfo + // c.dataID = *id + // key, _ := c.generalKey(true) + // tc, err = c.FussyClear(key) + // count += tc + // if err != nil { + // return + // } + //} + return +} + +// LogError 日志错误记录 +func (c *BaseCache) LogError(msg string, fs ...zap.Field) { + if c.logger != nil { + dep := 0 + t := make([]string, 0, 10) + for i := 1; i < 10; i++ { + _, file, line, ok := runtime.Caller(i) + if !ok { + break + } + if strings.Contains(file, "/runtime/") || strings.Contains(file, "/reflect/") { + continue + } + t = append(t, fmt.Sprintf("%s∟%s:%d", strings.Repeat(" ", dep), file, line)) + dep++ + } + exception := fmt.Sprintf("[MSG]%s\n[Stack]\n%s", msg, strings.Join(t, "\n")) + + c.logger.Error(exception, fs...) + } +} + +// reset 重置缓存 +func (c *BaseCache) reset() { + c.ttl = defaultCacheTime + c.random = true + c.randomTTL = 0 + c.refresh = false + c.key = "" + c.autoKey = false + c.table = "" + c.dataType = "" + c.dataID = "" + c.object = nil +} diff --git a/common/cachev2/proto.go b/common/cachev2/proto.go new file mode 100644 index 0000000..fb45629 --- /dev/null +++ b/common/cachev2/proto.go @@ -0,0 +1,14 @@ +package cachev2 + +import "time" + +// Driver 缓存驱动接口定义 +type Driver interface { + IsExist(key string) bool + Get(key string) (*string, error) + Set(key string, value interface{}, expire time.Duration) error + SetNX(key string, value interface{}, expiration time.Duration) (bool, error) + ScanKeys(match string) (keys []string, err error) + Del(keys ...string) (n int64, err error) + Close() error +} diff --git a/common/cachev2/util.go b/common/cachev2/util.go new file mode 100644 index 0000000..061c14e --- /dev/null +++ b/common/cachev2/util.go @@ -0,0 +1,76 @@ +package cachev2 + +import ( + "encoding/json" + "fmt" + "reflect" + "runtime" +) + +// funcInvoke 函数调用 +func funcInvoke(fun interface{}, args ...interface{}) (data interface{}, err error) { + ft := reflect.TypeOf(fun) + fv := reflect.ValueOf(fun) + if ft.Kind() != reflect.Func { + return nil, fmt.Errorf("CACHE:不是一个有效的函数") + } + funcArgsNum := ft.NumIn() + if len(args) != funcArgsNum { + return nil, fmt.Errorf("CACHE:参数未对齐错误,方法名:%s,方法参数数:%d,传入参数数:%d", runtime.FuncForPC(fv.Pointer()).Name(), funcArgsNum, len(args)) + } + funcResNum := ft.NumOut() + if funcResNum < 1 { + return nil, fmt.Errorf("CACHE:没有返回值的无效调用,方法名:%s", runtime.FuncForPC(fv.Pointer()).Name()) + } + + // 调用函数 + var funRes []reflect.Value + if funcArgsNum == 0 { + funRes = fv.Call(nil) + } else { + argsV := make([]reflect.Value, 0, len(args)) + for _, arg := range args { + argsV = append(argsV, reflect.ValueOf(arg)) + } + + funRes = fv.Call(argsV) + } + + data = funRes[0].Interface() + for _, ret := range funRes { + if ret.Type().String() == "error" { + e, ok := ret.Interface().(error) + if ok { + err = e + } + break + } + } + + return +} + +// serialJson 序列化为JSON +func serialJson(m interface{}) (string, error) { + b, err := json.Marshal(m) + if err != nil { + return "", err + } + + return string(b), nil +} + +// unSerialJson 从JSON字符串反序列化 +func unSerialJson(str string, resObj interface{}) (m interface{}, err error) { + if resObj == nil { + err = json.Unmarshal([]byte(str), &m) + } else { + err = json.Unmarshal([]byte(str), &resObj) + m = resObj + } + if err != nil { + return nil, err + } + + return +} diff --git a/common/checkWx/checkWx.go b/common/checkWx/checkWx.go new file mode 100644 index 0000000..0bb0ed6 --- /dev/null +++ b/common/checkWx/checkWx.go @@ -0,0 +1,65 @@ +package checkWx + +import ( + "bytes" + "context" + "time" + + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/common/stderr" +) + +const ApiToken string = "866mcJyA4teCehxivaCJec3qbghWStjA" + +type Resp struct { + Code stderr.Code `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` +} + +type CheckResp struct { + Code stderr.Code `json:"code"` + Msg string `json:"msg"` + Data struct { + Pass bool `json:"pass"` + } `json:"data"` +} + +// 获取订单信息 +func CheckWx(execUrl, domian string) (checkResp CheckResp, err error) { + c, cancle := context.WithTimeout(context.Background(), 5*time.Second) + defer cancle() + code, err := httputil.DefaultClientGetWithRespWithCtx(c, &checkResp, execUrl+"?"+bindUrl(domian), nil) + log.Info("http method CheckWx response code ==>", log.Any("statusCode", code), log.Any("checkResp", checkResp)) + if err != nil { + log.Error("CheckWx failed", log.E(err)) + } + return +} + +func TgSend(msgPayload, text string) (resp Resp, err error) { + params := map[string]interface{}{ + "tgName": "ys_bot", + "text": text, + "msgPayload": msgPayload, + "chatId": -228259065, + } + c, cancle := context.WithTimeout(context.Background(), 5*time.Second) + defer cancle() + code, err := httputil.DefaultClientPostJsonWithRespWithCtx(c, &resp, "http://uni.ztgba.com/api/tg/send", nil, params) + log.Info("http method TgSend response code ==>", log.Any("statusCode", code), log.Any("resp", resp)) + if err != nil { + log.Error("TgSend failed", log.E(err)) + } + return +} + +func bindUrl(domain string) string { + buf := bytes.Buffer{} + buf.WriteString("apiToken=") + buf.WriteString(ApiToken + "&") + buf.WriteString("req_url=") + buf.WriteString(domain) + return buf.String() +} diff --git a/common/conf/conf.go b/common/conf/conf.go new file mode 100644 index 0000000..f9f57f8 --- /dev/null +++ b/common/conf/conf.go @@ -0,0 +1,23 @@ +package conf + +import ( + "encoding/json" + "io" + "os" +) + +func LoadJSON(path string, cfg interface{}) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer func() { _ = f.Close() }() + bs, err := io.ReadAll(f) + if err != nil { + return err + } + if err = json.Unmarshal(bs, cfg); err != nil { + return err + } + return nil +} diff --git a/common/constant/cacheconst/cacheconst.go b/common/constant/cacheconst/cacheconst.go new file mode 100644 index 0000000..150789a --- /dev/null +++ b/common/constant/cacheconst/cacheconst.go @@ -0,0 +1,3 @@ +package cacheconst + +const LocalCdnCacheKey = "cdn:cache" //本地缓存cdn域名列表 diff --git a/common/constant/constant.go b/common/constant/constant.go new file mode 100644 index 0000000..a2c5da4 --- /dev/null +++ b/common/constant/constant.go @@ -0,0 +1,375 @@ +package constant + +import "strings" + +const ( + //DevRunmod 开发环境 + DevRunmod = "dev" + //TestRunmod 测试环境 + TestRunmod = "test" + //RelRunmod 生产环境 + ReleaseRunmod = "prod" + //ChannelTypeFile 渠道日志导出 + ChannelLogTypeFile = 1 + //ChannelNumTypeFile 渠道号导出 + ChannelNumTypeFile = 2 + //UserList 用户列表导出 + UserList = 3 + Ver1_6_4 = "1.6.4" + Ver2_1_0 = "2.1.0" + Ver3_0_7 = "3.0.7" + Ver3_1_0 = "3.1.0" + Ver3_6_0 = "3.6.0" //更换了加密方式, 增加了注册 devID 签名校验, 修正了x-api-key的问题 +) + +const DefaultBloggerVideoIncomeTaxLevel int64 = 4 + +type NewsType = string +type AcgNewsType = string +type KeywordType = string + +// 全局统一类型 +const ( + SP NewsType = "SP" // 长视频 + SHORT NewsType = "SHORT" // 短视频 + COVER NewsType = "COVER" // 图文帖子 + PIC NewsType = "PIC" // 图集帖子 + AD_COVER NewsType = "AD_COVER" // 图片广告 + AD_SP NewsType = "AD_SP" // 视频广告 + SEED_LINK NewsType = "SEED_LINK" // 种子/黄油帖子 + AiPlaza string = "AiPlaza" // ai广场 + + KeywordTag KeywordType = "TAG" // 标签关键词 + KeywordUser KeywordType = "USER" // 用户关键词 + KeywordComment KeywordType = "COMMENT" // 评论关键词 + + Cartoon AcgNewsType = "video" // 动漫 + Comics AcgNewsType = "image" // 漫画 + Text AcgNewsType = "text" // 小说 + Drama AcgNewsType = "drama" // 短剧 +) + +type RealmType = string + +const ( + SearchSP RealmType = SP + SearchShort RealmType = SHORT + SearchCover RealmType = COVER + SearchPic RealmType = PIC + + SearchTag RealmType = KeywordTag + SearchUser RealmType = KeywordUser + + // 单个功能特殊自定义 + //SearchAll = RealmType("ALL") // 搜索全部 + SearchComplex = RealmType("COMPLEX") + + // Deprecated + FictionRealmType RealmType = "fiction" + // Deprecated + AudioBookRealmType RealmType = "audiobook" +) + +// 收藏类型 +type CollectType = string + +const ( + CollectTypeSP CollectType = SP // 影视 + CollectTypeShort CollectType = SHORT // 短视频 + CollectTypeCover CollectType = COVER // 图文 + CollectTypePIC CollectType = PIC // 图集帖子 + CollectTypeSEED_LINK CollectType = SEED_LINK // 黄油/种子帖子 + CollectTypeAiPlaza CollectType = AiPlaza // ai广场帖子 + + CollectTypeTag CollectType = KeywordTag // 话题 + + // Deprecated + CollectTypeLocation = "location" +) + +// 点赞类型 +type LikeType = string + +const ( + LikeTypeSP LikeType = SP // 长视频 + LikeTypeShort LikeType = SHORT // 短视频 + LikeTypeCover LikeType = COVER // 图文 + LikeTypePic LikeType = PIC // 图集 + LikeTypeSEED_LINK LikeType = SEED_LINK // 黄油种子帖 + LikeTypeAiPlaza LikeType = AiPlaza // ai广场帖子 + + // ACG "video":视频,"image":图片,"text":小说 + LikeTypeCartoon LikeType = Cartoon // 动漫 + LikeTypeComics LikeType = Comics // 漫画 + LikeTypeText LikeType = Text // 小说 + LikeTypeDrama LikeType = Drama // 短剧 + + LikeTypeComment LikeType = KeywordComment // 评论 +) + +// 评论状态 +const ( + CommentSTSAccessed = 1 // 通过审核 + CommentSTSDeleted = 2 // 已删除 + CommentSTSAutoFiltered = 3 // 被自动过滤 +) + +// 设备 +const ( + DeviceTypeIOS = "ios" + DeviceTypeAndroid = "android" + DeviceTypeH5 = "h5" +) + +const AdGroupClientVersion = "1.12.2" // 这里得改成自己上线的这个ab测安卓版本号 + +// 机型 +const ( + SysTypeIOS = "ios" + SysTypeAndroid = "android" + SysTypeH5 = "h5" +) + +// gin中ctx使用的key +const ( + CtxUserID = "USER_ID" + CtxAdminAct = "ADMIN_ACT" + CtxDistrictName = "DISTRICT_USER_ID" + CtxUA = "UA" + CtxIP = "IP" + CtxJuShangCID = "JU_SHANG_CID" + CtxAdminRole = "ADMIN_ROLE" + CtxNudeChatMerchant = "Nude_Chat_Merchant" +) + +const ( + CaptchaLen = 6 +) + +const ( + ProdEnv = "prod" +) + +// 推广码链接参数 +const ( + PromotionField = "?pc=" + DiscField = "?dc=" +) + +// web操作栏目 +const ( + Administrator = "系统管理员" + AuthorManager = "权限管理" + UserManageList = "用户列表" + VideoManageCheck = "视频审核" + VideoManageList = "视频列表" + VideoDiscountArea = "折扣专区" + VideoDiscountAreaVideo = "折扣专区视频" + VideoManageTag = "标签管理" + VideoManageCity = "城市列表" + VideoManageHotCity = "热门城市" + VideoManageComment = "评论管理" + VideoManageSource = "资源管理" + VideoManageTone = "音色最热" + ProductManageVIP = "vip商品列表" + ProductManageCoin = "金币列表" + ProductManagePage = "推广落地页配置" + ProductManagePayType = "支付方式" + TradeManageRechargeOrder = "充值订单管理" + TradeManageWithdrawOrder = "提现订单列表" + FinancialTransferOrder = "财务转账列表" + TradeManageWithdrawType = "提现方式配置" + TradeManageWithdrawBank = "提现银行配置" + TradeManageWithdrawRefund = "提现人工退款" + RejectTemplate = "审核拒绝模板" + + AdsManageAdsList = "广告列表" + AdsManageAnnousList = "公告管理" + AdsManageAnnounceList = "会员中心跑马灯管理" + SystemManageVersion = "版本管理" + ActiveManageList = "活动列表" + IPBlockList = "IP限制列表" + IPWhiteList = "IP白名单列表" + IPWhiteRedisKey = "white_ip_" + SwitchList = "开关量列表" + AutoAccount = "自助结算审核" + ExchangeCode = "兑换码管理" + MerchantAdmin = "代充商人管理" + Disc = "商区" + GoldConfig = "支付优惠配置" + FreeVidConfig = "免费观看配置" + RoleConfig = "角色配置" + JuShangManageList = "聚商管理" + NewsModel = "嫩模活动" + RewardList = "获奖列表" + Loufeng = "楼风" + VerifyReport = "验证报告" + Fiction = "电子书小说" + Audiobook = "有声小说" + VIPConfig = "VIP配置" + AiUndressList = "AI脱衣列表" + IntegralConfig = "积分配置列表" + IntegralExchange = "积分兑换列表" + OfficialConfig = "官方配置" + AiChangefaceVidMod = "AI换脸视频模版" + AiChangeface = "AI视频换脸" + AiChangeFaceImgList = "AI图片换脸列表" + Section = "专题列表" + VideoGoldCoinList = "金币视频列表" + AdvanceConfig = "预售配置列表" + OfficialWebsiteConfig = "官网配置" +) + +// web操作方式 +const ( + Add = "新增" + Delete = "删除" + Modify = "修改" +) + +// 推荐维度 +const ( + ChosenVideo = "chosenVideo" + TagVideo = "tagVideo" + SameCityVideo = "sameCityVideo" + NewVideo = "newVideo" + UnPopularVideo = "unPopularVideo" + ForcePushSP = "forcePushSP" //强推视频纬度 + ChargeVideo = "chargeVideo" +) + +// IpBlock 类型 +const ( + BlockComment = "comment" + FrequencyComment = "frequencyComment" // 评论频率 + Register = "login" //限制用户注册 + GlobalBlock = "global" //全局限制该IP用户访问 +) + +// switch 落地页按钮类型 +type SwitchAct string + +const ( + SwitchIosEnterprise SwitchAct = "iosEnterprise" + SwitchIosStore SwitchAct = "iosStore" //商店包(TF包) + SwitchAndroid SwitchAct = "android" +) + +// switch 落地页按钮样式 +type SwitchStyle string + +const ( + SwitchStress SwitchStyle = "switchStress" //突出的开关风格 + SwitchSimple SwitchStyle = "switchSimple" //简单的开关风格 +) + +// 机器人的uid最大值 +const ( + RobotUIDLimit = 111999 +) + +// DeleteUID +const ( + DeleteUID uint64 = 100008 +) + +const ( + UserLowestTrueScore int64 = 10 +) + +type DomainNameStatus int64 + +const ( + Normal DomainNameStatus = iota + WxBlock +) +const ( + RCHG_Mode_SDK = "sdk" + RCHG_Mode_URL = "url" + RechargeAmtTolerance = 500 +) +const ( + MongoRandomSpareLen int = 5 +) + +const ( + Media_Del = 1 //"媒体删除" + + Media_Edit_Status = 2 //"媒体更新状态" + + Media_Edit_Price = 3 //"媒体更新价格" + + Media_Edit_Content = 4 //"媒体更新标题内容" + + Media_Edit_Remark = 5 //"媒体更新备注" + + User_INCoin = 6 //"用户追加金币" + + User_Edit_VIP_Type = 7 //"用户更新VIP类型" + + User_Edit_VIP_PromoteEnd = 8 //"用户更新VIP新推广赠送免费到期时间" + + User_Edit_VIP_Expire = 9 //"用户更新VIP过期时间" + + User_Edit_VIP_RechargeLevel = 10 //"用户更新VIP充值等级" + + User_INFruitCoin = 11 //"用户追加果币" + + User_ReduceAmount = 12 //"回收用户金币" + + User_ReduceIncome = 13 //"回收用户收益" + + User_INAiMateBalance = 14 // 用户追加ai伴侣币 + + User_INIntegral = 15 //"用户追加积分" +) + +const ( + FakeMobilePrefix string = "+86122" +) +const ( + MediaSourceSP string = "sp" + MediaSourceSPPrefixPath string = "/sp" + MediaSourcePMS string = "pms" + MediaSourcePMSPrefixPath string = "/pms" + ImageSourceIMS string = "ims" + ImageSourceIMSPrefixPath string = "/ims" + MediaSourceJH1B string = "jh1b" // 嘉华1部 + MediaSourceLaoSiJi string = "laosiji" // 老司机 + MediaSourceAuthKey string = "fT5xSg4hltHpzVy6aV9rVECDJ1J1pN" +) + +// DefaultTsAuthKeyVersion 未配置密钥版本时的默认版本标识,随 TS URL 以 v={keyVersion} 下发。 +const DefaultTsAuthKeyVersion = "default" + +// TsAuthKeyConfig 媒体分片(TS)鉴权签名密钥配置。 +type TsAuthKeyConfig struct { + KeyVersion string `json:"keyVersion"` // 密钥版本,随 TS URL 以 v={keyVersion} 下发 + Key string `json:"key"` // 签名密钥 +} + +// Resolve 返回生效的密钥版本与签名密钥: +// 仅当未配置 Key(空或纯空白)时,才回退到内置默认——版本 DefaultTsAuthKeyVersion、密钥 MediaSourceAuthKey; +// 配置了 Key 时,版本与密钥均按配置原样返回(版本可为空)。 +func (c TsAuthKeyConfig) Resolve() (keyVersion, key string) { + if strings.TrimSpace(c.Key) == "" { + return DefaultTsAuthKeyVersion, MediaSourceAuthKey + } + return strings.TrimSpace(c.KeyVersion), c.Key +} + +type Terminal = string //终端型号 + +const ( + TerminalAndroid Terminal = "0" + TerminalH5 Terminal = "1" + TerminalWeb Terminal = "2" +) + +// UIThemeEnum UI主题枚举 +type UIThemeEnum int + +const ( + ThemeDefault UIThemeEnum = 0 // 默认主题 + ThemeNewYear UIThemeEnum = 1 // 新春主题 +) diff --git a/common/constant/imnotifyconst/imnotifyconst.go b/common/constant/imnotifyconst/imnotifyconst.go new file mode 100644 index 0000000..eb26c8d --- /dev/null +++ b/common/constant/imnotifyconst/imnotifyconst.go @@ -0,0 +1,136 @@ +package imnotifyconst + +import "fmt" + +//im 通知 模块 + +// Action 消息行为 +type Action string + +// Message 消息结构体 +type Message struct { + Title string `json:"title"` //消息标题 + MsgType Action `json:"msgType"` //消息类型 + Data string `json:"data"` //内容 +} + +// 消息类型 +const ( + AccountCharge Action = "CHARGE" //充值消息 + VIPCharge Action = "VIP-CHARGE" //VIP充值消息 + BuyModel Action = "BUY—MODEL" //购买嫩模 + AccountWithDraw Action = "WITHDRAW" //提现消息 + VideoCheck Action = "VIDCHECK" //视频审核 + BlockUser Action = "BLOCK" //用户禁止登陆 + BannedUser Action = "BANNED" //用户禁言 + UnBannedUser Action = "UNBANNED" //解除禁言 + ForbidUpload Action = "FORBIDUPLOAD" //禁止上传文件 + UnForbidUpload Action = "UNFORBIDUPLOAD" //解除禁止上传文件 + SysOpUserCoins Action = "SysOpUserCoins" //系统操作用户金币 + OfficialRecharge Action = "OfficialRecharge" //官方充值 + OfficialSeekScore Action = "OfficialSeekScore" //官方追分 + MeetingCard Action = "MeetingCard" //购买约会卡 + UserReward Action = "UserReward" //玩家打赏通知 + OtherCard Action = "OtherCard" //购买其他卡 + UserFeedBackReply Action = "UserFeedBackReply" //用户反馈回复 + LoufengFeedBackReply Action = "LoufengFeedBackReply" //楼凤举报回复 + VerifyReport Action = "VerifyReport" //验证报告回复 + UnBlockUser Action = "UNBLOCK" //解除用户禁止登陆 + WithDrawRefuse Action = "WithDrawRefuse" //提现拒绝消息 + WithDrawUnknownErr Action = "WithDrawUnknownErr" //提现未知错误 + GrandPrize Action = "GrandPrize" //大奖通知 + ConsumerFeedbackGame Action = "ConsumerFeedbackGame" //充值有礼(游戏) + ConsumerFeedback Action = "ConsumerFeedback" //消费回馈(楼凤) + NudeChatFeedBackReply Action = "NudeChatFeedBackReply" //裸聊举报回复 + SysOpUserFruitCoin Action = "SysOpUserFruitCoin" //系统操作用户果币 +) + +var imNotifyTitle = map[Action]string{ + VIPCharge: "VIP购买", + BuyModel: "购买嫩模", + AccountCharge: "充值到账", + AccountWithDraw: "提现到账", + VideoCheck: "视频审核", + BlockUser: "禁止登陆", + BannedUser: "禁言通知", + UnBannedUser: "解除用户禁言", + ForbidUpload: "禁止上传帖子", + UnForbidUpload: "解除禁止上传帖子", + SysOpUserCoins: "系统金币管理", + OfficialRecharge: "官方充值", + OfficialSeekScore: "官方追分", + MeetingCard: "购买约会卡", + UserReward: "玩家打赏", + UserFeedBackReply: "官方回复", + OtherCard: "购买其他卡", + LoufengFeedBackReply: "楼凤举报", + VerifyReport: "验证报告", + UnBlockUser: "解除封禁", + WithDrawRefuse: "提现拒绝", + WithDrawUnknownErr: "提现未知错误", + GrandPrize: "中奖通知", + ConsumerFeedbackGame: "充值有礼", + ConsumerFeedback: "消费回馈", + NudeChatFeedBackReply: "裸聊举报", +} + +var imNotifyContent = map[Action]string{ + VIPCharge: "官人,恭喜您购买%s成功,小娘子在此等候您的光临哦!", + BuyModel: "官人,恭喜您%s,小娘子在此等候您的光临哦!", + AccountCharge: "充值成功到账%s金币,请查收!", + AccountWithDraw: "提现成功到账%s金币,请查收!", + VideoCheck: "您的视频%s因%s审核未通过!", + BlockUser: "尊敬的用户,您因:%s,现做出封禁处理,如需要解除封禁,请联系客服咨询,谢谢!", + BannedUser: "尊敬的用户,您因:%s,受到系统禁言处理,如需要解除禁言,请联系客服咨询,谢谢!", + UnBannedUser: "尊敬的用户,恭喜您解除禁言,现在您可以畅所欲言啦!", + ForbidUpload: "尊敬的用户,您因:%s,系统作出禁止上传文件处理,如需要解除限制,请联系客服咨询,谢谢!", + UnForbidUpload: "尊敬的用户,恭喜您解除禁止上传帖子,现在您可以上传帖子,痛快的赚取收益啦!", + SysOpUserCoins: "尊敬的用户,系统%s您%s金币,请注意查看,谢谢!", + OfficialRecharge: "尊敬的用户,您已经通过官方充值到账%s金币,请注意查看,谢谢!", + OfficialSeekScore: "尊敬的用户,官方追分%s金币,如有疑惑,请联系客服!", + MeetingCard: "官人,恭喜您购买%s成功,小娘子在此等候您的光临哦!", + UserReward: "玩家%s打赏%s金币,请查收!", + OtherCard: "官人,恭喜您购买%s成功,小娘子在此等候您的光临哦!", + UserFeedBackReply: "尊敬的用户!针对您的问题:%s,官方在此作出认真解答:%s,如果您还有疑惑,请联系客服或继续反馈,谢谢您对我们的支持!", + LoufengFeedBackReply: "尊敬的用户!针对你举报的楼凤信息,官方已作出相应处理:%s,给您造成的不便,深感抱歉!", + VerifyReport: "尊敬的用户!您提交的关于%s验证报告因%s审核未通过!", + UnBlockUser: "尊敬的用户,恭喜您解除封禁.%s", + WithDrawRefuse: "尊敬的用户,你的提现申请因-s%已失败,金币已返还.", + WithDrawUnknownErr: "尊敬的用户,你的提现申请因网络异常原因发生异常,请联系客服确认金币是否返还", + GrandPrize: "官人,恭喜您%s,小娘子在此等候您的光临哦!", + ConsumerFeedbackGame: "恭喜您昨日游戏累计楼充值%s元,已满足游戏充值有礼领取条件,快点击本消息领取吧~", + ConsumerFeedback: "恭喜您今日累计楼凤消费%s金币,已满足消费回馈领取条件,快点击本消息领取吧~", + NudeChatFeedBackReply: "尊敬的用户!针对你举报的裸聊信息,官方已作出相应处理:%s,给您造成的不便,深感抱歉!", + SysOpUserFruitCoin: "尊敬的用户,系统%s您%s果币币,请注意查看,谢谢!", +} + +func (a Action) Title() string { + if v, ok := imNotifyTitle[a]; ok { + return v + } + return "" +} + +// 适用于无字符串字符的fmt模版 +func (a Action) Desc() string { + if v, ok := imNotifyContent[a]; ok { + return v + } + return "" +} + +// 适用于单个字符串字符的fmt模版 +func (a Action) SingleStringParamData(amount string) string { + if v, ok := imNotifyContent[a]; ok { + return fmt.Sprintf(v, amount) + } + return "" +} + +// 适用于双个字符串字符的fmt模版 +func (a Action) DoubleStringParamData(p1 string, p2 string) string { + if v, ok := imNotifyContent[a]; ok { + return fmt.Sprintf(v, p1, p2) + } + return "" +} diff --git a/common/constant/redisconst/most_new_module_test.go b/common/constant/redisconst/most_new_module_test.go new file mode 100644 index 0000000..241ed65 --- /dev/null +++ b/common/constant/redisconst/most_new_module_test.go @@ -0,0 +1,10 @@ +package redisconst + +import "testing" + +func TestMostNewModuleVideoListKeyKeepsOriginalFormat(t *testing.T) { + got := GetMostNewModuleVideoListKey(1, 1, 10) + if want := "mostNewModuleVideoList:1:1:10"; got != want { + t.Fatalf("cache key = %q, want %q", got, want) + } +} diff --git a/common/constant/redisconst/redisconst.go b/common/constant/redisconst/redisconst.go new file mode 100644 index 0000000..8ff1a1d --- /dev/null +++ b/common/constant/redisconst/redisconst.go @@ -0,0 +1,688 @@ +package redisconst + +import ( + "91porn-server/common/crypt" + "91porn-server/common/timeutil" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" + "encoding/json" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + Db0ExpiredChannel = "__keyevent@0__:expired" + + userTokenFmt = "token:user:%d" + UserTokenExpire = time.Hour + + imSDKUserTokenFmt = "im:sdk:userToken:%d" // IM SDK 用户 token 缓存(按 imUserID) + IMSDKUserTokenExpire = 12 * time.Hour + + imSDKEnsureLockFmt = "im:sdk:ensure:lock:%d" // EnsureSDKUser 按 uid 加锁,防并发重复注册 + + IMSDKAppTokenKey = "im:sdk:appToken" // IM SDK appToken 缓存(内存 L1 + redis L2) + + MobileCaptchaMaxCountInMinute = 1 // 1分钟内同一手机号只能发送一次验证码 + MobileCaptchaMaxCountInHour = 5 // 1小时内同一手机号只能发送五次验证码 + MobileCaptchaMaxCountInDay = 10 // 1天内同一手机号只能发送十次验证码 + mobileCaptchaFmt = "captcha:mobile:%s" + MobileCaptchaExpire = 5 * time.Minute + + smsCaptchaIPFmt = "sms:captcha:ip:%s" + smsCaptchaUIDFmt = "sms:captcha:uid:%d" + smsCaptchaPhoneNumberFmt = "sms:captcha:number:%s" // sms:captcha:number:mobile:xxx 用于统计手机号发送验证码的次数 + smsCaptchaUIDCooldownFmt = "sms:captcha:uid:cd:%d" + SMSCaptchaUIDCooldown = 1 * time.Minute + + emailCaptchaFmt = "captcha:email:%s" + EmailCaptchaExpire = 5 * time.Minute + + traderChatCacheFmt = "trader:cache:%d:%d" + TraderChatCacheExpire = 2 * time.Minute + + withdrawCfgFmt = "withdraw:cache:cfg" + WithdrawCfgExpire = 8 * time.Hour + + withdrawTariffFmt = "withdraw:tariff" + WithdrawTariffExpire = 8 * time.Hour + + webTokenFmt = "token:%s:%s" //token:admin:xxx, token:channel:xxx + WebTokenExpire = 10 * time.Hour + + visitLogsFmt = "visitlogs:%s" //visitlog:20301020 + + visitFmt = "visitFmt:%s" //统计访问频率 + VisitExpireMax = 1 * time.Hour + + registFmt = "registFmt:%s" //用户注册频率 + RegistExpireMax = 1 * time.Hour + + rankFmt = "rankFmt:%s" + RankExpireMax = 20 * time.Minute + + toneFmt = "toneFmt" + ToneExpireMax = 20 * time.Minute + + ipLocationFmt = "ip:city:%s" //ip:city:124.168.1.0.248 缓存ip + IPLocationExpireMax = 24 * time.Hour + + ipBlockFmt = "ip:block:%s" // ip:block:comment 用于存放限制发表评论的ip + IPBlockExpire = 7 * 24 * time.Hour + + ipAutoBlockFmt = "ip:auto:%s:%s" // ip:block:comment 用于自动限制基于IP的接口访问 + autoBlockFmt = "auto:%s:%d" // ip:block:comment 用于自动限制基于次数的接口访问 + + dataCachFmt = "data:%s:%s" //visitlog:20301020:uid + DataCachExpire = time.Minute * 2 + + m3u8CacheFmt = "video:m3u8:%s" //video:m3u8:b0ac9b27eec64abebfc4c65cc4b89d03 m3u8键 //用于缓存添加相对的路径的m3u8文件 + m3u8PureCacheFmt = "video:m3u8:pure:%s" //video:m3u8:b0ac9b27eec64abebfc4c65cc4b89d03 m3u8键 //用于缓存原生的m3u8文件 + m3u8H5CacheFmt = "video:m3u8:h5:%s" //video:m3u8:b0ac9b27eec64abebfc4c65cc4b89d03 m3u8键 //用于缓存原生的m3u8文件 + M3u8CacheExpire = 1 * time.Hour + + m3u8HourlyCountFmt = "m3u8:hourly:count:%d" // 每用户每小时 m3u8 请求次数 + M3u8HourlyCountExpire = 1 * time.Hour + + // M3u8H5RefererSet H5 m3u8 请求来源计数(Redis ZSet):member=来源 scheme://host(Referer 缺省回退 Origin), + // score=累计请求次数。ZScore 查单个来源总数,ZRevRangeWithScores 看 Top 来源;每天凌晨 5 点(Asia/Shanghai) + // 过期清零、从零重新累计(刷回时 EXPIREAT 到下一个 5 点,见 updownctrl.FlushM3u8RefererStats)。 + M3u8H5RefererSet = "m3u8:h5:referer" + + CdnCacheKey = "cdn:cache:url" //cdn域名缓存 + CdnCacheExpire = 2 * time.Minute + + NewRegisterBehaFmt = "user:new:register:%d" //用户行为记录 + NewRegisterBehaCountFmt = "user:new:register:%d:%s" //用户行为次数记录 + NotifyKeyFmt = "notify:user:new:register:%d" //过期键通知 + NotifyKeyExpire = 20 * time.Minute //专门设置的通知键 + NewRegisterBehaExpire = 30 * time.Minute //用户 + NewRegisterBehaCountExpire = 30 * time.Minute //用户 + + replayNonceFmt = "replay:nonce:%s" //接口调用sign值 + + tagGroup = "taggroup" + TagGroupExpireMax = time.Minute + + DailyConsumeReward = "dailyConsumeReward:" + + PayableAgentsKey = "payableAgents" + PayableAgentsExpire = 5 * time.Minute + + MarqueeKey = "marquee" + + playLeaderboardKey = "leaderboard:play:%s:%d" + playLeaderboardKeyExpire = 24 * time.Hour + + NameSource = "name:source" //姓名池 + NameSourceExpire = 2 * 24 * time.Hour + + AdNewsListKeyFmt = "video:news:ad:%d" //广告ID列表 + AdNewsListKeyExpire = time.Hour + + LandDomainCacheKey = "domain:land" //落地页/推广域名 Key + LandDomainCacheExpire = 2 * time.Hour + + userInfo = "user:info:%d" + userInfoExpire = 20 * time.Minute + + tagInfo = "tag:info:%s" + tagInfoExpire = 20 * time.Minute + + videoInfo = "video:info:%s" + videoInfoExpire = 30 * time.Minute + + newestNews = "newest:news:%s" + newestNewsExpire = 5 * time.Minute + + userRechargeLimit = "userRechargeLimit:%d" //token:admin:xxx, token:channel:xxx + userRechargeLimitExpire = 6 * time.Second + + userReqLimit = "userRechargeLimit:%d:%s" //token:admin:xxx, token:channel:xxx + userReqLimitExpire = 1 * time.Second + + userWithdrawLimit = "userWithdrawimit:%d" //userWithdrawimit:xxx + userWithdrawLimitExpire = 60 * time.Second + + promotionCodeSetKey = "promotion_code_set" //推广码set key值 + + userLikeRateLimitKey = "userLike:%d" + userLikeRateLimitExpire = 6 * time.Second + + userBuyVidRateLimitKey = "userBuyVid:%d" + userBuyVidRateLimitExpire = 6 * time.Second + + initialPopKey = "vid:initialPopularity" + + tagSortVidKey = "tagSortVid:%s:%v:%v:%d:%d" + tagSortVidExpire = 30 * time.Minute + + userCollectionKey = "vid:userCollection:%d:%d:%d:%s:%d" + userCollectionExpire = 5 * time.Minute + + vidPageViewListKey = "vidPageViewList:%d" + vidPageViewListExpire = 30 * time.Minute + + waliPlayerMacSet = "waliPlayerMacSet:%s" + waliPlayerMacExpire = 24 * 7 * time.Hour + waliPlayerGlobalID = "waliPlayerGlobalIDSet:%s" + waliPlayerGlobalIDExpire = 24 * 7 * time.Hour + waliPlayerIP = "waliPlayerIPSet:%s" + waliPlayerIPExpire = 24 * time.Hour + + WithdrawOrderKey = "withdraw:order:processing" // 提现处于审核中的用户 + WithdrawOrderDuration = 15 * time.Minute // 过滤用户窗口时间 + + RedsyncUserKey = "redsync:user:%d" // 用户维度分布式锁 + AiMateLoginLockKey = "redsync:ai-mate-login:%d" + AiFundLockKey = "redsync:ai-fund:%d" + RedsyncUserExpiration = 10 * time.Second // 用户维度分布式锁过期时间 + + ContentUpdateMarkersCache = "content:update-markers:v1" + ContentUpdateMarkersCacheExpire = 30 * time.Second + + CenterAdvertiseCache = "centerAdvertiseCache" // 数据中心广告缓存 + CenterVersionCache = "centerVersionCache:%s:%s:%s:%s" // 数据中心版本缓存 + + VidAdKey = "video:ad" //广告ID列表 + VidAdKeyExpire = time.Minute * 2 + + active2023RedisKey = "active_2023:%d" + active2023Expired = time.Minute + + // 幸福广场缓存 + ImageTopKey = "imageTop:filter:%d" + ImageTopExpire = time.Minute * 5 + + userDailyTaskRewardLockKey = "userDailyTask:%d:%s" + userDailyTaskRewardLockExpire = time.Minute + + // 缓存用户每日完成数量. 比如点击了x次广告 + userDailyTaskCountKey = "userDailyTaskCount:%d:%d:%s" + + // 缓存用户每日领取奖励次数. + userDailyTaskRewardTimeKey = "userDailyTaskRewardTime:%d:%d:%s" + + // 缓存用户一次性任务完成情况 + userOnceTaskCompleteKey = "userOnceTaskComplete:%d:%d" + userOnceTaskCompleteExpire = time.Hour * 24 + ModulesCache = "modulesCache" // 模块缓存 + dailyTaskCacheFmt = "dailyTask:%v:%v:%v" // 每日任务缓存 + InviteCache = "inviteCache:uid:%v" // 推广数据缓存 + PublishTagInfoCache = "publishTagInfoCache" // 发布标签缓存 + ModuleVideoInfoCache = "moduleVideoInfoCache:mid:%v:%v:%v:%v:%v:%v" // 视频模块缓存 + SectionVideoInfoCache = "sectionVideoInfoCache:mid:%v:sortType:%v:pageSize:%v:pageNumber:%v" // 视频模块缓存 + AIModCache = "aiModCache" // AI模版缓存 + GetAwVipInfo = "getAwVipInfo" // AWVip信息缓存 + checkApiKeyFmt = "checkApi:ip:%s:api:%s:s:%v" // 接口调用 + SensitiveWordsCache = "sensitiveWordsCache" // 敏感词库缓存 + GameAdvanceCache = "gameAdvanceCache:%v" // 游戏预售缓存 + AdvanceCache = "hj_advanceCache:%v" // 预售缓卡存 + CheckApiKeyExpire = 1 * time.Minute + RecommendVip = "RecommendVip" // VIP推荐展示 + RecommendVipExpire = 2 * time.Minute + DiscountArea = "DiscountArea" // 折扣专区列表缓存 + DiscountAreaExpire = 2 * time.Minute + shortVideoListCache = "shortDiscoverList:types:%d:tagId:%s:page:%v_%v" // 短视频发现模块数据换粗 + ShortVideoListCacheExpire = time.Minute * 10 // 短视频发现模块数据换粗 + + mediaTagInfoExpire = 20 * time.Minute + mediaTagInfo = "mediaTag:info:%s" // 动漫标签缓存 + acgBrowseCountListKey = "acgBrowseCountList:%d" + MediaLibraryInfo = "mediaLibraryInfo:info" // 动漫片库缓存 + ShortVideosKey = "short-videos-all-ids-list" // ShortVideosKey 短视频ID列表 - 用于推荐 + VideoLibraryCache = "videoLibraryCache" // 片库缓存 + ShortVideosRecoCacheKey = "shortVideosRecoCache" // 短视频-推荐 ID列表 - 用于短视频推荐 + followUpUsersAndSHORTKey = "follow:%d:SHORT:%d_%d" + + mostNewModuleVideoListKey = "mostNewModuleVideoList:%v:%v:%v" // 91porn 首页最新视频列表缓存key + MostNewModuleVideoListExpire = time.Minute * 10 + + MonitorCacheKey = "rs_monitorCache" // 监控缓存 + MonitorExpired = time.Minute * 4 + + OfficialWebsiteBasicDataCacheKey = "officialWebsiteBasicDataCache" // 官方网站基础数据缓存 + OfficialWebsiteBasicDataCacheExpire = time.Minute * 10 + OfficialWebsiteAlbumListCacheKey = "officialWebsiteAlbumListCache:%d:%d" // 官方网站专辑列表缓存 + OfficialWebsiteAlbumListCacheExpire = time.Minute * 10 + OfficialWebsiteHeroListCacheKey = "officialWebsiteHeroListCache:%d:%d:%d" // 官方网站演员列表缓存 + OfficialWebsiteHeroListCacheExpire = time.Minute * 10 + OfficialWebsiteVideoListCacheKey = "officialWebsiteVideoListCache:%d:%s:%d:%d:%d" // 官方网站视频列表缓存 + OfficialWebsiteVideoListCacheExpire = time.Minute * 10 + OfficialWebsiteNewsListCacheKey = "officialWebsiteNewsListCache:%d:%d" // 官方网站资讯列表缓存 + OfficialWebsiteNewsListCacheExpire = time.Minute * 10 + OfficialWebsitePartnerListCacheKey = "officialWebsitePartnerListCache" // 官方网站合作伙伴列表缓存 + OfficialWebsitePartnerListCacheExpire = time.Minute * 10 +) + +const ( + NavigateRecreationCache = "navigateRecreationCache" // 导航站娱乐广告缓存 + UserPaymentStatusPopupCache = "UserPaymentStatusPopup" // 用户分层弹窗配置缓存 +) + +const ( + aiFreeUndressTodayUseTimes = "aiFreeUndressTodayUseTimes:%v" +) +const ( + JanGangQuCacheKey = "jingangqu" + JanGangQuCacheExpire = 10 * time.Minute +) +const ( + rankingListCacheKey = "ranking:%v:%v:%d:%d" + hotRankingListCacheKey = "hotRanking:%v:%v:%v" + RankingListCacheExpire = 10 * time.Minute +) + +// 渠道订阅的 消息体 +type RecommedPayload struct { + UID uint64 `json:"uid"` + City string `json:"city"` + Page int `json:"page"` +} + +func GetFollowUpUsersAndSHORTKey(uid uint64, pageSize, pageNumber uint64) (string, time.Duration) { + return fmt.Sprintf(followUpUsersAndSHORTKey, uid, pageSize, pageNumber), time.Minute * 10 +} + +func GetMonitorCacheExpired() time.Duration { + return MonitorExpired +} + +func AcgBrowseCountListKey(timestamp int64) string { + return fmt.Sprintf(acgBrowseCountListKey, timestamp) +} + +func MediaTagInfoExpire() time.Duration { + return mediaTagInfoExpire +} + +func MediaTagInfoKey(tid string) string { + return fmt.Sprintf(mediaTagInfo, tid) +} + +func AiFreeUndressTodayUseTimesKey(uid uint64) string { + return fmt.Sprintf(aiFreeUndressTodayUseTimes, uid) +} + +func UserTokenKey(uid uint64) string { + return fmt.Sprintf(userTokenFmt, uid) +} + +// IMSDKUserTokenKey IM SDK 用户 token 缓存 key(按 imUserID) +func IMSDKUserTokenKey(imUserID int64) string { + return fmt.Sprintf(imSDKUserTokenFmt, imUserID) +} + +// IMSDKEnsureLockKey EnsureSDKUser 并发注册锁 key(按 uid) +func IMSDKEnsureLockKey(uid uint64) string { + return fmt.Sprintf(imSDKEnsureLockFmt, uid) +} + +func GameAdvanceKey(uid uint64) string { + return fmt.Sprintf(GameAdvanceCache, uid) +} + +func AdvanceKey(uid uint64) string { + return fmt.Sprintf(AdvanceCache, uid) +} + +// InviteCacheKey 推广数据 +func InviteCacheKey(uid uint64) string { + return fmt.Sprintf(InviteCache, uid) +} + +func CheckApiKey(ip, apiPath string, second time.Time) string { + return fmt.Sprintf(checkApiKeyFmt, ip, apiPath, second) +} + +// SectionVideoInfoCacheKey 模块视频Key +func SectionVideoInfoCacheKey(sid string, sortType string, pageSize, pageNumber int64) string { + return fmt.Sprintf(SectionVideoInfoCache, sid, sortType, pageSize, pageNumber) +} + +func DailyTaskCacheKey(uid uint64, taskType any, today time.Time) string { + return fmt.Sprintf(dailyTaskCacheFmt, taskType, uid, today) +} + +func MobileCaptchaKey(mobile string) string { + return fmt.Sprintf(mobileCaptchaFmt, mobile) +} + +func SMSCaptchaIPKey(ip string) string { + return fmt.Sprintf(smsCaptchaIPFmt, ip) +} + +func SMSCaptchaIPExpire() time.Duration { + year, month, day := time.Now().Date() + return time.Until(time.Date(year, month, day+1, 0, 0, 0, 0, time.Local)) +} + +func SMSCaptchaUIDKey(uid uint64) string { + return fmt.Sprintf(smsCaptchaUIDFmt, uid) +} + +func SMSCaptchaUIDCooldownKey(uid uint64) string { + return fmt.Sprintf(smsCaptchaUIDCooldownFmt, uid) +} + +func SMSCaptchaPhoneNumberKey(mobile string) string { + return fmt.Sprintf(smsCaptchaPhoneNumberFmt, mobile) +} + +func WebTokenKey(typ string, act string) string { + return fmt.Sprintf(webTokenFmt, typ, act) +} + +func UserAdNewsListKey(uid uint64) string { + return fmt.Sprintf(AdNewsListKeyFmt, uid) +} + +func VisitKey(t time.Time) string { + recentMinute := timerange.RecentMinute(t, statrecordmod.FiveMinuteScale) //对齐本次统计时间 Minute % frequency == 0 + return fmt.Sprintf(visitFmt, recentMinute.Format("200601021504")) +} + +func RegistKey(t time.Time) string { + recentMinute := timerange.RecentMinute(t, statrecordmod.FiveMinuteScale) //对齐本次统计时间 Minute % frequency == 0 + return fmt.Sprintf(registFmt, recentMinute.Format("200601021504")) +} + +func NotifyKey(uid uint64) string { + return fmt.Sprintf(NotifyKeyFmt, uid) +} + +func RegistBehaviorKey(uid uint64) string { + return fmt.Sprintf(NewRegisterBehaFmt, uid) +} + +func RegistBehaviorCountKey(uid uint64, req string) string { + return fmt.Sprintf(NewRegisterBehaCountFmt, uid, req) +} + +func RankKey(rankType string) string { + return fmt.Sprintf(rankFmt, rankType) +} + +func ToneKey() string { + return toneFmt +} + +func UserVisitLogsKey(t time.Time) string { + return fmt.Sprintf(visitLogsFmt, t.Format("20060102")) +} + +func UserVisitLogExpire(now time.Time) time.Duration { + return timeutil.BeginningOfTomorrow(now).Sub(now) + 5*time.Minute +} + +func IpLocationFmt(ip string) string { + return fmt.Sprintf(ipLocationFmt, ip) +} + +func M3u8CacheFmt(m3u8 string) string { + return fmt.Sprintf(m3u8CacheFmt, m3u8) +} + +func M3u8PureCacheFmt(m3u8 string) string { + return fmt.Sprintf(m3u8PureCacheFmt, m3u8) +} +func M3u8H5CacheFmt(m3u8 string) string { + return fmt.Sprintf(m3u8H5CacheFmt, m3u8) +} + +func M3u8HourlyCountKey(uid uint64) string { + return fmt.Sprintf(m3u8HourlyCountFmt, uid) +} +func UserInfoKey() string { + return userInfo +} +func TagInfoKey() string { + return tagInfo +} +func VideoInfoKey() string { + return videoInfo +} + +func UserInfoExpire() time.Duration { + return userInfoExpire +} +func TagInfoExpire() time.Duration { + return tagInfoExpire +} +func VideoInfoExpire() time.Duration { + return videoInfoExpire +} +func DataCachKey(db string, m string) string { + return fmt.Sprintf(dataCachFmt, db, m) +} + +func IPBlockKey(blockType string) string { + return fmt.Sprintf(ipBlockFmt, blockType) +} + +func IPAutoBlockKey(blockType, ip string) string { + return fmt.Sprintf(ipAutoBlockFmt, blockType, ip) +} + +func AutoBlockKey(blockType string, uid uint64) string { + return fmt.Sprintf(autoBlockFmt, blockType, uid) +} + +func ReplayNonceKey(nonce string) string { + return fmt.Sprintf(replayNonceFmt, nonce) +} + +func TagGroup() string { + return tagGroup +} + +func TraderChatCacheKey(uid uint64, proT int) string { + return fmt.Sprintf(traderChatCacheFmt, uid, proT) +} + +func WithdrawCfgCacheKey() string { + return withdrawCfgFmt +} + +func WithdrawTariffCacheKey() string { + return withdrawTariffFmt +} + +func NewestNewsKey(t time.Time) string { + return fmt.Sprintf(newestNews, t.String()) +} + +func NewestNewsKey_old() string { + return "newest:news" +} + +func NewestShortVideoKey() string { + return "newest:shortvideo" +} + +func NewestNewsExpire() time.Duration { + return newestNewsExpire +} + +func RechargeLimtKey(uid uint64) string { + return fmt.Sprintf(userRechargeLimit, uid) +} +func RechargeLimtKeyExpire() time.Duration { + return userRechargeLimitExpire +} +func WithdrawLimtKey() string { + return userWithdrawLimit +} +func WithdrawLimtKeyExpire() time.Duration { + return userWithdrawLimitExpire +} +func PromotionCodeKey() string { + return promotionCodeSetKey +} +func WaLiIPSetKey(ip string) string { + return fmt.Sprintf(waliPlayerIP, ip) +} +func WaLiIPSetExpire() time.Duration { + return waliPlayerIPExpire +} +func WaLiMacSetKey(mac string) string { + return fmt.Sprintf(waliPlayerMacSet, mac) +} +func WaLiMacSetExpire() time.Duration { + return waliPlayerMacExpire +} +func WaLiDevSetKey(dev string) string { + return fmt.Sprintf(waliPlayerGlobalID, dev) +} +func WaLiDevSetExpire() time.Duration { + return waliPlayerGlobalIDExpire +} + +func GetRedsyncUserKey(uid uint64) string { + return fmt.Sprintf(RedsyncUserKey, uid) +} + +func GetAiMateLoginLockKey(uid uint64) string { + return fmt.Sprintf(AiMateLoginLockKey, uid) +} + +func GetAiFundLockKey(uid uint64) string { + return fmt.Sprintf(AiFundLockKey, uid) +} + +func GetUserLikeRateLimitKey() string { + return userLikeRateLimitKey +} + +func GetUserLikeRateLimitExpire() time.Duration { + return userLikeRateLimitExpire +} + +func GetEmailCaptchaKey(email string) string { + return fmt.Sprintf(emailCaptchaFmt, email) +} + +func GetUserBuyVidRateLimitKey() string { + return userBuyVidRateLimitKey +} + +func GetUserBuyVidRateLimitExpire() time.Duration { + return userBuyVidRateLimitExpire +} + +func GetInitialVideoPopularityKey() string { + return initialPopKey +} + +func GetTagSortVideoKey(tid string, newsType string, sort int, skip, limit uint64) string { + return fmt.Sprintf(tagSortVidKey, newsType, tid, sort, skip, limit) +} +func GetTagSortVideoExpire() time.Duration { + return tagSortVidExpire +} +func ReqLimtKey(uid uint64, m string) string { + return fmt.Sprintf(userReqLimit, uid, m) +} +func ReqLimtKeyExpire() time.Duration { + return userReqLimitExpire +} +func UserCollectionKey(uid uint64, pageNum, pageSize uint64, playTimeType int, sortType string) string { + return fmt.Sprintf(userCollectionKey, uid, pageNum, pageSize, sortType, playTimeType) +} +func UserCollectionExpire() time.Duration { + return userCollectionExpire +} + +func VidPageViewListKey(timestamp int64) string { + return fmt.Sprintf(vidPageViewListKey, timestamp) +} +func VidPageViewListExpire() time.Duration { + return vidPageViewListExpire +} + +func GetUserActive2023RedisKey(uid uint64) string { + return fmt.Sprintf(active2023RedisKey, uid) +} + +func GetUserActive2023Expred() time.Duration { + return active2023Expired +} + +func GetVersionRedisKey(a, b, c, d string) string { + return fmt.Sprintf(CenterVersionCache, a, b, c, d) +} + +func PlayLeaderboardKey(rT string, timestamp int64) string { + return fmt.Sprintf(playLeaderboardKey, rT, timestamp) +} +func PlayLeaderboardKeyExpire() time.Duration { + return playLeaderboardKeyExpire +} + +func GetUserDailyTaskRewardLockKey(uid uint64, taskId string) string { + return fmt.Sprintf(userDailyTaskRewardLockKey, uid, taskId) +} + +func GetUserDailyTaskRewardLockExpired() time.Duration { + return userDailyTaskRewardLockExpire +} + +// 缓存用户每日完成数量. 比如点击了x次广告 +func GetUserDailyTaskCountKey(uid uint64, taskType int64) string { + return fmt.Sprintf(userDailyTaskCountKey, uid, taskType, time.Now().Format("2006-01-02")) +} + +// 缓存用户每日领取奖励次数. +func GetUserDailyTaskRewardTimeKey(uid uint64, taskType int64) string { + return fmt.Sprintf(userDailyTaskRewardTimeKey, uid, taskType, time.Now().Format("2006-01-02")) +} + +func GetUserDailyTaskExpired() time.Duration { + year, month, day := time.Now().Date() + return time.Until(time.Date(year, month, day+1, 0, 1, 0, 0, time.Local)) // 多缓存1分钟 +} + +// 用户一次性任务是否完成. value: 0 未完成; 1 已完成 +func GetUserOnceTaskCompleteKey(uid uint64, taskType int64) string { + return fmt.Sprintf(userOnceTaskCompleteKey, uid, taskType) +} + +func GetUserOnceTaskCompleteExpired() time.Duration { + return userOnceTaskCompleteExpire +} + +const ( + videoShareList = "videoShareList" + videoShareExpired = time.Minute * 10 +) + +func GetVideoShareListKey() string { + return videoShareList +} + +func GetVideoShareListExpired() time.Duration { + return videoShareExpired +} + +func GetShortVideoListCacheKey(types int, tid string, page, pagesize uint64) string { + return fmt.Sprintf(shortVideoListCache, types, tid, page, pagesize) +} + +func GetRankingListExpired() time.Duration { + return RankingListCacheExpire +} +func GetRankingListCacheKey(v ...any) string { + return fmt.Sprintf(rankingListCacheKey, v...) +} + +func GetHotRankingListCacheKey(tagIds []primitive.ObjectID, pageNumber, pageSize uint64) string { + b, _ := json.Marshal(tagIds) + md5Str := crypt.ByteToMd5(b) + return fmt.Sprintf(hotRankingListCacheKey, md5Str, pageNumber, pageSize) +} + +func GetMostNewModuleVideoListKey(sortType int, page, pageSize uint64) string { + return fmt.Sprintf(mostNewModuleVideoListKey, sortType, page, pageSize) +} diff --git a/common/convUtil.go b/common/convUtil.go new file mode 100644 index 0000000..71fde78 --- /dev/null +++ b/common/convUtil.go @@ -0,0 +1,187 @@ +package common + +import ( + "encoding/json" + "path/filepath" + "reflect" + "strings" + "time" + + "91porn-server/common/constant" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ID = primitive.ObjectID + +// ToJsonM 将struct转换为 Json Map +func ToJsonM(obj interface{}) (map[string]interface{}, error) { + j, err := json.Marshal(obj) + if err != nil { + return nil, err + } + m := make(map[string]interface{}) + return m, json.Unmarshal(j, &m) +} + +// ToBsonM 将struct转换为 Bson Map +func ToBsonM(s interface{}) (bson.M, error) { + data, err := bson.Marshal(s) + if err != nil { + return nil, err + } + m := make(bson.M) + return m, bson.Unmarshal(data, &m) +} + +func MapToJsonString(m map[string]string) (string, error) { + data, err := json.Marshal(m) + if err != nil { + return "", err + } + return string(data), nil +} + +// BMapToStruct s is struct ptr +func BMapToStruct(s interface{}, m map[string]interface{}) error { + bsonBytes, err := bson.Marshal(m) + if err != nil { + return err + } + return bson.Unmarshal(bsonBytes, s) +} + +// JMapToStruct s is struct ptr +func JMapToStruct(s interface{}, m map[string]interface{}) error { + bsonBytes, err := json.Marshal(m) + if err != nil { + return err + } + return json.Unmarshal(bsonBytes, s) +} + +// JSONStruct2Map 将struct转换为Map +func JSONStruct2Map(obj interface{}) (map[string]interface{}, error) { + j, err := json.Marshal(obj) + if err != nil { + return nil, err + } + m := make(map[string]interface{}) + return m, json.Unmarshal(j, &m) +} + +// Map2JSONStruct 将map转换为Json struct +func Map2JSONStruct(v interface{}, m map[string]interface{}) error { + j, err := json.Marshal(m) + if err != nil { + return err + } + return json.Unmarshal(j, v) +} + +func IDArray(ids []string) ([]ID, error) { + idArray := make([]ID, len(ids)) + for i, id := range ids { + _id, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + idArray[i] = _id + } + return idArray, nil +} + +// 拼接https:// +func BindHttpSch(path string) string { + if strings.HasPrefix(path, "http") { + return path + } + return "https://" + path +} + +func BindUrl(hostname string, path ...string) string { + if hostname == "" { + return strings.TrimLeft(filepath.Join(path...), "/") + } + if len(path) == 0 { + return hostname + } + su := strings.TrimLeft(filepath.Join(path...), "/") + pr := strings.TrimRight(hostname, "/") + return pr + "/" + su +} + +func ObjectIDs2String(obj []primitive.ObjectID) []string { + if len(obj) == 0 { + return []string{} + } + arr := make([]string, len(obj)) + for i, o := range obj { + arr[i] = o.Hex() + } + return arr +} + +func String2ObjectID(obj []string) []primitive.ObjectID { + arr := make([]primitive.ObjectID, 0, len(obj)) + for _, o := range obj { + oid, err := primitive.ObjectIDFromHex(o) + if err != nil { + continue + } + arr = append(arr, oid) + } + return arr +} + +// PercentOf - calculate what percent [number1] is of [number2]. +// ex. 300 is 12.5% of 2400 +func PercentOf(part int, total int) float64 { + return (float64(part) * float64(100)) / float64(total) +} + +func MergeMap(src, dest map[string]interface{}) map[string]interface{} { + if len(src) == 0 { + return dest + } + if len(dest) == 0 { + return src + } + if len(src) == 0 && len(dest) == 0 { + return make(map[string]interface{}) + } + for k, v := range dest { + src[k] = v + } + return src +} + +func HandleSysType(sysType string) string { + if strings.Contains(strings.ToLower(sysType), constant.SysTypeIOS) { + return constant.SysTypeIOS + } + if strings.Contains(strings.ToLower(sysType), constant.SysTypeH5) { + return constant.SysTypeH5 + } + return constant.SysTypeAndroid +} + +func GetJsonTags(tags *[]string, t reflect.Type) { + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if field.Type.Kind() == reflect.Struct { + GetJsonTags(tags, field.Type) + continue + } + tag := field.Tag.Get("json") + *tags = append(*tags, tag) + } +} + +// NormalizeDate 将时间归一化为北京时间的当天 0 点 +func NormalizeDate(t time.Time) time.Time { + local, _ := time.LoadLocation("Asia/Shanghai") + t = t.In(local) + return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, local) +} diff --git a/common/crypt/checkSign.go b/common/crypt/checkSign.go new file mode 100644 index 0000000..e0196f5 --- /dev/null +++ b/common/crypt/checkSign.go @@ -0,0 +1,137 @@ +package crypt + +import ( + "91porn-server/common/log" + "91porn-server/web/webg" + "crypto/aes" + "crypto/cipher" + "encoding/hex" + "fmt" + "net/url" + "reflect" + "strings" +) + +//const appSecret = "iPEnB74mfCZhNNnY" //(客服后台->产品列表 对应产品的产品密钥) 测试 + +func CheckSign(sign string, data interface{}, router string) bool { + //对参数进行加密,和sign 进行对比 + dataMap, _ := StructToMap(data, router) + params := url.Values{} + for k, v := range dataMap { + params.Set(k, v) + } + mySign, err := GenerateSign(params.Encode(), webg.Conf.Customer.Secret) + if err != nil { + log.Error("CheckSign Generate signature error:", log.Any("Secret", webg.Conf.Customer.Secret), log.E(err)) + return false + } + return mySign == sign +} + +// StructToMap 将结构体转换为map[string]string +func StructToMap(data interface{}, router string) (map[string]string, error) { + result := make(map[string]string) + + // 使用反射获取结构体信息 + v := reflect.ValueOf(data) + t := reflect.TypeOf(data) + + // 如果是指针,获取指向的元素 + if t.Kind() == reflect.Ptr { + v = v.Elem() + t = t.Elem() + } + + // 确保是结构体类型 + if t.Kind() != reflect.Struct { + return nil, fmt.Errorf("data must be a struct or pointer to struct") + } + + // 遍历结构体字段 + for i := 0; i < v.NumField(); i++ { + field := t.Field(i) + value := v.Field(i) + + // 获取json标签作为key + jsonTag := field.Tag.Get("json") + if jsonTag == "" { + // 如果没有json标签,使用字段名 + jsonTag = strings.ToLower(field.Name) + } + if router != "" { + if jsonTag == "appId" || jsonTag == "sign" { + continue + } + } + // 获取字段值并转换为字符串 + var valueStr string + switch value.Kind() { + case reflect.String: + valueStr = value.String() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + valueStr = fmt.Sprintf("%d", value.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + valueStr = fmt.Sprintf("%d", value.Uint()) + case reflect.Bool: + valueStr = fmt.Sprintf("%t", value.Bool()) + case reflect.Float32, reflect.Float64: + valueStr = fmt.Sprintf("%f", value.Float()) + default: + valueStr = fmt.Sprintf("%v", value.Interface()) + } + + // 添加到结果中 + result[jsonTag] = valueStr + } + + return result, nil +} + +// PKCS7Padding PKCS7填充 +func PKCS7Padding_1(data []byte, blockSize int) []byte { + padding := blockSize - len(data)%blockSize + padtext := make([]byte, padding) + for i := range padtext { + padtext[i] = byte(padding) + } + return append(data, padtext...) +} + +// AESEncrypt AES-CBC加密 +func AESEncrypt(origData, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + // 使用密钥的前16字节作为IV + iv := key[:16] + + // PKCS7填充 + blockSize := block.BlockSize() + origData = PKCS7Padding_1(origData, blockSize) + + // CBC模式加密 + mode := cipher.NewCBCEncrypter(block, iv) + crypted := make([]byte, len(origData)) + mode.CryptBlocks(crypted, origData) + + return crypted, nil +} + +// GenerateSign 生成签名 +func GenerateSign(data, appSecret string) (string, error) { + key := []byte(appSecret) + plaintext := []byte(data) + + ciphertext, err := AESEncrypt(plaintext, key) + if err != nil { + return "", err + } + + // 转换为十六进制字符串 + hexStr := hex.EncodeToString(ciphertext) + + return hexStr, nil +} diff --git a/common/crypt/crypt.go b/common/crypt/crypt.go new file mode 100644 index 0000000..d3b0f61 --- /dev/null +++ b/common/crypt/crypt.go @@ -0,0 +1,755 @@ +package crypt + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/md5" + "crypto/rand" + "crypto/sha1" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "hash/crc32" + "io" + "net/url" + "os" + "reflect" + "sort" + "strconv" + "strings" + + "91porn-server/common/crypt/ecb" + sli "91porn-server/common/slice" + + "github.com/dgrijalva/jwt-go" +) + +// CreateToken 生成Token算法 +func CreateToken(secret string, tokenClaims map[string]interface{}) (tokeness string, err error) { + if secret == "" { + return "", errors.New("secret is empty") + } + claims := jwt.MapClaims(tokenClaims) + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(secret)) +} + +func keyFunc(secret string) jwt.Keyfunc { + return func(token *jwt.Token) (interface{}, error) { + return []byte(secret), nil + } +} + +// ParseToken 解析Token +func ParseToken(secret string, tokeness string) (map[string]interface{}, error) { + if secret == "" || tokeness == "" { + return nil, errors.New("secret or tokeness is empty") + } + token, err := jwt.Parse(tokeness, keyFunc(secret)) + if err != nil { + return nil, err + } + //验证token,如果token被修改过则为false + if !token.Valid { + return nil, errors.New("token is invalid") + } + claimsToken, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, errors.New("cannot convert claim to MapClaim") + } + return claimsToken, nil +} + +// StructToStr 结构体转json 先对结构体按照字典顺序排序 并返回字符串 +func StructToStr(obj interface{}) (string, error) { + if obj == nil { + return "", errors.New("obj is not nil") + } + data := make(map[string]interface{}) + bytes, err := json.Marshal(obj) + if err != nil { + return "", err + } + if err = json.Unmarshal(bytes, &data); err != nil { + return "", err + } + return MapToStr(data) +} + +func JsonStr2Str(str string, key ...string) string { + if str == "" { + return "" + } + mapstr, err := JSON2Map(str) + if err != nil { + return "" + } + sortStr, err := MapToStrSkipObject(mapstr, key...) + if err != nil { + return "" + } + return sortStr +} + +func Obj2Obj(src, dest interface{}) error { + data, err := json.Marshal(src) + if err != nil { + return err + } + return json.Unmarshal(data, dest) +} + +// MapToStr 对map按照字典顺序排序 并返回字符串 +func MapToStr(data map[string]interface{}) (string, error) { + if len(data) == 0 { + return "", errors.New("data is nil or empty") + } + newData := make(map[string]interface{}) + keys := make([]string, 0, len(data)) + for k := range data { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + newData[k] = data[k] + } + b, err := json.Marshal(newData) + if err != nil { + return "", err + } + return string(b), nil +} + +// MapToStr 对map按照字典顺序排序 并返回字符串 对value是对象 map list 类型的key 过滤掉 +func MapToStrSkipObject(data map[string]interface{}, key ...string) (string, error) { + if len(data) == 0 { + return "", errors.New("data is nil or empty") + } + newData := make(map[string]interface{}) + keys := make([]string, 0, len(data)) + for k := range data { + if sli.Contains(key, k) { + continue + } + t := reflect.TypeOf(data[k]) + if t != nil { + tkind := t.Kind() + if tkind == reflect.Map || tkind == reflect.Slice || tkind == reflect.Array || tkind == reflect.Struct { + continue + } + keys = append(keys, k) + } + } + sort.Strings(keys) + for _, k := range keys { + newData[k] = data[k] + } + b, err := json.Marshal(newData) + if err != nil { + return "", err + } + return string(b), nil +} + +// JSON2Struct json转struct +func JSON2Struct(str string, obj interface{}) error { + return json.Unmarshal([]byte(str), &obj) +} + +// JSONArray2Struct json转struct +func JSONArray2Struct(str []string, obj interface{}) []interface{} { + data := make([]interface{}, len(str)) + for i, v := range str { + _ = JSON2Struct(v, &obj) + data[i] = obj + } + return data +} + +// JSON2Map json转map +func JSON2Map(str string) (map[string]interface{}, error) { + var mapResult map[string]interface{} + return mapResult, json.Unmarshal([]byte(str), &mapResult) +} + +// StructToStrNormal 结构体转str +func StructToStrNormal(obj interface{}) (string, error) { + str, err := json.Marshal(obj) + if err != nil { + return "", err + } + return string(str), nil +} + +func UrlValueToStr(values url.Values, exclude string) (string, string) { + if values == nil { + return "", "" + } + var buf bytes.Buffer + var sign string + keys := make([]string, 0, len(values)) + for k := range values { + if k == exclude { + sign = strings.Join(values[k], "") + continue + } + keys = append(keys, k) + } + sort.Strings(keys) + for i, k := range keys { + if i != 0 { + buf.WriteString("&") + } + buf.WriteString(k) + buf.WriteString("=") + buf.WriteString(strings.Join(values[k], "")) + } + return buf.String(), sign +} + +// 对参数进行排序后 拼接成url字符串 只对一级字符串类型的参数做处理 +func MapToUrlOnlyStr(data map[string]interface{}) string { + if len(data) == 0 { + return "" + } + var buf bytes.Buffer + keys := make([]string, len(data)) + i := 0 + for k := range data { + keys[i] = k + i++ + } + sort.Strings(keys) + for i, k := range keys { + if i != 0 { + buf.WriteString("&") + } + buf.WriteString(k) + buf.WriteString("=") + if v, ok := data[k].(string); ok { + buf.WriteString(v) + } + } + return buf.String() +} + +// MapToStr 对map按照字典顺序排序 并返回字符串url 拼接的字段 +func MapToUrlStr(data map[string]interface{}) string { + if len(data) == 0 { + return "" + } + var buf bytes.Buffer + keys := make([]string, len(data)) + i := 0 + for k := range data { + keys[i] = k + i++ + } + sort.Strings(keys) + for i, k := range keys { + if i != 0 { + buf.WriteString("&") + } + buf.WriteString(k) + buf.WriteString("=") + if v, ok := data[k].(string); ok { + buf.WriteString(v) + continue + } + t := reflect.TypeOf(data[k]) + if t != nil && t.Kind() == reflect.Map { + if v, ok := data[k].(map[string]interface{}); ok { + buf.WriteString("{") + str := MapToUrlStr(v) + buf.WriteString(str) + buf.WriteString("}") + } + } + if t != nil && (t.Kind() == reflect.Slice || t.Kind() == reflect.Array) { + arraybyte, _ := json.Marshal(data[k]) + buf.WriteString(string(arraybyte)) + } + } + return buf.String() +} + +// MapToURL 集合转URL +func MapToURL(m map[string]string) string { + if len(m) == 0 { + return "" + } + v := url.Values{} + for k := range m { + v.Set(k, string(m[k])) + } + return v.Encode() +} + +func Strcut2UrlValue(obj interface{}) (string, error) { + t := reflect.TypeOf(obj) + v := reflect.ValueOf(obj) + if t.Kind() != reflect.Struct { + return "", errors.New("obj must be struct type") + } + u := url.Values{} + for i := 0; i < t.NumField(); i++ { + var value string + tag := t.Field(i).Tag.Get("json") + switch t.Field(i).Type.Kind() { + case reflect.String: + value = v.Field(i).String() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + value = strconv.Itoa(int(v.Field(i).Int())) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + value = strconv.FormatUint(v.Field(i).Uint(), 10) + case reflect.Slice, reflect.Array: + buf := bytes.Buffer{} + b, _ := json.Marshal(v.Field(i).Interface()) + t := strings.TrimRight(strings.TrimLeft(string(b), "["), "]") + array := strings.Split(t, ",") + if len(array) == 0 { + value = "" + } + if len(array) == 1 { + value = array[0] + } else { + for i, v := range array { + if i == 0 { + buf.WriteString(v) + continue + } + buf.WriteString("&") + buf.WriteString(tag) + buf.WriteString("=") + buf.WriteString(v) + value = buf.String() + } + } + } + u.Add(tag, value) + } + dcodeurl, err := url.QueryUnescape(u.Encode()) + return dcodeurl, err +} + +// StrToMd5 字符串转md5 +func StrToMd5(str string) string { + data := []byte(str) + return ByteToMd5(data) +} + +// ByteToMd5 数组转md5 +func ByteToMd5(data []byte) string { + md5Ctx := md5.New() + md5Ctx.Write(data) + cipherStr := md5Ctx.Sum(nil) + return hex.EncodeToString(cipherStr) +} + +// FileToMd5 文件转MD5 +func FileToMd5(fileName string) string { + file, err := os.Open(fileName) + if err != nil { + return "" + } + //defer file.Close() + md5Ctx := md5.New() + if _, err := io.Copy(md5Ctx, file); err != nil { + return "" + } + cipherStr := md5Ctx.Sum(nil) + return hex.EncodeToString(cipherStr) +} + +// HashCode 对一个字符串生成唯一的hasHcode 码 +func HashCode(src string) int { + v := int(crc32.ChecksumIEEE([]byte(src))) + if v < 0 { + return -v + } + return v +} + +// HexToString converts +func HexToString(data []byte) string { + return hex.EncodeToString(data) +} + +// StrToSha256 字符串转sha256 +func StrToSha256(str string) string { + shaCtx := sha256.New() + shaCtx.Write([]byte(str)) + shaBytes := shaCtx.Sum(nil) + cipherStr := hex.EncodeToString(shaBytes[:]) + return cipherStr +} + +// StrToSha1 字符串转sha1 +func StrToSha1(str string, secret string) []byte { + h := hmac.New(sha1.New, []byte(secret)) + h.Write([]byte(str)) + return h.Sum(nil) +} + +// StrToSha1 字符串转sha1 转16进制后 +func StrToHmacSha1(str, secret string) string { + return hex.EncodeToString(StrToSha1(str, secret)) +} + +// StrToHmacSha256 加密 字符串转sha256加密字符串 +func StrToHmacSha256(str string, secret string) string { + h := hmac.New(sha256.New, []byte(secret)) + h.Write([]byte(str)) + cipherStr := hex.EncodeToString(h.Sum(nil)) + return cipherStr +} + +func PKCS7Padding(ciphertext []byte, blockSize int) []byte { + padding := blockSize - len(ciphertext)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(ciphertext, padtext...) +} + +func PKCS7UnPadding(origData []byte) []byte { + length := len(origData) + unpadding := int(origData[length-1]) + return origData[:(length - unpadding)] +} + +// AesEncrypt AES加密 +func AesEncrypt(origData, key string) ([]byte, error) { + origDataByte := []byte(origData) + return CoreAesEncrypt(origDataByte, key) +} + +// AesDecrypt AES解密 +func AesDecrypt(crypted, key string) (string, error) { + cryptedByte := []byte(crypted) + return CoreAesDecrypt(cryptedByte, key) +} + +// CoreAesEncrypt AES加密 +func CoreAesEncrypt(origData []byte, key string) ([]byte, error) { + keybyte := []byte(key) + block, err := aes.NewCipher(keybyte) + if err != nil { + return nil, err + } + blockSize := block.BlockSize() + origData = PKCS7Padding(origData, blockSize) + blockMode := cipher.NewCBCEncrypter(block, keybyte[:blockSize]) + crypted := make([]byte, len(origData)) + blockMode.CryptBlocks(crypted, origData) + return crypted, nil +} + +// https://core.telegram.org/api/end-to-end#sending-and-receiving-messages-in-a-secret-chat +func CoreAesEncryptEx(plain []byte, nonceLen int, key string) ([]byte, error) { + nonce := make([]byte, nonceLen) + _, err := io.ReadFull(rand.Reader, nonce) + if err != nil { + return nil, err + } + var largeShaRaw []byte + largeShaRaw = append(largeShaRaw, []byte(key)...) + largeShaRaw = append(largeShaRaw, nonce...) + largeShaRawMid := len(largeShaRaw) / 2 + msgKeyLarge := sha256.Sum256(largeShaRaw) + msgKey := msgKeyLarge[8:24] //16 bytes + + var shaRawA []byte + shaRawA = append(shaRawA, msgKey...) + shaRawA = append(shaRawA, largeShaRaw[:largeShaRawMid]...) + sha256a := sha256.Sum256(shaRawA) //32 bytes + + var shaRawB []byte + shaRawB = append(shaRawB, largeShaRaw[largeShaRawMid:]...) + shaRawB = append(shaRawB, msgKey...) + sha256b := sha256.Sum256(shaRawB) //32 bytes + + var aesKey []byte //32 bytes AES-256 + aesKey = append(aesKey, sha256a[:8]...) //a: 8 bytes + aesKey = append(aesKey, sha256b[8:24]...) //b: 16 bytes + aesKey = append(aesKey, sha256a[24:32]...) //a: 8 bytes + + var aesIV []byte // 16 bytes + aesIV = append(aesIV, sha256b[:4]...) //b: 4 bytes + aesIV = append(aesIV, sha256a[12:20]...) //a: 8 bytes + aesIV = append(aesIV, sha256b[28:]...) //b: 4 bytes + + block, err := aes.NewCipher(aesKey) + if err != nil { + return nil, err + } + blockSize := block.BlockSize() + ciphertext := PKCS7Padding(plain, blockSize) + mode := cipher.NewCBCEncrypter(block, aesIV) + cipher := make([]byte, nonceLen+len(ciphertext)) + copy(cipher, nonce) + mode.CryptBlocks(cipher[nonceLen:], ciphertext) + return cipher, nil +} + +// CoreAesDecryptEx 是 CoreAesEncryptEx 的逆运算。 +// 入参 crypt 前 nonceLen 字节为随机 nonce,其余为 CBC 密文;密钥派生与加密端完全一致。 +// 由于 crypt 可能来自不可信输入(如客户端伪造的票据),这里对长度和分组边界做了保护,避免切片或 CBC 解密 panic。 +func CoreAesDecryptEx(crypt []byte, nonceLen int, key string) ([]byte, error) { + if nonceLen < 0 || len(crypt) < nonceLen { + return nil, errors.New("CoreAesDecryptEx: ciphertext shorter than nonce") + } + nonce := make([]byte, nonceLen) + copy(nonce, crypt) + var largeShaRaw []byte + largeShaRaw = append(largeShaRaw, []byte(key)...) + largeShaRaw = append(largeShaRaw, nonce...) + largeShaRawMid := len(largeShaRaw) / 2 + msgKeyLarge := sha256.Sum256(largeShaRaw) + msgKey := msgKeyLarge[8:24] //16 bytes + + var shaRawA []byte + shaRawA = append(shaRawA, msgKey...) + shaRawA = append(shaRawA, largeShaRaw[:largeShaRawMid]...) + sha256a := sha256.Sum256(shaRawA) //32 bytes + + var shaRawB []byte + shaRawB = append(shaRawB, largeShaRaw[largeShaRawMid:]...) + shaRawB = append(shaRawB, msgKey...) + sha256b := sha256.Sum256(shaRawB) //32 bytes + + var aesKey []byte //32 bytes AES-256 + aesKey = append(aesKey, sha256a[:8]...) //a: 8 bytes + aesKey = append(aesKey, sha256b[8:24]...) //b: 16 bytes + aesKey = append(aesKey, sha256a[24:32]...) //a: 8 bytes + + var aesIV []byte // 16 bytes + aesIV = append(aesIV, sha256b[:4]...) //b: 4 bytes + aesIV = append(aesIV, sha256a[12:20]...) //a: 8 bytes + aesIV = append(aesIV, sha256b[28:]...) //b: 4 bytes + + block, err := aes.NewCipher(aesKey) + if err != nil { + return nil, err + } + realData := crypt[nonceLen:] + blockSize := block.BlockSize() + if len(realData) == 0 || len(realData)%blockSize != 0 { + return nil, errors.New("CoreAesDecryptEx: ciphertext is not a multiple of block size") + } + blockMode := cipher.NewCBCDecrypter(block, aesIV) + origData := make([]byte, len(realData)) + blockMode.CryptBlocks(origData, realData) + // 用带校验的 PKCS7 去填充:错误密钥/损坏数据几乎必然产生非法填充,这里返回错误而非 panic, + // 也顺带充当一次完整性校验(共享的 PKCS7UnPadding 在非法填充时会越界 panic,故不复用)。 + return pkcs7UnpadSafe(origData, blockSize) +} + +// pkcs7UnpadSafe 校验并剥离 PKCS7 填充,非法填充返回错误而不 panic。 +func pkcs7UnpadSafe(data []byte, blockSize int) ([]byte, error) { + length := len(data) + if length == 0 || length%blockSize != 0 { + return nil, errors.New("invalid PKCS7 padding: bad length") + } + pad := int(data[length-1]) + if pad <= 0 || pad > blockSize || pad > length { + return nil, errors.New("invalid PKCS7 padding: bad size") + } + for _, b := range data[length-pad:] { + if int(b) != pad { + return nil, errors.New("invalid PKCS7 padding: inconsistent bytes") + } + } + return data[:length-pad], nil +} + +// CoreAesDecrypt AES解密 +func CoreAesDecrypt(crypted []byte, key string) (s string, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("CoreAesDecrypt is panic. crypted:%s,key:%s", string(crypted), key) + return + } + }() + keybyte := []byte(key) + block, err := aes.NewCipher(keybyte) + if err != nil { + return "", err + } + blockSize := block.BlockSize() + if blockSize < 0 { + return "", errors.New("blockSize less than zero") + } + blockMode := cipher.NewCBCDecrypter(block, keybyte[:blockSize]) + origData := make([]byte, len(crypted)) + blockMode.CryptBlocks(origData, crypted) + origData = PKCS7UnPadding(origData) + return string(origData), nil +} + +// XorEnc +func XorEnc(src string, xorKey string) string { + var result string + j := 0 + bt := []rune(src) + xor := []rune(xorKey) + for i := 0; i < len(bt); i++ { + s := strconv.FormatInt(int64(bt[i]^xor[j]), 10) + result = result + s + j = 1 % len(xor) + } + return result +} + +// XorDec +func XorDec(src string, xorKey string) string { + var result string + j := 0 + bt := []rune(src) + xor := []rune(xorKey) + for i := 0; i < len(bt); i++ { + s := strconv.FormatInt(int64(bt[i]^xor[j]), 10) + result = result + s + j = i % len(xor) + } + return result +} + +func XorDecode(src, key string) string { + srcByte, _ := base64.StdEncoding.DecodeString(src) + keyByte, _ := base64.StdEncoding.DecodeString(key) + srcByte = xor(srcByte, keyByte) + return base64.StdEncoding.EncodeToString(srcByte) +} + +func XorEncode(src, key string) string { + return XorDecode(src, key) +} + +func xor(src []byte, key []byte) []byte { + for i := 0; i < len(src); i++ { + src[i] ^= key[i%len(key)] + } + return src +} + +func XORLength(src []byte, key []byte, length int) []byte { + for i := 0; i < length; i++ { + src[i] ^= key[i%len(key)] + } + return src[:length] +} + +// ECB PKCS5 加密 +func AESECBEncrypt(src, key []byte) []byte { + block, err := aes.NewCipher(key) + if err != nil { + fmt.Printf("txn put fail: %v", err) + return nil + } + ecbMod := ecb.NewECBEncrypter(block) + content := PKCS5Padding(src, block.BlockSize()) + des := make([]byte, len(content)) + ecbMod.CryptBlocks(des, content) + return des +} + +// ECB PKCS5 解密 +func AESECBDecrypt(encrypted, key []byte) []byte { + block, err := aes.NewCipher(key) + if err != nil { + fmt.Printf("decrypt fail: %v", err) + return nil + } + + ecbMod := ecb.NewECBDecrypter(block) + des := make([]byte, len(encrypted)) + ecbMod.CryptBlocks(des, encrypted) + + // 去除 PKCS5 填充 + result := PKCS5UnPadding(des) + return result +} + +// PKCS5UnPadding +func PKCS5UnPadding(origData []byte) []byte { + length := len(origData) + // 去掉最后一个字节 unpadding 次 + unpadding := int(origData[length-1]) + if unpadding > length { + return origData + } + return origData[:(length - unpadding)] +} + +// PKCS5Padding +func PKCS5Padding(ciphertext []byte, blockSize int) []byte { + padding := blockSize - len(ciphertext)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(ciphertext, padtext...) +} + +// 加密 +func AESCBCPck5Encrypt(origData, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + blockSize := block.BlockSize() + origData = PKCS5Padding(origData, blockSize) + blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) + crypted := make([]byte, len(origData)) + blockMode.CryptBlocks(crypted, origData) + return crypted, nil +} + +// base64补全 +func padding(origin string) string { + missing := len(origin) % 4 + if missing != 0 { + origin += strings.Repeat("=", 4-missing) + } + + return origin +} + +// AdDecrypt 集团广告中心-广告数据解密专用 +func AdDecrypt(encryptedData, keyBase64 string) (res string, err error) { + // 1. Base64 解码获取密钥 + keyBytes, err := base64.StdEncoding.DecodeString(padding(keyBase64)) + if err != nil { + return "", fmt.Errorf("密钥 Base64 解码失败: %v", err) + } + // 2. Base64 解码获取加密数据 + data, err := base64.StdEncoding.DecodeString(padding(encryptedData)) + if err != nil { + return "", fmt.Errorf("数据 Base64 解码失败: %v", err) + } + + // 3. 拆分 IV 与密文(前 12 字节为 IV) + if len(data) < 12 { + return "", errors.New("数据长度不足,无法提取 IV") + } + iv := data[:12] + ciphertext := data[12:] + // 4. 初始化 AES 密码块 + block, err := aes.NewCipher(keyBytes) + if err != nil { + return "", fmt.Errorf("创建 AES 密码块失败: %v", err) + } + // 5. 采用 GCM 模式 + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("创建 GCM 失败: %v", err) + } + // 6. 解密数据 + plainText, err := aesGCM.Open(nil, iv, ciphertext, nil) + if err != nil { + return "", fmt.Errorf("解密失败: %v", err) + } + + return string(plainText), nil +} diff --git a/common/crypt/crypt_ex_test.go b/common/crypt/crypt_ex_test.go new file mode 100644 index 0000000..e04d2d6 --- /dev/null +++ b/common/crypt/crypt_ex_test.go @@ -0,0 +1,49 @@ +package crypt + +import ( + "bytes" + "testing" +) + +// TestCoreAesEncryptDecryptExRoundTrip 校验新增的 CoreAesDecryptEx 与 CoreAesEncryptEx 互逆。 +func TestCoreAesEncryptDecryptExRoundTrip(t *testing.T) { + key := "h5-m3u8-ticket-key-roundtrip-000000000000" + plains := [][]byte{ + []byte(""), + []byte("a"), + []byte(`{"u":10086,"p":"v3/av/a.m3u8","e":1737000000}`), + bytes.Repeat([]byte("x"), 512), + } + for _, plain := range plains { + enc, err := CoreAesEncryptEx(plain, 12, key) + if err != nil { + t.Fatalf("encrypt: %v", err) + } + dec, err := CoreAesDecryptEx(enc, 12, key) + if err != nil { + t.Fatalf("decrypt: %v", err) + } + if !bytes.Equal(dec, plain) { + t.Fatalf("round trip mismatch: got %q want %q", dec, plain) + } + } +} + +// TestCoreAesDecryptExRejectsShort 确保短于 nonce 的非法输入不 panic 且返回错误。 +func TestCoreAesDecryptExRejectsShort(t *testing.T) { + if _, err := CoreAesDecryptEx([]byte{1, 2, 3}, 12, "key"); err == nil { + t.Fatal("expected error for input shorter than nonce") + } +} + +// TestCoreAesDecryptExWrongKey 用错误密钥解密不应还原出原文。 +func TestCoreAesDecryptExWrongKey(t *testing.T) { + enc, err := CoreAesEncryptEx([]byte("secret-payload"), 12, "key-a-000000000000000000000000000000") + if err != nil { + t.Fatalf("encrypt: %v", err) + } + dec, err := CoreAesDecryptEx(enc, 12, "key-b-111111111111111111111111111111") + if err == nil && string(dec) == "secret-payload" { + t.Fatal("wrong key must not recover plaintext") + } +} diff --git a/common/crypt/ecb/ecb.go b/common/crypt/ecb/ecb.go new file mode 100644 index 0000000..e47fc18 --- /dev/null +++ b/common/crypt/ecb/ecb.go @@ -0,0 +1,63 @@ +package ecb + +import "crypto/cipher" + +type ecb struct { + b cipher.Block + blockSize int +} + +func newECB(b cipher.Block) *ecb { + return &ecb{ + b: b, + blockSize: b.BlockSize(), + } +} + +type ecbEncrypter ecb + +// NewECBEncrypter returns a BlockMode which encrypts in electronic code book +// mode, using the given Block. +func NewECBEncrypter(b cipher.Block) cipher.BlockMode { + return (*ecbEncrypter)(newECB(b)) +} + +func (x *ecbEncrypter) BlockSize() int { return x.blockSize } + +func (x *ecbEncrypter) CryptBlocks(dst, src []byte) { + if len(src)%x.blockSize != 0 { + panic("crypto/cipher: input not full blocks") + } + if len(dst) < len(src) { + panic("crypto/cipher: output smaller than input") + } + for len(src) > 0 { + x.b.Encrypt(dst, src[:x.blockSize]) + src = src[x.blockSize:] + dst = dst[x.blockSize:] + } +} + +type ecbDecrypter ecb + +// NewECBDecrypter returns a BlockMode which decrypts in electronic code book +// mode, using the given Block. +func NewECBDecrypter(b cipher.Block) cipher.BlockMode { + return (*ecbDecrypter)(newECB(b)) +} + +func (x *ecbDecrypter) BlockSize() int { return x.blockSize } + +func (x *ecbDecrypter) CryptBlocks(dst, src []byte) { + if len(src)%x.blockSize != 0 { + panic("crypto/cipher: input not full blocks") + } + if len(dst) < len(src) { + panic("crypto/cipher: output smaller than input") + } + for len(src) > 0 { + x.b.Decrypt(dst, src[:x.blockSize]) + src = src[x.blockSize:] + dst = dst[x.blockSize:] + } +} diff --git a/common/crypt/rsa.go b/common/crypt/rsa.go new file mode 100644 index 0000000..27e33bf --- /dev/null +++ b/common/crypt/rsa.go @@ -0,0 +1,140 @@ +package crypt + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + + "91porn-server/common/log" +) + +func splitBytesSlice(data []byte, n int) [][]byte { + var chunk []byte + chunks := make([][]byte, 0, len(data)/n+1) + for len(data) >= n { + chunk, data = data[:n], data[n:] + chunks = append(chunks, chunk) + } + if len(data) > 0 { + chunks = append(chunks, data[:]) + } + return chunks +} + +func rsaBytesToPublicKey(pub []byte) (*rsa.PublicKey, error) { + block, _ := pem.Decode(pub) + b := block.Bytes + var err error + pk, err := x509.ParsePKIXPublicKey(b) + if err != nil { + log.Error("RSABytesToPublicKey ParsePKIXPublicKey err", log.E(err)) + return nil, err + } + if pk_, ok := pk.(*rsa.PublicKey); ok { + return pk_, nil + } + log.Error("RSABytesToPublicKey error input not PublicKey") + return nil, errors.New("Not RSA PublicKey format") +} + +func rsaBytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(priv) + b := block.Bytes + var err error + key, err := x509.ParsePKCS8PrivateKey(b) + if err != nil { + log.Error("RSABytesToPrivateKey ParsePKCS8PrivateKey err", log.E(err)) + return nil, err + } + if pk_, ok := key.(*rsa.PrivateKey); ok { + return pk_, nil + } + log.Error("RSABytesToPrivateKey error input not PrivateKey") + return nil, errors.New("Not RSA PrivateKey format") +} + +// 公钥加密 +func RSAEncryptByPublicKey(pemPubKey string, data []byte) ([]byte, error) { + pubKey, err := rsaBytesToPublicKey([]byte(pemPubKey)) + if err != nil { + log.Error("RSAEncryptByPublicKey rsaBytesToPublicKey err", log.E(err)) + return nil, err + } + partLen := pubKey.N.BitLen()/8 - 11 + chunks := splitBytesSlice(data, partLen) + result := bytes.Buffer{} + for _, chunk := range chunks { + cipherSilce, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, chunk) + if err != nil { + log.Error("RSAEncryptByPublicKey EncryptPKCS1v15 err", log.E(err)) + return nil, err + } + result.Write(cipherSilce) + } + return result.Bytes(), nil +} + +// 私玥签名 +func RSASignByPrivateKey(pemPrivateKey string, data []byte, hash crypto.Hash) ([]byte, error) { + privateKey, err := rsaBytesToPrivateKey([]byte(pemPrivateKey)) + if err != nil { + log.Error("RSASignByPrivateKey rsaBytesToPrivateKey err", log.E(err)) + return nil, err + } + h := hash.New() + h.Write(data) + hashed := make([]byte, 0) + hashed = h.Sum(hashed) + return rsa.SignPKCS1v15(rand.Reader, privateKey, hash, hashed) +} + +func RSAVerifyByPublicKey(publicKey string, data, sign []byte, hash crypto.Hash) (err error) { + pubKey, err := rsaBytesToPublicKey([]byte(publicKey)) + if err != nil { + log.Error("RSAEncryptByPublicKey rsaBytesToPublicKey err", log.E(err)) + return err + } + h := hash.New() + h.Write(data) + hashed := make([]byte, 0) + hashed = h.Sum(hashed) + return rsa.VerifyPKCS1v15(pubKey, hash, hashed, sign) +} + +// 公钥解密 +func RSADecryptByPrivateKey(pemPrivateKey []byte, cipherData []byte) ([]byte, error) { + privateKey, err := rsaBytesToPrivateKey(pemPrivateKey) + if err != nil { + log.Error("RSADecryptByPrivateKey rsaBytesToPrivateKey err", log.E(err)) + return nil, err + } + blockLen := privateKey.N.BitLen() / 8 + chunks := splitBytesSlice(cipherData, blockLen) + result := bytes.Buffer{} + for _, chunk := range chunks { + plain, err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, chunk) + if err != nil { + log.Error("RSADecryptByPrivateKey DecryptPKCS1v15 err", log.E(err)) + return nil, err + } + result.Write(plain) + } + return result.Bytes(), nil +} + +// 公钥解密 +func RSADecryptByPrivateKey1(pemPrivateKey []byte, cipherData []byte) ([]byte, error) { + block, _ := pem.Decode(pemPrivateKey) //将密钥解析成私钥实例 + if block == nil { + return nil, errors.New("private key error!") + } + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) //解析pem.Decode()返回的Block指针实例 + if err != nil { + return nil, err + } + return rsa.DecryptPKCS1v15(rand.Reader, priv, cipherData) //RSA算法解密 +} diff --git a/common/daichong/daichong.go b/common/daichong/daichong.go new file mode 100644 index 0000000..5145648 --- /dev/null +++ b/common/daichong/daichong.go @@ -0,0 +1,163 @@ +package daichong + +import "91porn-server/common/stderr" + +// 接口地址 +const ( + takeChat = "/api/chat/playerChat" //发起聊天 + getOrder = "/api/chat/getOrder" //获取订单信息 + orderCallBack = "/api/call/orderCallBack" //通知代充平台订单处理结果 + uploadFile = "/file" //上传图片 路径file 拼接要上传的图片名称 玩家ID加上毫秒时间戳 /file/u123456156332544.jpg + getChatRecordsByOrderId = "/api/chat/getChatRecordsByOrderId" //根据订单ID获取聊天记录 + getTraderPayInfo = "/api/chat/getTraderPayInfo" //获取商人支付信息 + + // 接口地址 + newtakeChat = "/api/dc/daichong/takeChat" //发起聊天 + newgetOrder = "/api/dc/daichong/orderInfo" //获取订单信息 + newgetTraderPayInfo = "/api/dc/daichong/traderPayInfo" //获取商人支付信息 + + // 通知订单状态 + Success int = 4 //成功 + Failed int = 5 //失败 +) + +// ====================================================================================================================== +// 聊天返回响应模型 +type ChatResp struct { + Code stderr.Code `json:"code"` + Msg string `json:"msg"` + Data Chat `json:"data"` +} + +// 发起聊天参数模型 +type ChatReq struct { + AppId string `json:"appId"` //代充平台提供的appId + Data string `json:"data"` //加密结果字符串 + ProtoType int `json:"protoType"` //协议类型 1: Google Protobuf 2: Pomelo Protobuf 3: JSON + ProductType int `json:"productType"` //0 站群 (默认) 1棋牌 +} + +type ChatSign struct { + UID string `json:"uid"` + NickName string `json:"nickName"` + Avatar string `json:"avatar"` + Channel string `json:"channel"` +} + +// 聊天模型 +type Chat struct { + IsReconnect bool `json:"isReconnect"` //当前玩家是否处于断线重连 + Traders []Trader `json:"traders"` + URL string `json:"url"` //客户端连接webSocket的地址 + WsURL string `json:"wsUrl"` //客户端连接webSocket的地址 + PicUrl string `json:"picUrl"` //图片文件服务 + OrdUrl string `json:"ordUrl,omitempty"` //获取订单信息地址 + TraderUrl string `json:"traderUrl,omitempty"` //获取商人列表地址 + UserInfo struct { + UID uint64 `json:"uid,omitempty"` //用户id + Gender string `json:"gender,omitempty"` //性别 + Name string `json:"name,omitempty"` //用户名称 + Portrait string `json:"portrait,omitempty"` //头像 + } `json:"userInfo"` //用户uid + ChargeMoney uint64 `json:"chargeMoney"` //充值金额 + Limit uint64 `json:"limit"` //限定金额 超过限定金额 使用大额扫码 ,小于等于限定金额 使用小额扫码 + UserAgent string `json:"userAgent,omitempty"` //UserAgent header +} + +// 商人模型 +type Trader struct { + ImId int64 `json:"imId"` //商人在聊天系统中的ID + UserId string `json:"userId"` //商人在代充系统中的ID + Avatar string `json:"avatar"` //头像 + NickName string `json:"nickName"` //昵称 + PayInfos []PayInfo `json:"payInfos"` //支付方式 + WelcomeMsg string `json:"welcomeMsg"` //商人欢迎语 +} + +// 支付方式 +type PayInfo struct { + PayMethod int64 `json:"payMethod"` //支付方式大类别 + PayType []int64 `json:"payType"` //支付方式小类别 +} + +// ====================================================================================================================== +// 获取订单响应模型 +type OrderResp struct { + Code stderr.Code `json:"code"` + Msg string `json:"msg"` + Data Order `json:"data"` +} + +type OrderSign struct { + TraderId int64 `form:"traderId" json:"traderId"` + PlayerId int64 `form:"playerId" json:"playerId"` + Proof string `form:"proof" json:"proof"` //玩家上传的图片名称 + SessionId string `form:"sessionId" json:"sessionId"` //当前会话的sessionId + ProductInfo string `form:"productInfo" json:"productInfo"` //商品信息 + ProT int `form:"productType" json:"productType" ` //0 站群 1棋牌 +} + +// 请求模型 +type CommonReq struct { + AppId string `json:"appId"` //代充平台提供的appId + Data string `json:"data"` //加密字符串 +} + +// 订单模型 +type Order struct { + OrderId string `json:"orderId"` //订单号 + Proof string `json:"proof"` //当前订单生成的凭证(即玩家上传的图片名称 + CreateTime string `json:"createTime"` //订单创建的时间 +} + +// ====================================================================================================================== +// 上分通知 +type OrderCallSign struct { + OrderId string `json:"orderId"` //订单号 代充平台订单号 + Time int64 `json:"time"` //通知时间 时间戳 毫秒 + Status int `json:"status"` //状态 +} + +// 上传参数 +type UploadFileReq struct { + FileName string `json:"fileName"` //文件名 + AppId string `json:"appId"` //代充平台提供的appId + UserID string `json:"userId"` //用户ID + SessionId string `json:"sessionId"` //会话ID + File string `json:"file"` //base64 字符串 +} + +// 聊天记录返回模型 +type RecordResp struct { + Code stderr.Code `json:"code"` + Msg string `json:"msg"` + Data recordChat `json:"data"` +} + +// 聊天记录列表 +type recordChat struct { + List []chat `json:"list"` + Total int64 `json:"total"` +} + +// 聊天 +type chat struct { + MessageId string `json:"messageId"` + SessionId string `json:"sessionId"` + SenderId int64 `json:"senderId"` + TargetId int64 `json:"targetId"` + SendType int `json:"sendType"` + MessageType int `json:"messageType"` + Text string `json:"text"` + Photo []string `json:"photo"` + Payload string `json:"payload"` + CreateDate int `json:"createDate"` + IsRead int `json:"isRead"` +} + +// 商人支付信息 +type PayInfoResp struct { + Code stderr.Code `json:"code"` + Msg string `json:"msg"` + Data PayInfo `json:"data"` +} diff --git a/common/daichong/dcfunction.go b/common/daichong/dcfunction.go new file mode 100644 index 0000000..8e6a060 --- /dev/null +++ b/common/daichong/dcfunction.go @@ -0,0 +1,149 @@ +package daichong + +import ( + "context" + "encoding/hex" + "fmt" + "net/url" + "reflect" + "strconv" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/crypt" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/models/commod" +) + +// 发起聊天 +func TakeChat(req ChatReq, domian string) (chatResp ChatResp, err error) { + var code int + param, _ := common.ToJsonM(req) + c, cancle := context.WithTimeout(context.Background(), time.Duration(appg.Conf.HttpOptions.DcCtxTimeOut)*time.Second) + defer cancle() + code, err = httputil.DefaultClientPostJsonWithResp(&chatResp, common.BindUrl(domian, takeChat), nil, param) + common.Go(func() { + <-c.Done() + fmt.Print("current http method takeChat", c.Err()) + }) + log.Info("http method TakeChat response code ==>", log.Any("statusCode", code), log.Any("respCode", chatResp.Code)) + if err != nil { + log.Error("TakeChat failed", log.E(err)) + } + return +} + +// 获取订单信息 +func GetOrder(req CommonReq, domian string) (orderResp OrderResp, err error) { + param, _ := common.ToJsonM(req) + code, err := httputil.DefaultClientPostJsonWithResp(&orderResp, common.BindUrl(domian, getOrder), nil, param) + log.Info("http method GetOrder response code ==>", log.Any("statusCode", code), log.Any("respCode", orderResp.Code)) + if err != nil { + log.Error("GetOrder failed", log.E(err)) + } + return +} + +// 上分通知 +func OrderCallBack(ctx context.Context, req CommonReq, domian string) (resp commod.Resp, err error) { + param, _ := common.ToJsonM(req) + code, err := httputil.DefaultClientPostJsonWithResp(&resp, common.BindUrl(domian, orderCallBack), nil, param) + log.InfoX(ctx, "http method OrderCallBack response code ==>", log.Any("statusCode", code), + log.Any("respCode", resp.Code)) + if err != nil { + log.ErrorX(ctx, "OrderCallBack failed", log.E(err)) + } + return +} + +// 上传图片 +func UploadFile(req UploadFileReq, domian string) (resp commod.Resp, err error) { + header := map[string]string{ + "Authorization": req.UserID + "&" + req.SessionId, + } + data := map[string]interface{}{ + "file": req.File, + } + code, err := httputil.DefaultClientPostJsonWithResp(&resp, common.BindUrl(domian, uploadFile, req.FileName), header, data) + log.Info("http method UploadFile response code ==>", log.Any("statusCode", code), log.Any("respCode", resp.Code)) + if err != nil { + log.Error("UploadFile failed", log.E(err)) + } + return +} + +// 根据订单ID获取聊天记录 +func GetChatRecordsByOrderId(req CommonReq, domian string) (resp RecordResp, err error) { + code, err := httputil.DefaultClientPostJsonWithResp(&resp, common.BindUrl(domian, getChatRecordsByOrderId), nil, req) + log.Info("http method GetChatRecordsByOrderId response code ==>", log.Any("statusCode", code), log.Any("respCode", resp.Code)) + if err != nil { + log.Error("GetChatRecordsByOrderId failed", log.E(err)) + } + return +} + +// 获取商人支付信息 +func GetTraderPayInfo(req CommonReq, domian string) (resp PayInfoResp, err error) { + code, err := httputil.DefaultClientPostJsonWithResp(&resp, common.BindUrl(domian, getTraderPayInfo), nil, req) + log.Info("http method GetTraderPayInfo response code ==>", log.Any("statusCode", code), log.Any("respCode", resp.Code)) + if err != nil { + log.Error("GetTraderPayInfo failed", log.E(err)) + } + return +} + +// 结构体转换为加密的16进制字符串 +func Convert2Sign(obj interface{}, appSecret string) (string, error) { + urlParam, err := crypt.Strcut2UrlValue(obj) + if err != nil { + log.Warn("chat sign struct convert to url values wrong", log.Any("urlParam", urlParam), log.Any("warn", err)) + return "", err + } + log.Info("covert obj to urlParam ==>", log.Any("obj", obj), log.Any("urlParam", urlParam)) + sign, err := crypt.AesEncrypt(urlParam, appSecret) + if err != nil { + log.Warn("create sign data wrong", log.Any("warn", err)) + return "", err + } + cryptdata := hex.EncodeToString(sign) + return cryptdata, nil +} + +// 加密的16进制字符串转为结构体 +func Sign2Struct(obj interface{}, sign, appSecret string) error { + cryptdata, err := hex.DecodeString(sign) + if err != nil { + log.Warn("hex decode string occour error", log.Any("warn", err)) + return err + } + origin, err := crypt.CoreAesDecrypt(cryptdata, appSecret) + if err != nil { + log.Warn("core aes decrypt sign data wrong", log.Any("warn", err)) + return err + } + t := reflect.TypeOf(obj) + m := make(map[string]interface{}) + u, err := url.ParseQuery(origin) + if err == nil { + for k, v := range u { + s, _ := t.Elem().FieldByName(common.Ucfirst(k)) + typ := s.Type.Kind() + switch typ { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + rv, _ := strconv.ParseInt(v[0], 10, 64) + m[k] = rv + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + rv, _ := strconv.ParseUint(v[0], 10, 64) + m[k] = rv + default: + m[k] = v[0] + } + } + } + if len(m) > 0 { + return common.Map2JSONStruct(obj, m) + } + return nil +} diff --git a/common/daichong/new-dcfunction.go b/common/daichong/new-dcfunction.go new file mode 100644 index 0000000..117f20a --- /dev/null +++ b/common/daichong/new-dcfunction.go @@ -0,0 +1,53 @@ +package daichong + +import ( + "context" + "fmt" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/models/e/daichongmod" +) + +// 发起聊天 +func NewTakeChat(ctx context.Context, req NewChatReq, domain string) (chatResp ChatResp, err error) { + var code int + param, _ := common.ToJsonM(req) + c, cancle := context.WithTimeout(ctx, time.Duration(appg.Conf.HttpOptions.DcCtxTimeOut)*time.Second) + defer cancle() + code, err = httputil.DefaultClientPostJsonWithResp(&chatResp, common.BindUrl(domain, newtakeChat), nil, param) + common.Go(func() { + <-c.Done() + fmt.Print("current http method takeChat", c.Err()) + }) + log.InfoX(c, "http method TakeChat response code ==>", log.Any("statusCode", code), + log.Any("respCode", chatResp.Code)) + if err != nil { + log.ErrorX(c, "TakeChat failed", log.Any("req", req), log.Any("domain", domain), log.E(err)) + } + return +} + +// 获取订单信息 +func NewGetOrder(req OrderSign, domain string) (orderResp OrderResp, err error) { + param, _ := common.ToJsonM(req) + code, err := httputil.DefaultClientPostJsonWithResp(&orderResp, common.BindUrl(domain, newgetOrder), nil, param) + log.Info("http method GetOrder response code ==>", log.Any("statusCode", code), log.Any("respCode", orderResp.Code)) + if err != nil { + log.Error("GetOrder failed", log.E(err)) + } + return +} + +// 获取商人支付信息 +func NewGetTraderPayInfo(req daichongmod.PayInfoReq, domain string) (resp PayInfoResp, err error) { + code, err := httputil.DefaultClientPostJsonWithResp(&resp, common.BindUrl(domain, newgetTraderPayInfo), nil, req) + log.Info("http method GetTraderPayInfo response code ==>", log.Any("statusCode", code), log.Any("respCode", resp.Code)) + if err != nil { + log.Error("GetTraderPayInfo failed", log.E(err)) + } + return +} diff --git a/common/daichong/newdaichong.go b/common/daichong/newdaichong.go new file mode 100644 index 0000000..1731fa7 --- /dev/null +++ b/common/daichong/newdaichong.go @@ -0,0 +1,9 @@ +package daichong + +// 发起聊天参数模型 +type NewChatReq struct { + UID string `json:"uid"` //用户id + Name string `json:"name"` //用户名称 + Portrait string `json:"portrait"` //用户头像 + ProductType int `json:"productType"` //0 站群 (默认) 1棋牌 +} diff --git a/common/dataReport/handle.go b/common/dataReport/handle.go new file mode 100644 index 0000000..44e8896 --- /dev/null +++ b/common/dataReport/handle.go @@ -0,0 +1,184 @@ +package dataReport + +import ( + "91porn-server/common/log" + "91porn-server/middleware/ua" + "91porn-server/models/commod" + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + "github.com/moul/http2curl" +) + +var config = Config{} + +func Init(c Config) { + config = c +} + +// GenerateGlobalEventID 生成全局唯一的 event_id +func GenerateGlobalEventID() string { + // 生成一个新的 UUID + return strings.ReplaceAll(uuid.New().String(), "-", "") +} + +func ReportAppId() string { + if commod.KFK_APPID < 10 { + return fmt.Sprintf("JHA-00%v", commod.KFK_APPID) + } + if commod.KFK_APPID < 100 { + return fmt.Sprintf("JHA-0%v", commod.KFK_APPID) + } + return fmt.Sprintf("JHA-%v", commod.KFK_APPID) +} + +// 上报事件 +func BatchReport(data interface{}) error { + apiUrl := config.ApiUrl + "/api/eventTracking/batchReport.json" + reqBody, err := json.Marshal(data) + if err != nil { + log.Error("Event report json.Marshal fail", log.E(err)) + return err + } + req, err := http.NewRequest("POST", apiUrl, bytes.NewReader(reqBody)) + if err != nil { + log.Error("Event report NewRequest fail", log.E(err)) + return fmt.Errorf("failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + curl, _ := http2curl.GetCurlCommand(req) + client := &http.Client{ + Timeout: 5 * time.Second, // 设置超时时间为 5 秒 + } + resp, err := client.Do(req) + if err != nil { + log.Error("Event report fail", log.Any("curl", curl), log.E(err)) + return fmt.Errorf("failed to send request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + log.Error("Event report fail", log.Any("curl", curl), log.Any("http.code", resp.StatusCode)) + return fmt.Errorf("failed to report event, status code: %d", resp.StatusCode) + } + // 读取返回的 body + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Error("Event report fail", log.Any("curl", curl), log.E(err)) + return fmt.Errorf("failed to read response body: %v", err) + } + // 输出响应状态和返回的 body + log.Info("Event report Response", log.Any("body", string(body))) + return nil +} + +func structToFormData(data interface{}) (string, error) { + // 序列化结构体为 JSON 字符串 + jsonData, err := json.Marshal(data) + if err != nil { + return "", err + } + + // 反序列化为 map + var dataMap map[string]interface{} + err = json.Unmarshal(jsonData, &dataMap) + if err != nil { + return "", err + } + + var formData []string + + // 遍历 map,将其转换为 form 数据 + for key, value := range dataMap { + if value == nil { + continue // 如果值为 nil,跳过 + } + + // 将值转换为字符串 + valueStr := fmt.Sprintf("%v", value) + + // 使用 URL 编码 + formData = append(formData, fmt.Sprintf( + "%s=%s", + url.QueryEscape(key), + url.QueryEscape(valueStr), + )) + } + + return strings.Join(formData, "&"), nil +} + +func NewCommonField(eventId string, event string, uid uint64, districtCode string, ua ua.UA, ip string) CommonField { + device := normalizeDevice(ua.SysType) + deviceID := CutTo50(strings.TrimSpace(ua.DevID)) + deviceModel := strings.TrimSpace(ua.DeviceModel) + if deviceModel == "" { + deviceModel = strings.TrimSpace(ua.DevType) + } + systemName := strings.TrimSpace(ua.SystemName) + if systemName == "" { + systemName = device + } + return CommonField{ + UID: fmt.Sprintf("%v", uid), + EventID: eventId, + Event: event, // 事件类型由外部事件对象指定 + Channel: strings.TrimSpace(districtCode), + SID: strings.TrimSpace(ua.SID), + AppID: ReportAppId(), + ClientTS: JsonNumber(time.Now().Unix()), + Device: device, + DeviceID: deviceID, + UserAgent: strings.TrimSpace(ua.UserAgent), + DeviceModel: deviceModel, + DeviceBrand: strings.TrimSpace(ua.DeviceBrand), + SystemName: systemName, + SystemVersion: strings.TrimSpace(ua.SystemVersion), + IP: ip, + } +} + +func Device(ua ua.UA) string { + return normalizeDevice(ua.SysType) +} + +func normalizeDevice(value string) string { + lower := strings.ToLower(strings.TrimSpace(value)) + switch { + case lower == "": + return "" + case strings.Contains(lower, "ios"), strings.Contains(lower, "iphone"), strings.Contains(lower, "ipad"), strings.Contains(lower, "apple"): + return "iOS" + case strings.Contains(lower, "android"): + return "Android" + case strings.Contains(lower, "pc"), strings.Contains(lower, "windows"), strings.Contains(lower, "mac"), strings.Contains(lower, "h5"): + return "PC" + } + return strings.TrimSpace(value) +} + +// CutTo50 截取前 50 个“字符”(支持中文、-、_ 等特殊字符) +func CutTo50(s string) string { + runes := []rune(s) + if len(runes) <= 50 { + return s + } + return string(runes[:50]) +} + +func JsonNumber(v int64) json.Number { + return json.Number(strconv.FormatInt(v, 10)) +} + +func JsonRawMessage(v interface{}) json.RawMessage { + b, _ := json.Marshal(v) + return b +} diff --git a/common/dataReport/report_test.go b/common/dataReport/report_test.go new file mode 100644 index 0000000..fbcd252 --- /dev/null +++ b/common/dataReport/report_test.go @@ -0,0 +1,31 @@ +package dataReport + +import ( + "testing" +) + +func TestReportEvent(t *testing.T) { + reportAppId := "11" + // 创建一个广告点击 + event := &CommonField{ + UID: "user_123", + EventID: GenerateGlobalEventID(), + Event: EventTypeAdClick, + Channel: "666", + AppID: reportAppId, + //SID string `json:"sid"` // 会话ID + //ClientTS int64 `json:"client_ts"` // 客户端时间戳 + Device: "Android", + DeviceID: "device_12345", + UserAgent: "Mozilla/5.0", + DeviceBrand: "Huawei", + DeviceModel: "Mate 40", + IP: "192.168.0.1", + } + event.Payload = JsonRawMessage(AdClickEvent{ + AdID: "1286400000000040", // 广告ID + }) + config.ApiUrl = "https://api.shuifeng.cc" + list := []*CommonField{event} + BatchReport(list) +} diff --git a/common/dataReport/type.go b/common/dataReport/type.go new file mode 100644 index 0000000..a110f35 --- /dev/null +++ b/common/dataReport/type.go @@ -0,0 +1,242 @@ +package dataReport + +import "encoding/json" + +type Config struct { + ApiUrl string `json:"apiUrl"` +} + +const ( + EventTypeAdClick = "ad_click" + EventTypeUserLogin = "user_login" + EventTypeUserRegister = "user_register" + EventTypeOrderCreated = "order_created" + EventTypeOrderPaid = "order_paid" + EventTypeCoinConsume = "coin_consume" + EventTypeVideoPurchase = "video_purchase" + EventTypeVideoLike = "video_like" + EventTypeVideoCollect = "video_collect" + EventTypeVideoComment = "video_comment" + EventTypeVideoStatusChange = "video_status_change" + + // 漫画事件 + EventTypeComicPurchase = "comic_purchase" // 漫画购买事件 + EventTypeComicLike = "comic_like" // 漫画点赞 + EventTypeComicCollect = "comic_collect" // 漫画收藏 + EventTypeComicComment = "comic_comment" // 漫画评论 + + // 小说事件 + EventTypeNovelPurchase = "novel_purchase" // 小说购买事件 + EventTypeNovelLike = "novel_like" // 小说点赞 + EventTypeNovelCollect = "novel_collect" // 小说收藏 + EventTypeNovelComment = "novel_comment" // 小说评论 + +) + +// 定义公共字段和事件数据结构 +type CommonField struct { + Event string `json:"event"` // 事件类型 + Channel string `json:"channel"` // 渠道码 + EventID string `json:"event_id"` // 事件唯一标识符 + AppID string `json:"app_id"` // 应用ID + UID string `json:"uid"` // 用户ID + SID string `json:"sid"` // 会话ID + ClientTS json.Number `json:"client_ts"` // 客户端时间戳 + Device string `json:"device"` // 设备类型(如:Android, iOS, PC) + DeviceID string `json:"device_id"` // 设备ID + UserAgent string `json:"user_agent"` // 用户代理信息 + DeviceBrand string `json:"device_brand"` // 设备品牌(如:HUAWEI) + DeviceModel string `json:"device_model"` // 设备型号 + SystemName string `json:"system_name"` // 系统名称 + SystemVersion string `json:"system_version"` // 系统版本 + IP string `json:"ip,omitempty"` // 用户IP地址 + Payload json.RawMessage `json:"payload"` // 事件数据 +} + +// UserRegisterEvent 注册事件 +type UserRegisterEvent struct { + Type string `json:"type"` // 注册方式:phone, deviceid, email, username + TraceID string `json:"trace_id"` // 唯一标识,落地页点击生成 + CreateTime json.Number `json:"create_time"` // 注册时间,10位时间戳 +} + +// UserLoginEvent 登录事件 +type UserLoginEvent struct { + Type string `json:"type"` // 登录方式:phone, deviceid, email, username +} + +// OrderCreatedEvent 订单创建 +type OrderCreatedEvent struct { + OrderID string `json:"order_id"` // 订单号(唯一标识) + OrderType string `json:"order_type"` // 订单类型 + ProductID string `json:"product_id"` // 商品ID + ProductName string `json:"product_name"` // 商品名称 + Amount json.Number `json:"amount"` // 订单金额(分) + Currency string `json:"currency"` // 货币类型 + CoinQuantity json.Number `json:"coin_quantity"` // 金币数量 + VIPDurationID string `json:"vip_duration_type"` + VIPDurationName string `json:"vip_duration_name"` + SourcePageKey string `json:"source_page_key"` // 来源页面标识 + SourcePageName string `json:"source_page_name"` // 来源页面名称 + CreateTime json.Number `json:"create_time"` // 订单创建时间 +} + +// OrderPaidEvent 订单支付成功 +type OrderPaidEvent struct { + OrderID string `json:"order_id"` // 订单号(关联创建事件) + OrderType string `json:"order_type"` // 订单类型:coin_purchase, vip_subscription + ProductID string `json:"product_id"` // 商品ID + Amount json.Number `json:"amount"` // 实际支付金额(分) + Currency string `json:"currency"` // 货币类型:CNY, USD + CoinQuantity json.Number `json:"coin_quantity"` // 金币数量(仅金币订单) + VIPExpirationTime json.Number `json:"vip_expiration_time"` // VIP过期时间(10位时间戳) + PayType string `json:"pay_type"` // 支付方式:wechat, alipay, bank_card + PayChannel string `json:"pay_channel"` // 支付渠道:银行名称 + TransactionID string `json:"transaction_id"` // 第三方支付交易号 + CreateTime json.Number `json:"create_time"` // 订单创建时间 + +} + +// CoinConsumeEvent 金币消耗 +type CoinConsumeEvent struct { + ProductID string `json:"product_id"` // 商品ID + ProductName string `json:"product_name"` // 商品名称 + CoinConsumeAmount json.Number `json:"coin_consume_amount"` // 金币消耗数量 + CoinBalanceBefore json.Number `json:"coin_balance_before"` // 消耗前金币余额 + CoinBalanceAfter json.Number `json:"coin_balance_after"` // 消耗后金币余额 + ConsumeReasonKey string `json:"consume_reason_key"` // 消耗原因 + ConsumeReasonName string `json:"consume_reason_name"` // 消耗原因名称 + CreateTime json.Number `json:"create_time"` // 订单创建时间 + OrderId string `json:"order_id"` // 订单id + +} + +// VideoPurchaseEvent 视频购买事件 +type VideoPurchaseEvent struct { + VideoEventCore + CoinQuantity json.Number `json:"coin_quantity"` // 金币数量 + OrderID string `json:"order_id"` // 订单号 +} + +type VideoEventCore struct { + MediaID string `json:"media_id"` // 老司机媒体资源ID(老司机接口返回的id字段) + VideoID string `json:"video_id"` // 视频ID + VideoTitle string `json:"video_title"` // 视频标题 + VideoTypeID string `json:"video_type_id"` // 视频分类ID + VideoTypeName string `json:"video_type_name"` // 视频分类名称 + VideoContentType string `json:"video_content_type"` // 视频类型: video(长视频)、short_video(短视频) + RecommendTraceID string `json:"recommend_trace_id"` // 推荐引擎的trace_id。没有对接搜索推荐引擎的上报空字符串。 +} + +type VideoLikeEvent struct { + VideoEventCore + Flag json.Number `json:"flag"` // 点赞状态:1(点赞), 2(取消点赞) +} + +type VideoCollectEvent struct { + VideoEventCore + Flag json.Number `json:"flag"` // 收藏状态:1(收藏), 2(取消收藏) +} + +type VideoCommentEvent struct { + VideoEventCore + CommentContent string `json:"comment_content"` // 评论内容 +} + +type VideoStatusChangeEvent struct { + MediaID string `json:"media_id"` // 老司机媒体资源ID(老司机接口返回的id字段) + VideoID string `json:"video_id"` // 视频ID + VideoTitle string `json:"video_title"` // 视频标题 + VideoTypeID string `json:"video_type_id"` // 视频分类ID + VideoTypeName string `json:"video_type_name"` // 视频分类名称 + Flag json.Number `json:"flag"` // 1(上架), 2(下架), 3(审核通过), 4(审核拒绝) +} + +// AdClickEvent 广告点击 +type AdClickEvent struct { + PageKey string `json:"page_key"` // 页面标识 + PageName string `json:"page_name"` // 页面名称 + AdSlotKey string `json:"ad_slot_key"` // 广告位标识 + AdSlotName string `json:"ad_slot_name"` // 广告位名称 + AdID string `json:"ad_id"` // 广告ID + CreativeID string `json:"creative_id"` // 素材ID(可选) + AdType string `json:"ad_type"` // 广告类型 +} + +// +// +// 漫画相关上报 +// +// + +type ( + ComicEventPub struct { + MediaId string `json:"media_id"` // 老司机媒体资源ID(老司机接口返回的id字段) + ComicId string `json:"comic_id"` // 漫画 id + ComicTitle string `json:"comic_title"` // 漫画标题 + ComicTypeId string `json:"comic_type_id"` // 分类ID + ComicTypeName string `json:"comic_type_name"` // 分类名称 + RecommendTraceId string `json:"recommend_trace_id"` // 推荐引擎的trace_id。没有对接搜索推荐引擎的上报空字符串。 + } + + // ComicPurchaseEvent 漫画购买 + ComicPurchaseEvent struct { + ComicEventPub + CoinQuantity json.Number `json:"coin_quantity"` // 金币数量 + OrderId string `json:"order_id"` // 订单号 + PageNo json.Number `json:"page_no"` // 事件发生在第几页(如果在列表页当前字段设置为0) + } + + // ComicCollectOrLikeEvent 漫画收藏/点赞 + ComicCollectOrLikeEvent struct { + ComicEventPub + Flag json.Number `json:"flag"` //收藏状态:1(收藏), 2(取消收藏) + PageNo json.Number `json:"page_no"` // 事件发生在第几页(如果在列表页当前字段设置为0) + } + + // ComicCommentEvent 漫画评论 + ComicCommentEvent struct { + ComicEventPub + CommentContent string `json:"comment_content"` // 评论内容 + PageNo json.Number `json:"page_no"` // 事件发生在第几页(如果在列表页当前字段设置为0) + } +) + +// +// +// 小说相关上报 +// +// + +type ( + NovelEventPub struct { + MediaId string `json:"media_id"` // 老司机媒体资源ID(老司机接口返回的id字段) + NovelId string `json:"novel_id"` // 小说 id + NovelTitle string `json:"novel_title"` // 小说标题 + NovelTypeId string `json:"novel_type_id"` // 分类ID + NovelTypeName string `json:"novel_type_name"` // 分类名称 + RecommendTraceId string `json:"recommend_trace_id"` // 推荐引擎的trace_id。没有对接搜索推荐引擎的上报空字符串。 + } + + // NovelPurchaseEvent 小说购买 + NovelPurchaseEvent struct { + NovelEventPub + CoinQuantity json.Number `json:"coin_quantity"` // 金币数量 + OrderId string `json:"order_id"` // 订单号 + PageNo json.Number `json:"page_no"` // 事件发生在第几页(如果在列表页当前字段设置为0) + } + + // NovelCollectOrLikeEvent 小说收藏/点赞 + NovelCollectOrLikeEvent struct { + NovelEventPub + Flag json.Number `json:"flag"` // 收藏状态:1(收藏), 2(取消收藏) + PageNo json.Number `json:"page_no"` // 事件发生在第几页(如果在列表页当前字段设置为0) + } + + // NovelCommentEvent 小说评论 + NovelCommentEvent struct { + NovelEventPub + CommentContent string `json:"comment_content"` // 评论内容 + PageNo json.Number `json:"page_no"` // 事件发生在第几页(如果在列表页当前字段设置为0) + } +) diff --git a/common/datacenter/handle.go b/common/datacenter/handle.go new file mode 100644 index 0000000..b921cee --- /dev/null +++ b/common/datacenter/handle.go @@ -0,0 +1,65 @@ +package datacenter + +import ( + "91porn-server/common" + "91porn-server/common/log" + "time" + + "github.com/IBM/sarama" +) + +// kafka 使用 +var ( + gSyncProducer sarama.SyncProducer +) + +/**********************************************客户端 *********************/ +// 初始化kafka生产者 发送消息入口 +func InitKafkaProducter(addrs []string) error { + config := sarama.NewConfig() + config.Version = sarama.V2_0_0_0 + config.Producer.Return.Successes = true + config.Net.KeepAlive = 2 * time.Hour + + cli, err := sarama.NewClient(addrs, config) + if err != nil { + log.Error("startUp Kafka Init Kafka error", log.E(err)) + return err + } + + gSyncProducer, err = SyncProducter(cli) + if err != nil { + log.Error("startUp Kafka new SyncProducter error", log.E(err)) + return nil + } + return nil +} + +// AsyncSendMessage 同步发送确保消息成功 +func AsyncSendMessage(topic TopicType, message []byte) { + common.Go(func() { + if gSyncProducer != nil { + msg := sarama.ProducerMessage{ + Topic: string(topic), + Value: sarama.ByteEncoder(message), + } + partition, offset, err := gSyncProducer.SendMessage(&msg) + if err != nil { + log.Error("gSyncProducer SendMessage Fail", log.Any("topic", topic), log.E(err)) + + return + } + log.Info("SyncSendMessage ", log.Any("topic", topic), log.Any("Partition", partition), log.Any("Offset", offset)) + } + }) +} + +// 创建同步生产者 用于对消息的顺序有严格要求的场景 性能相对较低 +func SyncProducter(client sarama.Client) (sarama.SyncProducer, error) { + producer, err := sarama.NewSyncProducerFromClient(client) + if err != nil { + log.Error("create syncProducer error", log.E(err)) + return nil, err + } + return producer, nil +} diff --git a/common/datacenter/proto.go b/common/datacenter/proto.go new file mode 100644 index 0000000..c5d770d --- /dev/null +++ b/common/datacenter/proto.go @@ -0,0 +1,127 @@ +package datacenter + +import ( + "encoding/json" + "log" + "time" +) + +type IJsonMarshal interface { + Marshal() []byte //用户ID +} +type TopicType string + +const ( + TopicUserRegister TopicType = "user_register" + TopicUserVisit TopicType = "user_visit" + TopicUserOrder TopicType = "user_order" + TopicUserOrderSuccess TopicType = "user_order_success" + TopicUserAction TopicType = "user_action" +) + +/* 通信协议,第一版*/ +// 新用户注册消息 +type UserRegisterMsg struct { + UserId int64 `json:"userId" bson:"userId"` // 用户ID + AppId int32 `json:"appId" bson:"appId"` // appID + SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS + PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id + DevType string `json:"devType" bson:"devType"` // 设备类型 + Name string `json:"name" bson:"name"` // 名称 + IsDirect bool `json:"isDirect" bson:"isDirect"` // true:是直推用户 + DeviceId string `json:"deviceId" bson:"deviceId"` // 设备Id + IP string `json:"ip,omitempty" bson:"ip"` // IP + DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码 + InviteCode string `json:"inviteCode" bson:"inviteCode"` // 被邀请码 + RegisterAt time.Time `json:"registerAt,omitempty" bson:"registerAt"` // 注册时间 + UserAgent string `json:"userAgent" bson:"userAgent"` // User-Agent识别 + RegisterTime time.Time `json:"registerTime" bson:"registerTime"` // 注册时间时间 +} + +var _ IJsonMarshal = &UserRegisterMsg{} + +func (u *UserRegisterMsg) Marshal() []byte { + jsonData, err := json.Marshal(u) + if err != nil { + log.Fatalf("Failed to marshal JSON: %v", err) + } + return jsonData +} + +// UserVisitMsg 用户每日第一次访问 +type UserVisitMsg struct { + UserId int64 `bson:"userId" json:"userId"` // 用户Id + SumDate time.Time `bson:"sumDate" json:"sumDate"` // 记录时间 + AppId int32 `json:"appId" bson:"appId"` // appID + SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS + DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码 + DevType string `json:"devType" bson:"devType"` // 设备类型 + IP string `json:"ip,omitempty" bson:"ip"` // IP + Version string `json:"version,omitempty" bson:"version"` // APP版本号 + VisitAt time.Time `json:"visitAt,omitempty" bson:"visitAt"` // 访问时间 +} + +var _ IJsonMarshal = &UserVisitMsg{} + +func (u *UserVisitMsg) Marshal() []byte { + jsonData, err := json.Marshal(u) + if err != nil { + log.Fatalf("Failed to marshal JSON: %v", err) + } + return jsonData +} + +// UserOrderMsg 用户订单消息 +type UserOrderMsg struct { + UserId int64 `json:"userId" bson:"userId"` // 用户ID + AppId int32 `json:"appId" bson:"appId"` // appID + PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id + SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS + DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码 + DevType string `json:"devType" bson:"devType"` // 设备类型 + ChannelName string `json:"channelName" bson:"channelName"` // 支付渠道名字 + CID string `json:"cid" bson:"cid"` // 渠道id + Type string `bson:"type" json:"type"` // 充值方式 + RchgUse int64 `json:"rchgUse" bson:"rchgUse"` // 充值用途 1:金币 2:VIP + Repeat bool `bson:"repeat" json:"repeat"` // 复冲: true 重复充值 false 第一次 + OrderId string `json:"orderId" bson:"orderId"` // 流水id + Money int64 `json:"money" bson:"money"` // 充值金额 订单金额 + PayMoney int64 `json:"payMoney" bson:"payMoney"` // 实际到账金额 用户实际支付金额 + Status int `json:"status" bson:"status"` // 2付款失败 3付款成功(目前只有成功才发送) + Rate string `json:"rate" bson:"rate"` // 渠道费率 + SuccessAt time.Time `json:"successAt" bson:"successAt"` // 成功时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 +} + +var _ IJsonMarshal = &UserOrderMsg{} + +func (u *UserOrderMsg) Marshal() []byte { + jsonData, err := json.Marshal(u) + if err != nil { + log.Fatalf("Failed to marshal JSON: %v", err) + } + return jsonData +} + +// UserChannelEvent 用户渠道统计 +type UserChannelEvent struct { + UserId int64 `bson:"userId" json:"userId"` // 用户Id + AppId int32 `json:"appId" bson:"appId"` // appID + DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码 + WatchCount int64 `json:"watchCount,omitempty" bson:"watchCount"` // 观看次数 + WatchTime int64 `json:"watchTime,omitempty" bson:"watchTime"` // 观看时长 + AdClick int64 `json:"adClick,omitempty" bson:"adClick"` // 广告点击 + AppClick int64 `json:"appClick,omitempty" bson:"appClick"` // APP点击 + RegisterAt time.Time `json:"registerAt,omitempty" bson:"registerAt"` // 注册时间 + RequestCount int64 `json:"requestCount,omitempty" bson:"requestCount"` // 请求次数 +} + +var _ IJsonMarshal = &UserChannelEvent{} + +func (u *UserChannelEvent) Marshal() []byte { + jsonData, err := json.Marshal(u) + if err != nil { + log.Fatalf("Failed to marshal JSON: %v", err) + } + return jsonData +} diff --git a/common/db/codec.go b/common/db/codec.go new file mode 100644 index 0000000..c355fd8 --- /dev/null +++ b/common/db/codec.go @@ -0,0 +1,108 @@ +package db + +import ( + "fmt" + "reflect" + "time" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/bsoncodec" + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var registry = func() *bsoncodec.Registry { + builder := bson.NewRegistryBuilder() + builder.RegisterTypeDecoder(reflect.TypeOf(time.Time{}), &localTimeDecoder{}) + builder.RegisterTypeDecoder(reflect.TypeOf(decimal.Decimal{}), &decimalDecoder{}) + builder.RegisterTypeEncoder(reflect.TypeOf(decimal.Decimal{}), &decimalEncoder{}) + builder.RegisterDefaultDecoder(reflect.Float32, &floatDecoder{}) + builder.RegisterDefaultDecoder(reflect.Float64, &floatDecoder{}) + return builder.Build() +}() + +type floatDecoder struct { +} + +func (dvd *floatDecoder) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + var f float64 + var err error + switch vr.Type() { + case bsontype.Int32: + i32, err := vr.ReadInt32() + if err != nil { + return err + } + f = float64(i32) + case bsontype.Int64: + i64, err := vr.ReadInt64() + if err != nil { + return err + } + f = float64(i64) + case bsontype.Double: + f, err = vr.ReadDouble() + if err != nil { + return err + } + default: + return fmt.Errorf("cannot decode %v into a float32 or float64 type", vr.Type()) + } + val.SetFloat(f) + return nil +} + +type localTimeDecoder struct{} + +func (*localTimeDecoder) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if err := (&bsoncodec.TimeCodec{}).DecodeValue(dc, vr, val); err != nil { + return err + } + t := val.Interface().(time.Time) + val.Set(reflect.ValueOf(t.Local())) + return nil +} + +type decimalDecoder struct{} + +func (*decimalDecoder) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { + if !val.IsValid() || val.Type() != reflect.TypeOf(decimal.Decimal{}) { + return bsoncodec.ValueDecoderError{Name: "DecimalDecodeValue", Types: []reflect.Type{reflect.TypeOf(decimal.Decimal{})}, Received: val} + } + if vr.Type() == bson.TypeInt32 || vr.Type() == bson.TypeInt64 { + _, _ = vr.ReadInt32() + _, _ = vr.ReadInt64() + d := decimal.NewFromFloat(0.0) + val.Set(reflect.ValueOf(d)) + return nil + } + mongodecimal, err := vr.ReadDecimal128() + if err != nil { + return err + } + d, err := decimal.NewFromString(mongodecimal.String()) + if err != nil { + return err + } + val.Set(reflect.ValueOf(d)) + return nil +} + +type decimalEncoder struct{} + +func (*decimalEncoder) EncodeValue(ctx bsoncodec.EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error { + if !val.IsValid() || val.Type() != reflect.TypeOf(decimal.Decimal{}) { + return bsoncodec.ValueDecoderError{Name: "DecimalEncodeValue", Types: []reflect.Type{reflect.TypeOf(decimal.Decimal{})}, Received: val} + } + if d, ok := val.Interface().(decimal.Decimal); ok { + mongodecimal, err := primitive.ParseDecimal128(d.StringFixed(2)) + if err != nil { + return err + } + val = reflect.ValueOf(mongodecimal) + } + dve := bsoncodec.DefaultValueEncoders{} + return dve.Decimal128EncodeValue(ctx, vw, val) +} diff --git a/common/db/helper.go b/common/db/helper.go new file mode 100644 index 0000000..b5cdd9b --- /dev/null +++ b/common/db/helper.go @@ -0,0 +1,7 @@ +package db + +import "go.mongodb.org/mongo-driver/mongo" + +func IsMongoDupKey(err error) bool { + return mongo.IsDuplicateKeyError(err) +} diff --git a/common/db/mongo.go b/common/db/mongo.go new file mode 100644 index 0000000..c22806f --- /dev/null +++ b/common/db/mongo.go @@ -0,0 +1,392 @@ +package db + +import ( + "context" + "errors" + "fmt" + "reflect" + "time" + + "91porn-server/common/log" + "91porn-server/common/stderr" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var ( + enableSortMCheck = true + bsonDType = reflect.TypeOf(bson.D{}) + bsonDPtrType = reflect.TypeOf(&bson.D{}) + bsonEType = reflect.TypeOf(bson.E{}) + bsonEPtrType = reflect.TypeOf(&bson.E{}) + bsonMType = reflect.TypeOf(bson.M{}) + bsonMPtrType = reflect.TypeOf(&bson.M{}) +) + +var skipErrors = []error{mongo.ErrNoDocuments} + +func handleDbError(err error) error { + for _, e := range skipErrors { + if err == e { + return nil + } + } + if err != nil { + if mongo.IsDuplicateKeyError(err) { + return stderr.InsertExistError + } + } + return err +} + +func sortCheck(sort interface{}) error { + if !enableSortMCheck { + return nil + } + if sort == nil { + return nil + } + typ := reflect.TypeOf(sort) + var m bson.M + switch typ { + case bsonMType: + //log.Warn("mongo sort use bson.M use bson.D instead", log.Any("sort", sort)) + m, _ = sort.(bson.M) + case bsonMPtrType: + //log.Warn("mongo sort use *bson.M use bson.D instead", log.Any("sort", sort)) + pm, _ := sort.(*bson.M) + m = *pm + case bsonDType, bsonDPtrType, bsonEType, bsonEPtrType: + return nil + default: + log.Warn("sort use unknown sort type please check", log.Any("sort", sort), log.Any("typ", typ)) + return errors.New("mongo error sort type") + } + if len(m) > 1 { + return errors.New("mongo error sort, use bson.M and len(sort) > 1") + } + return nil +} + +// CreateIndex 创建数据索引.options 在index里面创建 +func (u *MongoTool) CreateIndex(models []mongo.IndexModel) ([]string, error) { + //return nil, nil + return u.coll.Indexes().CreateMany(u.ctx, models) +} + +// DropIndex 删除数据索引.options 在index里面创建 +func (u *MongoTool) DropIndex(indexname string) error { + if _, err := u.coll.Indexes().DropOne(u.ctx, indexname); err != nil { + log.Error(fmt.Sprintf("drop indexes error %+v", err)) + return err + } + return nil +} + +// DropIndexIfExists 删除指定索引;索引或所在集合尚未创建时均视为成功。 +func (u *MongoTool) DropIndexIfExists(indexname string) error { + if _, err := u.coll.Indexes().DropOne(u.ctx, indexname); err != nil { + var commandErr mongo.CommandError + // 27=IndexNotFound(索引不存在)、26=NamespaceNotFound(集合/库尚未创建):目标索引本就不存在,视为成功。 + if errors.As(err, &commandErr) && (commandErr.Code == 27 || commandErr.Code == 26) { + return nil + } + log.Error(fmt.Sprintf("drop indexes error %+v", err)) + return err + } + return nil +} + +// InsertOne 插入单条信息 +func (u *MongoTool) InsertOne(document interface{}, opts ...*options.InsertOneOptions) (*mongo.InsertOneResult, error) { + insertResult, err := u.coll.InsertOne(u.ctx, document, opts...) + return insertResult, handleDbError(err) +} + +// InsertMany 批量插入信息 +func (u *MongoTool) InsertMany(documents interface{}, opts ...*options.InsertManyOptions) (*mongo.InsertManyResult, error) { + if err := validInterfaceSlice(documents); err != nil { + return nil, err + } + res := setTimeForSlice(documents) + return u.coll.InsertMany(u.ctx, res, opts...) +} + +// Find 查询多条数据 +func (u *MongoTool) Find(model interface{}, filter bson.M, opts ...*options.FindOptions) error { + if err := validInterfaceSlice(model); err != nil { + return err + } + cur, err := u.FindCursor(filter, opts...) + if err != nil { + return err + } + return handleDbError(cur.All(u.ctx, model)) +} + +// FindCursor 查询多条数据并返回游标。 +// Cursor 不是并发安全的,调用方必须在完成或失败后关闭它。 +func (u *MongoTool) FindCursor(filter bson.M, opts ...*options.FindOptions) (*mongo.Cursor, error) { + for _, opt := range opts { + if opt == nil { + continue + } + if opt.Limit != nil && *opt.Limit > 1000 { + fmt.Println("limit beyond 1000 ==================>", *opt.Limit) + } + if opt.Sort != nil { + if err := sortCheck(opt.Sort); err != nil { + return nil, err + } + } + } + return u.coll.Find(u.ctx, filter, opts...) +} + +// FindOne 单条查询 +func (u *MongoTool) FindOne(model interface{}, filter bson.M, opts ...*options.FindOneOptions) error { + for _, opt := range opts { + if opt.Sort != nil { + if err := sortCheck(opt.Sort); err != nil { + return err + } + } + } + return handleDbError(u.coll.FindOne(u.ctx, filter, opts...).Decode(model)) +} + +// FindOneAndDelete 单条查询并删除 +func (u *MongoTool) FindOneAndDelete(model interface{}, filter bson.M, opts ...*options.FindOneAndDeleteOptions) error { + for _, opt := range opts { + if opt.Sort != nil { + if err := sortCheck(opt.Sort); err != nil { + return err + } + } + } + return handleDbError(u.coll.FindOneAndDelete(u.ctx, filter, opts...).Decode(model)) +} + +// FindOneAndReplace 单条查询 rd set to Before 表示返回原始数据, set to After 表示返回替换后的数据 +func (u *MongoTool) FindOneAndReplace(model interface{}, filter bson.M, replacement bson.M, opts ...*options.FindOneAndReplaceOptions) error { + for _, opt := range opts { + if opt.Sort != nil { + if err := sortCheck(opt.Sort); err != nil { + return err + } + } + } + return handleDbError(u.coll.FindOneAndReplace(u.ctx, filter, replacement, opts...).Decode(model)) +} + +// FindOneAndUpdate 单条查询 rd set to Before 表示返回原始数据, set to After 表示返回更新后的数据 默认为返回更新后的数据 +func (u *MongoTool) FindOneAndUpdate(model interface{}, filter bson.M, update bson.M, opts ...*options.FindOneAndUpdateOptions) error { + for _, opt := range opts { + if opt.Sort != nil { + if err := sortCheck(opt.Sort); err != nil { + return err + } + } + } + setReturn := false + for _, opt := range opts { + if opt != nil && opt.ReturnDocument != nil { + setReturn = true + break + } + } + if !setReturn { + after := options.After + opts = append(opts, &options.FindOneAndUpdateOptions{ReturnDocument: &after}) + } + return handleDbError(u.coll.FindOneAndUpdate(u.ctx, filter, update, opts...).Decode(model)) +} + +// FindOneAndUpsert 单条查询 匹配到数据更新,未匹配到数据则upsert +func (u *MongoTool) FindOneAndUpsert(model interface{}, filter bson.M, update bson.M, opts ...*options.FindOneAndUpdateOptions) error { + upsert := true + var beforeOrAfter options.ReturnDocument + for _, opt := range opts { + if opt.Sort != nil { + if err := sortCheck(opt.Sort); err != nil { + return err + } + } + } + setReturn := false + for _, opt := range opts { + if opt != nil && opt.ReturnDocument != nil { + beforeOrAfter = *opt.ReturnDocument + setReturn = true + break + } + } + if !setReturn { + beforeOrAfter = options.After + } + opts = append(opts, &options.FindOneAndUpdateOptions{ReturnDocument: &beforeOrAfter, Upsert: &upsert}) + return handleDbError(u.coll.FindOneAndUpdate(u.ctx, filter, update, opts...).Decode(model)) +} + +func (u *MongoTool) FindOneAndUpdateReturnTiny(bind interface{}, query bson.M, update bson.M, afterDoc bool, opts ...*options.FindOneAndUpdateOptions) error { + if afterDoc { + opts = append(opts, options.FindOneAndUpdate().SetReturnDocument(options.After)) + } else { + opts = append(opts, options.FindOneAndUpdate().SetReturnDocument(options.Before)) + } + result := handleDbError(u.coll.FindOneAndUpdate(u.ctx, query, update, opts...).Decode(bind)) + return result +} + +// FindOneByID 通过id查找一条数据 +func (u *MongoTool) FindOneByID(model interface{}, id primitive.ObjectID, opts ...*options.FindOneOptions) error { + for _, opt := range opts { + if opt.Sort != nil { + if err := sortCheck(opt.Sort); err != nil { + return err + } + } + } + return handleDbError(u.coll.FindOne(u.ctx, bson.M{"_id": id}, opts...).Decode(model)) +} + +// Aggregate 聚合查找数据 +func (u *MongoTool) Aggregate(model interface{}, pipeline []bson.M, opts ...*options.AggregateOptions) error { + if err := validInterfaceSlice(model); err != nil { + return err + } + cur, err := u.coll.Aggregate(u.ctx, pipeline, opts...) + if err != nil { + return err + } + return handleDbError(cur.All(u.ctx, model)) +} + +// AggregateDecode 聚合.Decode +func (u *MongoTool) AggregateDecode(model interface{}, pipeline []bson.M, opts ...*options.AggregateOptions) error { + cur, err := u.coll.Aggregate(u.ctx, pipeline, opts...) + if err != nil { + return err + } + if cur.Next(u.ctx) { + return handleDbError(cur.Decode(model)) + } + return nil +} + +// Distinct 去重查询 +func (u *MongoTool) Distinct(fieldName string, filter bson.M, opts ...*options.DistinctOptions) ([]interface{}, error) { + return u.coll.Distinct(u.ctx, fieldName, filter, opts...) +} + +// DeleteOne 删除一条数据 +func (u *MongoTool) DeleteOne(filter bson.M, opt ...*options.DeleteOptions) (*mongo.DeleteResult, error) { + return u.coll.DeleteOne(u.ctx, filter, opt...) +} + +// DeleteMany 删除多条数据 +func (u *MongoTool) DeleteMany(filter bson.M, opt ...*options.DeleteOptions) (*mongo.DeleteResult, error) { + return u.coll.DeleteMany(u.ctx, filter, opt...) +} + +// DeleteById 根据ID删除数据单条数据 +func (u *MongoTool) DeleteById(id primitive.ObjectID) (*mongo.DeleteResult, error) { + return u.coll.DeleteOne(u.ctx, bson.M{"_id": id}) +} + +// UpdateOne 更新单条数据 +func (u *MongoTool) UpdateOne(filter bson.M, update interface{}) (*mongo.UpdateResult, error) { + return u.coll.UpdateOne(u.ctx, filter, update) +} + +// UpdateMany 修改多条数据 +func (u *MongoTool) UpdateMany(filter bson.M, update bson.M) (*mongo.UpdateResult, error) { + return u.coll.UpdateMany(u.ctx, filter, update) +} + +// UpsertMany 或者修改或者插入多条数据 +func (u *MongoTool) UpsertMany(filter bson.M, update bson.M) (*mongo.UpdateResult, error) { + return u.coll.UpdateMany(u.ctx, filter, update, options.Update().SetUpsert(true)) +} + +// UpsertOne 或者修改或者插入一条数据 +func (u *MongoTool) UpsertOne(filter bson.M, update bson.M) (*mongo.UpdateResult, error) { + return u.coll.UpdateOne(u.ctx, filter, update, options.Update().SetUpsert(true)) +} + +// UpdateOneForSet 修改一条数据 【根据修改数据中集合类型字段】 +func (u *MongoTool) UpdateOneForSet(filter bson.M, update bson.D) (*mongo.UpdateResult, error) { + return u.coll.UpdateOne(u.ctx, filter, update) +} + +// Count 获取数量 +func (u *MongoTool) Count(filter interface{}, opts ...*options.CountOptions) (int64, error) { + if reflect.TypeOf(filter).Kind() == reflect.Slice { + cur, err := u.coll.Aggregate(u.ctx, filter) + if err != nil { + return 0, err + } + var count int64 = 0 + for cur.Next(context.TODO()) { + count++ + } + return count, nil + } + return u.coll.CountDocuments(u.ctx, filter, opts...) +} + +// EstimateCount 获取数量 +func (u *MongoTool) EstimateCount(opts ...*options.EstimatedDocumentCountOptions) (int64, error) { + return u.coll.EstimatedDocumentCount(u.ctx, opts...) +} + +// Bulk Bulk +func (u *MongoTool) Bulk(models []mongo.WriteModel, opts ...*options.BulkWriteOptions) (*mongo.BulkWriteResult, error) { + return u.coll.BulkWrite(u.ctx, models, opts...) +} + +// Exists 是否存在数据 +func (u *MongoTool) Exists(filter interface{}, opts ...*options.FindOneOptions) (bool, error) { + var limit int64 = 1 + lo := &options.CountOptions{Limit: &limit} + n, err := u.Count(filter, lo) + if err != nil { + return false, err + } + return n > 0, nil +} + +func setTimeForSlice(docs interface{}) []interface{} { + arr := reflect.ValueOf(docs) + if arr.Kind() == reflect.Ptr { + arr = reflect.ValueOf(docs).Elem() + } + result := make([]interface{}, arr.Len()) + for i := 0; i < arr.Len(); i++ { + ele := arr.Index(i) + now := time.Now() + if ma := ele.FieldByName("UpdatedAt"); ma.IsValid() { + ma.Set(reflect.ValueOf(now)) + } + if ca := ele.FieldByName("CreatedAt"); ca.IsValid() { + ca.Set(reflect.ValueOf(now)) + } + result[i] = ele.Interface() + } + return result +} + +func validInterfaceSlice(bind interface{}) error { + t := reflect.TypeOf(bind) + k := t.Kind() + if t.Kind() == reflect.Ptr { + k = t.Elem().Kind() + } + if k != reflect.Slice { + return stderr.MustSliceOrSlicePtr + } + return nil +} diff --git a/common/db/mongoClient.go b/common/db/mongoClient.go new file mode 100644 index 0000000..3c60688 --- /dev/null +++ b/common/db/mongoClient.go @@ -0,0 +1,307 @@ +package db + +import ( + "context" + "errors" + "fmt" + "os" + "runtime/debug" + "strings" + "time" + + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/x/mongo/driver/connstring" +) + +var dataSource = make(map[string]*MongoDB) +var registerPool []Register + +// DBmap +type DBmap struct { + Key string + URL string +} + +type Register struct { + Key string + Table []string +} + +// MongoOptions 数据库配置 +type MongoOptions struct { + URL string `json:"url"` // 服务器连接地址 +} + +// MongoDB MongoDB +type MongoDB struct { + db *mongo.Database +} + +func (m *MongoDB) Tool() *MongoTool { + return m.ToolCtx(context.Background()) +} + +func (m *MongoDB) ToolCtx(ctx context.Context) *MongoTool { + return &MongoTool{db: m.db, ctx: ctx} +} + +// Coll 获取表名 +func (m *MongoDB) Coll(name string) *MongoTool { + t := m.Tool() + return t.Coll(name) +} + +// CollCtx 获取表名 从外部传入ctx +func (m *MongoDB) CollCtx(ctx context.Context, name string) *MongoTool { + t := m.ToolCtx(ctx) + return t.Coll(name) +} + +// MongoTool mongo官方库事务封装 +type MongoTool struct { + db *mongo.Database + ctx context.Context // 当前使用的ctx + coll *mongo.Collection +} + +func (m *MongoTool) Coll(name string) *MongoTool { + opt := options.CollectionOptions{Registry: registry} + m.coll = m.db.Collection(name, &opt) + return m +} + +// Trans 开启事务处理包裹处理,里面处理的全是利用的事务的ctx +func (m *MongoDB) Trans(fn func(*MongoTool) error, opts ...*TransOpts) error { + return m.TransCtx(context.Background(), fn, opts...) +} + +// TransCtx 外部传入ctx +func (m *MongoDB) TransCtx(ctx context.Context, fn func(*MongoTool) error, opts ...*TransOpts) error { + t := m.ToolCtx(ctx) + return m.db.Client().UseSession(ctx, func(sessionContext mongo.SessionContext) error { + if err := sessionContext.StartTransaction(); err != nil { + return err + } + defer func() { + if err := recover(); err != nil { + log.Warn(fmt.Sprintf("caught panic during transaction, aborting. err: %+v, stack: %s", err, debug.Stack())) + if err := sessionContext.AbortTransaction(sessionContext); err != nil { + log.Warn("mongo AbortTransaction panic err", log.E(err)) + } + } + sessionContext.EndSession(sessionContext) + }() + t.ctx = sessionContext + if err := runTransactionWithRetry(t, fn, MergeTransOpts(opts)); err != nil { + if strings.Contains(err.Error(), "NoSuchTransaction") { + log.Warn("NoSuchTransaction error, return") + return err + } + if err := sessionContext.AbortTransaction(sessionContext); err != nil { + log.Warn("mongo AbortTransaction err", log.E(err)) + } + log.Warn("caught exception during transaction, aborting.", log.E(err)) + sessionContext.EndSession(sessionContext) + return err + } + return commitWithRetry(sessionContext) + }) +} + +// runTransactionWithRetry is an example function demonstrating transaction retry logic. +func runTransactionWithRetry(t *MongoTool, txnFn func(t *MongoTool) error, opts *TransOpts) error { + //no set ReEntryCount is loop retry + for { + err := txnFn(t) // Performs transaction. + if err == nil { + return nil + } + log.Warn("Transaction aborted. Caught exception during transaction.", log.E(err)) + // If transient error, retry the whole transaction + if strings.Contains(err.Error(), "NoSuchTransaction") { + log.Info("NoSuchTransaction error,return and break retry loop") + return err + } + cmdErr, ok := err.(mongo.CommandError) + if ok && cmdErr.HasErrorLabel("TransientTransactionError") { + if opts != nil { + if opts.ReEntryCount != nil { + if *opts.ReEntryCount <= 0 { + return cmdErr + } + *opts.ReEntryCount-- + } + } + log.Info("TransientTransactionError, retrying transaction...") + continue + } + // else return err + return err + } +} + +// commitWithRetry is an example function demonstrating transaction retry logic. +func commitWithRetry(sess mongo.SessionContext) error { + for { + err := sess.CommitTransaction(sess) + switch e := err.(type) { + case nil: + log.Info("Transaction committed.") + return nil + case mongo.CommandError: + // Can retry commit + if e.HasErrorLabel("UnknownTransactionCommitResult") { + log.Info("UnknownTransactionCommitResult, retrying commit operation...") + continue + } + log.Info("Error during commit...") + return e + default: + log.Info("Error during commit...") + return e + } + } +} + +// Disconnect 关闭数据库连接 +func (m *MongoDB) Disconnect() error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + log.Info("closing mongodb connections") + defer cancel() + if err := m.db.Client().Disconnect(ctx); err != nil { + log.Warn(fmt.Sprintf("close mongo connections err: %+v", err)) + return err + } + return nil +} + +// InitMongo 初始化Mongo +func InitMongo(murl string) (*MongoDB, error) { + cs, err := connstring.Parse(murl) + if err != nil { + log.Error("mongo URL parse fail", log.Any("url", murl), log.E(err)) + return nil, err + } + db := cs.Database + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + // Connect to MongoDB + mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(murl)) + if err != nil { + log.Error("mongodb connect fail", log.E(err)) + return nil, err + } + // Check the connection + if err = mongoClient.Ping(context.Background(), nil); err != nil { + log.Error("mongodb connect ping is fail") + return nil, err + } + mongoDataBase := mongoClient.Database(db) + return &MongoDB{db: mongoDataBase}, nil +} + +func InitDS(dbmap []DBmap, register []Register) map[string]*MongoDB { + if len(dbmap) == 0 { + log.Error("dbmap must be not empty ") + os.Exit(1) + } + for _, v := range dbmap { + db, err := InitMongo(v.URL) + if err != nil { + log.Error(fmt.Sprintf("[DB-%s] start up error", v.Key), log.E(err)) + os.Exit(1) + } + log.Info(fmt.Sprintf("[DB-%s] connect is successfully", v.Key)) + dataSource[v.Key] = db + } + registerPool = register + return dataSource +} + +func Init(tableName string) *MongoDB { + if tableName == "" { + panic(errors.New("table name must not be empty")) + } + return Selector(tableName) +} + +// 初始化只读数据 +func InitRead(tableName string) *MongoDB { + if tableName == "" { + panic(errors.New("table name must not be empty")) + } + return SelectorRead(tableName) +} + +func CloseDS() { + if len(dataSource) == 0 { + log.Warn("dataSource empty ") + return + } + for k, v := range dataSource { + if err := v.Disconnect(); err != nil { + log.Error(fmt.Sprintf("[DB-%s]Mongo Disconnect error", k), log.E(err)) + continue + } + log.Info(fmt.Sprintf("[DB-%s]Mongo Disconnect OK", k)) + } +} + +// BaseDAO 如果ctx为nil 则表示不使用事务,如果ctx不为空则表示使用事务 +func BaseDAO(tableName string, ctx context.Context) *MongoTool { + if tableName == "" { + panic(errors.New("table name must not be empty")) + } + mongdb := Selector(tableName) + if ctx == nil { + return &MongoTool{db: mongdb.db, ctx: context.Background(), coll: mongdb.db.Collection(tableName)} + } + return &MongoTool{db: mongdb.db, ctx: ctx, coll: mongdb.db.Collection(tableName)} +} + +func Selector(tableName string) *MongoDB { + var isExist = false + var db *MongoDB + for _, r := range registerPool { + if strings.HasPrefix(r.Key, "Read") { + continue + } + for _, v := range r.Table { + if v == tableName { + isExist = true + db = dataSource[r.Key] + break + } + } + } + if !isExist { + log.Warn("current table not register,please register it first", log.Any("table", tableName)) + panic(errors.New("current table not register")) + } + return db +} + +func SelectorRead(tableName string) *MongoDB { + var isExist = false + var db *MongoDB + for _, r := range registerPool { + if !strings.HasPrefix(r.Key, "Read") { + continue + } + for _, v := range r.Table { + if v == tableName { + isExist = true + db = dataSource[r.Key] + break + } + } + } + if !isExist { + log.Warn("current table not register,please register it first", log.Any("table", tableName)) + panic(errors.New("current table not register")) + } + return db +} diff --git a/common/db/opt.go b/common/db/opt.go new file mode 100644 index 0000000..97b5694 --- /dev/null +++ b/common/db/opt.go @@ -0,0 +1,27 @@ +package db + +// 事务设置 +type TransOpts struct { + ReEntryCount *int //重入次数 + //... +} + +func (t *TransOpts) SetReEntry(count int) *TransOpts { + t.ReEntryCount = &count + return t +} + +// MergeTransOpts 合并事务设置 +func MergeTransOpts(opts []*TransOpts) *TransOpts { + transOpts := &TransOpts{} + for _, opt := range opts { + if opt == nil { + continue + } + if opt.ReEntryCount != nil { + transOpts.ReEntryCount = opt.ReEntryCount + //... + } + } + return transOpts +} diff --git a/common/deduction.go b/common/deduction.go new file mode 100644 index 0000000..5140edf --- /dev/null +++ b/common/deduction.go @@ -0,0 +1,26 @@ +package common + +import "math/rand" + +// CanDeductByProbability 扣量指标的未完成度和今日时间进度递增 能扣量的概率递增 [0,100] +func CanDeductByProbability(probability float64) bool { + if probability <= 0 { + return false + } + if probability == 0 { + return false + } + if probability >= 1 { + return true //绝对扣量 + } + canDeduct := probability > rand.Float64() //根据 probability 概率决定是否扣量 + return canDeduct +} + +// ConDeductionRatio 格式:% +func ConsumeDeductionRatio(optDividendRatio, dividendRatio float64) float64 { + if optDividendRatio == 0 { + return dividendRatio + } + return (1 - dividendRatio/optDividendRatio) +} diff --git a/common/dramatopic/topic.go b/common/dramatopic/topic.go new file mode 100644 index 0000000..6eca516 --- /dev/null +++ b/common/dramatopic/topic.go @@ -0,0 +1,89 @@ +package dramatopic + +import ( + "strings" + "time" + + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/sysconfmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + TypeSystem = "SYSTEM" + TypeCustom = "CUSTOM" + + IDHot = "hot_recommend" + IDLatest = "latest" + IDEveryone = "everyone_likes" + + KeyHot = "HOT_RECOMMEND" + KeyLatest = "LATEST" + KeyEveryone = "EVERYONE_LIKES" +) + +type SystemTopic struct { + ID string + Name string + SystemKey string + Sort int + VCode sysconfmod.VCode + TieOrder int +} + +func SystemTopics() []SystemTopic { + defaults := map[sysconfmod.VCode]int{ + sysconfmod.VCodeDramaTopicHotSort: 500, + sysconfmod.VCodeDramaTopicLatestSort: 400, + sysconfmod.VCodeDramaTopicEveryoneSort: 300, + } + values, err := sysconfdata.GetIntsFromSharedCache(defaults) + if err != nil { + values = defaults + } + topics := []SystemTopic{ + {ID: IDHot, Name: "热门推荐", SystemKey: KeyHot, Sort: values[sysconfmod.VCodeDramaTopicHotSort], VCode: sysconfmod.VCodeDramaTopicHotSort, TieOrder: 3}, + {ID: IDLatest, Name: "最新上架", SystemKey: KeyLatest, Sort: values[sysconfmod.VCodeDramaTopicLatestSort], VCode: sysconfmod.VCodeDramaTopicLatestSort, TieOrder: 2}, + {ID: IDEveryone, Name: "大家爱看", SystemKey: KeyEveryone, Sort: values[sysconfmod.VCodeDramaTopicEveryoneSort], VCode: sysconfmod.VCodeDramaTopicEveryoneSort, TieOrder: 1}, + } + return topics +} + +func FindSystem(id string) (SystemTopic, bool) { + for _, topic := range SystemTopics() { + if topic.ID == id { + return topic, true + } + } + return SystemTopic{}, false +} + +// ModuleID returns the configured hot-drama module that owns custom topics. +// It follows the same compatibility fallback as the short-drama channel. +func ModuleID(now time.Time) (primitive.ObjectID, bool, error) { + modules, err := moduleconfmod.GetModuleConfByType(moduleconfmod.Drama) + if err != nil { + return primitive.NilObjectID, false, err + } + active := make([]moduleconfmod.ModuleConf, 0, len(modules)) + for _, module := range modules { + if module.IsActiveAt(now) { + active = append(active, module) + } + } + for _, module := range active { + name := strings.ToLower(strings.TrimSpace(module.ModuleName + " " + module.SubModuleName)) + if strings.Contains(name, "热门") || strings.Contains(name, "hot") { + return module.ID, true, nil + } + } + if len(active) > 1 { + return active[1].ID, true, nil + } + if len(active) == 1 { + return active[0].ID, true, nil + } + return primitive.NilObjectID, false, nil +} diff --git a/common/elastic/elastic.go b/common/elastic/elastic.go new file mode 100644 index 0000000..8a11907 --- /dev/null +++ b/common/elastic/elastic.go @@ -0,0 +1,110 @@ +package elastic + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + + "github.com/elastic/go-elasticsearch/v8" +) + +var ( + AnalyzerIkSmart = "ik_smart" + AnalyzerIkMaxWord = "ik_max_word" + NumberOfShards = 1 + NumberOfReplicas = 1 +) + +// Client +type Client struct { + Client *elasticsearch.Client +} + +func unmarshalAggregateBody(bind interface{}, resp *Response) error { + var v map[string]interface{} + if bind == nil { //传入空表示只执行 defer + return nil + } + if err := json.NewDecoder(resp.Body).Decode(&v); err != nil { + return err + } + if len(v) > 0 { + if v, ok := v["aggregations"].(map[string]interface{}); ok { + b, _ := json.Marshal(v) + _ = json.Unmarshal(b, &bind) + } + } + return nil +} + +func unmarshalBodyWithTotal(bind interface{}, resp *Response) error { + var v map[string]interface{} + if bind == nil { //传入空表示只执行 defer + return nil + } + if err := json.NewDecoder(resp.Body).Decode(&v); err != nil { + return err + } + if len(v) > 0 { + if h, ok := v["hits"].(map[string]interface{}); ok { + b, err := json.Marshal(h) + if err != nil { + return err + } + if err = json.Unmarshal(b, &bind); err != nil { + return err + } + } + } + return nil +} + +func unmarshalBody(bind interface{}, resp *Response) error { + var v map[string]interface{} + if bind == nil { //传入空表示只执行 defer + return nil + } + if err := json.NewDecoder(resp.Body).Decode(&v); err != nil { + return err + } + if len(v) > 0 { + if h, ok := v["hits"].(map[string]interface{}); ok { + if hh, ok := h["hits"].([]interface{}); ok { + b, _ := json.Marshal(hh) + _ = json.Unmarshal(b, &bind) + } + } else if _, ok := v["docs"].([]interface{}); ok { + b, _ := json.Marshal(v["docs"]) + _ = json.Unmarshal(b, &bind) + } else if _, ok := v["_source"].(map[string]interface{}); ok { + b, _ := json.Marshal(v) + _ = json.Unmarshal(b, &bind) + } + } + return nil +} + +func marshalSearchM(query M) io.Reader { + var buf bytes.Buffer + if len(query) == 0 { + return nil + } + if err := json.NewEncoder(&buf).Encode(query); err != nil { + return nil + } + return bytes.NewReader(buf.Bytes()) +} + +func marshalBulkM(source M) io.Reader { + var buf bytes.Buffer + for k, v := range source { + var meta = []byte(fmt.Sprintf(`{ "index" : { "_id" : "%s" } }%s`, k, "\n")) + var data, _ = json.Marshal(v) + data = append(data, "\n"...) + buf.Grow(len(meta) + len(data)) + buf.Write(meta) + buf.Write(data) + } + return bytes.NewReader(buf.Bytes()) +} diff --git a/common/elastic/elasticClient.go b/common/elastic/elasticClient.go new file mode 100644 index 0000000..d71c129 --- /dev/null +++ b/common/elastic/elasticClient.go @@ -0,0 +1,267 @@ +package elastic + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "time" + + "91porn-server/common/log" + + "github.com/elastic/go-elasticsearch/v8/esapi" +) + +type M map[string]interface{} + +type A []map[string]interface{} + +type MGetBody struct { + ID string `json:"_id"` +} + +// ElasticResp 响应 +type Response = esapi.Response + +type Options struct { + Address []string + MaxIdleConnsPerHost int //每个client z最多允许的空闲连接数 + IdleConnTimeout time.Duration //空闲连接超时时间 + UserName string + PassWord string +} + +// 查看集群信息 +func (c *Client) Info() (*Response, error) { + resp, err := c.Client.Info(c.Client.Info.WithContext(context.Background())) + if err != nil { + return nil, err + } + resp.Body.Close() + return resp, err +} + +// 初始化索引 +func (c *Client) CreateIndices(index string, setting M) error { + resp, err := c.Client.Indices.Create(index, func(request *esapi.IndicesCreateRequest) { + request.Body = marshalSearchM(setting) + }) + if err != nil { + return err + } + resp.Body.Close() + return nil +} + +// search 搜索 +func (c *Client) Search(index string, bind interface{}, query M, o ...func(*esapi.SearchRequest)) error { + o = append(o, + c.Client.Search.WithContext(context.Background()), + c.Client.Search.WithIndex(index), + c.Client.Search.WithBody(marshalSearchM(query)), + c.Client.Search.WithTrackTotalHits(true), + c.Client.Search.WithPretty(), + ) + resp, err := c.Client.Search(o...) + if err != nil { + return err + } + defer resp.Body.Close() + return unmarshalBody(bind, resp) +} + +func (c *Client) SearchWithTotal(index string, bind interface{}, query M, o ...func(*esapi.SearchRequest)) error { + o = append(o, + c.Client.Search.WithContext(context.Background()), + c.Client.Search.WithIndex(index), + c.Client.Search.WithBody(marshalSearchM(query)), + c.Client.Search.WithTrackTotalHits(true), + c.Client.Search.WithPretty(), + ) + resp, err := c.Client.Search(o...) + if err != nil { + return err + } + defer resp.Body.Close() + return unmarshalBodyWithTotal(bind, resp) +} + +// Aggregate Aggregate 聚合 +func (c *Client) Aggregate(index string, bind interface{}, query M, o ...func(*esapi.SearchRequest)) (int, error) { + o = append(o, + c.Client.Search.WithContext(context.Background()), + c.Client.Search.WithIndex(index), + c.Client.Search.WithBody(marshalSearchM(query)), + c.Client.Search.WithTrackTotalHits(true), + c.Client.Search.WithPretty(), + ) + resp, err := c.Client.Search(o...) + if err != nil { + return 0, err + } + defer resp.Body.Close() + return resp.StatusCode, unmarshalAggregateBody(bind, resp) +} + +// Get 根据ID搜索 +func (c *Client) Get(index string, bind interface{}, id string, o ...func(*esapi.GetRequest)) (int, error) { + o = append(o, + c.Client.Get.WithContext(context.Background()), + c.Client.Get.WithPretty(), + ) + resp, err := c.Client.Get(index, id, o...) + if err != nil { + return 0, err + } + defer resp.Body.Close() + return resp.StatusCode, unmarshalBody(bind, resp) +} + +// MGet MGet 批量查询 +func (c *Client) MGet(index string, bind interface{}, ids []string, o ...func(*esapi.MgetRequest)) error { + m := make([]MGetBody, len(ids)) + for i, v := range ids { + m[i] = MGetBody{ID: v} + } + o = append(o, + c.Client.Mget.WithIndex(index), + c.Client.Mget.WithContext(context.Background()), + ) + resp, err := c.Client.Mget(marshalSearchM(M{"docs": m}), o...) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("%+v", resp) + } + return unmarshalBody(bind, resp) +} + +// BulkDelete 函数 +func (c *Client) BulkDelete(index string, source M, o ...func(*esapi.BulkRequest)) error { + o = append(o, + c.Client.Bulk.WithIndex(index), + c.Client.Bulk.WithContext(context.Background()), + c.Client.Bulk.WithRefresh("true"), + ) + resp, err := c.Client.Bulk(marshalDeleteBulkM(source), o...) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.IsError() { + return fmt.Errorf("error response: %s", resp.String()) + } + return nil +} + +func marshalDeleteBulkM(source M) io.Reader { + var buf bytes.Buffer + for k, _ := range source { + var meta = []byte(fmt.Sprintf(`{ "delete" : { "_id" : "%s" } }%s`, k, "\n")) + buf.Grow(len(meta)) + buf.Write(meta) + } + return bytes.NewReader(buf.Bytes()) +} + +// 批量插入 +func (c *Client) Bulk(index string, source M, o ...func(*esapi.BulkRequest)) error { + o = append(o, + c.Client.Bulk.WithIndex(index), + c.Client.Bulk.WithContext(context.Background()), + ) + resp, err := c.Client.Bulk(marshalBulkM(source), o...) + if err != nil { + return err + } + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("%+v", resp) + } + return nil +} + +// BulkChecked 在 HTTP 成功后继续检查每条写入结果,供需要可靠推进同步进度的任务使用。 +func (c *Client) BulkChecked(index string, source M) error { + if len(source) == 0 { + return nil + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + resp, err := c.Client.Bulk(marshalBulkM(source), + c.Client.Bulk.WithIndex(index), c.Client.Bulk.WithContext(ctx)) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bulk %s: HTTP %d", index, resp.StatusCode) + } + var result struct { + Errors bool `json:"errors"` + Items []map[string]struct { + ID string `json:"_id"` + Status int `json:"status"` + Error json.RawMessage `json:"error"` + } `json:"items"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("decode bulk %s response: %w", index, err) + } + if len(result.Items) != len(source) { + return fmt.Errorf("bulk %s: expected %d results, got %d", index, len(source), len(result.Items)) + } + for _, item := range result.Items { + entry, ok := item["index"] + if !ok || len(item) != 1 { + return fmt.Errorf("bulk %s: missing index result", index) + } + if entry.Status < 200 || entry.Status >= 300 || (len(entry.Error) > 0 && string(entry.Error) != "null") { + return fmt.Errorf("bulk %s: document %s failed, status %d", index, entry.ID, entry.Status) + } + } + if result.Errors { + return fmt.Errorf("bulk %s: response contains errors", index) + } + return nil +} + +// Count 获取数量 +func (c *Client) Count(index string, query M, o ...func(*esapi.CountRequest)) (cnt int, err error) { + o = append(o, + c.Client.Count.WithIndex(index), + c.Client.Count.WithBody(marshalSearchM(query)), + c.Client.Count.WithContext(context.Background()), + ) + resp, err := c.Client.Count(o...) + if err != nil { + return 0, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return 0, fmt.Errorf("%+v", resp) + } + var bind struct { + Count int `json:"count"` + } + return bind.Count, unmarshalBody(&bind, resp) +} + +func (c *Client) Ping() error { + resp, err := c.Client.Ping(c.Client.Ping.WithContext(context.Background())) + if err != nil { + log.Warn("elasticSarch ping error ", log.E(err)) + return err + } + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + log.Warn("elasticSarch ping failed ", log.Any("status code", resp.StatusCode)) + return errors.New("elasticSarch ping failed") + } + return nil +} diff --git a/common/elastic/elasticInit.go b/common/elastic/elasticInit.go new file mode 100644 index 0000000..1fa28b4 --- /dev/null +++ b/common/elastic/elasticInit.go @@ -0,0 +1,39 @@ +package elastic + +import ( + "errors" + + "91porn-server/common/log" + + "github.com/elastic/go-elasticsearch/v8" +) + +var esClient *Client + +// Client 获取elastic客户端 +func InitElastic(opt Options) (*Client, error) { + if len(opt.Address) == 0 { + return nil, errors.New("elasticSearch address is empty") + } + cfg := elasticsearch.Config{ + Addresses: opt.Address, + Username: opt.UserName, + Password: opt.PassWord, + } + es, err := elasticsearch.NewClient(cfg) + if err != nil { + log.Error("create elasticSearch client occour error", log.E(err)) + return nil, err + } + esClient = &Client{Client: es} + if err = esClient.Ping(); err != nil { + log.Warn("elasticSarch ping failed", log.E(err)) + return nil, err + } + log.Info("init elasticSearch successful") + return esClient, nil +} + +func Init() *Client { + return esClient +} diff --git a/common/email/email.go b/common/email/email.go new file mode 100644 index 0000000..08d5d7d --- /dev/null +++ b/common/email/email.go @@ -0,0 +1,74 @@ +package email + +import ( + "bytes" + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ses" + "github.com/pkg/errors" + gomail "gopkg.in/gomail.v2" +) + +// Send sends email without attachments. +func (c *EmailClient) Send(ctx context.Context, sender string, mailList []*string, title string, body string) error { + if err := check(sender, mailList); err != nil { + return err + } + sesEmailInput := &ses.SendEmailInput{ + Destination: &ses.Destination{ + ToAddresses: mailList, + }, + Message: &ses.Message{ + Body: &ses.Body{ + Html: &ses.Content{ + Data: aws.String(body)}, + }, + Subject: &ses.Content{ + Data: aws.String(title), + }, + }, + Source: aws.String(sender), + } + if _, err := c.ses.SendEmail(sesEmailInput); err != nil { + return errors.Wrap(err, "send email failed") + } + return nil +} + +// SendRaw send email that supports attachments. +func (c *EmailClient) SendRaw(ctx context.Context, sender string, mailList []*string, title string, body string, attachments []string) error { + if err := check(sender, mailList); err != nil { + return err + } + msg := gomail.NewMessage(gomail.SetCharset("UTF-8")) + msg.SetHeader("From", sender) + toList := make([]string, len(mailList)) + for i, l := range mailList { + toList[i] = *l + } + msg.SetHeader("To", toList...) + msg.SetHeader("Subject", title) + msg.SetBody("text/html", body) + for _, a := range attachments { + msg.Attach(a) + } + var emailRaw bytes.Buffer + _, _ = msg.WriteTo(&emailRaw) + if _, err := c.ses.SendRawEmail(&ses.SendRawEmailInput{ + RawMessage: &ses.RawMessage{Data: emailRaw.Bytes()}, + }); err != nil { + return errors.Wrap(err, "send raw email failed") + } + return nil +} + +func check(sender string, mailList []*string) error { + if sender == "" { + return errors.New("no sender") + } + if len(mailList) == 0 { + return errors.New("no recipient") + } + return nil +} diff --git a/common/email/init.go b/common/email/init.go new file mode 100644 index 0000000..149a899 --- /dev/null +++ b/common/email/init.go @@ -0,0 +1,37 @@ +package email + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ses" +) + +type Config struct { + Region string `json:"region"` // aws region + AccessKeyID string `json:"accessKeyID"` // aws accessKeyID + SecretAccessKey string `json:"secretAccessKey"` // aws secretAccessKey + VerifiedDomain string `json:"verifiedDomain"` // aws verifiedDomain. The sender's email must in this domain. +} + +type EmailClient struct { + ses *ses.SES +} + +var Client *EmailClient + +// MustInit 初始化ses连接 +func MustInit(ctx context.Context, cfg Config) error { + session, err := session.NewSession(&aws.Config{ + Region: &cfg.Region, + Credentials: credentials.NewStaticCredentials(cfg.AccessKeyID, cfg.SecretAccessKey, ""), + }) + if err != nil { + panic(err) + } + Client = &EmailClient{} + Client.ses = ses.New(session) + return nil +} diff --git a/common/enum/imad/position.go b/common/enum/imad/position.go new file mode 100644 index 0000000..b880267 --- /dev/null +++ b/common/enum/imad/position.go @@ -0,0 +1,15 @@ +package imad + +import "strings" + +const ( + ListBanner = "IM_LIST_BANNER" + ListFeedNative = "IM_LIST_FEED_NATIVE" + ChatNotificationBar = "IM_CHAT_NOTIFICATION_BAR" + ChatFloatingGifBall = "IM_CHAT_FLOATING_GIF_BALL" +) + +// IsPositionCode reports whether code is an IM ad-center location identifier. +func IsPositionCode(code string) bool { + return strings.HasPrefix(strings.TrimSpace(code), "IM_") +} diff --git a/common/export/export.go b/common/export/export.go new file mode 100644 index 0000000..ef7a239 --- /dev/null +++ b/common/export/export.go @@ -0,0 +1,79 @@ +package export + +import ( + "path/filepath" + "reflect" + "strconv" + "time" + + "github.com/tealeg/xlsx" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func ExportFilePath(fileName string) string { + var path, _ = filepath.Abs("temp") + return path + "/" + fileName +} + +func ExportExcel(sheeters []ExcelSheeter, filePath string) error { + f := xlsx.NewFile() + for _, sheeter := range sheeters { + sheet, _ := f.AddSheet(sheeter.GetSheetName()) + for _, rower := range sheeter.GetRows() { + row := sheet.AddRow() + for _, cell := range rower.GetCells() { + c := row.AddCell() + c.Value = getValue(reflect.TypeOf(cell).Kind(), reflect.ValueOf(cell)) + } + } + } + return f.Save(filePath) +} + +func getValue(kind reflect.Kind, value reflect.Value) string { + switch kind { + case reflect.String: + return value.String() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(value.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return strconv.FormatUint(value.Uint(), 10) + case reflect.Bool: + return strconv.FormatBool(value.Bool()) + case reflect.Float32, reflect.Float64: + return strconv.FormatFloat(value.Float(), 'f', 2, 64) + case reflect.Struct: + if timeV, ok := value.Interface().(time.Time); ok { + return timeV.String() + } + if oid, ok := value.Interface().(primitive.ObjectID); ok { + return oid.Hex() + } + return "" + case reflect.Interface: + if timeV, ok := value.Interface().(time.Time); ok { + return timeV.String() + } + return "" + case reflect.Ptr: + if value.IsNil() { + return "" + } + value = value.Elem() + if value.Interface() != nil { + if timeV, ok := value.Interface().(time.Time); ok { + return timeV.String() + } + } else { + return "" + } + case reflect.Array: + if id, ok := value.Interface().(primitive.ObjectID); ok { + if id.IsZero() { + return "" + } + return id.Hex() + } + } + return "未知" +} diff --git a/common/export/interface.go b/common/export/interface.go new file mode 100644 index 0000000..1b2b08e --- /dev/null +++ b/common/export/interface.go @@ -0,0 +1,20 @@ +package export + +type Rower interface { + GetCells() []interface{} +} + +type ExcelSheeter interface { + GetSheetName() string + GetRows() []Rower +} + +type TitleSlice []string + +func (t TitleSlice) GetCells() []interface{} { + list := []interface{}{} + for _, v := range t { + list = append(list, v) + } + return list +} diff --git a/common/file/file.go b/common/file/file.go new file mode 100644 index 0000000..45d85af --- /dev/null +++ b/common/file/file.go @@ -0,0 +1,103 @@ +package file + +import ( + "errors" + "fmt" + "os" + "reflect" + + "github.com/xuri/excelize/v2" +) + +// CreateEmptyFile 创建一个固定size大小的空文件 +// 如果seek文件失败,则删除文件,并返回错误信息 +func CreateEmptyFile(name string, size int64) (f *os.File, err error) { + defer func() { + if err != nil && f != nil { + f.Close() + os.Remove(name) + } + }() + f, err = os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) + if err != nil { + return + } + if _, err = f.Seek(size-1, 0); err != nil { + return + } + _, err = f.Write([]byte{0}) + return +} + +func MakeDir(name string) error { + if err := os.MkdirAll(name, 0755); err != nil { + return errors.New("make dir wrong") + } + return nil +} + +func WriteFile(data []byte, filename string) (err error) { + defer func() { + if err != nil { + _ = os.Remove(filename) + } + }() + pi := GetPathInfo(filename) + if pi.Dir != "" { + if err = os.MkdirAll(pi.Dir, 0755); err != nil { + return + } + } + f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) + if err != nil { + return + } + _, err = f.Write(data) + _ = f.Close() + return +} + +// WriteStruct2Xlsx 将struct切片写入Excel sheet +func WriteStruct2Xlsx(sheet string, records interface{}) *excelize.File { + xlsx := excelize.NewFile() // new file + index, _ := xlsx.NewSheet(sheet) // new sheet + xlsx.SetActiveSheet(index) // set active (default) sheet + t := reflect.TypeOf(records) + if t.Kind() != reflect.Slice { + panic("records must be slice") + } + s := reflect.ValueOf(records) + for i := 0; i < s.Len(); i++ { + elem := s.Index(i).Interface() + elemType := reflect.TypeOf(elem) + elemValue := reflect.ValueOf(elem) + if elemType.Kind() == reflect.Ptr { + elemType = elemType.Elem() + elemValue = elemValue.Elem() + } + if elemType.Kind() != reflect.Struct { + panic("record in slice must be a struct") + } + k := 0 + for j := 0; j < elemType.NumField(); j++ { + field := elemType.Field(j) + tag := field.Tag.Get("xlsx") + if tag == "-" { + continue + } + if tag == "" { + tag = field.Name + } + column, _ := excelize.ColumnNumberToName(k + 1) + k++ + name := tag + // 设置表头 + if i == 0 { + _ = xlsx.SetCellValue(sheet, fmt.Sprintf("%s%d", column, i+1), name) + } + // 设置内容 + _ = xlsx.SetCellValue(sheet, fmt.Sprintf("%s%d", column, i+2), elemValue.Field(j).Interface()) + } + } + return xlsx +} diff --git a/common/file/path.go b/common/file/path.go new file mode 100644 index 0000000..64a6217 --- /dev/null +++ b/common/file/path.go @@ -0,0 +1,32 @@ +package file + +import ( + "path/filepath" + "strings" +) + +// PathInfo 路径信息 +type PathInfo struct { + Dir string // 目录 + Ext string // 扩展名 + FileName string // 没有扩展名的文件名名称 + FullFileName string // 文件全名 +} + +// GetPathInfo 解析路径 +// 返回文件,路径,文件名等信息 +func GetPathInfo(path string) (pi PathInfo) { + if path == "" { + return + } + if !filepath.IsAbs(path) { + path, _ = filepath.Abs(path) + } + ext := filepath.Ext(path) + dir, name := filepath.Split(path) + pi.Dir = dir + pi.Ext = ext + pi.FileName = strings.TrimRight(name, ext) + pi.FullFileName = name + return +} diff --git a/common/file/s3.go b/common/file/s3.go new file mode 100644 index 0000000..d9d53a7 --- /dev/null +++ b/common/file/s3.go @@ -0,0 +1,107 @@ +package file + +import ( + "bytes" + "fmt" + "io" + "mime" + "path" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" +) + +const ( + id = "AKIAJM3XLDNUBXUY36EQ" + secret = "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" + region = "ap-east-1" + bucket = "tknk.zahokc.cn" + URL = "https://tknk.zahokc.cn.s3.ap-east-1.amazonaws.com/" + APPFlag = "ys-7527/" +) + +var ( + endpoint = "" + disableSSL = true + AwsSession *session.Session +) + +func init() { + NewSession() +} + +/** + * 创建session + */ +func NewSession() { + creds := credentials.NewStaticCredentials(id, secret, "") + config := &aws.Config{ + Region: aws.String(region), + Endpoint: &endpoint, + S3ForcePathStyle: aws.Bool(true), + Credentials: creds, + DisableSSL: &disableSSL, + } + se, err := session.NewSession(config) + AwsSession = se + if err != nil { + fmt.Printf("create session fail %+v", err) + return + } +} + +func ListAllBucket() { + svc := s3.New(AwsSession) + resp, err := svc.ListBuckets(&s3.ListBucketsInput{}) + if err != nil { + fmt.Println(err) + } else { + fmt.Println(resp.Buckets) + } +} + +/**上传文件*/ +func PutObject(key string, content []byte, ttl int64) error { + svc := s3.New(AwsSession) + params := &s3.PutObjectInput{ + Bucket: aws.String(bucket), // Required + Key: aws.String(key), // Required + ACL: aws.String("public-read"), //设置成公共读。 + Body: bytes.NewReader(content), + } + duration := time.Duration(ttl * 1000) + ext := path.Ext(key) + if ttl > 0 { + params.SetExpires(time.Now().Add(duration)) + } + if ext != "" { + contentType := mime.TypeByExtension(ext) + if contentType != "" { + params.SetContentType(contentType) + } + } + _, err := svc.PutObject(params) + //svc.PutObjectLegalHold + return err +} + +func GetObject(key string) ([]byte, error) { + svc := s3.New(AwsSession) + params := &s3.GetObjectInput{ + Bucket: aws.String(bucket), // Required + Key: aws.String(key), // Require + } + getObjectOutput, err := svc.GetObject(params) + if err != nil { + return nil, err + } + data, err := io.ReadAll(getObjectOutput.Body) + getObjectOutput.Body.Close() + if err != nil { + return nil, err + } + return data, nil +} diff --git a/common/filter/filter.go b/common/filter/filter.go new file mode 100755 index 0000000..dfb3e9c --- /dev/null +++ b/common/filter/filter.go @@ -0,0 +1,291 @@ +package filter + +import ( + "bufio" + "bytes" + "io" + "unicode" +) + +// Filter 提供敏感词过滤接口 +type Filter interface { + // Filter 文本过滤函数 + // excludes 表示排除指定的字符 + // 返回文本中出现的敏感词,如果敏感词不存在则返回nil + // 如果出现异常,则返回error + Filter(text string, excludes ...rune) ([]string, error) + + // FilterResult 文本过滤函数 + // excludes 表示排除指定的字符 + // 返回文本中出现的敏感词及出现次数,如果敏感词不存在则返回nil + // 如果出现异常,则返回error + FilterResult(text string, excludes ...rune) (map[string]int, error) + + // FilterReader 从可读流中过滤敏感词 + // excludes 表示排除指定的字符 + // 返回可读流中出现的敏感词,如果敏感词不存在则返回nil + // 如果出现异常,则返回error + FilterReader(reader io.Reader, excludes ...rune) ([]string, error) + + // FilterReaderResult 从可读流中过滤敏感词 + // excludes 表示排除指定的字符 + // 返回可读流中出现的敏感词及出现次数,如果敏感词不存在则返回nil + // 如果出现异常,则返回error + FilterReaderResult(reader io.Reader, excludes ...rune) (map[string]int, error) + + // Replace 使用字符替换文本中的敏感词 + // delim 替换的字符 + // 如果出现异常,则返回error + Replace(text string, delim rune) (string, error) +} + +// NewNodeReaderFilter 创建节点过滤器,实现敏感词的过滤 +// 从可读流中读取敏感词数据(以指定的分隔符读取数据) +func NewNodeReaderFilter(rd io.Reader, delim byte) Filter { + nf := &nodeFilter{ + root: newNode(), + } + buf := new(bytes.Buffer) + _, _ = io.Copy(buf, rd) + buf.WriteByte(delim) + for { + line, err := buf.ReadString(delim) + if err != nil { + break + } + if line == "" { + continue + } + nf.addFilterWords(line) + } + buf.Reset() + return nf +} + +// NewNodeChanFilter 创建节点过滤器,实现敏感词的过滤 +// 从通道中读取敏感词数据 +func NewNodeChanFilter(text <-chan string) Filter { + nf := &nodeFilter{ + root: newNode(), + } + for v := range text { + nf.addFilterWords(v) + } + return nf +} + +// NewNodeFilter 创建节点过滤器,实现敏感词的过滤 +// 从切片中读取敏感词数据 +func NewNodeFilter(text []string) Filter { + nf := &nodeFilter{ + root: newNode(), + } + for i, l := 0, len(text); i < l; i++ { + nf.addFilterWords(text[i]) + } + return nf +} + +func newNode() *node { + return &node{ + child: make(map[rune]*node), + } +} + +type node struct { + end bool + child map[rune]*node +} + +type nodeFilter struct { + root *node +} + +func (nf *nodeFilter) addFilterWords(text string) { + n := nf.root + uChars := []rune(text) + for i, l := 0, len(uChars); i < l; i++ { + if unicode.IsSpace(uChars[i]) { + continue + } + if _, ok := n.child[uChars[i]]; !ok { + n.child[uChars[i]] = newNode() + } + n = n.child[uChars[i]] + } + n.end = true +} + +func (nf *nodeFilter) Filter(text string, excludes ...rune) ([]string, error) { + buf := bytes.NewBufferString(text) + defer buf.Reset() + return nf.FilterReader(buf, excludes...) +} + +func (nf *nodeFilter) FilterResult(text string, excludes ...rune) (map[string]int, error) { + buf := bytes.NewBufferString(text) + defer buf.Reset() + return nf.FilterReaderResult(buf, excludes...) +} + +func (nf *nodeFilter) FilterReader(reader io.Reader, excludes ...rune) ([]string, error) { + data, err := nf.FilterReaderResult(reader, excludes...) + if err != nil { + return nil, err + } + result := make([]string, len(data)) + i := 0 + for k := range data { + result[i] = k + i++ + } + return result, nil +} + +func (nf *nodeFilter) FilterReaderResult(reader io.Reader, excludes ...rune) (map[string]int, error) { + var uChars []rune + data := make(map[string]int) + bi := bufio.NewReader(reader) + for { + ur, _, err := bi.ReadRune() + if err != nil { + if err != io.EOF { + return nil, err + } + break + } + if nf.checkExclude(ur, excludes...) { + continue + } + if (unicode.IsSpace(ur) || unicode.IsPunct(ur)) && len(uChars) > 0 { + nf.doFilter(uChars[:], data) + uChars = nil + continue + } + uChars = append(uChars, ur) + } + if len(uChars) > 0 { + nf.doFilter(uChars, data) + } + return data, nil +} + +func (nf *nodeFilter) Replace(text string, delimiter rune) (string, error) { + newNF, err := CheckUpdate() + if err != nil { + return "", err + } + if newNF != nil { + nf = newNF + } + uChars := []rune(text) + indexes := nf.doIndexes(uChars) + if len(indexes) == 0 { + return text, nil + } + for i := 0; i < len(indexes); i++ { + uChars[indexes[i]] = delimiter + } + return string(uChars), nil +} + +func (nf *nodeFilter) checkExclude(u rune, excludes ...rune) bool { + if len(excludes) == 0 { + return false + } + var exist bool + for i, l := 0, len(excludes); i < l; i++ { + if u == excludes[i] { + exist = true + break + } + } + return exist +} + +func (nf *nodeFilter) doFilter(uChars []rune, data map[string]int) { + var result []string + ul := len(uChars) + buf := new(bytes.Buffer) + n := nf.root + for i := 0; i < ul; i++ { + if _, ok := n.child[uChars[i]]; !ok { + continue + } + n = n.child[uChars[i]] + buf.WriteRune(uChars[i]) + if n.end { + result = append(result, buf.String()) + } + for j := i + 1; j < ul; j++ { + if _, ok := n.child[uChars[j]]; !ok { + break + } + n = n.child[uChars[j]] + buf.WriteRune(uChars[j]) + if n.end { + result = append(result, buf.String()) + } + } + buf.Reset() + n = nf.root + } + for i, l := 0, len(result); i < l; i++ { + var c int + if v, ok := data[result[i]]; ok { + c = v + } + data[result[i]] = c + 1 + } +} + +func (nf *nodeFilter) doIndexes(uChars []rune) (indexes []int) { + var ( + tIndexes []int + ul = len(uChars) + n = nf.root + ) + for i := 0; i < ul; i++ { + if _, ok := n.child[uChars[i]]; !ok { + continue + } + n = n.child[uChars[i]] + tIndexes = append(tIndexes, i) + if n.end { + indexes = nf.appendTo(indexes, tIndexes) + tIndexes = nil + } + for j := i + 1; j < ul; j++ { + if _, ok := n.child[uChars[j]]; !ok { + break + } + n = n.child[uChars[j]] + tIndexes = append(tIndexes, j) + if n.end { + indexes = nf.appendTo(indexes, tIndexes) + } + } + if tIndexes != nil { + tIndexes = nil + } + n = nf.root + } + return +} + +func (nf *nodeFilter) appendTo(dst, src []int) []int { + var t []int + for i, il := 0, len(src); i < il; i++ { + var exist bool + for j, jl := 0, len(dst); j < jl; j++ { + if src[i] == dst[j] { + exist = true + break + } + } + if !exist { + t = append(t, src[i]) + } + } + return append(dst, t...) +} diff --git a/common/filter/filterClient.go b/common/filter/filterClient.go new file mode 100644 index 0000000..6ecc1de --- /dev/null +++ b/common/filter/filterClient.go @@ -0,0 +1,148 @@ +package filter + +import ( + "encoding/json" + "fmt" + "os" + "time" + + "91porn-server/app/appg" + "91porn-server/common/cache" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/models/v/filtermod" + "91porn-server/web/webg" +) + +var ( + WordCache = &cache.Cache{} + WordFilter Filter + TagFilter Filter + + WordsFilterKey = "FILTER-WORDS" + WordsReadStsKey = "isRead" + + Read = "read" + UnRead = "unread" + + ReplaceMark = '*' +) + +// FilterStart 启动文本过滤器-(评论) +func Start() { + Init() + InitTagFilter() +} + +func InitTagFilter() { + log.Info("Init Tag Filter start...") + TagFilter = NewNodeFilter(appg.Static.TagFilter) +} + +// Init Init +func Init() { + log.Info("Init WordsFilter start...") + WordCache.New() + // 从数据库获取过滤词 + words, err := GetFilterWordsFromDB() + if err != nil { + log.Error("WordFilter Init GetFilterWords error", log.E(err)) + os.Exit(1) + } + // 添加过滤词到缓存 + if err = WordCache.Add(WordsFilterKey, words, 0); err != nil { + log.Error("WordFilter Init WordCache Add FILTER-WORD error", log.E(err), log.Any("words", words)) + os.Exit(1) + } + if len(words) > 0 { + WordFilter = NewNodeFilter(words) + // 添加过滤词到Redis + if err = appg.Redis.Lpush(WordsFilterKey, words); err != nil { + log.Error("WordFilter Init GetNewestFilterWord error", log.E(err), log.Any("words", words)) + os.Exit(1) + } + } + log.Info("Init WordsFilter end") +} + +// GetFilterWords 从Redis获取过滤词 +func GetFilterWordsFromDB() ([]string, error) { + filterWords, err := filtermod.GetAllFilterWords() + if err != nil { + return nil, err + } + words := make([]string, len(filterWords)) + for i, v := range filterWords { + words[i] = v.Word + } + return words, nil +} + +// GetFilterWords 从Redis获取过滤词 +func GetFilterWordsFromRedis() ([]string, error) { + count, err := appg.Redis.LCount(WordsFilterKey) + if err != nil { + return nil, err + } + return appg.Redis.LRange(WordsFilterKey, 0, count) +} + +// 检查是否有跟新 +func CheckUpdate() (*nodeFilter, error) { + isRead, err := appg.Redis.Get(WordsReadStsKey) + if err != nil { + log.Error("CheckUpdate Redis Get error") + return nil, err + } + if isRead != nil && *isRead == UnRead { + words, err := GetFilterWordsFromRedis() + if err != nil { + log.Error("WordFilter CheckUpdate GetFilterWords error") + return nil, err + } + WordCache.New() + if err = WordCache.Add(WordsFilterKey, words, 0); err != nil { + log.Error("WordFilter CheckUpdate WordCache Add error") + return nil, err + } + WordFilter = NewNodeFilter(words) + if err = appg.Redis.Set(WordsReadStsKey, Read, 0); err != nil { + log.Error("WordFilter Redis Del error") + return nil, err + } + var tmp interface{} = WordFilter + var nf = tmp.(*nodeFilter) + return nf, nil + } + return nil, nil +} + +// UpdateFilterWords web更新Redis +func UpdateFilterWords() error { + var ( + sensitiveWords []string + ) + // 更新Redis + if _, err := webg.Redis.Del(redisconst.SensitiveWordsCache); err != nil { + log.Error("WordsFilter UpdateFilterWords Del error", log.E(err)) + return err + } + words, err := filtermod.GetAllFilterWords() + if err != nil || len(words) <= 0 { + return err + } + + for _, w := range words { + sensitiveWords = append(sensitiveWords, w.Word) + } + + if len(sensitiveWords) > 0 { + // 放入缓存 + bytes, _ := json.Marshal(sensitiveWords) + err := webg.Redis.Set(redisconst.SensitiveWordsCache, bytes, 10*time.Minute) + if err != nil { + log.Warn(fmt.Sprintf("redis set SensitiveWords err:%v", err)) + } + } + return nil +} diff --git a/common/game/game.go b/common/game/game.go new file mode 100644 index 0000000..629afbc --- /dev/null +++ b/common/game/game.go @@ -0,0 +1,130 @@ +package game + +import ( + "91porn-server/app/appg" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/web/webg" + "crypto/md5" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "net/http" +) + +const ( + GainGameCodeUrl = "/api/jt/product/getscode" + QueryGameCodeUrl = "/api/jt/product/getsclist" +) + +type GameCodeReq struct { + Level int `json:"level" bson:"level"` // 商品等级 + Sign string `json:"sign" bson:"sign"` // md5加密 +} + +type GameCodeResp struct { + Code int `json:"code" bson:"code"` // code编码 + Data Data `json:"data" bson:"data"` // 游戏码响应内容 + Msg string `json:"msg" bson:"msg"` // 返回内容 + Hash bool `json:"hash" bson:"hash"` // hash + Time string `json:"time" bson:"time"` // 时间 + Tip string `json:"tip" bson:"tip"` // 提示内容 +} + +type Data struct { + Code int `json:"code" bson:"code"` // code编码 + Msg string `json:"msg" bson:"msg"` // 返回内容 + SubscriptionCode string `json:"subscriptionCode" bson:"subscriptionCode"` // 订单订阅码(无用) + SerialCode string `json:"serialCode" bson:"serialCode"` // 序列号 (玩家游戏使用) +} + +type QueryGameCodeReq struct { + PageNum int `json:"pageNum" bson:"pageNum"` // 当前页 + PageSize int `json:"pageSize" bson:"pageSize"` // 页码 +} + +type QueryGameCodeResp struct { + Code int `json:"code" bson:"code"` // code编码 + Data QueryData `json:"data" bson:"data"` // 游戏码响应内容 + Msg string `json:"msg" bson:"msg"` // 返回内容 + Hash bool `json:"hash" bson:"hash"` // hash + Time string `json:"time" bson:"time"` // 时间 + Tip string `json:"tip" bson:"tip"` // 提示内容 +} + +type QueryData struct { + Total int `json:"total" bson:"total"` // 总数 + List []Detail `json:"list" bson:"list"` // 返回内容 +} + +type Detail struct { + SubscriptionCode string `json:"subscriptionCode" bson:"subscriptionCode"` // 订单订阅码(无用) + SerialCode string `json:"serialCode" bson:"serialCode"` // 序列号 (玩家游戏使用) + UserId string `json:"userId" bson:"userId"` // 用户ID + MerchantId int `json:"merchantId" bson:"merchantId"` // 商户号 + ProductPrice int `json:"productPrice" bson:"productPrice"` // 商品价格 + ProductLevel int `json:"productLevel" bson:"productLevel"` // 商品等级 + CreatedAt string `json:"createdAt" bson:"createdAt"` // 创建时间 +} + +func GainTripartiteGameCode(uid uint64) (string, error) { + var gameCode string + userId := fmt.Sprintf("LLD_%v", uid) + sprintf := fmt.Sprintf("userid=%v&merchantid=%v", userId, appg.Conf.Game.MercId) + reqHeader := base64.StdEncoding.EncodeToString([]byte(sprintf)) + + md5Str := fmt.Sprintf("level=%d&userid=%v&merchantid=%d", 1, userId, appg.Conf.Game.MercId) + hash := md5.Sum([]byte(md5Str)) + // 将 MD5 转换为十六进制字符串 + md5String := hex.EncodeToString(hash[:]) + + var ( + url = appg.Conf.Game.URL + GainGameCodeUrl + params = GameCodeReq{Level: 1, Sign: md5String} + out GameCodeResp + ) + h := map[string]string{"m-api-key": reqHeader} + + code, err := httputil.DefaultClientPostJsonWithResp(&out, url, h, params) + if err != nil { + log.Error(fmt.Sprintf("GainTripartiteGameCode http request err:[%v],uid:[%v],url:[%v],params:[%v]", err, uid, url, params)) + return gameCode, err + } + if code != http.StatusOK { + return gameCode, errors.New("status code is err") + } + + if out.Data.Code == http.StatusOK { + gameCode = out.Data.SerialCode + } + return gameCode, nil +} + +func QueryTripartiteGameCode(uid uint64) (*QueryData, error) { + userId := fmt.Sprintf("LLD_%v", uid) + sprintf := fmt.Sprintf("userid=%v&merchantid=%v", userId, webg.Conf.Game.MercId) + reqHeader := base64.StdEncoding.EncodeToString([]byte(sprintf)) + + var ( + url = webg.Conf.Game.URL + QueryGameCodeUrl + params = QueryGameCodeReq{PageNum: 1, PageSize: 10} + out QueryGameCodeResp + ) + h := map[string]string{"m-api-key": reqHeader} + + code, err := httputil.DefaultClientPostJsonWithResp(&out, url, h, params) + if err != nil { + log.Error(fmt.Sprintf("QueryTripartiteGameCode http request err:[%v],uid:[%v],url:[%v],params:[%v]", err, uid, url, params)) + return nil, err + } + if code != http.StatusOK { + return nil, errors.New("status code is err") + } + + if out.Code == http.StatusOK { + return &out.Data, nil + } + + return nil, nil +} diff --git a/common/ginUtil.go b/common/ginUtil.go new file mode 100644 index 0000000..0be325b --- /dev/null +++ b/common/ginUtil.go @@ -0,0 +1,529 @@ +package common + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "os" + "reflect" + "strconv" + "time" + "unicode" + + "91porn-server/common/constant" + "91porn-server/common/crypt" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/version" + "91porn-server/middleware/ua" + + "github.com/gin-gonic/gin" +) + +var ( + env string + pid = os.Getgid() + cryptSecret360 string + ErrUserNotExist = errors.New("userId not exist") + + FirstRealUserID uint64 +) + +type FileType string + +const ( + FileCSV FileType = "csv" + FileExcel FileType = "xlsx" + + firstRealUserIDTest = 300001 // 首个真实的用户id(测试环境) + firstRealUserIDProd = 300001 // 首个真实的用户id(正式环境) +) + +// ServeFile serve request with an file by attachment. +func ServeFile(c *gin.Context, fileName string, fileType FileType, fileBuffer *bytes.Buffer) { + c.DataFromReader(http.StatusOK, int64(fileBuffer.Len()), "application/octet-stream", fileBuffer, map[string]string{ + "Content-Disposition": fmt.Sprintf("attachment;filename=%s-%s.%s", + fileName, time.Now().Local().Format("2006-01-02"), fileType), + }) +} + +// ServeJSON 返回数据并处理多语言 +func ServeJSON(c *gin.Context, code stderr.Code, data interface{}) { + ServeJsonWithExtra(c, code, data, nil) +} + +func ServeJsonWithExtra(c *gin.Context, code stderr.Code, data interface{}, extra map[string]interface{}) { + var hash bool + if code != stderr.Success && code != stderr.ErrVersionUpdate { //发生错误,记录日志 + var version, sysType, devType string + id, _ := GetUID(c) + ua, _ := GetUA(c) + if ua.Ver != "" { + version = ua.Ver + } + if ua.SysType != "" { + sysType = ua.SysType + } + if devType != "" { + devType = ua.DevType + } + // 预防打印出现空指针异常 PANIC=runtime error: invalid memory address or nil pointer dereference + if data == nil { + data = "nil" + } + log.WarnX(c, "Error:", + log.Any("UID", strconv.FormatUint(id, 10)), + log.Any("IP", c.ClientIP()), + log.Any("Version", version), + log.Any("SysType", sysType), + log.Any("DevType", devType), + log.Any("Router", c.Request.RequestURI), + log.Any("PID", pid), + log.Any("Data", data), + log.Any("Code", code)) + } + if !IsNilOrEmpty(data) && code == stderr.Success { + t := reflect.TypeOf(data) + if !(t.Kind() == reflect.Map || t.Kind() == reflect.Struct || t.Kind() == reflect.Slice) { + log.WarnX(c, "[===TypeError===] Return Data Type error is not struct or slice", + log.Any("path", c.Request.URL.Path)) + } + } + // 在 data 加密前抽取一次 msg:敏感词命中等场景需要把 data 的明文详情同步到 msg + msg := resolveMsg(code, data) + if env == constant.ProdEnv { + //返给前端是否加密 + hash = true + dataByte, _ := json.Marshal(data) + cipher, _ := crypt.CoreAesEncryptEx(dataByte, 12, cryptSecret360) + data = base64.StdEncoding.EncodeToString(cipher) + } + if IsNilOrEmpty(data) { + data = "" + } + h := gin.H{ + "code": code, + "hash": hash, + "msg": msg, + "tip": code.Tip(), + "data": data, + "time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), + } + if len(extra) > 0 { + for k, v := range extra { + h[k] = v + } + } + c.JSON(http.StatusOK, h) +} + +// resolveMsg 按 code 选择 msg:默认走 code.Msg();对于内容敏感词命中这类 +// 详情完全包含在 data 里的错误码,把 data 的字符串明文同步覆盖到 msg, +// 便于前端直接用 msg 弹窗,无需再额外读取 data 字段。 +func resolveMsg(code stderr.Code, data interface{}) string { + if code == stderr.ContentSensitiveHit { + if s, ok := data.(string); ok && s != "" { + return s + } + } + return code.Msg() +} + +// ServeToJSON 返回数据并处理多语言 +func ServeToJSON(c *gin.Context, code stderr.Code, data interface{}) { + var hash bool + if code != stderr.Success && code != stderr.ErrVersionUpdate { //发生错误,记录日志 + var version, sysType, devType string + id, _ := GetUID(c) + ua, _ := GetUA(c) + if ua.Ver != "" { + version = ua.Ver + } + if ua.SysType != "" { + sysType = ua.SysType + } + if devType != "" { + devType = ua.DevType + } + log.WarnX(c, "Error:", + log.Any("UID", strconv.FormatUint(id, 10)), + log.Any("IP", c.ClientIP()), + log.Any("Version", version), + log.Any("SysType", sysType), + log.Any("DevType", devType), + log.Any("Router", c.Request.RequestURI), + log.Any("PID", pid), + log.Any("Error", data), + log.Any("Code", code)) + } + if !IsNilOrEmpty(data) && code == stderr.Success { + t := reflect.TypeOf(data) + if !(t.Kind() == reflect.Map || t.Kind() == reflect.Struct || t.Kind() == reflect.Slice || t.Kind() == reflect.Ptr) { + log.WarnX(c, "[===TypeError===] Return Data Type error is not struct or slice", + log.Any("kind", t.Kind()), + log.Any("path", c.Request.URL.Path)) + } + } + if IsNilOrEmpty(data) { + data = gin.H{} + } + c.JSON(http.StatusOK, gin.H{ + "code": code, + "hash": hash, + "msg": code.Msg(), + "tip": code.Tip(), + "data": data, + "time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), + }) +} + +func ServeError(c *gin.Context, err error) { + cErr, ok := err.(*stderr.CustomErr) + if ok { + serveJsonLogic(c, cErr.Code, cErr.Msg, nil, cErr.Msg, cErr.Msg) + } else { + log.ErrorX(c, "ServeError", log.E(err)) + var errMsg string + if err != nil { + errMsg = err.Error() + } + serveJsonLogic(c, stderr.Failure, errMsg, nil, stderr.Failure.Error(), stderr.Failure.Tip()) + } + +} + +func serveJsonLogic(c *gin.Context, code stderr.Code, data interface{}, extra map[string]interface{}, msg, tip string) { + var hash bool + if code != stderr.Success && code != stderr.ErrVersionUpdate { //发生错误,记录日志 + var version, sysType, devType string + id, _ := GetUID(c) + ua, _ := GetUA(c) + if ua.Ver != "" { + version = ua.Ver + } + if ua.SysType != "" { + sysType = ua.SysType + } + if devType != "" { + devType = ua.DevType + } + // 预防打印出现空指针异常 PANIC=runtime error: invalid memory address or nil pointer dereference + if data == nil { + data = "nil" + } + log.WarnX(c, "Error:", + log.Any("UID", strconv.FormatUint(id, 10)), + log.Any("IP", c.ClientIP()), + log.Any("Version", version), + log.Any("SysType", sysType), + log.Any("DevType", devType), + log.Any("Router", c.Request.RequestURI), + log.Any("PID", pid), + log.Any("Data", data), + log.Any("Code", code)) + } + if !IsNilOrEmpty(data) && code == stderr.Success { + t := reflect.TypeOf(data) + if !(t.Kind() == reflect.Map || t.Kind() == reflect.Struct || t.Kind() == reflect.Slice) { + log.WarnX(c, "[===TypeError===] Return Data Type error is not struct or slice", + log.Any("path", c.Request.URL.Path)) + } + } + if env == constant.ProdEnv { + //返给前端是否加密 + hash = true + dataByte, _ := json.Marshal(data) + cipher, _ := crypt.CoreAesEncryptEx(dataByte, 12, cryptSecret360) + data = base64.StdEncoding.EncodeToString(cipher) + } + if IsNilOrEmpty(data) { + data = "" + } + h := gin.H{ + "code": code, + "hash": hash, + "msg": msg, + "tip": tip, + "data": data, + "time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), + } + if len(extra) > 0 { + for k, v := range extra { + h[k] = v + } + } + c.JSON(http.StatusOK, h) +} + +// GetUID 获取用户uid +// oauth.Auth执行后uid有效 +func GetUID(ctx *gin.Context) (uid uint64, err error) { + val, exists := ctx.Get(constant.CtxUserID) + if !exists { + err = ErrUserNotExist + return + } + uid, ok := val.(uint64) + if !ok { + return 0, errors.New("userId type error") + } + return +} + +// TryGetUID 尝试获取用户ID,用户未登录的情况下uid为0 +func TryGetUID(ctx *gin.Context) (uid uint64) { + val, exists := ctx.Get(constant.CtxUserID) + if !exists { + return + } + uid, _ = val.(uint64) + return +} + +// GetUA +func GetUA(ctx *gin.Context) (u ua.UA, err error) { + val, exists := ctx.Get(constant.CtxUA) + if !exists { + err = errors.New("user-agent not exists") + return + } + u, ok := val.(ua.UA) + if !ok { + err = errors.New("user-agent type error") + return + } + return u, nil +} + +/** +* 判断当前App 是否是制定的版本号 +* 历史判断: + 1. 是否是flutter版本 ver=2.0.0 主要更新列表,使用hasNext 替换total + 2. 是否2.0.1版本 ver=2.0.1 用于获取账单更改 + 3. 是否2.0.9版本 ver=2.0.9 用户用户手机登陆流程 + 4. 是否2.1.0版本 ver=2.1.0 用于接口防重放 +*/ + +func IsGTESpecifyVer(ctx *gin.Context, specVer string) bool { + val, exists := ctx.Get(constant.CtxUA) + if !exists { + log.Warn("user-agent not exists") + return false + } + u, ok := val.(ua.UA) + if !ok { + log.Warn("user-agent type error") + return false + } + if u.Ver == "" { + return false + } + v1, err := version.New(u.Ver) + if v1 == nil || err != nil { + return false + } + v2, err := version.New(specVer) + if v2 == nil || err != nil { + return false + } + if v1.GTE(v2) { + return true + } + return false +} + +func IsSpecifyVerBaseOnUa(u ua.UA, specVer string) bool { + if u.Ver == "" { + return false + } + v1, err := version.New(u.Ver) + if v1 == nil || err != nil { + return false + } + v2, err := version.New(specVer) + if v2 == nil || err != nil { + return false + } + + if v1.GTE(v2) { + return true + } + return false +} + +// GetAdminAct 获取管理员账号 +// oauth.Auth执行后账号有效 +func GetAdminAct(ctx *gin.Context) (string, error) { + t1 := ctx.Request.Header.Get("mod") + if t1 == "debug" { + return "debug", nil + } + val, exists := ctx.Get(constant.CtxAdminAct) + if !exists { + return "", errors.New("Admin not exist") + } + v, ok := val.(string) + if !ok { + return "", errors.New("Admin type error") + } + return v, nil +} + +// GetAdminRole 获取管理员账号 +// oauth.Auth执行后账号有效 +func GetAdminRole(ctx *gin.Context) (string, error) { + val, exists := ctx.Get(constant.CtxAdminRole) + if !exists { + return "", errors.New("AdminRole not exist") + } + v, ok := val.(string) + if !ok { + return "", errors.New("AdminRole type error") + } + return v, nil +} + +// GetJuShangId 获取管理员账号 +// oauth.Auth执行后账号有效 +func GetJuShangID(ctx *gin.Context) (string, error) { + val, exists := ctx.Get(constant.CtxJuShangCID) + if !exists { + return "", errors.New("JuShangID not exist") + } + v, ok := val.(string) + if !ok { + return "", errors.New("JuShangID type error") + } + return v, nil +} + +// GetUIDAct 获取管理员账号 +// oauth.Auth执行后uid有效 +func GetDistrictAct(ctx *gin.Context) (string, error) { + val, exists := ctx.Get(constant.CtxDistrictName) + if !exists { + return "", errors.New("DistrictUserID not exist") + } + act, ok := val.(string) + if !ok { + return "", errors.New("DistrictAct type error") + } + return act, nil +} + +// InitGinSecret InitResponseParam +func InitGinSecret(secret, secret360, env_ string) { + cryptSecret360 = secret360 + env = env_ + FirstRealUserID = firstRealUserIDTest + if env == constant.ProdEnv { + FirstRealUserID = firstRealUserIDProd + } +} + +func IsNilOrEmpty(in interface{}) bool { + return in == nil || in == "" +} + +// GetIP 获取真实IP +func GetIP(ctx *gin.Context) string { + relIP, exists := ctx.Get(constant.CtxIP) + v, _ := relIP.(string) + // CtxIP 未设置(或存的是空串)时回落到 ClientIP; + // 此前写成 if !exists { if relIP == "" ... } —— relIP 为 nil 接口,永不等于 "",回落是死代码,会返回空串 + if !exists || v == "" { + v = ctx.ClientIP() + } + //log.Info(fmt.Sprintf("[IP-ROUTER] %s,PID %d", ctx.GetHeader("X-Forwarded-For"), os.Getpid())) + return v +} + +// 判断是否是ip4 +func IsIP4(ip string) (bool, string) { + ipAddr := net.ParseIP(ip).To4() + if ipAddr == nil { + return false, "" + } + return true, ipAddr.String() +} + +// 判断是否是正常的设备id +func IsNormalDevId(devId string) bool { + for _, r := range devId { + //判断是否包含中文汉子 + if unicode.Is(unicode.Scripts["Han"], r) { + return false + } + //判断是否包含空格 + if unicode.IsSpace(r) { + return false + } + } + return true +} + +func ServeJSONNoEncrypt(c *gin.Context, code stderr.Code, data interface{}) { + ServeJsonWithExtraNoEncrypt(c, code, data, nil) +} + +func ServeJsonWithExtraNoEncrypt(c *gin.Context, code stderr.Code, data interface{}, extra map[string]interface{}) { + var hash bool + if code != stderr.Success && code != stderr.ErrVersionUpdate { //发生错误,记录日志 + var version, sysType, devType string + id, _ := GetUID(c) + ua, _ := GetUA(c) + if ua.Ver != "" { + version = ua.Ver + } + if ua.SysType != "" { + sysType = ua.SysType + } + if devType != "" { + devType = ua.DevType + } + // 预防打印出现空指针异常 PANIC=runtime error: invalid memory address or nil pointer dereference + if data == nil { + data = "nil" + } + log.WarnX(c, "Error:", + log.Any("UID", strconv.FormatUint(id, 10)), + log.Any("IP", c.ClientIP()), + log.Any("Version", version), + log.Any("SysType", sysType), + log.Any("DevType", devType), + log.Any("Router", c.Request.RequestURI), + log.Any("PID", pid), + log.Any("Data", data), + log.Any("Code", code)) + } + if !IsNilOrEmpty(data) && code == stderr.Success { + t := reflect.TypeOf(data) + if !(t.Kind() == reflect.Map || t.Kind() == reflect.Struct || t.Kind() == reflect.Slice) { + log.WarnX(c, "[===TypeError===] Return Data Type error is not struct or slice", + log.Any("path", c.Request.URL.Path)) + } + } + + msg := resolveMsg(code, data) + if IsNilOrEmpty(data) { + data = "" + } + h := gin.H{ + "code": code, + "hash": hash, + "msg": msg, + "tip": code.Tip(), + "data": data, + "time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), + } + if len(extra) > 0 { + for k, v := range extra { + h[k] = v + } + } + c.JSON(http.StatusOK, h) +} diff --git a/common/go.go b/common/go.go new file mode 100644 index 0000000..8b19619 --- /dev/null +++ b/common/go.go @@ -0,0 +1,108 @@ +package common + +import ( + "fmt" + "runtime/debug" + "sync" + "sync/atomic" + "time" + + "91porn-server/common/log" + "91porn-server/common/stderr" +) + +var ( + routinePanicHandler func(string) + routineCount int64 +) + +func SetPanicHandler(handler func(string)) { + routinePanicHandler = handler +} + +func Go(f func()) { + atomic.AddInt64(&routineCount, 1) + go func() { + defer func() { + atomic.AddInt64(&routineCount, -1) + if r := recover(); r != nil { + log.Error("【Warning】serevr occour error,please attention", log.Any("Detail", r)) + if routinePanicHandler != nil { + var stack string + if !stderr.IsContain(r, stderr.WriteBrokenError) { + stack = string(debug.Stack()) + } + routinePanicHandler(fmt.Sprintf("[Panic] routineCount:%d \n err: %+v\n", routineCount, r) + stack) + } + } + }() + f() + }() +} + +func GoParam(i int, f func(i int)) { + atomic.AddInt64(&routineCount, 1) + go func(i int) { + defer func() { + atomic.AddInt64(&routineCount, -1) + if r := recover(); r != nil { + log.Error("【Warning】serevr occour error,please attention", log.Any("Detail", r)) + if routinePanicHandler != nil { + var stack string + if !stderr.IsContain(r, stderr.WriteBrokenError) { + stack = string(debug.Stack()) + } + routinePanicHandler(fmt.Sprintf("[Panic] routineCount:%d \n err: %+v\n", routineCount, r) + stack) + } + } + }() + f(i) + }(i) +} + +// WaitGoQuit 等待go退出,timeoutSec 超时秒数,<=0表示不超时 +func WaitGoQuit(timeoutSec int32) { + for { + count := atomic.LoadInt64(&routineCount) + if count == 0 { + return + } + time.Sleep(time.Second) + if timeoutSec > 0 { + timeoutSec-- + if timeoutSec == 0 { + return + } + } + } +} + +func GoAndWait(funcs ...func() error) (err error) { + var wg sync.WaitGroup + wg.Add(len(funcs)) + for i := range funcs { + go func(f func() error) { + atomic.AddInt64(&routineCount, 1) + defer wg.Done() + defer func() { + atomic.AddInt64(&routineCount, -1) + if r := recover(); r != nil { + log.Error("【Warning】serevr occour error,please attention", log.Any("Detail", r)) + if routinePanicHandler != nil { + var stack string + if !stderr.IsContain(r, stderr.WriteBrokenError) { + stack = string(debug.Stack()) + } + routinePanicHandler(fmt.Sprintf("[Panic] routineCount:%d \n err: %+v\n", routineCount, r) + stack) + } + } + }() + if routineErr := f(); routineErr != nil { + err = routineErr + + } + }(funcs[i]) + } + wg.Wait() + return +} diff --git a/common/googauth/googauth.go b/common/googauth/googauth.go new file mode 100644 index 0000000..5ca3f63 --- /dev/null +++ b/common/googauth/googauth.go @@ -0,0 +1,32 @@ +package googauth + +import "github.com/dgryski/dgoogauth" + +const windowSize = 30 +const hotpWindowSize = 5 + +var HotpCfg = dgoogauth.OTPConfig{ + Secret: "MRSHM6LTMFYHAMJSGM2DKNQ=", + HotpCounter: 1, + WindowSize: hotpWindowSize, +} + +func New(secret string, name string) string { + cfg := &dgoogauth.OTPConfig{ + Secret: secret, + WindowSize: windowSize, + } + return cfg.ProvisionURI(name) +} + +func Verify(secret string, pwd string) (bool, error) { + cfg := &dgoogauth.OTPConfig{ + Secret: secret, + WindowSize: windowSize, + } + return cfg.Authenticate(pwd) +} + +func VerifyHotp(cfg *dgoogauth.OTPConfig, authCode string, counter int) (bool, error) { + return cfg.Authenticate(authCode) +} diff --git a/common/googauth/htop.go b/common/googauth/htop.go new file mode 100644 index 0000000..6c922ac --- /dev/null +++ b/common/googauth/htop.go @@ -0,0 +1,64 @@ +package googauth + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/binary" + "strconv" +) + +// HOTP implementation +type HOTP struct { + secret []byte + digits int +} + +// At generate code +func (h HOTP) At(counter uint64) string { + counterBytes := make([]byte, 8) + binary.BigEndian.PutUint64(counterBytes, counter) + hash := hmac.New(sha1.New, h.secret) + hash.Write(counterBytes) + hs := hash.Sum(nil) + offset := hs[19] & 0x0f + binCodeBytes := make([]byte, 4) + binCodeBytes[0] = hs[offset] & 0x7f + binCodeBytes[1] = hs[offset+1] & 0xff + binCodeBytes[2] = hs[offset+2] & 0xff + binCodeBytes[3] = hs[offset+3] & 0xff + binCode := binary.BigEndian.Uint32(binCodeBytes) + mod := uint32(1) + for i := 0; i < h.digits; i++ { + mod *= 10 + } + code := binCode % mod + codeString := strconv.FormatUint(uint64(code), 10) + if len(codeString) < h.digits { + paddingByteLength := h.digits - len(codeString) + paddingBytes := make([]byte, paddingByteLength) + for i := 0; i < paddingByteLength; i++ { + paddingBytes[i] = '0' + } + codeString = string(paddingBytes) + codeString + } + return codeString +} + +// Verify verify OTP code +func (h HOTP) VerifyCode(code string, counter uint64) bool { + realCode := h.At(counter) + return realCode == code +} + +// Verify verify OTP code +func (h HOTP) GenerateCode(counter uint64) string { + return h.At(counter) +} + +// NewHOTP generate new HOTP instance +func NewHOTP(secret []byte, digits int) (h *HOTP) { + h = new(HOTP) + h.secret = secret + h.digits = digits + return +} diff --git a/common/handleCong.go b/common/handleCong.go new file mode 100644 index 0000000..9533120 --- /dev/null +++ b/common/handleCong.go @@ -0,0 +1,25 @@ +package common + +import "errors" + +// FsOption 读取文件服务器配置 +type FsOption struct { + BucketName string `json:"bucketName"` + AccessKey string `json:"accessKey"` + Secret string `json:"secret"` + TTL string `json:"ttl"` +} + +// SelectFsOption 从配置文件中读取Bucket对应的相关值 +func SelectFsOption(bucket string, FsOptions []FsOption) (FsOption, error) { + option := FsOption{} + if bucket == "" || len(FsOptions) == 0 { + return option, errors.New("bucket or FsOptions must nor be empty") + } + for _, option := range FsOptions { + if option.BucketName == bucket { + return option, nil + } + } + return option, nil +} diff --git a/common/hevcpull/sign.go b/common/hevcpull/sign.go new file mode 100644 index 0000000..605c581 --- /dev/null +++ b/common/hevcpull/sign.go @@ -0,0 +1,269 @@ +package hevcpull + +import ( + "91porn-server/common/log" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "net/url" + "path" + "regexp" + "strconv" + "strings" + "time" +) + +const ( + ExpiresParam = "hevc_exp" + SignatureParam = "hevc_sig" + + minSecretBytes = 32 + maxSignatureTTL = 480*time.Hour + 5*time.Minute +) + +var ( + ErrInvalidSecret = errors.New("HEVC pull secret must contain at least 32 bytes") + ErrInvalidSignature = errors.New("invalid HEVC pull signature") + ErrExpired = errors.New("expired HEVC pull signature") + + signatureTextPattern = regexp.MustCompile(`(?i)(hevc_sig(?:=|%3D))[0-9a-f]{64}`) +) + +// NormalizeSource accepts only the canonical relative m3u8 paths stored by +// the video service. Absolute URLs, query strings, fragments, backslashes, +// and dot traversal are rejected so the signed path and cloud task identity +// always refer to the same source. +func NormalizeSource(source string) (string, error) { + source = strings.TrimSpace(source) + if source == "" || strings.Contains(source, "\\") { + return "", errors.New("invalid HEVC pull source") + } + parsed, err := url.Parse(source) + if err != nil || + parsed.IsAbs() || + parsed.Host != "" || + parsed.User != nil || + parsed.RawQuery != "" || + parsed.Fragment != "" { + return "", errors.New("invalid HEVC pull source") + } + sourcePath := strings.TrimLeft(parsed.Path, "/") + if sourcePath == "" || strings.Contains(sourcePath, "\\") { + return "", errors.New("invalid HEVC pull source") + } + for _, char := range sourcePath { + if char < 0x20 || char == 0x7f { + return "", errors.New("invalid HEVC pull source") + } + } + for _, segment := range strings.Split(sourcePath, "/") { + if segment == "." || segment == ".." { + return "", errors.New("invalid HEVC pull source traversal") + } + } + normalized := strings.TrimLeft(path.Clean("/"+sourcePath), "/") + if normalized == "" || + normalized == "." || + path.Ext(normalized) != ".m3u8" { + return "", errors.New("invalid HEVC pull source") + } + return normalized, nil +} + +// ResolveChildSource resolves a child playlist URI from a signed master +// playlist against the parent source path. Only local m3u8 paths are accepted; +// absolute URLs and query-bearing variants are rejected. +func ResolveChildSource(parentSource, childURI string) (string, error) { + parentSource, err := NormalizeSource(parentSource) + if err != nil { + return "", err + } + childURI = strings.TrimSpace(childURI) + if childURI == "" || strings.Contains(childURI, "\\") { + return "", errors.New("invalid HEVC child playlist") + } + child, err := url.Parse(childURI) + if err != nil || + child.IsAbs() || + child.Host != "" || + child.User != nil || + child.RawQuery != "" || + child.Fragment != "" || + child.Path == "" { + return "", errors.New("invalid HEVC child playlist") + } + + parentNamespace := knownSourceNamespace(parentSource) + var resolved string + if strings.HasPrefix(child.Path, "/") { + resolved = strings.TrimLeft(child.Path, "/") + childNamespace := knownSourceNamespace(resolved) + switch parentNamespace { + case "", "sp": + // A path without a reserved prefix still resolves to the default + // SP origin. Explicitly switching to another origin is forbidden. + if childNamespace != "" && childNamespace != "sp" { + return "", errors.New("HEVC child playlist changes source namespace") + } + default: + // For non-default origins the source namespace is encoded in the + // route path. A plain root-relative URI cannot preserve that origin, + // so require the playlist to name the same namespace explicitly. + if childNamespace != parentNamespace { + return "", errors.New("ambiguous HEVC root-relative child playlist") + } + } + } else { + if parentNamespace != "" && childEscapesSourceNamespace(parentSource, child.Path) { + return "", errors.New("HEVC child playlist escapes source namespace") + } + resolved = path.Join("/", path.Dir(parentSource), child.Path) + } + resolved, err = NormalizeSource(strings.TrimLeft(resolved, "/")) + if err != nil { + return "", err + } + if !strings.HasPrefix(child.Path, "/") && + parentNamespace != "" && + knownSourceNamespace(resolved) != parentNamespace { + return "", errors.New("HEVC child playlist escapes source namespace") + } + return resolved, nil +} + +func childEscapesSourceNamespace(parentSource, childPath string) bool { + parentDir := strings.Trim(path.Dir(parentSource), "/") + depth := 0 + if parentDir != "" && parentDir != "." { + depth = len(strings.Split(parentDir, "/")) + } + for _, segment := range strings.Split(childPath, "/") { + switch segment { + case "", ".": + continue + case "..": + if depth <= 1 { + return true + } + depth-- + default: + depth++ + } + } + return false +} + +func knownSourceNamespace(source string) string { + first, _, _ := strings.Cut(strings.TrimLeft(source, "/"), "/") + switch first { + case "sp", "pms", "laosiji", "v1", "v2", "v3": + return first + default: + return "" + } +} + +// SignURL signs an exact source path for the cloud transcoder. The returned +// URL contains only the expiry and signature query parameters. +func SignURL(rawURL, secret string, expiresAt time.Time) (string, error) { + secret = strings.TrimSpace(secret) + if len([]byte(secret)) < minSecretBytes { + return "", ErrInvalidSecret + } + parsed, err := url.Parse(strings.TrimSpace(rawURL)) + if err != nil { + return "", err + } + if parsed.Scheme == "" || parsed.Host == "" || parsed.EscapedPath() == "" { + return "", errors.New("invalid HEVC pull URL") + } + if parsed.RawQuery != "" || parsed.Fragment != "" { + return "", errors.New("HEVC pull URL must not contain query or fragment") + } + expires := expiresAt.UTC().Unix() + if expires <= 0 { + return "", errors.New("invalid HEVC pull expiry") + } + query := parsed.Query() + query.Set(ExpiresParam, strconv.FormatInt(expires, 10)) + query.Set(SignatureParam, signature(parsed.EscapedPath(), expires, secret)) + parsed.RawQuery = query.Encode() + return parsed.String(), nil +} + +// VerifyURL validates expiry, exact path binding, and the HMAC signature. +func VerifyURL(parsed *url.URL, secret string, now time.Time) error { + secret = strings.TrimSpace(secret) + if len([]byte(secret)) < minSecretBytes { + log.Error("VerifyURL fail 1") + return ErrInvalidSecret + } + if parsed == nil || parsed.EscapedPath() == "" { + log.Error("VerifyURL fail 2", log.Any("parsed", parsed)) + return ErrInvalidSignature + } + query := parsed.Query() + if len(query) != 2 || + len(query[ExpiresParam]) != 1 || + len(query[SignatureParam]) != 1 { + log.Error("VerifyURL fail 3", log.Any("query", query)) + return ErrInvalidSignature + } + expires, err := strconv.ParseInt(query.Get(ExpiresParam), 10, 64) + if err != nil { + log.Error("VerifyURL fail 4", log.E(err)) + return ErrInvalidSignature + } + now = now.UTC() + expiresAt := time.Unix(expires, 0).UTC() + if !expiresAt.After(now) { + log.Error("VerifyURL fail 5", log.Any("expires", expires)) + return ErrExpired + } + if expiresAt.After(now.Add(maxSignatureTTL)) { + log.Error("VerifyURL fail 6", log.Any("expires", expires)) + return ErrInvalidSignature + } + + provided, err := hex.DecodeString(query.Get(SignatureParam)) + if err != nil { + log.Error("VerifyURL fail 7", log.E(err)) + return ErrInvalidSignature + } + expected, err := hex.DecodeString(signature(parsed.EscapedPath(), expires, secret)) + if err != nil || !hmac.Equal(provided, expected) { + log.Error("VerifyURL fail 8", log.Any("expires", expires)) + return ErrInvalidSignature + } + return nil +} + +// RedactText removes signed pull bearer values from logs and diagnostics, +// including when the nested URL has been query-escaped by another API. +func RedactText(text string) string { + return signatureTextPattern.ReplaceAllString(text, `${1}[REDACTED]`) +} + +// RedactURL returns a diagnostic form of a signed pull URL that is safe to +// persist. It is not a usable pull URL. +func RedactURL(rawURL string) string { + parsed, err := url.Parse(strings.TrimSpace(rawURL)) + if err != nil { + return RedactText(rawURL) + } + query := parsed.Query() + if query.Has(SignatureParam) { + query.Set(SignatureParam, "[REDACTED]") + parsed.RawQuery = query.Encode() + } + return parsed.String() +} + +func signature(escapedPath string, expires int64, secret string) string { + canonical := fmt.Sprintf("%s\n%d", escapedPath, expires) + mac := hmac.New(sha256.New, []byte(secret)) + _, _ = mac.Write([]byte(canonical)) + return hex.EncodeToString(mac.Sum(nil)) +} diff --git a/common/hevcpull/sign_test.go b/common/hevcpull/sign_test.go new file mode 100644 index 0000000..f804bb5 --- /dev/null +++ b/common/hevcpull/sign_test.go @@ -0,0 +1,176 @@ +package hevcpull + +import ( + "errors" + "net/url" + "testing" + "time" +) + +func TestSignAndVerifyURL(t *testing.T) { + now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC) + signed, err := SignURL( + "https://app.example.com/api/app/vid/transcode/m3u8/laosiji/m3m/demo.m3u8", + "test-pull-secret-strong-32-bytes!!", + now.Add(480*time.Hour), + ) + if err != nil { + t.Fatalf("SignURL failed: %v", err) + } + parsed, err := url.Parse(signed) + if err != nil { + t.Fatalf("parse signed URL: %v", err) + } + if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now); err != nil { + t.Fatalf("VerifyURL failed: %v", err) + } + + parsed.Path += ".tampered" + if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now); !errors.Is(err, ErrInvalidSignature) { + t.Fatalf("tampered path returned %v", err) + } +} + +func TestVerifyURLRejectsExpiredOrExtraQuery(t *testing.T) { + now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC) + signed, err := SignURL( + "https://app.example.com/api/app/vid/transcode/m3u8/source.m3u8", + "test-pull-secret-strong-32-bytes!!", + now.Add(time.Minute), + ) + if err != nil { + t.Fatalf("SignURL failed: %v", err) + } + parsed, _ := url.Parse(signed) + if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now.Add(time.Minute)); !errors.Is(err, ErrExpired) { + t.Fatalf("expired URL returned %v", err) + } + + parsed, _ = url.Parse(signed) + query := parsed.Query() + query.Set("c", "unbound-cdn") + parsed.RawQuery = query.Encode() + if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now); !errors.Is(err, ErrInvalidSignature) { + t.Fatalf("extra query returned %v", err) + } +} + +func TestSignURLRejectsWeakSecret(t *testing.T) { + _, err := SignURL("https://app.example.com/source.m3u8", "short", time.Now().Add(time.Hour)) + if !errors.Is(err, ErrInvalidSecret) { + t.Fatalf("weak secret returned %v", err) + } +} + +func TestRedactSignedPullURL(t *testing.T) { + const signature = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + raw := "https://app.example/source.m3u8?hevc_exp=1&hevc_sig=" + signature + redacted := RedactURL(raw) + if redacted == raw || RedactText(redacted) != redacted { + t.Fatalf("URL was not redacted: %s", redacted) + } + if got := RedactText("file_url=" + url.QueryEscape(raw)); got == "file_url="+url.QueryEscape(raw) { + t.Fatalf("escaped nested URL was not redacted: %s", got) + } +} + +func TestNormalizeSource(t *testing.T) { + tests := []struct { + source string + want string + }{ + {source: " /laosiji/m3m/demo.m3u8 ", want: "laosiji/m3m/demo.m3u8"}, + {source: "sp/movie/index.m3u8", want: "sp/movie/index.m3u8"}, + {source: "sp/movie/../index.m3u8"}, + {source: `sp\movie\index.m3u8`}, + {source: `sp/movie%5Cindex.m3u8`}, + {source: `sp/movie%0Aindex.m3u8`}, + {source: "https://cdn.example.com/index.m3u8"}, + {source: "sp/movie/index.m3u8?token=x"}, + {source: "sp/movie/index.mp4"}, + {source: "sp/movie/index.M3U8"}, + } + for _, tt := range tests { + got, err := NormalizeSource(tt.source) + if tt.want == "" { + if err == nil { + t.Errorf("NormalizeSource(%q) = %q, want error", tt.source, got) + } + continue + } + if err != nil || got != tt.want { + t.Errorf("NormalizeSource(%q) = %q, %v; want %q", tt.source, got, err, tt.want) + } + } +} + +func TestResolveChildSource(t *testing.T) { + tests := []struct { + name string + parent string + child string + want string + }{ + { + name: "relative child", + parent: "sp/movie/master.m3u8", + child: "720/index.m3u8", + want: "sp/movie/720/index.m3u8", + }, + { + name: "parent traversal", + parent: "sp/movie/master.m3u8", + child: "../audio/index.m3u8", + want: "sp/audio/index.m3u8", + }, + { + name: "root child remains on default SP origin", + parent: "sp/movie/master.m3u8", + child: "/shared/index.m3u8", + want: "shared/index.m3u8", + }, + { + name: "explicit PMS root namespace is preserved", + parent: "pms/movie/master.m3u8", + child: "/pms/shared/index.m3u8", + want: "pms/shared/index.m3u8", + }, + { + name: "explicit laosiji root namespace is preserved", + parent: "laosiji/m3m/movie/master.m3u8", + child: "/laosiji/shared/index.m3u8", + want: "laosiji/shared/index.m3u8", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ResolveChildSource(tt.parent, tt.child) + if err != nil { + t.Fatalf("ResolveChildSource error: %v", err) + } + if got != tt.want { + t.Fatalf("ResolveChildSource = %q, want %q", got, tt.want) + } + }) + } + + for _, tt := range []struct { + parent string + child string + }{ + {parent: "sp/movie/master.m3u8", child: "https://cdn.example.com/index.m3u8"}, + {parent: "sp/movie/master.m3u8", child: "index.m3u8?token=secret"}, + {parent: "sp/movie/master.m3u8", child: "index.ts"}, + {parent: "sp/movie/master.m3u8", child: `..\index.m3u8`}, + {parent: "sp/movie/master.m3u8", child: "../../outside/index.m3u8"}, + {parent: "sp/movie/master.m3u8", child: "../../../sp/outside/index.m3u8"}, + {parent: "sp/movie/master.m3u8", child: "/pms/outside/index.m3u8"}, + {parent: "pms/movie/master.m3u8", child: "/shared/index.m3u8"}, + {parent: "pms/movie/master.m3u8", child: "/sp/outside/index.m3u8"}, + {parent: "laosiji/movie/master.m3u8", child: "/shared/index.m3u8"}, + } { + if _, err := ResolveChildSource(tt.parent, tt.child); err == nil { + t.Fatalf("unsafe child accepted: parent=%q child=%q", tt.parent, tt.child) + } + } +} diff --git a/common/httputil/http_client.go b/common/httputil/http_client.go new file mode 100644 index 0000000..bfce0c8 --- /dev/null +++ b/common/httputil/http_client.go @@ -0,0 +1,101 @@ +package httputil + +import ( + "errors" + "io" + "net" + "net/http" + "net/url" + "strconv" + "sync" + "time" +) + +const defaultTimeOut = 10 + +var ( + defaultClient *http.Client + httpClientMap sync.Map + errProxyNil = errors.New("proxy is nil") +) + +func init() { + defaultClient = &http.Client{ + Timeout: time.Second * time.Duration(defaultTimeOut), + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: time.Second * 30, + KeepAlive: time.Second * 30, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: time.Second * 90, + TLSHandshakeTimeout: time.Second * 10, + ExpectContinueTimeout: time.Second, + }, + } +} + +// 传入参数单位秒 +func getClientByTimeoutSet(connTimeout int) *http.Client { + if connTimeout == defaultTimeOut { + return defaultClient + } + client, _ := httpClientMap.LoadOrStore(connTimeout, &http.Client{ + Timeout: time.Duration(connTimeout) * time.Second, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: time.Second * 30, + KeepAlive: time.Second * 30, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: time.Second * 90, + TLSHandshakeTimeout: time.Second * 10, + ExpectContinueTimeout: time.Second, + }, + }) + return client.(*http.Client) +} + +type ProxyCfg struct { + Host string + Timeout int //秒 + Source string + IsActive bool //是否开启代理 +} + +func (p *ProxyCfg) Build(rawurl string, rawHeaders map[string]string) (string, map[string]string, error) { + if !p.IsActive { + return rawurl, rawHeaders, nil + } + u, err := url.Parse(rawurl) + if err != nil { + return "", nil, err + } + target := url.URL{Scheme: u.Scheme, Host: u.Host} + u.Host = p.Host + u.Scheme = "http" + m := make(map[string]string) + m["X-Proxy-Target-Host"] = target.String() + m["X-Proxy-Source"] = "ys" + if p.Timeout > 0 { + m["X-Proxy-Timeout"] = strconv.Itoa(p.Timeout) + } + if len(rawHeaders) > 0 { + for k, v := range rawHeaders { + m[k] = v + } + } + return u.String(), m, nil +} + +// 提供关闭函数以防止内存泄露 +func IgnoreResp(resp *http.Response) { + _, _ = io.Copy(io.Discard, resp.Body) + resp.Body.Close() +} diff --git a/common/httputil/http_get.go b/common/httputil/http_get.go new file mode 100644 index 0000000..cf75342 --- /dev/null +++ b/common/httputil/http_get.go @@ -0,0 +1,175 @@ +package httputil + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +func ClientGet(connTimeout int, url string, headers map[string]string, params ...any) (*http.Response, error) { + for _, p := range params { + url = addParams(url, toUrlValues(p)) + } + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + for k, v := range headers { + req.Header.Set(k, v) + } + cl := getClientByTimeoutSet(connTimeout) + return cl.Do(req) +} + +func DefaultClientGet(url string, headers map[string]string, params ...any) (*http.Response, error) { + return ClientGet(defaultTimeOut, url, headers, params...) +} + +func ClientGetBytes(connTimeout int, url string, headers map[string]string, params ...any) (int, []byte, error) { + resp, err := ClientGet(connTimeout, url, headers, params...) + if err != nil { + return 0, nil, err + } + ct, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return resp.StatusCode, nil, err + } + return resp.StatusCode, ct, nil +} + +func DefaultClientGetBytes(url string, headers map[string]string, params ...any) (int, []byte, error) { + return ClientGetBytes(defaultTimeOut, url, headers, params...) +} + +func ClientGetBytesWithProxy(connTimeout int, p *ProxyCfg, url string, headers map[string]string, params ...any) (int, []byte, error) { + if p == nil { + return 0, nil, errProxyNil + } + pUrl, pHeaders, err := p.Build(url, headers) + if err != nil { + return 0, nil, err + } + return ClientGetBytes(connTimeout, pUrl, pHeaders, params...) +} + +func ClientGetWithRespWithProxy(connTimeout int, p *ProxyCfg, bind any, url string, headers map[string]string, params ...any) (int, error) { + code, bts, err := ClientGetBytesWithProxy(connTimeout, p, url, headers, params...) + if err != nil { + return code, err + } + return code, json.Unmarshal(bts, &bind) +} + +func DefaultClientGetWithRespWithProxy(p *ProxyCfg, bind any, url string, headers map[string]string, params ...any) (int, error) { + return ClientGetWithRespWithProxy(defaultTimeOut, p, bind, url, headers, params...) +} + +func ClientGetWithResp(bind any, connTimeout int, url string, headers map[string]string, params ...any) (int, error) { + code, ct, err := ClientGetBytes(connTimeout, url, headers, params...) + if err != nil { + return code, err + } + return code, json.Unmarshal(ct, &bind) +} + +func DefaultClientGetWithResp(bind any, url string, headers map[string]string, params ...any) (int, error) { + return ClientGetWithResp(bind, defaultTimeOut, url, headers, params...) +} + +func ClientGetWithRespWithCtx(ctx context.Context, bind any, connTimeout int, url string, headers map[string]string, params ...any) (int, error) { + code, ct, err := ClientGetBytesWithCtx(ctx, connTimeout, url, headers, params...) + if err != nil { + return code, err + } + return code, json.Unmarshal(ct, &bind) +} + +func DefaultClientGetWithRespWithCtx(ctx context.Context, bind any, url string, headers map[string]string, params ...any) (int, error) { + code, ct, err := ClientGetBytesWithCtx(ctx, defaultTimeOut, url, headers, params...) + if err != nil { + return code, err + } + return code, json.Unmarshal(ct, &bind) +} + +func ClientGetWithCtx(ctx context.Context, connTimeout int, url string, headers map[string]string, params ...any) (*http.Response, error) { + for _, p := range params { + url = addParams(url, toUrlValues(p)) + } + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + for k, v := range headers { + req.Header.Add(k, v) + } + req = req.WithContext(ctx) + cl := getClientByTimeoutSet(connTimeout) + return cl.Do(req) +} + +func DefaultClientGetWithCtx(ctx context.Context, url string, headers map[string]string, params ...any) (*http.Response, error) { + return ClientGetWithCtx(ctx, defaultTimeOut, url, headers, params...) +} + +func ClientGetBytesWithCtx(ctx context.Context, connTimeout int, url string, headers map[string]string, params ...any) (int, []byte, error) { + resp, err := ClientGetWithCtx(ctx, connTimeout, url, headers, params...) + if err != nil { + return 0, nil, err + } + ct, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return resp.StatusCode, nil, err + } + return resp.StatusCode, ct, nil +} + +func DefaultClientGetBytesWithCtx(ctx context.Context, url string, headers map[string]string, params ...any) (int, []byte, error) { + return ClientGetBytesWithCtx(ctx, defaultTimeOut, url, headers, params...) +} + +func toUrlValues(v interface{}) url.Values { + switch t := v.(type) { + case url.Values: + return t + case map[string][]string: + return url.Values(t) + case map[string]string: + rst := make(url.Values) + for k, v := range t { + rst.Add(k, v) + } + return rst + case map[string]interface{}: + rst := make(url.Values) + for k, v := range t { + rst.Add(k, fmt.Sprintf("%v", v)) + } + return rst + case nil: + return make(url.Values) + default: + panic("Invalid value") + } +} + +func addParams(url_ string, params url.Values) string { + if len(params) == 0 { + return url_ + } + if !strings.Contains(url_, "?") { + url_ += "?" + } + if strings.HasSuffix(url_, "?") || strings.HasSuffix(url_, "&") { + url_ += params.Encode() + } else { + url_ += "&" + params.Encode() + } + return url_ +} diff --git a/common/httputil/http_post.go b/common/httputil/http_post.go new file mode 100644 index 0000000..5567d04 --- /dev/null +++ b/common/httputil/http_post.go @@ -0,0 +1,198 @@ +package httputil + +import ( + "bytes" + "context" + "encoding/json" + "io" + "mime/multipart" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" +) + +func ClientPost(connTimeout int, url string, headers map[string]string, data any) (*http.Response, error) { + req, err := getPostRequest(url, headers, data) + if err != nil { + return nil, err + } + cl := getClientByTimeoutSet(connTimeout) + return cl.Do(req) +} + +func DefaultClientPost(url string, headers map[string]string, data any) (*http.Response, error) { + return ClientPost(defaultTimeOut, url, headers, data) +} + +func ClientPostWithCtx(ctx context.Context, connTimeout int, url string, headers map[string]string, data any) (*http.Response, error) { + req, err := getPostRequest(url, headers, data) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + cl := getClientByTimeoutSet(connTimeout) + return cl.Do(req) +} + +func DefaultClientPostWithCtx(ctx context.Context, url string, headers map[string]string, data any) (*http.Response, error) { + return ClientPostWithCtx(ctx, defaultTimeOut, url, headers, data) +} + +func ClientPostWithProxy(connTimeout int, p *ProxyCfg, url string, headers map[string]string, data any) (*http.Response, error) { + if p == nil { + return nil, errProxyNil + } + pUrl, pHeaders, err := p.Build(url, headers) + if err != nil { + return nil, err + } + return ClientPost(connTimeout, pUrl, pHeaders, data) +} + +func ClientPostWithResp(connTimeout int, bind any, url string, headers map[string]string, data any) (int, error) { + resp, err := ClientPost(connTimeout, url, headers, data) + if err != nil { + return 0, err + } + ct, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return resp.StatusCode, err + } + return resp.StatusCode, json.Unmarshal(ct, &bind) +} + +func DefaultClientPostWithResp(bind any, url string, headers map[string]string, data any) (int, error) { + return ClientPostWithResp(defaultTimeOut, bind, url, headers, data) +} + +func ClientPostWithRespWithCtx(ctx context.Context, connTimeout int, bind any, url string, headers map[string]string, data any) (int, error) { + resp, err := ClientPostWithCtx(ctx, connTimeout, url, headers, data) + if err != nil { + return 0, err + } + ct, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return resp.StatusCode, err + } + return resp.StatusCode, json.Unmarshal(ct, &bind) +} + +func DefaultClientPostWithRespWithCtx(ctx context.Context, bind any, url string, headers map[string]string, data any) (int, error) { + return ClientPostWithRespWithCtx(ctx, defaultTimeOut, bind, url, headers, data) +} + +func ClientPostWithRespWithProxy(connTimeout int, p *ProxyCfg, bind any, url string, headers map[string]string, data any) (int, error) { + resp, err := ClientPostWithProxy(connTimeout, p, url, headers, data) + if err != nil { + return 0, err + } + ct, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return resp.StatusCode, err + } + return resp.StatusCode, json.Unmarshal(ct, &bind) +} + +func DefaultClientPostWithRespWithProxy(p *ProxyCfg, bind any, url string, headers map[string]string, data any) (int, error) { + return ClientPostWithRespWithProxy(defaultTimeOut, p, &bind, url, headers, data) +} + +func getPostRequest(url string, headers map[string]string, data any) (*http.Request, error) { + switch data.(type) { + case []byte, string, *bytes.Reader, *bytes.Buffer: + req, err := http.NewRequest(http.MethodPost, url, toReader(data)) + if err != nil { + return nil, err + } + for k, v := range headers { + req.Header.Set(k, v) + } + return req, nil + default: + } + paramsValues := toUrlValues(data) + if checkParamFile(paramsValues) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + for k, v := range paramsValues { + for _, vv := range v { + // is file + if k[0] == '@' { + if err := addFormFile(writer, k[1:], vv); err != nil { + return nil, err + } + continue + } + _ = writer.WriteField(k, vv) + } + } + req, err := http.NewRequest(http.MethodPost, url, body) + if err != nil { + return nil, err + } + for k, v := range headers { + req.Header.Set(k, v) + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + if err = writer.Close(); err != nil { + return nil, err + } + return req, nil + } + req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(paramsValues.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + for k, v := range headers { + req.Header.Set(k, v) + } + return req, nil +} + +func toReader(v interface{}) *bytes.Reader { + switch t := v.(type) { + case []byte: + return bytes.NewReader(t) + case string: + return bytes.NewReader([]byte(t)) + case *bytes.Buffer: + return bytes.NewReader(t.Bytes()) + case *bytes.Reader: + return t + case nil: + return bytes.NewReader(nil) + default: + panic("Invalid value") + } +} + +// Does the params contain a file? +func checkParamFile(params url.Values) bool { + for k := range params { + if k[0] == '@' { + return true + } + } + return false +} + +// Add a file to a multipart writer. +func addFormFile(writer *multipart.Writer, name, path string) error { + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + part, err := writer.CreateFormFile(name, filepath.Base(path)) + if err != nil { + return err + } + _, err = io.Copy(part, file) + return err +} diff --git a/common/httputil/http_post_json.go b/common/httputil/http_post_json.go new file mode 100644 index 0000000..580068c --- /dev/null +++ b/common/httputil/http_post_json.go @@ -0,0 +1,135 @@ +package httputil + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" +) + +func ClientPostJson(connTimeout int, url string, headers map[string]string, data any) (*http.Response, error) { + body, err := dataToJsonReader(data) + if err != nil { + return nil, err + } + req, err := http.NewRequest(http.MethodPost, url, body) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + for k, v := range headers { + req.Header.Set(k, v) + } + cl := getClientByTimeoutSet(connTimeout) + return cl.Do(req) +} + +func DefaultClientPostJson(url string, headers map[string]string, data any) (*http.Response, error) { + return ClientPostJson(defaultTimeOut, url, headers, data) +} + +func ClientPostJsonWithProxy(connTimeout int, p *ProxyCfg, url string, headers map[string]string, params any) (*http.Response, error) { + if p == nil { + return nil, errProxyNil + } + pUrl, pHeaders, err := p.Build(url, headers) + if err != nil { + return nil, err + } + return ClientPostJson(connTimeout, pUrl, pHeaders, params) +} + +func DefaultClientPostJsonWithProxy(p *ProxyCfg, url string, headers map[string]string, params any) (*http.Response, error) { + return ClientPostJsonWithProxy(defaultTimeOut, p, url, headers, params) +} + +func ClientPostJsonWithRespWithProxy(connTimeout int, p *ProxyCfg, bind any, url string, headers map[string]string, data any) (int, error) { + resp, err := ClientPostJsonWithProxy(connTimeout, p, url, headers, data) + if err != nil { + return 0, err + } + ct, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return resp.StatusCode, err + } + return resp.StatusCode, json.Unmarshal(ct, &bind) +} + +func DefaultClientPostJsonWithRespWithProxy(p *ProxyCfg, bind any, url string, headers map[string]string, data any) (int, error) { + return ClientPostJsonWithRespWithProxy(defaultTimeOut, p, bind, url, headers, data) +} + +func ClientPostJsonWithResp(connTimeout int, bind any, url string, headers map[string]string, data any) (int, error) { + resp, err := ClientPostJson(connTimeout, url, headers, data) + if err != nil { + return 0, err + } + ct, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return resp.StatusCode, err + } + return resp.StatusCode, json.Unmarshal(ct, &bind) +} + +func DefaultClientPostJsonWithResp(bind any, url string, headers map[string]string, data any) (int, error) { + return ClientPostJsonWithResp(defaultTimeOut, bind, url, headers, data) +} + +func ClientPostJsonWithCtx(ctx context.Context, connTimeout int, url string, headers map[string]string, data any) (*http.Response, error) { + cl := getClientByTimeoutSet(connTimeout) + body, err := dataToJsonReader(data) + if err != nil { + return nil, err + } + req, err := http.NewRequest(http.MethodPost, url, body) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + for k, v := range headers { + req.Header.Set(k, v) + } + req = req.WithContext(ctx) + return cl.Do(req) +} + +func DefaultClientPostJsonWithCtx(ctx context.Context, url string, headers map[string]string, data any) (*http.Response, error) { + return ClientPostJsonWithCtx(ctx, defaultTimeOut, url, headers, data) +} + +func ClientPostJsonWithRespWithCtx(ctx context.Context, bind any, connTimeout int, url string, headers map[string]string, data any) (int, error) { + resp, err := ClientPostJsonWithCtx(ctx, connTimeout, url, headers, data) + if err != nil { + return 0, err + } + ct, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return resp.StatusCode, err + } + return resp.StatusCode, json.Unmarshal(ct, &bind) +} + +func DefaultClientPostJsonWithRespWithCtx(ctx context.Context, bind any, url string, headers map[string]string, data any) (int, error) { + return ClientPostJsonWithRespWithCtx(ctx, bind, defaultTimeOut, url, headers, data) +} + +func dataToJsonReader(data any) (*bytes.Reader, error) { + var body []byte + switch t := data.(type) { + case []byte: + body = t + case string: + body = []byte(t) + default: + var err error + body, err = json.Marshal(data) + if err != nil { + return nil, err + } + } + return bytes.NewReader(body), nil +} diff --git a/common/httputil/httputil.go b/common/httputil/httputil.go new file mode 100644 index 0000000..e39b55c --- /dev/null +++ b/common/httputil/httputil.go @@ -0,0 +1,282 @@ +package httputil + +import ( + "91porn-server/common/log" + "91porn-server/middleware/requestid" + "context" + "encoding/json" + "errors" + "fmt" + "github.com/ddliu/go-httpclient" + "io" + "net/http" + "strconv" +) + +// HTTPClient htttp客户端 +type HTTPClient struct { + client *httpclient.HttpClient +} + +func NewHTTPClient(client *httpclient.HttpClient) *HTTPClient { + return &HTTPClient{client} +} + +type httpClientPool struct { + cliPool map[string]*httpclient.HttpClient + poolSize int + offset int +} + +// HTTPResponse http响应 +type HTTPResponse struct { + StatusCode int `json:"statusCode"` + Body *httpclient.Response `json:"boby"` +} + +var httpCliPool = httpClientPool{cliPool: map[string]*httpclient.HttpClient{}, poolSize: 50} + +func New() *HTTPClient { + h := httpclient.NewHttpClient().WithOptions(httpclient.Map{ + httpclient.OPT_CONNECTTIMEOUT: 5, + httpclient.OPT_TIMEOUT: 10, + }) + return &HTTPClient{client: h} +} + +func NewCtx(ctx context.Context) *HTTPClient { + reqID, _ := ctx.Value(requestid.ContextKey).(string) + h := httpclient.NewHttpClient().WithOptions(httpclient.Map{ + httpclient.OPT_CONNECTTIMEOUT: 5, + httpclient.OPT_TIMEOUT: 10, + httpclient.OPT_CONTEXT: ctx, + }).WithHeader(requestid.HeaderKey, reqID) + + return &HTTPClient{client: h.Begin()} +} + +// GetHTTPClient 获取httpclient +func Client() *HTTPClient { + cliID := "CLIENT-ID-" + if len(httpCliPool.cliPool) < httpCliPool.poolSize { + h := httpclient.NewHttpClient().Defaults(httpclient.Map{ + httpclient.OPT_CONNECTTIMEOUT: 5, + }) + cliID = cliID + strconv.FormatInt(int64(len(httpCliPool.cliPool)+1), 10) + httpCliPool.cliPool[cliID] = h + httpCliPool.offset = httpCliPool.poolSize + return &HTTPClient{client: h} + } + if httpCliPool.offset == httpCliPool.poolSize+1 { + httpCliPool.offset = 1 + } + cliID = cliID + strconv.FormatInt(int64(httpCliPool.offset), 10) + httpCliPool.offset = httpCliPool.offset + 1 + h, ok := httpCliPool.cliPool[cliID] + if !ok || h == nil { + h = httpclient.NewHttpClient().WithOption(httpclient.OPT_CONNECTTIMEOUT, 10) + httpCliPool.cliPool[cliID] = h + return &HTTPClient{client: h} + } + return &HTTPClient{client: h} +} + +func jsonUnmarshalResp(bind interface{}, resp *HTTPResponse) error { + if bind == nil { //传入空表示只执行 defer + return nil + } + b, err := resp.Body.ReadAll() + if err != nil { + log.Error("httputil jsonUnmarshalResp readall err", log.E(err)) + return err + } + err = json.Unmarshal(b, bind) + if err != nil { + log.Error("httputil jsonUnmarshalResp Unmarshal err", log.E(err)) + } + return err +} + +// Get Get方法 +func (h *HTTPClient) Get(url string, headers map[string]string, params ...interface{}) (*HTTPResponse, error) { + h.client.WithHeaders(headers) + response, err := h.client.Get(url, params...) + if err != nil { + log.Error(fmt.Sprintf("http client Get method failed %+v:", err)) + return nil, err + } + return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil +} + +func (h *HTTPClient) PGet(p *ProxyCfg, url string, headers map[string]string, params ...interface{}) (*HTTPResponse, error) { + purl, pheaders, err := p.Build(url, headers) + if err != nil { + return nil, err + } + return h.Get(purl, pheaders, params) +} + +// Get GetBytes +func (h *HTTPClient) GetBytes(url string, headers map[string]string, params ...interface{}) ([]byte, error) { + h.client.WithHeaders(headers) + response, err := h.client.Get(url, params...) + if err != nil { + log.Error(fmt.Sprintf("http client Get method failed %+v:", err)) + return nil, err + } + if response.StatusCode != http.StatusOK { + log.Error("http client Get method status code not ok", log.Any("url", url), log.Any("headers", headers), log.Any("params", params), log.Any("res", response)) + return nil, errors.New("http response satus code not ok statusCoe:" + response.Status) + } + data, err := response.ReadAll() + if err != nil { + log.ZapLog.Error("resp readAll errror", log.Any("Error", err)) + return data, err + } + return data, nil +} + +// GetWithJResp 结果json.Unmarshal到bind中 +func (h *HTTPClient) GetWithJResp(bind interface{}, url string, headers map[string]string, params ...interface{}) (int, error) { + resp, err := h.Get(url, headers, params...) + if err != nil { + return 0, err + } + defer func() { _ = resp.Body.Body.Close() }() + if resp.StatusCode == http.StatusOK { + err = jsonUnmarshalResp(bind, resp) + } + return resp.StatusCode, err +} + +func (h *HTTPClient) PGetWithJResp(p *ProxyCfg, bind interface{}, url string, headers map[string]string, params ...interface{}) (int, error) { + purl, pheaders, err := p.Build(url, headers) + if err != nil { + return 0, err + } + return h.GetWithJResp(bind, purl, pheaders, params...) +} + +// Post Post方法 +func (h *HTTPClient) Post(url string, headers map[string]string, params interface{}) (*HTTPResponse, error) { + h.client.WithHeaders(headers) + response, err := h.client.Post(url, params) + if err != nil { + log.Error(fmt.Sprintf("http client Post method failed %+v:", err)) + return nil, err + } + return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil +} + +func (h *HTTPClient) PPost(p *ProxyCfg, bind interface{}, url string, headers map[string]string, params interface{}) (*HTTPResponse, error) { + purl, pheaders, err := p.Build(url, headers) + if err != nil { + return nil, err + } + return h.Post(purl, pheaders, params) +} + +// POSTJson PostJson方法 +func (h *HTTPClient) POSTJson(url string, headers map[string]string, data interface{}) (*HTTPResponse, error) { + h.client.WithHeaders(headers) + response, err := h.client.PostJson(url, data) + if err != nil { + log.Error(fmt.Sprintf("http client POSTJson method failed %+v:", err)) + return nil, err + } + return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil +} + +// POSTJson PostJson方法, bind json.Unmarshal +func (h *HTTPClient) POSTJsonWithJResp(bind interface{}, url string, headers map[string]string, data interface{}) (int, error) { + resp, err := h.POSTJson(url, headers, data) + if err != nil { + log.Error("POSTJsonWithResp err", log.E(err)) + return 0, err + } + defer resp.Body.Body.Close() + if resp.StatusCode == http.StatusOK { + err = jsonUnmarshalResp(bind, resp) + } + return resp.StatusCode, err +} + +func (h *HTTPClient) PPOSTJsonWithJResp(p *ProxyCfg, bind interface{}, url string, headers map[string]string, data interface{}) (int, error) { + if p == nil { + return 0, fmt.Errorf("ProxyCfg is nil") + } + purl, pheaders, err := p.Build(url, headers) + if err != nil { + return 0, err + } + return h.POSTJsonWithJResp(bind, purl, pheaders, data) +} + +// PostMultipart PostMultipart 上传文件 +func (h *HTTPClient) PostMultipart(url string, headers map[string]string, params interface{}) (*HTTPResponse, error) { + h.client.WithHeaders(headers) + response, err := h.client.PostMultipart(url, params) + if err != nil { + log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err)) + return nil, err + } + return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil +} + +// Put 流式传输 可用于上传文件 +func (h *HTTPClient) Put(url string, headers map[string]string, body io.Reader) (*HTTPResponse, error) { + h.client.WithHeaders(headers) + response, err := h.client.Put(url, body) + if err != nil { + log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err)) + return nil, err + } + return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil +} + +// PutJSON PutJSON方法 +func (h *HTTPClient) PutJSON(url string, headers map[string]string, data interface{}) (*HTTPResponse, error) { + h.client.WithHeaders(headers) + response, err := h.client.PutJson(url, data) + if err != nil { + log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err)) + return nil, err + } + return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil +} + +// Header Header +func (h *HTTPClient) Header(url string) (*HTTPResponse, error) { + response, err := h.client.Head(url) + if err != nil { + log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err)) + return nil, err + } + return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil +} + +// POSTWithJResp 结果json.Unmarshal到bind中 用于上传文件, content-type :"application/x-www-form-urlencoded" 如参数以@开头 则为上传文件 +func (h *HTTPClient) POSTWithJResp(bind interface{}, url string, headers map[string]string, params interface{}) (int, error) { + resp, err := h.Post(url, headers, params) + if err != nil { + return 0, err + } + defer resp.Body.Body.Close() + if resp.StatusCode == http.StatusOK { + err = jsonUnmarshalResp(bind, resp) + } + return resp.StatusCode, err +} + +// PPOSTWithJResp 结果json.Unmarshal到bind中 用于上传文件, content-type :"application/x-www-form-urlencoded" 如参数以@开头 则为上传文件 +func (h *HTTPClient) PPOSTWithJResp(p *ProxyCfg, bind interface{}, url string, headers map[string]string, params interface{}) (int, error) { + resp, err := h.PPost(p, bind, url, headers, params) + if err != nil { + return 0, err + } + defer resp.Body.Body.Close() + if resp.StatusCode == http.StatusOK { + err = jsonUnmarshalResp(bind, resp) + } + return resp.StatusCode, err +} diff --git a/common/imclient/client.go b/common/imclient/client.go new file mode 100644 index 0000000..db3297a --- /dev/null +++ b/common/imclient/client.go @@ -0,0 +1,879 @@ +package imclient + +import ( + "bytes" + "crypto/aes" + "crypto/md5" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "time" + + "91porn-server/common/crypt/ecb" + "91porn-server/common/log" + + "github.com/moul/http2curl" +) + +const ( + MessageTypeText = 0 + MessageTypeImage = 2 +) + +type Config struct { + Enable bool + BaseURL string + MerchantCode string + TenantCode string + AppKey string + ClientID string + ClientSecret string + SignKey string + AESKey string + EnableSign bool + EncryptTimestamp bool + OS string + OSType string + BusinessType string + Language string + TokenTTL int64 + RequestTimeoutSeconds int +} + +type Client struct { + cfg Config + httpClient *http.Client +} + +type Response struct { + Code int `json:"code"` + Data json.RawMessage `json:"data"` + Msg string `json:"msg"` + Message string `json:"message"` + Result string `json:"result"` +} + +type Attachment struct { + URL string `json:"url,omitempty"` + ThumbURL string `json:"thumbUrl,omitempty"` + Type string `json:"type,omitempty"` + Size int64 `json:"size,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + Length int64 `json:"length,omitempty"` + FileName string `json:"fileName,omitempty"` +} + +type AppTokenRequest struct { + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + OSType string `json:"osType"` + TTL int64 `json:"ttl,omitempty"` +} + +type UserTokenRequest struct { + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + UserID int64 `json:"userId"` + OSType string `json:"osType"` + TTL int64 `json:"ttl,omitempty"` +} + +type RegisterRequest struct { + ThirdPartyID string `json:"thirdPartyId"` + Password string `json:"password"` + Nickname string `json:"nickname,omitempty"` + Avatar string `json:"avatar,omitempty"` +} + +type UpdateBaseInfoRequest struct { + UserID int64 `json:"userId"` + Name string `json:"name,omitempty"` + ImgURL string `json:"imgUrl,omitempty"` + Signature string `json:"signature,omitempty"` +} + +type UserBaseInfo struct { + ID int64 `json:"id"` + UserPhone string `json:"userPhone"` + Name string `json:"name"` + Sex int `json:"sex"` + ImgURL string `json:"imgUrl"` + Enable int `json:"enable"` + CreateTime int64 `json:"createTime"` + Signature string `json:"signature"` +} + +type UpdatePasswordRequest struct { + UserID int64 `json:"userId"` + Pwd string `json:"pwd"` + Code string `json:"code,omitempty"` +} + +type UpdateHeadImgRequest struct { + UserID int64 `json:"userId"` + ImgURL string `json:"imgUrl"` +} + +type SetOnlineStatusRequest struct { + UserID int64 `json:"userId"` + ShowOnlineStatus bool `json:"showOnlineStatus"` +} + +type BatchOnlineStatusRequest struct { + UserIDs []int64 `json:"userIds"` +} + +type OnlineStatus struct { + UserID int64 `json:"userId"` + UserName string `json:"userName"` + Visible bool `json:"visible"` + Online bool `json:"online"` +} + +type Friend struct { + FriendID int64 `json:"friendId"` + Remark string `json:"remark"` + CreatedAt int64 `json:"createdAt"` + UpdatedAt int64 `json:"updatedAt"` + Status int `json:"status"` +} + +type AddFriendRequest struct { + UserID int64 `json:"userId"` + FriendID int64 `json:"friendId"` + Message string `json:"message,omitempty"` +} + +type DirectAddFriendRequest struct { + UserID int64 `json:"userId"` + FriendID int64 `json:"friendId"` + Message string `json:"message,omitempty"` + Archive bool `json:"archive,omitempty"` +} + +type HandleFriendRequest struct { + RequestID int64 `json:"requestId"` + UserID int64 `json:"userId"` +} + +type PendingFriendRequest struct { + ID int64 `json:"id"` + FriendID int64 `json:"friendId"` + Message string `json:"message"` + Status int `json:"status"` + CreatedAt int64 `json:"createdAt"` + UpdatedAt int64 `json:"updatedAt"` + HandleTime int64 `json:"handleTime"` + ExpireTime int64 `json:"expireTime"` +} + +type AvailableFriend struct { + UserID int64 `json:"userId"` + FriendID int64 `json:"friendId"` + ConvID string `json:"convId"` + Remark string `json:"remark"` + CreatedAt int64 `json:"createdAt"` + UpdatedAt int64 `json:"updatedAt"` + DisturbStatus int `json:"disturbStatus"` + Archive int `json:"archive"` +} + +type SetFriendArchiveRequest struct { + UserID int64 `json:"userId"` + FriendID int64 `json:"friendId"` + Archive bool `json:"archive"` +} + +type SendMessageRequest struct { + SenderID int64 `json:"senderId"` + ReceiverID int64 `json:"receiverId"` + Content string `json:"content"` + MessageType int `json:"messageType"` + ExtInfo string `json:"extInfo,omitempty"` + ChannelType string `json:"channelType,omitempty"` + Attachment *Attachment `json:"attachment,omitempty"` +} + +type BatchSendMessageRequest struct { + SenderID int64 `json:"senderId"` + ReceiverIDSet []int64 `json:"receiverIdSet"` + Content string `json:"content"` + MessageType int `json:"messageType"` + ExtInfo string `json:"extInfo,omitempty"` + ChannelType string `json:"channelType,omitempty"` + Attachment *Attachment `json:"attachment,omitempty"` +} + +type OnlinePassthroughRequest struct { + SenderID int64 `json:"senderId,omitempty"` + ReceiverIDSet []int64 `json:"receiverIdSet"` + PassthroughType string `json:"passthroughType,omitempty"` + Content string `json:"content,omitempty"` + ExtInfo string `json:"extInfo,omitempty"` + ChannelType string `json:"channelType,omitempty"` +} + +type AppPassthroughRequest struct { + SenderID int64 `json:"senderId,omitempty"` + PassthroughType string `json:"passthroughType,omitempty"` + Content string `json:"content,omitempty"` + ExtInfo string `json:"extInfo,omitempty"` + ChannelType string `json:"channelType,omitempty"` +} + +type PassthroughResult struct { + MessageID string `json:"messageId"` + MessageType int `json:"messageType"` + ReceiverCount int `json:"receiverCount"` + CreatedAt int64 `json:"createdAt"` +} + +type MessageSendResult struct { + MessageID string `json:"messageId"` + ConvID string `json:"convId"` + Seq int64 `json:"seq"` + CreatedAt int64 `json:"createdAt"` +} + +type HistoryMessageRequest struct { + UserID1 int64 `json:"userId1"` + UserID2 int64 `json:"userId2"` + StartTime int64 `json:"startTime,omitempty"` + EndTime int64 `json:"endTime,omitempty"` + StartSeq int64 `json:"startSeq,omitempty"` + EndSeq int64 `json:"endSeq,omitempty"` + Direction string `json:"direction,omitempty"` + Size int `json:"size,omitempty"` +} + +type MessageDetail struct { + MessageID string `json:"messageId"` + SenderID int64 `json:"senderId"` + ReceiverID int64 `json:"receiverId"` + Content string `json:"content"` + MessageType string `json:"messageType"` + Seq int64 `json:"seq"` + CreatedAt int64 `json:"createdAt"` +} + +type ModifyMessageRequest struct { + MessageID string `json:"messageId"` + ConvID string `json:"convId"` + SenderID int64 `json:"senderId"` + ReceiverID int64 `json:"receiverId"` + ModifierID int64 `json:"modifierId"` + Content string `json:"content,omitempty"` + ExtInfo string `json:"extInfo,omitempty"` +} + +type HideInboxRequest struct { + UserID int64 `json:"userId"` + ConvID string `json:"convId"` + StartSeq int64 `json:"startSeq"` + EndSeq int64 `json:"endSeq"` +} + +func New(cfg Config) *Client { + timeout := cfg.RequestTimeoutSeconds + if timeout <= 0 { + timeout = 10 + } + return &Client{ + cfg: cfg, + httpClient: &http.Client{ + Timeout: time.Duration(timeout) * time.Second, + }, + } +} + +func (c *Client) Enabled() bool { + return c.DisabledReason() == "" +} + +// DisabledReason 返回 SDK 不可用的具体原因(未开启 / 缺失的配置项);可用时返回 ""。 +func (c *Client) DisabledReason() string { + if c == nil { + return "im client is nil" + } + if !c.cfg.Enable { + return "config Enable=false" + } + var missing []string + if c.cfg.BaseURL == "" { + missing = append(missing, "BaseURL") + } + if c.cfg.MerchantCode == "" { + missing = append(missing, "MerchantCode") + } + if c.tenantCode() == "" { + missing = append(missing, "TenantCode/AppKey") + } + if len(missing) > 0 { + missingStr := "missing config: " + strings.Join(missing, ", ") + log.Error(missingStr) + return missingStr + } + return "" +} + +func (c *Client) AppToken() (string, error) { + req := AppTokenRequest{ + ClientID: c.cfg.ClientID, + ClientSecret: c.cfg.ClientSecret, + OSType: c.osType(), + TTL: c.tokenTTL(), + } + return c.postString("/authenticate/token.e", "", req) +} + +func (c *Client) UserToken(userID int64) (string, error) { + req := UserTokenRequest{ + ClientID: c.cfg.ClientID, + ClientSecret: c.cfg.ClientSecret, + UserID: userID, + OSType: c.osType(), + TTL: c.tokenTTL(), + } + return c.postString("/authenticate/user/token.e", "", req) +} + +func (c *Client) Register(req RegisterRequest, token string) (int64, error) { + var userID int64 + if err := c.postData("/user/register", token, req, &userID); err != nil { + return 0, err + } + return userID, nil +} + +func (c *Client) UpdateBaseInfo(req UpdateBaseInfoRequest, token string) error { + var ok bool + return c.postData("/user/updateBaseInfo", token, req, &ok) +} + +func (c *Client) UserBaseInfo(userID int64, token string) (*UserBaseInfo, error) { + query := url.Values{} + query.Set("userId", strconv.FormatInt(userID, 10)) + var info UserBaseInfo + if err := c.getData("/user/baseUserInfo", token, query, nil, &info); err != nil { + return nil, err + } + return &info, nil +} + +func (c *Client) UpdatePassword(req UpdatePasswordRequest, token string) error { + return c.postData("/user/updatePwd", token, req, nil) +} + +func (c *Client) UpdateHeadImg(req UpdateHeadImgRequest, token string) error { + var ok bool + return c.postData("/user/updateHeadImg", token, req, &ok) +} + +func (c *Client) SetOnlineStatus(req SetOnlineStatusRequest, token string) error { + var ok bool + return c.postData("/user/onlineStatus/set", token, req, &ok) +} + +func (c *Client) BatchOnlineStatus(req BatchOnlineStatusRequest, token string) ([]OnlineStatus, error) { + var statuses []OnlineStatus + if err := c.postData("/user/onlineStatus/batch", token, req, &statuses); err != nil { + return nil, err + } + return statuses, nil +} + +func (c *Client) FriendList(userID, now int64, token string) ([]Friend, error) { + query := url.Values{} + query.Set("userId", strconv.FormatInt(userID, 10)) + if now > 0 { + query.Set("now", strconv.FormatInt(now, 10)) + } + headers := map[string]string{ + "userId": strconv.FormatInt(userID, 10), + } + var friends []Friend + if err := c.getData("/friend/list", token, query, headers, &friends); err != nil { + return nil, err + } + return friends, nil +} + +func (c *Client) AddFriend(req AddFriendRequest, token string) (int64, error) { + var requestID int64 + if err := c.postData("/friend/add", token, req, &requestID); err != nil { + return 0, err + } + return requestID, nil +} + +func (c *Client) DirectAddFriend(req DirectAddFriendRequest, token string) error { + var ok bool + return c.postData("/friend/add/direct", token, req, &ok) +} + +func (c *Client) ConfirmFriend(req HandleFriendRequest, token string) error { + var ok bool + return c.postData("/friend/confirm", token, req, &ok) +} + +func (c *Client) RejectFriend(req HandleFriendRequest, token string) error { + var ok bool + return c.postData("/friend/reject", token, req, &ok) +} + +func (c *Client) DeleteFriend(userID, friendID int64, token string) error { + query := url.Values{} + query.Set("userId", strconv.FormatInt(userID, 10)) + query.Set("friendId", strconv.FormatInt(friendID, 10)) + var ok bool + return c.getData("/friend/delete", token, query, nil, &ok) +} + +func (c *Client) DeleteAllFriends(userID int64, token string) error { + query := url.Values{} + query.Set("userId", strconv.FormatInt(userID, 10)) + var ok bool + return c.getData("/allFriend/delete", token, query, nil, &ok) +} + +func (c *Client) PendingFriendRequests(userID, now int64, token string) ([]PendingFriendRequest, error) { + query := url.Values{} + query.Set("userId", strconv.FormatInt(userID, 10)) + if now > 0 { + query.Set("now", strconv.FormatInt(now, 10)) + } + var requests []PendingFriendRequest + if err := c.getData("/friend/pending", token, query, nil, &requests); err != nil { + return nil, err + } + return requests, nil +} + +func (c *Client) AvailableFriends(userID int64, token string) ([]AvailableFriend, error) { + query := url.Values{} + query.Set("userId", strconv.FormatInt(userID, 10)) + var friends []AvailableFriend + if err := c.getData("/friend/available/list", token, query, nil, &friends); err != nil { + return nil, err + } + return friends, nil +} + +func (c *Client) SetFriendArchive(req SetFriendArchiveRequest, token string) error { + var ok bool + return c.postData("/friend/archive", token, req, &ok) +} + +func (c *Client) SendMessage(req SendMessageRequest, token string) error { + _, err := c.SendMessageWithResult(req, token) + return err +} + +func (c *Client) SendMessageWithResult(req SendMessageRequest, token string) (*MessageSendResult, error) { + if req.ExtInfo == "" { + req.ExtInfo = "{}" + } + if req.ChannelType == "" { + req.ChannelType = "UNKNOW" + } + var result MessageSendResult + if err := c.postData("/message/send", token, req, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (c *Client) BatchSendMessage(req BatchSendMessageRequest, token string) error { + _, err := c.BatchSendMessageWithResult(req, token) + return err +} + +func (c *Client) BatchSendMessageWithResult(req BatchSendMessageRequest, token string) (json.RawMessage, error) { + if req.ExtInfo == "" { + req.ExtInfo = "{}" + } + if req.ChannelType == "" { + req.ChannelType = "UNKNOW" + } + var data json.RawMessage + if err := c.postData("/message/send/batch", token, req, &data); err != nil { + return nil, err + } + return data, nil +} + +func (c *Client) HistoryMessages(req HistoryMessageRequest, token string) ([]MessageDetail, error) { + var messages []MessageDetail + if err := c.postData("/message/history", token, req, &messages); err != nil { + return nil, err + } + return messages, nil +} + +func (c *Client) MessageDetail(messageID, convID, token string) (*MessageDetail, error) { + query := url.Values{} + query.Set("messageId", messageID) + query.Set("convId", convID) + var detail MessageDetail + if err := c.getData("/message/detail", token, query, nil, &detail); err != nil { + return nil, err + } + return &detail, nil +} + +func (c *Client) ModifyMessage(req ModifyMessageRequest, token string) (string, error) { + var messageID string + if err := c.postData("/message/modify", token, req, &messageID); err != nil { + return "", err + } + return messageID, nil +} + +func (c *Client) HideInbox(req HideInboxRequest, token string) (int64, error) { + var count int64 + if err := c.postData("/message/inbox/hide", token, req, &count); err != nil { + return 0, err + } + return count, nil +} + +func (c *Client) SendOnlinePassthrough(req OnlinePassthroughRequest, token string) (*PassthroughResult, error) { + if len(req.ReceiverIDSet) == 0 { + return nil, errors.New("receiverIdSet is empty") + } + normalizePassthrough(&req.PassthroughType, &req.ExtInfo, &req.ChannelType) + var result PassthroughResult + if err := c.postData("/message/passthrough/send", token, req, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (c *Client) SendAppPassthrough(req AppPassthroughRequest, token string) (*PassthroughResult, error) { + normalizePassthrough(&req.PassthroughType, &req.ExtInfo, &req.ChannelType) + var result PassthroughResult + if err := c.postData("/message/passthrough/send/batch/all", token, req, &result); err != nil { + return nil, err + } + return &result, nil +} + +func normalizePassthrough(passthroughType, extInfo, channelType *string) { + if *passthroughType == "" { + *passthroughType = "AD_NOTIFY" + } + if *extInfo == "" { + *extInfo = "{}" + } + if *channelType == "" { + *channelType = "UNKNOW" + } +} + +func marshalRequestBody(data any) ([]byte, error) { + switch req := data.(type) { + case AppTokenRequest: + return marshalOrderedJSON(map[string]any{ + "clientId": req.ClientID, + "clientSecret": req.ClientSecret, + "osType": req.OSType, + "ttl": req.TTL, + }) + case UserTokenRequest: + return marshalOrderedJSON(map[string]any{ + "clientId": req.ClientID, + "clientSecret": req.ClientSecret, + "osType": req.OSType, + "ttl": req.TTL, + "userId": req.UserID, + }) + case RegisterRequest: + fields := map[string]any{ + "password": req.Password, + "thirdPartyId": req.ThirdPartyID, + } + if req.Nickname != "" { + fields["nickname"] = req.Nickname + } + if req.Avatar != "" { + fields["avatar"] = req.Avatar + } + return marshalOrderedJSON(fields) + default: + return json.Marshal(data) + } +} + +func marshalOrderedJSON(fields map[string]any) ([]byte, error) { + keys := make([]string, 0, len(fields)) + for key, value := range fields { + if value == nil { + continue + } + if s, ok := value.(string); ok && s == "" { + continue + } + if n, ok := value.(int64); ok && n <= 0 { + continue + } + keys = append(keys, key) + } + sort.Strings(keys) + + var buf bytes.Buffer + buf.WriteByte('{') + for i, key := range keys { + if i > 0 { + buf.WriteByte(',') + } + keyJSON, err := json.Marshal(key) + if err != nil { + return nil, err + } + valueJSON, err := json.Marshal(fields[key]) + if err != nil { + return nil, err + } + buf.Write(keyJSON) + buf.WriteByte(':') + buf.Write(valueJSON) + } + buf.WriteByte('}') + return buf.Bytes(), nil +} + +func (c *Client) postString(path, token string, data any) (string, error) { + var out string + if err := c.postData(path, token, data, &out); err != nil { + return "", err + } + return out, nil +} + +func (c *Client) postData(path, token string, data any, out any) error { + if !c.Enabled() { + return errors.New("im client is disabled") + } + body, err := marshalRequestBody(data) + if err != nil { + return err + } + resp, err := c.do(http.MethodPost, path, token, nil, nil, body) + if err != nil { + return err + } + return decodeResponseData(resp, out) +} + +func (c *Client) getData(path, token string, query url.Values, headers map[string]string, out any) error { + if !c.Enabled() { + return errors.New("im client is disabled") + } + resp, err := c.do(http.MethodGet, path, token, query, headers, nil) + if err != nil { + return err + } + return decodeResponseData(resp, out) +} + +func decodeResponseData(resp *Response, out any) error { + if out == nil || len(resp.Data) == 0 || bytes.Equal(resp.Data, []byte("null")) { + return nil + } + return json.Unmarshal(resp.Data, out) +} + +func (c *Client) do(method, path, token string, query url.Values, headers map[string]string, body []byte) (*Response, error) { + fullURL, queryString := c.endpoint(path, query) + req, err := http.NewRequest(method, fullURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("X-Merchant-Code", c.cfg.MerchantCode) + req.Header.Set("X-App-Key", c.appKey()) + req.Header.Set("X-Client-Id", c.cfg.ClientID) + if token != "" { + req.Header.Set("token", token) + } + for k, v := range headers { + req.Header.Set(k, v) + } + for k, v := range c.baseHeaders(queryString, body) { + req.Header.Set(k, v) + } + start := time.Now() + httpResp, err := c.httpClient.Do(req) + if err != nil { + log.Warn("im sdk request error", log.Any("method", method), log.Any("path", path), log.Any("costMs", time.Since(start).Milliseconds()), log.Any("curl", requestCurl(req, body)), log.E(err)) + return nil, err + } + defer httpResp.Body.Close() + respBody, err := io.ReadAll(httpResp.Body) + cost := time.Since(start) + if err != nil { + log.Warn("im sdk read body error", log.Any("method", method), log.Any("path", path), log.Any("costMs", cost.Milliseconds()), log.E(err)) + return nil, err + } + log.Info("im sdk request", log.Any("method", method), log.Any("path", path), log.Any("status", httpResp.StatusCode), log.Any("costMs", cost.Milliseconds())) + if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { + log.Warn("im sdk http status error", log.Any("method", method), log.Any("path", path), log.Any("status", httpResp.StatusCode), log.Any("curl", requestCurl(req, body)), log.Any("resp", string(respBody))) + return nil, apiErrorFromResponse(httpResp.StatusCode, respBody) + } + var resp Response + if err = json.Unmarshal(respBody, &resp); err != nil { + log.Warn("im sdk resp unmarshal error", log.Any("method", method), log.Any("path", path), log.Any("curl", requestCurl(req, body)), log.Any("resp", string(respBody)), log.E(err)) + return nil, err + } + if resp.Code != 0 { + log.Warn("im sdk api error", log.Any("method", method), log.Any("path", path), log.Any("code", resp.Code), log.Any("curl", requestCurl(req, body)), log.Any("resp", string(respBody))) + return nil, &APIError{ + StatusCode: httpResp.StatusCode, + Code: resp.Code, + Message: firstNonEmpty(resp.Message, resp.Msg, resp.Result), + Body: respBody, + } + } + return &resp, nil +} + +// requestCurl 把请求还原成 curl 命令字符串,用于报错时打印复现。 +// body 为请求体原始字节:Do 已消费 req.Body,这里用 body 重新填充再生成。 +func requestCurl(req *http.Request, body []byte) string { + req.Body = io.NopCloser(bytes.NewReader(body)) + cmd, err := http2curl.GetCurlCommand(req) + if err != nil { + return "" + } + return cmd.String() +} + +func apiErrorFromResponse(statusCode int, body []byte) *APIError { + resp := Response{} + _ = json.Unmarshal(body, &resp) + return &APIError{ + StatusCode: statusCode, + Code: resp.Code, + Message: firstNonEmpty(resp.Message, resp.Msg, resp.Result, string(body)), + Body: body, + } +} + +func (c *Client) endpoint(path string, query url.Values) (string, string) { + base := strings.TrimRight(c.cfg.BaseURL, "/") + path = strings.TrimLeft(path, "/") + u := fmt.Sprintf("%s/api/endpoint/%s/%s/%s", base, url.PathEscape(c.cfg.MerchantCode), url.PathEscape(c.tenantCode()), path) + if len(query) == 0 { + return u, "" + } + queryString := query.Encode() + return u + "?" + queryString, queryString +} + +func (c *Client) baseHeaders(queryString string, body []byte) map[string]string { + headers := map[string]string{ + "os": c.os(), + "osType": c.osType(), + "businessType": c.businessType(), + "language": c.language(), + } + if !c.cfg.EnableSign || c.cfg.SignKey == "" { + return headers + } + timestamps := strconv.FormatInt(time.Now().UnixMilli(), 10) + headers["timestamps"] = timestamps + signTimestamp := timestamps + if c.cfg.EncryptTimestamp && c.cfg.AESKey != "" { + if encrypted := encryptTimestamp(timestamps, c.cfg.AESKey); encrypted != "" { + headers["envTimestamps"] = encrypted + } + } + signBody := queryString + if len(body) > 0 { + signBody += string(body) + } + sum := md5.Sum([]byte(signBody + signTimestamp + c.cfg.SignKey)) + headers["sign"] = hex.EncodeToString(sum[:]) + return headers +} + +func (c *Client) os() string { + if c.cfg.OS != "" { + return c.cfg.OS + } + return "web" +} + +func (c *Client) osType() string { + if c.cfg.OSType != "" { + return c.cfg.OSType + } + return "web" +} + +func (c *Client) businessType() string { + if c.cfg.BusinessType != "" { + return c.cfg.BusinessType + } + return "im_sdk_customer" +} + +func (c *Client) language() string { + if c.cfg.Language != "" { + return c.cfg.Language + } + return "zh-cn" +} + +func (c *Client) tokenTTL() int64 { + if c.cfg.TokenTTL > 0 { + return c.cfg.TokenTTL + } + return 86400 +} + +func (c *Client) appKey() string { + if c.cfg.AppKey != "" { + return c.cfg.AppKey + } + if c.cfg.MerchantCode == "" || c.cfg.TenantCode == "" { + return "" + } + return c.cfg.MerchantCode + "#" + c.cfg.TenantCode +} + +func (c *Client) tenantCode() string { + if c.cfg.TenantCode != "" { + return c.cfg.TenantCode + } + return c.cfg.AppKey +} + +func encryptTimestamp(timestamps, key string) string { + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return "" + } + src := zeroPadding([]byte(timestamps), block.BlockSize()) + dst := make([]byte, len(src)) + ecb.NewECBEncrypter(block).CryptBlocks(dst, src) + return base64.StdEncoding.EncodeToString(dst) +} + +func zeroPadding(src []byte, blockSize int) []byte { + padding := blockSize - len(src)%blockSize + if padding == blockSize { + padding = blockSize + } + return append(src, bytes.Repeat([]byte{0}, padding)...) +} diff --git a/common/imclient/defaults.go b/common/imclient/defaults.go new file mode 100644 index 0000000..d05eff4 --- /dev/null +++ b/common/imclient/defaults.go @@ -0,0 +1,6 @@ +package imclient + +const ( + DefaultSignKey = "shDOUArrDhpeAMw9FGY79Zmy3MLWwNWy" + DefaultAESKey = "4d5bc50346c22dde12be2c3b1b89ada6" +) diff --git a/common/imclient/errors.go b/common/imclient/errors.go new file mode 100644 index 0000000..51191cf --- /dev/null +++ b/common/imclient/errors.go @@ -0,0 +1,41 @@ +package imclient + +import ( + "fmt" + "net/http" + "strings" +) + +const sessionExpiredCode = 401 + +type APIError struct { + StatusCode int + Code int + Message string + Body []byte +} + +func (e *APIError) Error() string { + if e == nil { + return "" + } + if e.Code != 0 { + return fmt.Sprintf("im api error: status=%d code=%d message=%s", e.StatusCode, e.Code, e.Message) + } + return fmt.Sprintf("im api error: status=%d message=%s", e.StatusCode, e.Message) +} + +func IsSessionExpired(err error) bool { + apiErr, ok := err.(*APIError) + if !ok || apiErr == nil { + return false + } + if apiErr.Code == sessionExpiredCode || apiErr.StatusCode == http.StatusUnauthorized { + return true + } + msg := strings.ToLower(apiErr.Message) + return strings.Contains(msg, "session has expired") || + strings.Contains(msg, "log in again") || + strings.Contains(msg, "token expired") || + strings.Contains(msg, "token invalid") +} diff --git a/common/imclient/history_message.go b/common/imclient/history_message.go new file mode 100644 index 0000000..5dbbe04 --- /dev/null +++ b/common/imclient/history_message.go @@ -0,0 +1,318 @@ +package imclient + +import ( + "encoding/base64" + "encoding/json" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "google.golang.org/protobuf/encoding/protowire" +) + +type historyMessageWire struct { + MessageID json.RawMessage `json:"messageId"` + ID json.RawMessage `json:"id"` + MsgID json.RawMessage `json:"msgId"` + MID json.RawMessage `json:"mid"` + SenderID int64 `json:"senderId"` + ReceiverID int64 `json:"receiverId"` + Content string `json:"content"` + Text string `json:"text"` + Body string `json:"body"` + MsgContent string `json:"msgContent"` + MessageContent string `json:"messageContent"` + Payload json.RawMessage `json:"payload"` + ExtInfo json.RawMessage `json:"extInfo"` + Attachment json.RawMessage `json:"attachment"` + Message json.RawMessage `json:"message"` + MessageType json.RawMessage `json:"messageType"` + Seq int64 `json:"seq"` + CreatedAt int64 `json:"createdAt"` +} + +func (m *MessageDetail) UnmarshalJSON(data []byte) error { + var wire historyMessageWire + if err := json.Unmarshal(data, &wire); err != nil { + return err + } + m.SenderID = wire.SenderID + m.ReceiverID = wire.ReceiverID + m.Seq = wire.Seq + m.CreatedAt = wire.CreatedAt + m.MessageID = firstNonEmpty( + jsonScalarString(wire.MessageID), + jsonScalarString(wire.MsgID), + jsonScalarString(wire.MID), + jsonScalarString(wire.ID), + ) + m.MessageType = normalizeHistoryMessageType(wire.MessageType) + if m.MessageType == "" { + m.MessageType = extractPayloadMessageType(wire.Payload) + } + m.Content = firstNonEmpty( + strings.TrimSpace(wire.Content), + strings.TrimSpace(wire.Text), + strings.TrimSpace(wire.Body), + strings.TrimSpace(wire.MsgContent), + strings.TrimSpace(wire.MessageContent), + extractPayloadContent(wire.Payload, m.MessageID, strconv.FormatInt(m.SenderID, 10), strconv.FormatInt(m.ReceiverID, 10)), + extractPayloadContent(wire.ExtInfo, m.MessageID, strconv.FormatInt(m.SenderID, 10), strconv.FormatInt(m.ReceiverID, 10)), + extractAttachmentContent(wire.Attachment), + ) + if nested := decodeHistoryMessageWire(wire.Message); nested != nil { + if m.MessageID == "" { + m.MessageID = nested.messageID + } + if m.Content == "" { + m.Content = nested.content + } + if m.MessageType == "" { + m.MessageType = nested.messageType + } + } + if m.MessageID == "" && m.Seq > 0 { + m.MessageID = strconv.FormatInt(m.Seq, 10) + } + return nil +} + +type decodedHistoryMessage struct { + messageID string + content string + messageType string +} + +func decodeHistoryMessageWire(raw json.RawMessage) *decodedHistoryMessage { + if len(raw) == 0 || string(raw) == "null" { + return nil + } + var nested historyMessageWire + if err := json.Unmarshal(raw, &nested); err != nil { + return nil + } + msg := &decodedHistoryMessage{ + messageID: firstNonEmpty( + jsonScalarString(nested.MessageID), + jsonScalarString(nested.MsgID), + jsonScalarString(nested.MID), + jsonScalarString(nested.ID), + ), + messageType: normalizeHistoryMessageType(nested.MessageType), + } + if msg.messageType == "" { + msg.messageType = extractPayloadMessageType(nested.Payload) + } + msg.content = firstNonEmpty( + strings.TrimSpace(nested.Content), + strings.TrimSpace(nested.Text), + strings.TrimSpace(nested.Body), + strings.TrimSpace(nested.MsgContent), + strings.TrimSpace(nested.MessageContent), + extractPayloadContent(nested.Payload, msg.messageID, strconv.FormatInt(nested.SenderID, 10), strconv.FormatInt(nested.ReceiverID, 10)), + extractPayloadContent(nested.ExtInfo, msg.messageID, strconv.FormatInt(nested.SenderID, 10), strconv.FormatInt(nested.ReceiverID, 10)), + extractAttachmentContent(nested.Attachment), + ) + if msg.messageID == "" && msg.content == "" && msg.messageType == "" { + return nil + } + return msg +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return strings.TrimSpace(value) + } + } + return "" +} + +func jsonScalarString(raw json.RawMessage) string { + if len(raw) == 0 || string(raw) == "null" { + return "" + } + var s string + if err := json.Unmarshal(raw, &s); err == nil { + return strings.TrimSpace(s) + } + var n json.Number + if err := json.Unmarshal(raw, &n); err == nil { + return n.String() + } + return "" +} + +func extractPayloadContent(raw json.RawMessage, excludes ...string) string { + if len(raw) == 0 || string(raw) == "null" { + return "" + } + var s string + if err := json.Unmarshal(raw, &s); err == nil { + return strings.TrimSpace(s) + } + var payload map[string]json.RawMessage + if err := json.Unmarshal(raw, &payload); err != nil { + return "" + } + for _, key := range []string{"text", "content", "body"} { + if value := jsonScalarString(payload[key]); value != "" { + return value + } + } + if value := jsonScalarString(payload["data"]); value != "" { + if content := extractProtobufPayloadText(value, excludes...); content != "" { + return content + } + } + return "" +} + +func extractPayloadMessageType(raw json.RawMessage) string { + if len(raw) == 0 || string(raw) == "null" { + return "" + } + var payload map[string]json.RawMessage + if err := json.Unmarshal(raw, &payload); err != nil { + return "" + } + return normalizeHistoryMessageType(payload["type"]) +} + +func extractAttachmentContent(raw json.RawMessage) string { + if len(raw) == 0 || string(raw) == "null" { + return "" + } + var attachment map[string]json.RawMessage + if err := json.Unmarshal(raw, &attachment); err != nil { + return "" + } + if fileName := jsonScalarString(attachment["fileName"]); fileName != "" { + return fileName + } + if url := jsonScalarString(attachment["url"]); url != "" { + return url + } + return "" +} + +func normalizeHistoryMessageType(raw json.RawMessage) string { + if len(raw) == 0 || string(raw) == "null" { + return "" + } + var s string + if err := json.Unmarshal(raw, &s); err == nil { + return strings.TrimSpace(s) + } + var n int + if err := json.Unmarshal(raw, &n); err == nil { + return historyMessageTypeName(n) + } + return strings.TrimSpace(string(raw)) +} + +func historyMessageTypeName(messageType int) string { + switch messageType { + case MessageTypeText: + return "TEXT" + case 1: + return "AUDIO" + case MessageTypeImage: + return "IMAGE" + case 3: + return "VIDEO" + case 4: + return "FILE" + case 5: + return "EMOJI" + case 100: + return "CUSTOMIZED" + default: + return strconv.Itoa(messageType) + } +} + +func extractProtobufPayloadText(encoded string, excludes ...string) string { + raw, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return "" + } + excludeSet := make(map[string]struct{}, len(excludes)) + for _, item := range excludes { + if item = strings.TrimSpace(item); item != "" { + excludeSet[item] = struct{}{} + } + } + candidates := make([]string, 0, 4) + collectProtoTextCandidates(raw, 0, excludeSet, &candidates) + if len(candidates) == 0 { + return "" + } + return candidates[len(candidates)-1] +} + +func collectProtoTextCandidates(raw []byte, depth int, excludes map[string]struct{}, candidates *[]string) { + if len(raw) == 0 || depth > 8 { + return + } + for len(raw) > 0 { + _, typ, n := protowire.ConsumeTag(raw) + if n < 0 { + return + } + raw = raw[n:] + switch typ { + case protowire.BytesType: + value, m := protowire.ConsumeBytes(raw) + if m < 0 { + return + } + if candidate := protoStringCandidate(value, excludes); candidate != "" { + *candidates = append(*candidates, candidate) + } + collectProtoTextCandidates(value, depth+1, excludes, candidates) + raw = raw[m:] + default: + m := protowire.ConsumeFieldValue(0, typ, raw) + if m < 0 { + return + } + raw = raw[m:] + } + } +} + +func protoStringCandidate(raw []byte, excludes map[string]struct{}) string { + if len(raw) == 0 || !utf8.Valid(raw) { + return "" + } + value := strings.TrimSpace(string(raw)) + if value == "" { + return "" + } + if _, ok := excludes[value]; ok { + return "" + } + if isLongNumber(value) { + return "" + } + for _, r := range value { + if unicode.IsControl(r) && r != '\n' && r != '\r' && r != '\t' { + return "" + } + } + return value +} + +func isLongNumber(value string) bool { + if len(value) < 9 { + return false + } + for _, r := range value { + if r < '0' || r > '9' { + return false + } + } + return true +} diff --git a/common/ip.go b/common/ip.go new file mode 100644 index 0000000..d3ff403 --- /dev/null +++ b/common/ip.go @@ -0,0 +1,78 @@ +package common + +import ( + "net" + + "91porn-server/common/httputil" + "91porn-server/common/log" +) + +var ( + defaultCity = "广州" + defaultProvince = "广东" + ipNewURL = "https://loc.ztgba.com/self/getLocationByIp" +) + +// 老接口 +type LocationResp struct { + RegionName string `json:"region_name"` + CityName string `json:"city_name"` + IP string `json:"ip"` +} + +func GetLocationByIP(ip string) (string, string) { + + return "-", "-" + + // var params = map[string]string{"ip": ip} + // loc := LocationResp{} + // c, cancel := context.WithTimeout(context.Background(), 3*time.Second) + // defer cancel() + // if _, err := httputil.DefaultClientPostJsonWithRespWithCtx(c, &loc, ipNewURL, nil, params); err != nil { + // log.Error("GetLocationByIP error", log.Any("ip", ip), log.E(err)) + // return defaultCity, defaultProvince + // } + // if (len(loc.RegionName) == 0 && len(loc.CityName) == 0) || loc.RegionName == "局域网" || loc.RegionName == "柬埔寨" { + // return defaultCity, defaultProvince + // } + // if len(loc.CityName) == 0 { + // return loc.RegionName, loc.RegionName + // } + // return loc.CityName, loc.RegionName +} + +type CountryResp struct { + Country string `json:"country_name"` + RegionName string `json:"region_name"` + IP string `json:"ip"` +} + +// InChina +// 香港返回false +func InChina(ip string) bool { + var params = map[string]string{"ip": ip} + resp := CountryResp{} + if _, err := httputil.DefaultClientPostJsonWithResp(&resp, ipNewURL, nil, params); err != nil { + log.Error("InChina error", log.Any("ip", ip), log.E(err)) + return true + } + if resp.RegionName == "香港" { + return false + } + return resp.Country == "中国" +} + +func GetLocalIP() string { + addrs, err := net.InterfaceAddrs() + if err != nil { + return "" + } + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil && ipnet.IP.IsGlobalUnicast() { + return ipnet.IP.String() + } + } + } + return "" +} diff --git a/common/kafka/kafka.go b/common/kafka/kafka.go new file mode 100644 index 0000000..15d4f14 --- /dev/null +++ b/common/kafka/kafka.go @@ -0,0 +1,74 @@ +package kafka + +import ( + "fmt" + "time" + + "91porn-server/common/log" + + "github.com/Shopify/sarama" +) + +const ClientID string = "pf_sp_kafka" + +// kafka 使用 +var gSyncProducer sarama.SyncProducer + +/**********************************************客户端 *********************/ +// 初始化kafka生产者 发送消息入口 +func InitKafkaProducter(addrs []string) error { + config := sarama.NewConfig() + config.Version = sarama.V2_0_0_0 + config.Producer.Return.Successes = true + config.Net.KeepAlive = 2 * time.Hour + cli, err := sarama.NewClient(addrs, config) + if err != nil { + log.Error("startUp Kafka Init Kafka error", log.E(err)) + return err + } + gSyncProducer, err = SyncProducter(cli) + if err != nil { + log.Error("startUp Kafka new SyncProducter error", log.E(err)) + return nil + } + return nil +} + +// SyncSendMessage 同步发送确保消息成功 +func SyncSendMessage(topic string, message []byte) { + if gSyncProducer != nil { + msg := sarama.ProducerMessage{ + Topic: topic, + Value: sarama.ByteEncoder(message), + } + if _, _, err := gSyncProducer.SendMessage(&msg); err != nil { + log.Error("gSyncProducer SendMessage Fail", log.Any("topic", topic), log.E(err)) + return + } + } +} + +// 创建同步生产者 用于对消息的顺序有严格要求的场景 性能相对较低 +func SyncProducter(client sarama.Client) (sarama.SyncProducer, error) { + producer, err := sarama.NewSyncProducerFromClient(client) + if err != nil { + log.Error("create syncProducer error", log.E(err)) + return nil, err + } + return producer, nil +} + +/**********************************************客户端 end*********************/ + +/**********************************************服务端 *********************/ +func InitKafkaConsumerGroup(addrs []string, groupName string) sarama.ConsumerGroup { + config := sarama.NewConfig() + config.Version = sarama.V2_0_0_0 + config.Consumer.Return.Errors = true + fmt.Println("addrs:", addrs) + group, err := sarama.NewConsumerGroup(addrs, groupName, config) + if err != nil { + panic(err) + } + return group +} diff --git a/common/laosiji/comics.go b/common/laosiji/comics.go new file mode 100644 index 0000000..db576b2 --- /dev/null +++ b/common/laosiji/comics.go @@ -0,0 +1,114 @@ +package laosiji + +import ( + "91porn-server/common/log" + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" +) + +func getComicsSearchUrl() string { + return fmt.Sprintf("%s/lsjapi/comics/search", APIUrl) +} + +func getComicsDetailUrl() string { + return fmt.Sprintf("%s/lsjapi/comics/detail", APIUrl) +} + +func ComicsSearch(ctx context.Context, req ComicsSearchListReq) (resp ComicsSearchListResp, err error) { + endpoint := getComicsSearchUrl() + req.Need_total_info = "y" + var data map[string]interface{} + data, err = StructToMapViaJSON(req) + if err != nil { + log.Error("GenerateUndress post fail", log.E(err)) + return resp, err + } + + err = comicsPost(ctx, endpoint, data, &resp) + if err != nil { + log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err)) + } + return +} + +func ComicsDetail(ctx context.Context, req ComicsDetailReq) (resp ComicsDetailResp, err error) { + endpoint := getComicsDetailUrl() + + var data map[string]interface{} + data, err = StructToMapViaJSON(req) + if err != nil { + log.Error("GenerateUndress post fail", log.E(err)) + return resp, err + } + + err = comicsPost(ctx, endpoint, data, &resp) + if err != nil { + log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err)) + } + return +} + +func comicsPost(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) (err error) { + jsonData, err := json.Marshal(req) + if err != nil { + log.Warn("GenerateAiUndress", log.E(err)) + return + } + encryptedData, err := encryptBase64(string(jsonData), APIKey) + if err != nil { + log.Warn("GenerateAiUndress encryptBase64", log.E(err)) + return + } + // 创建 POST 请求 + httpReq, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer([]byte(encryptedData))) + if err != nil { + return + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedData))) + httpReq.Header.Set("appid", Appid) + + // 发送请求 + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(httpReq) + if err != nil { + return + } + defer resp.Body.Close() + + // 读取响应体 + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return + } + + // 检查 HTTP 状态码 + if resp.StatusCode != http.StatusOK { + return + } + // 解析 JSON + var res Response + if err = json.Unmarshal(body, &res); err != nil { + return + } + if res.Status != "y" { + err = fmt.Errorf("errorCode:%v error:%v", res.ErrorCode, res.Error) + return + } + + // 进行解密 + dataStr, err := decryptBase64(res.Data, APIKey) + if err != nil { + return + } + err = json.Unmarshal([]byte(dataStr), response) + if err != nil { + return + } + return +} diff --git a/common/laosiji/comics_define.go b/common/laosiji/comics_define.go new file mode 100644 index 0000000..7b59efe --- /dev/null +++ b/common/laosiji/comics_define.go @@ -0,0 +1,74 @@ +package laosiji + +type Tags struct { + Id string `json:"id"` + Name string `json:"name"` +} + +type ContentInfo struct { + W string `json:"w"` + H string `json:"h"` + F string `json:"f"` +} + +type ComicsSearchInfo struct { + Id string `json:"id"` // id + Name string `json:"name"` // 名称 + Alias_name string `json:"alias_name"` // 别名 + Type string `json:"type"` // 类型 + Img string `json:"img"` // 封面 + Description string `json:"description"` // 描述 + Money string `json:"money"` // + Category string `json:"category"` // 分类 + Update_status string `json:"update_status"` // 更新状态 0更新中 1更新完成 + Update_date string `json:"update_date"` // 更新时间 + Chapter_count string `json:"chapter_count"` // 章节数量 + Is_adult string `json:"is_adult"` // 是否有声漫 + Tags []Tags `json:"tags"` // 标签 + Sub_title string `json:"sub_title"` // 子标题 + Chapter []ChapterInfo `json:"chapter"` // 漫画章节 + IsAdd bool `json:"isAdd"` +} + +type ComicsSearchListReq struct { + Page string `json:"page"` // + Page_size string `json:"page_size"` // + Cat_id string `json:"cat_id"` // + Start_time string `json:"start_time"` // 更新开始时间 + End_time string `json:"end_time"` // 更新结束时间 + Is_end string `json:"is_end"` // 是否完结 y | n + Source_site string `json:"source_site"` // 源站 如 www.toptoon.net + Source_url string `json:"source_url"` // 源链接 如 https://www.toptoon.net/comic/epList/81181 + Need_total_info string `json:"need_total_info"` // 分页信息 默认y + Keywords string `json:"keywords"` // 关键字 +} + +type ChapterInfo struct { + Id string `json:"id"` + Name string `json:"name"` + Img string `json:"img"` + Content []ContentInfo `json:"content"` +} +type ComicsSearchListResp struct { + Data []ComicsSearchInfo `json:"data"` // + Total string `json:"total"` // + Current_page string `json:"current_page"` // + Page_size string `json:"page_size"` // + Last_page string `json:"last_page"` // +} + +type ComicsDetailReq struct { + Id string `json:"id"` // id +} + +type ComicsDetailResp struct { + ComicsSearchInfo +} + +type ComicsSyncReq struct { + Ids []string `json:"ids"` +} + +type ComicsSyncResp struct { + List []ComicsSearchInfo `json:"list"` +} diff --git a/common/laosiji/laosiji.go b/common/laosiji/laosiji.go new file mode 100644 index 0000000..4693b2e --- /dev/null +++ b/common/laosiji/laosiji.go @@ -0,0 +1,262 @@ +package laosiji + +import ( + "91porn-server/common/log" + "91porn-server/web/webg" + "bytes" + + "context" + "crypto/aes" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" +) + +var ( + Name string + Appid string + APIKey string + APIUrl string + IMAGEYUAN string + NoticeURL string +) + +// Config 是老司机接口配置。所有凭证必须由各服务自己的配置文件注入。 +type Config struct { + AppID string + APIKey string + APIUrl string + ImageYuan string + NoticeURL string +} + +func Init(lsjCfg webg.GlobalConfig) { + InitConfig(Config{ + AppID: lsjCfg.LSJ.AppID, + APIKey: lsjCfg.LSJ.APIKey, + APIUrl: lsjCfg.LSJ.APIUrl, + ImageYuan: lsjCfg.LSJ.ImageYuan, + }) +} + +// InitConfig 供不依赖 web 配置结构的服务(例如 skd)初始化老司机客户端。 +func InitConfig(cfg Config) { + Name = "老司机" + Appid = strings.TrimSpace(cfg.AppID) + APIKey = strings.TrimSpace(cfg.APIKey) + APIUrl = strings.TrimRight(strings.TrimSpace(cfg.APIUrl), "/") + IMAGEYUAN = strings.TrimRight(strings.TrimSpace(cfg.ImageYuan), "/") + NoticeURL = strings.TrimSpace(cfg.NoticeURL) +} + +// Configured 判断调用老司机接口所需的配置是否完整。 +func Configured() bool { + return Appid != "" && APIKey != "" && APIUrl != "" +} + +func ensureConfigured() error { + if Configured() { + return nil + } + missing := make([]string, 0, 3) + if Appid == "" { + missing = append(missing, "appId") + } + if APIKey == "" { + missing = append(missing, "apiKey") + } + if APIUrl == "" { + missing = append(missing, "apiUrl") + } + return fmt.Errorf("laosiji configuration missing: %s", strings.Join(missing, ",")) +} + +type Response struct { + Status string `json:"status"` + Data string `json:"data"` // 如果成功,返回的这个数据是加密的 + Time string `json:"time"` + Error string `json:"error"` + ErrorCode int `json:"errorCode"` +} + +func QueryUndress(ctx context.Context, taskID string) (resp QueryUndressResponse, err error) { + endpoint := getQueryUrl() + err = post(ctx, endpoint, map[string]interface{}{ + "task_id": taskID, + }, &resp) + if err != nil { + log.Error("GenerateUndress post fail", log.Any("taskID", taskID), log.E(err)) + } + return +} + +func GenerateUndress(ctx context.Context, req map[string]interface{}) (resp GenerateUndressResponse, err error) { + endpoint := getAiUndressGenerateUrl() + err = post(ctx, endpoint, req, &resp) + if err != nil { + log.Error("GenerateUndress post fail", log.Any("req", req), log.E(err)) + } + return +} + +func QueryTextToImage(ctx context.Context, taskID string) (resp QueryTextToImageResponse, err error) { + endpoint := getQueryUrl() + err = post(ctx, endpoint, map[string]interface{}{ + "task_id": taskID, + }, &resp) + if err != nil { + log.Error("QueryTextToImage post fail", log.Any("taskID", taskID), log.E(err)) + } + return +} + +func GenerateTextToImage(ctx context.Context, req map[string]interface{}) (resp GenerateTextToImageResponse, err error) { + endpoint := getTextToImageGenerateUrl() + err = post(ctx, endpoint, req, &resp) + if err != nil { + log.Error("GenerateTextToImage post fail", log.Any("req", req), log.E(err)) + } + return +} + +func getQueryUrl() string { + return fmt.Sprintf("%s/lsjapi/ai/detail", APIUrl) +} + +func getTextToImageGenerateUrl() string { + return fmt.Sprintf("%s/lsjapi/ai/generate", APIUrl) +} + +func getAiUndressGenerateUrl() string { + return fmt.Sprintf("%s/lsjapi/ai/undress", APIUrl) +} + +func post(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) (err error) { + if err = ensureConfigured(); err != nil { + return err + } + jsonData, err := json.Marshal(req) + if err != nil { + log.Warn("GenerateAiUndress", log.E(err)) + return + } + encryptedData, err := encryptBase64(string(jsonData), APIKey) + if err != nil { + log.Warn("GenerateAiUndress encryptBase64", log.E(err)) + return + } + // 创建 POST 请求 + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer([]byte(encryptedData))) + if err != nil { + return + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedData))) + httpReq.Header.Set("appid", Appid) + + // 发送请求 + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(httpReq) + if err != nil { + return + } + defer resp.Body.Close() + + // 读取响应体 + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return + } + + // 检查 HTTP 状态码 + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + return fmt.Errorf("laosiji api http status:%d body:%s", resp.StatusCode, truncateToolBody(body, APIKey, Appid)) + } + + // 解析 JSON + var res Response + if err = json.Unmarshal(body, &res); err != nil { + return + } + if res.Status != "y" { + err = fmt.Errorf("errorCode:%v error:%s", res.ErrorCode, truncateToolText(res.Error, APIKey, Appid)) + return + } + + // 进行解密 + dataStr, err := decryptBase64(res.Data, APIKey) + if err != nil { + return + } + err = json.Unmarshal([]byte(dataStr), response) + if err != nil { + return + } + return +} + +// AES-128-ECB encrypt +func encryptBase64(input, key string) (string, error) { + if len(key) > 16 { + key = key[:16] + } + plainText := []byte(input) + keyBytes := []byte(key) + block, err := aes.NewCipher(keyBytes) + if err != nil { + return "", err + } + + blockSize := block.BlockSize() + plainText = pkcs7Padding(plainText, blockSize) + + encrypted := make([]byte, len(plainText)) + for bs, be := 0, blockSize; bs < len(plainText); bs, be = bs+blockSize, be+blockSize { + block.Encrypt(encrypted[bs:be], plainText[bs:be]) + } + + return base64.StdEncoding.EncodeToString(encrypted), nil +} + +// AES-128-ECB decrypt +func decryptBase64(cipherText, key string) (string, error) { + if len(key) > 16 { + key = key[:16] + } + data, err := base64.StdEncoding.DecodeString(cipherText) + if err != nil { + return "", err + } + + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return "", err + } + + blockSize := block.BlockSize() + decrypted := make([]byte, len(data)) + for bs, be := 0, blockSize; bs < len(data); bs, be = bs+blockSize, be+blockSize { + block.Decrypt(decrypted[bs:be], data[bs:be]) + } + + decrypted = pkcs7UnPadding(decrypted) + return string(decrypted), nil +} + +// PKCS7Padding pads plaintext for AES ECB +func pkcs7Padding(src []byte, blockSize int) []byte { + padding := blockSize - len(src)%blockSize + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(src, padText...) +} + +// PKCS7UnPadding removes padding +func pkcs7UnPadding(src []byte) []byte { + length := len(src) + unpadding := int(src[length-1]) + return src[:(length - unpadding)] +} diff --git a/common/laosiji/movie.go b/common/laosiji/movie.go new file mode 100644 index 0000000..48e9418 --- /dev/null +++ b/common/laosiji/movie.go @@ -0,0 +1,142 @@ +package laosiji + +import ( + "91porn-server/common/log" + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" +) + +func getMovieSearchUrl() string { + return fmt.Sprintf("%s/lsjapi/movie/search", APIUrl) +} + +func getMovieDetailUrl() string { + return fmt.Sprintf("%s/lsjapi/movie/detail", APIUrl) +} + +func getMoviedDetailByMidUrl() string { + return fmt.Sprintf("%s/lsjapi/movie/detailByMid", APIUrl) +} + +// StructToMapViaJSON 通过 JSON 转换结构体到 map +func StructToMapViaJSON(obj interface{}) (map[string]interface{}, error) { + // 将结构体转换为 JSON + jsonData, err := json.Marshal(obj) + if err != nil { + return nil, err + } + + // 将 JSON 解析为 map + var result map[string]interface{} + err = json.Unmarshal(jsonData, &result) + if err != nil { + return nil, err + } + + return result, nil +} + +// MovieSearch 视频查询 +func MovieSearch(ctx context.Context, req MovieSearchReq) (resp MovieSearchResp, err error) { + endpoint := getMovieSearchUrl() + req.Need_total_info = "y" + + var data map[string]interface{} + data, err = StructToMapViaJSON(req) + if err != nil { + log.Error("GenerateUndress post fail", log.E(err)) + return resp, err + } + + err = moviePost(ctx, endpoint, data, &resp) + if err != nil { + log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err)) + } + return +} + +// MovieDetail 获取视频列表 +func MovieDetail(ctx context.Context, req MovieDetailReq) (resp MovieDetailResp, err error) { + endpoint := getMovieDetailUrl() + + var data map[string]interface{} + data, err = StructToMapViaJSON(req) + if err != nil { + log.Error("GenerateUndress post fail", log.E(err)) + return resp, err + } + + err = moviePost(ctx, endpoint, data, &resp) + if err != nil { + log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err)) + } + return +} + +func moviePost(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) (err error) { + if err = ensureConfigured(); err != nil { + return err + } + jsonData, err := json.Marshal(req) + if err != nil { + log.Warn("GenerateAiUndress", log.E(err)) + return + } + encryptedData, err := encryptBase64(string(jsonData), APIKey) + if err != nil { + log.Warn("GenerateAiUndress encryptBase64", log.E(err)) + return + } + // 创建 POST 请求 + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer([]byte(encryptedData))) + if err != nil { + return + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedData))) + httpReq.Header.Set("appid", Appid) + + // 发送请求 + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(httpReq) + if err != nil { + return + } + defer resp.Body.Close() + + // 读取响应体 + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return + } + + // 检查 HTTP 状态码 + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + return fmt.Errorf("laosiji movie api http status:%d body:%s", resp.StatusCode, truncateToolBody(body, APIKey, Appid)) + } + // 解析 JSON + var res Response + if err = json.Unmarshal(body, &res); err != nil { + return + } + if res.Status != "y" { + err = fmt.Errorf("errorCode:%v error:%s", res.ErrorCode, truncateToolText(res.Error, APIKey, Appid)) + return + } + + // 进行解密 + dataStr, err := decryptBase64(res.Data, APIKey) + if err != nil { + return + } + err = json.Unmarshal([]byte(dataStr), response) + if err != nil { + return + } + return +} diff --git a/common/laosiji/movie_define.go b/common/laosiji/movie_define.go new file mode 100644 index 0000000..43d3e97 --- /dev/null +++ b/common/laosiji/movie_define.go @@ -0,0 +1,156 @@ +package laosiji + +type TagInfo struct { + Id string `json:"id"` // 视频编号 + Name string `json:"name"` // 视频名字 +} + +type LinkInfo struct { + Id string `json:"id"` // 视频编号 + Name string `json:"name"` // 视频名字 + Preview_m3u8_url string `json:"preview_m3u8_url"` // 预览地址(部分视频无预览地址) + M3u8_url string `json:"m3u8_url"` // 播放地址 + Hevc_m3u8_url string `json:"hevc_m3u8_url"` // H.265 播放地址 +} + +type MovieInfo struct { + Id string `json:"id"` // 视频编号 + Name string `json:"name"` // 视频名字 + Img_x string `json:"img_x"` // 横图封面 + Img_y string `json:"img_y"` // 竖图封面 + Img_type string `json:"img_type"` // 封面类型 long 横图 short竖图 + Cat_id string `json:"cat_id"` // 分类编号 + Cat_name string `json:"cat_name"` // 分类名字 + Status string `json:"status"` // 上架状态 + Status_text string `json:"status_text"` // + Show_at string `json:"show_at"` // 上架时间 + Tags []TagInfo `json:"tags"` // 标签 + Update_status string `json:"update_status"` // 更新状态0 更新重 1已完结 + Description string `json:"description"` // 视频描述 + Language string `json:"language"` // 视频语言 + Director string `json:"director"` // 视频导演 + Issue_date string `json:"issue_date"` // 视频上架时间 + Actor string `json:"actor"` // 演员 + Up_user string `json:"up_user"` // + IsAdd bool `json:"isAdd"` +} + +type NovelSearchInfo struct { + Id string `json:"id"` // id + Name string `json:"name"` // 名称 + Alias_name string `json:"alias_name"` // 别名 + Author string `json:"author"` // 作者 + Type string `json:"type"` // 类型 + Img string `json:"img"` // 封面 + Description string `json:"description"` // 描述 + Money string `json:"money"` // + Sub_title string `json:"sub_title"` // 子标题 + Category string `json:"category"` // 分类 + Category_name string `json:"category_name"` // 分类名称 + Update_status string `json:"update_status"` // 更新状态 0更新中 1更新完成 + Update_date string `json:"update_date"` // 更新时间 + Chapter_count string `json:"chapter_count"` // 章节数量 + Is_adult string `json:"is_adult"` // 是否是有声 + Tags []Tags `json:"tags"` // 标签 + Last_update string `json:"last_update"` // 最后更新时间 + Created_at string `json:"created_at"` // 创建时间 + Updated_at string `json:"updated_at"` // 更新时间 + Chapter []NovelChapterInfo `json:"chapter"` // 小说章节 + IsAdd bool `json:"isAdd"` +} + +type NovelChapterInfo struct { + Id string `json:"id"` // id + Name string `json:"name"` // 标题 + Img string `json:"img"` // 图片 + Content string `json:"content"` // 内容 + Is_audio string `json:"is_audio"` // 是否有声 +} + +type MovieDetailInfo struct { + Id string `json:"id"` // 视频编号 + Name string `json:"name"` // 视频名字 + Img_x string `json:"img_x"` // 横图封面 + Img_y string `json:"img_y"` // 竖图封面 + Img_type string `json:"img_type"` // 封面类型 long 横图 short竖图 + Cat_id string `json:"cat_id"` // 分类编号 + Cat_name string `json:"cat_name"` // 分类名字 + Status string `json:"status"` // 上架状态 + Status_text string `json:"status_text"` // + Show_at string `json:"show_at"` // 上架时间 + Tags []TagInfo `json:"tags"` // 标签 + Update_status string `json:"update_status"` // 更新状态0 更新重 1已完结 + Description string `json:"description"` // 视频描述 + Language string `json:"language"` // 视频语言 + Director string `json:"director"` // 视频导演 + Issue_date string `json:"issue_date"` // 视频上架时间 + Duration string `json:"duration"` // 视频时长 + Actor string `json:"actor"` // 演员 + + Is_more_link string `json:"is_more_link"` // 是多集还是单集 + Preview_images []string `json:"preview_images"` // 预览图片 + Links []LinkInfo `json:"links"` // 链接 + Series string `json:"series"` // 系列 主要是av + Source_tags string `json:"source_tags"` // 采集网站的标签 + // Source_actor string `json:"source_actor"` // 采集网站的演员 主要是av使用 +} + +/* +| 分类编号(cat_id) | 名称 | 分区(position) | +| 13 | 成人短视频 | guochan | +| 12 | VR | av | +| 11 | 电影解说 | movie | +| 10 | 音乐 | movie | +| 9 | 短剧 | movie | +| 8 | 纪录片 | movie | +| 7 | 动漫 | movie | +| 6 | 电影 | movie | +| 5 | 连续剧 | movie | +| 4 | 综艺 | movie | +| 3 | GC | guochan | +| 2 | DM | guochan | +| 1 | AV | av | +| | | +| position 说明 guochan 是国产成人视频 av 主要是日本和欧美成人视频 movie 是正规影视资源 bl 男同 douyin 短视频 cartoon 动漫 dark 暗网资源 所有资源需要用户具备权限才能获取 全部传递all +*/ +type MovieSearchReq struct { + Position string `json:"position"` // position 说明 guochan 是国产成人视频 av 主要是日本和欧美成人视频 movie 是正规影视资源 bl 男同 douyin 短视频 cartoon 动漫 dark 暗网资源 + Keywords string `json:"keywords"` // 关键字 + Ids string `json:"ids"` // 视频ID + Cat_id string `json:"cat_id"` // 分类ID + Update_status string `json:"update_status"` // 1表示已经完结 0表示未完结 + Hevc_status int `json:"hevc_status"` // 5表示只查询已有 H.265 资源的视频 + Mid string `json:"mid"` // 一般无需使用 多个用,分开 + Page string `json:"page"` // 分页 + Page_size string `json:"page_size"` // 每页数据 建议不要超过500 + Start_time string `json:"start_time"` // 更新日期开始日期 格式 2022-01-01 12:00:00 + End_time string `json:"end_time"` // 更新日期结束日期 格式 2022-01-01 12:00:00 + Home_id string `json:"home_id"` // up主id + Need_total_info string `json:"need_total_info"` // 分页信息 默认y +} + +// MovieSearchResp 视频查询返回接口 +type MovieSearchResp struct { + // Status string `json:"status"` // 状态 0待处理 -1处理失败 1处理中 2处理成功 + // Time string `json:"time"` // + Data []MovieInfo `json:"data"` // + Total string `json:"total"` // + Current_page string `json:"current_page"` // + Page_size string `json:"page_size"` // + Last_page string `json:"last_page"` // +} + +// MovieDetailReq 视频详情 +type MovieDetailReq struct { + Id string `json:"id"` // id +} + +// MovieSearchResp 视频详情返回接口 +type MovieDetailResp struct { + MovieDetailInfo +} + +type MovieAddListReq struct { + Ids []string `json:"ids"` + Position string `json:"position"` +} diff --git a/common/laosiji/novel.go b/common/laosiji/novel.go new file mode 100644 index 0000000..e605b95 --- /dev/null +++ b/common/laosiji/novel.go @@ -0,0 +1,115 @@ +package laosiji + +import ( + "91porn-server/common/log" + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" +) + +func getNovelSearchUrl() string { + return fmt.Sprintf("%s/lsjapi/novel/search", APIUrl) +} + +func getNovelDetailUrl() string { + return fmt.Sprintf("%s/lsjapi/novel/detail", APIUrl) +} + +func NovelSearch(ctx context.Context, req NovelSearchListReq) (resp NovelSearchListResp, err error) { + endpoint := getNovelSearchUrl() + req.Need_total_info = "y" + + var data map[string]interface{} + data, err = StructToMapViaJSON(req) + if err != nil { + log.Error("GenerateUndress post fail", log.E(err)) + return resp, err + } + + err = novelPost(ctx, endpoint, data, &resp) + if err != nil { + log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err)) + } + return +} + +func NovelDetail(ctx context.Context, req NovelDetailReq) (resp NovelDetailResp, err error) { + endpoint := getNovelDetailUrl() + + var data map[string]interface{} + data, err = StructToMapViaJSON(req) + if err != nil { + log.Error("GenerateUndress post fail", log.E(err)) + return resp, err + } + + err = novelPost(ctx, endpoint, data, &resp) + if err != nil { + log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err)) + } + return +} + +func novelPost(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) (err error) { + jsonData, err := json.Marshal(req) + if err != nil { + log.Warn("GenerateAiUndress", log.E(err)) + return + } + encryptedData, err := encryptBase64(string(jsonData), APIKey) + if err != nil { + log.Warn("GenerateAiUndress encryptBase64", log.E(err)) + return + } + // 创建 POST 请求 + httpReq, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer([]byte(encryptedData))) + if err != nil { + return + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedData))) + httpReq.Header.Set("appid", Appid) + + // 发送请求 + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(httpReq) + if err != nil { + return + } + defer resp.Body.Close() + + // 读取响应体 + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return + } + + // 检查 HTTP 状态码 + if resp.StatusCode != http.StatusOK { + return + } + // 解析 JSON + var res Response + if err = json.Unmarshal(body, &res); err != nil { + return + } + if res.Status != "y" { + err = fmt.Errorf("errorCode:%v error:%v", res.ErrorCode, res.Error) + return + } + + // 进行解密 + dataStr, err := decryptBase64(res.Data, APIKey) + if err != nil { + return + } + err = json.Unmarshal([]byte(dataStr), response) + if err != nil { + return + } + return +} diff --git a/common/laosiji/novel_define.go b/common/laosiji/novel_define.go new file mode 100644 index 0000000..d1abd44 --- /dev/null +++ b/common/laosiji/novel_define.go @@ -0,0 +1,32 @@ +package laosiji + +type NovelSearchListReq struct { + Page string `json:"page"` // + Page_size string `json:"page_size"` // + Cat_id string `json:"cat_id"` // 分类 audio 18R normal + Start_time string `json:"start_time"` // 更新开始时间 + End_time string `json:"end_time"` // 更新结束时间 + Is_end string `json:"is_end"` // 是否完结 y | n + Need_total_info string `json:"need_total_info"` // 分页信息 默认y + Keywords string `json:"keywords"` // 关键字 +} + +type NovelSearchListResp struct { + Data []NovelSearchInfo `json:"data"` // + Total string `json:"total"` // 总数 + Current_page string `json:"current_page"` // 当前页 + Page_size string `json:"page_size"` // 当前页数 + Last_page string `json:"last_page"` // +} + +type NovelDetailReq struct { + Id string `json:"id"` // id +} + +type NovelDetailResp struct { + NovelSearchInfo +} + +type NovelAddListReq struct { + Ids []string `json:"ids"` +} diff --git a/common/laosiji/post.go b/common/laosiji/post.go new file mode 100644 index 0000000..6efc6f0 --- /dev/null +++ b/common/laosiji/post.go @@ -0,0 +1,115 @@ +package laosiji + +import ( + "91porn-server/common/log" + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" +) + +func getPostSearchUrl() string { + return fmt.Sprintf("%s/lsjapi/post/search", APIUrl) +} + +func getPostDetailUrl() string { + return fmt.Sprintf("%s/lsjapi/post/detail", APIUrl) +} + +func PostSearch(ctx context.Context, req PostSearchListReq) (resp PostSearchListResp, err error) { + endpoint := getPostSearchUrl() + req.Need_total_info = "y" + + var data map[string]interface{} + data, err = StructToMapViaJSON(req) + if err != nil { + log.Error("GenerateUndress post fail", log.E(err)) + return resp, err + } + + err = postPost(ctx, endpoint, data, &resp) + if err != nil { + log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err)) + } + return +} + +func PostDetail(ctx context.Context, req PostDetailReq) (resp PostDetailResp, err error) { + endpoint := getPostDetailUrl() + + var data map[string]interface{} + data, err = StructToMapViaJSON(req) + if err != nil { + log.Error("GenerateUndress post fail", log.E(err)) + return resp, err + } + + err = postPost(ctx, endpoint, data, &resp) + if err != nil { + log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err)) + } + return +} + +func postPost(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) (err error) { + jsonData, err := json.Marshal(req) + if err != nil { + log.Warn("GenerateAiUndress", log.E(err)) + return + } + encryptedData, err := encryptBase64(string(jsonData), APIKey) + if err != nil { + log.Warn("GenerateAiUndress encryptBase64", log.E(err)) + return + } + // 创建 POST 请求 + httpReq, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer([]byte(encryptedData))) + if err != nil { + return + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedData))) + httpReq.Header.Set("appid", Appid) + + // 发送请求 + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(httpReq) + if err != nil { + return + } + defer resp.Body.Close() + + // 读取响应体 + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return + } + + // 检查 HTTP 状态码 + if resp.StatusCode != http.StatusOK { + return + } + // 解析 JSON + var res Response + if err = json.Unmarshal(body, &res); err != nil { + return + } + if res.Status != "y" { + err = fmt.Errorf("errorCode:%v error:%v", res.ErrorCode, res.Error) + return + } + + // 进行解密 + dataStr, err := decryptBase64(res.Data, APIKey) + if err != nil { + return + } + err = json.Unmarshal([]byte(dataStr), response) + if err != nil { + return + } + return +} diff --git a/common/laosiji/post_define.go b/common/laosiji/post_define.go new file mode 100644 index 0000000..a21f492 --- /dev/null +++ b/common/laosiji/post_define.go @@ -0,0 +1,69 @@ +package laosiji + +type Categories struct { + Id string `json:"id"` + Name string `json:"name"` +} +type PostFiles struct { + Image string `json:"image"` + Type string `json:"type"` + Ico string `json:"ico"` + Tips string `json:"tips"` + Video_link string `json:"video_link"` +} + +type UpUserInfo struct { + Id string `json:"id"` + Nickname string `json:"nickname"` + Img string `json:"img"` + Sign string `json:"sign"` +} + +type PostSearchListReq struct { + Home_id string `json:"home_id"` + Page string `json:"page"` + Page_size string `json:"page_size"` + Pay_type string `json:"pay_type"` + Start_time int `json:"start_time"` + End_time int `json:"end_time"` + Need_total_info string `json:"need_total_info"` + Keywords string `json:"keywords"` // 关键字 +} + +type PostSearchInfo struct { + Id string `json:"id"` // id + Title string `json:"title"` // 标题 + Time string `json:"time"` // 更新时间 + Money string `json:"money"` // + User_id string `json:"user_id"` // + Content string `json:"content"` // 文本内容 + Img string `json:"img"` // 封面 + Hide_files string `json:"hide_files"` // + Position string `json:"position"` // 类型 + Categories []Categories `json:"categories"` // 分类 + Img_count string `json:"img_count"` // 图片数量 + Rich_content []string `json:"up_user"` // 富文本 + Files []PostFiles `json:"files"` // 内容节点 + Up_user UpUserInfo `json:"up_user"` // 发布者 + IsAdd bool `json:"isAdd"` +} + +type PostSearchListResp struct { + Data []PostSearchInfo `json:"data"` // + Total string `json:"total"` // + Current_page string `json:"current_page"` // + Page_size string `json:"page_size"` // + Last_page string `json:"last_page"` // +} + +type PostDetailReq struct { + Id string `json:"id"` // id +} + +type PostDetailResp struct { + PostSearchInfo +} + +type PostAddListReq struct { + Ids []string `json:"ids"` +} diff --git a/common/laosiji/textToImage.go b/common/laosiji/textToImage.go new file mode 100644 index 0000000..f30afbe --- /dev/null +++ b/common/laosiji/textToImage.go @@ -0,0 +1,14 @@ +package laosiji + +type GenerateTextToImageResponse struct { + TaskID string `json:"task_id"` // 任务 ID +} + +type QueryTextToImageResponse struct { + TaskID string `json:"task_id"` // 任务 ID + Bid string `json:"bid"` + Fee string `json:"fee"` + Type string `json:"type"` + Status string `json:"status"` // 状态 0待处理 -1处理失败 1处理中 2处理成功 + OutData string `json:"out_data"` // 输出数据 不同的ai 处理的数据不一样 查看文档的描述 +} diff --git a/common/laosiji/tools.go b/common/laosiji/tools.go new file mode 100644 index 0000000..026c21d --- /dev/null +++ b/common/laosiji/tools.go @@ -0,0 +1,487 @@ +package laosiji + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "path" + "strings" + "time" + + "91porn-server/common/hevcpull" + "91porn-server/common/log" +) + +const ( + videoTranscodingVersion = "2.0" + transcodeNoNeedStatus = 4 + transcodeSuccessStatus = 5 + transcodeFailedStatus = -1 + + sourceM3u8CheckMaxBytes = 8 << 20 +) + +var ( + errTranscodeTaskNotFound = errors.New("laosiji transcode task not found") + // ErrTranscodeQueueFull 表示云端 H.265 待处理队列已经达到限制。 + ErrTranscodeQueueFull = errors.New("laosiji transcode queue full") + sourceM3u8HTTPClient = &http.Client{Timeout: 15 * time.Second} + uploadHTTPClient = &http.Client{Timeout: 30 * time.Second} +) + +// SystemDomainsResp 是老司机 system/domains 接口返回的资源域名和上传配置。 +type SystemDomainsResp struct { + ImgFreeCDN string `json:"img_free_cdn"` + MovieFreeCDN string `json:"movie_free_cdn"` + MovieSourceCDN string `json:"movie_source_cdn"` + UploadURL string `json:"upload_url"` + MediaDir string `json:"media_dir"` + UploadKey string `json:"upload_key"` + UserID string `json:"user_id"` +} + +type transcodeAPIResp struct { + Status string `json:"status"` + Data json.RawMessage `json:"data"` + Time string `json:"time"` + Error string `json:"error"` + ErrorCode int `json:"errorCode"` +} + +type transcodeTask struct { + ID string `json:"id"` + FileID string `json:"file_id"` + FileURL string `json:"file_url"` + Status int `json:"status"` + Duration int `json:"duration"` + Height int `json:"height"` + Width int `json:"width"` + TranscodeError string `json:"transcode_error"` + TranscodeFile string `json:"transcode_file"` +} + +// TranscodeQueueInfo 是云端转码队列概览。 +type TranscodeQueueInfo struct { + Waiting int `json:"waiting"` + Done int `json:"done"` + Error int `json:"error"` +} + +// TranscodeResult 是本地异步任务使用的统一转码结果。 +type TranscodeResult struct { + Status int + Done bool + NoNeed bool + Failed bool + HevcURL string + FileURL string + FileID string + ErrorMsg string +} + +func getSystemDomainsURL() string { + return strings.TrimRight(APIUrl, "/") + "/lsjapi/system/domains" +} + +// SystemDomains 获取老司机临时上传、转码配置。 +func SystemDomains(ctx context.Context) (resp SystemDomainsResp, err error) { + if err = ensureConfigured(); err != nil { + return resp, err + } + if err = moviePost(ctx, getSystemDomainsURL(), map[string]interface{}{}, &resp); err != nil { + return resp, err + } + missing := make([]string, 0, 3) + if strings.TrimSpace(resp.UploadURL) == "" { + missing = append(missing, "upload_url") + } + if strings.TrimSpace(resp.UploadKey) == "" { + missing = append(missing, "upload_key") + } + if strings.TrimSpace(resp.MovieSourceCDN) == "" { + missing = append(missing, "movie_source_cdn") + } + if len(missing) > 0 { + return resp, fmt.Errorf("laosiji system/domains missing: %s", strings.Join(missing, ",")) + } + return resp, nil +} + +// SubmitH264ToH265ByFileURL 使用完整 file_url 异步提交转码任务。 +func SubmitH264ToH265ByFileURL(ctx context.Context, fileURL string, domains SystemDomainsResp, maxRunning int) (TranscodeResult, error) { + fileURL = strings.TrimSpace(fileURL) + if fileURL == "" { + return TranscodeResult{}, errors.New("empty file url") + } + return submitByTask(ctx, md5Hex(fileURL), fileURL, domains, maxRunning) +} + +// SubmitH264ToH265Task decouples the stable cloud task ID from the temporary +// signed fetch URL. This keeps polling stable across App domain or signing-key +// changes while a task is pending. +func SubmitH264ToH265Task(ctx context.Context, fileID, fileURL string, domains SystemDomainsResp, maxRunning int) (TranscodeResult, error) { + fileID = strings.TrimSpace(fileID) + fileURL = strings.TrimSpace(fileURL) + if fileID == "" { + return TranscodeResult{}, errors.New("empty file id") + } + if fileURL == "" { + return TranscodeResult{}, errors.New("empty file url") + } + return submitByTask(ctx, fileID, fileURL, domains, maxRunning) +} + +func submitByTask(ctx context.Context, fileID, fileURL string, domains SystemDomainsResp, maxRunning int) (TranscodeResult, error) { + task, err := queryVideoTranscoding(ctx, domains, fileID) + if err == nil { + return transcodeTaskToResult(domains, task, fileURL, fileID), nil + } + if !errors.Is(err, errTranscodeTaskNotFound) { + return TranscodeResult{}, err + } + if err = checkSourceM3u8(ctx, fileURL); err != nil { + return TranscodeResult{}, err + } + task, err = createVideoTranscoding(ctx, domains, fileID, fileURL, maxRunning) + if err != nil { + return TranscodeResult{}, err + } + return transcodeTaskToResult(domains, task, fileURL, fileID), nil +} + +func checkSourceM3u8(ctx context.Context, fileURL string) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileURL, nil) + if err != nil { + return fmt.Errorf("build source m3u8 request: %s", hevcpull.RedactText(err.Error())) + } + resp, err := sourceM3u8HTTPClient.Do(req) + if err != nil { + return fmt.Errorf("source m3u8 request failed: %s", hevcpull.RedactText(err.Error())) + } + defer resp.Body.Close() + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + return fmt.Errorf("source m3u8 http status:%d url:%s", resp.StatusCode, hevcpull.RedactURL(fileURL)) + } + body, err := io.ReadAll(io.LimitReader(resp.Body, sourceM3u8CheckMaxBytes)) + if err != nil { + return err + } + if !looksLikeM3u8(body) { + return fmt.Errorf("source m3u8 invalid content url:%s", hevcpull.RedactURL(fileURL)) + } + return nil +} + +func looksLikeM3u8(body []byte) bool { + body = bytes.TrimPrefix(body, []byte{0xEF, 0xBB, 0xBF}) + body = bytes.TrimLeft(body, " \t\r\n") + return bytes.HasPrefix(body, []byte("#EXTM3U")) +} + +// QueryH264ToH265 查询老司机源视频对应的 H.265 云转码结果。 +func QueryH264ToH265(ctx context.Context, h264URL string) (TranscodeResult, bool, error) { + domains, err := SystemDomains(ctx) + if err != nil { + return TranscodeResult{}, false, err + } + return QueryH264ToH265WithDomains(ctx, h264URL, domains) +} + +// QueryH264ToH265WithDomains 使用已经获取的域名配置查询转码结果。 +func QueryH264ToH265WithDomains(ctx context.Context, h264URL string, domains SystemDomainsResp) (TranscodeResult, bool, error) { + h264URL = strings.TrimSpace(h264URL) + if h264URL == "" { + return TranscodeResult{}, false, errors.New("empty h264 m3u8 url") + } + return queryByFileURL(ctx, transcodeSourceURL(h264URL, domains.MovieSourceCDN), domains) +} + +// QueryH264ToH265ByFileURL 与 SubmitH264ToH265ByFileURL 使用相同的完整 file_url 查询结果。 +func QueryH264ToH265ByFileURL(ctx context.Context, fileURL string, domains SystemDomainsResp) (TranscodeResult, bool, error) { + fileURL = strings.TrimSpace(fileURL) + if fileURL == "" { + return TranscodeResult{}, false, errors.New("empty file url") + } + return queryByTask(ctx, md5Hex(fileURL), fileURL, domains) +} + +func queryByFileURL(ctx context.Context, fileURL string, domains SystemDomainsResp) (TranscodeResult, bool, error) { + return queryByTask(ctx, md5Hex(fileURL), fileURL, domains) +} + +// QueryH264ToH265Task queries by the stable task ID used during submission. +// fileURL is diagnostic metadata only and is not sent to the cloud query API. +func QueryH264ToH265Task(ctx context.Context, fileID, fileURL string, domains SystemDomainsResp) (TranscodeResult, bool, error) { + fileID = strings.TrimSpace(fileID) + fileURL = strings.TrimSpace(fileURL) + if fileID == "" { + return TranscodeResult{}, false, errors.New("empty file id") + } + return queryByTask(ctx, fileID, fileURL, domains) +} + +func queryByTask(ctx context.Context, fileID, fileURL string, domains SystemDomainsResp) (TranscodeResult, bool, error) { + task, err := queryVideoTranscoding(ctx, domains, fileID) + if err != nil { + if errors.Is(err, errTranscodeTaskNotFound) { + return TranscodeResult{}, false, nil + } + return TranscodeResult{}, false, err + } + return transcodeTaskToResult(domains, task, fileURL, fileID), true, nil +} + +func transcodeSourceURL(rawURL, movieSourceCDN string) string { + rawURL = strings.TrimSpace(rawURL) + movieSourceCDN = strings.TrimSpace(movieSourceCDN) + if rawURL == "" || movieSourceCDN == "" { + return rawURL + } + parsed, err := url.Parse(rawURL) + if err == nil && parsed.Scheme != "" { + name := path.Base(parsed.Path) + if name != "" && name != "." && strings.Contains(name, ".m3u8") { + return joinURLPath(movieSourceCDN, name) + } + return rawURL + } + rawURL = strings.TrimPrefix(rawURL, "/") + rawURL = strings.TrimPrefix(rawURL, "laosiji/") + if idx := strings.Index(rawURL, "m3m/"); idx >= 0 { + rawURL = rawURL[idx+len("m3m/"):] + } + return joinURLPath(movieSourceCDN, rawURL) +} + +// TranscodeSourceURL 把本地资源 path 或播放 URL 转换成云转码使用的源站 URL。 +func TranscodeSourceURL(rawURL, movieSourceCDN string) string { + return transcodeSourceURL(rawURL, movieSourceCDN) +} + +func createVideoTranscoding(ctx context.Context, domains SystemDomainsResp, fileID, fileURL string, maxRunning int) (transcodeTask, error) { + if q, err := getVideoTranscodingQueue(ctx, domains); err != nil { + log.Warn("getVideoTranscodingQueue failed", log.E(err)) + } else if maxRunning > 0 && q.Waiting >= maxRunning { + return transcodeTask{}, fmt.Errorf("%w waiting:%d limit:%d", ErrTranscodeQueueFull, q.Waiting, maxRunning) + } + params := map[string]string{ + "v": videoTranscodingVersion, + "key": domains.UploadKey, + "file_id": fileID, + "file_url": fileURL, + "ext_data": `{"project":"91porn","source":"laosiji","type":"full"}`, + } + if NoticeURL != "" { + params["notice_url"] = NoticeURL + } + endpoint := joinURLPath(domains.UploadURL, "upload/videoTranscoding") + return uploadAPIGet(ctx, endpoint, params) +} + +func getVideoTranscodingQueue(ctx context.Context, domains SystemDomainsResp) (TranscodeQueueInfo, error) { + endpoint := joinURLPath(domains.UploadURL, "upload/getVideoTranscodingQueueInfo") + data, err := uploadAPICall(ctx, endpoint, map[string]string{ + "v": videoTranscodingVersion, + "key": domains.UploadKey, + }) + if err != nil { + return TranscodeQueueInfo{}, err + } + var queue TranscodeQueueInfo + if len(data) > 0 { + if err = json.Unmarshal(data, &queue); err != nil { + return TranscodeQueueInfo{}, err + } + } + return queue, nil +} + +func queryVideoTranscoding(ctx context.Context, domains SystemDomainsResp, fileID string) (transcodeTask, error) { + endpoint := joinURLPath(domains.UploadURL, "upload/queryVideoTranscoding") + return uploadAPIGet(ctx, endpoint, map[string]string{ + "v": videoTranscodingVersion, + "key": domains.UploadKey, + "file_id": fileID, + }) +} + +func transcodeTaskToResult(domains SystemDomainsResp, task transcodeTask, fileURL, fileID string) TranscodeResult { + result := TranscodeResult{Status: task.Status, FileURL: fileURL, FileID: fileID} + switch { + case task.Status == transcodeNoNeedStatus: + result.Done = true + result.NoNeed = true + result.HevcURL = task.FileURL + case task.Status == transcodeSuccessStatus: + if transcodeFile := strings.TrimSpace(task.TranscodeFile); transcodeFile != "" { + result.Done = true + result.HevcURL = joinURLPath(domains.MovieSourceCDN, transcodeFile) + } else { + result.Failed = true + result.ErrorMsg = "laosiji transcode succeeded without transcode_file" + } + case task.Status <= transcodeFailedStatus: + result.Failed = true + result.ErrorMsg = truncateToolText(task.TranscodeError, domains.UploadKey) + if result.ErrorMsg == "" { + result.ErrorMsg = fmt.Sprintf("laosiji transcode failed status:%d", task.Status) + } + } + return result +} + +func uploadAPIGet(ctx context.Context, endpoint string, params map[string]string) (transcodeTask, error) { + data, err := uploadAPICall(ctx, endpoint, params) + if err != nil { + return transcodeTask{}, err + } + var task transcodeTask + if len(data) > 0 { + if err = json.Unmarshal(data, &task); err != nil { + return transcodeTask{}, err + } + } + return task, nil +} + +func uploadAPICall(ctx context.Context, endpoint string, params map[string]string) (json.RawMessage, error) { + parsed, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + query := parsed.Query() + for key, value := range params { + query.Set(key, value) + } + parsed.RawQuery = query.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil) + if err != nil { + return nil, err + } + resp, err := uploadHTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("upload api request failed: %s", truncateToolText(err.Error(), params["key"])) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + safeBody := truncateToolBody(body, params["key"]) + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + return nil, fmt.Errorf("upload api http status:%d body:%s", resp.StatusCode, safeBody) + } + var apiResp transcodeAPIResp + if err = json.Unmarshal(body, &apiResp); err != nil { + return nil, err + } + if apiResp.Status != "y" { + errMsg := truncateToolText(apiResp.Error, params["key"]) + if strings.Contains(errMsg, "最多允许待处理") || strings.Contains(errMsg, "排队处理") { + return nil, fmt.Errorf("%w errorCode:%d error:%s", ErrTranscodeQueueFull, apiResp.ErrorCode, errMsg) + } + if apiResp.ErrorCode == 2000 { + return nil, fmt.Errorf("%w errorCode:%d error:%s", errTranscodeTaskNotFound, apiResp.ErrorCode, errMsg) + } + return nil, fmt.Errorf("upload api errorCode:%d error:%s", apiResp.ErrorCode, errMsg) + } + return apiResp.Data, nil +} + +func md5Hex(value string) string { + sum := md5.Sum([]byte(value)) + return hex.EncodeToString(sum[:]) +} + +// TranscodeFileID 返回源视频 URL 对应的云端幂等键。 +func TranscodeFileID(fileURL string) string { + fileURL = strings.TrimSpace(fileURL) + if fileURL == "" { + return "" + } + return md5Hex(fileURL) +} + +func joinURLPath(host, uri string) string { + host = strings.TrimRight(strings.TrimSpace(host), "/") + uri = strings.TrimLeft(strings.TrimSpace(uri), "/") + if host == "" { + return uri + } + if uri == "" { + return host + } + return host + "/" + uri +} + +// MovieM3u8SourcePath 把完整 URL 或相对路径规范成本地保存的老司机资源 path。 +func MovieM3u8SourcePath(raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" { + return "" + } + return joinURLPath("laosiji", movieM3u8RelativePath(raw)) +} + +// MovieM3u8OriginURL 把本地老司机资源 path 还原成源站完整 URL。 +func MovieM3u8OriginURL(raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" { + return "" + } + if parsed, err := url.Parse(raw); err == nil && parsed.IsAbs() { + return raw + } + return joinURLPath(APIUrl, movieM3u8RelativePath(raw)) +} + +func movieM3u8RelativePath(raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" { + return "" + } + if parsed, err := url.Parse(raw); err == nil { + if parsed.IsAbs() { + raw = parsed.Path + } else if parsed.Path != "" { + raw = parsed.Path + } + } + raw = strings.TrimPrefix(raw, "/") + raw = strings.TrimPrefix(raw, "laosiji/") + if idx := strings.Index(raw, "m3m/"); idx >= 0 { + return raw[idx:] + } + if strings.Contains(raw, "/") { + return raw + } + return joinURLPath("m3m", raw) +} + +func truncateToolBody(body []byte, secrets ...string) string { + return truncateToolText(string(body), secrets...) +} + +func truncateToolText(text string, secrets ...string) string { + const limit = 300 + text = hevcpull.RedactText(strings.TrimSpace(text)) + secrets = append(secrets, APIKey, Appid) + for _, secret := range secrets { + if secret = strings.TrimSpace(secret); secret != "" { + text = strings.ReplaceAll(text, secret, "[REDACTED]") + text = strings.ReplaceAll(text, url.QueryEscape(secret), "[REDACTED]") + } + } + if len(text) <= limit { + return text + } + return text[:limit] + "..." +} diff --git a/common/laosiji/tools_test.go b/common/laosiji/tools_test.go new file mode 100644 index 0000000..b465141 --- /dev/null +++ b/common/laosiji/tools_test.go @@ -0,0 +1,217 @@ +package laosiji + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" +) + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +func preserveConfig(t *testing.T) { + t.Helper() + old := Config{ + AppID: Appid, + APIKey: APIKey, + APIUrl: APIUrl, + ImageYuan: IMAGEYUAN, + NoticeURL: NoticeURL, + } + t.Cleanup(func() { + InitConfig(old) + }) +} + +func TestConfiguredRejectsMissingCredentials(t *testing.T) { + preserveConfig(t) + InitConfig(Config{}) + + if Configured() { + t.Fatal("empty configuration must not be ready") + } + _, err := SystemDomains(context.Background()) + if err == nil || !strings.Contains(err.Error(), "appId,apiKey,apiUrl") { + t.Fatalf("expected a diagnostic configuration error, got %v", err) + } +} + +func TestMovieM3u8PathNormalization(t *testing.T) { + tests := map[string]string{ + "": "", + "demo.m3u8": "laosiji/m3m/demo.m3u8", + "m3m/demo.m3u8": "laosiji/m3m/demo.m3u8", + "laosiji/m3m/demo.m3u8": "laosiji/m3m/demo.m3u8", + "https://cdn.example.com/m3m/demo.m3u8?token=ignored": "laosiji/m3m/demo.m3u8", + "https://cdn.example.com/rk130/hevc/demo/index.m3u8": "laosiji/rk130/hevc/demo/index.m3u8", + "laosiji/rk130/hevc/demo/index.m3u8?token=also-ignored": "laosiji/rk130/hevc/demo/index.m3u8", + } + for input, want := range tests { + if got := MovieM3u8SourcePath(input); got != want { + t.Errorf("MovieM3u8SourcePath(%q) = %q, want %q", input, got, want) + } + } +} + +func TestTranscodeTaskNoNeedIsExplicit(t *testing.T) { + sourceURL := "https://app.example/api/app/vid/h5/light/m3u8/source.m3u8" + result := transcodeTaskToResult(SystemDomainsResp{MovieSourceCDN: "https://cdn.example"}, transcodeTask{ + Status: transcodeNoNeedStatus, + FileURL: sourceURL, + }, sourceURL, TranscodeFileID(sourceURL)) + + if !result.Done || !result.NoNeed { + t.Fatalf("status=4 must be done and no-need: %+v", result) + } + if result.HevcURL != sourceURL { + t.Fatalf("no-need diagnostic URL = %q, want %q", result.HevcURL, sourceURL) + } +} + +func TestTranscodeSuccessWithoutFileIsFailed(t *testing.T) { + result := transcodeTaskToResult(SystemDomainsResp{}, transcodeTask{ + Status: transcodeSuccessStatus, + }, "https://app.example/source.m3u8", "file-id") + + if result.Done || !result.Failed { + t.Fatalf("empty successful task must fail locally instead of staying pending: %+v", result) + } + if !strings.Contains(result.ErrorMsg, "without transcode_file") { + t.Fatalf("missing diagnostic error: %+v", result) + } +} + +func TestTranscodeFailureRedactsCredentials(t *testing.T) { + preserveConfig(t) + const ( + appID = "app-id-secret" + apiKey = "api-key-secret" + uploadKey = "upload key+secret" + ) + InitConfig(Config{ + AppID: appID, + APIKey: apiKey, + APIUrl: "https://api.example", + }) + + result := transcodeTaskToResult(SystemDomainsResp{UploadKey: uploadKey}, transcodeTask{ + Status: transcodeFailedStatus, + TranscodeError: fmt.Sprintf( + "failure app=%s api=%s upload=%s encoded=%s", + appID, + apiKey, + uploadKey, + url.QueryEscape(uploadKey), + ), + }, "https://app.example/source.m3u8", "file-id") + + if !result.Failed { + t.Fatalf("negative cloud status must be failed: %+v", result) + } + for _, secret := range []string{appID, apiKey, uploadKey, url.QueryEscape(uploadKey)} { + if strings.Contains(result.ErrorMsg, secret) { + t.Fatalf("transcode error leaked credential %q: %s", secret, result.ErrorMsg) + } + } + if !strings.Contains(result.ErrorMsg, "[REDACTED]") { + t.Fatalf("transcode error did not show redaction marker: %s", result.ErrorMsg) + } +} + +func TestUploadNetworkErrorRedactsKey(t *testing.T) { + preserveConfig(t) + InitConfig(Config{ + AppID: "app-id-secret", + APIKey: "1234567890abcdef", + APIUrl: "https://api.example", + }) + uploadKey := "upload key+secret" + oldClient := uploadHTTPClient + uploadHTTPClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + return nil, fmt.Errorf("dial failed for %s", req.URL.String()) + })} + t.Cleanup(func() { + uploadHTTPClient = oldClient + }) + + _, err := uploadAPICall(context.Background(), "https://upload.example/query", map[string]string{ + "key": uploadKey, + }) + if err == nil { + t.Fatal("expected network error") + } + errText := err.Error() + if strings.Contains(errText, uploadKey) || strings.Contains(errText, url.QueryEscape(uploadKey)) { + t.Fatalf("network error leaked upload key: %s", errText) + } + if !strings.Contains(errText, "[REDACTED]") { + t.Fatalf("network error did not show redaction marker: %s", errText) + } +} + +func TestMoviePostNon2xxIsDiagnosticAndRedacted(t *testing.T) { + preserveConfig(t) + const ( + appID = "app-id-secret" + apiKey = "1234567890abcdef" + ) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusBadGateway) + _, _ = fmt.Fprintf(w, "failure app=%s key=%s %s", appID, apiKey, strings.Repeat("x", 500)) + })) + defer server.Close() + InitConfig(Config{AppID: appID, APIKey: apiKey, APIUrl: server.URL}) + + err := moviePost(context.Background(), server.URL, map[string]interface{}{}, &struct{}{}) + if err == nil || !strings.Contains(err.Error(), "status:502") { + t.Fatalf("expected HTTP status diagnostic, got %v", err) + } + if strings.Contains(err.Error(), appID) || strings.Contains(err.Error(), apiKey) { + t.Fatalf("HTTP error leaked credentials: %s", err) + } + if !strings.Contains(err.Error(), "[REDACTED]") || !strings.HasSuffix(err.Error(), "...") { + t.Fatalf("HTTP error was not safely redacted/truncated: %s", err) + } +} + +func TestQueryH264ToH265TaskUsesExplicitStableFileID(t *testing.T) { + const fileID = "stable-file-id" + oldClient := uploadHTTPClient + uploadHTTPClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + if got := req.URL.Query().Get("file_id"); got != fileID { + t.Fatalf("query file_id = %q, want %q", got, fileID) + } + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader( + `{"status":"y","data":{"file_id":"stable-file-id","status":1}}`, + )), + Request: req, + }, nil + })} + t.Cleanup(func() { + uploadHTTPClient = oldClient + }) + + result, found, err := QueryH264ToH265Task( + context.Background(), + fileID, + "https://app.example/source.m3u8?hevc_exp=1&hevc_sig=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + SystemDomainsResp{UploadURL: "https://upload.example", UploadKey: "upload-key"}, + ) + if err != nil || !found { + t.Fatalf("explicit task query failed: found=%v err=%v", found, err) + } + if result.FileID != fileID { + t.Fatalf("result file ID = %q, want %q", result.FileID, fileID) + } +} diff --git a/common/laosiji/undress.go b/common/laosiji/undress.go new file mode 100644 index 0000000..bf014c7 --- /dev/null +++ b/common/laosiji/undress.go @@ -0,0 +1,14 @@ +package laosiji + +type GenerateUndressResponse struct { + TaskID string `json:"task_id"` // 任务 ID +} + +type QueryUndressResponse struct { + TaskID string `json:"task_id"` // 任务 ID + Bid string `json:"bid"` + Fee string `json:"fee"` + Type string `json:"type"` + Status string `json:"status"` // 状态 0待处理 -1处理失败 1处理中 2处理成功 + OutData string `json:"out_data"` // 输出数据 不同的ai 处理的数据不一样 查看文档的描述 +} diff --git a/common/laosiji_app/ai_mate.go b/common/laosiji_app/ai_mate.go new file mode 100644 index 0000000..8c744e0 --- /dev/null +++ b/common/laosiji_app/ai_mate.go @@ -0,0 +1,100 @@ +package laosiji_app + +import ( + "context" + "fmt" + "time" +) + +func GetUserName(env string, appID int32, uid uint64) string { + if env != "prod" { + return fmt.Sprintf("TEST-%d_%d", appID, uid) + } + return fmt.Sprintf("JHA-%d_%d", appID, uid) +} + +type GetAiMateURLReq struct { + Username string + Nickname string + Asset string + Currency string + Theme string + UserAvatar string + LogoURL string +} + +type GetAiMateURLResp struct { + AuthURL string `json:"auth_url"` +} + +func GetAiMateURL(ctx context.Context, req GetAiMateURLReq) (resp GetAiMateURLResp, err error) { + err = post(ctx, cfg.APIURL+"/lsjapi/aiGirlFriend/auth", map[string]interface{}{ + "username": req.Username, + "nickname": req.Nickname, + "asset": req.Asset, + "currency": req.Currency, + "theme": req.Theme, + "user_avatar": req.UserAvatar, + "logo_url": req.LogoURL, + }, &resp) + return +} + +type AiMateBringOutReq struct { + Username string +} + +type AiMateBringOutResp struct { + Currency string `json:"currency"` + Balance string `json:"balance"` +} + +func AiMateBringOut(ctx context.Context, req AiMateBringOutReq) (resp AiMateBringOutResp, err error) { + err = post(ctx, cfg.APIURL+"/lsjapi/aiGirlFriend/bringOutAssets", map[string]interface{}{ + "username": req.Username, + }, &resp) + return +} + +type AiMateOrderLogsReq struct { + AppID int32 + UID uint64 + Page int + PageSize int + StartTime time.Time + EndTime time.Time +} + +type AiMateOrderLogsResponse struct { + Page string `json:"page"` + PageSize string `json:"page_size"` + Total string `json:"total"` + TotalPage string `json:"total_page"` + Items []AiMateOrderLog `json:"items"` +} + +type AiMateOrderLog struct { + ID string `json:"id"` + Username string `json:"username"` + Type string `json:"type"` + TypeStr string `json:"type_str"` + Amount string `json:"amount"` + Balance string `json:"balance"` + Currency string `json:"currency"` + Remark string `json:"remark"` + TypeName string `json:"type_name"` + RoleID string `json:"role_id"` + RoleName string `json:"role_name"` + CreatedAt string `json:"created_at"` +} + +func GetAiMateOrderLogs(ctx context.Context, env string, req AiMateOrderLogsReq) (resp AiMateOrderLogsResponse, err error) { + err = post(ctx, cfg.APIURL+"/lsjapi/aiGirlFriend/orderLogs", map[string]interface{}{ + "username": GetUserName(env, req.AppID, req.UID), + "page": req.Page, + "page_size": req.PageSize, + "start_time": req.StartTime.Unix(), + "end_time": req.EndTime.Unix(), + }, &resp) + return +} diff --git a/common/laosiji_app/laosiji.go b/common/laosiji_app/laosiji.go new file mode 100644 index 0000000..0780dc2 --- /dev/null +++ b/common/laosiji_app/laosiji.go @@ -0,0 +1,169 @@ +package laosiji_app + +import ( + "bytes" + "context" + "crypto/aes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + "91porn-server/common/log" +) + +type Config struct { + AppID string `json:"appId"` + APIKey string `json:"apiKey"` + APIURL string `json:"apiUrl"` +} + +var cfg Config + +func Init(c Config) { + c.APIURL = strings.TrimRight(strings.TrimSpace(c.APIURL), "/") + cfg = c +} + +func Configured() bool { + return cfg.AppID != "" && cfg.APIKey != "" && cfg.APIURL != "" +} + +type Response struct { + Status string `json:"status"` + Data string `json:"data"` + Time string `json:"time"` + Error string `json:"error"` + ErrorCode int `json:"errorCode"` +} + +func post(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) error { + if !Configured() { + return errors.New("laosiji app config is incomplete") + } + jsonData, err := json.Marshal(req) + if err != nil { + return err + } + encryptedData, err := encryptBase64(string(jsonData), cfg.APIKey) + if err != nil { + return err + } + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBufferString(encryptedData)) + if err != nil { + return err + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("appid", cfg.AppID) + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(httpReq) + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("laosiji app http status %d", resp.StatusCode) + } + + var result Response + if err = json.Unmarshal(body, &result); err != nil { + return err + } + if result.Status != "y" { + return fmt.Errorf("laosiji app errorCode:%d error:%s", result.ErrorCode, result.Error) + } + data, err := decryptBase64(result.Data, cfg.APIKey) + if err != nil { + return err + } + if err = json.Unmarshal([]byte(data), response); err != nil { + log.Warn("laosiji_app unmarshal response failed", log.E(err)) + return err + } + return nil +} + +func encryptBase64(input, key string) (string, error) { + keyBytes, err := aesKey(key) + if err != nil { + return "", err + } + block, err := aes.NewCipher(keyBytes) + if err != nil { + return "", err + } + plainText := pkcs7Padding([]byte(input), block.BlockSize()) + encrypted := make([]byte, len(plainText)) + for start, end := 0, block.BlockSize(); start < len(plainText); start, end = start+block.BlockSize(), end+block.BlockSize() { + block.Encrypt(encrypted[start:end], plainText[start:end]) + } + return base64.StdEncoding.EncodeToString(encrypted), nil +} + +func decryptBase64(cipherText, key string) (string, error) { + keyBytes, err := aesKey(key) + if err != nil { + return "", err + } + data, err := base64.StdEncoding.DecodeString(cipherText) + if err != nil { + return "", err + } + block, err := aes.NewCipher(keyBytes) + if err != nil { + return "", err + } + if len(data) == 0 || len(data)%block.BlockSize() != 0 { + return "", errors.New("invalid encrypted payload length") + } + decrypted := make([]byte, len(data)) + for start, end := 0, block.BlockSize(); start < len(data); start, end = start+block.BlockSize(), end+block.BlockSize() { + block.Decrypt(decrypted[start:end], data[start:end]) + } + decrypted, err = pkcs7UnPadding(decrypted, block.BlockSize()) + if err != nil { + return "", err + } + return string(decrypted), nil +} + +func aesKey(key string) ([]byte, error) { + if len(key) > aes.BlockSize { + key = key[:aes.BlockSize] + } + if len(key) != aes.BlockSize { + return nil, fmt.Errorf("invalid AES key length %d", len(key)) + } + return []byte(key), nil +} + +func pkcs7Padding(src []byte, blockSize int) []byte { + padding := blockSize - len(src)%blockSize + return append(src, bytes.Repeat([]byte{byte(padding)}, padding)...) +} + +func pkcs7UnPadding(src []byte, blockSize int) ([]byte, error) { + if len(src) == 0 { + return nil, errors.New("empty padded payload") + } + padding := int(src[len(src)-1]) + if padding == 0 || padding > blockSize || padding > len(src) { + return nil, errors.New("invalid PKCS7 padding") + } + for _, value := range src[len(src)-padding:] { + if int(value) != padding { + return nil, errors.New("invalid PKCS7 padding") + } + } + return src[:len(src)-padding], nil +} diff --git a/common/laosiji_app/laosiji_test.go b/common/laosiji_app/laosiji_test.go new file mode 100644 index 0000000..6cc5754 --- /dev/null +++ b/common/laosiji_app/laosiji_test.go @@ -0,0 +1,109 @@ +package laosiji_app + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" +) + +func TestEncryptDecryptRoundTrip(t *testing.T) { + const ( + key = "1234567890abcdef" + input = `{"uid":204,"name":"测试用户"}` + ) + encrypted, err := encryptBase64(input, key) + if err != nil { + t.Fatalf("encryptBase64() error = %v", err) + } + decrypted, err := decryptBase64(encrypted, key) + if err != nil { + t.Fatalf("decryptBase64() error = %v", err) + } + if decrypted != input { + t.Fatalf("decryptBase64() = %q, want %q", decrypted, input) + } +} + +func TestGetUserName(t *testing.T) { + if got := GetUserName("test", 204, 123); got != "TEST-204_123" { + t.Fatalf("test username = %q", got) + } + if got := GetUserName("prod", 204, 123); got != "JHA-204_123" { + t.Fatalf("prod username = %q", got) + } +} + +func TestConfigured(t *testing.T) { + original := cfg + t.Cleanup(func() { cfg = original }) + + Init(Config{}) + if Configured() { + t.Fatal("empty config must not be configured") + } + Init(Config{AppID: "app", APIKey: "1234567890abcdef", APIURL: "https://example.com/"}) + if !Configured() { + t.Fatal("complete config must be configured") + } + if cfg.APIURL != "https://example.com" { + t.Fatalf("APIURL = %q", cfg.APIURL) + } +} + +func TestGetAiMateURLRequestAndResponse(t *testing.T) { + const key = "1234567890abcdef" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/lsjapi/aiGirlFriend/auth" { + t.Errorf("path = %q", r.URL.Path) + } + if r.Header.Get("appid") != "test-app" { + t.Errorf("appid = %q", r.Header.Get("appid")) + } + body, err := io.ReadAll(r.Body) + if err != nil { + t.Errorf("read request: %v", err) + return + } + plainText, err := decryptBase64(string(body), key) + if err != nil { + t.Errorf("decrypt request: %v", err) + return + } + var request map[string]interface{} + if err = json.Unmarshal([]byte(plainText), &request); err != nil { + t.Errorf("unmarshal request: %v", err) + return + } + if request["username"] != "TEST-204_99" || request["asset"] != "12.30" { + t.Errorf("request = %#v", request) + } + + data, err := encryptBase64(`{"auth_url":"https://example.com/ai"}`, key) + if err != nil { + t.Errorf("encrypt response: %v", err) + return + } + _ = json.NewEncoder(w).Encode(Response{Status: "y", Data: data}) + })) + defer server.Close() + + original := cfg + t.Cleanup(func() { cfg = original }) + Init(Config{AppID: "test-app", APIKey: key, APIURL: server.URL}) + + response, err := GetAiMateURL(context.Background(), GetAiMateURLReq{ + Username: "TEST-204_99", + Asset: "12.30", + Currency: "CNY", + Theme: "dark", + }) + if err != nil { + t.Fatalf("GetAiMateURL() error = %v", err) + } + if response.AuthURL != "https://example.com/ai" { + t.Fatalf("AuthURL = %q", response.AuthURL) + } +} diff --git a/common/localcache/localcache.go b/common/localcache/localcache.go new file mode 100644 index 0000000..21cd7fe --- /dev/null +++ b/common/localcache/localcache.go @@ -0,0 +1,16 @@ +// Package localcache 提供进程内一级缓存(基于 go-cache)。 +// +// C 为包级变量,导入即就绪、永不为 nil,可被任意分层(common/models/app/web/skd) +// 与任意二进制安全使用。此前各服务各自维护 appg.Cache / skdg.Cache,公共代码一旦 +// 依赖某个具体服务的缓存全局变量,就会在未初始化该变量的进程(如 skd 调用 appg.Cache) +// 中触发 nil panic。统一到本包后从根上消除该耦合。 +package localcache + +import ( + "time" + + "github.com/patrickmn/go-cache" +) + +// C 进程内一级缓存实例。默认 5 分钟过期、10 分钟清理一次(与原 appg/skdg 缓存参数保持一致)。 +var C = cache.New(5*time.Minute, 10*time.Minute) diff --git a/common/log/log.go b/common/log/log.go new file mode 100644 index 0000000..b3d9109 --- /dev/null +++ b/common/log/log.go @@ -0,0 +1,113 @@ +package log + +import ( + "context" + "strings" + "time" + + "91porn-server/middleware/requestid" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var ( + defaultZap, _ = zap.NewProductionConfig().Build() + + zapLogger *zap.Logger + ZapLog *zap.Logger + Debug = defaultZap.Debug + Info = defaultZap.Info + Warn = defaultZap.Warn + Error = defaultZap.Error + Fatal = defaultZap.Fatal +) + +// Options +type Options struct { + Level string // debug, warn, info, error fatal //default info + DisableStack bool `json:"disableStack"` +} + +func Init(opts Options) { + cfg := zap.NewProductionConfig() + cfg.EncoderConfig.EncodeTime = timeEncoder + cfg.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder + cfg.Level = zap.NewAtomicLevelAt(zapLevel(opts.Level)) + cfg.DisableStacktrace = opts.DisableStack + //cfg.EncoderConfig.EncodeCaller = zapcore.FullCallerEncoder + zapLogger, _ = cfg.Build() + ZapLog = zapLogger + Debug = zapLogger.Debug + Info = zapLogger.Info + Warn = zapLogger.Warn + Error = zapLogger.Error + Fatal = zapLogger.Fatal +} + +func DebugX(ctx context.Context, msg string, fs ...Field) { + Debug(msg, appendContext(ctx, fs...)...) +} + +func InfoX(ctx context.Context, msg string, fs ...Field) { + Info(msg, appendContext(ctx, fs...)...) +} + +func WarnX(ctx context.Context, msg string, fs ...Field) { + Warn(msg, appendContext(ctx, fs...)...) +} + +func ErrorX(ctx context.Context, msg string, fs ...Field) { + Error(msg, appendContext(ctx, fs...)...) +} + +func FatalX(ctx context.Context, msg string, fs ...Field) { + Fatal(msg, appendContext(ctx, fs...)...) +} + +func timeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Format("2006-01-02 15:04:05")) +} + +func zapLevel(level string) zapcore.Level { + level = strings.ToLower(level) + l := zapcore.InfoLevel + switch level { + case "debug": + l = zapcore.DebugLevel + case "info": + l = zapcore.InfoLevel + case "warn": + l = zapcore.WarnLevel + case "error": + l = zapcore.ErrorLevel + case "fatal": + l = zapcore.FatalLevel + default: + } + return l +} + +type Field = zap.Field + +// Any +func Any(key string, value interface{}) Field { + return zap.Any(key, value) +} + +// E shortcut for Any("err", err) +func E(err error) Field { + return Any("err", err) +} + +// R is a conveinient way to log request id. +func R(ctx context.Context) Field { + rid := ctx.Value(requestid.ContextKey) + return Any("request-id", rid) +} + +func appendContext(ctx context.Context, fs ...Field) (fields []Field) { + fields = append(fields, R(ctx)) + fields = append(fields, fs...) + return +} diff --git a/common/m3u8/m3u8.go b/common/m3u8/m3u8.go new file mode 100644 index 0000000..b7c36e3 --- /dev/null +++ b/common/m3u8/m3u8.go @@ -0,0 +1,503 @@ +package m3u8 + +import ( + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/web/webg" + "bytes" + "context" + "crypto/md5" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "net/http" + "net/url" + "path" + "path/filepath" + "strings" + "time" + + "github.com/grafov/m3u8" +) + +func GetAPPM3u8(source, fileName, _ string, cdn string, fsIo func(source, mds string) (data []byte, err error)) (*bytes.Buffer, error) { + mds := GetMediaResouce(source) + key := m3u8PureCacheKey(source, mds) + m3u8redis, err := appg.Redis.Get(key) + var m3u8Byte []byte + if m3u8redis != nil && err == nil { + //判断是否是m3u8文件 + if IsM3u8([]byte(*m3u8redis)) { + m3u8Byte = []byte(*m3u8redis) + } else { + _, _ = appg.Redis.Del(key) + } + } + if mds == constant.MediaSourceLaoSiJi { + source = normalizeLaosijiM3u8Source(source) + } + if m3u8Byte == nil || len(m3u8Byte) == 0 { + m3u8Byte, err = fsIo(source, mds) + if err != nil { + return nil, err + } + _ = appg.Redis.Set(key, string(m3u8Byte), redisconst.M3u8CacheExpire) + } + // TS 分片鉴权签名密钥:优先取配置(appg.Conf.Base.TsAuth),未配置时回退内置默认(版本 default + 内置常量) + keyVersion, authKey := appg.Conf.Base.TsAuth.Resolve() + var bytebuff *bytes.Buffer + if mds == constant.MediaSourcePMS { + bytebuff = DecodeFromReader(m3u8Byte, "", "/api/app/vid", "", mds, authKey, keyVersion, source) + } else if mds == constant.MediaSourceSP { + bytebuff = DecodeFromReader(m3u8Byte, cdn+strings.TrimSuffix(source, fileName), "/api/app/vid/sec", "", mds, authKey, keyVersion, source) + } else if mds == constant.MediaSourceLaoSiJi { + bytebuff = DecodeFromReader(m3u8Byte, cdn, "/api/app/vid/lsjsec", "", mds, authKey, keyVersion, source) + } else if mds == constant.MediaSourceJH1B { + bytebuff = DecodeFromReader(m3u8Byte, cdn+strings.TrimSuffix(source, fileName), "/api/app/vid/m3u8sec", "", mds, authKey, keyVersion, source) + } + if bytebuff == nil { + log.Warn("can't create m3u8 file", log.Any("source", source)) + return nil, errors.New(stderr.CodeEmptyData.Msg()) + } + return bytebuff, nil +} + +func normalizeLaosijiM3u8Source(source string) string { + source = strings.TrimLeft(strings.TrimSpace(source), "/") + return strings.TrimPrefix(source, "laosiji/") +} + +// m3u8PureCacheKey isolates raw playlists by media source and their complete +// normalized source path. Hashing keeps the Redis key bounded while avoiding +// collisions between common basenames such as index.m3u8. +func m3u8PureCacheKey(source, mds string) string { + normalizedSource := normalizeM3u8CacheSource(source) + sum := sha256.Sum256([]byte(mds + "\x00" + normalizedSource)) + return redisconst.M3u8PureCacheFmt(mds + ":" + hex.EncodeToString(sum[:])) +} + +func normalizeM3u8CacheSource(source string) string { + source = strings.TrimSpace(source) + parsed, err := url.Parse(source) + if err != nil { + return path.Clean("/" + strings.TrimLeft(strings.ReplaceAll(source, "\\", "/"), "/")) + } + + parsed.Scheme = strings.ToLower(parsed.Scheme) + parsed.Host = strings.ToLower(parsed.Host) + parsed.Fragment = "" + parsed.Path = path.Clean("/" + strings.TrimLeft(strings.ReplaceAll(parsed.Path, "\\", "/"), "/")) + parsed.RawPath = "" + if parsed.RawQuery != "" { + if query, queryErr := url.ParseQuery(parsed.RawQuery); queryErr == nil { + parsed.RawQuery = query.Encode() + } + } + return parsed.String() +} + +func DecodeFromReader(reader []byte, cdnUrl, serUrl, key, mds, authKey, keyVersion, source string) *bytes.Buffer { + p, listType, err := m3u8.DecodeFrom(bytes.NewReader(reader), false) + if err != nil { + log.Error("m3u8 decodeFrom error", log.E(err)) + return nil + } + switch listType { + case m3u8.MEDIA: + return Create(p.(*m3u8.MediaPlaylist), cdnUrl, serUrl, key, mds, authKey, keyVersion, source) + case m3u8.MASTER: + return p.(*m3u8.MasterPlaylist).Encode() + } + return nil +} + +// RewriteMasterPlaylist rewrites every child playlist URI in a master HLS +// playlist. Media playlists are returned unchanged. The signed H.265 pull +// endpoint uses this so variants and alternate renditions do not lose their +// HMAC when the cloud transcoder follows a relative child URI. +func RewriteMasterPlaylist(reader []byte, rewrite func(string) (string, error)) (*bytes.Buffer, error) { + if rewrite == nil { + return bytes.NewBuffer(reader), nil + } + playlist, listType, err := m3u8.DecodeFrom(bytes.NewReader(reader), false) + if err != nil { + return nil, err + } + if listType != m3u8.MASTER { + return bytes.NewBuffer(reader), nil + } + + master := playlist.(*m3u8.MasterPlaylist) + rewritten := make(map[string]string) + rewriteURI := func(rawURI string) (string, error) { + rawURI = strings.TrimSpace(rawURI) + if rawURI == "" { + return "", nil + } + if value, ok := rewritten[rawURI]; ok { + return value, nil + } + value, rewriteErr := rewrite(rawURI) + if rewriteErr != nil { + return "", rewriteErr + } + rewritten[rawURI] = value + return value, nil + } + + seenAlternatives := make(map[*m3u8.Alternative]struct{}) + for _, variant := range master.Variants { + if variant == nil { + continue + } + variant.URI, err = rewriteURI(variant.URI) + if err != nil { + return nil, fmt.Errorf("rewrite HLS variant: %w", err) + } + for _, alternative := range variant.Alternatives { + if alternative == nil { + continue + } + if _, ok := seenAlternatives[alternative]; ok { + continue + } + seenAlternatives[alternative] = struct{}{} + alternative.URI, err = rewriteURI(alternative.URI) + if err != nil { + return nil, fmt.Errorf("rewrite HLS alternative: %w", err) + } + } + } + return master.Encode(), nil +} + +func Create(src *m3u8.MediaPlaylist, cdnUrl, serUrl, key, mds, authKey, keyVersion, source string) *bytes.Buffer { + p, e := m3u8.NewMediaPlaylist(src.WinSize(), src.Count()) + if e != nil { + log.Error(fmt.Sprintf("Creating of media playlist failed: %s", e)) + return nil + } + p.SetVersion(src.Version()) + p.SeqNo = src.SeqNo + p.DiscontinuitySeq = src.DiscontinuitySeq + p.StartTime = src.StartTime + p.StartTimePrecise = src.StartTimePrecise + p.MediaType = src.MediaType + p.Iframe = src.Iframe + p.Args = src.Args + p.WV = src.WV + for _, customTag := range src.Custom { + p.SetCustomTag(customTag) + } + now := time.Now() + var activeMap *m3u8.Map + if src.Map != nil { + activeMap = src.Map + p.SetDefaultMap( + rewritePlaylistMediaURI(src.Map.URI, cdnUrl, authKey, keyVersion, mds, source, now), + src.Map.Limit, + src.Map.Offset, + ) + } + activeKey := src.Key + for _, v := range src.Segments { + if v != nil { + currentMapURI := "" + if activeMap != nil { + currentMapURI = activeMap.URI + } + if v.Map != nil { + currentMapURI = v.Map.URI + } + tsurl := resolvePlaylistMediaURI(v.URI, currentMapURI) + tsurl = rewritePlaylistMediaURI(tsurl, cdnUrl, authKey, keyVersion, mds, source, now) + if err := p.Append(tsurl, v.Duration, v.Title); err != nil { + log.Error(fmt.Sprintf("Appending of media playlist failed: %s", err)) + return nil + } + if v.Limit > 0 { + if err := p.SetRange(v.Limit, v.Offset); err != nil { + log.Error(fmt.Sprintf("Setting media byte range failed: %s", err)) + return nil + } + } + if v.Discontinuity { + if err := p.SetDiscontinuity(); err != nil { + log.Error(fmt.Sprintf("Setting media discontinuity failed: %s", err)) + return nil + } + } + if !v.ProgramDateTime.IsZero() { + if err := p.SetProgramDateTime(v.ProgramDateTime); err != nil { + log.Error(fmt.Sprintf("Setting media program date failed: %s", err)) + return nil + } + } + if v.SCTE != nil { + if err := p.SetSCTE35(v.SCTE); err != nil { + log.Error(fmt.Sprintf("Setting media SCTE tag failed: %s", err)) + return nil + } + } + for _, customTag := range v.Custom { + if err := p.SetCustomSegmentTag(customTag); err != nil { + log.Error(fmt.Sprintf("Setting media custom tag failed: %s", err)) + return nil + } + } + if v.Map != nil && !playlistMapsEqual(v.Map, activeMap) { + if err := p.SetMap( + rewritePlaylistMediaURI(v.Map.URI, cdnUrl, authKey, keyVersion, mds, source, now), + v.Map.Limit, + v.Map.Offset, + ); err != nil { + log.Error(fmt.Sprintf("Setting map of media playlist failed: %s", err)) + return nil + } + activeMap = v.Map + } + if v.Key != nil && !playlistKeysEqual(v.Key, activeKey) { + if err := p.SetKey( + v.Key.Method, + playlistKeyURI(v.Key.URI, serUrl, mds), + playlistKeyIV(v.Key.IV, key), + v.Key.Keyformat, + v.Key.Keyformatversions, + ); err != nil { + log.Error(fmt.Sprintf("Setting segment key failed: %s", err)) + return nil + } + activeKey = v.Key + } + } + } + if src.Key != nil { + _ = p.SetDefaultKey( + src.Key.Method, + playlistKeyURI(src.Key.URI, serUrl, mds), + playlistKeyIV(src.Key.IV, key), + src.Key.Keyformat, + src.Key.Keyformatversions, + ) + } + if src.TargetDuration > p.TargetDuration { + p.TargetDuration = src.TargetDuration + } + p.Close() + return p.Encode() +} + +func playlistMapsEqual(a, b *m3u8.Map) bool { + if a == nil || b == nil { + return a == b + } + return a.URI == b.URI && a.Limit == b.Limit && a.Offset == b.Offset +} + +func playlistKeysEqual(a, b *m3u8.Key) bool { + if a == nil || b == nil { + return a == b + } + return a.Method == b.Method && + a.URI == b.URI && + a.IV == b.IV && + a.Keyformat == b.Keyformat && + a.Keyformatversions == b.Keyformatversions +} + +func playlistKeyURI(sourceKeyURI, serverURL, mds string) string { + if mds != constant.MediaSourcePMS { + return serverURL + } + if strings.Contains(sourceKeyURI, "/mt/enkeymt") { + return serverURL + "/pms/mt_sec" + } + return serverURL + "/pms/sec" +} + +func playlistKeyIV(sourceIV, override string) string { + if override != "" { + return override + } + return sourceIV +} + +// resolvePlaylistMediaURI uses an absolute EXT-X-MAP URI as the base for +// relative fMP4/CMAF segment paths. +func resolvePlaylistMediaURI(mediaURI, mapURI string) string { + if strings.TrimSpace(mediaURI) == "" || strings.TrimSpace(mapURI) == "" { + return mediaURI + } + parsedMediaURI, err := url.Parse(mediaURI) + if err == nil && parsedMediaURI.IsAbs() { + return mediaURI + } + parsedMapURI, err := url.Parse(mapURI) + if err != nil || !parsedMapURI.IsAbs() { + return mediaURI + } + parsedMapURI.RawQuery = "" + parsedMapURI.Fragment = "" + ref, err := url.Parse(mediaURI) + if err != nil { + return mediaURI + } + return parsedMapURI.ResolveReference(ref).String() +} + +// rewritePlaylistMediaURI applies the same source, auth and CDN rewriting to +// regular media segments and EXT-X-MAP initialization segments. +func rewritePlaylistMediaURI(mediaURI, cdnUrl, authKey, keyVersion, mds, source string, now time.Time) string { + mediaURL := mediaURI + if mds == constant.MediaSourceLaoSiJi { + mediaURL = removeDomainPrefix(mediaURL) + mediaURL = filepath.Join("laosiji", mediaURL) + } + if len(authKey) > 0 { + uri := filepath.Join(filepath.Dir(source), mediaURL) + if mds == constant.MediaSourceLaoSiJi { + uri = "/" + mediaURL + } + urlAuth := generateUrlAuth(now, uri, authKey, keyVersion) + mediaURL = fmt.Sprintf("%s%s", mediaURL, urlAuth) + } + if cdnUrl != "" { + mediaURL = common.BindUrl(cdnUrl, mediaURL) + } + return mediaURL +} + +func GetTsUrlFromReader(reader []byte, preUrl, cdnUrl string) []string { + p, listType, err := m3u8.DecodeFrom(bytes.NewReader(reader), false) + if err != nil { + log.Error("m3u8 decodeFrom error", log.E(err)) + return []string{} + } + var tsUrl []string + switch listType { + case m3u8.MEDIA: + src := p.(*m3u8.MediaPlaylist) + tsUrl = make([]string, 0, len(src.Segments)) + for _, v := range src.Segments { + if v != nil { + tsUrl = append(tsUrl, common.BindUrl(cdnUrl, preUrl, v.URI)) + } + } + } + return tsUrl +} + +func IsM3u8(reader []byte) bool { + _, _, err := m3u8.DecodeFrom(bytes.NewReader(reader), false) + return err == nil +} + +const timeout = 5 * time.Second + +// UploadSuccess 上传成功回调, 通知文件服务, 文件上传完成 +func UploadSuccess(id string) (code stderr.Code) { + var params = map[string]interface{}{ + "id": id, + } + c, cancle := context.WithTimeout(context.Background(), timeout) + defer cancle() + respBody := commod.Resp{} + //请求 + httpStatus, err := httputil.DefaultClientPostJsonWithRespWithCtx(c, &respBody, webg.Conf.URL.FileInfoUrl, nil, params) + log.Info("http method UploadSuccess response code ==>", log.Any("httpStatus", httpStatus), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("UploadSuccess POSTJsonWithJResp error", log.E(err)) + return stderr.ErrConnectToFs + } + if respBody.Code != http.StatusOK { + log.Error("UploadSuccess status error", log.Any("respBody.Code", respBody.Code), log.E(err)) + return stderr.ErrFsServerFile + } + return stderr.Success +} + +// 媒体资源库选择 +func GetMediaResouce(source string) string { + if strings.HasPrefix(source, "v1/") || strings.HasPrefix(source, "/v1/") || + strings.HasPrefix(source, "v2/") || strings.HasPrefix(source, "/v2/") || + strings.HasPrefix(source, "v3/") || strings.HasPrefix(source, "/v3/") { + return constant.MediaSourceJH1B + } + + if strings.HasPrefix(source, constant.MediaSourcePMSPrefixPath) { + return constant.MediaSourcePMS + } + if strings.HasPrefix(source, constant.MediaSourceSPPrefixPath) { + return constant.MediaSourceSP + } + if strings.Contains(source, "laosiji") { + return constant.MediaSourceLaoSiJi + } + return constant.MediaSourceSP +} + +// 生成鉴权url +func generateUrlAuth(now time.Time, path, authKey, keyVersion string) string { + timestamp := now.Unix() + // randId := 0 + //signStr := fmt.Sprintf("%s%s%d", authKey, path, timestamp) + //md5Str := getMD5Sign(signStr) + //urlAuth := fmt.Sprintf("?t=%d&k=%s", timestamp, md5Str) + signStr := fmt.Sprintf("%s-%d-0-0-%s", path, timestamp, authKey) + md5Str := getMD5Sign(signStr) + // c={appid}(commod.KFK_APPID) 供 CDN 按应用区分统计/路由;v={keyVersion} 供 CDN 按版本选择校验密钥 + urlAuth := fmt.Sprintf("?md=%d-0-0-%s&c=%d&v=%s", timestamp, md5Str, commod.KFK_APPID, keyVersion) + return urlAuth +} + +// getMD5Sign 得到签名 +func getMD5Sign(buf string) string { + md5Ctx := md5.New() + md5Ctx.Write([]byte(buf)) + cipherStr := md5Ctx.Sum(nil) + nsign := hex.EncodeToString(cipherStr) + return nsign +} + +func replaceDomainAndPath(originalURL, newBase string) string { + // 解析原始 URL + parsedURL, err := url.Parse(originalURL) + if err != nil { + return originalURL + } + + // 解析新基础 URL + newBaseURL, err := url.Parse(newBase) + if err != nil { + return originalURL + } + + // 替换协议、主机和基础路径 + parsedURL.Scheme = newBaseURL.Scheme + parsedURL.Host = newBaseURL.Host + + // 构建新路径:/laosiji + 原始路径(去掉旧域名部分) + oldBasePath := "" + parsedURL.Path = path.Join(newBaseURL.Path, strings.TrimPrefix(parsedURL.Path, oldBasePath)) + + // 移除查询参数 + parsedURL.RawQuery = "" + + return parsedURL.String() +} +func removeDomainPrefix(originalURL string) string { + // 解析原始 URL + parsedURL, err := url.Parse(originalURL) + if err != nil { + return originalURL + } + oldBasePath := "" + return strings.TrimPrefix(parsedURL.Path, oldBasePath) +} diff --git a/common/m3u8/m3u8_test.go b/common/m3u8/m3u8_test.go new file mode 100644 index 0000000..1a0b16c --- /dev/null +++ b/common/m3u8/m3u8_test.go @@ -0,0 +1,281 @@ +package m3u8 + +import ( + "bytes" + "strings" + "testing" + + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + + grafovm3u8 "github.com/grafov/m3u8" +) + +func TestM3u8PureCacheKeyUsesFullSource(t *testing.T) { + tests := []struct { + name string + sourceA string + sourceB string + }{ + { + name: "same basename in different directories", + sourceA: "/sp/movie-a/index.m3u8", + sourceB: "/sp/movie-b/index.m3u8", + }, + { + name: "filenames previously collapsed by strings.Trim", + sourceA: "/sp/movie/movie.m3u8", + sourceB: "/sp/movie/ovie.m3u8", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + keyA := m3u8PureCacheKey(tt.sourceA, constant.MediaSourceSP) + keyB := m3u8PureCacheKey(tt.sourceB, constant.MediaSourceSP) + if keyA == keyB { + t.Fatalf("different playlist sources share cache key %q", keyA) + } + }) + } +} + +func TestNormalizeLaosijiM3u8Source(t *testing.T) { + for _, source := range []string{ + "laosiji/m3m/movie/index.m3u8", + "/laosiji/m3m/movie/index.m3u8", + } { + if got := normalizeLaosijiM3u8Source(source); got != "m3m/movie/index.m3u8" { + t.Fatalf("normalizeLaosijiM3u8Source(%q) = %q", source, got) + } + } +} + +func TestM3u8PureCacheKeyNormalizesSourceAndIncludesMediaSource(t *testing.T) { + const source = "/sp/movie/season/../index.m3u8?b=2&a=1" + normalizedVariant := " /sp/movie/index.m3u8?a=1&b=2#ignored " + + spKey := m3u8PureCacheKey(source, constant.MediaSourceSP) + if got := m3u8PureCacheKey(normalizedVariant, constant.MediaSourceSP); got != spKey { + t.Fatalf("equivalent sources should share a cache key: %q != %q", got, spKey) + } + if got := m3u8PureCacheKey(source, constant.MediaSourcePMS); got == spKey { + t.Fatalf("different media sources should not share a cache key: %q", got) + } + if !strings.HasPrefix(spKey, redisconst.M3u8PureCacheFmt(constant.MediaSourceSP+":")) { + t.Fatalf("cache key should include media source prefix, got %q", spKey) + } +} + +func TestDecodeFromReaderKeepsHevcMap(t *testing.T) { + data := []byte(`#EXTM3U +#EXT-X-TARGETDURATION:6 +#EXT-X-VERSION:6 +#EXT-X-MEDIA-SEQUENCE:1 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="https://cdn.g3ejjm8m.com/rk130/m3u8-v5/f98/f98495114eca4ae66714b0d0249d8980/683a_0.m4s" +#EXTINF:6.006, +683a_1.m4s +#EXTINF:6.006, +683a_2.m4s +#EXT-X-ENDLIST +`) + buf := DecodeFromReader( + data, + "https://rs.hyxrp.cn", + "/api/app/vid/lsjsec", + "", + constant.MediaSourceLaoSiJi, + constant.MediaSourceAuthKey, + constant.DefaultTsAuthKeyVersion, + "m3m/demo.m3u8", + ) + if buf == nil { + t.Fatal("DecodeFromReader returned nil") + } + + got := buf.String() + for _, want := range []string{ + `#EXT-X-MAP:URI="https://rs.hyxrp.cn/laosiji/rk130/m3u8-v5/f98/f98495114eca4ae66714b0d0249d8980/683a_0.m4s?md=`, + `https://rs.hyxrp.cn/laosiji/rk130/m3u8-v5/f98/f98495114eca4ae66714b0d0249d8980/683a_1.m4s?md=`, + `https://rs.hyxrp.cn/laosiji/rk130/m3u8-v5/f98/f98495114eca4ae66714b0d0249d8980/683a_2.m4s?md=`, + } { + if !strings.Contains(got, want) { + t.Fatalf("rewritten playlist missing %q:\n%s", want, got) + } + } + if strings.Contains(got, "#EXT-X-KEY") { + t.Fatalf("unencrypted HEVC playlist should not add EXT-X-KEY:\n%s", got) + } + if strings.Contains(got, "/laosiji/683a_") { + t.Fatalf("relative HEVC segments were not resolved against EXT-X-MAP:\n%s", got) + } +} + +func TestDecodeFromReaderKeepsEncryptedH264Key(t *testing.T) { + data := []byte(`#EXTM3U +#EXT-X-TARGETDURATION:6 +#EXT-X-VERSION:3 +#EXT-X-KEY:METHOD=AES-128,URI="source.key",IV=0x00000000000000000000000000000001 +#EXTINF:6, +segment.ts +#EXT-X-ENDLIST +`) + buf := DecodeFromReader( + data, + "https://cdn.example.com/video", + "/api/app/vid/sec", + "", + constant.MediaSourceSP, + "", + constant.DefaultTsAuthKeyVersion, + "sp/demo.m3u8", + ) + if buf == nil { + t.Fatal("DecodeFromReader returned nil") + } + + got := buf.String() + for _, want := range []string{ + `#EXT-X-KEY:METHOD=AES-128,URI="/api/app/vid/sec",IV=0x00000000000000000000000000000001`, + "https://cdn.example.com/video/segment.ts", + } { + if !strings.Contains(got, want) { + t.Fatalf("rewritten playlist missing %q:\n%s", want, got) + } + } +} + +func TestCreateResolvesFirstSegmentAgainstSegmentMap(t *testing.T) { + src, err := grafovm3u8.NewMediaPlaylist(1, 1) + if err != nil { + t.Fatalf("create source playlist: %v", err) + } + if err = src.Append("first.m4s", 6, ""); err != nil { + t.Fatalf("append source segment: %v", err) + } + mapURI := "https://cdn.g3ejjm8m.com/hevc/movie/init.m4s" + if err = src.SetMap(mapURI, 0, 0); err != nil { + t.Fatalf("set source segment map: %v", err) + } + if src.Map != nil { + t.Fatal("test setup must use a segment-level map") + } + + buf := Create( + src, + "https://rs.hyxrp.cn", + "/api/app/vid/lsjsec", + "", + constant.MediaSourceLaoSiJi, + constant.MediaSourceAuthKey, + constant.DefaultTsAuthKeyVersion, + "m3m/demo.m3u8", + ) + if buf == nil { + t.Fatal("Create returned nil") + } + + got := buf.String() + for _, want := range []string{ + `#EXT-X-MAP:URI="https://rs.hyxrp.cn/laosiji/hevc/movie/init.m4s?md=`, + "https://rs.hyxrp.cn/laosiji/hevc/movie/first.m4s?md=", + } { + if !strings.Contains(got, want) { + t.Fatalf("rewritten playlist missing %q:\n%s", want, got) + } + } + if strings.Contains(got, "/laosiji/first.m4s") { + t.Fatalf("first relative segment was not resolved against its map:\n%s", got) + } +} + +func TestDecodeFromReaderPreservesCMAFSegmentMetadata(t *testing.T) { + data := []byte(`#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-TARGETDURATION:6 +#EXT-X-KEY:METHOD=AES-128,URI="first.key",IV=0x00000000000000000000000000000001 +#EXT-X-MAP:URI="https://cdn.example.com/cmaf/init.mp4" +#EXT-X-BYTERANGE:100@0 +#EXTINF:6, +chunk.mp4 +#EXT-X-DISCONTINUITY +#EXT-X-KEY:METHOD=AES-128,URI="second.key",IV=0x00000000000000000000000000000002 +#EXT-X-BYTERANGE:120@100 +#EXTINF:6, +chunk.mp4 +#EXT-X-ENDLIST +`) + buf := DecodeFromReader( + data, + "https://play.example.com", + "/api/app/vid/sec", + "", + constant.MediaSourceSP, + "", + constant.DefaultTsAuthKeyVersion, + "sp/cmaf/index.m3u8", + ) + if buf == nil { + t.Fatal("DecodeFromReader returned nil") + } + + got := buf.String() + for _, want := range []string{ + "#EXT-X-BYTERANGE:100@0", + "#EXT-X-BYTERANGE:120@100", + "#EXT-X-DISCONTINUITY", + "IV=0x00000000000000000000000000000001", + "IV=0x00000000000000000000000000000002", + } { + if !strings.Contains(got, want) { + t.Fatalf("rewritten playlist missing %q:\n%s", want, got) + } + } + if count := strings.Count(got, "#EXT-X-KEY:"); count != 2 { + t.Fatalf("rewritten playlist key count = %d, want 2:\n%s", count, got) + } +} + +func TestRewriteMasterPlaylist(t *testing.T) { + input := []byte(`#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="main",DEFAULT=YES,AUTOSELECT=YES,URI="audio/index.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=1500000,RESOLUTION=1280x720,AUDIO="audio" +video/720/index.m3u8 +`) + output, err := RewriteMasterPlaylist(input, func(rawURI string) (string, error) { + return "/signed/" + rawURI, nil + }) + if err != nil { + t.Fatalf("RewriteMasterPlaylist failed: %v", err) + } + playlist, listType, err := grafovm3u8.DecodeFrom(bytes.NewReader(output.Bytes()), false) + if err != nil { + t.Fatalf("decode rewritten master: %v", err) + } + if listType != grafovm3u8.MASTER { + t.Fatalf("list type = %v, want master", listType) + } + master := playlist.(*grafovm3u8.MasterPlaylist) + if len(master.Variants) != 1 || master.Variants[0].URI != "/signed/video/720/index.m3u8" { + t.Fatalf("variant was not rewritten: %+v", master.Variants) + } + if len(master.Variants[0].Alternatives) != 1 || + master.Variants[0].Alternatives[0].URI != "/signed/audio/index.m3u8" { + t.Fatalf("alternative was not rewritten: %+v", master.Variants[0].Alternatives) + } +} + +func TestRewriteMasterPlaylistLeavesMediaPlaylistUnchanged(t *testing.T) { + input := []byte("#EXTM3U\n#EXT-X-TARGETDURATION:4\n#EXTINF:4,\nsegment.ts\n#EXT-X-ENDLIST\n") + output, err := RewriteMasterPlaylist(input, func(string) (string, error) { + t.Fatal("media playlist URI rewriter must not be called") + return "", nil + }) + if err != nil { + t.Fatalf("RewriteMasterPlaylist failed: %v", err) + } + if !bytes.Equal(output.Bytes(), input) { + t.Fatalf("media playlist changed:\n%s", output.String()) + } +} diff --git a/common/maths/correlation.go b/common/maths/correlation.go new file mode 100755 index 0000000..4ecd4d9 --- /dev/null +++ b/common/maths/correlation.go @@ -0,0 +1,44 @@ +package maths + +import "math" + +func Correlation(data1, data2 Float64Data) (float64, error) { + l1 := data1.Len() + l2 := data2.Len() + if l1 == 0 || l2 == 0 { + return math.NaN(), EmptyInputErr + } + if l1 != l2 { + return math.NaN(), SizeErr + } + sdev1, _ := StandardDeviationPopulation(data1) + sdev2, _ := StandardDeviationPopulation(data2) + if sdev1 == 0 || sdev2 == 0 { + return 0, nil + } + covp, _ := CovariancePopulation(data1, data2) + return covp / (sdev1 * sdev2), nil +} + +func Pearson(data1, data2 Float64Data) (float64, error) { + return Correlation(data1, data2) +} + +func AutoCorrelation(data Float64Data, lags int) (float64, error) { + if len(data) < 1 { + return 0, EmptyInputErr + } + mean, _ := Mean(data) + var result, q float64 + for i := 0; i < lags; i++ { + v := (data[0] - mean) * (data[0] - mean) + for i := 1; i < len(data); i++ { + delta0 := data[i-1] - mean + delta1 := data[i] - mean + q += (delta0*delta1 - q) / float64(i+1) + v += (delta1*delta1 - v) / float64(i+1) + } + result = q / v + } + return result, nil +} diff --git a/common/maths/data.go b/common/maths/data.go new file mode 100755 index 0000000..6f139b5 --- /dev/null +++ b/common/maths/data.go @@ -0,0 +1,23 @@ +package maths + +type Float64Data []float64 + +func (f Float64Data) Get(i int) float64 { return f[i] } + +func (f Float64Data) Len() int { return len(f) } + +func (f Float64Data) Sum() (float64, error) { return Sum(f) } + +func (f Float64Data) Mean() (float64, error) { return Mean(f) } + +func (f Float64Data) Correlation(d Float64Data) (float64, error) { + return Correlation(f, d) +} + +func (f Float64Data) AutoCorrelation(lags int) (float64, error) { + return AutoCorrelation(f, lags) +} + +func (f Float64Data) Pearson(d Float64Data) (float64, error) { + return Pearson(f, d) +} diff --git a/common/maths/decimal.go b/common/maths/decimal.go new file mode 100644 index 0000000..7630e08 --- /dev/null +++ b/common/maths/decimal.go @@ -0,0 +1,78 @@ +package maths + +import ( + "fmt" + "strconv" + + "github.com/shopspring/decimal" +) + +// 保留2位小数 +func Decimal2Bit(value float64) float64 { + data, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", value), 64) + return data +} + +// 保留4位小数 +func Decimal4Bit(value float64) float64 { + data, _ := strconv.ParseFloat(fmt.Sprintf("%.4f", value), 64) + return data +} + +// 保留6位小数 +func Decimal6Bit(value float64) float64 { + data, _ := strconv.ParseFloat(fmt.Sprintf("%.6f", value), 64) + return data +} + +// 相乘 +func DecimalMul(dec1, dec2 string) (string, error) { + n1, err := decimal.NewFromString(dec1) + if err != nil { + return "", err + } + n2, err := decimal.NewFromString(dec2) + if err != nil { + return "", err + } + return n1.Mul(n2).String(), nil +} + +// 相加 +func DecimalAdd(dec1, dec2 string) (string, error) { + n1, err := decimal.NewFromString(dec1) + if err != nil { + return "", err + } + n2, err := decimal.NewFromString(dec2) + if err != nil { + return "", err + } + return n1.Add(n2).String(), nil +} + +// 相除 +func DecimalDiv(dec1, dec2 string) (string, error) { + n1, err := decimal.NewFromString(dec1) + if err != nil { + return "", err + } + n2, err := decimal.NewFromString(dec2) + if err != nil { + return "", err + } + return n1.Div(n2).String(), nil +} + +// 相减 +func DecimalSub(dec1, dec2 string) (string, error) { + n1, err := decimal.NewFromString(dec1) + if err != nil { + return "", err + } + n2, err := decimal.NewFromString("-" + dec2) + if err != nil { + return "", err + } + return n1.Add(n2).String(), nil +} diff --git a/common/maths/deviation.go b/common/maths/deviation.go new file mode 100755 index 0000000..f229c3e --- /dev/null +++ b/common/maths/deviation.go @@ -0,0 +1,34 @@ +package maths + +import ( + "fmt" + "math" + "strconv" +) + +func StandardDeviationPopulation(input Float64Data) (sdev float64, err error) { + if input.Len() == 0 { + return math.NaN(), EmptyInputErr + } + vp, _ := PopulationVariance(input) + return math.Pow(vp, 0.5), nil +} + +func DivideInt64(a, b int64) float64 { + if b == 0 { + return 0 + } + return float64(a) / float64(b) +} + +func DivideFloat64(a, b float64) float64 { + if b == 0 { + return 0 + } + return a / b +} + +func ToFloat64_b2(v float64) float64 { + f, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", v), 64) + return f +} diff --git a/common/maths/errors.go b/common/maths/errors.go new file mode 100755 index 0000000..8695365 --- /dev/null +++ b/common/maths/errors.go @@ -0,0 +1,31 @@ +package maths + +type statsError struct { + err string +} + +func (s statsError) Error() string { + return s.err +} + +func (s statsError) String() string { + return s.err +} + +var ( + ErrEmptyInput = statsError{"Input must not be empty."} + + ErrNaN = statsError{"Not a number."} + + ErrNegative = statsError{"Must not contain negative values."} + + ErrZero = statsError{"Must not contain zero values."} + + ErrBounds = statsError{"Input is outside of range."} + + ErrSize = statsError{"Must be the same length."} + + ErrInfValue = statsError{"Value is infinite."} + + ErrYCoord = statsError{"Y Value must be greater than zero."} +) diff --git a/common/maths/legacy.go b/common/maths/legacy.go new file mode 100755 index 0000000..48bfa15 --- /dev/null +++ b/common/maths/legacy.go @@ -0,0 +1,17 @@ +package maths + +func VarP(input Float64Data) (sdev float64, err error) { + return PopulationVariance(input) +} + +var ( + EmptyInputErr = ErrEmptyInput + NaNErr = ErrNaN + NegativeErr = ErrNegative + ZeroErr = ErrZero + BoundsErr = ErrBounds + SizeErr = ErrSize + InfValue = ErrInfValue + YCoordErr = ErrYCoord + EmptyInput = ErrEmptyInput +) diff --git a/common/maths/load.go b/common/maths/load.go new file mode 100755 index 0000000..38eb4f8 --- /dev/null +++ b/common/maths/load.go @@ -0,0 +1,181 @@ +package maths + +import ( + "strconv" + "time" +) + +func LoadRawData(raw interface{}) (f Float64Data) { + var r []interface{} + var s Float64Data + switch t := raw.(type) { + case []interface{}: + r = t + case []uint: + for _, v := range t { + s = append(s, float64(v)) + } + return s + case []uint8: + for _, v := range t { + s = append(s, float64(v)) + } + return s + case []uint16: + for _, v := range t { + s = append(s, float64(v)) + } + return s + case []uint32: + for _, v := range t { + s = append(s, float64(v)) + } + return s + case []uint64: + for _, v := range t { + s = append(s, float64(v)) + } + return s + case []bool: + for _, v := range t { + if v { + s = append(s, 1.0) + } else { + s = append(s, 0.0) + } + } + return s + case []float64: + return Float64Data(t) + case []int: + for _, v := range t { + s = append(s, float64(v)) + } + return s + case []int8: + for _, v := range t { + s = append(s, float64(v)) + } + return s + case []int16: + for _, v := range t { + s = append(s, float64(v)) + } + return s + case []int32: + for _, v := range t { + s = append(s, float64(v)) + } + return s + case []int64: + for _, v := range t { + s = append(s, float64(v)) + } + return s + case []string: + for _, v := range t { + r = append(r, v) + } + case []time.Duration: + for _, v := range t { + r = append(r, v) + } + case map[int]int: + for i := 0; i < len(t); i++ { + s = append(s, float64(t[i])) + } + return s + case map[int]int8: + for i := 0; i < len(t); i++ { + s = append(s, float64(t[i])) + } + return s + case map[int]int16: + for i := 0; i < len(t); i++ { + s = append(s, float64(t[i])) + } + return s + case map[int]int32: + for i := 0; i < len(t); i++ { + s = append(s, float64(t[i])) + } + return s + case map[int]int64: + for i := 0; i < len(t); i++ { + s = append(s, float64(t[i])) + } + return s + case map[int]string: + for i := 0; i < len(t); i++ { + r = append(r, t[i]) + } + case map[int]uint: + for i := 0; i < len(t); i++ { + s = append(s, float64(t[i])) + } + return s + case map[int]uint8: + for i := 0; i < len(t); i++ { + s = append(s, float64(t[i])) + } + return s + case map[int]uint16: + for i := 0; i < len(t); i++ { + s = append(s, float64(t[i])) + } + return s + case map[int]uint32: + for i := 0; i < len(t); i++ { + s = append(s, float64(t[i])) + } + return s + case map[int]uint64: + for i := 0; i < len(t); i++ { + s = append(s, float64(t[i])) + } + return s + case map[int]bool: + for i := 0; i < len(t); i++ { + if t[i] { + s = append(s, 1.0) + } else { + s = append(s, 0.0) + } + } + return s + case map[int]float64: + for i := 0; i < len(t); i++ { + s = append(s, t[i]) + } + return s + case map[int]time.Duration: + for i := 0; i < len(t); i++ { + r = append(r, t[i]) + } + } + for _, v := range r { + switch t := v.(type) { + case int: + a := float64(t) + f = append(f, a) + case uint: + f = append(f, float64(t)) + case float64: + f = append(f, t) + case string: + fl, err := strconv.ParseFloat(t, 64) + if err == nil { + f = append(f, fl) + } + case bool: + if t { + f = append(f, 1.0) + } else { + f = append(f, 0.0) + } + case time.Duration: + f = append(f, float64(t)) + } + } + return f +} diff --git a/common/maths/maths.go b/common/maths/maths.go new file mode 100755 index 0000000..50a5346 --- /dev/null +++ b/common/maths/maths.go @@ -0,0 +1,31 @@ +package maths + +import ( + "fmt" + "math/rand" + "sort" +) + +func copyslice(input Float64Data) Float64Data { + s := make(Float64Data, input.Len()) + copy(s, input) + return s +} + +func sortedCopyDif(input Float64Data) (copy Float64Data) { + if sort.Float64sAreSorted(input) { + return input + } + copy = copyslice(input) + sort.Float64s(copy) + return +} + +func RandDigits(n uint) string { + s := "" + for n != 0 { + s += fmt.Sprintf("%d", rand.Intn(10)) + n-- + } + return s +} diff --git a/common/maths/max.go b/common/maths/max.go new file mode 100755 index 0000000..9891e9b --- /dev/null +++ b/common/maths/max.go @@ -0,0 +1,16 @@ +package maths + +import "math" + +func Max(input Float64Data) (max float64, err error) { + if input.Len() == 0 { + return math.NaN(), EmptyInputErr + } + max = input.Get(0) + for i := 1; i < input.Len(); i++ { + if input.Get(i) > max { + max = input.Get(i) + } + } + return max, nil +} diff --git a/common/maths/mean.go b/common/maths/mean.go new file mode 100755 index 0000000..fdfbe86 --- /dev/null +++ b/common/maths/mean.go @@ -0,0 +1,11 @@ +package maths + +import "math" + +func Mean(input Float64Data) (float64, error) { + if input.Len() == 0 { + return math.NaN(), EmptyInputErr + } + sum, _ := input.Sum() + return sum / float64(input.Len()), nil +} diff --git a/common/maths/min.go b/common/maths/min.go new file mode 100755 index 0000000..b691ecc --- /dev/null +++ b/common/maths/min.go @@ -0,0 +1,17 @@ +package maths + +import "math" + +func Min(input Float64Data) (min float64, err error) { + l := input.Len() + if l == 0 { + return math.NaN(), EmptyInputErr + } + min = input.Get(0) + for i := 1; i < l; i++ { + if input.Get(i) < min { + min = input.Get(i) + } + } + return min, nil +} diff --git a/common/maths/mode.go b/common/maths/mode.go new file mode 100755 index 0000000..d59d18f --- /dev/null +++ b/common/maths/mode.go @@ -0,0 +1,41 @@ +package maths + +func Mode(input Float64Data) (mode []float64, err error) { + l := input.Len() + if l == 1 { + return input, nil + } + if l == 0 { + return nil, EmptyInputErr + } + c := sortedCopyDif(input) + // Traverse sorted array, + // tracking the longest repeating sequence + mode = make([]float64, 5) + cnt, maxCnt := 1, 1 + for i := 1; i < l; i++ { + switch { + case c[i] == c[i-1]: + cnt++ + case cnt == maxCnt && maxCnt != 1: + mode = append(mode, c[i-1]) + cnt = 1 + case cnt > maxCnt: + mode = append(mode[:0], c[i-1]) + maxCnt, cnt = cnt, 1 + default: + cnt = 1 + } + } + switch { + case cnt == maxCnt: + mode = append(mode, c[l-1]) + case cnt > maxCnt: + mode = append(mode[:0], c[l-1]) + maxCnt = cnt + } + if maxCnt == 1 || len(mode)*maxCnt == l && maxCnt != l { + return Float64Data{}, nil + } + return mode, nil +} diff --git a/common/maths/round.go b/common/maths/round.go new file mode 100755 index 0000000..ca96a83 --- /dev/null +++ b/common/maths/round.go @@ -0,0 +1,23 @@ +package maths + +import "math" + +func Round(input float64, places int) (rounded float64, err error) { + if math.IsNaN(input) { + return math.NaN(), NaNErr + } + sign := 1.0 + if input < 0 { + sign = -1 + input *= -1 + } + precision := math.Pow(10, float64(places)) + digit := input * precision + _, decimal := math.Modf(digit) + if decimal >= 0.5 { + rounded = math.Ceil(digit) + } else { + rounded = math.Floor(digit) + } + return rounded / precision * sign, nil +} diff --git a/common/maths/sum.go b/common/maths/sum.go new file mode 100755 index 0000000..09eadfb --- /dev/null +++ b/common/maths/sum.go @@ -0,0 +1,25 @@ +package maths + +import "math" + +func Sum(input Float64Data) (sum float64, err error) { + if input.Len() == 0 { + return math.NaN(), EmptyInputErr + } + for _, n := range input { + sum += n + } + return sum, nil +} + +func CumulativeInt64(a *int64, bs ...int64) *int64 { + var c int64 + for _, b := range bs { + c += b + } + if a == nil { + return &c + } + *a += c + return a +} diff --git a/common/maths/variance.go b/common/maths/variance.go new file mode 100755 index 0000000..eebc013 --- /dev/null +++ b/common/maths/variance.go @@ -0,0 +1,49 @@ +package maths + +import "math" + +// _variance finds the variance for both population and sample data +func _variance(input Float64Data, sample int) (variance float64, err error) { + if input.Len() == 0 { + return math.NaN(), EmptyInputErr + } + // Sum the square of the mean subtracted from each number + m, _ := Mean(input) + for _, n := range input { + variance += (n - m) * (n - m) + } + // When getting the mean of the squared differences + // "sample" will allow us to know if it's a sample + // or population and wether to subtract by one or not + return variance / float64((input.Len() - (1 * sample))), nil +} + +// PopulationVariance finds the amount of variance within a population +func PopulationVariance(input Float64Data) (pvar float64, err error) { + v, err := _variance(input, 0) + if err != nil { + return math.NaN(), err + } + return v, nil +} + +// CovariancePopulation computes covariance for entire population between two variables. +func CovariancePopulation(data1, data2 Float64Data) (float64, error) { + l1 := data1.Len() + l2 := data2.Len() + if l1 == 0 || l2 == 0 { + return math.NaN(), EmptyInputErr + } + if l1 != l2 { + return math.NaN(), SizeErr + } + m1, _ := Mean(data1) + m2, _ := Mean(data2) + var s float64 + for i := 0; i < l1; i++ { + delta1 := (data1.Get(i) - m1) + delta2 := (data2.Get(i) - m2) + s += delta1 * delta2 + } + return s / float64(l1), nil +} diff --git a/common/mdrsdb/mediaresourcedatabase.go b/common/mdrsdb/mediaresourcedatabase.go new file mode 100644 index 0000000..c2dee83 --- /dev/null +++ b/common/mdrsdb/mediaresourcedatabase.go @@ -0,0 +1,291 @@ +package mdrsdb + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strconv" + "time" + + "91porn-server/common" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/web/webg" +) + +type findMediaListReq struct { + IDs string `json:"ids"` + ProId string `json:"pro_id"` + Type string `json:"type"` + Page string `json:"page"` + Size string `json:"size"` + Timestamp string `json:"timestamp"` + Sign string `json:"sign"` + MediaType string `json:"filter[media_type],omitempty"` // 媒体类型 0视频 1图文 + Title string `json:"filter[title],omitempty"` + TerminalCdo string `json:"filter[terminal_cdo],omitempty"` + TagText string `json:"filter[tags_text],omitempty"` + HashId string `json:"filter[hash_id],omitempty"` + VloggerId string `json:"filter[vlogger_id],omitempty"` + VloggerUserId string `json:"filter[vlogger_user_id],omitempty"` + StartTime string `json:"filter[start_time],omitempty"` + EndTime string `json:"filter[end_time],omitempty"` + SortType string `json:"sort_type" form:"sort_type"` // 0-id倒序 1-id正序 + MaxTotal string `json:"max_total" form:"max_total"` // 最大列表总数 +} + +func (f *findMediaListReq) getParam() interface{} { + f.Sign = sign(*f) + m := make(map[string]string) + b, err := json.Marshal(f) + if err != nil { + return m + } + _ = json.Unmarshal(b, &m) + return m +} + +func FindMediaList(ctx context.Context, pageNumber, pageSize, vloggerUserId int64, title, tagText, hashId string, mVia, videoType int, syncType, terminalCdo string, startTime, endTime time.Time, sortType, maxTotal int) (mslist []MediaResource, count int64, err error) { + var msgData struct { + Data []MediaResource `json:"data"` + Pagination Pagination `json:"pagination"` + } + var retMsg msg + var errlog string + mt := "" + if videoType == 0 { + mt = "0" + } else if videoType == 2 { + mt = "1" + } + var req = findMediaListReq{ + ProId: getMerchat(), + Type: syncType, + Page: strconv.FormatInt(pageNumber, 10), + Size: strconv.FormatInt(pageSize, 10), + Timestamp: strconv.FormatInt(time.Now().Unix(), 10), + HashId: hashId, + Title: title, + TerminalCdo: terminalCdo, + TagText: tagText, + MediaType: mt, + SortType: strconv.Itoa(sortType), + MaxTotal: strconv.Itoa(maxTotal), + StartTime: strconv.FormatInt(startTime.Unix(), 10), + EndTime: strconv.FormatInt(endTime.Unix(), 10), + } + if vloggerUserId > 0 { + req.VloggerUserId = strconv.FormatInt(vloggerUserId, 10) + } + switch mVia { + case 0: + case 1: + req.VloggerId = "1," + case 2: + req.VloggerId = "0" + } + var url string + if videoType == 0 || videoType == 2 { + url = "/services/video/videos/v2" + } else { + url = "/services/short_video/videos/v2" + } + code, err := httputil.DefaultClientGetWithResp(&retMsg, common.BindUrl(getOrignUrl(), url), nil, req.getParam()) + if err != nil { + errlog = fmt.Sprintf("meadia resource FindMediaList http GetWithJResp error \nerror:%+v;", err) + log.Error(errlog) + err = errors.New(errlog) + return + } + if code != 200 { + errlog = fmt.Sprintf("meadia resource FindMediaList http code error \nerror:%+v;", err) + log.Error(errlog) + err = errors.New(errlog) + return + } + if retMsg.Code != 0 { + errlog = fmt.Sprintf("meadia resource FindMediaList retmsg code error \nerror:%+v;", err) + log.Error(errlog) + err = errors.New(errlog) + return + } + b, err := json.Marshal(retMsg.Data) + if err != nil { + errlog := fmt.Sprintf("meadia resource FindMediaList Marshal fail \nerror:%+v;", err) + log.Error(errlog) + err = errors.New(errlog) + return + } + if err = json.Unmarshal(b, &msgData); err != nil { + errlog := fmt.Sprintf("meadia resource FindMediaList Unmarshal fail \nerror:%+v;", err) + log.Error(errlog) + err = errors.New(errlog) + return + } + mslist = msgData.Data + count = msgData.Pagination.Count + return +} + +func FindMediaListWithIDs(ctx context.Context, ids []int64, videoType int) (mslist []MediaResource, err error) { + var msgData struct { + Data []MediaResource `json:"data"` + Pagination Pagination `json:"pagination"` + } + var retMsg msg + var errlog string + var req = findMediaListReq{ + ProId: getMerchat(), + Type: "all", + IDs: sliCovertStr(ids), + Timestamp: strconv.FormatInt(time.Now().Unix(), 10), + Size: strconv.Itoa(len(ids)), + Page: "1", + } + var url string + if videoType == 0 || videoType == 2 { + url = "/services/video/videos" + } else { + url = "/services/short_video/videos" + } + code, err := httputil.DefaultClientGetWithResp(&retMsg, common.BindUrl(getOrignUrl(), url), nil, req.getParam()) + if err != nil { + errlog = fmt.Sprintf("meadia resource FindMediaList http GetWithJResp error \nerror:%+v;", err) + log.Error(errlog) + err = errors.New(errlog) + return + } + if code != 200 { + errlog = fmt.Sprintf("meadia resource FindMediaList http code error \nerror:%+v;code:%v", err, code) + log.Error(errlog) + err = errors.New(errlog) + return + } + if retMsg.Code != 0 { + errlog = fmt.Sprintf("meadia resource FindMediaList retmsg code error \nerror:%+v;", err) + log.Error(errlog) + err = errors.New(errlog) + return + } + b, err := json.Marshal(retMsg.Data) + if err != nil { + errlog := fmt.Sprintf("meadia resource FindMediaList Marshal fail \nerror:%+v;", err) + log.Error(errlog) + err = errors.New(errlog) + return + } + if err = json.Unmarshal(b, &msgData); err != nil { + errlog := fmt.Sprintf("meadia resource FindMediaList Unmarshal fail \nerror:%+v;", err) + log.Error(errlog) + err = errors.New(errlog) + return + } + mslist = msgData.Data + return +} + +type useMediaReq struct { + IDs string `json:"ids"` + ProId string `json:"pro_id"` + Timestamp string `json:"timestamp"` + Sign string `json:"sign"` +} + +func (this *useMediaReq) getParam() interface{} { + this.Sign = sign(*this) + b, _ := json.Marshal(this) + return b +} + +func UseMedia(ctx context.Context, ids []int64, videoType int) error { + var retMsg msg + var errlog string + var p = useMediaReq{ + ProId: getMerchat(), + IDs: sliCovertStr(ids), + Timestamp: strconv.FormatInt(time.Now().Unix(), 10), + } + var url string + if videoType == 0 || videoType == 2 { + url = "/services/video/used" + } else { + url = "/services/short_video/used" + } + fmt.Println("-------url", getOrignUrl()+url) + code, err := httputil.DefaultClientPostJsonWithResp(&retMsg, common.BindUrl(getOrignUrl(), url), nil, p.getParam()) + if err != nil { + errlog = fmt.Sprintf("meadia resource FindMediaList http POSTJsonWithJResp error \nerror:%+v;", err) + log.Error(errlog) + return errors.New(errlog) + } + if code != 200 { + errlog = fmt.Sprintf("meadia resource FindMediaList http code error \nerror:%+v;", err) + log.Error(errlog) + return errors.New(errlog) + } + if retMsg.Code != 0 { + errlog = fmt.Sprintf("meadia resource FindMediaList retmsg code error \nerror:%+v;", err) + log.Error(errlog) + return errors.New(errlog) + } + return nil +} + +type ignoreMediaReq struct { + IDs string `json:"ids"` + ProId string `json:"pro_id"` + Timestamp string `json:"timestamp"` + Sign string `json:"sign"` +} + +func (this *ignoreMediaReq) getParam() interface{} { + this.Sign = sign(*this) + b, _ := json.Marshal(this) + return b +} + +func IgnoreMedia(ctx context.Context, ids []int64, videoType int) error { + var retMsg msg + var errlog string + var p = ignoreMediaReq{ + ProId: getMerchat(), + IDs: sliCovertStr(ids), + Timestamp: strconv.FormatInt(time.Now().Unix(), 10), + } + var url string + if videoType == 0 || videoType == 2 { + url = "/services/video/ignored" + } else { + url = "/services/short_video/ignored" + } + code, err := httputil.DefaultClientPostJsonWithResp(&retMsg, common.BindUrl(getOrignUrl(), url), nil, p.getParam()) + if err != nil { + errlog = fmt.Sprintf("meadia resource FindMediaList http POSTJsonWithJResp error \nerror:%+v;", err) + log.Error(errlog) + return errors.New(errlog) + } + if code != 200 { + errlog = fmt.Sprintf("meadia resource FindMediaList http code error \nerror:%+v;code %d", err, code) + log.Error(errlog) + return errors.New(errlog) + } + if retMsg.Code != 0 { + errlog = fmt.Sprintf("meadia resource FindMediaList retmsg code error \nerror:%+v;res:%+v", err, retMsg) + log.Error(errlog) + return errors.New(errlog) + } + return nil +} + +func getMerchat() string { + return webg.Conf.MediaResourceDBCfg.Merchat +} + +func getSecret() string { + return webg.Conf.MediaResourceDBCfg.Secret +} + +func getOrignUrl() string { + return webg.Conf.MediaResourceDBCfg.BaseUrl +} diff --git a/common/mdrsdb/type.go b/common/mdrsdb/type.go new file mode 100644 index 0000000..c9b029e --- /dev/null +++ b/common/mdrsdb/type.go @@ -0,0 +1,128 @@ +package mdrsdb + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "encoding/json" + "sort" + "strconv" + "strings" +) + +type MediaResource struct { + Id int64 `json:"id"` + HashId string `json:"hash_id"` + // SetsId int `json:"sets_id"` + // Episode int `json:"episode"` + MediaType int `json:"media_type"` + Title string `json:"title"` + Content string `json:"content"` + VerticalCover string `json:"vertical_cover"` + CoverImage string `json:"cover_image"` + CoverGif string `json:"cover_gif"` + Pics string `json:"pics"` + Gifs string `json:"gifs"` + Src480 string `json:"src_480"` + Src720 string `json:"src_720"` + Src1080 string `json:"src_1080"` + FileSize480 int64 `json:"file_size_480"` + FileSize720 int64 `json:"file_size_720"` + FileSize1080 int64 `json:"file_size_1080"` + Width int `json:"width"` + Height int `json:"height"` + Length uint `json:"length"` + PreviewSrc string `json:"preview_src"` + PreviewLength int `json:"pre view_length"` + // IsVertical int `json:"is_vertical"` + Tags string `json:"tags"` + Points string `json:"points"` + // QualityScore int `json:"quality_score"` + // IsHighQuality int `json:"is_high_quality"` + LastHeatTime interface{} `json:"last_heat_time"` + Status int `json:"status"` + // CreateTime string `json:"create_time"` + // UpdateTime string `json:"update_time"` + EditBy string `json:"edit_by"` + TerminalCdo string `json:"terminal_cdo"` + Used int64 `json:"used"` + Short []interface{} `json:"short"` + TagsText string `json:"tags_text"` + PointsText string `json:"points_text"` + VloggerUserId uint64 `json:"vlogger_user_id"` //up主 + TagList []string `json:"tagList"` //tag列表 + SaleCoin int64 `json:"sale_coin"` //售卖金币 + RichText string `json:"rich_text"` // 富文本内容 + PreviewStart int `json:"preview_start"` +} + +type Pagination struct { + IsLastPage int `json:"isPastPage"` + Page int `json:"page"` + Size int `json:"size"` + Count int64 `json:"count"` +} + +type msg struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` +} + +func sign(p interface{}) string { + m := make(map[string]interface{}) + rawData, _ := json.Marshal(p) + _ = json.Unmarshal(rawData, &m) + keyStrSli := make([]string, 0) + for k := range m { + if k == "sign" || m[k] == "" { + continue + } + keyStrSli = append(keyStrSli, k) + } + sort.Strings(keyStrSli) + var buf bytes.Buffer + for k := range keyStrSli { + buf.WriteString(keyStrSli[k]) + buf.WriteString("=") + buf.WriteString(allToString(m[keyStrSli[k]])) + buf.WriteString("&") + } + buf.WriteString("secret=" + getSecret()) + s := buf.String() + md5Str := strings.ToUpper(md5Sign(s)) + return md5Str +} + +// md5签名 +func md5Sign(buf string) string { + md5Ctx := md5.New() + md5Ctx.Write([]byte(buf)) + cipherStr := md5Ctx.Sum(nil) + nsign := hex.EncodeToString(cipherStr) + return nsign +} + +func allToString(p interface{}) string { + switch p := p.(type) { + case int, int64: + return strconv.FormatInt(p.(int64), 10) + case float32, float64: + return strconv.FormatFloat(p.(float64), 'f', 2, 64) + case string: + return p + default: + } + return "" +} + +func sliCovertStr(in []int64) string { + var buf bytes.Buffer + for i := range in { + if i > 0 { + buf.WriteString(",") + } + buf.WriteString(strconv.FormatInt(in[i], 10)) + } + return buf.String() +} diff --git a/common/media/h265_test.go b/common/media/h265_test.go new file mode 100644 index 0000000..a38ce1d --- /dev/null +++ b/common/media/h265_test.go @@ -0,0 +1,45 @@ +package mediaService + +import "testing" + +func TestShouldUpdateH265URL(t *testing.T) { + tests := []struct { + name string + current string + incoming string + want bool + }{ + { + name: "empty incoming does not erase existing URL", + current: "https://cdn.example.com/existing.m3u8", + incoming: "", + want: false, + }, + { + name: "whitespace incoming does not erase existing URL", + current: "https://cdn.example.com/existing.m3u8", + incoming: " ", + want: false, + }, + { + name: "same URL does not update", + current: "https://cdn.example.com/hevc.m3u8", + incoming: " https://cdn.example.com/hevc.m3u8 ", + want: false, + }, + { + name: "new non-empty URL updates", + current: "", + incoming: "https://cdn.example.com/hevc.m3u8", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := shouldUpdateH265URL(tt.current, tt.incoming); got != tt.want { + t.Fatalf("shouldUpdateH265URL() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/common/media/sync.go b/common/media/sync.go new file mode 100644 index 0000000..bf8ff75 --- /dev/null +++ b/common/media/sync.go @@ -0,0 +1,111 @@ +package mediaService + +import ( + "91porn-server/common/log" + "91porn-server/common/mediaCenter" + "91porn-server/common/redis" + v10 "91porn-server/common/v10" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/mediatagmod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +type mediaSync struct { + rd *redis.Client + Ms *mediaCenter.MediaCenterService + Worker *worker +} + +func NewMediaSync(rd *redis.Client, ms *mediaCenter.MediaCenterService) *mediaSync { + return &mediaSync{ + rd: rd, + Ms: ms, + Worker: w, //最多同时N个协程处理任务 + } +} + +func (m *mediaSync) SyncAll(admin string) { + // 判断当前是否有正在同步的任务,有的话就不执行 + key := "MediaSyncAll" + + has, err := m.rd.SetNX(key, 1, time.Minute*10) + if err != nil { + log.Error("SyncAll setNx fail", log.E(err)) + return + } + if !has { + return + } + // 释放锁 + defer m.rd.Del(key) + // 获取所有订阅未完结的动漫 + filter := bson.M{ + "mediaCenterId": bson.M{"$gt": 0}, + "updateStatus": 1, //未完结的状态 + "isDelete": false, + } + sort := bson.D{{"_id", -1}, {"createdAt", -1}} + var skip int64 + var limit int64 = 500 + for { + list, hasNext, err := mediamod.GetList(filter, skip, limit, sort) + if err != nil { + return + } + for _, v := range list { + media := v + m.Worker.Exec(func() { + err = m.SyncMediaData(media) + if err != nil { + log.Error("SyncMediaData fail", log.Any("mediaId", media.ID), log.E(err)) + return + } + }) + } + skip += limit + if !hasNext { + break + } + } + +} + +func (m *mediaSync) OnSyncTimeChange(mediaId primitive.ObjectID, syncTime int64) { + if mediaId.IsZero() || syncTime <= 0 { + return + } + // 修改下一次需要同步的时间点位 + updataData := make(map[string]interface{}) + updataData["syncTime"] = syncTime + updataData["updateTime"] = time.Now() + mediamod.UpdateByID(nil, mediaId, updataData) +} + +// HandleTag 处理标签 +func HandleTag(tagname, dimension string, dimensionType, kind int) (tagID primitive.ObjectID, suc bool) { + tMod := mediatagmod.MediaTag{} + tagname = v10.ExtractPureChar(tagname) + tagInfo, err := mediatagmod.FindOneTagByFilter(bson.M{ + "name": tagname, + "kind": kind, + "dimension": dimension, + "type": dimensionType, + }) + if err == nil && !tagInfo.ID.IsZero() { + return tagInfo.ID, true + } + tMod.Name = tagname + tMod.Dimension = dimension + tMod.Kind = kind + tMod.Type = dimensionType + tMod.Active = true + tMod.CreatedAt = time.Now() + tMod.UpdateTime = time.Now() + id, err := mediatagmod.Insert(nil, tMod) + if err != nil { + return primitive.NilObjectID, false + } + return id, true +} diff --git a/common/media/syncMedia.go b/common/media/syncMedia.go new file mode 100644 index 0000000..bdb382b --- /dev/null +++ b/common/media/syncMedia.go @@ -0,0 +1,202 @@ +package mediaService + +import ( + "91porn-server/common/log" + "91porn-server/common/mediaCenter" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + "fmt" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// SyncMediaData 同步acg动漫 +func (m *mediaSync) SyncMediaData(media *mediamod.Media) (err error) { + if media == nil { + return + } + // 加锁,防止多个同步出错 + // 判断当前是否有正在同步的任务,有的话就不执行 + key := fmt.Sprintf("MediaSync:%v", media.MediaCenterId) + has, err := m.rd.SetNX(key, 1, time.Minute*10) + if err != nil { + log.Error("SyncAll setNx fail", log.E(err)) + return + } + if !has { + log.Info("【ACG同步任务】正在进行", log.Any("task", key)) + return + } + // 释放锁 + defer m.rd.Del(key) + log.Info("【ACG同步任务】开始", log.Any("mediaId", media.ID), log.Any("mediaCenterId", media.MediaCenterId)) + // 同步数据 + data, err := m.Ms.MediaSync(&mediaCenter.MediaSyncReq{ + Id: media.MediaCenterId, + SyncTime: media.SyncTime, + }) + if err != nil { + log.Error("SyncMediaData call mediaSync Fail", log.Any("id", media.MediaCenterId), log.E(err)) + return err + } + if data.IsDelete { + // 数据已经删除,不需要同步 + log.Info("【ACG同步任务】 数据在媒资库已经删除,不需要同步", log.Any("mediaId", media.ID), log.Any("mediaCenterId", media.MediaCenterId)) + return + } + err = m.OnSyncMediaData(media.ID, data.Media) + if err != nil { + log.Error("SyncMediaData call OnSyncMediaData Fail", log.Any("id", media.MediaCenterId), log.E(err)) + return err + } + newSyncTime, isAdd, err := m.OnSyncMediaContentData(media, data.MediaContentList) + if err != nil { + return + } + err = OnMediaContentChange(media.ID, media.MediaType, isAdd) + if err != nil { + log.Error("【ACG同步任务】 OnMediaContentChange Fail", log.Any("mediaId", media.ID), log.E(err)) + return err + } + // 修改下一次需要同步的时间点位 + m.OnSyncTimeChange(media.ID, newSyncTime) + log.Info("【ACG同步任务】完成", log.Any("mediaId", media.ID), log.Any("mediaCenterId", media.MediaCenterId)) + return nil +} + +func (m *mediaSync) OnSyncMediaContentData(media *mediamod.Media, mediaContentDataList []mediaCenter.MediaContent) (int64, bool, error) { + isAdd := false + syncTime := media.SyncTime + updatedIDs := make([]primitive.ObjectID, 0) + + if len(mediaContentDataList) == 0 { + // 已经没有需要同步的数据了,直接返回 + return syncTime, false, nil + } + log.Info("【ACG同步任务】获取到需要同步的子集", + log.Any("mediaId", media.ID), + log.Any("mediaCenterId", media.MediaCenterId), + log.Any("子集数量", len(mediaContentDataList))) + for _, mediaContentData := range mediaContentDataList { + h265URL := normalizeH265URL(mediaContentData.H265Url) + // 更改下一次同步的时间点 + if mediaContentData.UpdateTime > syncTime { + syncTime = mediaContentData.UpdateTime + } + log.Info("【ACG同步任务】同步子集", + log.Any("mediaId", media.ID), + log.Any("mediaCenterId", media.MediaCenterId), + log.Any("子集ID", mediaContentData.ID)) + // 判断是否已经存在该记录 + mediaContent, err := mediacontentmod.GetInfoByHashId(media.ID, mediaContentData.HashId) + if err != nil && err != mediacontentmod.MediaContentNotFound { + log.Error("【ACG同步任务】同步子集失败", + log.Any("mediaId", media.ID), + log.Any("mediaCenterId", media.MediaCenterId), + log.Any("子集ID", mediaContentData.ID), + log.Any("err", err)) + return 0, false, err + } + + // TODO 后续同步,只做追加,不对之前的子集数据进行更新,避免数据出错 + if err == mediacontentmod.MediaContentNotFound { + isAdd = true + // 不存在则入库 + mediaContent = mediacontentmod.MediaContent{ + MediaID: media.ID, + MediaType: media.MediaType, + EpisodeNumber: mediaContentData.EpisodeNumber, + ListenPermission: 0, // 收听权限 0:会员 1:金币购买 2:免费 + Name: mediaContentData.Title, // 章节名 + Text: mediaContentData.Text, // 内容(小说专用) + Author: media.Anchor, // 作者 + Height: int(mediaContentData.Height), // 高 + Weight: int(mediaContentData.Width), // 宽 + MediaSize: int64(mediaContentData.MediaSize), // 资源大小 + PlayTime: mediaContentData.PlayTime, // 影片长度 + Status: 1, // 状态 0:转码中 1:转码成功 2:转码失败, 视频资源使用 + UpdatedAct: "系统自动同步", // 操作用户账号 + CreatedAt: time.Now(), // 文档创建时间 + UpdateTime: time.Now(), // 文档更新时间 + IsActive: true, // 同步过来的默认上架 + HashId: mediaContentData.HashId, // 在媒资库中的唯一标识 + VideoUrl: mediaContentData.M3u8Url, + H265Url: h265URL, + AudioUrl: mediaContentData.AudioUrl, + } + if media.MediaType == mediamod.MediaTypeDrama { + mediaContent.Price = int64(mediaContentData.SaleCoin) + mediaContent.ListenPermission = 1 + if mediaContentData.SaleCoin == 0 { + mediaContent.ListenPermission = 2 + } + } + if strings.TrimSpace(mediaContentData.Urls) != "" { + mediaContent.UrlSet = strings.Split(mediaContentData.Urls, ",") + } else { + mediaContent.UrlSet = []string{} + } + _, err = mediacontentmod.Insert(nil, mediaContent) + if err != nil { + log.Error("【ACG同步任务】同步子集失败", + log.Any("mediaId", media.ID), + log.Any("mediaCenterId", media.MediaCenterId), + log.Any("子集ID", mediaContentData.ID), + log.Any("err", err)) + return 0, false, err + } + } else if shouldUpdateH265URL(mediaContent.H265Url, h265URL) { + // 历史子集仍保持只追加策略,仅允许同步 H.265 地址并清理详情缓存。 + _, err = mediacontentmod.UpdateByID(nil, mediaContent.ID, map[string]interface{}{ + "h265Url": h265URL, + "updateTime": time.Now(), + }) + if err != nil { + log.Error("【ACG同步任务】更新H.265地址失败", + log.Any("mediaId", media.ID), + log.Any("mediaContentId", mediaContent.ID), + log.Any("子集ID", mediaContentData.ID), + log.Any("err", err)) + return 0, false, err + } + updatedIDs = append(updatedIDs, mediaContent.ID) + } + } + return syncTime, isAdd, nil +} + +func normalizeH265URL(raw string) string { + return strings.TrimSpace(raw) +} + +func shouldUpdateH265URL(current, incoming string) bool { + incoming = normalizeH265URL(incoming) + return incoming != "" && normalizeH265URL(current) != incoming +} + +func (m *mediaSync) OnSyncMediaData(mediaId primitive.ObjectID, mediaData *mediaCenter.Media) (err error) { + + //修改数据(不修改这个价格了,避免跟运营自己修改的价格冲突,只做部分内容的修改) + updateData := make(map[string]interface{}) + //updateData["title"] = data.Media.Title + //updateData["horizontalCover"] = data.Media.HorizontalCover + //updateData["verticalCover"] = data.Media.VerticalCover + //updateData["summary"] = data.Media.Summary + // TODO 后续更新只 对集数跟连载状态进行修改 + updateData["totalEpisode"] = int(mediaData.TotalEpisode) + updateData["updateStatus"] = mediaData.UpdateStatus + if mediaData.UpdateStatus == 0 { + // 直接是已完结 + updateData["updateStatus"] = 2 + } + //updateData["anchor"] = data.Media.Anchor + updateData["updateTime"] = time.Now() + + _, err = mediamod.UpdateByID(nil, mediaId, updateData) + if err != nil { + return + } + return +} diff --git a/common/media/useMedia.go b/common/media/useMedia.go new file mode 100644 index 0000000..d4900f2 --- /dev/null +++ b/common/media/useMedia.go @@ -0,0 +1,502 @@ +package mediaService + +import ( + "91porn-server/common/log" + "91porn-server/common/mediaCenter" + sli "91porn-server/common/slice" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/modulesectionmod" + "91porn-server/models/v/sensitivewordmod" + "fmt" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// UseMediaData 同步acg动漫 +func (m *mediaSync) UseMediaData(admin string, id uint, dimension string, dimensionType, kind int, sectionId primitive.ObjectID) (err error) { + // 加锁,防止多个同步出错 + // 判断当前是否有正在同步的任务,有的话就不执行 + key := fmt.Sprintf("MediaSync:%v", id) + has, err := m.rd.SetNX(key, 1, time.Minute*10) + if err != nil { + log.Error("SyncAll setNx fail", log.E(err)) + return + } + if !has { + log.Info("【ACG订阅】该任务已经在进行", log.Any("task", key)) + return + } + // 释放锁 + defer m.rd.Del(key) + log.Info("【ACG订阅】开始", + log.Any("admin", admin), + log.Any("mediaCenterId", id), + log.Any("dimension", dimension), + log.Any("dimensionType", dimensionType), + log.Any("kind", kind), + log.Any("sectionId", sectionId), + ) + var media mediamod.Media + var module moduleconfmod.ModuleConf + var section modulesectionmod.Section + // 获取专题 + if !sectionId.IsZero() { + section, _ = modulesectionmod.GetBySectionID(sectionId) + module, _ = moduleconfmod.GetByID(section.SubModuleID) + } + + // 同步数据 + data, err := m.Ms.MediaSync(&mediaCenter.MediaSyncReq{ + Id: id, + SyncTime: 0, + }) + if err != nil { + log.Error("【ACG订阅】 call mediaSync Fail", log.Any("id", id), log.E(err)) + return err + } + if data.IsDelete { + // 数据已经删除,不需要同步 + log.Info("【ACG订阅】 数据在媒资库已经删除,不需要同步", log.Any("mediaId", media.ID), log.Any("mediaCenterId", media.MediaCenterId)) + return + } + log.Info("【ACG订阅】获取到media数据", + log.Any("admin", admin), + log.Any("mediaCenterId", id), + log.Any("mediaData", data.Media), + ) + media, err = m.OnUseMediaData(admin, module, section, dimension, dimensionType, kind, data.Media) + if err != nil { + log.Error("【ACG订阅】 OnUseMediaData Fail", log.Any("id", id), log.E(err)) + return err + } + log.Info("【ACG订阅】处理media数据成功", + log.Any("admin", admin), + log.Any("mediaCenterId", id), + log.Any("media objId", media.ID), + ) + if len(data.MediaContentList) == 0 { + // 已经没有需要同步的数据了,直接返回 + return + } + + newSyncTime, isAdd, err := m.OnUseMediaContentData(admin, media, data.MediaContentList) + if err != nil { + log.Error("【ACG订阅】 OnUseMediaContentData Fail", log.Any("id", id), log.E(err)) + return err + } + err = OnMediaContentChange(media.ID, media.MediaType, isAdd) + if err != nil { + log.Error("【ACG订阅】 OnMediaContentChange Fail", log.Any("id", id), log.E(err)) + return err + } + m.OnSyncTimeChange(media.ID, newSyncTime) + + log.Info("【ACG订阅】完成", + log.Any("admin", admin), + log.Any("mediaCenterId", id), + log.Any("dimension", dimension), + log.Any("dimensionType", dimensionType), + log.Any("kind", kind), + log.Any("sectionId", sectionId), + ) + return nil +} + +func OnMediaContentChange(mediaId primitive.ObjectID, mediaType string, isAdd bool) (err error) { + // 获取当前最小的章节,重新update + defaultContent := mediamod.DefaultContent{} + if mediaType == mediamod.MediaTypeImage { + contentList, _, _, err := mediacontentmod.GetList(bson.M{"mediaId": mediaId, "isActive": true, "isDelete": false}, 0, 1, bson.D{{Key: "episodeNumber", Value: 1}, {Key: "createdAt", Value: 1}}) + if err != nil { + return err + } + if len(contentList) > 0 { + defaultContent.Id = contentList[0].ID + defaultContent.Name = contentList[0].Name + defaultContent.UrlSet = contentList[0].UrlSet + defaultContent.Cover = contentList[0].Cover + } + } + // 子集总数 + count, err := mediacontentmod.QueryAllCount(bson.M{"mediaId": mediaId, "isActive": true, "isDelete": false}) + if err != nil { + return err + } + // 获取子集免费章节总数 + freeCount, err := mediacontentmod.QueryAllCount(bson.M{"mediaId": mediaId, "listenPermission": 2, "isActive": true, "isDelete": false}) + if err != nil { + return err + } + contentsPrice, err := mediacontentmod.QueryContentsPrice(mediaId) + if err != nil { + return + } + updateData := bson.M{ + "currentEpisode": count, + "contentsPrice": contentsPrice, + "freeEpisode": freeCount, + "updateTime": time.Now(), + "defaultContent": defaultContent, // 默认展示的子集内容,冗余一个字段,减少一次查询 + } + if isAdd { + now := time.Now() + updateData["contentUpdateTime"] = now + updateData["latestPublishedAt"] = now + } + //修改当前父级的记录 + _, err = mediamod.UpdateByID(nil, mediaId, updateData) + + if err != nil { + return err + } + return nil +} + +func (m *mediaSync) OnUseMediaData(admin string, module moduleconfmod.ModuleConf, section modulesectionmod.Section, dimension string, dimensionType, kind int, mediaData *mediaCenter.Media) (media mediamod.Media, err error) { + if mediaData == nil { + return + } + dimension, dimensionType, kind = normalizeImportedTagClassification(mediaData.MediaType, dimension, dimensionType, kind) + // 查询是否存在 + media, err = mediamod.GetInfoByMediaCenterId(mediaData.ID) + if err != nil && err != mediamod.MediaNotFound { + return + } + var exists bool + if err == nil { + exists = true + } else if err == mediamod.MediaNotFound && mediaData.DataVersion == 0 { + exists = false + } else if err == mediamod.MediaNotFound && mediaData.DataVersion > 0 { + // 如果不存在,则需要判断是否之前的存在 + // 通过标题获取数据,看看是否存在 + oldMedia, err := mediamod.GetInfoByTitle(mediaData.Title) + if err != nil && err != mediamod.MediaNotFound { + return media, err + } + if err == nil { + // 如果之前就存在 + media = oldMedia + exists = true + } + } else { + // 数据库查询错误,直接返回 + return + } + + if !exists { + // 直接写入 + media = mediamod.Media{ + Title: mediaData.Title, + HorizontalCover: mediaData.HorizontalCover, + VerticalCover: mediaData.VerticalCover, + Summary: mediaData.Summary, + TotalEpisode: int(mediaData.TotalEpisode), // 总集数 + UpdateStatus: mediaData.UpdateStatus, // 更新状态 + PermissionIconHide: false, // 售卖类型标识隐藏 + Permission: 0, // 收听权限 0:会员 1:金币购买 2:免费 + Direction: 0, // 排版方向 0横 1竖(冗余字段) + FreeTime: 0, // 免费试看时长 单位 s + Anchor: mediaData.Anchor, // 作者 + UpdatedAct: admin, // 操作用户账号 + CreatedAt: time.Now(), // 文档创建时间 + UpdateTime: time.Now(), // 文档更新时间 + MediaCenterId: mediaData.ID, // 在媒资库中的唯一标识 + Status: 1, // 默认上架 + MID: module.ID, // 模块ID + ModuleName: module.ModuleName, // 模块名称 + SID: section.ID, + SectionName: section.SectionName, + } + if kind > 0 { + media.Kind = kind + } + + switch mediaData.MediaType { + case 1: + // 动漫 + media.MediaType = "video" + case 2: + // 漫画 + media.MediaType = "image" + case 3: + // 文本小说 + media.MediaType = "text" + case 4: + // 有声小说 + media.MediaType = "text" + media.MediaSubType = 1 + case 5: + // 短剧 + media.MediaType = mediamod.MediaTypeDrama + media.MediaSubType = 0 + media.Kind = mediamod.Theater + media.Permission = 1 + media.Price = 0 + media.Status = 0 + } + if media.UpdateStatus == 0 { + // 直接是已完结 + media.UpdateStatus = 2 + } + if strings.TrimSpace(mediaData.TagsText) != "" && kind > 0 && dimension != "" && dimensionType > 0 { + tagList := strings.Split(mediaData.TagsText, ",") + tagIds := make([]primitive.ObjectID, 0, len(tagList)) + for _, tagName := range tagList { + tid, suc := HandleTag(tagName, dimension, dimensionType, kind) + if !suc { + continue + } + tagIds = append(tagIds, tid) + } + media.Tags = tagIds + } + + // 敏感词命中:强制下架(Status=0),命中详情写 log,便于管理员从后台日志复查 + // 此接口为异步 worker 写库,无法回传 handler,故只 log + 强制状态 + terms := sensitivewordmod.LoadEnabledTerms() + titleHits := sensitivewordmod.MatchHits(media.Title, terms) + summaryHits := sensitivewordmod.MatchHits(media.Summary, terms) + if len(titleHits) > 0 || len(summaryHits) > 0 { + media.Status = 0 + log.Warn("OnUseMediaData sensitive hit, forced offline", + log.Any("mediaCenterId", mediaData.ID), + log.Any("title", media.Title), + log.Any("detail", sensitivewordmod.FormatHitDetail(titleHits, summaryHits, nil))) + } + + objId, err := mediamod.Insert(nil, media) + if err != nil { + return media, err + } + media.ID = objId + return media, nil + } + + //修改数据(不修改这个价格了,避免跟运营自己修改的价格冲突,只做部分内容的修改) + updateData := make(map[string]interface{}) + updateData["title"] = mediaData.Title + updateData["horizontalCover"] = mediaData.HorizontalCover + updateData["verticalCover"] = mediaData.VerticalCover + updateData["summary"] = mediaData.Summary + updateData["totalEpisode"] = int(mediaData.TotalEpisode) + updateData["updateStatus"] = mediaData.UpdateStatus + if media.UpdateStatus == 0 { + // 直接是已完结 + updateData["updateStatus"] = 2 + } + updateData["anchor"] = mediaData.Anchor + updateData["mediaCenterId"] = mediaData.ID + updateData["updateTime"] = time.Now() + if kind > 0 { + updateData["kind"] = kind + } + if strings.TrimSpace(mediaData.TagsText) != "" && kind > 0 && dimension != "" && dimensionType > 0 { + tagList := strings.Split(mediaData.TagsText, ",") + tagIds := prepareImportedTagIDs(mediaData.MediaType, media.Tags) + for _, tagName := range tagList { + tid, suc := HandleTag(tagName, dimension, dimensionType, kind) + if !suc { + continue + } + tagIds = append(tagIds, tid) + } + // 去重 + updateData["tags"] = sli.RemoveRepObjectID(tagIds) + } + if !section.ID.IsZero() { + updateData["mId"] = module.ID + updateData["moduleName"] = module.ModuleName + updateData["sectionName"] = section.SectionName + updateData["sId"] = section.ID + } + _, err = mediamod.UpdateByID(nil, media.ID, updateData) + if err != nil { + return media, err + } + + return +} + +func normalizeImportedTagClassification(mediaType int, dimension string, dimensionType, kind int) (string, int, int) { + if mediaType == 5 { + return "剧场", 5, mediamod.Theater + } + return dimension, dimensionType, kind +} + +func prepareImportedTagIDs(mediaType int, current []primitive.ObjectID) []primitive.ObjectID { + if mediaType == 5 { + return make([]primitive.ObjectID, 0, len(current)) + } + return current +} + +func (m *mediaSync) OnUseMediaContentData(admin string, media mediamod.Media, mediaContentDataList []mediaCenter.MediaContent) (int64, bool, error) { + syncTime := media.SyncTime + if len(mediaContentDataList) == 0 { + return syncTime, false, nil + } + isAdd := false + updatedIDs := make([]primitive.ObjectID, 0, len(mediaContentDataList)) + log.Info("【ACG订阅】获取到需要同步的子集", + log.Any("mediaId", media.ID), + log.Any("mediaCenterId", media.MediaCenterId), + log.Any("子集数量", len(mediaContentDataList))) + for _, mediaContentData := range mediaContentDataList { + h265URL := normalizeH265URL(mediaContentData.H265Url) + // 更改下一次同步的时间点 + if mediaContentData.UpdateTime > syncTime { + syncTime = mediaContentData.UpdateTime + } + log.Info("【ACG订阅】同步子集", + log.Any("mediaId", media.ID), + log.Any("mediaCenterId", media.MediaCenterId), + log.Any("子集ID", mediaContentData.ID)) + // 判断是否已经存在该记录 + mediaContent, err := mediacontentmod.GetInfoByHashId(media.ID, mediaContentData.HashId) + if err != nil && err != mediacontentmod.MediaContentNotFound { + log.Error("【ACG订阅】同步子集失败", + log.Any("mediaId", media.ID), + log.Any("mediaCenterId", media.MediaCenterId), + log.Any("子集ID", mediaContentData.ID), + log.Any("err", err)) + return 0, false, err + } + + var exists bool + if err == nil { + exists = true + } else if err == mediacontentmod.MediaContentNotFound && mediaContentData.DataVersion == 0 { + exists = false + } else if err == mediacontentmod.MediaContentNotFound && mediaContentData.DataVersion > 0 { + // 如果不存在,则需要判断是否之前的存在 + // 通过标题获取数据,看看是否存在 + oldMediaContent, err := mediacontentmod.GetInfoByMediaIdAndName(media.ID, mediaContentData.Title) + if err != nil && err != mediacontentmod.MediaContentNotFound { + log.Error("【ACG订阅】同步子集失败", + log.Any("mediaId", media.ID), + log.Any("mediaCenterId", media.MediaCenterId), + log.Any("子集ID", mediaContentData.ID), + log.Any("err", err)) + return 0, false, err + } + if err == nil { + // 如果之前就存在 + mediaContent = oldMediaContent + exists = true + } + } else { + // 数据库查询错误,直接返回 + log.Error("【ACG订阅】同步子集失败", + log.Any("mediaId", media.ID), + log.Any("mediaCenterId", media.MediaCenterId), + log.Any("子集ID", mediaContentData.ID), + log.Any("err", err)) + return 0, false, err + } + + if !exists { + isAdd = true + // 不存在则入库 + mediaContent = mediacontentmod.MediaContent{ + MediaID: media.ID, + MediaType: media.MediaType, + EpisodeNumber: mediaContentData.EpisodeNumber, + ListenPermission: 0, // 收听权限 0:会员 1:金币购买 2:免费 + Name: mediaContentData.Title, // 章节名 + Text: mediaContentData.Text, // 内容(小说专用) + Author: media.Anchor, // 作者 + Height: int(mediaContentData.Height), // 高 + Weight: int(mediaContentData.Width), // 宽 + MediaSize: int64(mediaContentData.MediaSize), // 资源大小 + PlayTime: mediaContentData.PlayTime, // 影片长度 + Status: 1, // 状态 0:转码中 1:转码成功 2:转码失败, 视频资源使用 + IsActive: false, // 默认不上架 + UpdatedAct: admin, // 操作用户账号 + CreatedAt: time.Now(), // 文档创建时间 + UpdateTime: time.Now(), // 文档更新时间 + HashId: mediaContentData.HashId, // 在媒资库中的唯一标识 + VideoUrl: mediaContentData.M3u8Url, + H265Url: h265URL, + AudioUrl: mediaContentData.AudioUrl, + } + if media.MediaType == mediamod.MediaTypeDrama { + mediaContent.Price = int64(mediaContentData.SaleCoin) + mediaContent.ListenPermission = 1 + if mediaContentData.SaleCoin == 0 { + mediaContent.ListenPermission = 2 + } + } + if strings.TrimSpace(mediaContentData.Urls) != "" { + mediaContent.UrlSet = strings.Split(mediaContentData.Urls, ",") + } else { + mediaContent.UrlSet = []string{} + } + + insertedID, insertErr := mediacontentmod.Insert(nil, mediaContent) + err = insertErr + if err != nil { + log.Error("OnUseMediaContentData mediacontentmod.Insert fail", log.Any("mediaContent", mediaContent), log.E(err)) + log.Error("【ACG订阅】同步子集失败", + log.Any("mediaId", media.ID), + log.Any("mediaCenterId", media.MediaCenterId), + log.Any("子集ID", mediaContentData.ID), + log.Any("err", err)) + return 0, false, err + } + updatedIDs = append(updatedIDs, insertedID) + } else { + + // 存在则修改 + updateData := make(map[string]interface{}) + updateData["episodeNumber"] = mediaContentData.EpisodeNumber + updateData["name"] = mediaContentData.Title + updateData["text"] = mediaContentData.Text + if strings.TrimSpace(mediaContentData.Urls) != "" { + updateData["urlSet"] = strings.Split(mediaContentData.Urls, ",") + } else { + updateData["urlSet"] = []string{} + } + updateData["weight"] = int(mediaContentData.Width) + updateData["height"] = int(mediaContentData.Height) + updateData["ratio"] = mediaContentData.Ratio + updateData["mediaSize"] = int64(mediaContentData.MediaSize) + updateData["playTime"] = mediaContentData.PlayTime + updateData["status"] = 1 + updateData["hashId"] = mediaContentData.HashId + updateData["videoUrl"] = mediaContentData.M3u8Url + if shouldUpdateH265URL(mediaContent.H265Url, h265URL) { + updateData["h265Url"] = h265URL + } + // 其他的只有有声小说有这个地址 + updateData["audioUrl"] = mediaContentData.AudioUrl + if media.MediaType == mediamod.MediaTypeDrama { + updateData["price"] = int64(mediaContentData.SaleCoin) + updateData["listenPermission"] = 1 + if mediaContentData.SaleCoin == 0 { + updateData["listenPermission"] = 2 + } + } + _, err = mediacontentmod.UpdateByID(nil, mediaContent.ID, updateData) + if err != nil { + log.Error("OnUseMediaContentData mediacontentmod.UpdateByID fail", log.Any("updateData", updateData), log.E(err)) + log.Error("【ACG订阅】同步子集失败", + log.Any("mediaId", media.ID), + log.Any("mediaCenterId", media.MediaCenterId), + log.Any("子集ID", mediaContentData.ID), + log.Any("err", err)) + return 0, false, err + } + updatedIDs = append(updatedIDs, mediaContent.ID) + + } + } + + return syncTime, isAdd, nil +} diff --git a/common/media/useMedia_test.go b/common/media/useMedia_test.go new file mode 100644 index 0000000..fb7a4ec --- /dev/null +++ b/common/media/useMedia_test.go @@ -0,0 +1,33 @@ +package mediaService + +import ( + "testing" + + "91porn-server/models/v/mediamod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestNormalizeImportedTagClassification(t *testing.T) { + dimension, dimensionType, kind := normalizeImportedTagClassification(5, "画面风格", 1, mediamod.RiFan) + if dimension != "剧场" || dimensionType != 5 || kind != mediamod.Theater { + t.Fatalf("drama classification = (%q, %d, %d), want (%q, %d, %d)", dimension, dimensionType, kind, "剧场", 5, mediamod.Theater) + } + + dimension, dimensionType, kind = normalizeImportedTagClassification(1, "画面风格", 1, mediamod.RiFan) + if dimension != "画面风格" || dimensionType != 1 || kind != mediamod.RiFan { + t.Fatalf("animation classification changed to (%q, %d, %d)", dimension, dimensionType, kind) + } +} + +func TestPrepareImportedTagIDs(t *testing.T) { + current := []primitive.ObjectID{primitive.NewObjectID()} + if got := prepareImportedTagIDs(5, current); len(got) != 0 { + t.Fatalf("drama retained %d existing tags, want 0", len(got)) + } + + got := prepareImportedTagIDs(1, current) + if len(got) != 1 || got[0] != current[0] { + t.Fatalf("animation tags = %v, want %v", got, current) + } +} diff --git a/common/media/worker.go b/common/media/worker.go new file mode 100644 index 0000000..9d46baa --- /dev/null +++ b/common/media/worker.go @@ -0,0 +1,59 @@ +package mediaService + +import ( + "91porn-server/common/log" + "fmt" + "runtime" + "strings" + "sync" +) + +type worker struct { + num int // 最大 + ch chan struct{} + wg sync.WaitGroup +} + +var w *worker + +func init() { + num := 30 //最大同时执行的执程,防止同时开启的协程过多,把其他服务弄挂 + w = &worker{ + num: num, + ch: make(chan struct{}, num), + } +} + +func (w *worker) Wait() { + w.wg.Wait() +} + +func (w *worker) Exec(f func()) { + // 超出则阻塞 + w.ch <- struct{}{} + w.wg.Add(1) + go func() { + defer func() { + <-w.ch + w.wg.Done() + if r := recover(); r != nil { + dep := 0 + t := make([]string, 0, 10) + for i := 1; i < 10; i++ { + _, file, line, ok := runtime.Caller(i) + if !ok { + break + } + if strings.Contains(file, "/runtime/") || strings.Contains(file, "/reflect/") { + continue + } + t = append(t, fmt.Sprintf("%s∟%s:%d", strings.Repeat(" ", dep), file, line)) + dep++ + } + exception := strings.Join(t, "\n") + log.Error("worker exec Panic: ", log.Any("Exception", exception)) + } + }() + f() + }() +} diff --git a/common/mediaCenter/base.go b/common/mediaCenter/base.go new file mode 100644 index 0000000..cf7409c --- /dev/null +++ b/common/mediaCenter/base.go @@ -0,0 +1,66 @@ +package mediaCenter + +import ( + "github.com/fatih/structs" + "reflect" + "strings" +) + +// MediaCenterService 媒资库相关服务调用 +type MediaCenterService struct { + url string + proId string + secret string +} + +type Config struct { + Url string + ProId string + Secret string +} + +func NewMediaCenterService(c Config) *MediaCenterService { + return &MediaCenterService{ + url: c.Url, + proId: c.ProId, + secret: c.Secret, + } +} + +func (m *MediaCenterService) getParams(p interface{}) (params map[string]interface{}) { + req := structs.New(p) + fields := req.Fields() + params = make(map[string]interface{}) + for _, field := range fields { + jsonTag := field.Tag("json") + tagList := strings.Split(jsonTag, ",") + key := field.Name() + omitempty := "" + if len(tagList) > 0 { + key = tagList[0] + } + if len(tagList) > 1 { + omitempty = tagList[1] + } + kind := reflect.TypeOf(field.Value()).Kind() + // 如果是0值忽略或者空指针,不需要传 + if field.IsZero() && (kind == reflect.Ptr || omitempty == "omitempty") { + continue + } + if kind == reflect.Ptr { + // 通过指针取值 + params[key] = reflect.ValueOf(field.Value()).Elem().Interface() + } else { + params[key] = field.Value() + } + + } + params["pro_id"] = m.proId + params["sign"] = m.sign() + return +} + +// TODO 暂时不需要加密 +func (m *MediaCenterService) sign() string { + return "" +} diff --git a/common/mediaCenter/media.go b/common/mediaCenter/media.go new file mode 100644 index 0000000..cb3d26e --- /dev/null +++ b/common/mediaCenter/media.go @@ -0,0 +1,168 @@ +package mediaCenter + +import ( + "91porn-server/common" + "91porn-server/common/httputil" + "91porn-server/common/log" + "encoding/json" + "errors" +) + +func (m *MediaCenterService) MediaList(p *MediaListReq) (result MediaListResp, err error) { + var retMsg msg + params := m.getParams(p) + path := "/services/media/list" + log.Info("meadia resource mediaList", log.Any("params", params), log.Any("req", p)) + code, err := httputil.DefaultClientGetWithResp(&retMsg, common.BindUrl(m.url, path), nil, params) + if err != nil { + log.Error("media MediaList fail", log.Any("code", code), log.E(err)) + err = errors.New("media MediaList fail") + return + } + if code != 200 { + log.Error("media MediaList fail", log.Any("code", code)) + err = errors.New("media MediaList fail") + return + } + if retMsg.Code != 0 { + log.Error("media MediaList fail", log.Any("retMsg", retMsg)) + err = errors.New("media MediaList fail") + return + } + b, err := json.Marshal(retMsg.Data) + if err != nil { + log.Error("media MediaList fail", log.E(err)) + err = errors.New("media MediaList fail") + return + } + + if err = json.Unmarshal(b, &result); err != nil { + log.Error("media MediaList fail", log.E(err)) + err = errors.New("media MediaList fail") + return + } + return +} + +func (m *MediaCenterService) MediaContentList(p *MediaContentListReq) (result MediaContentListResp, err error) { + var retMsg msg + params := m.getParams(p) + path := "/services/media/content/list" + code, err := httputil.DefaultClientGetWithResp(&retMsg, common.BindUrl(m.url, path), nil, params) + if err != nil { + log.Error("media MediaContentList fail", log.E(err)) + err = errors.New("media MediaContentList fail") + return + } + if code != 200 { + log.Error("media MediaContentList fail", log.Any("code", code)) + err = errors.New("media MediaContentList fail") + return + } + if retMsg.Code != 0 { + log.Error("media MediaContentList fail", log.Any("retMsg", retMsg)) + err = errors.New("media MediaContentList fail") + return + } + b, err := json.Marshal(retMsg.Data) + if err != nil { + log.Error("media MediaContentList fail", log.E(err)) + err = errors.New("media MediaContentList fail") + return + } + + if err = json.Unmarshal(b, &result); err != nil { + log.Error("media MediaContentList fail", log.E(err)) + err = errors.New("media MediaContentList fail") + return + } + + return +} + +func (m *MediaCenterService) MediaUse(p *MediaUseReq) (err error) { + var retMsg msg + params := m.getParams(p) + path := "/services/media/used" + code, err := httputil.DefaultClientPostJsonWithResp(&retMsg, common.BindUrl(m.url, path), nil, params) + if err != nil { + log.Error("media MediaUse fail", log.E(err)) + err = errors.New("media MediaUse fail") + return + } + if code != 200 { + log.Error("media MediaUse fail", log.Any("code", code), log.E(err)) + err = errors.New("media MediaUse fail") + return + } + if retMsg.Code != 0 { + log.Error("media MediaUse fail", log.Any("retMsg", retMsg), log.E(err)) + err = errors.New(retMsg.Msg) + return + } + return +} + +func (m *MediaCenterService) MediaIgnore(p *MediaIgnoreReq) (err error) { + var retMsg msg + params := m.getParams(p) + path := "/services/media/ignored" + code, err := httputil.DefaultClientPostJsonWithResp(&retMsg, common.BindUrl(m.url, path), nil, params) + if err != nil { + log.Error("media MediaIgnore fail", log.E(err)) + err = errors.New("media MediaIgnore fail") + return + } + if code != 200 { + log.Error("media MediaIgnore fail", log.Any("code", code), log.E(err)) + err = errors.New("media MediaIgnore fail") + return + } + if retMsg.Code != 0 { + log.Error("media MediaIgnore fail", log.Any("retMsg", retMsg), log.E(err)) + err = errors.New(retMsg.Msg) + return + } + return +} + +func (m *MediaCenterService) MediaSync(p *MediaSyncReq) (result MediaSyncResp, err error) { + var retMsg msg + params := m.getParams(p) + path := "/services/media/sync" + log.Info("MediaCenterService MediaSync", log.Any("params", params)) + code, err := httputil.DefaultClientGetWithResp(&retMsg, common.BindUrl(m.url, path), nil, params) + if err != nil { + log.Error("media MediaSync fail", log.Any("params", params), log.E(err)) + err = errors.New("media MediaSync fail") + return + } + if code != 200 { + log.Error("media MediaSync fail", log.Any("params", params), log.Any("code", code)) + err = errors.New("media MediaSync fail") + return + } + if retMsg.Code == 7003 { + result.IsDelete = true // 已经删除 + return + } + if retMsg.Code != 0 { + log.Error("media MediaSync fail", log.Any("params", params), log.Any("retMsg", retMsg)) + err = errors.New("media MediaSync fail") + return + } + b, err := json.Marshal(retMsg.Data) + if err != nil { + log.Error("media MediaSync json.Marshal fail", log.Any("params", params), log.E(err)) + err = errors.New("media MediaSync json.Marshal fail") + return + } + + if err = json.Unmarshal(b, &result); err != nil { + log.Error("media MediaSync json.Unmarshal fail", log.Any("params", params), log.Any("data", string(b)), log.E(err)) + err = errors.New("media MediaSync json.Unmarshal fail") + return + } + + return +} diff --git a/common/mediaCenter/mediaType.go b/common/mediaCenter/mediaType.go new file mode 100644 index 0000000..342625c --- /dev/null +++ b/common/mediaCenter/mediaType.go @@ -0,0 +1,134 @@ +package mediaCenter + +import ( + "encoding/json" + "strings" +) + +type msg struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` +} + +type Media struct { + ID uint `json:"id" ` + VloggerId uint `json:"vlogger_id" ` + Title string `json:"title"` // 标题 + SaleCoin uint `json:"sale_coin" ` // 整部售价 + HorizontalCover string `json:"horizontal_cover" ` // 横版封面 + VerticalCover string `json:"vertical_cover" ` // 竖版封面 + Summary string `json:"summary"` // 简介 + TotalEpisode uint `json:"total_episode" ` // 总集数 + UpdateStatus int `json:"update_status" ` // 更新状态 + MediaType int `json:"media_type" ` // 媒体类型 1-动画 2-漫画 3-文本小说 4-有声小说 5-短剧 + Anchor string `json:"anchor"` // 作者 + TagsText string `json:"tags_text" ` // 标签文字 + DataVersion int8 `json:"data_version"` // 数据版本 + TerminalCdo string `json:"terminal_cdo"` // 上传者 + CreateTime int64 `json:"create_time"` + UpdateTime int64 `json:"update_time"` +} + +type Pagination struct { + IsLastPage bool `json:"is_last_page"` + Page uint `json:"page"` + Size uint `json:"size"` + Count uint `json:"count"` +} + +type MediaListReq struct { + MediaType int `json:"media_type" form:"media_type"` // 1-动画 2-漫画 3-小说 + Id *uint `json:"id" form:"id"` + VloggerId *uint `json:"vlogger_id,omitempty" form:"vlogger_id"` // up主id + IsUp *int `json:"is_up,omitempty" form:"is_up"` // 是否是up主 0-不是 1-是 不传则是所有 + Title *string `json:"title,omitempty" form:"title"` // 标题 + Tag *string `json:"tag,omitempty" form:"tag"` // 标签 + TerminalCdo *string `json:"terminal_cdo,omitempty" form:"terminal_cdo"` // 上传者 + SyncType int `json:"sync_type" form:"sync_type"` // 同步类型 1-全量同步 2-增量同步 + Page uint `json:"page" form:"page"` + Size uint `json:"size" form:"size"` +} + +type MediaListResp struct { + Data []*Media `json:"data"` + Pagination Pagination `json:"pagination"` +} + +// MediaContent acg资源内容表 +type MediaContent struct { + ID uint `json:"id" ` + SaleCoin uint `json:"sale_coin"` + VloggerId uint `json:"vlogger_id"` + Cover string `json:"cover"` // 封面 + MediaId uint `json:"video_id"` // 媒体资源ID + MediaType int `json:"media_type"` // 媒体类型 + EpisodeNumber int `json:"episode_number"` // 第几集 + Title string `json:"title"` // 章节名 + Text string `json:"text"` // 内容(小说专用) + Urls string `json:"urls"` // 地址集(多个资源地址用 例如漫画,逗号分隔) + Height uint `json:"height"` // 高 + Width uint `json:"width"` // 宽 + Ratio float64 `json:"ratio"` // 宽高比 + MediaSize uint `json:"media_size"` // 资源大小 + PlayTime uint `json:"play_time"` // 影片长度 + M3u8Url string `json:"m3u8_url"` // 视频资源地址 + H265Url string `json:"h265_url"` // H.265 视频资源地址 + AudioUrl string `json:"audio_url"` // 有升小说资源地址 + FsResourceId string `json:"fs_resource_id"` + FsResourceStatus string `json:"fs_resource_status"` + HashId string `json:"hash_id"` + DataVersion int8 `json:"data_version"` // 数据版本 + TerminalCdo string `json:"terminal_cdo"` // 上传者 + CreateTime int64 `json:"create_time"` + UpdateTime int64 `json:"update_time"` +} + +// UnmarshalJSON accepts both media-center field names used by existing +// deployments. The app-facing and Mongo fields remain h265Url. +func (m *MediaContent) UnmarshalJSON(data []byte) error { + type mediaContentAlias MediaContent + aux := struct { + *mediaContentAlias + HevcM3u8URL string `json:"hevc_m3u8_url"` + }{ + mediaContentAlias: (*mediaContentAlias)(m), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + if strings.TrimSpace(m.H265Url) == "" { + m.H265Url = strings.TrimSpace(aux.HevcM3u8URL) + } + return nil +} + +type MediaContentListReq struct { + MediaId int `json:"media_id" form:"media_id"` + Page uint `json:"page" form:"page"` + Size uint `json:"size" form:"size"` +} + +type MediaContentListResp struct { + Data []*MediaContent `json:"data"` + Pagination Pagination `json:"pagination"` +} + +type MediaUseReq struct { + Ids []uint `json:"ids" form:"ids"` +} + +type MediaIgnoreReq struct { + Ids []uint `json:"ids" form:"ids"` +} + +type MediaSyncReq struct { + Id uint `json:"id" form:"id"` + SyncTime int64 `json:"sync_time" form:"sync_time"` //从某一刻开始同步获取数据 +} + +type MediaSyncResp struct { + IsDelete bool `json:"-"` + Media *Media `json:"media"` + MediaContentList []MediaContent `json:"media_content_list"` +} diff --git a/common/mediaCenter/mediaType_test.go b/common/mediaCenter/mediaType_test.go new file mode 100644 index 0000000..77f47ff --- /dev/null +++ b/common/mediaCenter/mediaType_test.go @@ -0,0 +1,57 @@ +package mediaCenter + +import ( + "encoding/json" + "testing" +) + +func TestMediaContentUnmarshalH265URL(t *testing.T) { + tests := []struct { + name string + data string + wantH264 string + wantH265 string + }{ + { + name: "h265_url", + data: `{ + "m3u8_url":"https://cdn.example.com/h264/index.m3u8", + "h265_url":"https://cdn.example.com/h265/index.m3u8" + }`, + wantH264: "https://cdn.example.com/h264/index.m3u8", + wantH265: "https://cdn.example.com/h265/index.m3u8", + }, + { + name: "hevc_m3u8_url alias", + data: `{ + "m3u8_url":"https://cdn.example.com/h264/index.m3u8", + "hevc_m3u8_url":"https://cdn.example.com/hevc/index.m3u8" + }`, + wantH264: "https://cdn.example.com/h264/index.m3u8", + wantH265: "https://cdn.example.com/hevc/index.m3u8", + }, + { + name: "h265_url takes precedence", + data: `{ + "h265_url":"https://cdn.example.com/h265/index.m3u8", + "hevc_m3u8_url":"https://cdn.example.com/hevc/index.m3u8" + }`, + wantH265: "https://cdn.example.com/h265/index.m3u8", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var content MediaContent + if err := json.Unmarshal([]byte(tt.data), &content); err != nil { + t.Fatalf("unmarshal media content: %v", err) + } + if content.M3u8Url != tt.wantH264 { + t.Fatalf("unexpected H.264 URL: got %q want %q", content.M3u8Url, tt.wantH264) + } + if content.H265Url != tt.wantH265 { + t.Fatalf("unexpected H.265 URL: got %q want %q", content.H265Url, tt.wantH265) + } + }) + } +} diff --git a/common/mergemap/mergemap.go b/common/mergemap/mergemap.go new file mode 100644 index 0000000..08c5f7f --- /dev/null +++ b/common/mergemap/mergemap.go @@ -0,0 +1,66 @@ +package mergemap + +import "reflect" + +var MaxDepth = 32 + +// Merge recursively merges the src and dst maps. Key conflicts are resolved by +// preferring src, or recursively descending, if both src and dst are maps. +func Merge(dst, src map[string]interface{}) map[string]interface{} { + return merge(dst, src, 0) +} + +func merge(dst, src map[string]interface{}, depth int) map[string]interface{} { + if depth > MaxDepth { + panic("too deep!") + } + for key, srcVal := range src { + if dstVal, ok := dst[key]; ok { + srcMap, srcMapOk := mapify(srcVal) + dstMap, dstMapOk := mapify(dstVal) + if srcMapOk && dstMapOk { + srcVal = merge(dstMap, srcMap, depth+1) + } + } + dst[key] = srcVal + } + return dst +} + +func Reduce(dst, src map[string]interface{}, method func(a, b interface{}) interface{}) map[string]interface{} { + return reduce(dst, src, 0, method) +} + +func reduce(dst, src map[string]interface{}, depth int, method func(a, b interface{}) interface{}) map[string]interface{} { + if depth > MaxDepth { + panic("too deep!") + } + for key, srcVal := range src { + if dstVal, ok := dst[key]; ok { + srcMap, srcMapOk := mapify(srcVal) + dstMap, dstMapOk := mapify(dstVal) + if srcMapOk && dstMapOk { + srcVal = reduce(dstMap, srcMap, depth+1, method) + } + } + value := reflect.ValueOf(srcVal) + if value.Kind() == reflect.Map { + dst[key] = srcVal + } else { + dst[key] = method(dst[key], srcVal) + } + } + return dst +} + +func mapify(i interface{}) (map[string]interface{}, bool) { + value := reflect.ValueOf(i) + if value.Kind() == reflect.Map { + m := map[string]interface{}{} + for _, k := range value.MapKeys() { + m[k.String()] = value.MapIndex(k).Interface() + } + return m, true + } + return map[string]interface{}{}, false +} diff --git a/common/modelUtil.go b/common/modelUtil.go new file mode 100644 index 0000000..f7e64b6 --- /dev/null +++ b/common/modelUtil.go @@ -0,0 +1,73 @@ +package common + +import ( + "91porn-server/common/timeutil" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// StandQuery 客户端传过来的时间条件信息 +type StandQuery struct { + StartTime *string `form:"startTime" json:"startTime,omitempty" ` // 开始时间 + EndTime *string `form:"endTime" json:"endTime,omitempty"` // 结束时间 + TimeString *string `form:"timeString" json:"timeString,omitempty"` // 时间 + DistrictCode *string `form:"districtCode" json:"districtCode" bson:"districtCode"` // 商区码 + + commod.Page +} + +// StandQueryMap 通用条件查询管道pipe组装 +func StandQueryMap(s StandQuery, obj interface{}) (cond bson.M, opts *options.FindOptions) { + stdq := commod.StdQuery{ + Page: &commod.PageBy{Num: s.PageNumber, Size: s.PageSize}, + } + opts = commod.ConvertToListQuery(stdq) + opts.SetSort(bson.D{{Key: "_id", Value: -1}}) + m, _ := JSONStruct2Map(obj) + date := make(map[string]interface{}) + if s.EndTime != nil { + date["$lte"] = timeutil.StrTimeToTime(*s.EndTime) + } + if s.StartTime != nil { + date["$gte"] = timeutil.StrTimeToTime(*s.StartTime) + } + if len(date) > 0 { + if s.TimeString != nil { + m[*s.TimeString] = date + } else { + m["createdAt"] = date + } + } + if v, ok := m["_id"]; ok { + str, ok := v.(string) + if !ok { + return + } + m["_id"], _ = primitive.ObjectIDFromHex(str) + } + if s.DistrictCode != nil { + m["districtCode"] = s.DistrictCode + } + cond = m + return + +} + +// Deduplication 切片去重 +func Deduplication(src []primitive.ObjectID) []primitive.ObjectID { + if len(src) == 0 { + return src + } + dst := []primitive.ObjectID{} + m := make(map[primitive.ObjectID]int) + for _, s := range src { + m[s] = 1 + } + for k := range m { + dst = append(dst, k) + } + return dst +} diff --git a/common/money.go b/common/money.go new file mode 100644 index 0000000..df0244c --- /dev/null +++ b/common/money.go @@ -0,0 +1,15 @@ +package common + +import "github.com/shopspring/decimal" + +func FenToYuan(money float64) decimal.Decimal { + return decimal.NewFromFloat(money).Div(decimal.NewFromFloat(100)).Round(4) +} + +func FenToBai(money float64) decimal.Decimal { + return decimal.NewFromFloat(money).Div(decimal.NewFromFloat(10000)).Round(4) +} + +func YuanToFen(money float64) decimal.Decimal { + return decimal.NewFromFloat(money).Mul(decimal.NewFromFloat(100)).Round(4) +} diff --git a/common/pageopt/match.go b/common/pageopt/match.go new file mode 100644 index 0000000..f62dd5d --- /dev/null +++ b/common/pageopt/match.go @@ -0,0 +1,465 @@ +package pageopt + +import ( + "reflect" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// M M from Map +type M = bson.M + +// Matcher +type Matcher interface { + Filter() M +} + +// MergeM +func MergeM(mats []Matcher) M { + merge := M{} + for _, m := range mats { + if m == nil { + continue + } + ql := m.Filter() + for k, v := range ql { + merge[k] = v + } + } + return merge +} + +type MergeMatch struct { + merge M +} + +func NewMergeMatch(mats ...Matcher) Matcher { + mergeM := MergeM(mats) + return &MergeMatch{mergeM} +} + +func (m *MergeMatch) Filter() M { + return m.merge +} + +// AssignMatch +type AssignMatch struct { + Key string + Val interface{} +} + +func NewAssignMatch(key string, val interface{}) Matcher { + return &AssignMatch{ + key, + val, + } +} + +func (m *AssignMatch) Filter() M { + if IsEmpt(m.Val) { + return M{} + } + return M{ + m.Key: m.Val, + } +} + +// RegexMatch +type RegexMatch struct { + Key string + Val interface{} + Options string +} + +func NewRegexMatch(key string, val interface{}, opt string) Matcher { + return &RegexMatch{ + Key: key, + Val: val, + Options: opt, + } +} + +func (r *RegexMatch) Filter() M { + if IsEmpt(r.Val) { + return M{} + } + return M{ + r.Key: M{"$regex": r.Val, "$options": r.Options}, + } +} + +func IsNil(i interface{}) bool { + vi := reflect.ValueOf(i) + return vi.IsNil() +} + +func IsEmpt(i interface{}) bool { + v := reflect.ValueOf(i) + switch v.Kind() { + case reflect.Ptr: + return v.IsNil() + case reflect.Slice, reflect.Array: + return v.Len() == 0 + } + return false +} + +type NEMatch struct { + Key string + NE interface{} +} + +func (m *NEMatch) Filter() M { + if IsEmpt(m.NE) { + return M{} + } + return M{m.Key: M{"$ne": m.NE}} +} + +func NewNEMatch(Key string, ne interface{}) Matcher { + return &NEMatch{ + Key: Key, + NE: ne, + } +} + +// GTEMatch +type GTEMatch struct { + Key string + GTE interface{} +} + +func NewGTEMatch(Key string, gte interface{}) Matcher { + return >EMatch{ + Key: Key, + GTE: gte, + } +} + +func (m *GTEMatch) Filter() M { + if IsEmpt(m.GTE) { + return M{} + } + return M{m.Key: M{"$gte": m.GTE}} +} + +// GTMatch +type GTMatch struct { + Key string + GT interface{} +} + +func NewGTMatch(Key string, gt interface{}) Matcher { + return >Match{ + Key: Key, + GT: gt, + } +} + +func (m *GTMatch) Filter() M { + if IsEmpt(m.GT) { + return M{} + } + return M{m.Key: M{"$gt": m.GT}} +} + +// LTMatch +type LTMatch struct { + Key string + LT interface{} +} + +func NewLTMatch(Key string, lt interface{}) Matcher { + return <Match{ + Key: Key, + LT: lt, + } +} + +func (m *LTMatch) Filter() M { + if IsEmpt(m.LT) { + return M{} + } + return M{m.Key: M{"$lt": m.LT}} +} + +// LTEMatch +type LTEMatch struct { + Key string + LTE interface{} +} + +func NewLTEMatch(Key string, lte interface{}) Matcher { + return <EMatch{ + Key: Key, + LTE: lte, + } +} + +func (m *LTEMatch) Filter() M { + if IsEmpt(m.LTE) { + return M{} + } + return M{m.Key: M{"$lte": m.LTE}} +} + +// GTEAndLTEMatch +type GTEAndLTEMatch struct { + Key string + GTE interface{} + LTE interface{} +} + +func NewGTEAndLTEMatch(Key string, gte, lte interface{}) Matcher { + return >EAndLTEMatch{ + Key, + gte, + lte, + } +} + +func (m *GTEAndLTEMatch) Filter() M { + if IsEmpt(m.GTE) || IsEmpt(m.LTE) { + return M{} + } + + return M{ + m.Key: M{ + "$gte": m.GTE, + "$lte": m.LTE, + }, + } +} + +// GTEAndLTMatch +type GTEAndLTMatch struct { + Key string + GTE interface{} + LT interface{} +} + +func NewGTEAndLTMatch(key string, gte, lt interface{}) Matcher { + return >EAndLTMatch{ + Key: key, + GTE: gte, + LT: lt, + } +} + +func (m *GTEAndLTMatch) Filter() M { + if IsEmpt(m.GTE) || IsEmpt(m.LT) { + return M{} + } + return M{ + m.Key: M{ + "$gte": m.GTE, + "$lt": m.LT, + }, + } +} + +// ExistsMatch +type ExistsMatch struct { + Key string + IsExisted *bool +} + +func NewExistsMatch(key string, isExisted *bool) Matcher { + return &ExistsMatch{ + Key: key, + IsExisted: isExisted, + } +} + +func (e *ExistsMatch) Filter() M { + if IsEmpt(e.IsExisted) { + return M{} + } + return M{ + e.Key: M{"$exists": e.IsExisted}, + } +} + +// InMatch +type InMatch struct { + Key string + Slice interface{} +} + +func NewInMatch(key string, slice interface{}) Matcher { + return &InMatch{ + Key: key, + Slice: slice, + } +} + +func (i *InMatch) Filter() M { + if IsNil(i.Slice) { + return M{} + } + return M{ + i.Key: M{"$in": i.Slice}, + } +} + +// NinMatch +type NinMatch struct { + Key string + Slice interface{} +} + +func NewNinMatch(key string, slice interface{}) Matcher { + return &NinMatch{ + Key: key, + Slice: slice, + } +} + +func (n *NinMatch) Filter() M { + if IsNil(n.Slice) { + return M{} + } + return M{ + n.Key: M{"$nin": n.Slice}, + } +} + +// ManualOrMatch +type ManualOrMatch struct { + Key string + Mats []Matcher +} + +func NewManualOrMatch(key string, mats []Matcher) Matcher { + return &ManualOrMatch{ + Key: key, + Mats: mats, + } +} + +func (o *ManualOrMatch) Filter() M { + if len(o.Mats) == 0 { + return M{} + } + fs := []interface{}{} + for _, v := range o.Mats { + f := v.Filter() + if len(f) != 0 { + fs = append(fs, v.Filter()) + } + } + if len(fs) == 0 { + return M{} + } + return M{"$or": fs} +} + +// BitsAllSetMatch +type BitsAllSetMatch struct { + Key string + Bits []int +} + +func NewBitsAllSetMatch(key string, bits []int) Matcher { + return &BitsAllSetMatch{ + Key: key, + Bits: bits, + } +} + +func (b *BitsAllSetMatch) Filter() M { + if len(b.Bits) == 0 { + return M{} + } + return M{ + b.Key: M{"$bitsAllSet": b.Bits}, + } +} + +// BitsAllClearMatch +type BitsAllClearMatch struct { + Key string + Bits []int +} + +func NewBitsAllClearMatch(key string, bits []int) Matcher { + return &BitsAllClearMatch{ + Key: key, + Bits: bits, + } +} + +func (b *BitsAllClearMatch) Filter() M { + if len(b.Bits) == 0 { + return M{} + } + return M{ + b.Key: M{"$bitsAllClear": b.Bits}, + } +} + +// UIDMatch +type UIDMatch struct { + UID *uint64 +} + +func (u *UIDMatch) New() Matcher { + return NewAssignMatch("uid", u.UID) +} + +// UIDInMatch +type UIDInMatch struct { + UIDS []uint64 +} + +func (u *UIDInMatch) New() Matcher { + return NewInMatch("uid", u.UIDS) +} + +// IDMatch +type IDMatch struct { + ID *primitive.ObjectID +} + +func (i *IDMatch) New() Matcher { + return NewAssignMatch("_id", i.ID) +} + +// IDInMatch +type IDInMatch struct { + IDs []primitive.ObjectID +} + +func (c *IDInMatch) New() Matcher { + return NewInMatch("_id", c.IDs) +} + +// CreatedAtGTEMatch +type CreatedAtGTEMatch struct { + GTE *time.Time +} + +func (c *CreatedAtGTEMatch) New() Matcher { + return NewGTEMatch("createdAt", c.GTE) +} + +// CreatedAtLTMatch +type CreatedAtLTMatch struct { + LT *time.Time +} + +func (c *CreatedAtLTMatch) New() Matcher { + return NewLTMatch("createdAt", c.LT) +} + +// CreatedAtGTEAndLTMatch +type CreatedAtGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (c *CreatedAtGTEAndLTMatch) New() Matcher { + return NewGTEAndLTMatch("createdAt", c.GTE, c.LT) +} diff --git a/common/parseFilter.go b/common/parseFilter.go new file mode 100644 index 0000000..392ede6 --- /dev/null +++ b/common/parseFilter.go @@ -0,0 +1,61 @@ +package common + +import ( + "reflect" + "strings" + "time" + + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" +) + +// ParseFilter ParseFilter +func ParseFilter(cond map[string]interface{}) []bson.M { + ret := []bson.M{} + for key := range cond { + if strings.HasSuffix(key, "Start") { + keyS := strings.TrimSuffix(key, "Start") + endK := keyS + "End" + ts, err := time.Parse(time.RFC3339, cond[key].(string)) + if err != nil { + log.Error("buildFilter解析查询条件出错", log.E(err)) + continue + } + te, err := time.Parse(time.RFC3339, cond[endK].(string)) + if err != nil { + continue + } + a := bson.M{ + keyS: bson.M{ + "$gte": ts, + "$lte": te, + }, + } + b := bson.M{ + "$match": a, + } + ret = append(ret, b) + continue + } + if strings.HasSuffix(key, "End") { + continue + } + if reflect.TypeOf(cond[key]).Kind() == reflect.Slice { + mm := bson.M{ + "$match": bson.M{ + key: bson.M{ + "$in": cond[key], + }, + }, + } + ret = append(ret, mm) + continue + } + e := bson.M{ + "$match": bson.M{key: cond[key]}, + } + ret = append(ret, e) + } + return ret +} diff --git a/common/picture.go b/common/picture.go new file mode 100644 index 0000000..fcc9a91 --- /dev/null +++ b/common/picture.go @@ -0,0 +1,38 @@ +package common + +import ( + "bytes" + "image" + "io" + "net/http" +) + +type PictureProp struct { + Size int `json:"size" bson:"size,omitempty"` // 单位:b + Width int `json:"height" bson:"height,omitempty"` + Height int `json:"width" bson:"width,omitempty"` +} + +const ImgUrlHost = "http://image.lhexm.com/" + +func GetPictureProp(imgURL string) PictureProp { + res, err := http.Get(imgURL) + if err != nil { + return PictureProp{} + } + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if err != nil { + return PictureProp{} + } + buf := bytes.NewBuffer(body) + cfg, _, err := image.DecodeConfig(buf) + if err != nil { + return PictureProp{} + } + return PictureProp{ + Size: len(body), + Width: cfg.Width, + Height: cfg.Height, + } +} diff --git a/common/qrcode.go b/common/qrcode.go new file mode 100644 index 0000000..823ea20 --- /dev/null +++ b/common/qrcode.go @@ -0,0 +1,30 @@ +package common + +import ( + "image" + _ "image/jpeg" + _ "image/png" + "io" + + "github.com/makiuchi-d/gozxing" + "github.com/makiuchi-d/gozxing/qrcode" +) + +func DecodeQRCode(file io.Reader) (content string, err error) { + img, _, err := image.Decode(file) + if err != nil { + return + } + // prepare BinaryBitmap + bmp, err := gozxing.NewBinaryBitmapFromImage(img) + if err != nil { + return + } + // decode image + qrReader := qrcode.NewQRCodeReader() + result, err := qrReader.Decode(bmp, nil) + if err != nil { + return + } + return result.String(), nil +} diff --git a/common/rand.go b/common/rand.go new file mode 100644 index 0000000..70383bb --- /dev/null +++ b/common/rand.go @@ -0,0 +1,188 @@ +package common + +import ( + "errors" + "math" + "math/rand" + "reflect" + "time" +) + +// 用掩码进行替换 +const letterBytes = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + +const letterBytesAll = "0123456789abcdefghijklmnopqrstvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +// base32大写 +const letterBytes32Upper = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ" + +var ( + firstName = []string{"宇文", "尉迟", "延陵", "羊舌", "羊角", "乐正", "诸葛", "颛孙", "仲孙", "仲长", "长孙", "钟离", "宗政", "左丘", "主父", + "宰父", "子书", "子车", "子桑", "百里", "北堂", "北野", "哥舒", "谷梁", "闻人", "王孙", "王官", "王叔", "巫马", "微生", + "淳于", "单于", "成公", "叱干", "叱利", "褚师", "端木", "东方", "东郭", "东宫", "东野", "东里", "东门", "第二", "第五", + "公祖", "公玉", "公西", "公孟", "公伯", "公仲", "公孙", "公广", "公上", "公冶", "公羊", "公良", "公户", "公仪", "公山", + "公门", "公坚", "公乘", "欧阳", "濮阳", "青阳", "漆雕", "壤驷", "上官", "司徒", "司马", "司空", "司寇", "士孙", "申屠", + "叔孙", "叔仲", "侍其", "令狐", "梁丘", "闾丘", "刘傅", "慕容", "万俟", "谷利", "高堂", "南宫", "南门", "南荣", "南野", + "女娲", "纳兰", "澹台", "拓跋", "太史", "太叔", "太公", "秃发", "夏侯", "西门", "鲜于", "轩辕", "相里", "皇甫", "赫连", + "呼延", "胡母", "亓官", "夹谷", "即墨", "独孤", "段干", "达奚"} + lastName = "一乙二十丁厂七卜人入八九几儿了力乃刀又三于干亏士工土才寸下大丈与万上小口巾山千乞川亿个勺久凡及夕丸么广亡门义之尸弓己已子卫也女飞刃习叉马乡丰王井开夫天无元专云扎艺木五支厅不太犬区历尤友匹车巨牙屯比互切瓦止少日中冈贝内水见午牛手毛气升长仁什片仆化仇币仍仅斤爪反介父从今凶分乏公仓月氏勿欠风丹匀乌凤勾文六方火为斗忆订计户认心尺引丑巴孔队办以允予劝双书幻玉刊示末未击打巧正扑扒功扔去甘世古节本术可丙左厉右石布龙平灭轧东卡北占业旧帅归且旦目叶甲申叮电号田由史只央兄叼叫另叨叹四生失禾丘付仗代仙们仪白仔他斥瓜乎丛令用甩印乐句匆册犯外处冬鸟务包饥主市立闪兰半汁汇头汉宁穴它讨写让礼训必议讯记永司尼民出辽奶奴加召皮边发孕圣对台矛纠母幼丝式刑动扛寺吉扣考托老执巩圾扩扫地扬场耳共芒亚芝朽朴机权过臣再协西压厌在有百存而页匠夸夺灰达列死成夹轨邪划迈毕至此贞师尘尖劣光当早吐吓虫曲团同吊吃因吸吗屿帆岁回岂刚则肉网年朱先丢舌竹迁乔伟传乒乓休伍伏优伐延件任伤价份华仰仿伙伪自血向似后行舟全会杀合兆企众爷伞创肌朵杂危旬旨负各名多争色壮冲冰庄庆亦刘齐交次衣产决充妄闭问闯羊并关米灯州汗污江池汤忙兴宇守宅字安讲军许论农讽设访寻那迅尽导异孙阵阳收阶阴防奸如妇好她妈戏羽观欢买红纤级约纪驰巡寿弄麦形进戒吞远违运扶抚坛技坏扰拒找批扯址走抄坝贡攻赤折抓扮抢孝均抛投坟抗坑坊抖护壳志扭块声把报却劫芽花芹芬苍芳严芦劳克苏杆杠杜材村杏极李杨求更束豆两丽医辰励否还歼来连步坚旱盯呈时吴助县里呆园旷围呀吨足邮男困吵串员听吩吹呜吧吼别岗帐财针钉告我乱利秃秀私每兵估体何但伸作伯伶佣低你住位伴身皂佛近彻役返余希坐谷妥含邻岔肝肚肠龟免狂犹角删条卵岛迎饭饮系言冻状亩况床库疗应冷这序辛弃冶忘闲间闷判灶灿弟汪沙汽沃泛沟没沈沉怀忧快完宋宏牢究穷灾良证启评补初社识诉诊词译君灵即层尿尾迟局改张忌际陆阿陈阻附妙妖妨努忍劲鸡驱纯纱纳纲驳纵纷纸纹纺驴纽奉玩环武青责现表规抹拢拔拣担坦押抽拐拖拍者顶拆拥抵拘势抱垃拉拦拌幸招坡披拨择抬其取苦若茂苹苗英范直茄茎茅林枝杯柜析板松枪构杰述枕丧或画卧事刺枣雨卖矿码厕奔奇奋态欧垄妻轰顷转斩轮软到非叔肯齿些虎虏肾贤尚旺具果味昆国昌畅明易昂典固忠咐呼鸣咏呢岸岩帖罗帜岭凯败贩购图钓制知垂牧物乖刮秆和季委佳侍供使例版侄侦侧凭侨佩货依的迫质欣征往爬彼径所舍金命斧爸采受乳贪念贫肤肺肢肿胀朋股肥服胁周昏鱼兔狐忽狗备饰饱饲变京享店夜庙府底剂郊废净盲放刻育闸闹郑券卷单炒炊炕炎炉沫浅法泄河沾泪油泊沿泡注泻泳泥沸波泼泽治怖性怕怜怪学宝宗定宜审宙官空帘实试郎诗肩房诚衬衫视话诞询该详建肃录隶居届刷屈弦承孟孤陕降限妹姑姐姓始驾参艰线练组细驶织终驻驼绍经贯奏春帮珍玻毒型挂封持项垮挎城挠政赴赵挡挺括拴拾挑指垫挣挤拼挖按挥挪某甚革荐巷带草茧茶荒茫荡荣故胡南药标枯柄栋相查柏柳柱柿栏树要咸威歪研砖厘厚砌砍面耐耍牵残殃轻鸦皆背战点临览竖省削尝是盼眨哄显哑冒映星昨畏趴胃贵界虹虾蚁思蚂虽品咽骂哗咱响哈咬咳哪炭峡罚贱贴骨钞钟钢钥钩卸缸拜看矩怎牲选适秒香种秋科重复竿段便俩贷顺修保促侮俭俗俘信皇泉鬼侵追俊盾待律很须叙剑逃食盆胆胜胞胖脉勉狭狮独狡狱狠贸怨急饶蚀饺饼弯将奖哀亭亮度迹庭疮疯疫疤姿亲音帝施闻阀阁差养美姜叛送类迷前首逆总炼炸炮烂剃洁洪洒浇浊洞测洗活派洽染济洋洲浑浓津恒恢恰恼恨举觉宣室宫宪突穿窃客冠语扁袄祖神祝误诱说诵垦退既屋昼费陡眉孩除险院娃姥姨姻娇怒架贺盈勇怠柔垒绑绒结绕骄绘给络骆绝绞统耕耗艳泰珠班素蚕顽盏匪捞栽捕振载赶起盐捎捏埋捉捆捐损都哲逝捡换挽热恐壶挨耻耽恭莲莫荷获晋恶真框桂档桐株桥桃格校核样根索哥速逗栗配翅辱唇夏础破原套逐烈殊顾轿较顿毙致柴桌虑监紧党晒眠晓鸭晃晌晕蚊哨哭恩唤啊唉罢峰圆贼贿钱钳钻铁铃铅缺氧特牺造乘敌秤租积秧秩称秘透笔笑笋债借值倚倾倒倘俱倡候俯倍倦健臭射躬息徒徐舰舱般航途拿爹爱颂翁脆脂胸胳脏胶脑狸狼逢留皱饿恋桨浆衰高席准座脊症病疾疼疲效离唐资凉站剖竞部旁旅畜阅羞瓶拳粉料益兼烤烘烦烧烛烟递涛浙涝酒涉消浩海涂浴浮流润浪浸涨烫涌悟悄悔悦害宽家宵宴宾窄容宰案请朗诸读扇袜袖袍被祥课谁调冤谅谈谊剥恳展剧屑弱陵陶陷陪娱娘通能难预桑绢绣验继球理捧堵描域掩捷排掉堆推掀授教掏掠培接控探据掘职基著勒黄萌萝菌菜萄菊萍菠营械梦梢梅检梳梯桶救副票戚爽聋袭盛雪辅辆虚雀堂常匙晨睁眯眼悬野啦晚啄距跃略蛇累唱患唯崖崭崇圈铜铲银甜梨犁移笨笼笛符第敏做袋悠偿偶偷您售停偏假得衔盘船斜盒鸽悉欲彩领脚脖脸脱象够猜猪猎猫猛馅馆凑减毫麻痒痕廊康庸鹿盗章竟商族旋望率着盖粘粗粒断剪兽清添淋淹渠渐混渔淘液淡深婆梁渗情惜惭悼惧惕惊惨惯寇寄宿窑密谋谎祸谜逮敢屠弹随蛋隆隐婚婶颈绩绪续骑绳维绵绸绿琴斑替款堪搭塔越趁趋超提堤博揭喜插揪搜煮援裁搁搂搅握揉斯期欺联散惹葬葛董葡敬葱落朝辜葵棒棋植森椅椒棵棍棉棚棕惠惑逼厨厦硬确雁殖裂雄暂雅辈悲紫辉敞赏掌晴暑最量喷晶喇遇喊景践跌跑遗蛙蛛蜓喝喂喘喉幅帽赌赔黑铸铺链销锁锄锅锈锋锐短智毯鹅剩稍程稀税筐等筑策筛筒答筋筝傲傅牌堡集焦傍储奥街惩御循艇舒番释禽腊脾腔鲁猾猴然馋装蛮就痛童阔善羡普粪尊道曾焰港湖渣湿温渴滑湾渡游滋溉愤慌惰愧愉慨割寒富窜窝窗遍裕裤裙谢谣谦属屡强粥疏隔隙絮嫂登缎缓编骗缘瑞魂肆摄摸填搏塌鼓摆携搬摇搞塘摊蒜勤鹊蓝墓幕蓬蓄蒙蒸献禁楚想槐榆楼概赖酬感碍碑碎碰碗碌雷零雾雹输督龄鉴睛睡睬鄙愚暖盟歇暗照跨跳跪路跟遣蛾蜂嗓置罪罩错锡锣锤锦键锯矮辞稠愁筹签简毁舅鼠催傻像躲微愈遥腰腥腹腾腿触解酱痰廉新韵意粮数煎塑慈煤煌满漠源滤滥滔溪溜滚滨粱滩慎誉塞谨福群殿辟障嫌嫁叠缝缠静碧璃墙撇嘉摧截誓境摘摔聚蔽慕暮蔑模榴榜榨歌遭酷酿酸磁愿需弊裳颗嗽蜻蜡蝇蜘赚锹锻舞稳算箩管僚鼻魄貌膜膊膀鲜疑馒裹敲豪膏遮腐瘦辣竭端旗精歉熄熔漆漂漫滴演漏慢寨赛察蜜谱嫩翠熊凳骡缩慧撕撒趣趟撑播撞撤增聪鞋蕉蔬横槽樱橡飘醋醉震霉瞒题暴瞎影踢踏踩踪蝶蝴嘱墨镇靠稻黎稿稼箱箭篇僵躺僻德艘膝膛熟摩颜毅糊遵潜潮懂额慰劈操燕薯薪薄颠橘整融醒餐嘴蹄器赠默镜赞篮邀衡膨雕磨凝辨辩糖糕燃澡激懒壁避缴戴擦鞠藏霜霞瞧蹈螺穗繁辫赢糟糠燥臂翼骤鞭覆蹦镰翻鹰警攀蹲颤瓣爆疆壤耀躁嚼嚷籍魔灌蠢霸露囊罐匕刁丐歹戈夭仑讥冗邓艾夯凸卢叭叽皿凹囚矢乍尔冯玄邦迂邢芋芍吏夷吁吕吆屹廷迄臼仲伦伊肋旭匈凫妆亥汛讳讶讹讼诀弛阱驮驯纫玖玛韧抠扼汞扳抡坎坞抑拟抒芙芜苇芥芯芭杖杉巫杈甫匣轩卤肖吱吠呕呐吟呛吻吭邑囤吮岖牡佑佃伺囱肛肘甸狈鸠彤灸刨庇吝庐闰兑灼沐沛汰沥沦汹沧沪忱诅诈罕屁坠妓姊妒纬玫卦坷坯拓坪坤拄拧拂拙拇拗茉昔苛苫苟苞茁苔枉枢枚枫杭郁矾奈奄殴歧卓昙哎咕呵咙呻咒咆咖帕账贬贮氛秉岳侠侥侣侈卑刽刹肴觅忿瓮肮肪狞庞疟疙疚卒氓炬沽沮泣泞泌沼怔怯宠宛衩祈诡帚屉弧弥陋陌函姆虱叁绅驹绊绎契贰玷玲珊拭拷拱挟垢垛拯荆茸茬荚茵茴荞荠荤荧荔栈柑栅柠枷勃柬砂泵砚鸥轴韭虐昧盹咧昵昭盅勋哆咪哟幽钙钝钠钦钧钮毡氢秕俏俄俐侯徊衍胚胧胎狰饵峦奕咨飒闺闽籽娄烁炫洼柒涎洛恃恍恬恤宦诫诬祠诲屏屎逊陨姚娜蚤骇耘耙秦匿埂捂捍袁捌挫挚捣捅埃耿聂荸莽莱莉莹莺梆栖桦栓桅桩贾酌砸砰砾殉逞哮唠哺剔蚌蚜畔蚣蚪蚓哩圃鸯唁哼唆峭唧峻赂赃钾铆氨秫笆俺赁倔殷耸舀豺豹颁胯胰脐脓逛卿鸵鸳馁凌凄衷郭斋疹紊瓷羔烙浦涡涣涤涧涕涩悍悯窍诺诽袒谆祟恕娩骏琐麸琉琅措捺捶赦埠捻掐掂掖掷掸掺勘聊娶菱菲萎菩萤乾萧萨菇彬梗梧梭曹酝酗厢硅硕奢盔匾颅彪眶晤曼晦冕啡畦趾啃蛆蚯蛉蛀唬啰唾啤啥啸崎逻崔崩婴赊铐铛铝铡铣铭矫秸秽笙笤偎傀躯兜衅徘徙舶舷舵敛翎脯逸凰猖祭烹庶庵痊阎阐眷焊焕鸿涯淑淌淮淆渊淫淳淤淀涮涵惦悴惋寂窒谍谐裆袱祷谒谓谚尉堕隅婉颇绰绷综绽缀巢琳琢琼揍堰揩揽揖彭揣搀搓壹搔葫募蒋蒂韩棱椰焚椎棺榔椭粟棘酣酥硝硫颊雳翘凿棠晰鼎喳遏晾畴跋跛蛔蜒蛤鹃喻啼喧嵌赋赎赐锉锌甥掰氮氯黍筏牍粤逾腌腋腕猩猬惫敦痘痢痪竣翔奠遂焙滞湘渤渺溃溅湃愕惶寓窖窘雇谤犀隘媒媚婿缅缆缔缕骚瑟鹉瑰搪聘斟靴靶蓖蒿蒲蓉楔椿楷榄楞楣酪碘硼碉辐辑频睹睦瞄嗜嗦暇畸跷跺蜈蜗蜕蛹嗅嗡嗤署蜀幌锚锥锨锭锰稚颓筷魁衙腻腮腺鹏肄猿颖煞雏馍馏禀痹廓痴靖誊漓溢溯溶滓溺寞窥窟寝褂裸谬媳嫉缚缤剿赘熬赫蔫摹蔓蔗蔼熙蔚兢榛榕酵碟碴碱碳辕辖雌墅嘁踊蝉嘀幔镀舔熏箍箕箫舆僧孵瘩瘟彰粹漱漩漾慷寡寥谭褐褪隧嫡缨撵撩撮撬擒墩撰鞍蕊蕴樊樟橄敷豌醇磕磅碾憋嘶嘲嘹蝠蝎蝌蝗蝙嘿幢镊镐稽篓膘鲤鲫褒瘪瘤瘫凛澎潭潦澳潘澈澜澄憔懊憎翩褥谴鹤憨履嬉豫缭撼擂擅蕾薛薇擎翰噩橱橙瓢蟥霍霎辙冀踱蹂蟆螃螟噪鹦黔穆篡篷篙篱儒膳鲸瘾瘸糙燎濒憾懈窿缰壕藐檬檐檩檀礁磷瞭瞬瞳瞪曙蹋蟋蟀嚎赡镣魏簇儡徽爵朦臊鳄糜癌懦豁臀藕藤瞻嚣鳍癞瀑襟璧戳攒孽蘑藻鳖蹭蹬簸簿蟹靡癣羹鬓攘蠕巍鳞糯譬霹躏髓蘸镶瓤矗" +) + +// RandStrRandr 生成指定位数的随机数 +func RandStrRandr(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))] + } + return string(b) +} + +// RandStrRandrAll 生成指定位数的随机数 +func RandStrRandrAll(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = letterBytesAll[rand.Int63()%int64(len(letterBytesAll))] + } + return string(b) +} + +func RandInt(min int, max int) int { + return min + rand.Intn(max-min) +} + +func RandIntBase(rannum int64, min int, max int) int { + return min + rand.Intn(max-min) +} + +// 生成随机字符串 +func RandStr(strlen int) string { + data := make([]byte, strlen) + var num int + for i := 0; i < strlen; i++ { + num = rand.Intn(57) + 65 + for { + if num > 90 && num < 97 { + num = rand.Intn(57) + 65 + } else { + break + } + } + data[i] = byte(num) + } + return string(data) +} + +type Choice struct { + Weight int + Item interface{} +} + +// 加权随机数 +func WeightedChoice(choices []Choice) (Choice, error) { + var ret Choice + sum := 0 + for _, c := range choices { + sum += c.Weight + } + r := RandInt(0, sum) + for _, c := range choices { + r -= c.Weight + if r < 0 { + return c, nil + } + } + return ret, errors.New("Internal error - code should not reach this point") +} + +// base32生成 +func RandBase32Upper(n int) string { + b := make([]byte, n) + for i := 0; i < n; i++ { + b[i] = letterBytes32Upper[rand.Int63()%int64(len(letterBytes32Upper))] + } + return string(b) +} + +// GenName 随机生成名字 +func GenName() string { + w := []rune(lastName) + f := firstName[rand.Intn(len(firstName))] + s := string(w[rand.Intn(len(w))]) + t := string(w[rand.Intn(len(w))]) + return f + s + t +} + +// 切片分页方法 +func SlicePage(pageNum, pageSize, count uint64) (sliceStart, sliceEnd uint64) { + if pageNum == 0 { + pageNum = 1 + } + if pageSize == 0 { + pageSize = 10 + } + if pageSize > count { + return 0, count + } + // 总页数 + pageCount := uint64(math.Ceil(float64(count) / float64(pageSize))) + if pageNum > pageCount { + return 0, 0 + } + sliceStart = (pageNum - 1) * pageSize + sliceEnd = sliceStart + pageSize + if sliceEnd > count { + sliceEnd = count + } + return +} + +// RandomPicker 是一个随机选择器 +type RandomPicker struct { + rng *rand.Rand +} + +// NewRandomPicker 创建一个随机选择器实例 +func NewRandomPicker() *RandomPicker { + seed := time.Now().UnixNano() + return &RandomPicker{ + rng: rand.New(rand.NewSource(seed)), + } +} + +// PickElements 从数组中随机选择指定数量的元素 +func (rp *RandomPicker) PickElements(arr interface{}, num int) ([]interface{}, error) { + sliceValue, err := makeSliceValue(arr) + if err != nil { + return nil, err + + } + sliceLen := sliceValue.Len() + + if num < 0 { + return nil, errors.New("数量不能为负数") + } + + if num > sliceLen { + num = sliceLen + } + + // Fisher-Yates 洗牌算法 + result := make([]interface{}, num) + perm := rp.rng.Perm(sliceLen) + for i := 0; i < num; i++ { + result[i] = sliceValue.Index(perm[i]).Interface() + } + + return result, nil +} + +// makeSliceValue 将任意类型的数组转换为反射中的 Slice Value +func makeSliceValue(arr interface{}) (reflect.Value, error) { + sliceValue := reflect.ValueOf(arr) + + if sliceValue.Kind() != reflect.Slice { + return sliceValue, errors.New("参数必须为切片类型") + } + + return sliceValue, nil +} diff --git a/common/rchgutil/asen.go b/common/rchgutil/asen.go new file mode 100644 index 0000000..6df44c0 --- /dev/null +++ b/common/rchgutil/asen.go @@ -0,0 +1,188 @@ +package rchgutil + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "91porn-server/app/appg" + "91porn-server/common/httputil" + "91porn-server/common/log" +) + +var asc = config{ + MercID: "10005", + AppSecret: "176486f9d7c14189717fb88f12d6626a", + ExeUrl: "http://106.55.92.30:8081/api/pay", + NotifyUrl: "/3rd/defray/callback/rchg/asen", +} + +// ASen 充值订单请求体 +type ASen struct { + MercID string `json:"merchant_no" sign:"1"` //商户编号 + TradeNo string `json:"out_order_no" sign:"1"` //商户订单号 + Money string `json:"amount" sign:"1"` // 支付金额:元 + Type string `json:"pay_type" sign:"1"` // 订单充值类型 + NotifyURL string `json:"notify_url" sign:"1"` // 回调地址 + Sign string `json:"sign"` // 签名 + PayMethod string `json:"-"` +} + +type ASenMsgData struct { + MercID string `json:"merchant_no"` //商户编号 + OID string `json:"order_no"` + TradeNo string `json:"out_order_no"` //商户订单号 + Money string `json:"amount"` + PayWay string `json:"pay_type"` + PayUrl string `json:"pay_url"` +} + +type ASenMsg struct { + Code int `json:"code" sign:"1"` //交易返回码,返回SUCCESS 表示交易成功(不代表最终结果), + Data interface{} `json:"data"` + Msg string `json:"msg"` //签名 +} + +// RYRes 回调函数参数model +type ASenBack struct { + OID string `json:"order_no" form:"order_no" sign:"1"` //订单号 + MercID string `json:"merchant_no" form:"merchant_no" sign:"1" ` //商户编号 + TradeNo string `json:"out_order_no" form:"out_order_no" sign:"1"` //商户订单号 + PayMoney string `json:"amount" form:"amount" sign:"1"` //订单到账实际金额 + Type string `json:"pay_type" form:"pay_type" sign:"1"` // 订单充值类型 + Status string `json:"code" form:"code" sign:"1"` //交易结果 1(成功) + Sign string `json:"sign" form:"sign" ` //签名 +} + +// ToPay 支付发起 +func (this *ASen) ToPay() (res RchgMsg, err error) { + var errlog string + this.Fill() + msg := ASenMsg{} + code, err := httputil.DefaultClientPostJsonWithResp(&msg, this.GetURL(), this.GetHeader(), this.GetBody()) + if err != nil { + errlog = fmt.Sprintf("asen ToPay requset fail \nerror:%+v\n\ndata:%+v;", err, this) + log.Error(errlog) + err = errors.New(errlog) + return + } + if code != http.StatusOK { + errlog = fmt.Sprintf("asen ToPay response status exception;\nstatus %+v;", code) + log.Error(errlog) + err = errors.New(errlog) + return + } + if msg.Code != 1 { + errlog = fmt.Sprintf("asen ToPay be rejected;\n msg:%+v:", msg) + err = errors.New(errlog) + log.Error(errlog) + return + } + var data ASenMsgData + bytes, err := json.Marshal(msg.Data) + if err != nil { + errlog = fmt.Sprintf("asen payUrl urlDecode fail \nerror:%+v;", err) + log.Error(errlog) + err = errors.New(errlog) + return + } + if err = json.Unmarshal(bytes, &data); err != nil { + errlog = fmt.Sprintf("asen payUrl urlDecode fail \nerror:%+v;", err) + log.Error(errlog) + err = errors.New(errlog) + return + } + res.PayUrl = data.PayUrl + res.OID = data.OID + res.Mode = "sdk" + return +} + +// ASenCallBack 阿森结构回调函数 +func (this *ASenBack) Notify() (RchgBack, error) { + now := time.Now() + r := RchgBack{} + if this.Sign != this.ToSign() { + err := errors.New("check sign fail") + log.Error(fmt.Sprintf("ASen callback sign verify fail error:%+v:", err)) + return r, err + } + r.Code = 200 + if this.Status != "1" { + r.Code = 400 + } + fPayMoney, err := strconv.ParseFloat(this.PayMoney, 64) + if err != nil { + log.Error(fmt.Sprintf("ASen callback sign verify fail error:%+v:", err)) + return r, err + } + r.PayMoney = int64(fPayMoney * 100) + r.PaymentAt = now + r.TransNo = this.TradeNo + r.OID = this.OID + return r, nil +} + +func (this *ASenBack) Success() string { + return "success" +} + +// Fill 补全配置信息和签名信息 +func (this *ASen) Fill() { + this.Type = this.PayMethod + this.PayMethod = "" + this.MercID = this.GetMercID() + this.NotifyURL = this.GetCallBackURL() + this.Sign = this.sign() +} + +// sign 签名 +func (this *ASen) sign() string { + buf := fixedSignStrAdd(*this) + buf.WriteString(asc.AppSecret) + log.Info(fmt.Sprintf("--------------%s", buf.String())) + return md5Sign(buf.String()) +} + +// sign 签名 +func (this *ASenBack) ToSign() string { + buf := fixedSignStrAdd(*this) + buf.WriteString(asc.AppSecret) + return md5Sign(buf.String()) +} + +// GetBody 获取请求body +func (this *ASen) GetBody() interface{} { + m := make(map[string]string) + jsonStr, err := json.Marshal(this) + if err != nil { + log.Info(fmt.Sprintf("ASen GetBody json.Marshal is fail error:%+v/data:%+v", err, this)) + } + sstr := string(jsonStr) + log.Info(fmt.Sprintf("==================%+v", sstr)) + _ = json.Unmarshal(jsonStr, &m) + return m +} + +// GetHeader 获取请求头信息 +func (g *ASen) GetHeader() map[string]string { + return map[string]string{"Content-Type": "application/x-www-form-urlencoded"} +} + +// GetURL 获取请求地址 +func (*ASen) GetURL() string { + return asc.ExeUrl +} + +// GetCallBackURL 获取回掉函数 +func (*ASen) GetCallBackURL() string { + return appg.Conf.URL.LocalWebUrl + asc.NotifyUrl +} + +// GetMercID 获取商户编号 +func (*ASen) GetMercID() string { + return asc.MercID +} diff --git a/common/rchgutil/base.go b/common/rchgutil/base.go new file mode 100644 index 0000000..f659234 --- /dev/null +++ b/common/rchgutil/base.go @@ -0,0 +1,384 @@ +package rchgutil + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "errors" + "fmt" + "math/rand" + "reflect" + "strconv" + "strings" + "time" +) + +var PAY_CHANNEL_CLOSE_ERROR = errors.New("PAY CHANNEL CLOSE") +var PAY_CHANNEL_BAN_ERROR = errors.New("USER IS DISABLED") + +const ( + Alipay = "alipay" //支付宝 + Wechat = "wechat" //微信支付 + + UNKNOWError = 444 + RCHG_NOTIFY_URL = "/3rd/defray/callback/rchg/" +) + +type Rchg struct { + TransNo string `json:"transNo"` //订单号 + PlatformID string `json:"platformID"` //平台 + UID string `json:"uid" ` //用户id + DevID string `json:"devID" ` //设备id + UserIP string `json:"userIP"` //用户ip + Name string `json:"name"` //用户名称 + Tel string `json:"tel"` //用户手机号 + DevType string `json:"devType"` //设备系统类型 ios pc android + Money int64 `json:"money"` //充值金额 订单金额 确认 + Channel string `json:"channel"` //渠道类型 鲨鱼 金鱼 + Mode string `json:"mode,omitempty"` //式模 持支 表代则传 值定固。填选 + CreatedAt time.Time `json:"createdAt"` //创建时间 + NotifyUrl string `json:"notifyUrl"` //回调地址 + PType string `json:"pType"` //支付类型 wechat alipay + ProductType int `json:"productType"` //支付产品类型 0 站群 1 棋牌 + UserLevel int `json:"userLevel"` //限定用户层级 (0默认无等级 1充值成功过的1等级) + PayMethod string +} + +type RchgMsg struct { + Err string `json:"err"` + PayUrl string `json:"payUrl"` + OID string `json:"oid"` + Mode string `json:"mode"` + CID string `json:"cid"` + Rate string `json:"rate"` //渠道费率 + +} + +type RchgBack struct { + TransNo string `json:"transNo"` //流水id + OID string `json:"oid"` //支付平台订单号 + PayMoney int64 `json:"payMoney"` //实际到账金额 用户实际支付金额 李秋山确认 + FailureAt time.Time `json:"failureAt"` //回调失败时间 + SuccessAt time.Time `json:"successAt"` //回调成功时间 + PaymentAt time.Time `json:"paymentAt"` //支付时间 + Code int `json:"code"` //状态码 +} + +type RchgBackMsg struct { + Code int `json:"code"` + Err string `json:"err"` +} + +// MsgModel 返回消息结构体 +type MsgModel struct { + Code int `json:"code"` + Msg map[string]string `json:"msg"` + Err interface{} `json:"err"` +} + +// RchgRes 返回消息结构体 +type RchgRes struct { + Code int `json:"code"` + OID string `json:"oid"` + PayURL string `json:"payUrl"` + Err error `json:"err"` +} + +// MsgModel 返回消息结构体 +type ExchgMsgModel struct { + Code int `json:"code"` + Msg string `json:"msg"` + Err string `json:"err"` +} + +// config +type config struct { + MercID string `json:"mercID"` //商户id + AppSecret string `json:"appSecret"` + ExeUrl string `json:"exeUrl"` //业务发起地址 + NotifyUrl string `json:"notifyUrl"` //回调地址 + QueryUrl string `json:"queryUrl"` //充值方式查询地址 + LowAndUpperURL string `json:"lowAndUpperURL"` //充值范围查询地址 + CallbackURL string `json:"callbackURL"` //充值成功请求的页面地址 + VerifySecret string //验证密钥 +} + +// GetUTCTime 获取utc时间戳 +func GetUTCTimeMS(t time.Time) string { + return strconv.FormatInt(t.UnixNano()/1000000, 10) +} + +// VerifySign 签名 +func VerifySign(sign string, buf string) bool { + nsign := md5Sign(buf) + return nsign == sign +} + +// md5签名 +func md5Sign(buf string) string { + md5Ctx := md5.New() + md5Ctx.Write([]byte(buf)) + cipherStr := md5Ctx.Sum(nil) + nsign := hex.EncodeToString(cipherStr) + return nsign +} + +var ( + lastNameLen = len(lastName) + firstNameLen = len(firstName) + + lastName = []string{ + "赵", "钱", "孙", "李", "周", "吴", "郑", "王", "冯", "陈", "褚", "卫", "蒋", + "沈", "韩", "杨", "朱", "秦", "尤", "许", "何", "吕", "施", "张", "孔", "曹", "严", "华", "金", "魏", + "陶", "姜", "戚", "谢", "邹", "喻", "柏", "水", "窦", "章", "云", "苏", "潘", "葛", "奚", "范", "彭", + "郎", "鲁", "韦", "昌", "马", "苗", "凤", "花", "方", "任", "袁", "柳", "鲍", "史", "唐", "费", "薛", + "雷", "贺", "倪", "汤", "滕", "殷", "罗", "毕", "郝", "安", "常", "傅", "卞", "齐", "元", "顾", "孟", + "平", "黄", "穆", "萧", "尹", "姚", "邵", "湛", "汪", "祁", "毛", "狄", "米", "伏", "成", "戴", "谈", + "宋", "茅", "庞", "熊", "纪", "舒", "屈", "项", "祝", "董", "梁", "杜", "阮", "蓝", "闵", "季", "贾", + "路", "娄", "江", "童", "颜", "郭", "梅", "盛", "林", "钟", "徐", "邱", "骆", "高", "夏", "蔡", "田", + "樊", "胡", "凌", "霍", "虞", "万", "支", "柯", "管", "卢", "莫", "柯", "房", "裘", "缪", "解", "应", + "宗", "丁", "宣", "邓", "单", "杭", "洪", "包", "诸", "左", "石", "崔", "吉", "龚", "程", "嵇", "邢", + "裴", "陆", "荣", "翁", "荀", "于", "惠", "甄", "曲", "封", "储", "仲", "伊", "宁", "仇", "甘", "武", + "符", "刘", "景", "詹", "龙", "叶", "幸", "司", "黎", "溥", "印", "怀", "蒲", "邰", "从", "索", "赖", + "卓", "屠", "池", "乔", "胥", "闻", "莘", "党", "翟", "谭", "贡", "劳", "逄", "姬", "申", "扶", "堵", + "冉", "宰", "雍", "桑", "寿", "通", "燕", "浦", "尚", "农", "温", "别", "庄", "晏", "柴", "瞿", "阎", + "连", "习", "容", "向", "古", "易", "廖", "庾", "终", "步", "都", "耿", "满", "弘", "匡", "国", "文", + "寇", "广", "禄", "阙", "东", "欧", "利", "师", "巩", "聂", "关", "荆", "司马", "上官", "欧阳", "夏侯", + "诸葛", "闻人", "东方", "赫连", "皇甫", "尉迟", "公羊", "澹台", "公冶", "宗政", "濮阳", "淳于", "单于", + "太叔", "申屠", "公孙", "仲孙", "轩辕", "令狐", "徐离", "宇文", "长孙", "慕容", "司徒", "司空"} + firstName = []string{ + "伟", "刚", "勇", "毅", "俊", "峰", "强", "军", "平", "保", "东", "文", "辉", "力", "明", "永", "健", "世", "广", "志", "义", + "兴", "良", "海", "山", "仁", "波", "宁", "贵", "福", "生", "龙", "元", "全", "国", "胜", "学", "祥", "才", "发", "武", "新", + "利", "清", "飞", "彬", "富", "顺", "信", "子", "杰", "涛", "昌", "成", "康", "星", "光", "天", "达", "安", "岩", "中", "茂", + "进", "林", "有", "坚", "和", "彪", "博", "诚", "先", "敬", "震", "振", "壮", "会", "思", "群", "豪", "心", "邦", "承", "乐", + "绍", "功", "松", "善", "厚", "庆", "磊", "民", "友", "裕", "河", "哲", "江", "超", "浩", "亮", "政", "谦", "亨", "奇", "固", + "之", "轮", "翰", "朗", "伯", "宏", "言", "若", "鸣", "朋", "斌", "梁", "栋", "维", "启", "克", "伦", "翔", "旭", "鹏", "泽", + "晨", "辰", "士", "以", "建", "家", "致", "树", "炎", "德", "行", "时", "泰", "盛", "雄", "琛", "钧", "冠", "策", "腾", "楠", + "榕", "风", "航", "弘", "秀", "娟", "英", "华", "慧", "巧", "美", "娜", "静", "淑", "惠", "珠", "翠", "雅", "芝", "玉", "萍", + "红", "娥", "玲", "芬", "芳", "燕", "彩", "春", "菊", "兰", "凤", "洁", "梅", "琳", "素", "云", "莲", "真", "环", "雪", "荣", + "爱", "妹", "霞", "香", "月", "莺", "媛", "艳", "瑞", "凡", "佳", "嘉", "琼", "勤", "珍", "贞", "莉", "桂", "娣", "叶", "璧", + "璐", "娅", "琦", "晶", "妍", "茜", "秋", "珊", "莎", "锦", "黛", "青", "倩", "婷", "姣", "婉", "娴", "瑾", "颖", "露", "瑶", + "怡", "婵", "雁", "蓓", "纨", "仪", "荷", "丹", "蓉", "眉", "君", "琴", "蕊", "薇", "菁", "梦", "岚", "苑", "婕", "馨", "瑗", + "琰", "韵", "融", "园", "艺", "咏", "卿", "聪", "澜", "纯", "毓", "悦", "昭", "冰", "爽", "琬", "茗", "羽", "希", "欣", "飘", + "育", "滢", "馥", "筠", "柔", "竹", "霭", "凝", "晓", "欢", "霄", "枫", "芸", "菲", "寒", "伊", "亚", "宜", "可", "姬", "舒", + "影", "荔", "枝", "丽", "阳", "妮", "宝", "贝", "初", "程", "梵", "罡", "恒", "鸿", "桦", "骅", "剑", "娇", "纪", "宽", "苛", + "灵", "玛", "媚", "琪", "晴", "容", "睿", "烁", "堂", "唯", "威", "韦", "雯", "苇", "萱", "阅", "彦", "宇", "雨", "洋", "忠", + "宗", "曼", "紫", "逸", "贤", "蝶", "菡", "绿", "蓝", "儿", "翠", "烟", "小", "轩"} +) + +// 获取随机姓名 +func GetFullName() string { + var first string //名 + for i := 0; i <= rand.Intn(2); i++ { //随机产生2位或者3位的名 + first = fmt.Sprint(firstName[rand.Intn(firstNameLen-1)]) + } + //返回姓名 + return fmt.Sprintf("%s%s", fmt.Sprint(lastName[rand.Intn(lastNameLen-1)]), first) +} + +func genIpaddr() string { + ip := fmt.Sprintf("%d.%d.%d.%d", rand.Intn(255), rand.Intn(255), rand.Intn(255), rand.Intn(255)) + return ip +} + +type StringList []string + +func (sl *StringList) Len() int { + return len(*sl) +} + +func (sl *StringList) Less(i, j int) bool { + s := strings.Split((*sl)[i], "=")[0] + e := strings.Split((*sl)[j], "=")[0] + ib := []byte(s) + jb := []byte(e) + for k := range ib { + if ib[k] == jb[k] { + continue + } else { + return ib[k] < jb[k] + } + } + return false +} + +func (sl *StringList) Swap(i, j int) { + b := (*sl)[i] + (*sl)[i] = (*sl)[j] + (*sl)[j] = b +} + +const ( + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = rand.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + return string(b) +} + +func RChgIDAssemble(id string) string { + return "CZ-91PO-" + id +} + +func RChgIDDisassemble(transNo string) string { + temp := strings.Split(transNo, "-") + return temp[len(temp)-1] +} + +func RChgWLIDAssemble(id string) string { + return "CZ-91POW-" + id +} + +func EChgIDAssemble(id string) string { + return "TX-91PO-" + id +} + +func EChgWLIDAssemble(id string) string { + return "TX-91POW-" + id +} + +func EChgIDDisassemble(transNo string) string { + temp := strings.Split(transNo, "-") + return temp[len(temp)-1] +} + +// sign 结构体固定顺序签名 +func fixedSignStrAdd(obj interface{}) bytes.Buffer { + var buf bytes.Buffer + t := reflect.TypeOf(obj) + v := reflect.ValueOf(obj) + for k := 0; k < v.NumField(); k++ { + signTag, _ := t.Field(k).Tag.Lookup("sign") + fv := v.Field(k) + if signTag == "1" { + var value string + switch fv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + value = strconv.FormatInt(v.Field(k).Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + value = strconv.FormatUint(v.Field(k).Uint(), 10) + case reflect.String: + value = v.Field(k).String() + } + buf.WriteString(value) + } + } + return buf +} + +type Notify interface { + Notify() (RchgBack, error) + Success() string +} + +func GetNotifyBack(name string) Notify { + switch name { + case YongLiName: + return (&YongLiBack{}) + case DaBaiShaName: + return (&DaBaiShaRes{}) + case ASenName: + return (&ASenBack{}) + case "bill": + return (&RchgBack{}) + } + return nil +} + +// ToStringSlice 转化为stirng切片切片 +func ToStringSlice(obj interface{}) StringList { + t := reflect.TypeOf(obj) + v := reflect.ValueOf(obj) + data := make([]string, 0, v.NumField()) + for k := 0; k < v.NumField(); k++ { + jsonTag, _ := t.Field(k).Tag.Lookup("json") + signTag, _ := t.Field(k).Tag.Lookup("sign") + fv := v.Field(k).Type() + if signTag == "1" { + switch fv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + data = append(data, jsonTag+"="+strconv.FormatInt(v.Field(k).Int(), 10)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + data = append(data, jsonTag+"="+strconv.FormatUint(v.Field(k).Uint(), 10)) + case reflect.String: + data = append(data, jsonTag+"="+v.Field(k).String()) + case reflect.Float64, reflect.Float32: + data = append(data, jsonTag+"="+strconv.FormatFloat(v.Field(k).Float(), 'f', 2, 64)) + } + } + } + return data +} + +var ( + DaBaiShaName = "dabaisha" + ASenName = "asen" + YongLiName = "yongli" + YongLiWanShun = "ylws" +) + +const ProductName = "充值" + +func (this *Rchg) ToPay() (res RchgMsg, err error) { + strMoney := strconv.FormatFloat(float64(this.Money)/100, 'f', 2, 64) + //充值统一前缀 + this.TransNo = RChgIDAssemble(this.TransNo) + switch this.Channel { + case DaBaiShaName: + dbs := DaBaiSha{ + Money: strMoney, + TradeNo: this.TransNo, + Info: DaBaiShaInfo{ + PlayerID: this.UID, + PlayerIP: this.UserIP, + DeviceID: this.DevID, + DeviceType: this.DevType, + Name: ProductName, + }, + Mode: this.Mode, + Time: GetUTCTimeMS(this.CreatedAt), + PayMethod: this.PayMethod, + } + res, err = dbs.ToPay() + case ASenName: + strIntMoney := strconv.FormatInt(int64(float64(this.Money)/100), 10) + res, err = (&ASen{ + TradeNo: this.TransNo, + Money: strIntMoney, + PayMethod: this.PayMethod, + }).ToPay() + case YongLiName: + res, err = (&YongLi{ + TradeNo: this.TransNo, + Money: strMoney, + PayMethod: this.PayMethod, + Time: strconv.FormatInt(this.CreatedAt.UTC().Unix(), 10), + }).ToPay() + case YongLiWanShun: + res, err = (&WsylInfo{ + OutTradeNo: this.TransNo, + Amount: strMoney, + }).toPay() + default: + err = errors.New("invalid pay channel") + return + } + return +} diff --git a/common/rchgutil/bill.go b/common/rchgutil/bill.go new file mode 100644 index 0000000..84220a6 --- /dev/null +++ b/common/rchgutil/bill.go @@ -0,0 +1,170 @@ +package rchgutil + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "time" + + "91porn-server/app/appg" + "91porn-server/common/httputil" + "91porn-server/common/log" +) + +type PayTypeParam struct { + Moneys []int64 `json:"moneys"` + DevType string `json:"devType"` + ProductType int `json:"productType"` //产品类型 0站群 1棋牌 + UserLevel int `json:"userLevel"` //限定用户层级 (0默认无等级 1充值成功过的1等级) + PlatformID string `json:"platformID"` //平台 +} + +type RchgType struct { + Money int64 `json:"money"` //金额 单位 分 + Alipay bool `json:"alipay"` //是否支持支付宝 + Wechat bool `json:"wechat"` //是否支持微信 + Union bool `json:"union"` //是否支持银联 + DaiChong bool `json:"daichong"` //是否支持代充 + USDT bool `json:"usdt"` //是否支持代充 + QuickUnion bool `json:"quickUnion"` //是否支持快速银联 +} + +type StdMsg struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` + Tip string `json:"tip"` +} + +// GetPayType 从支付中心获取充值金额对应的充值方式 +func GetPayType(ctx context.Context, moneys []int64, devType string, productType, userLevel int) ([]RchgType, error) { + var errlog string + p := PayTypeParam{ + moneys, + devType, + productType, + userLevel, + "91PORN", + } + res := StdMsg{} + code, err := httputil.DefaultClientPostJsonWithResp(&res, p.GetURL(), nil, p.GetBody()) + if err != nil { + errlog = fmt.Sprintf("bill GetPayType requset fail \nerror:%+v\n\ndata:%+v;", err, p) + log.ErrorX(ctx, "get pay type in bill center failed", log.Any("param", p), log.Any("url", p.GetURL()), + log.E(err)) + return nil, errors.New(errlog) + } + if code != http.StatusOK { + errlog = fmt.Sprintf("bill GetPayType response status exception;\nstatus %+v;", code) + log.ErrorX(ctx, errlog, log.Any("code", code)) + return nil, errors.New(errlog) + } + if res.Code != 200 { + errlog = fmt.Sprintf("bill GetPayType be rejected;\n err:%+v:", res) + log.ErrorX(ctx, errlog, log.Any("res", res)) + return nil, errors.New(errlog) + } + bytes, err := json.Marshal(res.Data) + if err != nil { + errlog = fmt.Sprintf("bill payUrl urlDecode fail \nerror:%+v;", err) + log.Error(errlog) + return nil, errors.New(errlog) + } + var payTypes []RchgType + if err = json.Unmarshal(bytes, &payTypes); err != nil { + errlog = fmt.Sprintf("bill payUrl urlDecode fail \nerror:%+v;", err) + log.ErrorX(ctx, errlog, log.E(err)) + return nil, errors.New(errlog) + } + log.InfoX(ctx, "bill GetPayType data", log.Any("res", res)) + return payTypes, nil +} + +// GetBody 获取请求body +func (this *PayTypeParam) GetBody() []byte { + data, err := json.Marshal(this) + if err != nil { + log.Info(fmt.Sprintf("bill GetBody json.Marshal is fail error:%+v/data:%+v", err, this)) + return nil + } + return data +} + +// GetURL 获取请求地址 +func (*PayTypeParam) GetURL() string { + return appg.Conf.URL.TransactionUrl + "/api/recharge/payType" +} + +var ( + PayErrPostFailure = errors.New("recharge http post failure") + PayErrResponseCode = errors.New("recharge http code isn't 200") + PayErrBeRejected = errors.New("recharge has been rejected") +) + +// GetBody 获取请求body +func (this *Rchg) ToPayNew(ctx context.Context) (res RchgMsg, err error) { + defer func() { + log.InfoX(ctx, "bill ToPayNew request info end 2", log.Any("transNo", this.TransNo), log.Any("res", res)) + if err := recover(); err != nil { + log.ErrorX(ctx, "bill ToPayNew recover", log.Any("Rchg", this), log.Any("panic", err)) + } + }() + this.fill() + param, err := json.Marshal(this) + if err != nil { + log.ErrorX(ctx, "bill ToPayNew json.Marshal failed,", log.Any("transNo", this.TransNo), log.E(err)) + return + } + log.InfoX(ctx, "ToPayNew request info start", log.Any("transNo", this.TransNo), log.Any("param", string(param))) + startS := time.Now().UnixNano() + code, err := httputil.DefaultClientPostJsonWithResp(&res, this.GetURL(), nil, param) + log.InfoX(ctx, "ToPayNew POST resp ", log.Any("code", code), log.Any("transNo", this.TransNo), + log.Any("param", string(param)), log.Any("topay-bill-time cost", time.Now().UnixNano()-startS)) + if err != nil { + log.ErrorX(ctx, "bill ToPayNew post failed", log.Any("transNo", this.TransNo), log.E(err)) + err = PayErrPostFailure + return + } + if code != http.StatusOK { + log.ErrorX(ctx, "bill ToPayNew bad http statusCode", log.Any("transNo", this.TransNo), log.Any("statusCode", code)) + err = PayErrResponseCode + return + } + if res.Err != "" { + log.ErrorX(ctx, "bill ToPayNew has been rejected", log.Any("transNo", this.TransNo), log.Any("res err", res.Err)) + err = PayErrBeRejected + return + } + log.InfoX(ctx, "bill ToPayNew request info end", log.Any("transNo", this.TransNo), log.Any("res", res)) + return +} + +// GetURL 获取请求地址 +func (this *Rchg) fill() { + this.PlatformID = "91PORN" + if this.ProductType == 0 { + this.TransNo = RChgIDAssemble(this.TransNo) + } + if this.ProductType == 1 { + this.TransNo = RChgWLIDAssemble(this.TransNo) + } + this.NotifyUrl = appg.Conf.URL.LocalWebUrl + RCHG_NOTIFY_URL + "bill" + this.PType = this.PayMethod +} + +// GetURL 获取请求地址 +func (*Rchg) GetURL() string { + return appg.Conf.URL.TransactionUrl + "/api/recharge/recharge" +} + +// GetBody 获取请求body +func (this *RchgBack) Notify() (RchgBack, error) { + return *this, nil +} + +// GetBody 获取请求body +func (this *RchgBack) Success() string { + return "success" +} diff --git a/common/rchgutil/billExchg.go b/common/rchgutil/billExchg.go new file mode 100644 index 0000000..bf6aed8 --- /dev/null +++ b/common/rchgutil/billExchg.go @@ -0,0 +1,131 @@ +package rchgutil + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" + + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/models/v/wdordmod" + "91porn-server/web/webg" +) + +type Exchg struct { + TransNo string `json:"transNo" bson:"transNo"` //订单号 + UID string `json:"uid" bson:"uid"` //用户id + Name string `json:"name" bson:"name"` //用户名 + PayMoney int64 `json:"payMoney" bson:"payMoney"` //实际到账 + PayType string `json:"payType" bson:"payType" binding:"required"` //提现方式,alipay,bankcard + ActName string `json:"actName" bson:"actName"` //交易账户持有人 + Act string `json:"act" bson:"act"` //交易账户 + UserIP string `json:"userIp" bson:"userIp"` //用户ip + DeviceType string `json:"deviceType" bson:"deviceType"` //设备类型 + DevID string `json:"devID" bson:"devID"` //设备id + CheckedAt time.Time `json:"checkedAt" bson:"checkedAt"` //审核时间 + NotifyUrl string `json:"notifyUrl"` //回调地址 + ProductType int `json:"productType"` //支付产品类型 0 站群 1 棋牌 + Rate float64 `json:"rate"` //费率 + PlatformID string `json:"platformID"` + BankCode string `json:"bankCode" bson:"bankCode"` //银行编码 +} + +type ExchgBack struct { + OID string `json:"oid" ` //支付平台订单号 + Money int64 `json:"money"` //兑换金额 + TradeNo string `json:"tradeNo"` //商户订单号 + Status string `json:"status"` + Mark string `json:"mark"` //拒绝兑换理由 +} + +type ExchgBackMsg struct { + Code int `json:"code"` + Err string `json:"err"` +} + +type ExchgMsg struct { + OID string `json:"oid"` + Code int `json:"code"` + Err string `json:"err"` + Msg string `json:"msg"` +} + +// ToPay 支付发起 +func (this *Exchg) ToPay() ExchgMsg { + var msg ExchgMsg + var errlog string + this.fill() + code, err := httputil.DefaultClientPostJsonWithResp(&msg, this.GetURL(), this.GetHeader(), this.GetBody()) + if err != nil { + msg.Code = UNKNOWError + errlog = fmt.Sprintf("recharge ToPay Http.PostJson fail error:%+v/,data:%+v;", err, this) + msg.Err = errlog + log.Error(errlog) + return msg + } + if code != http.StatusOK { + msg.Code = UNKNOWError + errlog = fmt.Sprintf("recharge ToPay Http.PostJson fail error:%+v/,data:%+v;", err, code) + msg.Err = errlog + log.Error(errlog) + return msg + } + return msg +} + +func (this *Exchg) fill() { + this.PlatformID = "91PO" + this.NotifyUrl = webg.Conf.URL.LocalAppUrl + "/3rd/defray/callback/echg/bill" +} + +// GetBody 获取请求body +func (this *Exchg) GetBody() interface{} { + jsonStr, err := json.Marshal(this) + if err != nil { + log.Info(fmt.Sprintf("excharge json.Marshal is fail error:%+v/data:%+v", err, this)) + } + sstr := string(jsonStr) + log.Info(fmt.Sprintf("recharg topay json data :%+v", sstr)) + return jsonStr +} + +// GetHeader 获取请求头信息 +func (this *Exchg) GetHeader() map[string]string { + header := make(map[string]string, 0) + return header +} + +// GetURL 获取请求地址 +func (*Exchg) GetURL() string { + return webg.Conf.URL.TransactionUrl + "/api/recharge/withdraw" +} + +// 防洗钱校验 +// 提现防洗钱限制, 防止用户用usdt充值, 其它方式提现洗钱 +// 例如: 用户usdt充值1000, 则用户只能用usdt提现1000, +// +// 且, 用户usdt充值1000未提现时, 此时用其它方式提现, +// 则, 余额 - 提现金额 >= 1000 +// +// eType: 提现方式 +// allMoney: 可提现总金额 +// money: 提现金额 +// usdtAmount: usdt可提现额度 +// uid: 提现用户id +func CheckMoney(eType string, allMoney, money, usdtAmount int64, uid uint64) bool { + //usdt方式提现 + if eType == wdordmod.TypeUsdt && money*100 > usdtAmount { + log.Error("用户: " + strconv.FormatInt(int64(uid), 10) + ", USDT方式可提现余额为: " + strconv.FormatInt(usdtAmount, 10)) + return false + } + //可提现金额 + ktx_Amount := allMoney*100 - usdtAmount + //非usdt方式提现 + if eType != wdordmod.TypeUsdt && ktx_Amount < money*100 { + log.Error("用户: " + strconv.FormatInt(int64(uid), 10) + ", 当前方式可提现金额为: " + strconv.FormatInt(ktx_Amount, 10) + ", 其它金额请通过USDT提现!") + return false + } + return true +} diff --git a/common/rchgutil/dabaisha.go b/common/rchgutil/dabaisha.go new file mode 100644 index 0000000..698d407 --- /dev/null +++ b/common/rchgutil/dabaisha.go @@ -0,0 +1,349 @@ +package rchgutil + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/web/webg" +) + +var dbsc = config{ + MercID: "10082", + AppSecret: "b8f217afdcac162b569f856174bd811a", + ExeUrl: "https://umio.zjcthbkj.com/api/shark/topay", + QueryUrl: "https://umio.zjcthbkj.com/api/shark/order/queryOrder", + NotifyUrl: "/3rd/defray/callback/rchg/dabaisha", +} + +const ( + DaBaiSha_Alipay_Code = "1001" + DaBaiSha_Wechat_Code = "1002" + DaBaiSha_Union_Code = "1003" +) + +// DaBaiSha 充值订单请求体 +type DaBaiSha struct { + MercID string `json:"mercId"` //商户编号 + Type string `json:"type"` // 订单充值类型 + Money string `json:"money"` // 充值⾦额 + TradeNo string `json:"tradeNo"` //商户订单号 + NotifyURL string `json:"notifyUrl"` // 回调地址 + Info DaBaiShaInfo `json:"info"` // 用户信息 + Time string `json:"time"` //UTC时间戳(13位) + Mode string `json:"mode,omitempty"` //式模 持支 表代则传 值定固。填选 + Sign string `json:"sign"` // 签名 + PayMethod string +} + +// DaBaiShaInfo 玩家信息 +type DaBaiShaInfo struct { + PlayerID string `json:"playerId"` // 玩家ID + PlayerIP string `json:"playerIp"` // 玩家IP + DeviceID string `json:"deviceId"` // 玩家设备ID + DeviceType string `json:"deviceType"` // 玩家设备类型 ios pc android + Name string `json:"name"` // 玩家姓名 + Tel string `json:"tel"` // 玩家⼿机号 + AlipayAct string `json:"alipayAct"` // 玩家支付宝账号 +} + +// DaBaiShaRes 回调函数参数model +type DaBaiShaRes struct { + Code int `json:"code" binding:"required"` + MercID string `json:"mercId" binding:"required"` //商户编号 + OID string `json:"oid" binding:"required"` //支付平台订单号 + PayMoney string `json:"payMoney" binding:"required"` //订单到账实际金额 + TradeNo string `json:"tradeNo" binding:"required"` //商户订单号 + Sign string `json:"sign" binding:"required"` //签名 +} + +type QueryMSG struct { + MercID string `json:"mercID"` + TradeNo string `json:"tradeNo"` + Money float32 `json:"money"` + PayMoney float32 `json:"payMoney"` + PayTime string `json:"payTime"` + PayStatus string `json:"payStatus"` + NotifyStatus string `json:"notifyStatus"` +} + +// ToPay 支付发起 +func (this *DaBaiSha) ToPay() (res RchgMsg, err error) { + var errlog string + this.Fill() + msgModel := MsgModel{} + code, err := httputil.DefaultClientPostJsonWithRespWithProxy(appg.ProxyCfg, &msgModel, this.GetURL(), this.GetHeader(), this.GetBody()) + if err != nil { + errlog = fmt.Sprintf("DaBaiSha ToPay requset fail \nerror:%+v\n\ndata:%+v;", err, this) + log.Error(errlog) + err = errors.New(errlog) + return + } + if code != http.StatusOK { + errlog = fmt.Sprintf("DaBaiSha ToPay response status exception;\nstatus %+v;", code) + err = errors.New(errlog) + log.Error(errlog) + return + } + if msgModel.Code != 200 { + switch msgModel.Code { + case 40001: + err = PAY_CHANNEL_CLOSE_ERROR + case 80002: + err = PAY_CHANNEL_BAN_ERROR + default: + bytes, _ := json.Marshal(msgModel) + errlog = fmt.Sprintf("DaBaiSha ToPay be rejected;\n data:%+v:", string(bytes)) + err = errors.New(errlog) + } + log.Error(errlog) + return + } + msg := msgModel.Msg + var buf bytes.Buffer + buf.WriteString(msg["oid"]) + buf.WriteString(msg["payUrl"]) + buf.WriteString(this.GetAppSecret()) + if !VerifySign(msg["sign"], buf.String()) { + errlog = fmt.Sprintf("http goldfish VerifySign fail \nerr :%+v \ndata:%+v", err, msgModel) + err = errors.New(errlog) + log.Error(errlog) + return + } + res.PayUrl = msg["payUrl"] + res.OID = msg["oid"] + res.Mode = msg["mode"] + return +} + +// QueryOrder 订单状态查询发起 +func (g *DaBaiSha) QueryOrder() (msg QueryMSG, err error) { + var res struct { + Code int `json:"code"` + MSG QueryMSG `json:"msg"` + } + body := make(map[string]string) + body["mercId"] = g.GetMercID() + body["tradeNo"] = g.TradeNo + code, err := httputil.DefaultClientGetWithResp(&res, g.GetQueryURL(), nil, body) + if err != nil || code != 200 { + log.Error(fmt.Sprintf("goldfish QueryOrder http.Get fail error:%+v:", err)) + return + } + if res.Code != 200 { + err = errors.New("goldfish QueryOrder http query fail") + return + } + msg = res.MSG + return +} + +// ASenCallBack 阿森结构回调函数 +func (this *DaBaiShaRes) Notify() (RchgBack, error) { + r := RchgBack{} + buf := bytes.Buffer{} + buf.WriteString(strconv.Itoa(this.Code)) + buf.WriteString(this.MercID) + buf.WriteString(this.OID) + buf.WriteString(this.PayMoney) + buf.WriteString(this.TradeNo) + buf.WriteString(dbsc.AppSecret) + if !VerifySign(this.Sign, buf.String()) { + log.Error(fmt.Sprintf("DaBaiSha callback sign verify fail ,data:%+v", this)) + return r, errors.New("check sign fail") + } + g := DaBaiSha{ + TradeNo: this.TradeNo, + } + msg, err := g.QueryOrder() //订单状态查询发起 + if err != nil { + return r, err + } + if msg.PayTime == "" { + return r, errors.New("querry order err,no payTime") + } + loc, _ := time.LoadLocation("Local") + paymentAt, err := time.ParseInLocation("2006-01-02T15:04:05Z07:00", msg.PayTime, loc) + if err != nil { + log.Error("time.ParseInLocation failed", log.Any("TradeNo", this.TradeNo), log.E(err)) + return r, err + } + payMoneyf, err := strconv.ParseFloat(this.PayMoney, 64) + if err != nil { + log.Error(fmt.Sprintf("DaBaiSha callback ParseFloat fail data:%+v:", this)) + return r, fmt.Errorf("invalid payMoney %s", this.PayMoney) + } + r.PayMoney = int64(payMoneyf * 100) + r.PaymentAt = paymentAt + r.TransNo = this.TradeNo + r.OID = this.OID + r.Code = this.Code + return r, nil +} + +func (this *DaBaiShaRes) Success() string { + return "success" +} + +type PayType struct { + Name string `json:"name"` + PayMent string `json:"payMent"` + Type string `json:"type"` +} +type PayTypes struct { + Money string `json:"money"` + Types []PayType `json:"types"` +} + +// QueryOrder 订单状态查询发起 +func (g *DaBaiSha) GetPayType(moneys []string, mode string) (payTypes []PayTypes, err error) { + var errlog string + var res struct { + Code int `json:"code"` + MSG []PayTypes `json:"msg"` + Err string `json:"err"` + } + body := make(map[string]interface{}) + body["mercId"] = g.GetMercID() + body["money"] = moneys + body["time"] = GetUTCTimeMS(time.Now()) + if mode != "" { + body["mode"] = mode + } + var moneysStr bytes.Buffer + for k, v := range moneys { + if k == 0 { + moneysStr.WriteString(v) + } else { + moneysStr.WriteString(",") + moneysStr.WriteString(v) + } + } + fmt.Println(moneysStr.String()) + body["sign"] = md5Sign(dbsc.MercID + moneysStr.String() + dbsc.AppSecret) + code, err := httputil.DefaultClientPostJsonWithResp(&res, g.GetGetTypeURL(), nil, body) + if err != nil { + errlog = fmt.Sprintf("DaBaiSha GetPayType requset fail \nerror:%+v\n\ndata:%+v;", err, body) + log.Error(errlog) + err = errors.New(errlog) + return + } + if code != http.StatusOK { + errlog = fmt.Sprintf("DaBaiSha GetPayType response status exception;\nstatus %+v;", code) + err = errors.New(errlog) + log.Error(errlog) + return + } + if res.Code != 200 { + errlog = fmt.Sprintf("DaBaiSha GetPayType be rejected;\n err:%+v:", res.Err) + err = errors.New(errlog) + log.Error(errlog) + return + } + log.Info(fmt.Sprintf("DaBaiSha GetPayType data:%+v:", res)) + payTypes = res.MSG + return +} + +// Fill 补全配置信息和签名信息 +func (g *DaBaiSha) Fill() { + g.MercID = g.GetMercID() + g.NotifyURL = g.GetCallBackURL() + switch g.PayMethod { + case "alipay": + g.Type = "fixedAlipay" + case "wechat": + g.Type = "fixedWechat" + case "union": + g.Type = "union" + } + g.PayMethod = "" + if g.Info.Name == "" { + g.Info.Name = GetFullName() + } + if g.Info.PlayerIP == "" { + g.Info.PlayerIP = genIpaddr() + } + if g.Info.DeviceID == "" { + g.Info.DeviceID = common.UUID() + } + if g.Info.DeviceType == "" { + g.Info.DeviceType = "ios" + } + g.Sign = g.sign() +} + +// sign 签名 +func (g *DaBaiSha) sign() (sign string) { + var buf bytes.Buffer + buf.WriteString(g.GetMercID()) + buf.WriteString(g.Money) + buf.WriteString(g.GetCallBackURL()) + buf.WriteString(g.TradeNo) + buf.WriteString(g.Type) + buf.WriteString(g.GetAppSecret()) + md5Ctx := md5.New() + md5Ctx.Write(buf.Bytes()) + cipherStr := md5Ctx.Sum(nil) + return hex.EncodeToString(cipherStr) +} + +// GetBody 获取请求body +func (g *DaBaiSha) GetBody() interface{} { + jsonStr, err := json.Marshal(g) + if err != nil { + log.Info(fmt.Sprintf("DaBaiSha GetBody json.Marshal is fail error:%+v/data:%+v", err, g)) + } + sstr := string(jsonStr) + log.Info(fmt.Sprintf("==================%+v", sstr)) + return jsonStr +} + +// GetHeader 获取请求头信息 +func (g *DaBaiSha) GetHeader() map[string]string { + header := make(map[string]string, 0) + return header +} + +// GetURL 获取请求地址 +func (*DaBaiSha) GetURL() string { + return dbsc.ExeUrl +} + +// GetQueryURL 获取请求地址 +func (*DaBaiSha) GetQueryURL() string { + return dbsc.QueryUrl +} + +// GetQueryURL 获取请求地址 +func (*DaBaiSha) GetGetTypeURL() string { + return "https://umio.zjcthbkj.com/api/shark/getPayType" +} + +// GetMercID 获取商户编号 +func (*DaBaiSha) GetMercID() string { + return dbsc.MercID +} + +// GetAppSecret 获取app签名密钥 +func (*DaBaiSha) GetAppSecret() string { + return dbsc.AppSecret +} + +// GetCallBackURL 获取回掉函数 +func (*DaBaiSha) GetCallBackURL() string { + if appg.Conf != nil { + return appg.Conf.URL.LocalWebUrl + dbsc.NotifyUrl + } + return webg.Conf.URL.LocalAppUrl + dbsc.NotifyUrl +} diff --git a/common/rchgutil/dx.go b/common/rchgutil/dx.go new file mode 100644 index 0000000..5de9ff9 --- /dev/null +++ b/common/rchgutil/dx.go @@ -0,0 +1,263 @@ +package rchgutil + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "sort" + "strings" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/web/webg" +) + +var dc = config{ + MercID: "191228889", + AppSecret: "buptpyxrn8agj1pivx3bl90mu91rx6ih", + ExeUrl: "https://api.parado.vip/Pay_Index.html", + QueryUrl: "https://api.parado.vip/Pay_Trade_query.html", + NotifyUrl: "/3rd/defray/callback/dx", + CallbackURL: "/refuse", +} + +// DXZhiFu 充值订单请求体 +type DXZhiFu struct { + MercID string `json:"pay_memberid" sign:"1"` //商户编号 + TradeNo string `json:"pay_orderid" sign:"1"` //商户订单号 + Time string `json:"pay_applydate" sign:"1"` //提交时间2016-12-26 18:18:18 + Type string `json:"pay_bankcode" sign:"1"` // 产品编码 + NotifyURL string `json:"pay_notifyurl" sign:"1"` // 回调地址 + CallbackUrl string `json:"pay_callbackurl" sign:"1"` //支付成功后的跳转地址 + Money string `json:"pay_amount" sign:"1"` // 充值⾦额 + Format string `json:"format"` + PayIP string `json:"pay_ip"` + ProductName string `json:"pay_productname"` // 商品信息 + Sign string `json:"pay_md5sign" ` // 签名 + PayMethod string //付款方式 扫码|h5 +} + +// DXZhiFuRes 回调函数参数model +type DXZhiFuRes struct { + MercID string `json:"memberid" form:"memberid" sign:"1"` //商户编号 + TradeNo string `json:"orderid" form:"orderid" sign:"1"` //商户订单号 + PayMoney string `json:"amount" form:"amount" sign:"1"` //订单到账实际金额 + OID string `json:"transaction_id" form:"transaction_id" sign:"1"` //支付平台订单号 + DateTime string `json:"datetime" form:"datetime" sign:"1"` //交易时间 + Code string `json:"returncode" form:"returncode" sign:"1"` + Sign string `json:"sign" form:"sign" ` //签名 +} + +type DXZhiFuData struct { + PageURL string `json:"pageUrl"` //支付地址 原生 + PayURL string `json:"qrcodeUrl"` //支付地址 + LockSeconds int `json:"lock_seconds"` //有效时间 + Body string `json:"body"` //消息内容 + Money string `json:"payAmount"` //实际支付金额 +} + +// ToPay 支付发起 +func (this *DXZhiFu) ToPay() (payURL string, err error) { + var errlog string + this.Fill() + var msgModel struct { + Status string `json:"status"` + Msg string `json:"msg"` + Data DXZhiFuData `json:"data"` + } + code, err := httputil.DefaultClientPostJsonWithRespWithProxy(appg.ProxyCfg, &msgModel, this.GetURL(), this.GetHeader(), this.GetBody()) + if err != nil { + errlog = fmt.Sprintf("DXZhiFu ToPay Http.PostJson fail error:%+v/,data:%+v;", err, this) + err = errors.New(errlog) + log.Error(errlog) + return + } + if code != http.StatusOK { + errlog = fmt.Sprintf("DXZhiFu ToPay Http.PostJson fail error:%+v/,data:%+v;", err, code) + err = errors.New(errlog) + log.Error(errlog) + return + } + if msgModel.Msg != "success" { + bytes, _ := json.Marshal(msgModel) + errlog = fmt.Sprintf("LeiHuo topay response err :%s:", string(bytes)) + err = errors.New(errlog) + log.Error(errlog) + return + } + payURL = msgModel.Data.PageURL + return +} + +// QueryOrder 订单状态查询发起 +func (this *DXZhiFuRes) QueryOrder() (*time.Time, error) { + var msg struct { + MercID string `json:"memberid" sign:"1"` + OID string `json:"orderid" sign:"1"` + Money string `json:"amount" sign:"1"` + PayTime string `json:"time_end" sign:"1"` + TradeNO string `json:"transaction_id" sign:"1"` + Code string `json:"returncode" sign:"1"` + Status string `json:"trade_state" sign:"1"` + Sign string `json:"sign" sign:"1"` + } + this.MercID = dc.MercID + this.Sign = this.ToSign() + hearder := make(map[string]string) + hearder["Content-Type"] = " application/x-www-form-urlencoded" + code, err := httputil.DefaultClientPostWithRespWithProxy(webg.ProxyCfg, &msg, dc.QueryUrl, hearder, common.StructToMap(*this)) + if err != nil || code != 200 { + log.Error(fmt.Sprintf("DXZhiFu QueryOrder http.Get fail error:%+v:", err)) + return nil, err + } + if msg.Code != "00" { + err = errors.New("DXZhiFu QueryOrder http query fail") + return nil, err + } + if msg.Status != "SUCCESS" { + return nil, errors.New("DXZhiFu order not pay:" + this.TradeNo) + } + loc, _ := time.LoadLocation("Local") + paymentAt, _ := time.ParseInLocation("20060102150405", msg.PayTime, loc) + return &paymentAt, nil +} + +// Fill 补全配置信息和签名信息 +func (this *DXZhiFu) Fill() { + this.Format = "json" + this.MercID = this.GetMercID() + this.NotifyURL = this.GetNotifyURL() + this.Type = this.PayMethod + this.CallbackUrl = this.GetCallBackURL() + if this.PayIP == "" { + this.PayIP = genIpaddr() + } + this.Sign = this.sign() +} + +// sign 签名 +func (this *DXZhiFu) sign() (sign string) { + ss := this.ToStringSlice() + sort.Sort(&ss) + var buf bytes.Buffer + for k, v := range ss { + if k == 0 { + buf.WriteString(v) + } else { + buf.WriteString("&") + buf.WriteString(v) + } + } + buf.WriteString("&key=") + buf.WriteString(this.GetAppSecret()) + log.Info(fmt.Sprintf("DXZhiFu topay sign :%s", buf.String())) + sign = strings.ToUpper(md5Sign(buf.String())) + return +} + +// sign 签名 +func (this *DXZhiFuRes) ToSign() (sign string) { + ss := this.ToStringSlice() + sort.Sort(&ss) + var buf bytes.Buffer + for k, v := range ss { + if k == 0 { + buf.WriteString(v) + } else { + buf.WriteString("&") + buf.WriteString(v) + } + } + buf.WriteString("&key=") + buf.WriteString(dc.AppSecret) + sign = strings.ToUpper(md5Sign(buf.String())) + return +} + +// ToStringSlice 转化为stirng切片切片 +func (this *DXZhiFu) ToStringSlice() StringList { + data := make([]string, 0) + t := reflect.TypeOf(*this) + v := reflect.ValueOf(*this) + for k := 0; k < v.NumField(); k++ { + jsonTag, _ := t.Field(k).Tag.Lookup("json") + signTag, _ := t.Field(k).Tag.Lookup("sign") + if v.Field(k).String() != "" && signTag == "1" { + data = append(data, jsonTag+"="+v.Field(k).String()) + } + } + return data +} + +// ToStringSlice 转化为stirng切片切片 +func (this *DXZhiFuRes) ToStringSlice() StringList { + data := make([]string, 0) + t := reflect.TypeOf(*this) + v := reflect.ValueOf(*this) + for k := 0; k < t.NumField(); k++ { + jsonTag, _ := t.Field(k).Tag.Lookup("json") + signTag, _ := t.Field(k).Tag.Lookup("sign") + if v.Field(k).String() != "" && signTag == "1" { + data = append(data, jsonTag+"="+v.Field(k).String()) + } + } + return data +} + +// GetBody 获取请求body +func (this *DXZhiFu) GetBody() interface{} { + m := make(map[string]string) + jsonStr, err := json.Marshal(this) + if err != nil { + log.Info(fmt.Sprintf("DXZhiFu GetBody json.Marshal is fail error:%+v/data:%+v", err, this)) + } + sstr := string(jsonStr) + _ = json.Unmarshal(jsonStr, &m) + log.Info(fmt.Sprintf("DXZhiFu topay json data :%+v", sstr)) + return m +} + +// GetHeader 获取请求头信息 +func (this *DXZhiFu) GetHeader() map[string]string { + header := make(map[string]string, 0) + return header +} + +// GetURL 获取请求地址 +func (*DXZhiFu) GetURL() string { + return dc.ExeUrl +} + +// GetQueryURL 获取请求地址 +func (*DXZhiFu) GetQueryURL() string { + return dc.QueryUrl +} + +// GetCallBackURL 获取回掉函数 +func (*DXZhiFu) GetNotifyURL() string { + if appg.Conf != nil { + return appg.Conf.URL.LocalWebUrl + dc.NotifyUrl + } + return webg.Conf.URL.LocalAppUrl + dc.NotifyUrl +} + +// GetCallBackURL 获取回掉函数 +func (*DXZhiFu) GetCallBackURL() string { + return dc.CallbackURL +} + +// GetMercID 获取商户编号 +func (*DXZhiFu) GetMercID() string { + return dc.MercID +} + +// GetAppSecret 获取app签名密钥 +func (*DXZhiFu) GetAppSecret() string { + return dc.AppSecret +} diff --git a/common/rchgutil/goldfishExchange.go b/common/rchgutil/goldfishExchange.go new file mode 100644 index 0000000..4f9bc07 --- /dev/null +++ b/common/rchgutil/goldfishExchange.go @@ -0,0 +1,190 @@ +package rchgutil + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/web/webg" +) + +var gec = config{ + MercID: "10393", + AppSecret: "488b521efcfb8780f1e7731b8c1fc3ec", + ExeUrl: "https://zrpu.xtdfin.com/api/exchg/order/apply", + NotifyUrl: "/3rd/defray/callback/echg/goldfish", + QueryUrl: "https://inter.jxjssm.com/api/rechg/order/queryOrder", +} + +// GoldFishExchange 充值订单请求体 +type GoldFishExchange struct { + MercID string `json:"mercId"` //商户编号 + Type string `json:"type"` // 订单充值类型 + Money string `json:"money"` // 充值⾦额 + TradeNo string `json:"tradeNo"` //商户订单号 + NotifyURL string `json:"notifyUrl"` // 回调地址 + ReceCardNo string `json:"receCardNo"` //兑换人银行卡号 + ReceAlipayAct string `json:"receAlipayAct"` //兑换人支付宝账号 + ReceiptName string `json:"receiptName"` //兑款人姓名 + Info GoldFishExchangeInfo `json:"info"` // 用户信息 + Time string `json:"time"` //UTC时间戳(13位) + Sign string `json:"sign"` // 签名 +} + +// GoldFishExchangeInfo 玩家信息 +type GoldFishExchangeInfo struct { + PlayerID string `json:"playerId"` // 玩家ID + PlayerIP string `json:"playerIp"` // 玩家IP + DeviceID string `json:"deviceId"` // 玩家设备ID + DeviceType string `json:"deviceType"` // 玩家设备类型 ios pc android + Name string `json:"name"` // 玩家姓名 + Tel string `json:"tel"` // 玩家⼿机号 + AlipayAct string `json:"alipayAct"` // 玩家支付宝账号 +} + +// GoldFishExchangeRes 回调函数参数model +type GoldFishExchangeRes struct { + Code int `json:"code" binding:"required"` //交易状态值 + MercID string `json:"mercId" binding:"required"` //商户编号 + OID string `json:"oid" binding:"required"` //支付平台订单号 + Money string `json:"money" binding:"required"` //兑换金额 + TradeNo string `json:"tradeNo" binding:"required"` //商户订单号 + Status string `json:"status"` + Mark string `json:"mark"` //拒绝兑换理由 + Sign string `json:"sign" binding:"required"` //签名 +} + +// ToExchg 支付发起 +func (g *GoldFishExchange) ToExchg() (msg ExchgMsg) { + g.Fill() + code, err := httputil.DefaultClientPostWithRespWithProxy(webg.ProxyCfg, &msg, g.GetURL(), g.GetHeader(), g.GetBody()) + if err != nil { + msg.Code = UNKNOWError + msg.Err = fmt.Sprintf("GoldFishExchange ToPay Http.PostJson fail error:%+v/,data:%+v;", err, g) + log.Error(msg.Err) + return + } + if code != http.StatusOK { + msg.Code = UNKNOWError + msg.Err = fmt.Sprintf("GoldFishExchange ToPay Http.PostJsonhttp code err:%+v/,data:%+v;", err, code) + log.Error(msg.Err) + return + } + if msg.Code != 200 { + log.Error(fmt.Sprintf("GoldFishExchange http request err :%+v:", msg)) + return + } + return +} + +// QueryOrder 订单状态查询发起 +func (g *GoldFishExchangeRes) QueryOrder() (msg QueryMSG, err error) { + var res struct { + Code int `json:"code"` + MSG QueryMSG `json:"msg"` + } + body := make(map[string]string) + body["mercId"] = g.MercID + body["tradeNo"] = g.TradeNo + code, err := httputil.DefaultClientPostWithRespWithProxy(webg.ProxyCfg, &res, gec.QueryUrl, nil, body) + if err != nil || code != 200 { + log.Error(fmt.Sprintf("GoldFishExchange QueryOrder http.Get fail error:%+v:", err)) + return + } + if res.Code != 200 { + err = errors.New("GoldFishExchange QueryOrder http query fail") + return + } + msg = res.MSG + return +} + +// Fill 补全配置信息和签名信息 +func (g *GoldFishExchange) Fill() { + g.MercID = g.GetMercID() + g.NotifyURL = g.GetCallBackURL() + g.Sign = g.sign() + if g.Info.Name == "" { + g.Info.Name = GetFullName() + } + if g.Info.PlayerIP == "" { + g.Info.PlayerIP = genIpaddr() + } + if g.Info.DeviceID == "" { + g.Info.DeviceID = common.UUID() + } + if strings.Contains(g.Info.DeviceType, "ios") { + g.Info.DeviceType = "ios" + } else { + g.Info.DeviceType = "android" + } +} + +// sign 签名 +func (g *GoldFishExchange) sign() (sign string) { + var buf bytes.Buffer + buf.WriteString(g.GetMercID()) + buf.WriteString(g.Money) + buf.WriteString(g.GetCallBackURL()) + switch g.Type { + case "alipay": + buf.WriteString(g.ReceAlipayAct) + case "bankcard": + buf.WriteString(g.ReceCardNo) + } + buf.WriteString(g.ReceiptName) + buf.WriteString(g.TradeNo) + buf.WriteString(g.GetAppSecret()) + md5Ctx := md5.New() + md5Ctx.Write(buf.Bytes()) + cipherStr := md5Ctx.Sum(nil) + return hex.EncodeToString(cipherStr) +} + +// GetBody 获取请求body +func (e *GoldFishExchange) GetBody() interface{} { + jsonStr, err := json.Marshal(e) + if err != nil { + log.Info(fmt.Sprintf("GoldFishExchange GetBody json.Marshal is fail error:%+v/data:%+v", err, e)) + } + log.Info(fmt.Sprintf("=====================:%+v", string(jsonStr))) + return jsonStr +} + +// GetHeader 获取请求头信息 +func (g *GoldFishExchange) GetHeader() map[string]string { + header := make(map[string]string, 0) + return header +} + +// GetURL 获取请求地址 +func (*GoldFishExchange) GetURL() string { + return gec.ExeUrl +} + +// GetCallBackURL 获取回掉函数 +func (*GoldFishExchange) GetCallBackURL() string { + if appg.Conf != nil { + return appg.Conf.URL.LocalWebUrl + gec.NotifyUrl + } + return webg.Conf.URL.LocalAppUrl + gec.NotifyUrl +} + +// GetMercID 获取商户编号 +func (*GoldFishExchange) GetMercID() string { + return gec.MercID +} + +// GetAppSecret 获取app签名密钥 +func (*GoldFishExchange) GetAppSecret() string { + return gec.AppSecret +} diff --git a/common/rchgutil/goldfishplus.go b/common/rchgutil/goldfishplus.go new file mode 100644 index 0000000..2b50009 --- /dev/null +++ b/common/rchgutil/goldfishplus.go @@ -0,0 +1,206 @@ +package rchgutil + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/http" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/web/webg" +) + +var gpc = config{ + MercID: "10147", + AppSecret: "62b1e8b914de0647133a0648b17ed480", + ExeUrl: "https://sord.jmsnmall.com/api/djy/topay", + QueryUrl: "https://sord.jmsnmall.com/api/djy/order/queryOrder", + NotifyUrl: "/3rd/defray/callback/goldfishPlus", +} + +// GoldfishPlus 充值订单请求体 +type GoldfishPlus struct { + MercID string `json:"mercId"` //商户编号 + Type string `json:"type"` // 订单充值类型 + Money string `json:"money"` // 充值⾦额 + TradeNo string `json:"tradeNo"` //商户订单号 + NotifyURL string `json:"notifyUrl"` // 回调地址 + Info GoldfishPlusInfo `json:"info"` // 用户信息 + Time string `json:"time"` //UTC时间戳(13位) + Sign string `json:"sign"` // 签名 + PayMethod string +} + +// GoldfishPlusInfo 玩家信息 +type GoldfishPlusInfo struct { + PlayerID string `json:"playerId"` // 玩家ID + PlayerIP string `json:"playerIp"` // 玩家IP + DeviceID string `json:"deviceId"` // 玩家设备ID + DeviceType string `json:"deviceType"` // 玩家设备类型 ios pc android + Name string `json:"name"` // 玩家姓名 + Tel string `json:"tel"` // 玩家⼿机号 + PayAct string `json:"alipayAct"` // 玩家支付宝账号 +} + +// GoldfishPlusRes 回调函数参数model +type GoldfishPlusRes struct { + Code int `json:"code" binding:"required"` + MercID string `json:"mercId" binding:"required"` //商户编号 + OID string `json:"oid" binding:"required"` //支付平台订单号 + PayMoney string `json:"payMoney" binding:"required"` //订单到账实际金额 + TradeNo string `json:"tradeNo" binding:"required"` //商户订单号 + Sign string `json:"sign" binding:"required"` //签名 +} + +// ToPay 支付发起 +func (g *GoldfishPlus) ToPay() (payUrl string, oid string, err error) { + var errlog string + g.Fill() + msgModel := MsgModel{} + code, err := httputil.DefaultClientPostWithRespWithProxy(appg.ProxyCfg, &msgModel, g.GetURL(), g.GetHeader(), g.GetBody()) + if err != nil { + errlog = fmt.Sprintf("GoldfishPlus ToPay Http.PostJson fail error:%+v/,data:%+v;", err, g) + log.Error(errlog) + err = errors.New(errlog) + return + } + if code != http.StatusOK { + errlog = fmt.Sprintf("GoldfishPlus ToPay Http.PostJson fail error:%+v/,data:%+v;", err, code) + err = errors.New(errlog) + log.Error(errlog) + return + } + if msgModel.Code != 200 { + errlog = fmt.Sprintf("GoldfishPlus topay response err :%+v:", msgModel) + err = errors.New(errlog) + log.Error(errlog) + return + } + msg := msgModel.Msg + var buf bytes.Buffer + buf.WriteString(msg["oid"]) + buf.WriteString(msg["payUrl"]) + buf.WriteString(g.GetAppSecret()) + if !VerifySign(msg["sign"], buf.String()) { + errlog = fmt.Sprintf("http GoldfishPlus VerifySign fail err :%+v/data:%+v", err, msgModel) + err = errors.New(errlog) + log.Error(errlog) + return + } + payUrl = msg["payUrl"] + oid = msg["oid"] + return +} + +// QueryOrder 订单状态查询发起 +func (g *GoldfishPlus) QueryOrder() (msg QueryMSG, err error) { + var res struct { + Code int `json:"code"` + MSG QueryMSG `json:"msg"` + } + body := make(map[string]string) + body["mercId"] = g.GetMercID() + body["tradeNo"] = g.TradeNo + code, err := httputil.DefaultClientPostWithRespWithProxy(webg.ProxyCfg, &res, g.GetQueryURL(), nil, body) + if err != nil || code != 200 { + log.Error(fmt.Sprintf("GoldfishPlus QueryOrder http.Get fail error:%+v:", err)) + return + } + if res.Code != 200 { + err = errors.New("GoldfishPlus QueryOrder http query fail") + return + } + msg = res.MSG + return +} + +// Fill 补全配置信息和签名信息 +func (g *GoldfishPlus) Fill() { + g.Type = g.PayMethod + g.MercID = g.GetMercID() + g.NotifyURL = g.GetCallBackURL() + if g.Info.Name == "" { + g.Info.Name = GetFullName() + } + if g.Info.PlayerIP == "" { + g.Info.PlayerIP = genIpaddr() + } + if g.Info.DeviceID == "" { + g.Info.DeviceID = common.UUID() + } + if g.Info.DeviceType == "" { + g.Info.DeviceType = "ios" + } + g.Sign = g.sign() +} + +// sign 签名 +func (g *GoldfishPlus) sign() (sign string) { + var buf bytes.Buffer + buf.WriteString(g.GetMercID()) + buf.WriteString(g.Money) + buf.WriteString(g.GetCallBackURL()) + buf.WriteString(g.TradeNo) + buf.WriteString(g.Type) + buf.WriteString(g.GetAppSecret()) + md5Ctx := md5.New() + md5Ctx.Write(buf.Bytes()) + cipherStr := md5Ctx.Sum(nil) + return hex.EncodeToString(cipherStr) +} + +// GetBody 获取请求body +func (g *GoldfishPlus) GetBody() interface{} { + jsonStr, err := json.Marshal(g) + if err != nil { + log.Info(fmt.Sprintf("GoldfishPlus GetBody json.Marshal is fail error:%+v/data:%+v", err, g)) + } + sstr := string(jsonStr) + log.Info(fmt.Sprintf("==================%+v", sstr)) + return jsonStr +} + +// GetHeader 获取请求头信息 +func (g *GoldfishPlus) GetHeader() map[string]string { + header := make(map[string]string, 0) + return header +} + +// GetURL 获取请求地址 +func (*GoldfishPlus) GetURL() string { + return gpc.ExeUrl +} + +// GetQueryURL 获取请求地址 +func (*GoldfishPlus) GetQueryURL() string { + return gpc.QueryUrl +} + +// GetLowAndUpperURL 获取请求地址 +func (*GoldfishPlus) GetLowAndUpperURL() string { + return gpc.LowAndUpperURL +} + +// GetCallBackURL 获取回掉函数 +func (*GoldfishPlus) GetCallBackURL() string { + if appg.Conf != nil { + return appg.Conf.URL.LocalWebUrl + gpc.NotifyUrl + } + return webg.Conf.URL.LocalAppUrl + gpc.NotifyUrl +} + +// GetMercID 获取商户编号 +func (*GoldfishPlus) GetMercID() string { + return gpc.MercID +} + +// GetAppSecret 获取app签名密钥 +func (*GoldfishPlus) GetAppSecret() string { + return gpc.AppSecret +} diff --git a/common/rchgutil/izhifu.go b/common/rchgutil/izhifu.go new file mode 100644 index 0000000..0209021 --- /dev/null +++ b/common/rchgutil/izhifu.go @@ -0,0 +1,273 @@ +package rchgutil + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "sort" + "strconv" + "time" + + "91porn-server/app/appg" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/web/webg" +) + +var ic = config{ + MercID: "11880027", + AppSecret: "3e3d4c82f1c4aa125a899d1d2daaf999", + ExeUrl: "http://api.kkpay.online:9898/cgi-bin/cashier.do", + QueryUrl: "https://inter.jxjssm.com/api/rechg/order/queryOrder", + LowAndUpperURL: "https://inter.jxjssm.com/api/rechg/lowerAndUpper", + NotifyUrl: "/3rd/defray/callback/izhifu", + CallbackURL: "/refuse", +} + +const IZhiFuSuccess = 1001 + +// IZhiFu 充值订单请求体 +type IZhiFu struct { + MercID int64 `json:"partner_id" sign:"1"` //商户编号 + Channel int `json:"channel_id" sign:"1"` //渠道编号 + PayMethod string `json:"pay_method" sign:"1"` // 支付类型 + TradeNo string `json:"partner_order" sign:"1"` //商户订单号 + UID string `json:"user_id" sign:"1"` //用户id + Money int64 `json:"total_fee" sign:"1"` // 充值⾦额 + CallbackUrl string `json:"back_url" sign:"1"` //支付成功后的跳转地址 + NotifyURL string `json:"notify_url" sign:"1"` // 回调地址 + PayextraParam string `json:"payextra_param" sign:"1"` // 扩展参数 + UserIP string `json:"exter_invoke_ip" sign:"1"` //用户ip + SignType string `json:"sign_type" sign:"1"` //签名类型 + Sign string `json:"sign" ` // 签名 + Type string `json:"type" ` + PayFormat string `json:"pay_format"` //参数类型 +} + +// IZhiFuRes 回调函数参数model +type IZhiFuRes struct { + MercID int64 `json:"partner_id" form:"partner_id" sign:"1"` //商户编号 + ChannelID int `json:"channel_id" form:"channel_id" sign:"1"` //渠道编号 + TradeNo string `json:"partner_order" form:"partner_order" sign:"1"` //商户订单号 + OID string `json:"order_id" form:"order_id" sign:"1"` //支付平台订单号 + UID string `json:"user_id" form:"user_id" sign:"1"` //用户id + Money int64 `json:"total_fee" form:"total_fee" sign:"1"` //订单金额 + PayMoney int64 `json:"real_money" form:"real_money"` //订单到账实际金额 + PayTime string `json:"success_time" form:"success_time" sign:"1"` //交易时间 + Code int `json:"code" form:"code" sign:"1"` + Sign string `json:"sign" form:"sign"` //签名 +} + +type IzhifuMsgData struct { + PayURL string `json:"payurl"` //支付地址 + ChannelID string `json:"channel_id"` //聚到编号 + MercID string `json:"partner_id"` //商户编号 + TradeNo string `json:"partner_order"` //商户订单号 + OID string `json:"order_id"` //支付平台订单号 + UID string `json:"user_id"` //用户ID + Money string `json:"total_fee"` //订单金额 + PayMoney string `json:"real_money"` //实收金额 + PayTime string `json:"success_time"` //支付时间 +} + +type msgModel struct { + Code int `json:"code"` + Data IzhifuMsgData `json:"data"` + Msg string `json:"msg"` +} + +// ToPay 支付发起 +func (this *IZhiFu) ToPay() (payUrl string, err error) { + var errlog string + this.Fill() + var msg msgModel + code, err := httputil.DefaultClientPostWithRespWithProxy(appg.ProxyCfg, &msg, this.GetURL(), this.GetHeader(), this.GetBody()) + if err != nil { + errlog = fmt.Sprintf("izhifu ToPay Http.PostJson fail error:%+v/,data:%+v;", err, this) + log.Error(errlog) + err = errors.New(errlog) + return + } + if code != http.StatusOK { + errlog = fmt.Sprintf("izhifu ToPay Http.PostJson fail error:%+v/,data:%+v;", err, code) + log.Error(errlog) + err = errors.New(errlog) + return + } + if msg.Code != IZhiFuSuccess { + errlog = fmt.Sprintf("izhifu topay response err :%+v:", msg) + err = errors.New(errlog) + log.Error(errlog) + return + } + payUrl = msg.Data.PayURL + return +} + +// QueryOrder 订单状态查询发起 +func (this *IZhiFuRes) QueryOrder() (payTime time.Time, err error) { + msg := msgModel{} + body := make(map[string]string) + body["partner_id"] = ic.MercID + body["partner_order"] = this.TradeNo + body["sign"] = md5Sign("partner_id=" + ic.MercID + "&partner_order=" + this.TradeNo + "&key=" + ic.AppSecret) + code, err := httputil.DefaultClientPostWithRespWithProxy(webg.ProxyCfg, &msg, ic.QueryUrl, nil, body) + if err != nil || code != 200 { + log.Error(fmt.Sprintf("izhifu QueryOrder http.Get fail error:%+v:", err)) + return + } + if msg.Code != IZhiFuSuccess { + err = errors.New("izhifu QueryOrder fail") + log.Error(fmt.Sprintf("izhifu QueryOrder http.Get fail error:%+v:", msg)) + return + } + loc, _ := time.LoadLocation("Local") + payTime, err = time.ParseInLocation("2006-01-02 15:04:05", msg.Data.PayTime, loc) + return +} + +// Fill 补全配置信息和签名信息 +func (this *IZhiFu) Fill() { + this.MercID = this.GetMercID() + this.NotifyURL = this.GetNotifyURL() + this.CallbackUrl = this.GetCallBackURL() + if this.UserIP == "" { + this.UserIP = genIpaddr() + } + channel, _ := strconv.ParseInt(this.PayMethod, 10, 64) + this.Channel = int(channel) + this.SignType = "MD5" + this.PayFormat = "json" + this.Sign = this.sign() +} + +// sign 签名 +func (this *IZhiFu) sign() (sign string) { + ss := this.ToStringSlice() + sort.Sort(&ss) + var buf bytes.Buffer + for k, v := range ss { + if k == 0 { + buf.WriteString(v) + } else { + buf.WriteString("&") + buf.WriteString(v) + } + } + buf.WriteString("&key=") + buf.WriteString(this.GetAppSecret()) + log.Info(fmt.Sprintf("Izhifu topay sign :%s", buf.String())) + return md5Sign(buf.String()) +} + +// sign 签名 +func (this *IZhiFuRes) ToSign() string { + var buf bytes.Buffer + t := reflect.TypeOf(*this) + v := reflect.ValueOf(*this) + for k := 0; k < v.NumField(); k++ { + jsonTag, _ := t.Field(k).Tag.Lookup("json") + signTag, _ := t.Field(k).Tag.Lookup("sign") + fv := v.Field(k) + if signTag == "1" { + if k != 0 { + buf.WriteString("&") + } + buf.WriteString(jsonTag) + buf.WriteString("=") + switch fv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + buf.WriteString(strconv.FormatInt(v.Field(k).Int(), 10)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + buf.WriteString(strconv.FormatUint(v.Field(k).Uint(), 10)) + case reflect.String: + buf.WriteString(v.Field(k).String()) + } + } + } + buf.WriteString("&key=") + buf.WriteString(ic.AppSecret) + log.Info(fmt.Sprintf("Izhifu callback sign:%s", buf.String())) + return md5Sign(buf.String()) +} + +// ToStringSlice 转化为stirng切片切片 +func (this *IZhiFu) ToStringSlice() StringList { + t := reflect.TypeOf(*this) + v := reflect.ValueOf(*this) + data := make([]string, 0, v.NumField()) + for k := 0; k < v.NumField(); k++ { + jsonTag, _ := t.Field(k).Tag.Lookup("json") + signTag, _ := t.Field(k).Tag.Lookup("sign") + fv := v.Field(k).Type() + if signTag == "1" { + switch fv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + data = append(data, jsonTag+"="+strconv.FormatInt(v.Field(k).Int(), 10)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + data = append(data, jsonTag+"="+strconv.FormatUint(v.Field(k).Uint(), 10)) + case reflect.String: + data = append(data, jsonTag+"="+v.Field(k).String()) + } + } + } + return data +} + +// GetBody 获取请求body +func (this *IZhiFu) GetBody() interface{} { + jsonStr, err := json.Marshal(this) + if err != nil { + log.Info(fmt.Sprintf("IZhiFu GetBody json.Marshal is fail error:%+v/data:%+v", err, this)) + } + log.Info(fmt.Sprintf("==================%+v", string(jsonStr))) + return jsonStr +} + +// GetHeader 获取请求头信息 +func (*IZhiFu) GetHeader() map[string]string { + header := make(map[string]string, 0) + return header +} + +// GetURL 获取请求地址 +func (*IZhiFu) GetURL() string { + return ic.ExeUrl +} + +// GetQueryURL 获取请求地址 +func (*IZhiFu) GetQueryURL() string { + return ic.QueryUrl +} + +// GetLowAndUpperURL 获取请求地址 +func (*IZhiFu) GetLowAndUpperURL() string { + return ic.LowAndUpperURL +} + +// GetCallBackURL 获取回掉函数 +func (*IZhiFu) GetNotifyURL() string { + if appg.Conf != nil { + return appg.Conf.URL.LocalWebUrl + ic.NotifyUrl + } + return webg.Conf.URL.LocalAppUrl + ic.NotifyUrl +} + +// GetCallBackURL 获取回掉函数 +func (*IZhiFu) GetCallBackURL() string { + return ic.CallbackURL +} + +// GetMercID 获取商户编号 +func (*IZhiFu) GetMercID() int64 { + m, _ := strconv.ParseInt(ic.MercID, 10, 64) + return m +} + +// GetAppSecret 获取app签名密钥 +func (*IZhiFu) GetAppSecret() string { + return ic.AppSecret +} diff --git a/common/rchgutil/leihuo.go b/common/rchgutil/leihuo.go new file mode 100644 index 0000000..3976763 --- /dev/null +++ b/common/rchgutil/leihuo.go @@ -0,0 +1,238 @@ +package rchgutil + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "sort" + "strconv" + "strings" + _ "strings" + + "91porn-server/app/appg" + "91porn-server/common/httputil" + "91porn-server/common/log" + v10 "91porn-server/common/v10" + "91porn-server/web/webg" + + "github.com/go-ego/gpy" +) + +var lhc = config{ + MercID: "2019888612875", + AppSecret: "8ebff22391be4c88ff7398b63e6084ad", + ExeUrl: "http://bizpay12875.kyjihua.cn/cashier/", + NotifyUrl: "/3rd/defray/callback/leihuo", +} + +// LeiHuo 充值订单请求体 +type LeiHuo struct { + TradeNo string `json:"bizorderid" sign:"1"` //商户订单号 + Type string `json:"channel" sign:"1"` // 产品编码 + NotifyURL string `json:"notifyurl" sign:"1"` // 回调地址 + MercID string `json:"platformid" sign:"1"` //商户编号 + SignType string `json:"signtype" sign:"1"` // + ProductName string `json:"subject" sign:"1"` // 商品信息 + Money string `json:"tradeamount" sign:"1"` // 充值⾦额 + Sign string `json:"sign" form:"sign"` //签名 + PayMethod string `json:"payMethod,omitempty"` //付款方式 扫码|h5 +} + +// LeiHuoRes 回调函数参数model +type LeiHuoRes struct { + TradeNo string `json:"bizOrderID" form:"bizOrderID" sign:"1"` //商户订单号 + MercID string `json:"PlatformID" form:"PlatformID" sign:"1"` //商户编号 + OID string `json:"RequestID" form:"RequestID" sign:"1"` //系统流水号 + SignType string `json:"SignType" form:"SignType" sign:"1"` //签名类型 + Status string `json:"Status" form:"Status" sign:"1"` //支付平台订单号 + PayMoney string `json:"TradeAmount" form:"TradeAmount" sign:"1"` //订单到账实际金额 + Sign string `json:"sign" form:"sign"` //签名 +} + +type LeiHuoData struct { + Code int `json:"code"` + Msg string `json:"msg"` + OID int64 `json:"RequestID"` + PayURL string `json:"Url"` //支付地址 +} + +// ToPay 支付发起 +func (this *LeiHuo) ToPay() (payURL string, err error) { + var errlog string + this.Fill() + var msgModel LeiHuoData + code, err := httputil.DefaultClientPostWithRespWithProxy(appg.ProxyCfg, &msgModel, this.GetURL(), this.GetHeader(), this.GetBody()) + if err != nil { + errlog = fmt.Sprintf("LeiHuo ToPay Http.PostJson fail error:%+v/,data:%+v;", err, this) + err = errors.New(errlog) + log.Error(errlog) + return + } + if code != http.StatusOK { + errlog = fmt.Sprintf("LeiHuo ToPay Http.PostJson fail error:%+v/,data:%+v;", err, code) + err = errors.New(errlog) + log.Error(errlog) + return + } + if msgModel.Code == 1 && msgModel.Msg != "success" { + bytes, _ := json.Marshal(msgModel) + errlog = fmt.Sprintf("LeiHuo topay response err :%s:", string(bytes)) + err = errors.New(errlog) + log.Error(errlog) + return + } + payURL = msgModel.PayURL + return +} + +// Fill 补全配置信息和签名信息 +func (this *LeiHuo) Fill() { + if v10.IsPureChineseChar(this.ProductName) { + a := gpy.NewArgs() + this.ProductName = strings.Join(gpy.LazyConvert(this.ProductName, &a), "-") + } + this.SignType = "MD5" + this.MercID = this.GetMercID() + this.NotifyURL = this.GetNotifyURL() + this.Type = this.PayMethod + this.PayMethod = "" + this.Sign = this.sign() +} + +// sign 签名 +func (this *LeiHuo) sign() (sign string) { + ss := this.ToStringSlice() + sort.Sort(&ss) + var buf bytes.Buffer + for k, v := range ss { + if k == 0 { + buf.WriteString(v) + } else { + buf.WriteString("&") + buf.WriteString(v) + } + + } + buf.WriteString(this.GetAppSecret()) + log.Info(fmt.Sprintf("LeiHuo topay sign :%s", buf.String())) + sign = md5Sign(buf.String()) + return +} + +// sign 签名 +func (this *LeiHuoRes) ToSign() (sign string) { + var buf bytes.Buffer + t := reflect.TypeOf(*this) + v := reflect.ValueOf(*this) + for k := 0; k < v.NumField(); k++ { + jsonTag, _ := t.Field(k).Tag.Lookup("json") + signTag, _ := t.Field(k).Tag.Lookup("sign") + fv := v.Field(k) + if signTag == "1" { + if k != 0 { + buf.WriteString("&") + } + buf.WriteString(jsonTag) + buf.WriteString("=") + switch fv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + buf.WriteString(strconv.FormatInt(v.Field(k).Int(), 10)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + buf.WriteString(strconv.FormatUint(v.Field(k).Uint(), 10)) + case reflect.String: + content := v.Field(k).String() + buf.WriteString(content) + } + + } + } + buf.WriteString(lhc.AppSecret) + sign = md5Sign(buf.String()) + return +} + +// ToStringSlice 转化为stirng切片切片 +func (this *LeiHuo) ToStringSlice() StringList { + t := reflect.TypeOf(*this) + v := reflect.ValueOf(*this) + data := make([]string, 0, v.NumField()) + for k := 0; k < v.NumField(); k++ { + fv := v.Field(k) + jsonTag, _ := t.Field(k).Tag.Lookup("json") + signTag, _ := t.Field(k).Tag.Lookup("sign") + if signTag == "1" { + switch fv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + data = append(data, jsonTag+"="+strconv.FormatInt(v.Field(k).Int(), 10)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + data = append(data, jsonTag+"="+strconv.FormatUint(v.Field(k).Uint(), 10)) + case reflect.Float32, reflect.Float64: + data = append(data, jsonTag+"="+strconv.FormatFloat(v.Field(k).Float(), 'f', 2, 64)) + case reflect.String: + content := v.Field(k).String() + if v10.IsPureChineseChar(content) { + a := gpy.NewArgs() + content = strings.Join(gpy.LazyConvert(content, &a), "-") + } + data = append(data, jsonTag+"="+content) + } + } + } + return data +} + +// GetBody 获取请求body +func (this *LeiHuo) GetBody() interface{} { + m := make(map[string]string) + jsonStr, err := json.Marshal(this) + if err != nil { + log.Info(fmt.Sprintf("LeiHuo GetBody json.Marshal is fail error:%+v/data:%+v", err, this)) + } + sstr := string(jsonStr) + _ = json.Unmarshal(jsonStr, &m) + log.Info(fmt.Sprintf("LeiHuo topay json data :%+v", sstr)) + return m +} + +// GetHeader 获取请求头信息 +func (this *LeiHuo) GetHeader() map[string]string { + header := make(map[string]string, 0) + //header["Content-Type"] = "application/json;charset=utf-8" + return header +} + +// GetURL 获取请求地址 +func (*LeiHuo) GetURL() string { + return lhc.ExeUrl +} + +// GetQueryURL 获取请求地址 +func (*LeiHuo) GetQueryURL() string { + return lhc.QueryUrl +} + +// GetCallBackURL 获取回掉函数 +func (*LeiHuo) GetNotifyURL() string { + if appg.Conf != nil { + return appg.Conf.URL.LocalWebUrl + lhc.NotifyUrl + } + return webg.Conf.URL.LocalAppUrl + lhc.NotifyUrl +} + +// GetCallBackURL 获取回掉函数 +func (*LeiHuo) GetCallBackURL() string { + return lhc.CallbackURL +} + +// GetMercID 获取商户编号 +func (*LeiHuo) GetMercID() string { + return lhc.MercID +} + +// GetAppSecret 获取app签名密钥 +func (*LeiHuo) GetAppSecret() string { + return lhc.AppSecret +} diff --git a/common/rchgutil/lucky.go b/common/rchgutil/lucky.go new file mode 100644 index 0000000..0d383be --- /dev/null +++ b/common/rchgutil/lucky.go @@ -0,0 +1,252 @@ +package rchgutil + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "sort" + "strings" + "time" + + "91porn-server/app/appg" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/web/webg" +) + +var lc = config{ + MercID: "191008006", + AppSecret: "mxhbtu7xzfof1byn44fvf1whz8gd84dc", + ExeUrl: "http://www.jhfsas.xyz/Pay_Index.html", + QueryUrl: "https://inter.jxjssm.com/api/rechg/order/queryOrder", + NotifyUrl: "/3rd/defray/callback/lucky", + CallbackURL: "/refuse", +} + +// Lucky 充值订单请求体 +type Lucky struct { + MercID string `json:"pay_memberid" sign:"1"` //商户编号 + Type string `json:"pay_bankcode" sign:"1"` // 订单充值类型 + Money string `json:"pay_amount" sign:"1"` // 充值⾦额 + TradeNo string `json:"pay_orderid" sign:"1"` //商户订单号 + NotifyURL string `json:"pay_notifyurl" sign:"1"` // 回调地址 + CallbackUrl string `json:"pay_callbackurl" sign:"1"` //支付成功后的跳转地址 + ProductName string `json:"pay_productname"` // 用户信息 + Time string `json:"pay_applydate" sign:"1"` //UTC时间戳(13位) + Sign string `json:"pay_md5sign" ` // 签名 + PayMethod string +} + +// LuckyRes 回调函数参数model +type LuckyRes struct { + Code string `json:"returncode" form:"returncode" sign:"1"` + MercID string `json:"memberid" form:"memberid" sign:"1"` //商户编号 + OID string `json:"transaction_id" form:"transaction_id" sign:"1"` //支付平台订单号 + PayMoney string `json:"amount" form:"amount" sign:"1"` //订单到账实际金额 + TradeNo string `json:"orderid" form:"orderid" sign:"1"` //商户订单号 + DateTime string `json:"datetime" form:"datetime" sign:"1"` //交易时间 + Sign string `json:"sign" form:"sign" ` //签名 +} + +// ToPay 支付发起 +func (l *Lucky) ToPay() (payURL string, err error) { + var errlog string + l.Fill() + var msgModel struct { + Status string `json:"status"` + Msg string `json:"msg"` + URL string `json:"url"` + } + body := l.GetBody() + start := time.Now().UnixNano() / 1e6 + code, err := httputil.DefaultClientPostWithRespWithProxy(appg.ProxyCfg, &msgModel, l.GetURL(), l.GetHeader(), body) + end := time.Now().UnixNano() / 1e6 + period := end - start + if err != nil { + errlog = fmt.Sprintf("lucky ToPay Http.PostJson fail error:%+v/,data:%+v,延迟:%d;", err, code, period) + log.Error(errlog) + err = errors.New(errlog) + return + } + if code != http.StatusOK { + errlog = fmt.Sprintf("lucky ToPay Http.PostJson code is not 200:%+v/,data:%+v,延迟:%d;", err, code, period) + log.Error(errlog) + err = errors.New(errlog) + return + } + + if msgModel.Status != "success" { + bytes, _ := json.Marshal(msgModel) + errlog = fmt.Sprintf("lucky topay response err :%+v:", string(bytes)) + err = errors.New(errlog) + log.Error(errlog) + return + } + payURL = msgModel.URL + return +} + +// QueryOrder 订单状态查询发起 +func (l *Lucky) QueryOrder() (msg map[string]string, err error) { + msgModel := MsgModel{} + body := make(map[string]string) + body["mercId"] = l.GetMercID() + body["tradeNo"] = l.TradeNo + code, err := httputil.DefaultClientPostWithRespWithProxy(webg.ProxyCfg, &msgModel, l.GetQueryURL(), nil, body) + if err != nil || code != 200 { + log.Error(fmt.Sprintf("lucky QueryOrder http.Get fail error:%+v:", err)) + return + } + if msgModel.Code != 200 { + err = errors.New("lucky QueryOrder fail") + return + } + msg = msgModel.Msg + return +} + +// Fill 补全配置信息和签名信息 +func (l *Lucky) Fill() { + l.Type = l.PayMethod + l.MercID = l.GetMercID() + l.NotifyURL = l.GetNotifyURL() + l.CallbackUrl = l.GetCallBackURL() + l.Sign = l.sign() +} + +// sign 签名 +func (l *Lucky) sign() (sign string) { + ss := l.ToStringSlice() + sort.Sort(&ss) + var buf bytes.Buffer + for k, v := range ss { + if k == 0 { + buf.WriteString(v) + } else { + buf.WriteString("&") + buf.WriteString(v) + } + } + buf.WriteString("&key=") + buf.WriteString(l.GetAppSecret()) + fmt.Println("=========" + buf.String()) + md5Ctx := md5.New() + md5Ctx.Write(buf.Bytes()) + cipherStr := md5Ctx.Sum(nil) + return strings.ToUpper(hex.EncodeToString(cipherStr)) +} + +// sign 签名 +func (l *LuckyRes) ToSign() (sign string) { + ss := l.ToStringSlice() + sort.Sort(&ss) + var buf bytes.Buffer + for k, v := range ss { + if k == 0 { + buf.WriteString(v) + } else { + buf.WriteString("&") + buf.WriteString(v) + } + } + buf.WriteString("&key=") + buf.WriteString(lc.AppSecret) + md5Ctx := md5.New() + md5Ctx.Write(buf.Bytes()) + cipherStr := md5Ctx.Sum(nil) + sign = strings.ToUpper(hex.EncodeToString(cipherStr)) + return +} + +// ToStringSlice 转化为stirng切片切片 +func (l *Lucky) ToStringSlice() StringList { + t := reflect.TypeOf(*l) + v := reflect.ValueOf(*l) + data := make([]string, 0, v.NumField()) + for k := 0; k < v.NumField(); k++ { + jsonTag, _ := t.Field(k).Tag.Lookup("json") + signTag, _ := t.Field(k).Tag.Lookup("sign") + if v.Field(k).String() != "" && signTag == "1" { + data = append(data, jsonTag+"="+v.Field(k).String()) + } + } + return data +} + +// ToStringSlice 转化为stirng切片切片 +func (l *LuckyRes) ToStringSlice() StringList { + t := reflect.TypeOf(*l) + v := reflect.ValueOf(*l) + data := make([]string, 0, t.NumField()) + for k := 0; k < t.NumField(); k++ { + jsonTag, _ := t.Field(k).Tag.Lookup("json") + signTag, _ := t.Field(k).Tag.Lookup("sign") + if v.Field(k).String() != "" && signTag == "1" { + data = append(data, jsonTag+"="+v.Field(k).String()) + } + } + return data +} + +// GetBody 获取请求body +func (l *Lucky) GetBody() interface{} { + m := make(map[string]string) + jsonStr, err := json.Marshal(l) + if err != nil { + log.Info(fmt.Sprintf("Lucky GetBody json.Marshal is fail error:%+v/data:%+v", err, l)) + } + sstr := string(jsonStr) + _ = json.Unmarshal(jsonStr, &m) + log.Info(fmt.Sprintf("==================%+v", sstr)) + return m +} + +// GetHeader 获取请求头信息 +func (l *Lucky) GetHeader() map[string]string { + header := make(map[string]string, 0) + header["Content-Type"] = "application/x-www-form-urlencoded" + return header +} + +// GetURL 获取请求地址 +func (*Lucky) GetURL() string { + return lc.ExeUrl +} + +// GetQueryURL 获取请求地址 +func (*Lucky) GetQueryURL() string { + return lc.QueryUrl +} + +// GetLowAndUpperURL 获取请求地址 +func (*Lucky) GetLowAndUpperURL() string { + return lc.LowAndUpperURL +} + +// GetCallBackURL 获取回掉函数 +func (*Lucky) GetNotifyURL() string { + if appg.Conf != nil { + return appg.Conf.URL.LocalWebUrl + lc.NotifyUrl + } + return webg.Conf.URL.LocalAppUrl + lc.NotifyUrl +} + +// GetCallBackURL 获取回掉函数 +func (*Lucky) GetCallBackURL() string { + return lc.CallbackURL +} + +// GetMercID 获取商户编号 +func (*Lucky) GetMercID() string { + return lc.MercID +} + +// GetAppSecret 获取app签名密钥 +func (*Lucky) GetAppSecret() string { + return lc.AppSecret +} diff --git a/common/rchgutil/recharge.go b/common/rchgutil/recharge.go new file mode 100644 index 0000000..749fb57 --- /dev/null +++ b/common/rchgutil/recharge.go @@ -0,0 +1,460 @@ +package rchgutil + +import ( + "91porn-server/common/constant/redisconst" + "bytes" + "context" + "crypto/md5" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/http" + "sort" + "strconv" + "strings" + "time" + + "91porn-server/app/appg" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/common/stderr" + + "github.com/vmihailenco/msgpack/v5" + "golang.org/x/sync/singleflight" +) + +var payTypeCacheGroup singleflight.Group + +type GainPayTypeReq struct { + Time string `json:"time" bson:"time"` // 时间戳,最大长度50,必填 + Sign string `json:"sign" bson:"sign"` // 签名,最大长度100,必填 + MercID string `json:"mercId" bson:"mercId"` // 商户ID,最大长度50,必填 + Money []string `json:"money" bson:"money"` // 金额数组,必填 + Mode string `json:"mode,omitempty"` // 支付模式,兼容商户版本迭代,传sdk表示支持sdk模式,可选 + Payload string `json:"payload,omitempty"` // 备用参数:1-不匹配赔付渠道 2-空单 3-兑换划转,可选 + +} + +type AllPayType struct { + Money string `json:"money" bson:"money"` // 金额 + Types []mercPayTypeInfo `json:"types" bson:"types"` // 支付类型 +} + +type mercPayTypeInfo struct { + Name string `json:"name" bson:"name"` // 支付类型名称 + Type string `json:"type" bson:"type"` // 支付类型 +} + +type GainPayTypeResp struct { + Code int `json:"code" bson:"code"` // code码 + Info string `json:"info" bson:"info"` // code码信息 + Err string `json:"err" bson:"err"` // 错误信息 + Msg []AllPayType `json:"msg" bson:"msg"` // 响应数据 + Tip string `json:"tip" bson:"tip"` // 备注 +} + +// GetPayTypeFromCache 获取支付通道并缓存 +func (in *GainPayTypeReq) GetPayTypeFromCache() (bc []AllPayType, err error) { + // 金额排序 + sort.Strings(in.Money) + + redisKey := fmt.Sprintf("paycenter-channels:%s", strings.Join(in.Money, "-")) + value, err, _ := payTypeCacheGroup.Do(redisKey, func() (interface{}, error) { + str, cacheErr := appg.Redis.Get(redisKey) + if cacheErr != nil { + log.Warn(fmt.Sprintf("[paycenter]缓存获取支付渠道列表信息异常:%v, moneys: %v", cacheErr, in.Money)) + } else if str != nil { + cached, decodeErr := decodeCachedPayTypes(*str) + if decodeErr == nil { + return cached, nil + } + log.Warn(fmt.Sprintf("[paycenter]解析支付渠道缓存数据异常:%v", decodeErr)) + } + + fetched, fetchErr := in.GetPayType() + if fetchErr != nil { + log.Warn(fmt.Sprintf("[paycenter]GetPayType获取支付渠道错误:%v", fetchErr)) + return nil, fetchErr + } + + jsonBytes, encodeErr := json.Marshal(fetched) + if encodeErr != nil { + log.Warn(fmt.Sprintf("[paycenter]序列化支付渠道缓存异常:%v", encodeErr)) + return fetched, nil + } + if cacheErr = appg.Redis.Set(redisKey, string(jsonBytes), redisconst.DataCachExpire); cacheErr != nil { + log.Warn(fmt.Sprintf("[paycenter]保存支付渠道缓存异常:%v", cacheErr)) + } + return fetched, nil + }) + if err != nil { + return nil, err + } + + bc, ok := value.([]AllPayType) + if !ok { + return nil, fmt.Errorf("[paycenter] unexpected cached pay type: %T", value) + } + return bc, nil +} + +func decodeCachedPayTypes(raw string) ([]AllPayType, error) { + var payTypes []AllPayType + jsonErr := json.Unmarshal([]byte(raw), &payTypes) + if jsonErr == nil { + return payTypes, nil + } + + // 兼容历史上可能已经写入 Redis 的 MsgPack 数据;新数据统一使用 JSON。 + msgpackErr := msgpack.Unmarshal([]byte(raw), &payTypes) + if msgpackErr == nil { + return payTypes, nil + } + return nil, fmt.Errorf("json decode: %v; msgpack decode: %v", jsonErr, msgpackErr) +} + +// GetPayType 从支付中心获取充值金额对应的充值方式 +func (in *GainPayTypeReq) GetPayType() ([]AllPayType, error) { + res := GainPayTypeResp{} + code, err := httputil.DefaultClientPostJsonWithResp(&res, in.GetURL(), nil, in.GetBody()) + if err != nil { + log.Error(fmt.Sprintf("param:%+v,get pay type in bill center failed err:%v", in, err)) + return nil, errors.New(fmt.Sprintf("bill GetPayType requset fail \nerror:%+v\n\ndata:%+v;", err, in)) + } + if code != http.StatusOK { + log.Error(fmt.Sprintf("code:%v,get pay type response status exception", code)) + return nil, errors.New(fmt.Sprintf("bill GetPayType response status exception;\nstatus %+v;", code)) + } + if res.Code != 200 { + log.Error(fmt.Sprintf("bill GetPayType be rejected;\n param:%+v, err:%+v:", in, res)) + return nil, errors.New("bill GetPayType be rejected") + } + bytes, err := json.Marshal(res.Msg) + if err != nil { + log.Error(fmt.Sprintf("bill payUrl urlDecode fail error:%+v;", err)) + return nil, errors.New("getPayType json Marshal error") + } + var payTypes []AllPayType + if err = json.Unmarshal(bytes, &payTypes); err != nil { + log.Error(fmt.Sprintf("bill payUrl urlDecode fail error:%+v;", err)) + return nil, errors.New("bill payUrl urlDecode fail error") + } + log.Debug("bill GetPayType success", log.Any("payTypeCount", len(payTypes))) + return payTypes, nil +} + +// GetBody 获取请求body +func (in *GainPayTypeReq) GetBody() []byte { + in.MercID = appg.Conf.PayCenter.MercID + in.Time = Generate13DigitString() + in.Sign = in.GenerateSign() + data, err := json.Marshal(in) + if err != nil { + log.Info(fmt.Sprintf("bill GetBody json.Marshal is fail error:%+v/data:%+v", err, in)) + return nil + } + return data +} + +func Generate13DigitString() string { + // 生成13位时间戳(毫秒级) + timestamp := time.Now().UnixMilli() + return strconv.FormatInt(timestamp, 10) +} + +// generateMD5 生成MD5哈希 +func generateMD5(text string) string { + hash := md5.Sum([]byte(text)) + return hex.EncodeToString(hash[:]) +} + +// GenerateSign 生成加密 +func (in *GainPayTypeReq) GenerateSign() string { + // 默认签名方式1: md5(value+value) + signStr := in.MercID + strings.Join(in.Money, "") + appg.Conf.PayCenter.MercSecret + sign := generateMD5(signStr) + return sign +} + +// GetURL 获取请求地址 +func (in *GainPayTypeReq) GetURL() string { + return appg.Conf.PayCenter.ApiUrl + "/api/shark/getPayType" +} + +type Recharge struct { + Time string `json:"time" bson:"time"` // 时间戳,最大长度50,必填 + Sign string `json:"sign" bson:"sign"` // 签名,最大长度100,必填 + MercID string `json:"mercId" bson:"mercId"` // 商户ID,最大长度50,必填 + Type string `json:"type" bson:"type"` // 支付类型,必填 + Money string `json:"money" bson:"money"` // 金额,必填 + TradeNo string `json:"tradeNo" bson:"tradeNo"` // 交易号,最大长度50,必填 + NotifyUrl string `json:"notifyUrl" bson:"notifyUrl"` // 通知URL,最大长度150,必填 + CancelNotifyUrl string `json:"cancelNotifyUrl,omitempty"` // 退款URL,最大长度150,非必填 + Info PayInfo `json:"info" bson:"info"` // 支付信息对象,必填 + SignType string `json:"signType" bson:"signType"` // 加密方式:1-md5(value+value) 2-md5(key1=value1|key2=value2@@secret),可选 + Mode string `json:"mode" bson:"mode"` // 支付模式,兼容商户版本迭代,传sdk表示支持sdk模式,可选 + Payload string `json:"payload" bson:"payload"` // 备用参数:1-不匹配赔付渠道 2-空单 3-兑换划转,可选 +} + +// PayInfo 支付信息子结构体 +type PayInfo struct { + App string `json:"app" json:"app"` // 产品,最大长度15,必填 + PlayerId string `json:"playerId" bson:"playerId"` // 玩家ID,最大长度50,必填 + PlayerIp string `json:"playerIp" bson:"playerIp"` // 玩家IP,最大长度100,必填 + DeviceType string `json:"deviceType" bson:"deviceType"` // 设备类型,必填 + DeviceId string `json:"deviceId" bson:"deviceId"` // 设备ID,最大长度100,必填 + Name string `json:"name" bson:"name"` // 玩家姓名 + Tel string `json:"tel" bson:"tel"` // 玩家⼿机号 + AlipayAct string `json:"alipayAct" bson:"alipayAct"` // 玩家支付宝账号 +} + +type PayResp struct { + Code stderr.Code `json:"code" bson:"code"` // 状态 + OID string `json:"oid" bson:"oid"` // 订单号 + PayUrl string `json:"payUrl" bson:"payUrl"` // 支付链接 + Mode string `json:"mode" bson:"mode"` // 模式 + Rebate string `json:"rebate" bson:"rebate"` // 通道费率 + Sign string `json:"sign" bson:"sign"` // 加密 +} + +type RechargeResp struct { + Code int `json:"code" bson:"code"` // code码 + Info string `json:"info" bson:"info"` // code码信息 + Err string `json:"err" bson:"err"` // 错误信息 + Msg PayResp `json:"msg" bson:"msg"` // 响应数据 + Tip string `json:"tip" bson:"tip"` // 备注 +} + +type RechargeCallbackResp struct { + Code int `json:"code" binding:"required"` // 编码 + MercID string `json:"mercId" binding:"required"` // 商户编号 + OID string `json:"oid" binding:"required"` // 支付平台订单号 + PayMoney string `json:"payMoney" binding:"required"` // 订单到账实际金额 + TradeNo string `json:"tradeNo" binding:"required"` // 商户订单号 + Sign string `json:"sign" binding:"required"` // 签名 +} + +type RefundCallbackResp struct { + Code int `json:"code" bson:"code"` // 编码 + MercID string `json:"mercId" bson:"mercId"` // 商户编号 + OID string `json:"oid" bson:"oid"` // 支付平台订单号 + TradeNo string `json:"tradeNo" bson:"tradeNo"` // 商户订单号 + Sign string `json:"sign" bson:"sign"` // 签名 +} + +// ToPayNew 获取请求body +func (in *Recharge) ToPayNew(ctx context.Context) (res PayResp, err error) { + defer func() { + log.InfoX(ctx, "bill ToPayNew request info end 2", log.Any("transNo", in.TradeNo), log.Any("res", res)) + if err := recover(); err != nil { + log.ErrorX(ctx, "bill ToPayNew recover", log.Any("Rchg", in), log.Any("panic", err)) + } + }() + in.fill() + startS := time.Now().UnixNano() + + var rechargeResp RechargeResp + code, err := httputil.DefaultClientPostJsonWithResp(&rechargeResp, in.GetURL(), nil, in.GetBody()) + log.InfoX(ctx, "ToPayNew POST resp ", log.Any("code", code), log.Any("transNo", in.TradeNo), + log.Any("topay-bill-time cost", time.Now().UnixNano()-startS)) + if err != nil { + log.ErrorX(ctx, "bill ToPayNew post failed", log.Any("transNo", in.TradeNo), log.E(err)) + err = PayErrPostFailure + return + } + if code != http.StatusOK { + log.ErrorX(ctx, "bill ToPayNew bad http statusCode", log.Any("transNo", in.TradeNo), log.Any("statusCode", code)) + err = PayErrResponseCode + return + } + if rechargeResp.Err != "" { + log.ErrorX(ctx, "bill ToPayNew has been rejected", log.Any("transNo", in.TradeNo), log.Any("res err", rechargeResp.Err)) + err = PayErrBeRejected + return + } + log.InfoX(ctx, "bill ToPayNew request info end", log.Any("transNo", in.TradeNo), log.Any("res", res)) + return rechargeResp.Msg, nil +} + +// fill 组装数据 +func (in *Recharge) fill() { + in.MercID = in.GetMercID() + in.Time = Generate13DigitString() + in.TradeNo = RChgIDAssemble(in.TradeNo) + in.Info.App = appg.Conf.PayCenter.AppName + in.NotifyUrl = in.GetNotifyURL() + in.CancelNotifyUrl = in.GetCancelNotifyURL() + in.SignType = "1" + in.Sign = in.sign() +} + +// GetNotifyURL 获取回调地址 +func (in *Recharge) GetNotifyURL() string { + return appg.Conf.PayCenter.CallbackUrl + "/3rd/defray/callback/pay_center" +} + +// GetCancelNotifyURL 获取退款回调地址 +func (in *Recharge) GetCancelNotifyURL() string { + return appg.Conf.PayCenter.CallbackUrl + "/3rd/defray/callback/refund" +} + +// sign 签名 +func (in *Recharge) sign() (sign string) { + var buf bytes.Buffer + buf.WriteString(in.MercID) + buf.WriteString(in.Money) + buf.WriteString(in.NotifyUrl) + buf.WriteString(in.TradeNo) + buf.WriteString(in.Type) + buf.WriteString(in.GetAppSecret()) + md5Ctx := md5.New() + md5Ctx.Write(buf.Bytes()) + cipherStr := md5Ctx.Sum(nil) + return hex.EncodeToString(cipherStr) +} + +// GetBody 获取请求body +func (in *Recharge) GetBody() []byte { + jsonStr, err := json.Marshal(in) + if err != nil { + log.Info(fmt.Sprintf("DaBaiSha GetBody json.Marshal is fail error:%+v/data:%+v", err, in)) + } + sstr := string(jsonStr) + log.Info(fmt.Sprintf("==================%+v", sstr)) + return jsonStr +} + +// GetURL 获取请求地址 +func (in *Recharge) GetURL() string { + return appg.Conf.PayCenter.ApiUrl + "/api/shark/topay" +} + +// GetBody 获取请求body +//func (in *Recharge) Notify() (RchgBack, error) { +// return in, nil +//} + +// Success 获取请求body +func (in *Recharge) Success() string { + return "success" +} + +func (in *Recharge) GetAppSecret() string { + return appg.Conf.PayCenter.MercSecret +} + +// GetMercID 获取商户编号 +func (in *Recharge) GetMercID() string { + return appg.Conf.PayCenter.MercID +} + +func FenToYuan(price int64) string { + return strconv.FormatInt(price/100, 10) +} + +func YuanToFen(money string) int64 { + // 去除前后空格 + money = strings.TrimSpace(money) + if money == "" { + return 0 + } + + // 检查是否为负数 + isNegative := false + if strings.HasPrefix(money, "-") { + isNegative = true + money = money[1:] + } + + // 按小数点分割 + parts := strings.Split(money, ".") + + switch len(parts) { + case 1: + // 只有整数部分,如 "123", "456" + yuan, err := strconv.ParseInt(parts[0], 10, 64) + if err != nil { + log.Warn(fmt.Sprintf("invalid integer part: %v", err)) + return 0 + } + result := yuan * 100 + if isNegative { + result = -result + } + return result + + case 2: + // 有整数和小数部分,如 "123.45", "78.9" + yuan, err := strconv.ParseInt(parts[0], 10, 64) + if err != nil { + log.Warn(fmt.Sprintf("invalid integer part: %v", err)) + return 0 + } + + // 处理小数部分 + decimalPart := parts[1] + if len(decimalPart) > 2 { + // 如果小数部分超过2位,进行四舍五入或截断 + // 这里选择截断,也可以根据需要改为四舍五入 + decimalPart = decimalPart[:2] + } else if len(decimalPart) == 1 { + // 如果只有1位小数,补零 + decimalPart += "0" + } + + fen, err := strconv.ParseInt(decimalPart, 10, 64) + if err != nil { + log.Warn(fmt.Sprintf("invalid decimal part: %v", err)) + return 0 + } + + result := yuan*100 + fen + if isNegative { + result = -result + } + return result + default: + log.Warn(fmt.Sprintf("invalid money format: %s", money)) + return 0 + } +} + +// GetQueryURL 获取请求地址 +func (in *Recharge) GetQueryURL() string { + return appg.Conf.PayCenter.ApiUrl + "/api/shark/order/queryOrder" +} + +type RechargeQueryResp struct { + Code int `json:"code" bson:"code"` // 响应码 + Msg QueryOrderInfo `json:"msg" bson:"msg"` // 订单信息 +} + +type QueryOrderInfo struct { + MercID string `json:"mercID"` + TradeNo string `json:"tradeNo"` + Money float32 `json:"money"` + PayMoney float32 `json:"payMoney"` + PayTime string `json:"payTime"` + PayStatus string `json:"payStatus"` + NotifyStatus string `json:"notifyStatus"` +} + +// QueryOrder 订单状态查询发起 +func (g *Recharge) QueryOrder() (msg QueryOrderInfo, err error) { + var res RechargeQueryResp + body := make(map[string]string) + body["mercId"] = g.GetMercID() + body["tradeNo"] = g.TradeNo + code, err := httputil.DefaultClientGetWithResp(&res, g.GetQueryURL(), nil, body) + if err != nil || code != 200 { + log.Error(fmt.Sprintf("pay center QueryOrder http.Get fail error:%+v:", err)) + return + } + if res.Code != 200 { + err = errors.New("pay center QueryOrder http query fail") + return + } + msg = res.Msg + return res.Msg, nil +} diff --git a/common/rchgutil/recharge_cache_test.go b/common/rchgutil/recharge_cache_test.go new file mode 100644 index 0000000..4916845 --- /dev/null +++ b/common/rchgutil/recharge_cache_test.go @@ -0,0 +1,61 @@ +package rchgutil + +import ( + "encoding/json" + "testing" + + "github.com/vmihailenco/msgpack/v5" +) + +func TestDecodeCachedPayTypes(t *testing.T) { + want := []AllPayType{{ + Money: "100", + Types: []mercPayTypeInfo{{Name: "支付宝", Type: "alipay"}}, + }} + + tests := []struct { + name string + data func(t *testing.T) []byte + }{ + { + name: "json", + data: func(t *testing.T) []byte { + t.Helper() + data, err := json.Marshal(want) + if err != nil { + t.Fatal(err) + } + return data + }, + }, + { + name: "legacy msgpack", + data: func(t *testing.T) []byte { + t.Helper() + data, err := msgpack.Marshal(want) + if err != nil { + t.Fatal(err) + } + return data + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodeCachedPayTypes(string(tt.data(t))) + if err != nil { + t.Fatalf("decodeCachedPayTypes() error = %v", err) + } + if len(got) != 1 || got[0].Money != want[0].Money || len(got[0].Types) != 1 || got[0].Types[0] != want[0].Types[0] { + t.Fatalf("decodeCachedPayTypes() = %#v, want %#v", got, want) + } + }) + } +} + +func TestDecodeCachedPayTypesRejectsInvalidData(t *testing.T) { + if _, err := decodeCachedPayTypes("not-json-or-msgpack"); err == nil { + t.Fatal("decodeCachedPayTypes() error = nil, want decode error") + } +} diff --git a/common/rchgutil/shark.go b/common/rchgutil/shark.go new file mode 100644 index 0000000..cc8a208 --- /dev/null +++ b/common/rchgutil/shark.go @@ -0,0 +1,205 @@ +package rchgutil + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/http" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/web/webg" +) + +var sc = config{ + MercID: "10230", + AppSecret: "9500fd843c02fe8ca2607e1295215f8b", + ExeUrl: "https://lias.huimu311.com/api/shark/topay", + NotifyUrl: "/3rd/defray/callback/shark", + QueryUrl: "https://lias.huimu311.com/api/shark/order/queryOrder", +} + +// Shark 充值订单请求体 +type Shark struct { + MercID string `json:"mercId"` + Type string `json:"type"` + Money string `json:"money"` + TradeNo string `json:"tradeNo"` + NotifyURL string `json:"notifyUrl"` + Info SharkInfo `json:"info"` + Time string `json:"time"` + Sign string `json:"sign"` + PayMethod string +} + +// SharkInfo 玩家信息 +type SharkInfo struct { + PlayerID string `json:"playerId"` + PlayerIP string `json:"playerIp"` + DeviceID string `json:"deviceId"` + DeviceType string `json:"deviceType"` + Name string `json:"name"` + Tel string `json:"tel"` + PayAct string `json:"payAct"` +} + +// SharkRes 回调函数参数model +type SharkRes struct { + Code int `json:"code" binding:"required"` + MercID string `json:"mercId" binding:"required"` //商户编号 + OID string `json:"oid" binding:"required"` //支付平台订单号 + PayMoney string `json:"payMoney" binding:"required"` //订单到账实际金额 + TradeNo string `json:"tradeNo" binding:"required"` //商户订单号 + Sign string `json:"sign" binding:"required"` //签名 +} + +// ToPay 支付发起 +func (s *Shark) ToPay() (payUrl string, oid string, err error) { + var errlog string + s.Fill() + msgModel := MsgModel{} + code, err := httputil.DefaultClientPostWithRespWithProxy(appg.ProxyCfg, &msgModel, s.GetURL(), s.GetHeader(), s.GetBody()) + if err != nil { + errlog = fmt.Sprintf("shark ToPay Http.PostJson fail error:%+v/,data:%+v;", err, s) + log.Error(errlog) + err = errors.New(errlog) + return + } + if code != http.StatusOK { + errlog = fmt.Sprintf("shark ToPay Http.PostJson fail error:%+v/,data:%+v;", err, code) + err = errors.New(errlog) + log.Error(errlog) + return + } + if msgModel.Code != 200 { + switch msgModel.Code { + case 40001: + err = PAY_CHANNEL_CLOSE_ERROR + default: + errlog = fmt.Sprintf("shark topay response err :%+v/,data:%+v;", err, msgModel) + err = errors.New(errlog) + } + log.Error(errlog) + return + } + msg := msgModel.Msg + var buf bytes.Buffer + buf.WriteString(msg["oid"]) + buf.WriteString(msg["payUrl"]) + buf.WriteString(s.GetAppSecret()) + if !VerifySign(msg["sign"], buf.String()) { + errlog = fmt.Sprintf("http shark VerifySign fail err :%+v/data:%+v", err, msgModel) + err = errors.New(errlog) + log.Error(errlog) + return + } + payUrl = msg["payUrl"] + oid = msg["oid"] + return +} + +// QueryOrder 订单状态查询发起 +func (s *Shark) QueryOrder() (msg QueryMSG, err error) { + var res struct { + Code int `json:"code"` + MSG QueryMSG `json:"msg"` + } + body := make(map[string]string) + body["mercId"] = s.GetMercID() + body["tradeNo"] = s.TradeNo + code, err := httputil.DefaultClientPostWithRespWithProxy(webg.ProxyCfg, &res, s.GetQueryURL(), nil, body) + if err != nil || code != 200 { + log.Error(fmt.Sprintf("shark QueryOrder http.Get fail error:%+v:", err)) + return + } + if res.Code != 200 { + err = errors.New("shark QueryOrder fail") + return + } + msg = res.MSG + return +} + +// Fill 补全配置信息和签名信息 +func (s *Shark) Fill() { + s.Type = s.PayMethod + s.MercID = s.GetMercID() + s.NotifyURL = s.GetCallBackURL() + if s.Info.Name == "" { + s.Info.Name = GetFullName() + } + if s.Info.PlayerIP == "" { + s.Info.PlayerIP = genIpaddr() + } + if s.Info.DeviceID == "" { + s.Info.DeviceID = common.UUID() + } + if s.Info.DeviceType == "" { + s.Info.DeviceType = "ios" + } + s.Sign = s.sign() +} + +// sign 签名 +func (s *Shark) sign() (sign string) { + var buf bytes.Buffer + buf.WriteString(s.GetMercID()) + buf.WriteString(s.Money) + buf.WriteString(s.GetCallBackURL()) + buf.WriteString(s.TradeNo) + buf.WriteString(s.Type) + buf.WriteString(s.GetAppSecret()) + md5Ctx := md5.New() + md5Ctx.Write(buf.Bytes()) + cipherStr := md5Ctx.Sum(nil) + return hex.EncodeToString(cipherStr) +} + +// GetBody 获取请求body +func (s *Shark) GetBody() interface{} { + jsonStr, err := json.Marshal(s) + if err != nil { + log.Error(fmt.Sprintf("shark GetBody json.Marshal fail error:%+v/data:%+v", err, s)) + } + log.Error(fmt.Sprintf("shark topay json body:%+v:", string(jsonStr))) + return jsonStr +} + +// GetHeader 获取请求头 +func (s *Shark) GetHeader() map[string]string { + header := make(map[string]string, 0) + return header +} + +// GetURL 获取请求地址 +func (s *Shark) GetURL() string { + return sc.ExeUrl +} + +// GetQueryURL 获取查询请求地址 +func (s *Shark) GetQueryURL() string { + return sc.QueryUrl +} + +// GetCallBackURL 获取回掉函数 +func (s *Shark) GetCallBackURL() string { + if appg.Conf != nil { + return appg.Conf.URL.LocalWebUrl + sc.NotifyUrl + } + return webg.Conf.URL.LocalAppUrl + sc.NotifyUrl +} + +// GetMercID 获取商户编号 +func (s *Shark) GetMercID() string { + return sc.MercID +} + +// GetAppSecret 获取app签名密钥 +func (s *Shark) GetAppSecret() string { + return sc.AppSecret +} diff --git a/common/rchgutil/yilianfu.go b/common/rchgutil/yilianfu.go new file mode 100644 index 0000000..12fab97 --- /dev/null +++ b/common/rchgutil/yilianfu.go @@ -0,0 +1,260 @@ +package rchgutil + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "sort" + "strings" + + "91porn-server/app/appg" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/web/webg" +) + +var yc = config{ + MercID: "cm0020983778", + AppSecret: "8EB5C35ED06CC6EFE8020D8B67B7E867", + ExeUrl: "http://api.nb536.com/gateway", + NotifyUrl: "/3rd/defray/callback/yilianfu", +} + +// YiLIanFu 充值订单请求体 +type YiLIanFu struct { + Type string `sign:"1" json:"method"` // 产品编码 + Version string `sign:"1" json:"version" ` //版本 + MercID string `sign:"1" json:"mch_id"` //商户编号 + SignType string `sign:"1" json:"sign_type"` //签名类型 + Money string `sign:"1" json:"total_amount" ` // 充值⾦额 + NonceStr string `sign:"1" json:"nonce_str"` //随机字符串 + TradeNo string `sign:"1" json:"mch_order_no"` //商户订单号 + ProductName string `sign:"1" json:"body"` // 商品信息 + CurCode string `sign:"1" json:"cur_code"` //币种 + PayIP string `sign:"1" json:"spbill_create_ip"` //终端ip + Time string `sign:"1" json:"mch_req_time"` //提交时间2016-12-26 18:18:18 + NotifyURL string `sign:"1" json:"notify_url"` // 回调地址 + Sign string `sign:"1" json:"sign" ` // 签名 + PayMethod string `json:"paymethod,omitempty"` //付款方式 扫码|h5 +} + +// YiLIanFuRes 回调函数参数model +type YiLIanFuRes struct { + Type string `sign:"1" json:"method"` // 产品编码 + Version string `sign:"1" json:"version"` + Charset string `sign:"1" json:"charset"` + SignType string `sign:"1" json:"sign_type"` + MercID string `sign:"1" json:"mch_id"` + NonceStr string `sign:"1" json:"nonce_str"` + Sign string `json:"sign"` + ResCode string `sign:"1" json:"result_code"` //业务状态码 + ErrCode string `sign:"1" json:"err_code"` //错误码 + ErrCodeDes string `sign:"1" json:"err_code_des"` //错误原因 + CurCode string `sign:"1" json:"cur_code"` //币种 + Money string `sign:"1" json:"total_amount"` // 充值⾦额 + PayMoney string `sign:"1" json:"real_amount"` //实付金额 + BankOrderNo string `sign:"1" json:"bank_order_no"` //银行订单号 + ChannelOrderNo string `sign:"1" json:"channel_order_no"` //通道订单号 + OID string `sign:"1" json:"order_no"` + TradeNo string `sign:"1" json:"mch_order_no"` //商户订单号 + PayTime string `sign:"1" json:"order_finish_time"` //支付时间 +} + +type YiLIanFuData struct { + Version string `sign:"1" json:"version"` + Charset string `sign:"1" json:"charset"` + SignType string `sign:"1" json:"sign_type"` + Code string `sign:"1" json:"return_code"` + Msg string `sign:"1" json:"return_msg"` + NonceStr string `sign:"1" json:"nonce_str"` + MercID string `sign:"1" json:"mch_id"` + ResCode string `sign:"1" json:"result_code"` //业务状态码 + Sign string `json:"sign"` + ErrCode string `sign:"1" json:"err_code"` //错误码 + ErrCodeDes string `sign:"1" json:"err_code_des"` //错误原因 + OID string `sign:"1" json:"order_no"` + PayUrl string `sign:"1" json:"code_url"` + TimeExpire string `sign:"1" json:"time_expire"` +} + +// ToPay 支付发起 +func (this *YiLIanFu) ToPay() (payURL string, err error) { + var errlog string + this.Fill() + var msg YiLIanFuData + code, err := httputil.DefaultClientPostWithRespWithProxy(appg.ProxyCfg, &msg, this.GetURL(), this.GetHeader(), this.GetBody()) + if err != nil { + errlog = fmt.Sprintf("YiLIanFu ToPay Http.PostJson fail error:%+v/,data:%+v;", err, this) + err = errors.New(errlog) + log.Error(errlog) + return + } + if code != http.StatusOK { + errlog = fmt.Sprintf("YiLIanFu ToPay Http.PostJson fail error:%+v/,data:%+v;", err, code) + err = errors.New(errlog) + log.Error(errlog) + return + } + if msg.ResCode != "SUCCESS" { + bytes, _ := json.Marshal(msg) + errlog = fmt.Sprintf("YiLIanFu topay response err :%+v:", string(bytes)) + err = errors.New(errlog) + log.Error(errlog) + return + } + payURL = msg.PayUrl + return +} + +// Fill 补全配置信息和签名信息 +func (this *YiLIanFu) Fill() { + this.Version = "1.0" + this.Type = this.PayMethod + this.PayMethod = "" + this.MercID = this.GetMercID() + this.NotifyURL = this.GetNotifyURL() + this.SignType = "MD5" + this.NonceStr = RandStringBytesMaskImpr(10) + this.CurCode = "CNY" + if this.PayIP == "" { + this.PayIP = genIpaddr() + } + this.Sign = this.sign() +} + +// sign 签名 +func (this *YiLIanFu) sign() string { + ss := this.ToStringSlice() + sort.Sort(&ss) + var buf bytes.Buffer + for k, v := range ss { + if k == 0 { + buf.WriteString(v) + } else { + buf.WriteString("&") + buf.WriteString(v) + } + + } + buf.WriteString(this.GetAppSecret()) + str := buf.String() + log.Info(fmt.Sprintf("YiLIanFu topay sign :%s", str)) + return strings.ToUpper(md5Sign(buf.String())) +} + +// sign 签名 +func (this *YiLIanFuRes) ToSign() string { + ss := this.ToStringSlice() + sort.Sort(&ss) + var buf bytes.Buffer + for k, v := range ss { + if k == 0 { + buf.WriteString(v) + } else { + buf.WriteString("&") + buf.WriteString(v) + } + } + buf.WriteString(yc.AppSecret) + return strings.ToUpper(md5Sign(buf.String())) +} + +// ToStringSlice 转化为stirng切片切片 +func (this *YiLIanFu) ToStringSlice() StringList { + t := reflect.TypeOf(*this) + v := reflect.ValueOf(*this) + data := make([]string, 0, v.NumField()) + for k := 0; k < v.NumField(); k++ { + jsonTag, _ := t.Field(k).Tag.Lookup("json") + signTag, _ := t.Field(k).Tag.Lookup("sign") + if v.Field(k).String() != "" && signTag == "1" { + data = append(data, jsonTag+"="+v.Field(k).String()) + } + } + return data +} + +// ToStringSlice 转化为stirng切片切片 +func (this *YiLIanFuRes) ToStringSlice() StringList { + t := reflect.TypeOf(*this) + v := reflect.ValueOf(*this) + data := make([]string, 0, t.NumField()) + for k := 0; k < t.NumField(); k++ { + jsonTag, _ := t.Field(k).Tag.Lookup("json") + signTag, _ := t.Field(k).Tag.Lookup("sign") + if v.Field(k).String() != "" && signTag == "1" { + data = append(data, jsonTag+"="+v.Field(k).String()) + } + } + return data +} + +// ToStringSlice 转化为stirng切片切片 +func (this *YiLIanFuData) ToStringSlice() StringList { + t := reflect.TypeOf(*this) + v := reflect.ValueOf(*this) + data := make([]string, 0, t.NumField()) + for k := 0; k < t.NumField(); k++ { + jsonTag, _ := t.Field(k).Tag.Lookup("json") + signTag, _ := t.Field(k).Tag.Lookup("sign") + if v.Field(k).String() != "" && signTag == "1" { + data = append(data, jsonTag+"="+v.Field(k).String()) + } + } + return data +} + +// GetBody 获取请求body +func (this *YiLIanFu) GetBody() interface{} { + m := make(map[string]string) + jsonStr, err := json.Marshal(this) + if err != nil { + log.Info(fmt.Sprintf("YiLIanFu GetBody json.Marshal is fail error:%+v/data:%+v", err, this)) + } + sstr := string(jsonStr) + _ = json.Unmarshal(jsonStr, &m) + log.Info(fmt.Sprintf("YiLIanFu topay json data :%+v", sstr)) + return m +} + +// GetHeader 获取请求头信息 +func (this *YiLIanFu) GetHeader() map[string]string { + header := make(map[string]string, 0) + return header +} + +// GetURL 获取请求地址 +func (*YiLIanFu) GetURL() string { + return yc.ExeUrl +} + +// GetQueryURL 获取请求地址 +func (*YiLIanFu) GetQueryURL() string { + return yc.QueryUrl +} + +// GetCallBackURL 获取回掉函数 +func (*YiLIanFu) GetNotifyURL() string { + if appg.Conf != nil { + return appg.Conf.URL.LocalWebUrl + yc.NotifyUrl + } + return webg.Conf.URL.LocalAppUrl + yc.NotifyUrl +} + +// GetCallBackURL 获取回掉函数 +func (*YiLIanFu) GetCallBackURL() string { + return yc.CallbackURL +} + +// GetMercID 获取商户编号 +func (*YiLIanFu) GetMercID() string { + return yc.MercID +} + +// GetAppSecret 获取app签名密钥 +func (*YiLIanFu) GetAppSecret() string { + return yc.AppSecret +} diff --git a/common/rchgutil/yongli.go b/common/rchgutil/yongli.go new file mode 100644 index 0000000..8dbdfc9 --- /dev/null +++ b/common/rchgutil/yongli.go @@ -0,0 +1,214 @@ +package rchgutil + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "sort" + "strings" + "time" + + "91porn-server/app/appg" + "91porn-server/common/httputil" + "91porn-server/common/log" +) + +var ylc = config{ + MercID: "xq032", + AppSecret: "635379bb1abe7ed814c95e083480b09aa2d47d5a", + ExeUrl: "https://www.hsipxix.cn/?c=Pay", + NotifyUrl: "/3rd/defray/callback/rchg/yongli", +} + +// YongLi 充值订单请求体 +type YongLi struct { + MercID string `json:"mch_id" sign:"1"` //商户编号 + Type string `json:"ptype" sign:"1"` // 订单充值类型 + TradeNo string `json:"order_sn" sign:"1"` //商户订单号 + Money string `json:"money" sign:"1"` // 支付金额:元 + NotifyURL string `json:"notify_url" sign:"1"` // 回调地址 + Time string `json:"time" sign:"1"` + Sign string `json:"sign"` // 签名 + PayMethod string `json:"-"` + Format string `json:"format" sign:"1"` + Desc string `json:"goods_desc" sign:"1"` + IP string `json:"client_ip" sign:"1"` +} + +type YongLiMsgData struct { + Type string `json:"ptype" sign:"1"` // 订单充值类型 + PayTypeName string `json:"ptype_name"` + RealNmae string `json:"realnmae"` //姓名 + Act string `json:"account"` //账户 + PayMoney string `json:"pay_money"` + Bank string `json:"bank"` //银行 + QRcode string `json:"qrcode"` //二维码 + PayURL string `json:"pay_url"` // + OID string `json:"order_sn"` +} + +type YongLiMsg struct { + Code string `json:"code" sign:"1"` //交易返回码,返回SUCCESS 表示交易成功(不代表最终结果), + Data interface{} `json:"data"` + Msg string `json:"msg"` //签名 +} + +// RYRes 回调函数参数model +type YongLiBack struct { + TradeNo string `json:"sh_order" form:"sh_order" sign:"1"` //商户订单号 + OID string `json:"pt_order" form:"pt_order" sign:"1"` //订单号 + PayMoney float32 `json:"money" form:"money" sign:"1"` //订单到账实际金额 + PayTime int64 `json:"time" form:"time" sign:"1"` //支付时间 + Status string `json:"status" form:"status" sign:"1"` //交易结果 1(成功) + Sign string `json:"sign" form:"sign" ` //签名 +} + +// ToPay 支付发起 +func (this *YongLi) ToPay() (res RchgMsg, err error) { + var errlog string + this.Fill() + msg := YongLiMsg{} + code, err := httputil.DefaultClientPostJsonWithResp(&msg, this.GetURL(), this.GetHeader(), this.GetBody()) + if err != nil { + errlog = fmt.Sprintf("yongli ToPay requset fail \nerror:%+v\n\ndata:%+v;", err, this) + log.Error(errlog) + err = errors.New(errlog) + return + } + if code != http.StatusOK { + errlog = fmt.Sprintf("yongli ToPay response status exception;\nstatus %+v;", code) + log.Error(errlog) + err = errors.New(errlog) + return + } + if msg.Code != "1" { + errlog = fmt.Sprintf("yongli ToPay be rejected;\n msg:%+v:", msg) + err = errors.New(errlog) + log.Error(errlog) + return + } + var data YongLiMsgData + bytes, err := json.Marshal(msg.Data) + if err != nil { + errlog = fmt.Sprintf("yongli payUrl urlDecode fail \nerror:%+v;", err) + log.Error(errlog) + err = errors.New(errlog) + return + } + if err = json.Unmarshal(bytes, &data); err != nil { + errlog = fmt.Sprintf("yongli payUrl urlDecode fail \nerror:%+v;", err) + log.Error(errlog) + err = errors.New(errlog) + return + } + res.PayUrl = data.PayURL + res.OID = data.OID + res.Mode = "url" + return +} + +// ASenCallBack 阿森结构回调函数 +func (this *YongLiBack) Notify() (RchgBack, error) { + r := RchgBack{} + if this.Sign != this.ToSign() { + err := errors.New("check sign fail") + log.Error(fmt.Sprintf("ASen callback sign verify fail error:%+v:", err)) + return r, err + } + r.Code = 200 + if this.Status != "success" { + r.Code = 400 + } + r.PaymentAt = time.Unix(this.PayTime, 0) + r.PayMoney = int64(this.PayMoney * 100) + r.TransNo = this.TradeNo + r.OID = this.OID + return r, nil +} + +func (this *YongLiBack) Success() string { + return "success" +} + +// Fill 补全配置信息和签名信息 +func (this *YongLi) Fill() { + switch this.PayMethod { + case "alipay": + this.Type = "12" + case "wechat": + this.Type = "2" + } + this.PayMethod = "" + this.Format = "json" + this.Desc = "充值" + this.IP = genIpaddr() + this.MercID = this.GetMercID() + this.NotifyURL = this.GetCallBackURL() + this.Sign = this.sign() +} + +// sign 签名 +func (this *YongLi) sign() string { + ss := ToStringSlice(*this) + sort.Sort(&ss) + var buf bytes.Buffer + for k := range ss { + buf.WriteString(ss[k]) + buf.WriteString("&") + } + buf.WriteString("key=" + ylc.AppSecret) + s := buf.String() + log.Info(fmt.Sprintf("--------------%s", s)) + return strings.ToLower(md5Sign(s)) + +} + +// sign 签名 +func (this *YongLiBack) ToSign() string { + ss := ToStringSlice(*this) + sort.Sort(&ss) + var buf bytes.Buffer + for k := range ss { + buf.WriteString(ss[k]) + buf.WriteString("&") + } + buf.WriteString("key=" + ylc.AppSecret) + log.Info(fmt.Sprintf("--------------%s", buf.String())) + return strings.ToLower(md5Sign(buf.String())) +} + +// GetBody 获取请求body +func (this *YongLi) GetBody() interface{} { + m := make(map[string]string) + jsonStr, err := json.Marshal(this) + if err != nil { + log.Info(fmt.Sprintf("YongLi GetBody json.Marshal is fail error:%+v/data:%+v", err, this)) + } + // sstr := string(jsonStr) + _ = json.Unmarshal(jsonStr, &m) + log.Info(fmt.Sprintf("==================%+v", m)) + return m +} + +// GetHeader 获取请求头信息 +func (g *YongLi) GetHeader() map[string]string { + header := make(map[string]string, 0) + return header +} + +// GetURL 获取请求地址 +func (*YongLi) GetURL() string { + return ylc.ExeUrl +} + +// GetCallBackURL 获取回掉函数 +func (*YongLi) GetCallBackURL() string { + return appg.Conf.URL.LocalWebUrl + ylc.NotifyUrl +} + +// GetMercID 获取商户编号 +func (*YongLi) GetMercID() string { + return ylc.MercID +} diff --git a/common/rchgutil/yongliwanshun.go b/common/rchgutil/yongliwanshun.go new file mode 100644 index 0000000..bd5a9de --- /dev/null +++ b/common/rchgutil/yongliwanshun.go @@ -0,0 +1,207 @@ +package rchgutil + +import ( + "crypto/rc4" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "91porn-server/app/appg" + "91porn-server/common/httputil" + "91porn-server/common/log" +) + +var ylws = config{ + MercID: "10105", + AppSecret: "DA1C5476400B1D", + ExeUrl: "http://x.aee7.com/gateway/index/checkpoint.do?", + NotifyUrl: "/3rd/defray/callback/rchg/", +} + +// 订单请求体 +type WsylInfo struct { + AccountId string `json:"account_id"` // 商户ID + ContentType string `json:"content_type"` // 返回的网页类型 + Thoroughfare string `json:"thoroughfare"` // 通道 + Type string `json:"type"` // 支付类型 + OutTradeNo string `json:"out_trade_no"` // 商户订单号 + Robin string `json:"robin"` // 轮训 + UseCity string `json:"use_city"` // 按地区收款 + Amount string `json:"amount"` // 支付金额 + CallbackUrl string `json:"callback_url"` // 异步通知地址 + SuccessUrl string `json:"success_url"` // 支付成功后网页自动跳转地址 + ErrorUrl string `json:"error_url"` // 支付失败时,或支付超时后网页自动跳转地址 + Sign string `json:"sign"` // 签名 必填 +} + +type WsylMsgData struct { + OrderId string `json:"order_id"` + Qrcode string `json:"qrcode"` +} + +type WsylMsg struct { + Code int `json:"code"` // 交易返回码,返回SUCCESS 表示交易成功(不代表最终结果), + Msg string `json:"msg"` // 签名 + Data interface{} `json:"data,omitempty"` +} + +// 回调函数参数 +type WsylResultRes struct { + AccountName string `form:"account_name" json:"account_name"` // 商户登录名 + PayTime string `form:"pay_time" json:"pay_time"` // 支付成功的时间戳(10位) + Status string `form:"status" json:"status"` // 状态 + Amount string `form:"amount" json:"amount"` // 订单到账实际金额 + OutTradeNo string `form:"out_trade_no" json:"out_trade_no"` // 订单信息,在发起订单时附加的信息,如用户名,充值订单号等字段参数 + TradeNo string `form:"trade_no" json:"trade_no"` // 交易流水号,由系统生成的交易流水号 + Fees string `form:"fees" json:"fees"` // 手续费,本次回调过程产生的手续费用(已经在平台账户中扣除) + Sign string `form:"sign" json:"sign"` // 签名 + CallbackTime string `form:"callback_time" json:"callback_time"` // 回调时间,在回调时产生的时间戳(10位) + Type string `form:"type" json:"type"` // 支付类型 + AccountKey string `form:"account_key" json:"account_key"` // 商户KEY +} + +// ToPay 支付发起 +func (wyi *WsylInfo) toPay() (res RchgMsg, err error) { + var errLog string + msg := WsylMsg{} + wyi.fill() + code, err := httputil.DefaultClientPostJsonWithResp(&msg, ylws.ExeUrl, wyi.getHeader(), wyi.getBody()) + if err != nil { + errLog = fmt.Sprintf("Wsyl ToPay requset fail \nerror:%+v\n\ndata:%+v;", err, wyi) + log.Error(errLog) + err = errors.New(errLog) + return + } + if code != http.StatusOK { + errLog = fmt.Sprintf("Wsyl ToPay response status exception;\nstatus %+v;", code) + log.Error(errLog) + err = errors.New(errLog) + return + } + if msg.Code != 200 { + errLog = fmt.Sprintf("Wsyl ToPay be rejected;\n msg:%+v:", msg) + err = errors.New(errLog) + log.Error(errLog) + return + } + var data WsylMsgData + bytes, err := json.Marshal(msg.Data) + if err != nil { + errLog = fmt.Sprintf("Wsyl payUrl urlDecode fail \nerror:%+v;", err) + log.Error(errLog) + err = errors.New(errLog) + return + } + if err = json.Unmarshal(bytes, &data); err != nil { + errLog = fmt.Sprintf("Wsyl payUrl urlDecode fail \nerror:%+v;", err) + log.Error(errLog) + err = errors.New(errLog) + return + } + res.PayUrl = data.Qrcode + res.Mode = "url" + res.OID = data.OrderId + return +} + +// Notify 回调函数 +func (this *WsylResultRes) Notify() (RchgBack, error) { + r := RchgBack{} + if this.SignRes() != this.Sign { + log.Error(fmt.Sprintf("Wsyl callback sign verify fail ,data:%+v", this)) + return r, errors.New("check sign fail") + } + if this.PayTime == "" { + return r, errors.New("querry order err,no payTime") + } + sec, err := strconv.ParseInt(this.PayTime, 10, 64) + if err != nil { + log.Error("time.ParseInLocation failed", log.Any("TradeNo", this.TradeNo), log.E(err)) + return r, err + } + paymentAt := time.Unix(sec, 0) + payMoneyf, err := strconv.ParseFloat(this.Amount, 64) + if err != nil { + log.Error(fmt.Sprintf("Wsyl callback ParseFloat fail data:%+v:", this)) + return r, fmt.Errorf("invalid payMoney %s", this.Amount) + } + r.PayMoney = int64(payMoneyf * 100) + r.PaymentAt = paymentAt + r.TransNo = this.OutTradeNo + r.OID = this.TradeNo + r.Code = 400 + if this.Status == "success" { + r.Code = 200 + } + return r, nil +} + +func (this *WsylResultRes) Success() string { + return "success" +} + +// 补全配置信息和签名信息 +func (wyi *WsylInfo) fill() { + wyi.AccountId = ylws.MercID + wyi.ContentType = "json" + wyi.Thoroughfare = "paofen_auto" + wyi.Type = "71" + wyi.Robin = "2" + wyi.UseCity = "2" + wyi.CallbackUrl = wyi.getNotifyURL() + wyi.SuccessUrl = wyi.getNotifyURL() + wyi.ErrorUrl = wyi.getNotifyURL() + wyi.Sign = wyi.signReq() +} + +// 充值请求签名 +func (wyi *WsylInfo) signReq() (sign string) { + return DealSign(ylws.AppSecret, wyi.Amount, wyi.OutTradeNo) +} + +func DealSign(key_id string, amount string, out_trade_no string) (sign string) { + var data = amount + out_trade_no + md5Str := md5Sign(data) + c, err := rc4.NewCipher([]byte(key_id)) + if err != nil { + // log.Fatalln(err) + return + } + src := []byte(md5Str) + dst := make([]byte, len(src)) + c.XORKeyStream(dst, src) + fmt.Println(string(src), string(dst)) + rc4Str := md5Sign(string(dst)) + return rc4Str +} + +// 充值成功回调签名 +func (wyc *WsylResultRes) SignRes() (sign string) { + return DealSign(ylws.AppSecret, wyc.Amount, wyc.OutTradeNo) +} + +// 获取请求body +func (wyi *WsylInfo) getBody() interface{} { + m := make(map[string]string) + jsonStr, err := json.Marshal(wyi) + if err != nil { + log.Info(fmt.Sprintf("WSYL GetBody json.Marshal is fail error:%+v/data:%+v", err, wyi)) + } + sstr := string(jsonStr) + log.Info(fmt.Sprintf("==================%+v", sstr)) + _ = json.Unmarshal(jsonStr, &m) + return m +} + +// 获取请求头 +func (wyi *WsylInfo) getHeader() map[string]string { + return nil +} + +// 获取回掉函数 +func (wyi *WsylInfo) getNotifyURL() (url string) { + return appg.Conf.URL.LocalWebUrl + ylws.NotifyUrl + YongLiWanShun +} diff --git a/common/rchgutil/zhongfu.go b/common/rchgutil/zhongfu.go new file mode 100644 index 0000000..93a92ea --- /dev/null +++ b/common/rchgutil/zhongfu.go @@ -0,0 +1,264 @@ +package rchgutil + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "sort" + "strconv" + "strings" + "time" + + "91porn-server/app/appg" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/web/webg" +) + +var zfc = config{ + MercID: "191309702", + AppSecret: "oaha2s6ed67buc2rpy7of1p3mj17jpqwa0i0n3fjy633et7thwzmv1s3zbwaim9z2o4r3km799u8lp7a1luh1vpd350bazq5bmph66ffm3p9u8trow5lzmtmsx6b66rkya1up22x1rbjvhuby7wkpcidix064ay25d6ri48boo5f5fc0m05oi558d154qw6816p7ym1h52r2gsbndo6pi5i7qgonqp7aagkrh5ar3k3ircgvunqmcfbd4z45k9nz", + ExeUrl: "http://www.518006.com/Pay_Index.html", + QueryUrl: "http://www.518006.com/Pay_Trade_query.html", + NotifyUrl: "/3rd/defray/callback/zhongfu", + CallbackURL: "/refuse", + VerifySecret: "muykk2dlwnmvsn5m5eg5ajs8vhfvfdihqqc2t2rd0297h2bwnkhf2jrk6qveffog038jsfsk4js0yjmkb9ydeijdtjbq98ouh8eqm04gvva3ql6mo0yknaq1v377mw1j", +} + +// ZhongFu 充值订单请求体 +type ZhongFu struct { + Version string `json:"pay_version" sign:"1"` + MercID string `json:"pay_memberid" sign:"1"` //商户编号 + Type string `json:"pay_bankcode" sign:"1"` + TradeNo string `json:"pay_orderid" sign:"1"` //商户订单号 + Money string `json:"pay_amount" sign:"1"` // 充值⾦额 + CallbackUrl string `json:"pay_callbackurl" sign:"1"` //支付成功后的跳转地址 + NotifyURL string `json:"pay_notifyurl" sign:"1"` // 回调地址 + ProductName string `json:"pay_productname"` // 商品信息 + UserIP string `json:"pay_ip" sign:"1"` //用户ip + SignType string `json:"pay_signtype" sign:"1"` //签名类型 + Charset string `json:"pay_inputcharset" sign:"1"` + Time string `json:"pay_applydate" sign:"1"` + PayFormat string `json:"pay_retformat"` //参数类型 + Sign string `json:"pay_md5sign" ` // 签名 + PayMethod string `json:"pay_method,omitempty"` // 支付类型 +} + +// ZhongFuRes 回调函数参数model +type ZhongFuRes struct { + Version string `json:"version" form:"version" sign:"1"` + MercID string `json:"memberid" form:"memberid" sign:"1"` //商户编号 + TradeNo string `json:"orderid" form:"orderid" sign:"1"` //商户订单号 + OID string `json:"transaction_id" form:"transaction_id" sign:"1"` //支付平台订单号 + PayMoney string `json:"amount" form:"amount" sign:"1"` //订单到账实际金额 + PayTime string `json:"datetime" form:"datetime" sign:"1"` //交易时间 + Code string `json:"returncode" form:"returncode" sign:"1"` + Sign string `json:"sign" form:"sign"` //签名 +} + +// ToPay 支付发起 +func (this *ZhongFu) ToPay() (payUrl string, err error) { + defer func(err *error, payUrl string, obj interface{}) { + if err == nil && payUrl == "" { + fmt.Printf("zhongfu ToPay Http.PostJson fail error:data %+v;\r\n", this) + } + }(&err, payUrl, this) + var errlog string + this.Fill() + var msg struct { + PayURL string `json:"payurl"` + Msg string `json:"msg"` + Data interface{} `json:"data"` + Status string `json:"status"` + } + code, err := httputil.DefaultClientPostWithRespWithProxy(appg.ProxyCfg, &msg, this.GetURL(), this.GetHeader(), this.GetBody()) + if err != nil { + errlog = fmt.Sprintf("zhongfu ToPay Http.PostJson fail error:%+v/,data:%+v;", err, this) + log.Error(errlog) + err = errors.New(errlog) + return + } + if code != http.StatusOK { + errlog = fmt.Sprintf("zhongfu ToPay Http.PostJson fail error:%+v/,data:%+v;", err, code) + log.Error(errlog) + err = errors.New(errlog) + return + } + if msg.Msg != "success" { + errlog = fmt.Sprintf("zhongfu topay response err :%+v:", msg) + err = errors.New(errlog) + log.Error(errlog) + return + } + payUrl = msg.PayURL + return +} + +// QueryOrder 订单状态查询发起 +func (this *ZhongFuRes) QueryOrder() (payTime time.Time, err error) { + msg := msgModel{} + body := make(map[string]string) + body["partner_id"] = zfc.MercID + body["partner_order"] = this.TradeNo + body["sign"] = md5Sign("partner_id=" + zfc.MercID + "&partner_order=" + this.TradeNo + "&key=" + zfc.AppSecret) + code, err := httputil.DefaultClientPostWithRespWithProxy(webg.ProxyCfg, &msg, zfc.QueryUrl, nil, body) + if err != nil || code != 200 { + log.Error(fmt.Sprintf("zhongfu QueryOrder http.Get fail error:%+v:", err)) + return + } + if msg.Code != 1 { + err = errors.New("zhongfu QueryOrder fail") + log.Error(fmt.Sprintf("zhongfu QueryOrder http.Get fail error:%+v:", msg)) + return + } + loc, _ := time.LoadLocation("Local") + payTime, err = time.ParseInLocation("2006-01-02 15:04:05", msg.Data.PayTime, loc) + return +} + +// Fill 补全配置信息和签名信息 +func (this *ZhongFu) Fill() { + this.Version = "v2.3.5" + this.MercID = this.GetMercID() + this.NotifyURL = this.GetNotifyURL() + this.CallbackUrl = this.GetCallBackURL() + if this.UserIP == "" { + this.UserIP = genIpaddr() + } + this.Type = this.PayMethod + this.PayMethod = "" + this.Charset = "UTF-8" + this.SignType = "MD5" + this.PayFormat = "json" + this.Sign = this.sign() +} + +// sign 签名 +func (this *ZhongFu) sign() (sign string) { + ss := this.ToStringSlice() + sort.Sort(&ss) + var buf bytes.Buffer + for k, v := range ss { + if k == 0 { + buf.WriteString(v) + } else { + buf.WriteString("&") + buf.WriteString(v) + } + } + buf.WriteString("&key=") + buf.WriteString(this.GetAppSecret()) + log.Info(fmt.Sprintf("ZhongFu topay sign :%s", buf.String())) + return strings.ToUpper(md5Sign(buf.String())) +} + +// sign 签名 +func (this *ZhongFuRes) ToSign() (sign string) { + ss := this.ToStringSlice() + sort.Sort(&ss) + var buf bytes.Buffer + for _, v := range ss { + buf.WriteString(v) + buf.WriteString("&") + } + buf.WriteString("key=") + buf.WriteString(zfc.VerifySecret) + log.Info(fmt.Sprintf("ZhongFu callback sign:%s", buf.String())) + str := buf.String() + sign = strings.ToUpper(md5Sign(str)) + return +} + +// ToStringSlice 转化为stirng切片切片 +func (this *ZhongFu) ToStringSlice() StringList { + t := reflect.TypeOf(*this) + v := reflect.ValueOf(*this) + data := make([]string, 0, v.NumField()) + for k := 0; k < v.NumField(); k++ { + jsonTag, _ := t.Field(k).Tag.Lookup("json") + signTag, _ := t.Field(k).Tag.Lookup("sign") + fv := v.Field(k).Type() + if signTag == "1" { + switch fv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + data = append(data, jsonTag+"="+strconv.FormatInt(v.Field(k).Int(), 10)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + data = append(data, jsonTag+"="+strconv.FormatUint(v.Field(k).Uint(), 10)) + case reflect.String: + data = append(data, jsonTag+"="+v.Field(k).String()) + } + } + } + return data +} + +// ToStringSlice 转化为stirng切片切片 +func (this *ZhongFuRes) ToStringSlice() StringList { + t := reflect.TypeOf(*this) + v := reflect.ValueOf(*this) + data := make([]string, 0, v.NumField()) + for k := 0; k < v.NumField(); k++ { + jsonTag, _ := t.Field(k).Tag.Lookup("json") + signTag, _ := t.Field(k).Tag.Lookup("sign") + if signTag == "1" && v.Field(k).String() != "" { + data = append(data, jsonTag+"="+v.Field(k).String()) + } + } + return data +} + +// GetBody 获取请求body +func (this *ZhongFu) GetBody() interface{} { + jsonStr, err := json.Marshal(this) + if err != nil { + log.Info(fmt.Sprintf("ZhongFu GetBody json.Marshal is fail error:%+v/data:%+v", err, this)) + } + log.Info(fmt.Sprintf("==================%+v", string(jsonStr))) + return jsonStr +} + +// GetHeader 获取请求头信息 +func (*ZhongFu) GetHeader() map[string]string { + return map[string]string{} +} + +// GetURL 获取请求地址 +func (*ZhongFu) GetURL() string { + return zfc.ExeUrl +} + +// GetQueryURL 获取请求地址 +func (*ZhongFu) GetQueryURL() string { + return zfc.QueryUrl +} + +// GetLowAndUpperURL 获取请求地址 +func (*ZhongFu) GetLowAndUpperURL() string { + return zfc.LowAndUpperURL +} + +// GetCallBackURL 获取回掉函数 +func (*ZhongFu) GetNotifyURL() string { + if appg.Conf != nil { + return appg.Conf.URL.LocalWebUrl + zfc.NotifyUrl + } + return webg.Conf.URL.LocalAppUrl + zfc.NotifyUrl +} + +// GetCallBackURL 获取回掉函数 +func (*ZhongFu) GetCallBackURL() string { + return zfc.CallbackURL +} + +// GetMercID 获取商户编号 +func (*ZhongFu) GetMercID() string { + return zfc.MercID +} + +// GetAppSecret 获取app签名密钥 +func (*ZhongFu) GetAppSecret() string { + return zfc.AppSecret +} diff --git a/common/redis/redis.go b/common/redis/redis.go new file mode 100644 index 0000000..a5b252d --- /dev/null +++ b/common/redis/redis.go @@ -0,0 +1,1026 @@ +package redis + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/url" + "reflect" + "strconv" + "time" + + "91porn-server/common/log" + + goredis "github.com/go-redis/redis" +) + +//命令参考问题 http://redisdoc.com/persistence/index.html + +// Handler 缓存操作句柄 +var Handler *Client + +// Options 配置项 +type Options struct { + Addr string `json:"addr"` // 地址 + Pwd string `json:"pwd"` // 密码 + MaxRetries int `json:"maxRetries"` // 重试次数 + DB int `json:"db"` //数据库 + PoolSize int `json:"poolSize"` //链接池数量 + MinIdleConns int `json:"minIdleConns"` //最小空闲连接数 +} + +// Client Redis客户端 +type Client struct { + client *goredis.Client +} + +// Script 是可复用的 Redis Lua 脚本。RunScript 会优先执行 EVALSHA, +// Redis 尚未缓存脚本时由 go-redis 自动回退到 EVAL。 +type Script struct { + script *goredis.Script +} + +// NewScript 预计算脚本 SHA,调用方应复用返回值而不是每次请求重新创建。 +func NewScript(source string) *Script { + return &Script{script: goredis.NewScript(source)} +} + +// Hash 返回脚本 SHA1,主要用于诊断和测试。 +func (s *Script) Hash() string { + if s == nil || s.script == nil { + return "" + } + return s.script.Hash() +} + +type Member = goredis.Z + +// Message 消息体 +type Message struct { + Channel string `json:"channel"` + Payload string `json:"payload"` +} + +// WithOptions 启动redis客户端连接 +func WithOptions(options Options) (*Client, error) { + client := goredis.NewClient(&goredis.Options{ + Addr: options.Addr, + Password: options.Pwd, + MaxRetries: options.MaxRetries, + DB: options.DB, + MinIdleConns: options.MinIdleConns, + PoolSize: options.PoolSize, + }) + if err := client.Ping().Err(); err != nil { + log.Error("can't ping redis addr", + log.Any("addr", options.Addr), + log.Any("db", options.DB), + log.E(err)) + return nil, err + } + log.Info("redis connections success", + log.Any("addr", options.Addr), + log.Any("db", options.DB), + log.Any("poolSize", options.PoolSize), + log.Any("minIdleConns", options.MinIdleConns)) + + Handler = &Client{client: client} + return Handler, nil +} + +func WithURL(rawurl string) (*Client, error) { + u, err := url.Parse(rawurl) + if err != nil { + return nil, err + } + auth := u.User + host := u.Host + path := u.Path + rdb := 0 + query := u.Query() + if len(path) > 1 { + path = path[1:] + rdb, err = strconv.Atoi(path) + if err != nil { + log.Error("redis WithURL wrong db", log.Any("db", path)) + return nil, err + } + } + pwd, _ := auth.Password() + smr := query.Get("maxRetries") + maxRetries := 0 + if len(smr) > 0 { + maxRetries, err = strconv.Atoi(smr) + if err != nil { + log.Error("redis WithURL wrong maxRetries", log.Any("maxRetries", smr)) + return nil, err + } + } + poolSize := 70 + poolSizeStr := query.Get("poolSize") + if poolSizeStr != "" { + poolSize, err = strconv.Atoi(poolSizeStr) + if err != nil { + poolSize = 50 + } + } + minIdleConns := 50 + minIdleConnsStr := query.Get("minIdleConns") + if minIdleConnsStr != "" { + minIdleConns, err = strconv.Atoi(minIdleConnsStr) + if err != nil { + minIdleConns = 30 + } + } + opt := Options{ + Addr: host, + Pwd: pwd, + MaxRetries: maxRetries, + DB: rdb, + PoolSize: poolSize, + MinIdleConns: minIdleConns, + } + return WithOptions(opt) +} + +// Close 关闭redis +func (r *Client) Close() error { + if r.client == nil { + return nil + } + if err := r.client.Close(); err != nil { + log.Error("close redis-connection failed") + return err + } + log.Info("close redis-connection successfully") + return nil +} + +// SetBit 位操作 +func (r *Client) SetBit(key string, offset int64, value int) error { + if key == "" { + return errors.New("key should not empty") + } + return r.client.SetBit(key, offset, value).Err() +} + +// GetBit 获取位值 +func (r *Client) GetBit(key string, offset int64) (int, error) { + if key == "" { + return 0, errors.New("key should not empty") + } + + data, err := r.client.GetBit(key, offset).Result() + if err != nil { + return 0, err + } + + return int(data), nil +} + +// Set 设置键值对 +func (r *Client) Set(key string, value interface{}, expirTime time.Duration) error { + if key == "" { + return errors.New("key should not empty") + } + return r.client.Set(key, value, expirTime).Err() +} + +// Get 根据键 获取值 +func (r *Client) Get(key string) (*string, error) { + if key == "" { + return nil, errors.New("key should not empty") + } + data, err := r.client.Get(key).Result() + if err != nil { + if err == goredis.Nil { + return nil, nil + } + return nil, err + } + return &data, nil +} + +func (r *Client) GetString(key string) (string, error) { + if key == "" { + return "", errors.New("key should not empty") + } + value, err := r.client.Get(key).Result() + if err != nil && err != goredis.Nil { + return "", err + } + return value, nil +} + +// Get 根据键 获取值 +func (r *Client) GetObj(bind interface{}, key string) error { + if key == "" { + return errors.New("key should not empty") + } + data, err := r.client.Get(key).Bytes() + if err != nil { + if err == goredis.Nil { + return nil + } + return err + } + return json.Unmarshal(data, &bind) +} + +// Get 根据键 获取值 +func (r *Client) Scan(key string, val interface{}) error { + if key == "" { + return errors.New("key should not empty") + } + return r.client.Get(key).Scan(val) +} + +// ScanKeys 异步迭代匹配key,不阻塞存取线程 +func (r *Client) ScanKeys(match string) (keys []string, err error) { + if match == "" { + return nil, errors.New("match key should not empty") + } + + var cursor uint64 + var tmpKey []string + tmpKey, cursor, err = r.client.Scan(cursor, match, 100).Result() + if err != nil { + return + } + keys = append(keys, tmpKey...) + for cursor > 0 { + tmpKey, cursor, err = r.client.Scan(cursor, match, 100).Result() + if err != nil { + return + } + keys = append(keys, tmpKey...) + } + + return +} + +// Get 是否存在某一个建 +func (r *Client) IsExist(key string) bool { + if key == "" { + return false + } + data, err := r.client.Exists(key).Result() + if err != nil { + return false + } + return data != 0 +} + +// Del 删除键值 +func (r *Client) Del(keys ...string) (n int64, err error) { + return r.DelContext(context.Background(), keys...) +} + +// DelContext 删除键值并继承调用方取消和超时。 +func (r *Client) DelContext(ctx context.Context, keys ...string) (n int64, err error) { + if ctx == nil { + return 0, errors.New("redis DelContext context must not be nil") + } + if r == nil || r.client == nil { + return 0, errors.New("redis DelContext client must not be nil") + } + return r.client.WithContext(ctx).Del(keys...).Result() +} + +// Subscribe constructs a field +func (r *Client) Subscribe(channel ...string) (sub *goredis.PubSub, err error) { + pubsub := r.client.Subscribe(channel...) + if err := pubsub.Ping("ack:ok"); err != nil { + log.Error(fmt.Sprintf("pubsub ping failed %+v:", err)) + return nil, err + } + if _, err = pubsub.Receive(); err != nil { + log.Error(fmt.Sprintf("pubsub receive failed %+v:", err)) + return nil, err + } + log.Info("pubsub successfully") + return pubsub, nil +} + +// Publish 创建订阅频道 发布消息 +func (r *Client) Publish(channel string, message interface{}) error { + flag, err := r.client.Publish(channel, message).Result() + if err != nil { + log.Error(fmt.Sprintf("publish failed %+v:", err)) + return err + } + log.Info(fmt.Sprintf("publish successfully flag:%d", flag)) + return nil +} + +// Lpop 按顺序弹出元素 从队列头部弹出 +func (r *Client) Lpop(key string) (string, error) { + result, err := r.client.LPop(key).Result() + if err != nil { + log.Error(fmt.Sprintf("LPop failed %+v:", err)) + return "", err + } + return result, nil +} + +// RPop 从线程安全队列取尾部取出元素 +func (r *Client) RPop(key string) (string, error) { + result, err := r.client.RPop(key).Result() + if err != nil { + log.Error(fmt.Sprintf("RPop failed %+v:", err)) + return "", err + } + return result, nil +} + +// Lpush push 添加元素到头部 +func (r *Client) Lpush(key string, values ...interface{}) error { + if err := r.client.LPush(key, values...).Err(); err != nil { + log.Error(fmt.Sprintf("LPush failed %+v:", err)) + return err + } + return nil +} + +// RPush push 添加元素到尾部 +func (r *Client) RPush(key string, values ...interface{}) error { + if err := r.client.RPush(key, values...).Err(); err != nil { + log.Error(fmt.Sprintf("RPush failed %+v:", err)) + return err + } + return nil +} + +// RPushPipeline 将 values 按 batchSize 拆成多个 RPUSH,并通过一个 Pipeline 顺序执行。 +// 返回最后一个 RPUSH 执行后的列表长度。调用方应限制单次 values 的数量,避免生成过大的 Pipeline。 +func (r *Client) RPushPipeline(key string, values []string, batchSize int) (int64, error) { + return r.RPushPipelineContext(context.Background(), key, values, batchSize) +} + +// RPushPipelineContext 与 RPushPipeline 相同,并继承调用方取消和超时。 +func (r *Client) RPushPipelineContext( + ctx context.Context, + key string, + values []string, + batchSize int, + expirations ...time.Duration, +) (int64, error) { + if ctx == nil { + return 0, errors.New("redis RPushPipelineContext context must not be nil") + } + if batchSize <= 0 { + return 0, fmt.Errorf("redis RPushPipeline batchSize must be positive") + } + if len(values) == 0 { + return 0, nil + } + if len(expirations) > 1 { + return 0, errors.New("redis RPushPipelineContext accepts at most one expiration") + } + var expiration time.Duration + if len(expirations) == 1 { + expiration = expirations[0] + if expiration <= 0 { + return 0, errors.New("redis RPushPipelineContext expiration must be positive") + } + } + if r == nil || r.client == nil { + return 0, errors.New("redis RPushPipelineContext client must not be nil") + } + var lastCmd *goredis.IntCmd + var expireCmd *goredis.BoolCmd + _, err := r.client.WithContext(ctx).Pipelined(func(pipe goredis.Pipeliner) error { + for start := 0; start < len(values); start += batchSize { + end := start + batchSize + if end > len(values) { + end = len(values) + } + args := make([]interface{}, end-start) + for i := start; i < end; i++ { + args[i-start] = values[i] + } + lastCmd = pipe.RPush(key, args...) + } + if expiration > 0 { + // 与 RPUSH 位于同一连接、同一 Pipeline。即使响应丢失后清理 + // 与迟到命令交错,重新创建的 building key 也不会永久残留。 + expireCmd = pipe.Expire(key, expiration) + } + return nil + }) + if err != nil { + log.Error("RPushPipeline failed", log.E(err)) + return 0, err + } + length, err := lastCmd.Result() + if err != nil { + log.Error("RPushPipeline result failed", log.E(err)) + return 0, err + } + if expireCmd != nil { + expired, expireErr := expireCmd.Result() + if expireErr != nil { + log.Error("RPushPipeline expire result failed", log.E(expireErr)) + return 0, expireErr + } + if !expired { + return 0, errors.New("redis RPushPipeline failed to expire list") + } + } + return length, nil +} + +// RPopLPush 线程安全队列 +func (r *Client) RPopLPush(key string, value string) (string, error) { + result, err := r.client.RPopLPush(key, value).Result() + if err != nil { + log.Error(fmt.Sprintf("RPopLPush failed %+v:", err)) + return "", err + } + return result, nil +} + +// BRPopLPush 阻塞线程安全队列 +func (r *Client) BRPopLPush(key string, value string, timeout time.Duration) (string, error) { + result, err := r.client.BRPopLPush(key, value, timeout).Result() + if err != nil { + log.Error(fmt.Sprintf("BRPopLPush failed %+v:", err)) + return "", err + } + return result, nil +} + +// LRem 从列表中删除元素 +func (r *Client) LRem(key string, count int64, value string) (int64, error) { + result, err := r.client.LRem(key, count, value).Result() + if err != nil { + log.Error(fmt.Sprintf("BRPopLPush failed %+v:", err)) + return 0, err + } + return result, nil +} + +// MGet 批量获取Get值 +func (r *Client) MGet(keys ...string) ([]interface{}, error) { + if len(keys) == 0 { + return nil, nil + } + + result, err := r.client.MGet(keys...).Result() + if err != nil { + log.Error(fmt.Sprintf("MGet failed %+v:", err)) + return nil, err + } + return result, nil +} + +// MSet 批量设置Set值 +func (r *Client) MSet(pairs ...interface{}) error { + if _, err := r.client.MSet(pairs...).Result(); err != nil { + log.Error(fmt.Sprintf("MSet failed %+v:", err)) + return err + } + return nil +} + +// LRange 获取列表值 +func (r *Client) LRange(key string, start int64, stop int64) ([]string, error) { + data, err := r.client.LRange(key, start, stop).Result() + if err != nil { + log.Error(fmt.Sprintf("MSet failed %+v:", err)) + return nil, err + } + return data, nil +} + +// LCount 获取列表的数量 +func (r *Client) LCount(key string) (int64, error) { + cnt, err := r.client.LLen(key).Result() + if err != nil { + log.Error(fmt.Sprintf("MSet failed %+v:", err)) + return 0, err + } + return cnt, nil +} + +// LTrim 保留指定范围内的元素 +func (r *Client) Ltrim(key string, start, end int64) (string, error) { + result, err := r.client.LTrim(key, start, end).Result() + if err != nil { + log.Error(fmt.Sprintf("MSet failed %+v:", err)) + return "", err + } + return result, nil +} + +// AppendByte 追加bytes +func (r *Client) AppendByte(key string, data []byte) (int64, error) { + str := string(data) + result, err := r.client.Append(key, str).Result() + if err != nil { + log.Error(fmt.Sprintf("MSet failed %+v:", err)) + return 0, err + } + return result, nil +} + +// ExpireKey 对键设置过期时间 +func (r *Client) ExpireKey(key string, expirTime time.Duration) (bool, error) { + result, err := r.client.Expire(key, expirTime).Result() + if err != nil { + log.Error(fmt.Sprintf("ExpireKey failed %+v:", err)) + return false, err + } + return result, nil +} + +// ExpireKey 对键设置指定过期时间 +func (r *Client) ExpireKeAt(key string, expirTime time.Time) (bool, error) { + result, err := r.client.ExpireAt(key, expirTime).Result() + if err != nil { + log.Error(fmt.Sprintf("ExpireKeAt failed %+v:", err)) + return false, err + } + return result, nil +} + +// Do 批处理执行redis +func (r *Client) Do(arg ...interface{}) (interface{}, error) { + result, err := r.client.Do(arg).Result() + if err != nil { + log.Error(fmt.Sprintf("Do failed %+v:", err)) + return nil, err + } + return result, nil +} + +// Eval 执行Lua脚本。涉及多个Redis键的读写必须通过脚本保证原子性。 +func (r *Client) Eval(script string, keys []string, args ...interface{}) (interface{}, error) { + return r.EvalContext(context.Background(), script, keys, args...) +} + +// EvalContext 执行Lua脚本并继承调用方取消和超时。 +func (r *Client) EvalContext( + ctx context.Context, + script string, + keys []string, + args ...interface{}, +) (interface{}, error) { + if ctx == nil { + return nil, errors.New("redis EvalContext context must not be nil") + } + if r == nil || r.client == nil { + return nil, errors.New("redis EvalContext client must not be nil") + } + result, err := r.client.WithContext(ctx).Eval(script, keys, args...).Result() + if err != nil { + log.Error("Eval failed", log.E(err)) + return nil, err + } + return result, nil +} + +// RunScript 执行预编译脚本,优先使用 EVALSHA 并在 NOSCRIPT 时自动回退 EVAL。 +func (r *Client) RunScript(script *Script, keys []string, args ...interface{}) (interface{}, error) { + return r.RunScriptContext(context.Background(), script, keys, args...) +} + +// RunScriptContext 与 RunScript 相同,并将取消和超时传递到 Redis 命令。 +func (r *Client) RunScriptContext( + ctx context.Context, + script *Script, + keys []string, + args ...interface{}, +) (interface{}, error) { + if ctx == nil { + return nil, errors.New("redis RunScriptContext context must not be nil") + } + if script == nil || script.script == nil { + return nil, errors.New("redis RunScriptContext script must not be nil") + } + if r == nil || r.client == nil { + return nil, errors.New("redis RunScriptContext client must not be nil") + } + result, err := script.script.Run(r.client.WithContext(ctx), keys, args...).Result() + if err != nil { + log.Error("RunScript failed", log.E(err)) + return nil, err + } + return result, nil +} + +// redis 管道流 +func (r *Client) Piplined(fn func(goredis.Pipeliner) error) ([]goredis.Cmder, error) { + return r.client.Pipelined(fn) +} + +// 生成管道流对象 +func (r *Client) Pipliner() goredis.Pipeliner { + return r.client.Pipeline() +} + +// setNx 锁 +func (r *Client) SetNX(key string, value interface{}, expiration time.Duration) (bool, error) { + return r.SetNXContext(context.Background(), key, value, expiration) +} + +// SetNXContext 原子写入键并继承调用方取消和超时。 +func (r *Client) SetNXContext( + ctx context.Context, + key string, + value interface{}, + expiration time.Duration, +) (bool, error) { + if ctx == nil { + return false, errors.New("redis SetNXContext context must not be nil") + } + if r == nil || r.client == nil { + return false, errors.New("redis SetNXContext client must not be nil") + } + result, err := r.client.WithContext(ctx).SetNX(key, value, expiration).Result() + if err != nil { + log.Error(fmt.Sprintf("SetNX failed %+v:", err)) + return false, err + } + return result, nil +} + +// Setnx 设置键值对 正确的封装 +func (r *Client) Setnx_NewOK(key string, value interface{}, expirTime time.Duration) (v bool, err error) { + return r.SetNX(key, value, expirTime) +} + +// 有序集合操作 添加多个有序集合 +func (r *Client) ZAdd(key string, z ...Member) (int64, error) { + count, err := r.client.ZAdd(key, z...).Result() + if err != nil { + log.Error(fmt.Sprintf("ZAdd failed %+v:", err)) + return 0, err + } + return count, nil +} + +// 有序集合操作 指定成员增加传入值 +func (r *Client) ZIncrBy(key string, inc float64, m string) (float64, error) { + count, err := r.client.ZIncrBy(key, inc, m).Result() + if err != nil { + log.Error(fmt.Sprintf("ZIncrBy failed %+v:", err)) + return 0, err + } + return count, nil +} + +// 有序集合 数量 +func (r *Client) ZCard(key string) (int64, error) { + count, err := r.client.ZCard(key).Result() + if err != nil { + log.Error(fmt.Sprintf("ZCard failed %+v:", err)) + return 0, err + } + return count, nil +} + +// ZScore 获取元素分数 +func (r *Client) ZScore(key, member string) (float64, error) { + data, err := r.client.ZScore(key, member).Result() + if err != nil { + //log.Error(fmt.Sprintf("ZScore failed %+v:", err)) + return 0, err + } + + return data, nil +} + +// ZCount 统计分数区间的元素个数 +func (r *Client) ZCount(key, min, max string) (int64, error) { + data, err := r.client.ZCount(key, min, max).Result() + if err != nil { + log.Error(fmt.Sprintf("ZCount failed %+v:", err)) + return 0, err + } + + return data, nil +} + +// 无序集合 数量 +func (r *Client) SCard(key string) (int64, error) { + count, err := r.client.SCard(key).Result() + if err != nil { + log.Error(fmt.Sprintf("SCard failed %+v:", err)) + return 0, err + } + return count, nil +} + +// 返回区间成员 +func (r *Client) ZRange(key string, start, end int64) ([]string, error) { + data, err := r.client.ZRange(key, start, end).Result() + if err != nil { + log.Error(fmt.Sprintf("ZRange failed %+v:", err)) + } + return data, err +} + +// ZRangeByScore 按分数取成员 +func (r *Client) ZRangeByScore(key string, start, end string) ([]string, error) { + z := goredis.ZRangeBy{ + Min: start, + Max: end, + } + data, err := r.client.ZRangeByScore(key, z).Result() + if err != nil { + log.Error(fmt.Sprintf("ZRangeByScore failed %+v:", err)) + return nil, err + } + return data, nil +} + +// ZRevRange 通过索引,分数从高到低 +func (r *Client) ZRevRange(key string, start, stop int64) ([]string, error) { + data, err := r.client.ZRevRange(key, start, stop).Result() + if err != nil { + log.Error(fmt.Sprintf("ZRevRange failed %+v:", err)) + } + + return data, err +} + +// ZRevRangeWithScores 通过索引,分数从高到低 一并返回分值 +func (r *Client) ZRevRangeWithScores(key string, start, stop int64) ([]goredis.Z, error) { + data, err := r.client.ZRevRangeWithScores(key, start, stop).Result() + if err != nil { + log.Error(fmt.Sprintf("ZRevRangeWithScores failed %+v:", err)) + } + + return data, err +} + +// ZRevRangeWithScores2 按分数排序获取前n个元素并返回分数 +func (r *Client) ZRevRangeWithScores2(key string, start, limit int64) ([]string, map[string]float64) { + stop := start + limit - 1 + z, err := r.client.ZRevRangeWithScores(key, start, stop).Result() + if err != nil { + log.Error(fmt.Sprintf("ZRevRangeWithScores failed %+v:", err)) + } + + var srt []string + members := make(map[string]float64) + for _, v := range z { + srt = append(srt, fmt.Sprint(v.Member)) + members[fmt.Sprint(v.Member)] = v.Score + } + + return srt, members +} + +// 按照元素排序 删除指定排名的元素 +func (r *Client) ZRemRangeByRank(key string, start, end int64) (int64, error) { + count, err := r.client.ZRemRangeByRank(key, start, end).Result() + if err != nil { + log.Error(fmt.Sprintf("ZRemRangeByRank failed %+v:", err)) + return 0, err + } + return count, nil +} + +// 按照元素排序 删除按指定的分数区间的的元素 +func (r *Client) ZRemRangeByScore(key, start, end string) (int64, error) { + count, err := r.client.ZRemRangeByScore(key, start, end).Result() + if err != nil { + log.Error(fmt.Sprintf("ZRemRangeByScore failed %+v:", err)) + return 0, err + } + return count, nil +} + +// ZRem 删除有序集合元素 +func (r *Client) ZRem(key string, members ...interface{}) (int64, error) { + count, err := r.client.ZRem(key, members...).Result() + if err != nil { + log.Error(fmt.Sprintf("ZRem failed %+v:", err)) + return 0, err + } + return count, nil +} + +// 判断元素是否存在于当前set中 +func (r *Client) ZSISMember(key string, member string) bool { + result, err := r.client.ZRank(key, member).Result() + if err != nil { + return false + } + return result != 0 +} + +// 无序集合操作 向集合添加元素 +func (r *Client) SAdd(key string, members ...interface{}) (int64, error) { + count, err := r.client.SAdd(key, members...).Result() + if err != nil { + log.Error(fmt.Sprintf("SAdd failed %+v:", err)) + return 0, err + } + return count, nil +} + +// 获取无序集合的元素 +func (r *Client) SMembers(key string) ([]string, error) { + result, err := r.client.SMembers(key).Result() + if err != nil { + log.Error(fmt.Sprintf("SMembers failed %+v:", err)) + } + return result, err +} + +// 无序集合操作 删除集合中某一元素 +func (r *Client) SRem(key string, members ...interface{}) (int64, error) { + count, err := r.client.SRem(key, members...).Result() + if err != nil { + log.Error(fmt.Sprintf("SRem failed %+v:", err)) + return 0, err + } + return count, nil +} + +// 无序集合操作 随机删除集合中某一元素 +func (r *Client) SPop(key string, members ...interface{}) (string, error) { + val, err := r.client.SPop(key).Result() + if err != nil && err != goredis.Nil { + log.Error(fmt.Sprintf("SPop failed %+v:", err)) + } + return val, err +} + +// ScanSlice 返回集合成员 +func (r *Client) ScanSlice(key string, container interface{}) error { + if err := r.client.SMembers(key).ScanSlice(container); err != nil { + log.Error(fmt.Sprintf("ScanSlice failed %+v:", err)) + return err + } + return nil +} + +// 判断元素是否是集合中的成员 +func (r *Client) SISMember(key string, members interface{}) (bool, error) { + result, err := r.client.SIsMember(key, members).Result() + if err != nil { + log.Error(fmt.Sprintf("SAdd failed %+v:", err)) + return false, err + } + return result, nil +} + +// 随机从集合中的选取成员 +func (r *Client) SRandMember(key string) (string, error) { + result, err := r.client.SRandMember(key).Result() + if err != nil { + log.Error(fmt.Sprintf("SRandMember failed %+v:", err)) + return "", err + } + return result, nil +} + +// 随机从集合中的选取指定数量的成员 +func (r *Client) SRandMemberN(key string, count int64) ([]string, error) { + result, err := r.client.SRandMemberN(key, count).Result() + if err != nil { + log.Error(fmt.Sprintf("SRandMemberN failed %+v:", err)) + return nil, err + } + return result, nil +} + +// HMget 获取hash值 +func (r *Client) HMget(key string, fields []string) ([]interface{}, error) { + result, err := r.client.HMGet(key, fields...).Result() + if err != nil { + log.Error(fmt.Sprintf("HMget failed %+v:", err)) + return nil, err + } + return result, nil +} + +// Hget 获取hash值 +func (r *Client) Hget(key string, field string) (string, error) { + result, err := r.client.HGet(key, field).Result() + if err != nil { + log.Error(fmt.Sprintf("Hget failed %+v:", err)) + return "", err + } + return result, nil +} + +// HMset 设置hash值 +func (r *Client) HMset(key string, fields map[string]interface{}) error { + if _, err := r.client.HMSet(key, fields).Result(); err != nil { + log.Error(fmt.Sprintf("HMset failed %+v:", err)) + return err + } + return nil +} + +// HDel 删除hash多个键值 +func (r *Client) HDel(key string, fields ...string) error { + if _, err := r.client.HDel(key, fields...).Result(); err != nil { + log.Error(fmt.Sprintf("HDel failed %+v:", err)) + return err + } + return nil +} + +// 获取 哈希表的keys +func (r *Client) Hkeys(key string) []string { + result, err := r.client.HKeys(key).Result() + if err != nil { + log.Error(fmt.Sprintf("Hkeys failed %+v:", err)) + return nil + } + return result +} + +// Exists 检查给定 key 是否存在 +func (r *Client) Exists(keys ...string) bool { + status, err := r.client.Exists(keys...).Result() + if err != nil { + log.Error(fmt.Sprintf("Exists failed %+v:", err)) + return false + } + return status == 1 +} + +// 增加对应的键值 并返回增加后的结果 +func (r *Client) Incr(key string) int64 { + result, err := r.client.Incr(key).Result() + if err != nil { + log.Error(fmt.Sprintf("Incr failed %+v:", err)) + return 0 + } + return result +} + +// 增加对应的键值 并返回增加后的结果 +func (r *Client) IncrBy(key string, num int64, expire time.Duration) int64 { + result, err := r.client.IncrBy(key, num).Result() + if err != nil { + log.Error(fmt.Sprintf("IncrBy failed %+v:", err)) + return 0 + } + if expire != 0 { + r.client.Expire(key, expire) + } + return result +} + +// 将字符数组转换为 redis member +func Convert2RedisMemForNoScore(src []string) []Member { + if len(src) == 0 { + return nil + } + members := make([]Member, 0, len(src)) + for _, v := range src { + if v != "" { + members = append(members, Member{Member: v, Score: 0}) + } + } + return members +} + +// 将字符数组转换为 redis member +func Convert2RedisMem(src string) Member { + return Member{Member: src} +} + +// ping +func (r *Client) Ping() (*string, error) { + data, err := r.client.Ping().Result() + if err != nil { + if err == goredis.Nil { + return nil, nil + } + return nil, err + } + return &data, nil +} + +func (r *Client) GetWithJson2Any(key string, val any) error { + rv := reflect.ValueOf(val) + if rv.Kind() != reflect.Ptr { + return errors.New("[val] params must be a pointer") + } + v, err := r.client.Get(key).Bytes() + if err != nil { + return err + } + if v == nil { + return goredis.Nil + } + return json.Unmarshal(v, val) +} + +func (r *Client) SetWithAny2Json(key string, val any, exp time.Duration) error { + v, err := json.Marshal(val) + if err != nil { + return err + } + return r.client.Set(key, string(v), exp).Err() +} diff --git a/common/redis/redis_test.go b/common/redis/redis_test.go new file mode 100644 index 0000000..08edfd5 --- /dev/null +++ b/common/redis/redis_test.go @@ -0,0 +1,97 @@ +package redis + +import ( + "context" + "testing" + "time" +) + +func TestRPushPipelineRejectsNonPositiveBatchSize(t *testing.T) { + client := &Client{} + if _, err := client.RPushPipeline("queue", []string{"video-1"}, 0); err == nil { + t.Fatal("RPushPipeline() should reject a non-positive batch size") + } +} + +func TestRPushPipelineEmptyValuesDoesNotUseClient(t *testing.T) { + client := &Client{} + length, err := client.RPushPipeline("queue", nil, 1000) + if err != nil { + t.Fatalf("RPushPipeline() error = %v", err) + } + if length != 0 { + t.Fatalf("RPushPipeline() length = %d, want 0", length) + } +} + +func TestRPushPipelineContextValidatesExpiration(t *testing.T) { + client := &Client{} + for _, expirations := range [][]time.Duration{ + {0}, + {-time.Second}, + {time.Second, time.Minute}, + } { + if _, err := client.RPushPipelineContext( + context.Background(), + "queue", + []string{"video-1"}, + 1, + expirations..., + ); err == nil { + t.Fatalf("expirations=%v should be rejected", expirations) + } + } +} + +func TestScriptHashIsStable(t *testing.T) { + first := NewScript("return ARGV[1]") + second := NewScript("return ARGV[1]") + if first.Hash() == "" || first.Hash() != second.Hash() { + t.Fatalf("script hashes differ: first=%q second=%q", first.Hash(), second.Hash()) + } +} + +func TestRunScriptContextValidatesArguments(t *testing.T) { + client := &Client{} + if _, err := client.RunScriptContext(nil, NewScript("return 1"), nil); err == nil { + t.Fatal("RunScriptContext() should reject a nil context") + } + if _, err := client.RunScriptContext(context.Background(), nil, nil); err == nil { + t.Fatal("RunScriptContext() should reject a nil script") + } + if _, err := client.RunScriptContext(context.Background(), NewScript("return 1"), nil); err == nil { + t.Fatal("RunScriptContext() should reject an uninitialized client") + } +} + +func TestContextRedisHelpersValidateContextAndClient(t *testing.T) { + var client *Client + if _, err := client.DelContext(context.Background(), "key"); err == nil { + t.Fatal("DelContext() should reject nil client") + } + if _, err := client.EvalContext(context.Background(), "return 1", nil); err == nil { + t.Fatal("EvalContext() should reject nil client") + } + if _, err := client.RPushPipelineContext( + context.Background(), "queue", []string{"video-1"}, 1, + ); err == nil { + t.Fatal("RPushPipelineContext() should reject nil client") + } + if _, err := client.SetNXContext(context.Background(), "key", "value", 0); err == nil { + t.Fatal("SetNXContext() should reject nil client") + } + + empty := &Client{} + if _, err := empty.DelContext(nil, "key"); err == nil { + t.Fatal("DelContext() should reject nil context") + } + if _, err := empty.EvalContext(nil, "return 1", nil); err == nil { + t.Fatal("EvalContext() should reject nil context") + } + if _, err := empty.RPushPipelineContext(nil, "queue", []string{"video-1"}, 1); err == nil { + t.Fatal("RPushPipelineContext() should reject nil context") + } + if _, err := empty.SetNXContext(nil, "key", "value", 0); err == nil { + t.Fatal("SetNXContext() should reject nil context") + } +} diff --git a/common/redis/redisutil.go b/common/redis/redisutil.go new file mode 100644 index 0000000..643d134 --- /dev/null +++ b/common/redis/redisutil.go @@ -0,0 +1,11 @@ +package redis + +// 更新filed或insert 更新消息是否已读 +func (r *Client) UpSertMap(key string, fileds map[string]interface{}) { + for k, v := range fileds { + if k == "" { + continue + } + r.client.HSet(key, k, v) + } +} diff --git a/common/redis/redsync.go b/common/redis/redsync.go new file mode 100644 index 0000000..2a80d75 --- /dev/null +++ b/common/redis/redsync.go @@ -0,0 +1,11 @@ +package redis + +import ( + "github.com/go-redsync/redsync/v4" + "github.com/go-redsync/redsync/v4/redis/goredis" +) + +// BuildLock 构建redis分布式锁。 +func BuildLock(client *Client, key string, options ...redsync.Option) *redsync.Mutex { + return redsync.New(goredis.NewPool(client.client)).NewMutex(key, options...) +} diff --git a/common/services/follow/tip.go b/common/services/follow/tip.go new file mode 100644 index 0000000..fdafa07 --- /dev/null +++ b/common/services/follow/tip.go @@ -0,0 +1,35 @@ +package follow + +import "91porn-server/common/services" + +const tipKey = "user_followed_tip_key" + +// SetTip 设置关注提醒 +func SetTip(uid uint64) error { + err := services.Redis.SetBit(tipKey, int64(uid), 1) + if err != nil { + return err + } + + return nil +} + +// CancelTip 清除关注提醒 +func CancelTip(uid uint64) error { + err := services.Redis.SetBit(tipKey, int64(uid), 0) + if err != nil { + return err + } + + return nil +} + +// CheckTip 检查是否有提醒 +func CheckTip(uid uint64) bool { + val, err := services.Redis.GetBit(tipKey, int64(uid)) + if err != nil { + return false + } + + return val == 1 +} diff --git a/common/services/message/tip.go b/common/services/message/tip.go new file mode 100644 index 0000000..588c4a9 --- /dev/null +++ b/common/services/message/tip.go @@ -0,0 +1,35 @@ +package message + +import "91porn-server/common/services" + +const tipKey = "user_dynamic_msg_tip_key" + +// SetTip 设置消息提醒 +func SetTip(uid uint64) error { + err := services.Redis.SetBit(tipKey, int64(uid), 1) + if err != nil { + return err + } + + return nil +} + +// CancelTip 清除消息提醒 +func CancelTip(uid uint64) error { + err := services.Redis.SetBit(tipKey, int64(uid), 0) + if err != nil { + return err + } + + return nil +} + +// CheckTip 检查是否有提醒 +func CheckTip(uid uint64) bool { + val, err := services.Redis.GetBit(tipKey, int64(uid)) + if err != nil { + return false + } + + return val == 1 +} diff --git a/common/services/services.go b/common/services/services.go new file mode 100644 index 0000000..7c474aa --- /dev/null +++ b/common/services/services.go @@ -0,0 +1,10 @@ +package services + +import "91porn-server/common/redis" + +var Redis *redis.Client + +// Init 初始化 +func Init(redis *redis.Client) { + Redis = redis +} diff --git a/common/shortrecommend/shortrecommend.go b/common/shortrecommend/shortrecommend.go new file mode 100644 index 0000000..244b0a3 --- /dev/null +++ b/common/shortrecommend/shortrecommend.go @@ -0,0 +1,1260 @@ +package shortrecommend + +import ( + "bytes" + "context" + cryptorand "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "hash/fnv" + "math/rand" + "sort" + "strconv" + "strings" + "sync/atomic" + "time" + + "91porn-server/common/redis" + "91porn-server/models/v/vidmod" +) + +const ( + CurrentKey = "recommend:short:current" + BuildLockKey = "recommend:short:build-lock" + defaultKeyTTL = 72 * time.Hour + minKeyTTLHours = 24 + maxKeyTTLHours = 30 * 24 + buildingKeyTTL = time.Hour + takeIdempotencyTTL = 30 * time.Second + reservationTTL = 15 * time.Second + maxTakeRequestIDLength = 128 + healthTTLDriftTolerance = 2 * time.Second + queuePushBatchSize = 1000 + queuePipelineBatchCount = 16 + publishCleanupTimeout = 5 * time.Second +) + +var keyTTLSeconds atomic.Int64 + +func init() { + keyTTLSeconds.Store(int64(defaultKeyTTL / time.Second)) +} + +// SetKeyTTLHours 配置正式队列、元数据和用户偏移的统一绝对过期时间。 +func SetKeyTTLHours(hours int) error { + if hours < minKeyTTLHours || hours > maxKeyTTLHours { + return fmt.Errorf( + "short recommend key TTL hours must be between %d and %d", + minKeyTTLHours, maxKeyTTLHours, + ) + } + keyTTLSeconds.Store(int64(time.Duration(hours) * time.Hour / time.Second)) + return nil +} + +// KeyTTL 返回当前并发安全的短推荐键过期时间配置。 +func KeyTTL() time.Duration { + seconds := keyTTLSeconds.Load() + if seconds <= 0 { + return defaultKeyTTL + } + return time.Duration(seconds) * time.Second +} + +type QueueMeta struct { + Version string + Length int + GeneratedAt time.Time + StartOffset int +} + +func QueueKey(version string) string { return "recommend:short:queue:" + version } +func OffsetKey(version string) string { return "recommend:short:offset:" + version } +func MetaKey(version string) string { return "recommend:short:meta:" + version } +func LockKey(_ string) string { return BuildLockKey } + +func VersionAt(t time.Time) string { + return t.In(time.FixedZone("CST", 8*60*60)).Format("20060102") +} + +// VersionWithRevision 为同一天的每次实际重建生成独立队列版本。 +// revision 必须由调用方提供非空且唯一的构建标识。 +func VersionWithRevision(t time.Time, revision string) string { + return VersionAt(t) + "-" + revision +} + +func versionMatchesDate(version, date string) bool { + return version == date || + (len(version) > len(date)+1 && strings.HasPrefix(version, date+"-")) +} + +type AssembleStats struct { + TotalCount int + HighCount int + NewCount int + BlockCount int +} + +// Assemble 按每块17条高分+3条新视频组装,视频全局去重,尾块保留实际数量。 +func Assemble(input []vidmod.RecommendCandidate, generatedAt time.Time) []string { + ids, _ := AssembleWithStats(input, generatedAt) + return ids +} + +// AssembleWithStats 在组装队列的同时返回高分池、新视频池和块数指标。 +func AssembleWithStats( + input []vidmod.RecommendCandidate, + generatedAt time.Time, +) ([]string, AssembleStats) { + cutoff := generatedAt.Add(-24 * time.Hour) + news := make([]*vidmod.RecommendCandidate, 0) + high := make([]*vidmod.RecommendCandidate, 0) + for i := range input { + c := &input[i] + if c.ReviewAt.After(generatedAt) { + continue + } + if !c.ReviewAt.Before(cutoff) && !c.ReviewAt.After(generatedAt) { + news = append(news, c) + } else { + high = append(high, c) + } + } + sort.Slice(news, func(i, j int) bool { + if news[i].ReviewAt.Equal(news[j].ReviewAt) { + return bytes.Compare(news[i].ID[:], news[j].ID[:]) > 0 + } + return news[i].ReviewAt.After(news[j].ReviewAt) + }) + sort.Slice(high, func(i, j int) bool { + if high[i].RecommendScore != high[j].RecommendScore { + return high[i].RecommendScore > high[j].RecommendScore + } + if !high[i].ReviewAt.Equal(high[j].ReviewAt) { + return high[i].ReviewAt.After(high[j].ReviewAt) + } + return bytes.Compare(high[i].ID[:], high[j].ID[:]) > 0 + }) + + out := make([]string, 0, len(input)) + hi, ni, block := 0, 0, 0 + for hi < len(high) || ni < len(news) { + blockStart := len(out) + highCount := 17 + newCount := min(3, len(news)-ni) + highCount += 3 - newCount + highCount = min(highCount, len(high)-hi) + for n := 0; n < highCount; n++ { + out = append(out, high[hi].ID.Hex()) + hi++ + } + for n := 0; n < newCount; n++ { + out = append(out, news[ni].ID.Hex()) + ni++ + } + if len(out) == blockStart { // 高分耗尽后继续消费剩余新视频,仍不复制凑数 + for ni < len(news) && len(out)-blockStart < 3 { + out = append(out, news[ni].ID.Hex()) + ni++ + } + } + seed := deterministicSeed(VersionAt(generatedAt), block) + part := out[blockStart:] + rand.New(rand.NewSource(seed)).Shuffle(len(part), func(i, j int) { + part[i], part[j] = part[j], part[i] + }) + block++ + } + return out, AssembleStats{ + TotalCount: len(out), + HighCount: len(high), + NewCount: len(news), + BlockCount: block, + } +} + +func deterministicSeed(version string, block int) int64 { + h := fnv.New64a() + _, _ = h.Write([]byte(version + ":" + strconv.Itoa(block))) + return int64(h.Sum64()) +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +type publishClient interface { + DelContext(ctx context.Context, keys ...string) (int64, error) + EvalContext( + ctx context.Context, + script string, + keys []string, + args ...interface{}, + ) (interface{}, error) + RPushPipelineContext( + ctx context.Context, + key string, + values []string, + batchSize int, + expirations ...time.Duration, + ) (int64, error) +} + +// Publish 完整写入新版本后再切换current,构建失败不会破坏旧版本。 +func Publish(ctx context.Context, client *redis.Client, meta QueueMeta, ids []string, lockKey, lockToken string) error { + return publish(ctx, client, meta, ids, lockKey, lockToken) +} + +func publish( + ctx context.Context, + client publishClient, + meta QueueMeta, + ids []string, + lockKey, lockToken string, +) error { + if len(ids) == 0 { + return fmt.Errorf("short recommend queue is empty") + } + if lockKey == "" || lockToken == "" { + return fmt.Errorf("short recommend build lock is required") + } + if !versionMatchesDate(meta.Version, VersionAt(meta.GeneratedAt)) { + return fmt.Errorf("short recommend version does not match generatedAt") + } + if meta.Length != len(ids) { + return fmt.Errorf("short recommend metadata length does not match queue length") + } + queueTTL := KeyTTL() + queueKey, metaKey := QueueKey(meta.Version), MetaKey(meta.Version) + suffix := ":" + strconv.FormatInt(time.Now().UnixNano(), 10) + tmpQueueKey, tmpMetaKey := queueKey+":building"+suffix, metaKey+":building"+suffix + const firstPushScript = ` +for i = 1, #ARGV - 1 do + redis.call('RPUSH', KEYS[1], ARGV[i]) +end +redis.call('EXPIRE', KEYS[1], ARGV[#ARGV]) +return 1 +` + if err := ctx.Err(); err != nil { + return err + } + firstEnd := min(queuePushBatchSize, len(ids)) + firstValues := make([]interface{}, firstEnd) + for i := 0; i < firstEnd; i++ { + firstValues[i] = ids[i] + } + firstArgs := append(firstValues, int64(buildingKeyTTL/time.Second)) + if _, err := client.EvalContext(ctx, firstPushScript, []string{tmpQueueKey}, firstArgs...); err != nil { + cleanupPublishKeys(client, tmpQueueKey, tmpMetaKey) + return err + } + + pipelineWindowSize := queuePushBatchSize * queuePipelineBatchCount + for start := firstEnd; start < len(ids); start += pipelineWindowSize { + if err := ctx.Err(); err != nil { + cleanupPublishKeys(client, tmpQueueKey, tmpMetaKey) + return err + } + end := min(start+pipelineWindowSize, len(ids)) + length, err := client.RPushPipelineContext( + ctx, tmpQueueKey, ids[start:end], queuePushBatchSize, buildingKeyTTL, + ) + if err != nil { + cleanupPublishKeys(client, tmpQueueKey, tmpMetaKey) + return err + } + if length != int64(end) { + cleanupPublishKeys(client, tmpQueueKey, tmpMetaKey) + return fmt.Errorf( + "short recommend queue length mismatch after pipeline: got %d, want %d", + length, end, + ) + } + } + if err := ctx.Err(); err != nil { + cleanupPublishKeys(client, tmpQueueKey, tmpMetaKey) + return err + } + const writeMetaScript = ` +redis.call('HSET', KEYS[1], + 'length', ARGV[1], + 'generatedAt', ARGV[2], + 'startOffset', ARGV[3]) +redis.call('EXPIRE', KEYS[1], ARGV[4]) +return 1 +` + if _, err := client.EvalContext(ctx, writeMetaScript, []string{tmpMetaKey}, + len(ids), + meta.GeneratedAt.UTC().Format(time.RFC3339), + meta.StartOffset, + int64(buildingKeyTTL/time.Second)); err != nil { + cleanupPublishKeys(client, tmpQueueKey, tmpMetaKey) + return err + } + if err := ctx.Err(); err != nil { + cleanupPublishKeys(client, tmpQueueKey, tmpMetaKey) + return err + } + const swapScript = ` +if redis.call('GET', KEYS[7]) ~= ARGV[3] then + return 0 +end +local redisTime = redis.call('TIME') +if tonumber(redisTime[1]) >= tonumber(ARGV[4]) then + return -1 +end +if redis.call('LLEN', KEYS[1]) ~= tonumber(ARGV[5]) then + return -2 +end +if tonumber(redis.call('HGET', KEYS[2], 'length') or '-1') ~= tonumber(ARGV[5]) then + return -3 +end +local oldVersion = redis.call('GET', KEYS[6]) +redis.call('UNLINK', KEYS[5]) +if oldVersion and oldVersion ~= ARGV[1] then + redis.call('UNLINK', ARGV[6] .. oldVersion) +end +redis.call('RENAME', KEYS[1], KEYS[3]) +redis.call('RENAME', KEYS[2], KEYS[4]) +local expiresAtMs = + tonumber(redisTime[1]) * 1000 + + math.floor(tonumber(redisTime[2]) / 1000) + + tonumber(ARGV[2]) * 1000 +redis.call('HSET', KEYS[4], 'expiresAtMs', expiresAtMs) +redis.call('PEXPIREAT', KEYS[3], expiresAtMs) +redis.call('PEXPIREAT', KEYS[4], expiresAtMs) +redis.call('SET', KEYS[6], ARGV[1]) +return 1 +` + swapped, err := client.EvalContext(ctx, swapScript, + []string{ + tmpQueueKey, tmpMetaKey, queueKey, metaKey, + OffsetKey(meta.Version), CurrentKey, lockKey, + }, + meta.Version, int64(queueTTL/time.Second), lockToken, + versionValidUntil(meta).Unix(), len(ids), "recommend:short:offset:") + if err != nil || fmt.Sprint(swapped) != "1" { + cleanupPublishKeys(client, tmpQueueKey, tmpMetaKey) + if err != nil { + return err + } + switch fmt.Sprint(swapped) { + case "0": + return fmt.Errorf("short recommend build lock lost before queue swap") + case "-1": + return fmt.Errorf("short recommend version expired before queue swap") + case "-2": + return fmt.Errorf("short recommend queue length mismatch before queue swap") + case "-3": + return fmt.Errorf("short recommend metadata length mismatch before queue swap") + default: + return fmt.Errorf("short recommend queue swap rejected: %v", swapped) + } + } + return nil +} + +func cleanupPublishKeys(client publishClient, keys ...string) { + cleanupCtx, cancel := context.WithTimeout(context.Background(), publishCleanupTimeout) + defer cancel() + _, _ = client.DelContext(cleanupCtx, keys...) +} + +func versionValidUntil(meta QueueMeta) time.Time { + cst := time.FixedZone("CST", 8*60*60) + generatedAt := meta.GeneratedAt.In(cst) + return time.Date( + generatedAt.Year(), generatedAt.Month(), generatedAt.Day()+1, + 0, 0, 0, 0, cst, + ) +} + +var ( + // ErrVersionChanged 表示调用方固定的队列版本已被每日切版替换。 + ErrVersionChanged = errors.New("short recommend queue version changed") + // ErrQueueUnhealthy 表示current指向的队列、元数据或TTL不完整。 + ErrQueueUnhealthy = errors.New("short recommend queue is unhealthy") + // ErrReservationBusy 表示同一用户已有另一个请求正在读取当前偏移。 + ErrReservationBusy = errors.New("short recommend offset reservation is busy") + // ErrReservationExpired 表示预留在提交前已过期,偏移未推进。 + ErrReservationExpired = errors.New("short recommend offset reservation expired") + // ErrReservationConflict 表示预留内容或当前偏移与提交参数不一致。 + ErrReservationConflict = errors.New("short recommend offset reservation conflict") +) + +const takeScriptSource = ` +local function readBatch(queueKey, version, length, offset, wanted) + local result = {version, tostring(length), tostring(offset)} + local firstEnd = math.min(offset + wanted - 1, length - 1) + local first = redis.call('LRANGE', queueKey, offset, firstEnd) + for _, item in ipairs(first) do + table.insert(result, item) + end + local remaining = wanted - #first + if remaining > 0 then + local wrapped = redis.call('LRANGE', queueKey, 0, remaining - 1) + for _, item in ipairs(wrapped) do + table.insert(result, item) + end + end + return result +end +local version = redis.call('GET', KEYS[1]) +if not version then return {} end +if ARGV[6] ~= '' and version ~= ARGV[6] then + return {version, '-1', '0'} +end +local queueKey = ARGV[1] .. version +local offsetKey = ARGV[2] .. version +local metaKey = ARGV[3] .. version +local offsetKeyTTL = redis.call('PTTL', offsetKey) +local length = redis.call('LLEN', queueKey) +local metaLength = tonumber(redis.call('HGET', metaKey, 'length') or '-1') +local queueTTL = redis.call('PTTL', queueKey) +local metaTTL = redis.call('PTTL', metaKey) +if length == 0 or metaLength ~= length or queueTTL <= 0 or metaTTL <= 0 then + return {version, '-2', '0'} +end +local idempotencyKey = '' +if ARGV[7] ~= '' then + idempotencyKey = ARGV[7] .. ':' .. version + local cached = redis.call('LRANGE', idempotencyKey, 0, -1) + if #cached == 4 then + local cachedVersion = cached[1] + local cachedLength = tonumber(cached[2]) + local cachedOffset = tonumber(cached[3]) + local cachedWanted = tonumber(cached[4]) + if cachedVersion == version and cachedLength == length and + cachedOffset and cachedWanted then + return readBatch( + queueKey, cachedVersion, cachedLength, cachedOffset, cachedWanted + ) + end + end + if #cached > 0 then + redis.call('DEL', idempotencyKey) + end +end +local wanted = tonumber(ARGV[5]) +if wanted > length then wanted = length end +local storedOffset = redis.call('HGET', offsetKey, ARGV[4]) +local offset +if storedOffset then + offset = tonumber(storedOffset) or 0 +else + offset = tonumber(redis.call('HGET', metaKey, 'startOffset') or '0') +end +offset = offset % length +local result = readBatch(queueKey, version, length, offset, wanted) +redis.call('HSET', offsetKey, ARGV[4], (offset + wanted) % length) +local expiresAtMs = tonumber(redis.call('HGET', metaKey, 'expiresAtMs') or '0') +if expiresAtMs <= 0 then + local redisTime = redis.call('TIME') + expiresAtMs = + tonumber(redisTime[1]) * 1000 + + math.floor(tonumber(redisTime[2]) / 1000) + + math.min(queueTTL, metaTTL) +end +if offsetKeyTTL <= 0 then + redis.call('PEXPIREAT', offsetKey, expiresAtMs) +end +if idempotencyKey ~= '' then + redis.call( + 'RPUSH', idempotencyKey, + version, tostring(length), tostring(offset), tostring(wanted) + ) + redis.call('EXPIRE', idempotencyKey, tonumber(ARGV[8])) +end +return result +` + +var takeRedisScript = redis.NewScript(takeScriptSource) + +type TakeResult struct { + Version string + Length int + Offset int + IDs []string +} + +type scriptClient interface { + RunScriptContext( + ctx context.Context, + script *redis.Script, + keys []string, + args ...interface{}, + ) (interface{}, error) +} + +// Take 原子读取并推进单个用户的下一偏移,保留原调用方式。 +func Take(client *redis.Client, uid uint64, size int) (TakeResult, error) { + return TakeVersionContext(context.Background(), client, uid, size, "") +} + +// TakeVersion 原子读取指定版本;切版后返回 ErrVersionChanged 且不推进新队列。 +func TakeVersion( + client *redis.Client, + uid uint64, + size int, + expectedVersion string, +) (TakeResult, error) { + return TakeVersionContext(context.Background(), client, uid, size, expectedVersion) +} + +// TakeVersionContext 与 TakeVersion 相同,并将请求取消和超时传递到Redis。 +func TakeVersionContext( + ctx context.Context, + client *redis.Client, + uid uint64, + size int, + expectedVersion string, +) (TakeResult, error) { + return takeContext(ctx, client, uid, size, expectedVersion, "") +} + +// TakeIdempotentContext 为一次HTTP请求的单个补位批次提供短时幂等保护。 +// 同一uid/requestID/batchIndex重试会返回原批次,不会再次推进偏移; +// expectedVersion可在首批留空,后续批次应固定为首批返回的Version。 +func TakeIdempotentContext( + ctx context.Context, + client *redis.Client, + uid uint64, + size int, + expectedVersion, requestID string, + batchIndex int, +) (TakeResult, error) { + return takeIdempotentContext( + ctx, client, uid, size, expectedVersion, requestID, batchIndex, + ) +} + +// TakeCurrentIdempotentContext 用于首批读取:Lua原子读取current,并按该版本隔离幂等缓存, +// 因而不需要额外执行Health/GET;切版后不会复用上一版本缓存。 +func TakeCurrentIdempotentContext( + ctx context.Context, + client *redis.Client, + uid uint64, + size int, + requestID string, + batchIndex int, +) (TakeResult, error) { + return takeIdempotentContext(ctx, client, uid, size, "", requestID, batchIndex) +} + +func takeIdempotentContext( + ctx context.Context, + client scriptClient, + uid uint64, + size int, + expectedVersion, requestID string, + batchIndex int, +) (TakeResult, error) { + requestID = strings.TrimSpace(requestID) + if requestID == "" { + return TakeResult{}, fmt.Errorf("short recommend request ID is required") + } + if len(requestID) > maxTakeRequestIDLength { + return TakeResult{}, fmt.Errorf( + "short recommend request ID exceeds %d bytes", + maxTakeRequestIDLength, + ) + } + if batchIndex < 0 { + return TakeResult{}, fmt.Errorf("short recommend batch index must not be negative") + } + requestHash := sha256.Sum256([]byte(requestID)) + idempotencyKey := fmt.Sprintf( + "recommend:short:take-cache:%d:%x:%d", + uid, requestHash[:16], batchIndex, + ) + return takeContext(ctx, client, uid, size, expectedVersion, idempotencyKey) +} + +func takeContext( + ctx context.Context, + client scriptClient, + uid uint64, + size int, + expectedVersion, idempotencyKey string, +) (TakeResult, error) { + if size <= 0 { + return TakeResult{}, nil + } + if ctx == nil { + return TakeResult{}, fmt.Errorf("short recommend take context must not be nil") + } + if client == nil { + return TakeResult{}, fmt.Errorf("short recommend take Redis client must not be nil") + } + if err := ctx.Err(); err != nil { + return TakeResult{}, err + } + raw, err := client.RunScriptContext(ctx, takeRedisScript, []string{CurrentKey}, + "recommend:short:queue:", + "recommend:short:offset:", + "recommend:short:meta:", + strconv.FormatUint(uid, 10), + size, + expectedVersion, + idempotencyKey, + int64(takeIdempotencyTTL/time.Second)) + if err != nil { + return TakeResult{}, err + } + items, ok := raw.([]interface{}) + if !ok || len(items) < 3 { + return TakeResult{}, nil + } + result := TakeResult{Version: fmt.Sprint(items[0])} + if result.Version == "" { + return TakeResult{}, fmt.Errorf("short recommend take returned empty version") + } + result.Length, err = strconv.Atoi(fmt.Sprint(items[1])) + if err != nil { + return TakeResult{}, fmt.Errorf("short recommend take returned invalid length: %w", err) + } + result.Offset, err = strconv.Atoi(fmt.Sprint(items[2])) + if err != nil { + return TakeResult{}, fmt.Errorf("short recommend take returned invalid offset: %w", err) + } + switch result.Length { + case -1: + result.Length = 0 + return result, ErrVersionChanged + case -2: + result.Length = 0 + return result, ErrQueueUnhealthy + } + if result.Length <= 0 || result.Offset < 0 || result.Offset >= result.Length { + return TakeResult{}, fmt.Errorf( + "short recommend take returned invalid bounds: length=%d offset=%d", + result.Length, result.Offset, + ) + } + for _, item := range items[3:] { + result.IDs = append(result.IDs, fmt.Sprint(item)) + } + return result, nil +} + +// Reservation 是一次请求对用户偏移的临时预留。Reserve 只读取队列, +// Commit 成功后才推进偏移;预留过期或 Abort 均不会改变用户进度。 +type Reservation struct { + Version string + Length int + Offset int + Reserved int + IDs []string + LeaseToken string + ReceiptID string + AlreadyCommitted bool +} + +const reserveScriptSource = ` +local function readBatch(queueKey, version, status, length, offset, wanted, token) + local result = { + version, status, tostring(length), tostring(offset), tostring(wanted), token + } + local firstEnd = math.min(offset + wanted - 1, length - 1) + local first = redis.call('LRANGE', queueKey, offset, firstEnd) + for _, item in ipairs(first) do + table.insert(result, item) + end + local remaining = wanted - #first + if remaining > 0 then + local wrapped = redis.call('LRANGE', queueKey, 0, remaining - 1) + for _, item in ipairs(wrapped) do + table.insert(result, item) + end + end + return result +end + +local receiptKey = ARGV[6] .. ARGV[4] .. ':' .. ARGV[10] +local receipt = redis.call('LRANGE', receiptKey, 0, -1) +if #receipt == 4 then + local receiptVersion = receipt[1] + local receiptLength = tonumber(receipt[2]) + local receiptOffset = tonumber(receipt[3]) + local receiptConsumed = tonumber(receipt[4]) + local receiptQueueKey = ARGV[1] .. receiptVersion + if receiptLength and receiptOffset and receiptConsumed and + receiptLength > 0 and receiptConsumed > 0 and + receiptConsumed <= receiptLength and + redis.call('LLEN', receiptQueueKey) == receiptLength and + redis.call('PTTL', receiptQueueKey) > 0 then + return readBatch( + receiptQueueKey, receiptVersion, 'COMMITTED', + receiptLength, receiptOffset, receiptConsumed, ARGV[9] + ) + end + redis.call('DEL', receiptKey) +elseif #receipt > 0 then + redis.call('DEL', receiptKey) +end + +local version = redis.call('GET', KEYS[1]) +if not version then return {} end +if ARGV[8] ~= '' and version ~= ARGV[8] then + return {version, 'VERSION_CHANGED', '0', '0', '0', ''} +end +local queueKey = ARGV[1] .. version +local offsetKey = ARGV[2] .. version +local metaKey = ARGV[3] .. version +local length = redis.call('LLEN', queueKey) +local metaLength = tonumber(redis.call('HGET', metaKey, 'length') or '-1') +local queueTTL = redis.call('PTTL', queueKey) +local metaTTL = redis.call('PTTL', metaKey) +if length == 0 or metaLength ~= length or queueTTL <= 0 or metaTTL <= 0 then + return {version, 'UNHEALTHY', '0', '0', '0', ''} +end + +local leaseKey = ARGV[5] .. version .. ':' .. ARGV[4] +local lease = redis.call( + 'HMGET', leaseKey, 'token', 'receiptID', 'length', 'offset', 'reserved' +) +if lease[1] then + if lease[1] ~= ARGV[9] or lease[2] ~= ARGV[10] then + return {version, 'BUSY', tostring(length), '0', '0', ''} + end + local leaseLength = tonumber(lease[3]) + local leaseOffset = tonumber(lease[4]) + local leaseReserved = tonumber(lease[5]) + if leaseLength == length and leaseOffset and leaseReserved and + leaseOffset >= 0 and leaseOffset < length and + leaseReserved > 0 and leaseReserved <= length then + redis.call('PEXPIRE', leaseKey, tonumber(ARGV[11])) + return readBatch( + queueKey, version, 'RESERVED', + length, leaseOffset, leaseReserved, ARGV[9] + ) + end + redis.call('DEL', leaseKey) +end + +local wanted = tonumber(ARGV[7]) +if wanted > length then wanted = length end +local storedOffset = redis.call('HGET', offsetKey, ARGV[4]) +local offset +if storedOffset then + offset = tonumber(storedOffset) or 0 +else + offset = tonumber(redis.call('HGET', metaKey, 'startOffset') or '0') +end +offset = offset % length +redis.call('HSET', leaseKey, + 'token', ARGV[9], + 'receiptID', ARGV[10], + 'length', tostring(length), + 'offset', tostring(offset), + 'reserved', tostring(wanted)) +redis.call('PEXPIRE', leaseKey, tonumber(ARGV[11])) +return readBatch( + queueKey, version, 'RESERVED', length, offset, wanted, ARGV[9] +) +` + +const commitReservationScriptSource = ` +local receiptKey = ARGV[14] .. ARGV[5] .. ':' .. ARGV[8] +local receipt = redis.call('LRANGE', receiptKey, 0, -1) +if #receipt == 4 then + if receipt[1] == ARGV[6] and + tonumber(receipt[2]) == tonumber(ARGV[9]) and + tonumber(receipt[3]) == tonumber(ARGV[10]) and + tonumber(receipt[4]) == tonumber(ARGV[12]) then + return 2 + end + return -5 +elseif #receipt > 0 then + return -5 +end + +local version = redis.call('GET', KEYS[1]) +if version ~= ARGV[6] then return -1 end +local queueKey = ARGV[1] .. version +local offsetKey = ARGV[2] .. version +local metaKey = ARGV[3] .. version +local length = redis.call('LLEN', queueKey) +local metaLength = tonumber(redis.call('HGET', metaKey, 'length') or '-1') +if length <= 0 or metaLength ~= length or length ~= tonumber(ARGV[9]) or + redis.call('PTTL', queueKey) <= 0 or redis.call('PTTL', metaKey) <= 0 then + return -2 +end + +local leaseKey = ARGV[4] .. version .. ':' .. ARGV[5] +local lease = redis.call( + 'HMGET', leaseKey, 'token', 'receiptID', 'length', 'offset', 'reserved' +) +if not lease[1] then return -3 end +if lease[1] ~= ARGV[7] or lease[2] ~= ARGV[8] or + tonumber(lease[3]) ~= tonumber(ARGV[9]) or + tonumber(lease[4]) ~= tonumber(ARGV[10]) or + tonumber(lease[5]) ~= tonumber(ARGV[11]) then + return -4 +end +local consumed = tonumber(ARGV[12]) +if not consumed or consumed <= 0 or consumed > tonumber(lease[5]) then + return -4 +end + +local storedOffset = redis.call('HGET', offsetKey, ARGV[5]) +local currentOffset +if storedOffset then + currentOffset = tonumber(storedOffset) or 0 +else + currentOffset = tonumber(redis.call('HGET', metaKey, 'startOffset') or '0') +end +currentOffset = currentOffset % length +if currentOffset ~= tonumber(lease[4]) then return -4 end + +redis.call('HSET', offsetKey, ARGV[5], (currentOffset + consumed) % length) +local offsetKeyTTL = redis.call('PTTL', offsetKey) +local expiresAtMs = tonumber(redis.call('HGET', metaKey, 'expiresAtMs') or '0') +if expiresAtMs <= 0 then + local redisTime = redis.call('TIME') + expiresAtMs = + tonumber(redisTime[1]) * 1000 + + math.floor(tonumber(redisTime[2]) / 1000) + + math.min(redis.call('PTTL', queueKey), redis.call('PTTL', metaKey)) +end +if offsetKeyTTL <= 0 then + redis.call('PEXPIREAT', offsetKey, expiresAtMs) +end +redis.call('DEL', leaseKey) +redis.call( + 'RPUSH', receiptKey, + version, tostring(length), tostring(currentOffset), tostring(consumed) +) +redis.call('EXPIRE', receiptKey, tonumber(ARGV[13])) +return 1 +` + +const abortReservationScriptSource = ` +local leaseKey = ARGV[1] .. ARGV[2] .. ':' .. ARGV[3] +local lease = redis.call('HMGET', leaseKey, 'token', 'receiptID') +if lease[1] ~= ARGV[4] or lease[2] ~= ARGV[5] then + return 0 +end +return redis.call('DEL', leaseKey) +` + +var ( + reserveRedisScript = redis.NewScript(reserveScriptSource) + commitReservationRedisScript = redis.NewScript(commitReservationScriptSource) + abortReservationRedisScript = redis.NewScript(abortReservationScriptSource) +) + +// ReserveCurrentContext 原子预留当前版本的一段队列,但不推进用户偏移。 +func ReserveCurrentContext( + ctx context.Context, + client *redis.Client, + uid uint64, + size int, + requestID string, +) (Reservation, error) { + if client == nil { + return Reservation{}, fmt.Errorf("short recommend reserve Redis client must not be nil") + } + return reserveContext(ctx, client, uid, size, "", requestID) +} + +// ReserveVersionContext 与 ReserveCurrentContext 相同,但要求current仍为指定版本。 +func ReserveVersionContext( + ctx context.Context, + client *redis.Client, + uid uint64, + size int, + expectedVersion, requestID string, +) (Reservation, error) { + if client == nil { + return Reservation{}, fmt.Errorf("short recommend reserve Redis client must not be nil") + } + return reserveContext(ctx, client, uid, size, expectedVersion, requestID) +} + +func reserveContext( + ctx context.Context, + client scriptClient, + uid uint64, + size int, + expectedVersion, requestID string, +) (Reservation, error) { + if size <= 0 { + return Reservation{}, nil + } + if ctx == nil { + return Reservation{}, fmt.Errorf("short recommend reserve context must not be nil") + } + if client == nil { + return Reservation{}, fmt.Errorf("short recommend reserve Redis client must not be nil") + } + if uid == 0 { + return Reservation{}, fmt.Errorf("short recommend reserve uid must be positive") + } + if err := ctx.Err(); err != nil { + return Reservation{}, err + } + requestID = strings.TrimSpace(requestID) + if len(requestID) > maxTakeRequestIDLength { + return Reservation{}, fmt.Errorf( + "short recommend request ID exceeds %d bytes", + maxTakeRequestIDLength, + ) + } + leaseToken, err := newReservationToken() + if err != nil { + return Reservation{}, err + } + receiptID := leaseToken + if requestID != "" { + sum := sha256.Sum256([]byte(strconv.FormatUint(uid, 10) + ":" + requestID)) + receiptID = hex.EncodeToString(sum[:16]) + } + keys := []string{CurrentKey} + args := []interface{}{ + "recommend:short:queue:", + "recommend:short:offset:", + "recommend:short:meta:", + strconv.FormatUint(uid, 10), + "recommend:short:reservation:", + "recommend:short:commit-receipt:", + size, + expectedVersion, + leaseToken, + receiptID, + int64(reservationTTL / time.Millisecond), + } + raw, err := client.RunScriptContext(ctx, reserveRedisScript, keys, args...) + if err != nil && ctx.Err() == nil { + // 预留脚本可能已经执行成功但响应在网络中丢失。使用完全相同的 + // token/receipt 重试可命中原租约,不会创建第二个预留或推进偏移。 + raw, err = client.RunScriptContext(ctx, reserveRedisScript, keys, args...) + } + if err != nil { + return Reservation{}, err + } + return parseReservation(raw, receiptID) +} + +func newReservationToken() (string, error) { + value := make([]byte, 16) + if _, err := cryptorand.Read(value); err != nil { + return "", fmt.Errorf("generate short recommend reservation token: %w", err) + } + return hex.EncodeToString(value), nil +} + +func parseReservation(raw interface{}, receiptID string) (Reservation, error) { + items, ok := raw.([]interface{}) + if !ok || len(items) == 0 { + return Reservation{}, nil + } + if len(items) < 6 { + return Reservation{}, fmt.Errorf("short recommend reserve returned malformed result") + } + result := Reservation{ + Version: fmt.Sprint(items[0]), + LeaseToken: fmt.Sprint(items[5]), + ReceiptID: receiptID, + } + status := fmt.Sprint(items[1]) + var err error + result.Length, err = strconv.Atoi(fmt.Sprint(items[2])) + if err != nil { + return Reservation{}, fmt.Errorf("short recommend reserve returned invalid length: %w", err) + } + result.Offset, err = strconv.Atoi(fmt.Sprint(items[3])) + if err != nil { + return Reservation{}, fmt.Errorf("short recommend reserve returned invalid offset: %w", err) + } + result.Reserved, err = strconv.Atoi(fmt.Sprint(items[4])) + if err != nil { + return Reservation{}, fmt.Errorf("short recommend reserve returned invalid size: %w", err) + } + switch status { + case "VERSION_CHANGED": + return result, ErrVersionChanged + case "UNHEALTHY": + return result, ErrQueueUnhealthy + case "BUSY": + return result, ErrReservationBusy + case "COMMITTED": + result.AlreadyCommitted = true + case "RESERVED": + default: + return Reservation{}, fmt.Errorf("short recommend reserve returned unknown status %q", status) + } + if result.Version == "" || result.Length <= 0 || + result.Offset < 0 || result.Offset >= result.Length || + result.Reserved <= 0 || result.Reserved > result.Length || + len(items)-6 != result.Reserved { + return Reservation{}, fmt.Errorf( + "short recommend reserve returned invalid bounds: version=%q length=%d offset=%d reserved=%d ids=%d", + result.Version, result.Length, result.Offset, result.Reserved, len(items)-6, + ) + } + for _, item := range items[6:] { + result.IDs = append(result.IDs, fmt.Sprint(item)) + } + return result, nil +} + +// CommitReservationContext 原子校验预留并推进实际扫描的条数。 +func CommitReservationContext( + ctx context.Context, + client *redis.Client, + uid uint64, + reservation Reservation, + consumed int, +) error { + if client == nil { + return fmt.Errorf("short recommend commit Redis client must not be nil") + } + return commitReservationContext(ctx, client, uid, reservation, consumed) +} + +func commitReservationContext( + ctx context.Context, + client scriptClient, + uid uint64, + reservation Reservation, + consumed int, +) error { + if ctx == nil { + return fmt.Errorf("short recommend commit context must not be nil") + } + if client == nil { + return fmt.Errorf("short recommend commit Redis client must not be nil") + } + if err := validateReservationCommit(uid, reservation, consumed); err != nil { + return err + } + if err := ctx.Err(); err != nil { + return err + } + raw, err := client.RunScriptContext( + ctx, + commitReservationRedisScript, + []string{CurrentKey}, + "recommend:short:queue:", + "recommend:short:offset:", + "recommend:short:meta:", + "recommend:short:reservation:", + strconv.FormatUint(uid, 10), + reservation.Version, + reservation.LeaseToken, + reservation.ReceiptID, + reservation.Length, + reservation.Offset, + reservation.Reserved, + consumed, + int64(takeIdempotencyTTL/time.Second), + "recommend:short:commit-receipt:", + ) + if err != nil { + return err + } + switch fmt.Sprint(raw) { + case "1", "2": + return nil + case "-1": + return ErrVersionChanged + case "-2": + return ErrQueueUnhealthy + case "-3": + return ErrReservationExpired + case "-4", "-5": + return ErrReservationConflict + default: + return fmt.Errorf("short recommend commit returned unexpected result %v", raw) + } +} + +func validateReservationCommit(uid uint64, reservation Reservation, consumed int) error { + if uid == 0 { + return fmt.Errorf("short recommend commit uid must be positive") + } + if reservation.Version == "" || reservation.Length <= 0 || + reservation.Offset < 0 || reservation.Offset >= reservation.Length || + reservation.Reserved <= 0 || reservation.Reserved > reservation.Length || + reservation.LeaseToken == "" || reservation.ReceiptID == "" { + return fmt.Errorf("short recommend commit reservation is invalid") + } + if consumed <= 0 || consumed > reservation.Reserved { + return fmt.Errorf("short recommend consumed size is out of reservation bounds") + } + return nil +} + +// AbortReservationContext 仅删除token仍匹配的未提交预留,不改变用户偏移。 +func AbortReservationContext( + ctx context.Context, + client *redis.Client, + uid uint64, + reservation Reservation, +) error { + if client == nil { + return fmt.Errorf("short recommend abort Redis client must not be nil") + } + return abortReservationContext(ctx, client, uid, reservation) +} + +func abortReservationContext( + ctx context.Context, + client scriptClient, + uid uint64, + reservation Reservation, +) error { + if ctx == nil { + return fmt.Errorf("short recommend abort context must not be nil") + } + if client == nil { + return fmt.Errorf("short recommend abort Redis client must not be nil") + } + if uid == 0 || reservation.Version == "" || + reservation.LeaseToken == "" || reservation.ReceiptID == "" { + return nil + } + if err := ctx.Err(); err != nil { + return err + } + _, err := client.RunScriptContext( + ctx, + abortReservationRedisScript, + nil, + "recommend:short:reservation:", + reservation.Version, + strconv.FormatUint(uid, 10), + reservation.LeaseToken, + reservation.ReceiptID, + ) + return err +} + +type QueueHealth struct { + Healthy bool + Status string + Version string + QueueLength int + MetadataLength int + QueueTTL time.Duration + MetadataTTL time.Duration +} + +const healthScriptSource = ` +local version = redis.call('GET', KEYS[1]) +if not version then + return {'NO_CURRENT', '', '0', '-1', '-2', '-2'} +end +if ARGV[1] ~= '' then + local expectedDate = ARGV[1] + local matchesDate = + version == expectedDate or + ( + string.len(version) > string.len(expectedDate) + 1 and + string.sub(version, 1, string.len(expectedDate) + 1) == expectedDate .. '-' + ) + if not matchesDate then + return {'VERSION_MISMATCH', version, '0', '-1', '-2', '-2'} + end +end +local queueKey = ARGV[2] .. version +local metaKey = ARGV[3] .. version +local queueLength = redis.call('LLEN', queueKey) +local metadataLength = tonumber(redis.call('HGET', metaKey, 'length') or '-1') +local queueTTL = redis.call('PTTL', queueKey) +local metadataTTL = redis.call('PTTL', metaKey) +local status = 'OK' +if queueLength <= 0 then + status = 'QUEUE_EMPTY' +elseif metadataLength < 0 then + status = 'META_MISSING' +elseif metadataLength ~= queueLength then + status = 'LENGTH_MISMATCH' +elseif queueTTL <= 0 or metadataTTL <= 0 then + status = 'TTL_MISSING' +elseif math.abs(queueTTL - metadataTTL) > tonumber(ARGV[4]) then + status = 'TTL_MISMATCH' +end +return { + status, + version, + tostring(queueLength), + tostring(metadataLength), + tostring(queueTTL), + tostring(metadataTTL) +} +` + +var healthRedisScript = redis.NewScript(healthScriptSource) + +// Health 原子检查current、队列长度、metadata.length和两者TTL。 +func Health(client *redis.Client, expectedVersion string) (QueueHealth, error) { + return HealthContext(context.Background(), client, expectedVersion) +} + +// HealthContext 与 Health 相同,并将取消和超时传递到Redis。 +func HealthContext( + ctx context.Context, + client *redis.Client, + expectedVersion string, +) (QueueHealth, error) { + return healthContext(ctx, client, expectedVersion) +} + +func healthContext( + ctx context.Context, + client scriptClient, + expectedVersion string, +) (QueueHealth, error) { + if ctx == nil { + return QueueHealth{}, fmt.Errorf("short recommend health context must not be nil") + } + if client == nil { + return QueueHealth{}, fmt.Errorf("short recommend health Redis client must not be nil") + } + if err := ctx.Err(); err != nil { + return QueueHealth{}, err + } + raw, err := client.RunScriptContext(ctx, healthRedisScript, []string{CurrentKey}, + expectedVersion, + "recommend:short:queue:", + "recommend:short:meta:", + int64(healthTTLDriftTolerance/time.Millisecond)) + if err != nil { + return QueueHealth{}, err + } + items, ok := raw.([]interface{}) + if !ok || len(items) != 6 { + return QueueHealth{}, fmt.Errorf("short recommend health returned malformed result") + } + health := QueueHealth{ + Status: fmt.Sprint(items[0]), + Version: fmt.Sprint(items[1]), + } + health.QueueLength, err = strconv.Atoi(fmt.Sprint(items[2])) + if err != nil { + return QueueHealth{}, fmt.Errorf("short recommend health returned invalid queue length: %w", err) + } + health.MetadataLength, err = strconv.Atoi(fmt.Sprint(items[3])) + if err != nil { + return QueueHealth{}, fmt.Errorf("short recommend health returned invalid metadata length: %w", err) + } + queueTTLMillis, err := strconv.ParseInt(fmt.Sprint(items[4]), 10, 64) + if err != nil { + return QueueHealth{}, fmt.Errorf("short recommend health returned invalid queue TTL: %w", err) + } + metadataTTLMillis, err := strconv.ParseInt(fmt.Sprint(items[5]), 10, 64) + if err != nil { + return QueueHealth{}, fmt.Errorf("short recommend health returned invalid metadata TTL: %w", err) + } + health.QueueTTL = time.Duration(queueTTLMillis) * time.Millisecond + health.MetadataTTL = time.Duration(metadataTTLMillis) * time.Millisecond + health.Healthy = health.Status == "OK" + return health, nil +} diff --git a/common/shortrecommend/shortrecommend_test.go b/common/shortrecommend/shortrecommend_test.go new file mode 100644 index 0000000..f11053a --- /dev/null +++ b/common/shortrecommend/shortrecommend_test.go @@ -0,0 +1,1098 @@ +package shortrecommend + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + "sync" + "testing" + "time" + + "91porn-server/common/redis" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type fakePublishClient struct { + queue []string + pipelineWindowSizes []int + pipelineBatchSizes []int + pipelineExpirations []time.Duration + pipelineCalls int + pipelineErrorAt int + pipelineDuplicateAt int + cancelAfterPipelineAt int + cancel context.CancelFunc + duplicateFirstPush bool + firstPushTTL int64 + metaBuildingTTL int64 + publishedTTL int64 + metaLength int + swapAttempts int + swapped bool + deleteCalls int + queueLengthBeforeDelete int + swapScript string + swapKeys []string + swapArgs []interface{} +} + +func (f *fakePublishClient) DelContext(_ context.Context, _ ...string) (int64, error) { + f.deleteCalls++ + f.queueLengthBeforeDelete = len(f.queue) + f.queue = nil + return 1, nil +} + +func (f *fakePublishClient) EvalContext( + _ context.Context, + script string, + keys []string, + args ...interface{}, +) (interface{}, error) { + switch { + case strings.Contains(script, "for i = 1, #ARGV - 1"): + if len(args) < 2 { + return nil, fmt.Errorf("invalid first push args") + } + ids := make([]string, len(args)-1) + for i := range ids { + id, ok := args[i].(string) + if !ok { + return nil, fmt.Errorf("first push id %d is not a string", i) + } + ids[i] = id + } + ttl, ok := args[len(args)-1].(int64) + if !ok { + return nil, fmt.Errorf("first push ttl is not int64") + } + f.firstPushTTL = ttl + f.queue = append(f.queue, ids...) + if f.duplicateFirstPush { + f.queue = append(f.queue, ids...) + } + return int64(1), nil + case strings.Contains(script, "redis.call('HSET', KEYS[1]"): + length, ok := args[0].(int) + if !ok { + return nil, fmt.Errorf("meta length is not int") + } + f.metaLength = length + ttl, ok := args[3].(int64) + if !ok { + return nil, fmt.Errorf("meta ttl is not int64") + } + f.metaBuildingTTL = ttl + return int64(1), nil + case strings.Contains(script, "redis.call('RENAME'"): + f.swapAttempts++ + f.swapScript = script + f.swapKeys = append([]string(nil), keys...) + f.swapArgs = append([]interface{}(nil), args...) + ttl, ok := args[1].(int64) + if !ok { + return nil, fmt.Errorf("published ttl is not int64") + } + f.publishedTTL = ttl + expected, ok := args[4].(int) + if !ok { + return nil, fmt.Errorf("expected queue length is not int") + } + if len(f.queue) != expected { + return int64(-2), nil + } + f.swapped = true + return int64(1), nil + default: + return nil, fmt.Errorf("unexpected script") + } +} + +type fakeScriptClient struct { + result interface{} + err error + run func(*fakeScriptClient, context.Context, *redis.Script, []string, ...interface{}) (interface{}, error) + calls int + ctx context.Context + script *redis.Script + keys []string + args []interface{} +} + +func (f *fakeScriptClient) RunScriptContext( + ctx context.Context, + script *redis.Script, + keys []string, + args ...interface{}, +) (interface{}, error) { + f.calls++ + f.ctx = ctx + f.script = script + f.keys = append([]string(nil), keys...) + f.args = append([]interface{}(nil), args...) + if f.run != nil { + return f.run(f, ctx, script, keys, args...) + } + return f.result, f.err +} + +func (f *fakePublishClient) RPushPipelineContext( + _ context.Context, + _ string, + values []string, + batchSize int, + expirations ...time.Duration, +) (int64, error) { + f.pipelineCalls++ + f.pipelineWindowSizes = append(f.pipelineWindowSizes, len(values)) + f.pipelineBatchSizes = append(f.pipelineBatchSizes, batchSize) + if len(expirations) > 0 { + f.pipelineExpirations = append(f.pipelineExpirations, expirations[0]) + } + if f.pipelineCalls == f.pipelineErrorAt { + f.queue = append(f.queue, values[:len(values)/2]...) + return 0, fmt.Errorf("injected pipeline error") + } + f.queue = append(f.queue, values...) + if f.pipelineCalls == f.pipelineDuplicateAt { + f.queue = append(f.queue, values...) + } + if f.pipelineCalls == f.cancelAfterPipelineAt && f.cancel != nil { + f.cancel() + } + return int64(len(f.queue)), nil +} + +func candidate(n int, score int64, reviewAt time.Time) vidmod.RecommendCandidate { + return vidmod.RecommendCandidate{ + ID: primitive.NewObjectIDFromTimestamp(time.Unix(int64(n+1), int64(n))), + RecommendScore: score, + ReviewAt: reviewAt, + } +} + +func TestAssembleSeventeenHighThreeNew(t *testing.T) { + now := time.Date(2026, 7, 24, 8, 0, 0, 0, time.UTC) + input := make([]vidmod.RecommendCandidate, 0, 40) + for i := 0; i < 34; i++ { + input = append(input, candidate(i, int64(1000-i), now.Add(-48*time.Hour))) + } + for i := 0; i < 6; i++ { + input = append(input, candidate(100+i, 0, now.Add(-time.Duration(i)*time.Hour))) + } + got := Assemble(input, now) + if len(got) != 40 { + t.Fatalf("length=%d want=40", len(got)) + } + newIDs := map[string]bool{} + for i := 0; i < 6; i++ { + newIDs[input[34+i].ID.Hex()] = true + } + for block := 0; block < 2; block++ { + count := 0 + for _, id := range got[block*20 : block*20+20] { + if newIDs[id] { + count++ + } + } + if count != 3 { + t.Fatalf("block %d new count=%d want=3", block, count) + } + } + assertUnique(t, got) +} + +func TestAssembleWithStats(t *testing.T) { + now := time.Date(2026, 7, 24, 8, 0, 0, 0, time.UTC) + input := make([]vidmod.RecommendCandidate, 0, 23) + for i := 0; i < 20; i++ { + input = append(input, candidate(i, int64(1000-i), now.Add(-48*time.Hour))) + } + for i := 0; i < 3; i++ { + input = append(input, candidate(100+i, 0, now.Add(-time.Duration(i)*time.Hour))) + } + + ids, stats := AssembleWithStats(input, now) + if len(ids) != len(input) { + t.Fatalf("len(ids) = %d, want %d", len(ids), len(input)) + } + if stats.TotalCount != 23 || stats.HighCount != 20 || + stats.NewCount != 3 || stats.BlockCount != 2 { + t.Fatalf("stats = %+v", stats) + } +} + +func TestAssembleNewShortageFilledByHigh(t *testing.T) { + now := time.Now() + for newCount := 0; newCount <= 3; newCount++ { + t.Run(fmt.Sprintf("new_%d", newCount), func(t *testing.T) { + input := make([]vidmod.RecommendCandidate, 0, 20) + for i := 0; i < 20-newCount; i++ { + input = append(input, candidate(i, int64(100-i), now.Add(-48*time.Hour))) + } + for i := 0; i < newCount; i++ { + input = append(input, candidate(100+i, 0, now.Add(-time.Hour))) + } + got := Assemble(input, now) + if len(got) != 20 { + t.Fatalf("length=%d want=20", len(got)) + } + assertUnique(t, got) + }) + } +} + +func TestAssembleTailAndDeterminism(t *testing.T) { + now := time.Date(2026, 7, 24, 8, 0, 0, 0, time.UTC) + input := make([]vidmod.RecommendCandidate, 0, 9) + for i := 0; i < 7; i++ { + input = append(input, candidate(i, int64(i), now.Add(-48*time.Hour))) + } + input = append(input, candidate(100, 0, now.Add(-time.Hour)), candidate(101, 0, now.Add(-2*time.Hour))) + first, second := Assemble(input, now), Assemble(input, now) + if fmt.Sprint(first) != fmt.Sprint(second) { + t.Fatalf("same version must be deterministic") + } + if len(first) != 9 { + t.Fatalf("tail length=%d want=9", len(first)) + } + assertUnique(t, first) +} + +func TestAssembleReviewAtBoundariesExcludesFuture(t *testing.T) { + now := time.Date(2026, 7, 24, 8, 0, 0, 0, time.UTC) + input := []vidmod.RecommendCandidate{ + candidate(1, 100, now.Add(-24*time.Hour)), + candidate(2, 99, now.Add(-24*time.Hour-time.Nanosecond)), + candidate(3, 98, now.Add(time.Nanosecond)), + } + got, stats := AssembleWithStats(input, now) + if len(got) != 2 { + t.Fatalf("length=%d want=2", len(got)) + } + assertUnique(t, got) + if stats.TotalCount != 2 || stats.NewCount != 1 || stats.HighCount != 1 { + t.Fatalf("stats=%+v want total=2 new=1 high=1", stats) + } + futureID := input[2].ID.Hex() + for _, id := range got { + if id == futureID { + t.Fatalf("future reviewAt video %s must not enter recommendation pools", futureID) + } + } +} + +func TestTakeRejectsNonPositiveSizeBeforeRedis(t *testing.T) { + for _, size := range []int{0, -1} { + result, err := Take(nil, 123, size) + if err != nil { + t.Fatalf("Take(size=%d) error = %v", size, err) + } + if result.Length != 0 || len(result.IDs) != 0 { + t.Fatalf("Take(size=%d) result = %+v, want empty", size, result) + } + } +} + +func TestTakeVersionContextUsesReusableScriptAndExpectedVersion(t *testing.T) { + ctx := context.WithValue(context.Background(), struct{}{}, "request") + client := &fakeScriptClient{ + result: []interface{}{"20260731", "4", "2", "video-3", "video-4"}, + } + result, err := takeContext(ctx, client, 123, 2, "20260731", "") + if err != nil { + t.Fatalf("TakeVersionContext() error = %v", err) + } + if client.calls != 1 || client.ctx != ctx { + t.Fatalf("script calls = %d context propagated = %v", client.calls, client.ctx == ctx) + } + if client.script != takeRedisScript || client.script.Hash() == "" { + t.Fatal("TakeVersionContext() did not reuse the cached Redis script") + } + if got := client.args[5]; got != "20260731" { + t.Fatalf("expected version arg = %v, want 20260731", got) + } + if result.Version != "20260731" || result.Length != 4 || result.Offset != 2 || + !reflect.DeepEqual(result.IDs, []string{"video-3", "video-4"}) { + t.Fatalf("TakeVersionContext() result = %+v", result) + } +} + +func TestTakeVersionContextReportsVersionChangeWithoutIDs(t *testing.T) { + client := &fakeScriptClient{ + result: []interface{}{"20260801", "-1", "0"}, + } + result, err := takeContext( + context.Background(), client, 123, 20, "20260731", "", + ) + if !errors.Is(err, ErrVersionChanged) { + t.Fatalf("TakeVersionContext() error = %v, want ErrVersionChanged", err) + } + if result.Version != "20260801" || len(result.IDs) != 0 { + t.Fatalf("TakeVersionContext() result = %+v", result) + } +} + +func TestTakeVersionContextReportsUnhealthyQueue(t *testing.T) { + client := &fakeScriptClient{ + result: []interface{}{"20260731", "-2", "0"}, + } + _, err := takeContext(context.Background(), client, 123, 20, "20260731", "") + if !errors.Is(err, ErrQueueUnhealthy) { + t.Fatalf("TakeVersionContext() error = %v, want ErrQueueUnhealthy", err) + } +} + +func TestTakeContextStopsBeforeRedisWhenCancelled(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + client := &fakeScriptClient{} + _, err := takeContext(ctx, client, 123, 20, "20260731", "") + if !errors.Is(err, context.Canceled) { + t.Fatalf("TakeVersionContext() error = %v, want context.Canceled", err) + } + if client.calls != 0 { + t.Fatalf("script calls = %d, want 0", client.calls) + } +} + +func TestTakeIdempotentContextBuildsStableScopedCacheKey(t *testing.T) { + client := &fakeScriptClient{ + result: []interface{}{"20260731", "4", "0", "video-1", "video-2"}, + } + _, err := takeIdempotentContext( + context.Background(), client, 123, 2, "20260731", " request-1 ", 3, + ) + if err != nil { + t.Fatalf("takeIdempotentContext() error = %v", err) + } + idempotencyKey, ok := client.args[6].(string) + if !ok || !strings.HasPrefix( + idempotencyKey, + "recommend:short:take-cache:123:", + ) || !strings.HasSuffix(idempotencyKey, ":3") { + t.Fatalf("idempotency key = %#v", client.args[6]) + } + if strings.Contains(idempotencyKey, "request-1") { + t.Fatal("raw request ID must not be copied into Redis keys") + } + if client.args[7] != int64(takeIdempotencyTTL/time.Second) { + t.Fatalf("idempotency ttl = %v", client.args[7]) + } +} + +func TestTakeIdempotentContextAllowsFirstBatchWithoutExpectedVersion(t *testing.T) { + client := &fakeScriptClient{ + result: []interface{}{"20260731", "4", "0", "video-1"}, + } + result, err := takeIdempotentContext( + context.Background(), client, 123, 1, "", "request-1", 0, + ) + if err != nil { + t.Fatalf("takeIdempotentContext() error = %v", err) + } + if result.Version != "20260731" || client.args[5] != "" { + t.Fatalf("result = %+v expected version arg = %#v", result, client.args[5]) + } +} + +func TestTakeCurrentIdempotencyCacheIsScopedByLuaCurrentVersion(t *testing.T) { + if !strings.Contains( + takeScriptSource, + "idempotencyKey = ARGV[7] .. ':' .. version", + ) { + t.Fatal("take script must scope its idempotency cache by the atomic current version") + } + first := &fakeScriptClient{ + result: []interface{}{"20260731", "4", "0", "video-1"}, + } + second := &fakeScriptClient{ + result: []interface{}{"20260801", "4", "0", "video-2"}, + } + if _, err := takeIdempotentContext( + context.Background(), first, 123, 1, "", "request-1", 0, + ); err != nil { + t.Fatalf("first take error = %v", err) + } + if _, err := takeIdempotentContext( + context.Background(), second, 123, 1, "", "request-1", 0, + ); err != nil { + t.Fatalf("second take error = %v", err) + } + if first.args[6] != second.args[6] { + t.Fatalf("cache base differs: first=%v second=%v", first.args[6], second.args[6]) + } + // Lua appends the version it read atomically, yielding different physical + // keys ":20260731" and ":20260801". +} + +func TestTakeIdempotencyCacheStoresReservationInsteadOfVideoIDs(t *testing.T) { + for _, want := range []string{ + "local function readBatch", + "local cachedWanted = tonumber(cached[4])", + "version, tostring(length), tostring(offset), tostring(wanted)", + } { + if !strings.Contains(takeScriptSource, want) { + t.Fatalf("take script missing compact reservation logic %q", want) + } + } + if strings.Contains(takeScriptSource, "unpack(result)") { + t.Fatal("take script must not duplicate all video IDs in idempotency cache") + } +} + +func TestTakeIdempotentContextValidatesScope(t *testing.T) { + longRequestID := strings.Repeat("x", maxTakeRequestIDLength+1) + tests := []struct { + name string + expectedVersion string + requestID string + batchIndex int + }{ + {name: "blank request", expectedVersion: "20260731", requestID: " ", batchIndex: 0}, + {name: "long request", expectedVersion: "20260731", requestID: longRequestID, batchIndex: 0}, + {name: "negative batch", expectedVersion: "20260731", requestID: "request", batchIndex: -1}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := &fakeScriptClient{} + _, err := takeIdempotentContext( + context.Background(), client, 1, 20, + tt.expectedVersion, tt.requestID, tt.batchIndex, + ) + if err == nil { + t.Fatal("takeIdempotentContext() should reject invalid scope") + } + if client.calls != 0 { + t.Fatalf("script calls = %d, want 0", client.calls) + } + }) + } +} + +func TestTakeScriptUsesFixedAbsoluteOffsetExpiration(t *testing.T) { + if !strings.Contains(takeScriptSource, "redis.call('PEXPIREAT', offsetKey, expiresAtMs)") { + t.Fatal("take script must align offset to the queue absolute expiration") + } + if !strings.Contains(takeScriptSource, "if offsetKeyTTL <= 0 then") { + t.Fatal("take script must set offset expiration only when the hash has no TTL") + } + if strings.Contains(takeScriptSource, "redis.call('EXPIRE', offsetKey") { + t.Fatal("take script must not renew a sliding offset TTL") + } +} + +func TestReserveContextUsesStableReceiptAndRandomLeaseWithoutAdvancingOffset(t *testing.T) { + newClient := func() *fakeScriptClient { + return &fakeScriptClient{ + run: func( + f *fakeScriptClient, + _ context.Context, + _ *redis.Script, + _ []string, + args ...interface{}, + ) (interface{}, error) { + token := fmt.Sprint(args[8]) + return []interface{}{ + "20260731-build", "RESERVED", "4", "3", "2", token, + "video-4", "video-1", + }, nil + }, + } + } + firstClient, secondClient := newClient(), newClient() + first, err := reserveContext( + context.Background(), firstClient, 123, 2, "", "request-1", + ) + if err != nil { + t.Fatalf("first reserve error = %v", err) + } + second, err := reserveContext( + context.Background(), secondClient, 123, 2, "", "request-1", + ) + if err != nil { + t.Fatalf("second reserve error = %v", err) + } + if first.Version != "20260731-build" || first.Offset != 3 || + first.Reserved != 2 || + !reflect.DeepEqual(first.IDs, []string{"video-4", "video-1"}) { + t.Fatalf("first reservation = %+v", first) + } + if first.ReceiptID == "" || first.ReceiptID != second.ReceiptID { + t.Fatalf("receipt IDs first=%q second=%q", first.ReceiptID, second.ReceiptID) + } + if first.LeaseToken == "" || first.LeaseToken == second.LeaseToken { + t.Fatalf("lease tokens first=%q second=%q", first.LeaseToken, second.LeaseToken) + } + if firstClient.script != reserveRedisScript || + firstClient.args[10] != int64(reservationTTL/time.Millisecond) { + t.Fatal("reserve did not use the reusable script and bounded lease TTL") + } + if strings.Contains(reserveScriptSource, "HSET', offsetKey") { + t.Fatal("Reserve must never advance or create the user offset hash") + } + for _, want := range []string{ + "'HMGET', leaseKey", + "redis.call('PEXPIRE', leaseKey", + "'COMMITTED'", + "'BUSY'", + } { + if !strings.Contains(reserveScriptSource, want) { + t.Fatalf("reserve script missing %q", want) + } + } +} + +func TestReserveContextParsesCommittedReceiptAndErrors(t *testing.T) { + committedClient := &fakeScriptClient{ + result: []interface{}{ + "20260731-old", "COMMITTED", "4", "1", "2", "lease", + "video-2", "video-3", + }, + } + committed, err := reserveContext( + context.Background(), committedClient, 123, 2, "", "request", + ) + if err != nil { + t.Fatalf("committed reserve error = %v", err) + } + if !committed.AlreadyCommitted || committed.Reserved != 2 { + t.Fatalf("committed reservation = %+v", committed) + } + + tests := []struct { + status string + want error + }{ + {status: "VERSION_CHANGED", want: ErrVersionChanged}, + {status: "UNHEALTHY", want: ErrQueueUnhealthy}, + {status: "BUSY", want: ErrReservationBusy}, + } + for _, tt := range tests { + t.Run(tt.status, func(t *testing.T) { + client := &fakeScriptClient{ + result: []interface{}{"20260731", tt.status, "0", "0", "0", ""}, + } + result, err := reserveContext( + context.Background(), client, 1, 20, "", "request", + ) + if !errors.Is(err, tt.want) || result.Version != "20260731" { + t.Fatalf("result=%+v error=%v want=%v", result, err, tt.want) + } + }) + } +} + +func TestReserveContextRetriesUncertainResponseWithSameLease(t *testing.T) { + wantErr := errors.New("connection reset after write") + client := &fakeScriptClient{} + var firstArgs []interface{} + client.run = func( + f *fakeScriptClient, + _ context.Context, + _ *redis.Script, + _ []string, + args ...interface{}, + ) (interface{}, error) { + if f.calls == 1 { + firstArgs = append([]interface{}(nil), args...) + return nil, wantErr + } + if !reflect.DeepEqual(args, firstArgs) { + t.Fatalf("retry args changed:\nfirst=%#v\nretry=%#v", firstArgs, args) + } + return []interface{}{ + "20260731-build", "RESERVED", "2", "0", "2", + fmt.Sprint(args[8]), "video-1", "video-2", + }, nil + } + + reservation, err := reserveContext( + context.Background(), client, 123, 2, "", "request", + ) + if err != nil { + t.Fatalf("reserveContext() error = %v", err) + } + if client.calls != 2 || reservation.LeaseToken != fmt.Sprint(firstArgs[8]) || + reservation.ReceiptID != fmt.Sprint(firstArgs[9]) { + t.Fatalf("calls=%d reservation=%+v args=%#v", client.calls, reservation, firstArgs) + } +} + +func TestCommitReservationContextUsesCASReceiptAndAbsoluteTTL(t *testing.T) { + reservation := Reservation{ + Version: "20260731-build", + Length: 10, + Offset: 8, + Reserved: 5, + LeaseToken: "lease", + ReceiptID: "receipt", + } + for _, raw := range []interface{}{int64(1), int64(2)} { + client := &fakeScriptClient{result: raw} + if err := commitReservationContext( + context.Background(), client, 123, reservation, 3, + ); err != nil { + t.Fatalf("commit result %v error = %v", raw, err) + } + if client.script != commitReservationRedisScript { + t.Fatal("commit did not reuse cached Redis script") + } + if client.args[11] != 3 || + client.args[12] != int64(takeIdempotencyTTL/time.Second) || + client.args[13] != "recommend:short:commit-receipt:" { + t.Fatalf("commit args = %#v", client.args) + } + } + for _, want := range []string{ + "currentOffset ~= tonumber(lease[4])", + "(currentOffset + consumed) % length", + "redis.call('PEXPIREAT', offsetKey, expiresAtMs)", + "'RPUSH', receiptKey", + } { + if !strings.Contains(commitReservationScriptSource, want) { + t.Fatalf("commit script missing %q", want) + } + } + if strings.Index(commitReservationScriptSource, "local receipt =") > + strings.Index(commitReservationScriptSource, "local version =") { + t.Fatal("commit must check its receipt before current for uncertain-response retries") + } +} + +func TestCommitReservationContextMapsRejectedStates(t *testing.T) { + reservation := Reservation{ + Version: "20260731-build", + Length: 10, + Offset: 0, + Reserved: 5, + LeaseToken: "lease", + ReceiptID: "receipt", + } + tests := []struct { + raw int64 + want error + }{ + {raw: -1, want: ErrVersionChanged}, + {raw: -2, want: ErrQueueUnhealthy}, + {raw: -3, want: ErrReservationExpired}, + {raw: -4, want: ErrReservationConflict}, + {raw: -5, want: ErrReservationConflict}, + } + for _, tt := range tests { + client := &fakeScriptClient{result: tt.raw} + err := commitReservationContext( + context.Background(), client, 123, reservation, 3, + ) + if !errors.Is(err, tt.want) { + t.Fatalf("raw=%d error=%v want=%v", tt.raw, err, tt.want) + } + } +} + +func TestAbortReservationContextIsTokenScoped(t *testing.T) { + client := &fakeScriptClient{result: int64(1)} + reservation := Reservation{ + Version: "20260731-build", + LeaseToken: "lease", + ReceiptID: "receipt", + } + if err := abortReservationContext( + context.Background(), client, 123, reservation, + ); err != nil { + t.Fatal(err) + } + if client.script != abortReservationRedisScript || + !reflect.DeepEqual(client.args, []interface{}{ + "recommend:short:reservation:", + "20260731-build", + "123", + "lease", + "receipt", + }) { + t.Fatalf("abort script=%p args=%#v", client.script, client.args) + } + if !strings.Contains(abortReservationScriptSource, "lease[1] ~= ARGV[4]") || + !strings.Contains(abortReservationScriptSource, "lease[2] ~= ARGV[5]") { + t.Fatal("abort must not delete a newer lease after TTL takeover") + } +} + +func TestReservationAPIsRejectInvalidInputsBeforeRedis(t *testing.T) { + client := &fakeScriptClient{} + if _, err := reserveContext( + context.Background(), client, 0, 20, "", "", + ); err == nil { + t.Fatal("reserve should reject uid 0") + } + invalid := Reservation{ + Version: "20260731", + Length: 10, + Offset: 0, + Reserved: 2, + LeaseToken: "lease", + ReceiptID: "receipt", + } + if err := commitReservationContext( + context.Background(), client, 1, invalid, 3, + ); err == nil { + t.Fatal("commit should reject consumed > reserved") + } + if client.calls != 0 { + t.Fatalf("Redis calls = %d, want 0", client.calls) + } +} + +func TestHealthContextParsesAtomicHealthResult(t *testing.T) { + client := &fakeScriptClient{ + result: []interface{}{"OK", "20260731", "154290", "154290", "7200000", "7199999"}, + } + health, err := healthContext(context.Background(), client, "20260731") + if err != nil { + t.Fatalf("HealthContext() error = %v", err) + } + if !health.Healthy || health.Status != "OK" || health.Version != "20260731" || + health.QueueLength != 154290 || health.MetadataLength != 154290 || + health.QueueTTL != 2*time.Hour || + health.MetadataTTL != 2*time.Hour-time.Millisecond { + t.Fatalf("HealthContext() = %+v", health) + } + if client.script != healthRedisScript || client.args[0] != "20260731" { + t.Fatal("HealthContext() did not use the expected cached script/version") + } +} + +func TestHealthContextReturnsUnhealthyStatusWithoutRedisError(t *testing.T) { + client := &fakeScriptClient{ + result: []interface{}{"LENGTH_MISMATCH", "20260731", "20", "19", "1000", "1000"}, + } + health, err := healthContext(context.Background(), client, "20260731") + if err != nil { + t.Fatalf("HealthContext() error = %v", err) + } + if health.Healthy || health.Status != "LENGTH_MISMATCH" { + t.Fatalf("HealthContext() = %+v", health) + } +} + +func TestTakeAndHealthRejectMalformedScriptResults(t *testing.T) { + takeClient := &fakeScriptClient{ + result: []interface{}{"20260731", "not-a-length", "0"}, + } + if _, err := takeContext( + context.Background(), takeClient, 1, 20, "", "", + ); err == nil { + t.Fatal("takeContext() should reject malformed length") + } + healthClient := &fakeScriptClient{ + result: []interface{}{"OK", "20260731", "20", "20", "not-a-ttl", "1000"}, + } + if _, err := healthContext(context.Background(), healthClient, "20260731"); err == nil { + t.Fatal("HealthContext() should reject malformed TTL") + } +} + +func TestSetKeyTTLHoursValidatesAndPublishesConfiguredTTL(t *testing.T) { + originalHours := int(KeyTTL() / time.Hour) + defer func() { + if err := SetKeyTTLHours(originalHours); err != nil { + t.Fatalf("restore key TTL: %v", err) + } + }() + for _, hours := range []int{minKeyTTLHours - 1, maxKeyTTLHours + 1} { + if err := SetKeyTTLHours(hours); err == nil { + t.Fatalf("SetKeyTTLHours(%d) should fail", hours) + } + } + const configuredHours = 96 + if err := SetKeyTTLHours(configuredHours); err != nil { + t.Fatalf("SetKeyTTLHours() error = %v", err) + } + client := &fakePublishClient{} + if err := publish( + context.Background(), client, publishTestMeta(1), []string{"video-1"}, "lock", "token", + ); err != nil { + t.Fatalf("publish() error = %v", err) + } + if client.publishedTTL != int64(96*time.Hour/time.Second) { + t.Fatalf("published ttl = %d, want %d", client.publishedTTL, int64(96*time.Hour/time.Second)) + } +} + +func TestPublishRejectsVersionMismatchBeforeRedis(t *testing.T) { + meta := QueueMeta{ + Version: "20260730", + GeneratedAt: time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC), + } + err := Publish(context.Background(), nil, meta, []string{"video-id"}, "lock", "token") + if err == nil { + t.Fatal("Publish() should reject a version that does not match generatedAt") + } +} + +func TestVersionWithRevisionKeepsDateAndSeparatesSameDayBuilds(t *testing.T) { + generatedAt := time.Date(2026, 7, 31, 2, 0, 0, 0, time.UTC) + first := VersionWithRevision(generatedAt, "build-a") + second := VersionWithRevision(generatedAt, "build-b") + if first != "20260731-build-a" || second != "20260731-build-b" { + t.Fatalf("versions = %q, %q", first, second) + } + if first == second { + t.Fatal("same-day builds must use different queue versions") + } + for _, version := range []string{"20260731", first, second} { + if !versionMatchesDate(version, "20260731") { + t.Fatalf("version %q should belong to date 20260731", version) + } + } + for _, version := range []string{ + "20260730-build-a", + "202607310-build-a", + "20260731-", + } { + if versionMatchesDate(version, "20260731") { + t.Fatalf("version %q must not belong to date 20260731", version) + } + } +} + +func TestPublishAcceptsSameDayRevisionVersion(t *testing.T) { + meta := publishTestMeta(1) + meta.Version = VersionWithRevision(meta.GeneratedAt, "build-a") + client := &fakePublishClient{} + + if err := publish( + context.Background(), client, meta, []string{"video-1"}, "lock", "token", + ); err != nil { + t.Fatalf("publish() error = %v", err) + } + if !client.swapped || client.swapArgs[0] != meta.Version { + t.Fatalf("same-day revision version was not published: %#v", client.swapArgs) + } +} + +func TestHealthScriptAcceptsPureDateAndSameDayRevisionOnly(t *testing.T) { + for _, want := range []string{ + "version == expectedDate", + "string.len(version) > string.len(expectedDate) + 1", + "expectedDate .. '-'", + } { + if !strings.Contains(healthScriptSource, want) { + t.Fatalf("health script missing date-family check %q", want) + } + } +} + +func TestPublishUsesBoundedPipelineAndPreservesOrder(t *testing.T) { + total := queuePushBatchSize + queuePushBatchSize*queuePipelineBatchCount + 57 + ids := make([]string, total) + for i := range ids { + ids[i] = fmt.Sprintf("video-%06d", i) + } + meta := publishTestMeta(total) + client := &fakePublishClient{} + + if err := publish(context.Background(), client, meta, ids, "lock", "token"); err != nil { + t.Fatalf("publish() error = %v", err) + } + if !client.swapped || client.swapAttempts != 1 { + t.Fatalf("swap state = %v, attempts = %d, want successful single swap", client.swapped, client.swapAttempts) + } + if !reflect.DeepEqual(client.queue, ids) { + t.Fatal("published queue order differs from input") + } + wantWindows := []int{queuePushBatchSize * queuePipelineBatchCount, 57} + if !reflect.DeepEqual(client.pipelineWindowSizes, wantWindows) { + t.Fatalf("pipeline windows = %v, want %v", client.pipelineWindowSizes, wantWindows) + } + for _, batchSize := range client.pipelineBatchSizes { + if batchSize != queuePushBatchSize { + t.Fatalf("pipeline batch size = %d, want %d", batchSize, queuePushBatchSize) + } + } + if len(client.pipelineExpirations) != len(wantWindows) { + t.Fatalf("pipeline expirations = %v, want one per window", client.pipelineExpirations) + } + for _, expiration := range client.pipelineExpirations { + if expiration != buildingKeyTTL { + t.Fatalf("pipeline expiration = %v, want %v", expiration, buildingKeyTTL) + } + } + if client.firstPushTTL != int64(buildingKeyTTL/time.Second) { + t.Fatalf("building ttl = %d, want %d", client.firstPushTTL, int64(buildingKeyTTL/time.Second)) + } + if client.metaBuildingTTL != int64(buildingKeyTTL/time.Second) { + t.Fatalf("meta building ttl = %d, want %d", client.metaBuildingTTL, int64(buildingKeyTTL/time.Second)) + } + if client.publishedTTL != int64(KeyTTL()/time.Second) { + t.Fatalf("published ttl = %d, want %d", client.publishedTTL, int64(KeyTTL()/time.Second)) + } + if client.metaLength != total { + t.Fatalf("meta length = %d, want %d", client.metaLength, total) + } + if !strings.Contains(client.swapScript, "redis.call('UNLINK'") { + t.Fatal("swap script must asynchronously remove current and previous offset hashes") + } + if !strings.Contains(client.swapScript, "redis.call('PEXPIREAT'") { + t.Fatal("swap script must set one absolute expiration for queue and metadata") + } + if len(client.swapArgs) != 6 || client.swapArgs[5] != "recommend:short:offset:" { + t.Fatalf("swap args = %#v, want previous offset prefix", client.swapArgs) + } +} + +func TestPublishPipelineFailureCleansBuildingQueueWithoutSwap(t *testing.T) { + ids := make([]string, queuePushBatchSize+101) + for i := range ids { + ids[i] = fmt.Sprintf("video-%d", i) + } + client := &fakePublishClient{pipelineErrorAt: 1} + + err := publish(context.Background(), client, publishTestMeta(len(ids)), ids, "lock", "token") + if err == nil || !strings.Contains(err.Error(), "injected pipeline error") { + t.Fatalf("publish() error = %v, want injected pipeline error", err) + } + if client.swapped || client.swapAttempts != 0 { + t.Fatalf("swap state = %v, attempts = %d, want no swap", client.swapped, client.swapAttempts) + } + if client.deleteCalls == 0 || client.queueLengthBeforeDelete <= queuePushBatchSize { + t.Fatalf( + "cleanup calls = %d, pre-delete length = %d, want partial queue cleanup", + client.deleteCalls, client.queueLengthBeforeDelete, + ) + } +} + +func TestPublishRejectsPipelineDuplicateBeforeSwap(t *testing.T) { + ids := make([]string, queuePushBatchSize+10) + for i := range ids { + ids[i] = fmt.Sprintf("video-%d", i) + } + client := &fakePublishClient{pipelineDuplicateAt: 1} + + err := publish(context.Background(), client, publishTestMeta(len(ids)), ids, "lock", "token") + if err == nil || !strings.Contains(err.Error(), "queue length mismatch after pipeline") { + t.Fatalf("publish() error = %v, want pipeline length mismatch", err) + } + if client.swapped || client.swapAttempts != 0 { + t.Fatalf("swap state = %v, attempts = %d, want no swap", client.swapped, client.swapAttempts) + } + if client.deleteCalls == 0 { + t.Fatal("duplicate pipeline queue was not cleaned") + } +} + +func TestPublishFinalLengthGateRejectsFirstPushDuplicate(t *testing.T) { + ids := []string{"video-1", "video-2"} + client := &fakePublishClient{duplicateFirstPush: true} + + err := publish(context.Background(), client, publishTestMeta(len(ids)), ids, "lock", "token") + if err == nil || !strings.Contains(err.Error(), "queue length mismatch before queue swap") { + t.Fatalf("publish() error = %v, want final queue length mismatch", err) + } + if client.swapped || client.swapAttempts != 1 { + t.Fatalf("swap state = %v, attempts = %d, want rejected swap", client.swapped, client.swapAttempts) + } + if client.deleteCalls == 0 { + t.Fatal("invalid first push queue was not cleaned") + } +} + +func TestPublishCancellationStopsBeforeNextPipelineWindow(t *testing.T) { + total := queuePushBatchSize + queuePushBatchSize*queuePipelineBatchCount + 1 + ids := make([]string, total) + for i := range ids { + ids[i] = fmt.Sprintf("video-%d", i) + } + ctx, cancel := context.WithCancel(context.Background()) + client := &fakePublishClient{cancelAfterPipelineAt: 1, cancel: cancel} + + err := publish(ctx, client, publishTestMeta(len(ids)), ids, "lock", "token") + if !errors.Is(err, context.Canceled) { + t.Fatalf("publish() error = %v, want context.Canceled", err) + } + if client.pipelineCalls != 1 { + t.Fatalf("pipeline calls = %d, want 1", client.pipelineCalls) + } + if client.swapped || client.swapAttempts != 0 { + t.Fatalf("swap state = %v, attempts = %d, want no swap", client.swapped, client.swapAttempts) + } + if client.deleteCalls == 0 { + t.Fatal("cancelled building queue was not cleaned") + } +} + +func TestPublishRejectsMetadataLengthMismatchBeforeRedis(t *testing.T) { + meta := publishTestMeta(2) + meta.Length = 1 + + err := publish(context.Background(), nil, meta, []string{"video-1", "video-2"}, "lock", "token") + if err == nil || !strings.Contains(err.Error(), "metadata length") { + t.Fatalf("publish() error = %v, want metadata length mismatch", err) + } +} + +func publishTestMeta(length int) QueueMeta { + generatedAt := time.Date(2026, 7, 31, 2, 0, 0, 0, time.UTC) + return QueueMeta{ + Version: VersionAt(generatedAt), + Length: length, + GeneratedAt: generatedAt, + StartOffset: 0, + } +} + +func TestVersionValidUntilUsesCSTDayBoundary(t *testing.T) { + meta := QueueMeta{ + Version: "20260731", + GeneratedAt: time.Date(2026, 7, 31, 15, 59, 59, 0, time.UTC), + } + want := time.Date(2026, 7, 31, 16, 0, 0, 0, time.UTC) + if got := versionValidUntil(meta); !got.Equal(want) { + t.Fatalf("versionValidUntil() = %s, want %s", got, want) + } +} + +func TestAssembleConcurrentDeterministic(t *testing.T) { + now := time.Date(2026, 7, 24, 8, 0, 0, 0, time.UTC) + input := make([]vidmod.RecommendCandidate, 0, 100) + for i := 0; i < 100; i++ { + reviewAt := now.Add(-48 * time.Hour) + if i >= 90 { + reviewAt = now.Add(-time.Duration(i-90) * time.Hour) + } + input = append(input, candidate(i, int64(1000-i), reviewAt)) + } + want := fmt.Sprint(Assemble(input, now)) + var wg sync.WaitGroup + errs := make(chan string, 64) + for i := 0; i < 64; i++ { + wg.Add(1) + go func() { + defer wg.Done() + if got := fmt.Sprint(Assemble(input, now)); got != want { + errs <- got + } + }() + } + wg.Wait() + close(errs) + if got, ok := <-errs; ok { + t.Fatalf("concurrent result differs: %s", got) + } +} + +func assertUnique(t *testing.T, ids []string) { + t.Helper() + seen := map[string]bool{} + for _, id := range ids { + if seen[id] { + t.Fatalf("duplicate id %s", id) + } + seen[id] = true + } +} diff --git a/common/slice/sli.go b/common/slice/sli.go new file mode 100644 index 0000000..4010a55 --- /dev/null +++ b/common/slice/sli.go @@ -0,0 +1,297 @@ +package sli + +import ( + "fmt" + "math/rand" + "reflect" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 将任意切片类型转换为 []interface{} +func CreateAnyTypeSlice(slice interface{}) []interface{} { + //判断是否是切片类型 + v := reflect.ValueOf(slice) + if v.Kind() != reflect.Slice { + return nil + } + sliceLen := v.Len() + out := make([]interface{}, sliceLen) + for i := 0; i < sliceLen; i++ { + out[i] = v.Index(i).Interface() + } + return out +} + +// @brief: 删除任意切片类型指定下标的元素 +func Delete(slice interface{}, index int) (interface{}, bool) { + //判断是否是切片类型 + v := reflect.ValueOf(slice) + if v.Kind() != reflect.Slice { + return nil, false + } + //参数检查 + if v.Len() == 0 || index < 0 || index > v.Len()-1 { + return nil, false + } + return reflect.AppendSlice(v.Slice(0, index), v.Slice(index+1, v.Len())).Interface(), true +} + +// @brief: 删除任意切片类型指定范围下标的元素 +func DeleteIndexRange(slice interface{}, start, end int) (interface{}, bool) { + //判断是否是切片类型 + v := reflect.ValueOf(slice) + if v.Kind() != reflect.Slice { + return nil, false + } + //参数检查 + if v.Len() == 0 || start < 0 || end > v.Len()-1 { + return nil, false + } + if start == 0 && end == 0 { + return nil, true + } + return reflect.AppendSlice(v.Slice(0, start-1), v.Slice(end+1, v.Len())).Interface(), true +} + +// @brief: 删除任意切片类型指定的元素 , 0 表示删除匹配到的第一个元素,1 表示删除匹配到的所有元素 +func DeleteSpecifyEle(slice interface{}, spec interface{}, ct ...int) interface{} { + //判断是否是切片类型 + isAll := false + v := reflect.ValueOf(slice) + if v.Kind() != reflect.Slice { + return slice + } + //参数检查 + if v.Len() == 0 { + return slice + } + if len(ct) == 0 { + isAll = false + } else if ct[0] == 0 { + isAll = false + } else if ct[0] == 1 { + isAll = true + } + var handleSlice interface{} + handleSlice = slice + for i := 0; i < v.Len(); i++ { + fmt.Println(reflect.ValueOf(v.Index(i).Interface()), reflect.ValueOf(spec)) + if reflect.ValueOf(v.Index(i).Interface()) == reflect.ValueOf(spec) { + out, suc := Delete(handleSlice, i) + handleSlice = out + if suc { + if !isAll { + return out + } + } + } + } + return handleSlice +} + +// 给任意切片类型插入元素 +func Insert(slice interface{}, index int, value interface{}) (interface{}, bool) { + //判断是否是切片类型 + v := reflect.ValueOf(slice) + if v.Kind() != reflect.Slice { + return nil, false + } + //参数检查 + if index < 0 || index > v.Len() || reflect.TypeOf(slice).Elem() != reflect.TypeOf(value) { + return nil, false + } + //尾部追加元素 + if index == v.Len() { + return reflect.Append(v, reflect.ValueOf(value)).Interface(), true + } + v = reflect.AppendSlice(v.Slice(0, index+1), v.Slice(index, v.Len())) + v.Index(index).Set(reflect.ValueOf(value)) + return v.Interface(), true +} + +// 给任意字符串类型数组插入元素 +func InsertStringSlice(slice []string, index int, value ...string) []string { + var newStr []string + if index == 0 { + return append(append(newStr, value...), slice...) + } + if index == len(slice)-1 { + return append(append(newStr, slice...), value...) + } + return append(append(append(newStr, slice[0:index]...), value...), slice[index:]...) +} + +// 修改任意切片类型指定下标的元素 +func Update(slice interface{}, index int, value interface{}) (interface{}, bool) { + //判断是否是切片类型 + v := reflect.ValueOf(slice) + if v.Kind() != reflect.Slice { + return nil, false + } + //参数检查 + if index > v.Len()-1 || reflect.TypeOf(slice).Elem() != reflect.TypeOf(value) { + return nil, false + } + v.Index(index).Set(reflect.ValueOf(value)) + return v.Interface(), true +} + +// 查找指定元素在任意切片类型中的所有下标 +func Search(slice interface{}, value interface{}) ([]int, bool) { + //判断是否是切片类型 + v := reflect.ValueOf(slice) + if v.Kind() != reflect.Slice { + return nil, false + } + index := make([]int, 0, v.Len()) + for i := 0; i < v.Len(); i++ { + if v.Index(i).Interface() == reflect.ValueOf(value).Interface() { + index = append(index, i) + } + } + return index, true +} + +// 元素去重 +func RemoveRep[T comparable](slc []T) []T { + if len(slc) < 1024 { + // 切片长度小于1024的时候,循环来过滤 + return RemoveRepByLoop(slc) + } + // 大于的时候,通过map来过滤 + return RemoveRepByMap(slc) +} + +// 通过map主键唯一的特性过滤重复元素 +func RemoveRepByMap[T comparable](slc []T) []T { + result := []T{} + tempMap := map[T]byte{} // 存放不重复主键 + for _, e := range slc { + l := len(tempMap) + tempMap[e] = 0 + if len(tempMap) != l { // 加入map后,map长度变化,则元素不重复 + result = append(result, e) + } + } + return result +} + +// 通过map主键唯一的特性过滤重复元素: uint64 +func RemoveRepUintByMap(slc []uint64) []uint64 { + result := []uint64{} + tempMap := map[uint64]byte{} // 存放不重复主键 + for _, e := range slc { + l := len(tempMap) + tempMap[e] = 0 + if len(tempMap) != l { // 加入map后,map长度变化,则元素不重复 + result = append(result, e) + } + } + return result +} + +// 通过两重循环过滤重复元素 +func RemoveRepByLoop[T comparable](slc []T) []T { + result := make([]T, 0, len(slc)) // 存放结果 + for i := range slc { + flag := true + for j := range result { + if slc[i] == result[j] { + flag = false // 存在重复元素,标识为false + break + } + } + if flag { // 标识为false,不添加进结果 + result = append(result, slc[i]) + } + } + return result +} + +// RemoveRepObjectID 过滤重复 mongo ID +func RemoveRepObjectID(slc []primitive.ObjectID) []primitive.ObjectID { + res := []primitive.ObjectID{} + m := make(map[primitive.ObjectID]struct{}) + for _, v := range slc { + if _, ok := m[v]; !ok { + m[v] = struct{}{} + res = append(res, v) + } + } + return res +} + +// objID切片是否存在item +func PrimitiveObjIDIsExist(slc []primitive.ObjectID, item primitive.ObjectID) bool { + flag := false + for i := range slc { + if slc[i] == item { + flag = true + break + } + } + return flag +} + +// 是否包含某一个元素 +func Contains[T comparable](slc []T, item T) bool { + flag := false + for i := range slc { + if slc[i] == item { + flag = true + break + } + } + return flag +} + +func ContainsUint(slc []uint64, item uint64) bool { + flag := false + for i := range slc { + if slc[i] == item { + flag = true + break + } + } + return flag +} + +func ContainsInt(slc []int, item int) bool { + flag := false + for i := range slc { + if slc[i] == item { + flag = true + break + } + } + return flag +} + +func Shuffle(slice []interface{}) { + for len(slice) > 0 { + n := len(slice) + randIndex := rand.Intn(n) + slice[n-1], slice[randIndex] = slice[randIndex], slice[n-1] + slice = slice[:n-1] + } +} + +func ShuffleString(slice []string) { + for len(slice) > 0 { + n := len(slice) + randIndex := rand.Intn(n) + slice[n-1], slice[randIndex] = slice[randIndex], slice[n-1] + slice = slice[:n-1] + } +} + +// InSlice 判断元素是否存在于数组中 +func InSlice[T any | primitive.ObjectID](i T, s []T) bool { + for _, ii := range s { + if reflect.DeepEqual(i, ii) { + return true + } + } + return false +} diff --git a/common/sms/fengniao/fengniao.go b/common/sms/fengniao/fengniao.go new file mode 100644 index 0000000..ad9a957 --- /dev/null +++ b/common/sms/fengniao/fengniao.go @@ -0,0 +1,74 @@ +package fengniao + +import ( + "crypto/sha1" + "errors" + "fmt" + "net/http" + + "91porn-server/common" + "91porn-server/common/httputil" + "91porn-server/common/log" +) + +type requestBody struct { + MercId string `json:"mercId"` + Secret string `json:"secret"` + TemplateId string `json:"templateId"` + SmsCode string `json:"smsCode"` + Mobile string `json:"mobile"` + SignType string `json:"signType"` // 固定sha1 + Sign string `json:"sign"` // 签名 +} + +type msg struct { + Status string `json:"status"` + TaskId string `json:"taskId"` +} + +type responseBody struct { + Code int `json:"code"` + Msg msg `json:"msg"` + Err string `json:"err"` +} + +type FengNiao struct { + Url string + MercId string + Secret string + Tmpl string +} + +// Send 发送, 目前蜂鸟貌似只支持数字 +func (f *FengNiao) Send(mobile string, code string) error { + data := fmt.Sprintf("mercId=%s&templateId=%s&smsCode=%s&mobile=%s&secret=%s", f.MercId, f.Tmpl, code, mobile, f.Secret) + sha := sha1.New() + if _, err := sha.Write([]byte(data)); err != nil { + log.Error("fengniao send error", log.Any("mobile", mobile), log.Any("templateId", f.Tmpl), log.E(err)) + return err + } + args, _ := common.JSONStruct2Map(&requestBody{ + MercId: f.MercId, + Secret: f.Secret, + TemplateId: f.Tmpl, + SmsCode: code, + SignType: "sha1", + Mobile: mobile, + Sign: fmt.Sprintf("%x", sha.Sum(nil)), + }) + rp := responseBody{} + statusCode, err := httputil.DefaultClientPostJsonWithResp(&rp, f.Url, nil, args) + if err != nil { + log.Error("fengniao POSTJsonWithResp err", log.E(err)) + return err + } + if statusCode != http.StatusOK { + log.Error("fengniao status code not 200") + return errors.New("fengniao statusCode not 200") + } + if rp.Code != 200 { + log.Error("fengniao code not 200", log.Any("errdesc", rp.Err)) + return errors.New(rp.Err) + } + return nil +} diff --git a/common/sms/sms.go b/common/sms/sms.go new file mode 100644 index 0000000..96fb7e7 --- /dev/null +++ b/common/sms/sms.go @@ -0,0 +1,61 @@ +package sms + +import ( + "strings" + + "91porn-server/common/sms/fengniao" + "91porn-server/common/ysphone" +) + +type Provider = int + +const ( + FengNiao Provider = 1 //蜂鸟 + YunPian Provider = 2 //云片 +) + +type FengNiaoConfig struct { + Url string `json:"url"` + ChinaMercId string `json:"chinaMercId"` + ChinaSecret string `json:"chinaSecret"` + ChinaTmplId string `json:"chinaTmplId"` + InterMercId string `json:"interMercId"` + InterSecret string `json:"interSecret"` + InterTmplId string `json:"interTmplId"` +} + +type Config struct { + FengNiao FengNiaoConfig `json:"fengNiao"` +} + +var cfg Config + +func Init(c Config) { + cfg = c +} + +func sendByFengNiao(mobile string, code string) error { + newMobile := ysphone.FormatPhoneNumber(mobile) + fn := fengniao.FengNiao{ + Url: cfg.FengNiao.Url, + MercId: cfg.FengNiao.InterMercId, + Secret: cfg.FengNiao.InterSecret, + Tmpl: cfg.FengNiao.InterTmplId, + } + if strings.HasPrefix(newMobile, "+86") { + fn.MercId = cfg.FengNiao.ChinaMercId + fn.Secret = cfg.FengNiao.ChinaSecret + fn.Tmpl = cfg.FengNiao.ChinaTmplId + } + return (&fn).Send(newMobile, code) +} + +// Send 发送短信 +func Send(p Provider, mobile string, code string) error { + switch p { + case FengNiao: + return sendByFengNiao(mobile, code) + default: + return sendByFengNiao(mobile, code) + } +} diff --git a/common/stderr/code.go b/common/stderr/code.go new file mode 100644 index 0000000..48b48c8 --- /dev/null +++ b/common/stderr/code.go @@ -0,0 +1,421 @@ +package stderr + +// 通用code +const ( + Success Code = 200 //Success 通用成功 + Failure Code = 400 //Failure 通用失败 + FunctionNotEnabled Code = 404 //FunctionNotEnabled 功能未开启 + FunctionUpgrade Code = 405 //FunctionUpgrade 功能维护升级 + FunctionUpgradeToLatest Code = 408 //FunctionUpgradeToLatest 功能已升级,请重新下载最新版本安装使用 + +) + +const ( + _ Code = -1 * (iota + 1) + ErrUnrecognized //ErrUnrecognized 未知错误 -1 + MustSliceOrSlicePtr //必须要是切片或切片指针 -2 + UserIsNotExists //用户不存在 -3 + SignFailedToVerify //签名校验不通过 -4 + UserIsException //用户异常 -5 + BadUA // UA 异常 + UserIsNotMerchant //用户不是商家 + UserActiveNotEnough // 用户活跃值不足 +) + +const ( + ErrDownloadError Code = -1 * (iota + 1000) //ErrDownloadError 下载文件失败 -1000 + ErrUploadError //ErrUploadError 上传文件失败 -1001 + ErrFsBucketNotExist //ErrFsBucketNotExist ErrFsBucketNotExist -1002 + UpLoadingFile //UpLoadingFile UpLoadingFile -1003 + ErrFsOptionWrong //ErrFsOptionWrong ErrFsOptionWrong -1004 + ErrExportFileFail //ErrExportFileFail ErrExportFileFail -1005 + FileMerging //FileMerging 文件合并中 -1006 + FileConverting //FileConverting 文件转换中 -1007 + UploadLoadingToFs //UploadLoadingToFs 文件正在上传到FS服务器 -1008 + ErrConnectToFs //ErrConnectToFs 连接文件服务器失败 -1009 + CodeEmptyData //CodeEmptyData 空数据 -1010 + ErrMimeType //ErrMimeType 不支持的文件格式 -1011 + ErrFsServerFile //ErrFsServerFile 文件服务异常 -1012 + ErrMergeFile //ErrMergeFile 文件合并失败 -1013 + ErrConvertFile //ErrConvertFile 文件转码失败 -1014 + ErrPlayTimeTooShort //ErrPlayTimeTooShort 上传的视频播放时间太短 -1015 + ErrUploadExistVideo //ErrUploadExistVideo 重复上传视频 -1016 + ForbidUploadVideo //ForbidUploadVideo 该用户已经禁止上传文件 -1017 + UpLoadFileComplete Code = 2000 //UpLoadFileComplete UpLoadFileComplete 2000 + ResolutionNotEnough Code = 2001 //分辨率不足 +) + +const ( + ErrAccessForbid Code = iota + 1000 //ErrAccessForbid 没有访问权限 1000 + ErrNamePwdIncorrect //ErrNamePwdIncorrect 用户名或密码错误 1001 + ErrAuthExpired //ErrAuthExpired 证书过期 1002 + ErrAuthInvalid //ErrAuthInvalid 无效签名 1003 + ErrUserBeBanned //ErrUserBeBanned 用户已经禁言 1004 + ErrCustomerBanned //ErrCustomerBanned 客服已休息 1005 + ErrVersionUpdate //ErrVersionUpdate 版本强制更新 1006 + ErrVersionRange //ErrVersionRange 版本更新范围错误 1007 + ErrVersionTooLow //ErrVersionTooLow 指定升级版本太低 +) + +const ( + ErrLoginExpired Code = iota + 4000 //ErrLoginExpired 登陆过期 4000 + ErrParamError //ErrParamError 参数参数错误 4001 + _ //此错误吗暂未使用 4002 + ErrReqForbidden //ErrReqForbidden 请求被拒绝 4003 + ErrPathNotFount //ErrPathNotFount 请求路径不存在 4004 + ErrMethodIncorrect //ErrMethodIncorrect 请求方法错误 4005 + ErrTimeout //ErrTimeout 服务超时 4006 + ErrLoginTooFrequently //ErrLoginTooFrequently 4007 + ErrNetWorkBusy //ErrNetWorkBusy 网络环境不好 4008 + ErrInvalidRequest //ErrInvalidRequest 无效请求 4009 + ErrInvalidRequestReplayAttack //ErrInvalidRequestReplayAttack 重放攻击 4010 + ErrInterServerError //ErrInterServerError 内部错误 + ErrTimeIsEmpty + ErrTimeWithinWeek +) + +const ( + ErrServerUnavailable Code = iota + 5000 //ErrServerUnavailable 服务不可用 5000 + ErrDbQueryError //ErrDbQueryError 数据库查询错误 5001 + ErrDbInsertError //ErrDbInsertError 数据库插入错误 5002 + ErrDbUpdateError //ErrDbUpdateError 数据库修改错误 5003 + ErrDbDeleteError //ErrDbDeleteError 数据库删除错误 5004 + ErrNoToken //ErrNoToken 没有token 5005 + ErrCaptcha //ErrCaptcha 5006 + ErrCaptchaExists //ErrCaptchaExists 5007 + ErrSmsSendFail //短信发送失败 5008 + InvalidToken //无效token 5009 + ErrDbTransError //ErrDbTransError 事务失败 5010 + ErrDbInputExist //ErrDbInputExist 数据库输入值已经存在 5011 + ErrLoginMobileNotExist //ErrLoginMobileNotExist 用手机登陆,手机号不存在 5012 + ErrMobileHasBind //ErrMobileHasBind 已经绑定了该手机号 5013 + ErrDeviceIdAndMobileNil //ErrDeviceIdAndMobileNil devId和手机号不存在 5014 + ErrUserNameTooLong //ErrUserNameTooLong 用户名称太长 5015 + ErrMobileHasBindByOther //ErrMobileHasBindByOther 该手机号已经被使用 5016 + ErrMobileInvalid //ErrMobileInvalid 手机号不合法 5017 + ErrTokenIsNotExist //ErrTokenIsNotExist token token不存在 5018 + ErrCaptchaInvalid //ErrCaptchaInvalid 验证码不正确 5019 + ErrWithDrawAccountHasBind //ErrWithDrawAccountHasBind 提现账号已经绑定 5020 + ErrQrInvalid //ErrQrInvalid 无效的二维码 5021 + ErrBadDevID //ErrBadDevID 错误的设备ID 5022 Since 3.6.0 + ErrDbInsertManyError //ErrDbInsertManyError 数据库批量插入错误 5023 + ErrInvalidEmail // 无效邮箱 + ErrNotSupportedCaptcha // 验证码发送方式不支持 + ErrEmailHasBinded // 邮箱已经绑定 + ErrEmailBindedByOther // 邮箱已被其他uid的用户使用 + ErrAccountHasBinded // 账号已经绑定 + ErrAccountBindedByOther // 账号已被他人使用 + ErrAccountHasIllegalContent // 账号有非法内容 + ErrInvalidAccount // 账号无效 + ErrDbTooMuchRecords // 单词查询数据量太大 + AdvanceOrderStatusIsErr // 预售订单异常 + AdvanceOrderPayTimeIsErr // 预售订单支付时间异常 + ErrDataInvalid // 数据不合法 +) + +const ( + CommentErrSendFail Code = iota + 7000 // 评论发送失败 7000 + CommentErrGetDataFail // 评论数据获取失败 7001 + CommentErrIncCountFailed // 评论增加数量失败 7002 + CommentErrDecCountFailed // 评论减少数量失败 7003 + CommentErrVideoIsInvalid // 当前视频不可评论 7004 + CommentUserHasBanned // 用户已经被禁言 7005 + CommentUserNotBind // 用户还未绑定 7006 + CommentPublishTooFrequent // 发言太频繁 7007 + CommentUserInfoErr // 用户信息错误 7008 + CommentVideoInfoErr // 用户信息错误 7009 + CommentNotVip // 非vip用户 7010 + CommentCountOverLimit // 评论超过限制 7011 + CommentNotRchg // 非充值用户用户 7012 + CommentOnlyMerchant // 该商家已开启禁止评论权限 + UserHasDoTask // 用户已领取过任务 + UserHasReceivedIntegral // 用户已领取积分 + CommentOneMinuteOverLimit // 每分钟评论超过限制 + DownloadCountIsNotEnough // 用户下载次数足 + BatchAuditLengthTooLong // 批审核长度过长 +) + +const ( + InsufficientBalance Code = iota + 8000 //InsufficientBalance 余额不足 8000 + RechargeFaile //充值第三方错误 8001 + InvalidBankCard //InvalidBankCard 8002 + UnkonwErr //UnkonwErr 8003 + InvalidProduct //InvalidProduct 8004 + RepeatPurchase //RepeatPurchase 重复购买 8005 + PayBusy //PayBusy 支付繁忙 8006 + BuyFailed //BuyFailed 购买失败 8007 + RepeatSubmitOrder //RepeatSubmitOrder 订单重复提交 + CanNotTransfer //RepeatSubmitOrder 订单重复提交 + DifferentBankActName //与上次提现成功不一样的姓名 + GoldVideoCoupleNotExist //GoldVideoCoupleNotExist 金币视频抵用券不存在 + GoldVideoCoupleAmountErr //GoldVideoCoupleAmountErr 金币视频抵用券面值必须大于视频金额 + WithdrawBusy //提现操作繁忙,同一用户短时间内多次提交 8013 + WithDrawIllegalAmount //提现金额不满足要求 8014 + GetLikesBusy //获取“喜欢”列表过快 8015 + GetBuyVidBusy //获取已购列表过快 + InsufficientIntegral //InsufficientIntegral 积分不足 8000 + OrganizeInsufficientIntegral //OrganizeInsufficientIntegral 积分不足 8000 +) + +const ( + AdminIsNotExist Code = iota + 9000 //AdminIsNotExist 9000 + AdminIDErr //AdminIDErr 9001 + AdminRefreshTokenErr //AdminRefreshTokenErr 9002 + AdminIsHasLockedErr //管理员被封禁 9003 + LiaoBaTopOverLimit //撩吧置顶超出10条,请先删除多余置顶帖子 9004 + ModuleConfLimitExceed //模块下专题数量超过限制 9005 + NoSelfAddMsg // 不能给自己发消息 +) + +const ( + DistrictActErr Code = iota + 21001 //DistrictActErr 21001 + DistrictRefreshTokenErr //DistrictRefreshTokenErr 21002 + DistrictAgentNotBind //DistrictAgentNotBind 21003 + UserNotDistrictAgent //UserNotDistrictAgent 21004 + DistrictActIsNotExist //DistrictActIsNotExist 21005 + AgentNotInvitedUID //AgentNotInvitedUID 21006 + DiscNameExists //DiscNameExists 21007 + DistrictInsufficientBalance //DistrictInsufficientBalance 21008 + DistrictWithdrawAmountTooLittleError //DistrictWithdrawAmountTooLittleError 21009 + DistrictWithdrawAmountTooMuchError //DistrictWithdrawAmountTooMuchError 21010 + DistrictWithdrawMethodInvalidError //DistrictWithdrawMethodInvalidError 21011 + DistrictWithdrawFailed //DistrictWithdrawFailed 21012 + DistrictWithdrawTooFast //DistrictWithdrawTooFast 21013 + DistrictPasswordError //DistrictPasswordError 21014 + DistrictMobileHasBinded //DistrictMobileHasBinded 21005 + DistrictNotHaveMobile //DistrictNotHaveMobile 21006 + DistrictTimeOutOfRange7 //DistrictTimeOutOfRange7 21007 + DistrictLessThanDividendRatio //DistrictLessThanDividendRatio 21007 + DistrictTypeIsNotExist //DistrictTypeIsNotExist 21008 + DistrictDedTypeIsNotExist //DistrictDedTypeIsNotExist 21009 + DistrictConDedSetIsNotComplete //DistrictConDedSetIsNotComplete 21010 + DistrictNUDedSetIsNotComplete //DistrictNUDedSetIsNotComplete 21010 + DistrictCodeTooLong //DistrictCodeTooLong 21010 + DiscCodeExists //DiscCodeExists 21011 + DiscOptDividendRatioIsNotExist //DiscOptDividendRatioIsNotExist 21012 + DiscDividendRatioIsNotExist //DiscOptDividendRatioIsNotExist 21012 + DiscAuthInUsing //AuthInUsing 21013 + DiscAuthIsNotExist //AuthIsNotExist 21014 + DistrictDividendBreakdown //DistrictDividendBreakdown 21015 + DistrictRoleIsNotExist //DistrictRoleIsNotExist 21016 + DistrictCPACantAddCPS //DistrictCPACantAddCPS 21017 + DistrictAddAuthFailed //DistrictAddAuthFailed 21017 + DistrictAddChildGT5 //DistrictAddChildGT5 21018 + DistrictAddAuthGT10 //DistrictAddAuthGT10 21019 + DistrictAddRoleGT20 //DistrictAddRoleGT20 21020 + DistrictInviteLinkGT5 //DistrictInviteLinkGT5 21021 + DiscHasExists //DiscHasExists 21022 + DiscNoExists //DiscNoExists 21023 + DiscUserUnitError //DiscUserUnitError 21024 + DistrictLock //DistrictLock 21025 + DiscDisTrans //DiscDisTrans 21026 + DiscParentInvalid //DiscParentInvalid 21027 + DistrictInviteLinkGT50 //DistrictInviteLinkGT5 21028 + DiscIsUsed //DiscIsUsed 21029 + DiscAndSeqeNoExists //DiscAndSeqeNoExists 21030 + DiscAuthNoExists //DiscAuthNoExists 21031 + PrizeRecordNoExists //PrizeRecordNoExists 21032 + + OrderDoesNotExist Code = iota + 21001 + 500 //OrderDoesNotExist 21501 + OrderHasReceipted //OrderHasReceipted 21502 +) + +const ( + _ = (iota * 100) + 9000 + AdminIsExisted // AdminIsExisted 管理员存在 9100 + AuthInUsing // 权限正在使用 9200 + AuthIsNotExist // 权限不存在 9300 + LoginMobileNotBind // 渠道后台账号没有绑定电话号码 9400 + LoginMobileBindErr // 渠道后台账号绑定电话号码失败 9500 + AiUnDressStatusIsErr // AI脱衣订单状态异常 + AiChangeFaceImgStatusIsErr // AI图片换脸订单状态异常 + AiMateIsNotExists // AI伴侣用户未开通 +) + +const ( + LikeHasAlreadyThumbs Code = iota + 10000 //10000 + LikeHasNotThumbs //LikeHasNotThumbs 10001 + LikeErrGetDataFailed //LikeErrGetDataFailed 10002 + LikeErrThumbsFailed //LikeErrThumbsFailed 10003 + LikeErrCancelFailed //LikeErrCancelFailed 10004 + LikeErrIncCountFailed //LikeErrIncCountFailed 10005 + LikeErrDecCountFailed //LikeErrDecCountFailed 10006 + LikeErrVideoIsInvalid //LikeErrVideoIsInvalid 10007 +) + +const ( + CollectHasCollected Code = iota + 12000 //12000 + CollectHasNotCollected //12001 + CollectErrGetDataFailed //12002 + CollectErrCollectFailed //12003 + CollectErrCancelFailed //12004 +) +const ( + ErrGetTagDataFailed Code = (iota * 1000) + 13000 + GoogleAuthFail // GoogleAuthFail 14000 + Timeout // Timeout 超时时间 15000 +) +const ( + TagAddTagNameInvalidErr Code = iota + 16000 //TagAddTagNameInvalidErr tag名称无效 16000 + TagAddTagNameEmptyErr //TagAddTagNameEmptyErr tag名称不能为空 16001 + TagAddTagNameExistedErr //TagAddTagNameExistedErr tag名称已存在 16002 + TagAddTagDescInvalidErr //TagAddTagDescInvalidErr tag描述无效 16003 + +) + +const ( + //proxy + SelfPromotionCode Code = iota + 17000 //SelfPromotionCode 17000 + InvalidPromotionCode //17001 + PromoteLoop //17002 + NotInvitedUsers //17003 + PromoteHasBind //17004 PromoteHasBind +) + +const ( + //daichong error + ErrTakeChatCode Code = iota + 18000 //发起代充聊天失败 18000 + ErrGetOrderInfoCode //获取代理充值订单信息失败 18001 + ErrAppIDNotExist //代充AppID 不存在 18002 + ErrMerchantIsNotExist //此商人不存在 18003 + ErrMerchantBalanceIsNotEnough //商人余额不足 18004 + ErrMerchantBalanceHasLock //商人被禁止登陆 18005 + ErrMerchantSeekScoreFail //商人追分失败 18006 + ErrMerchantSeekScoreFailDueNotEnoughAmount //商人追分失败 18007 +) + +// 接口性能 +const ( + // Visit limit + VisitLimit Code = 19000 // 访问被限制 +) + +const ( + ActClose Code = iota + 20000 //活动关闭 20000 + ActUserDataTypeError //活动的用户数据类型错误 20001 + ActInvitReceiveCoinsLessThan10 //推广活动领取的金币小于10 20002 + ActLotteryNoTimes //轮盘活动次数不够了 +) + +// 后台渠道 +const ChannelAgentExisted Code = iota + 21000 //渠道账号已经存在 20000 + +const ( + ExchangeCodeProduceCodeFailed Code = iota + 22000 // 22000 兑换码生成失败 + ExchangeCodeExchangeFailed // 22001 兑换失败 + ExchangeCodeInvalidCode // 22002 无效兑换码 + ExchangeCodeIsNotEffective // 22003 未到兑换时间 + ExchangeCodeIsExpired // 22004 已过期 + ExchangeCodeIsUsed // 22005 兑换码已经使用 + ExchangeCodeInvalidApp // 22006 无效的app类型 + ExchangeCodeInvalidAuthority // 22007 无效的权限类型 + ExchangeCodeNullChannelForbidden // 22008 渠道不能为空 + ExchangeCodeOperateDenied // 22009 权限不够,操作被拒绝 + ExchangeCodeInvalidBatchNumber // 22010 无效的批次号 + ExchangeCodeForbidUpdate // 22011 该兑换码已使用或者已过期,不能修改 + ExchangeCodeRepeatExchange // 22012 一兑换过该批次的兑换码 + ExchangeCodeInvalid // 22006 无效的兑换类型 +) + +// 机器人配置与内容 +const ( + RobotTypeInvalid Code = iota + 23000 // 23000 无效的机器人类型 + RobotTypeExisted // 23001 该类机器人已经存在 + RobotContentDelParamIsNull // 23002 删除机器人聊天内容的参数不能为空 + RobotContentIsNull // 23003 增加的内容不能为空 + RobotContentDelFailed // 23004 删除内容失败,没有改内容 + RobotInvalidConf // 23005 机器人配置的最大值无效 +) + +// 机器人配置与内容 +const ( + NotSystemAds Code = iota + 25000 // 25000 不是系统广告 + ADSNotExist // 25001 广告不存在 + ADReviewNotExist // 25002 广告审核不存在 + URLInvalid // 25003 URL非法 + HostNameInvalid // 25004 域名非法 + ErrADScant // 25005 广告大小或尺寸错误 +) + +const ( + RecmdTagIsExisted Code = iota + 26000 //26000 推荐标签已存在 + TagNotExist //26001 标签不存在 +) + +// 新版增加错误码 +const ( + NoWatchingCoupon Code = iota + 27000 //27000 没有观影券 + PiggyBankNotEnough //27001 存钱罐余额不够 + NoVipPrivilege //27002 没有vip权限 + PayCoinsLimit200 //27003 观影券免费观看需200金币以下 + NoVipNoUpload //27004 非vip限制3次上传 + ExpiredVip //27005 vip已经过期 + AiGenningDelForbidden //27006 ai订单不允许删除生成中的订单 + ChangeFaceSuccessWithoutUrl //27007 ai订单通过时没有视频url + ChangeFaceNoModule //27008 ai换脸模版不存在 +) + +// 楼凤错误码 +const ( + RepeatUpload Code = iota + 28000 //未审核,重复提交验证报告 + GetLockTimeout + OverTargetCoin + RepeatFeedback + LouFengNotExists //楼凤不存在 +) + +// 签到错误码 +const ( + PrizeIsExistError Code = iota + 29000 + SignConfigIsExistError + ActivityDateError // 活动开始时间或者结束时间配置不正确 + PhoneNotBoundOrNotRecharge //绑定手机号或充值才可签到 + HasSigned // 已签到 + ExceededTimes // 超出次数 +) + +const ( + NudeChatRepeatFeedbackErr Code = iota + 30000 // 裸聊信息重复举报 + NotBuyBanFeedbackErr // 未购买禁止举报 +) + +const ( + AddTaskRepeatedError = 499 +) + +const ( + VidNotExists Code = iota + 50000 // 视频不存在 + OnlyOfficialVideo // 只能操作官方上传的视频 + VideoTypeDissatisfy // 帖子类型不符合 + VideoTagNoExists // 帖子标签未配置 + VideoNotPass // 帖子未审核 + VideoRepeatPop // 帖子重复推广 + VideoNotPop // 帖子未推广 + VideoInDiscountArea + NotAVideo + ContentSensitiveHit // 内容敏感词命中,不予上架 +) + +const ( + InsufficientGold Code = iota + 40000 // 40000 金币不足 + InsufficientPoint // 40001 积分不足 + InsufficientLotteryFreeTimes // 40002 免费次数不足 +) + +const ( + OfficialWebsiteBasicDataNotExist Code = iota + 60000 // 60000 官方站基础数据不存在 + OfficialWebsiteBasicDataExist // 60001 官方站基础数据存在 + OfficialWebsiteHeroExist // 60002 官方站创作者存在 + OfficialWebsiteHeroNotExist // 60003 官方站创作者不存在 + OfficialWebsiteAlbumExist // 60004 官方站专辑存在 + OfficialWebsiteAlbumNotExist // 60005 官方站专辑不存在 + OfficialWebsiteNewsExist // 60006 官方站新闻存在 + OfficialWebsiteNewsNotExist // 60007 官方站新闻不存在 + OfficialWebsitePartnerExist // 60008 官方站合作伙伴存在 + OfficialWebsitePartnerNotExist // 60009 官方站合作伙伴不存在 + OfficialWebsiteJobListExist // 60010 官方站职位列表存在 + OfficialWebsiteJobListNotExist // 60011 官方站职位列表不存在 + OfficialWebsiteVideoExist + OfficialWebsiteVideoNotExist + OfficialWebsiteImportVideoFailed +) diff --git a/common/stderr/customerr.go b/common/stderr/customerr.go new file mode 100644 index 0000000..f1c6537 --- /dev/null +++ b/common/stderr/customerr.go @@ -0,0 +1,31 @@ +package stderr + +import "fmt" + +// CustomErr 代替/common/error中的定义,逐渐在代码中弃掉(common/error模块) +type CustomErr struct { + Code Code `json:"code"` + Msg string `json:"msg"` + Tips string `json:"tips"` +} + +// NewCustomErr an error +func NewCustomErr(code Code, Msg string, tips ...string) *CustomErr { + res := &CustomErr{ + Code: code, + Msg: Msg, + Tips: Msg, + } + if len(tips) > 0 { + res.Tips = tips[0] + } + return res +} + +// Error 返回字符串 +func (s *CustomErr) Error() string { + if s == nil { + return "" + } + return fmt.Sprintf("code = %d, msg = %s ,tips = %v", s.Code, s.Msg, s.Tips) +} diff --git a/common/stderr/error.go b/common/stderr/error.go new file mode 100644 index 0000000..4b2a0e5 --- /dev/null +++ b/common/stderr/error.go @@ -0,0 +1,32 @@ +package stderr + +import ( + "errors" + "strings" +) + +var ( + InsertExistError = errors.New("E11000 duplicate key error") + WriteBrokenError = errors.New("broken pipe") +) + +func IsEqual(err error, target error) bool { + if err != nil && target != nil { + return err.Error() == target.Error() + } + return false +} + +func IsContain(err interface{}, target error) bool { + if err != nil && target != nil { + var origin string + switch err := err.(type) { + case string: + origin = err + case error: + origin = err.Error() + } + return strings.Contains(origin, target.Error()) + } + return false +} diff --git a/common/stderr/msg.go b/common/stderr/msg.go new file mode 100644 index 0000000..29e0e74 --- /dev/null +++ b/common/stderr/msg.go @@ -0,0 +1,209 @@ +package stderr + +const AiGenningDelForbiddenMessage = "不可删除未完成的ai订单" + +var codeAndMsg = map[Code]string{ + Success: "success", + Failure: "Failed", + FunctionNotEnabled: "功能未开启,敬请期待!", + FunctionUpgrade: "功能维护升级中,敬请期待最新版本!", + ErrUnrecognized: "unknown error", + UserIsNotExists: "user is not exist", + UserIsException: "user is exception", + SignFailedToVerify: "sign verify failed", + ErrDownloadError: "file download fail", + ErrUploadError: "file upload fail", + UpLoadingFile: "uploading file", + ErrFsBucketNotExist: "fs bucket not exist", + ErrFsOptionWrong: "fs option wrong", + UpLoadFileComplete: "upload file complete", + ErrExportFileFail: "export file fail", + FileMerging: "file is merging", + FileConverting: "file is converting", + UploadLoadingToFs: "file is uploading fs", + ErrConnectToFs: "connect to fs server is fail", + ErrFsServerFile: "fs server error", + ErrMergeFile: "file merge error", + ErrConvertFile: "file convert error", + ErrPlayTimeTooShort: "video play time is too short", + ErrUploadExistVideo: "upload exist video", + CodeEmptyData: "empty data", + ErrMimeType: "mime type error", + ErrAccessForbid: "no access", + ErrNamePwdIncorrect: "userName or password error", + ErrUserNameTooLong: "user name too long, length must be less than or equal to 12 characters length", + ErrAuthExpired: "certificate expired", + ErrAuthInvalid: "invalid signature", + ErrLoginExpired: "login expired", + ErrUserBeBanned: "User has been banned", + ErrVersionUpdate: "new app version need update", + ErrVersionRange: "set version update range error", + ErrVersionTooLow: "specify upgrade version is too low", + ErrParamError: "parameters error", + ErrReqForbidden: "request rejected", + ErrPathNotFount: "request path does not exist", + ErrMethodIncorrect: "request method error", + ErrTimeout: "service timeout", + ErrLoginTooFrequently: "login too frequently", + ErrSmsSendFail: "send sms code fail", + ErrNetWorkBusy: "network bad", + ErrInvalidRequest: "invalid request", + ErrServerUnavailable: "service is not available", + ErrDbQueryError: "database query error", + ErrDbInsertError: "database insert error", + ErrDbUpdateError: "database update error", + ErrDbDeleteError: "database delete error", + ErrDbInputExist: "current input has already exists", + ErrDeviceIdAndMobileNil: "devId and mobile nil", + ErrNoToken: "no token", + InvalidToken: "invalid token", + ErrCaptcha: "captcha err", + ErrCaptchaInvalid: "invalid", + ErrWithDrawAccountHasBind: "current withDraw account has bind", + PayBusy: "business too busy now,please try again later", + ErrCaptchaExists: "captcha code exist", + InsufficientBalance: "Insufficient balance", + RechargeFaile: "Other toPay error", + InvalidBankCard: "Invalid BankCard", + ErrMobileHasBind: "you have already bind this phone number", + ErrMobileInvalid: "invalid phone number", + ErrTokenIsNotExist: "token is not exist", + UnkonwErr: "Unkonw Err 订单异常,请与第三方核对该订单信息", + RepeatSubmitOrder: "订单在处理中,请勿重复提交", + CanNotTransfer: "划转功能已关闭,请在游戏钱包充值", + DifferentBankActName: "The name of the new card must be the same as your previous cash card name (Zhang San). Please fill in the bank card with the same name before cashing out. If you need to change your name, please contact customer service.", + ErrLoginMobileNotExist: "current phone is not bind", + InvalidProduct: "invalid product", + RepeatPurchase: "repeat purchase", + AdminIsNotExist: "admin is not exist, the account or password is incorrect.", + ErrEmailHasBinded: "邮箱已被绑定,请直接登录", + ErrEmailBindedByOther: "该邮箱已被其他用户使用,请换一个", + ErrAccountHasBinded: "账号已被绑定,请直接登录", + ErrAccountBindedByOther: "改账号已被其他用户使用,请换一个", + ErrAccountHasIllegalContent: "您的账号有非法内容,请换一个", + ErrInvalidAccount: "系统不存在此账号,请确认输入的账号已经注册", + ErrInterServerError: "系统内部错误,请联系客服", + ErrInvalidEmail: "邮箱格式有误,请重试", + ErrDbTooMuchRecords: "查询数据量太大,请缩小查询范围", + AdvanceOrderStatusIsErr: "预售订单异常", + AdvanceOrderPayTimeIsErr: "预售订单支付时间异常", + BuyFailed: "but failed,please try again later", + AdminIsExisted: "the account is existed", + AuthInUsing: "auth in using", + AuthIsNotExist: "auth is not exist", + GoogleAuthFail: "google 2 factor faild", + CommentErrSendFail: "Comment publish failed", + CommentErrGetDataFail: "Get data failed", + CommentErrIncCountFailed: "Increase comment count failed", + CommentErrDecCountFailed: "Decrease comment count failed", + CommentErrVideoIsInvalid: "Video is not verified", + CommentUserHasBanned: "您已被禁言!", + CommentUserNotBind: "您暂未绑定手机,无法发表评论", + ErrMobileHasBindByOther: "current phone has already bind by other", + CommentPublishTooFrequent: "Comment Publish Too Frequently", + CommentUserInfoErr: "User information is error", + CommentVideoInfoErr: "Video information is error", + CommentNotVip: "User is not vip", + CommentCountOverLimit: "非vip用户每24小时只能发送限量评论", + LikeHasAlreadyThumbs: "Has already liked, can't like it again", + LikeHasNotThumbs: "Has not liked, can't cancel", + LikeErrGetDataFailed: "Get like data error", + LikeErrThumbsFailed: "Like it failed", + LikeErrCancelFailed: "Cancel like failed", + LikeErrIncCountFailed: "Increase like count failed", + LikeErrDecCountFailed: "Decrease like count failed", + LikeErrVideoIsInvalid: "Video is not verified", + CollectHasCollected: "Has already collected, can't collect it again", + CollectHasNotCollected: "Has not collected, can't cancel", + CollectErrGetDataFailed: "Get collect data failed", + CollectErrCollectFailed: "Do collect failed", + CollectErrCancelFailed: "Cancel collect failed", + ErrGetTagDataFailed: "Get tag data failed", + Timeout: "timeout", + SelfPromotionCode: "The inviter cannot be himself", + InvalidPromotionCode: "invalid promotion code", + PromoteLoop: "Already a subordinate agent, not a superior agent", + NotInvitedUsers: "Not Invited Users", + PromoteHasBind: "current promote has bind", + TagAddTagNameInvalidErr: "Tag name is invalid", + TagAddTagNameEmptyErr: "Tag name is empty", + TagAddTagNameExistedErr: "Current Tag name is existed", + //daichong + ErrTakeChatCode: "take daichong chat failed", + ErrGetOrderInfoCode: "get daichong order info failed", + ErrAppIDNotExist: "daichong appid is not exist", + ErrMerchantIsNotExist: "current merchant not exist", + ErrMerchantBalanceIsNotEnough: "商人余额不足", + ErrMerchantBalanceHasLock: "商人被禁止登陆", + VisitLimit: "操作太频繁,请稍后再试!", + ActClose: "activity has closed", + ActUserDataTypeError: "act userData type error", + ChannelAgentExisted: "该账号已经存在", + // 兑换码 + ExchangeCodeProduceCodeFailed: "Produce code failed", + ExchangeCodeExchangeFailed: "Exchange failed", + ExchangeCodeInvalidCode: "Invalid code", + ExchangeCodeIsNotEffective: "Code is inactive", + ExchangeCodeIsExpired: "Code is expired", + ExchangeCodeIsUsed: "Code has used", + ExchangeCodeInvalidApp: "Invalid app type", + ExchangeCodeInvalidAuthority: "Invalid authority type", + ExchangeCodeNullChannelForbidden: "Null channel is forbidden", + ExchangeCodeOperateDenied: "Operate denied", + ExchangeCodeInvalidBatchNumber: "Batch number invalid", + ExchangeCodeForbidUpdate: "The code is expired or used, can't update", + ExchangeCodeRepeatExchange: "Has exchanged", + RobotTypeInvalid: "Invalid robot type", + RobotTypeExisted: "Existed robot type", + RobotContentDelParamIsNull: "Param delete content is null", + RobotContentIsNull: "Content is null", + RobotContentDelFailed: "Content is empty", + DistrictActErr: "错误的商区账号", + RepeatUpload: "Repeat Upload", + GetLockTimeout: "获取抽奖状态锁超时", + OverTargetCoin: "活动当前金额已经达目标金额,无法再投注", + RepeatFeedback: "Repeat Feedback", + LouFengNotExists: "invalid loufeng", + AdminIsHasLockedErr: "管理员被封禁", + LiaoBaTopOverLimit: "撩吧置顶超出10条,请先删除多余置顶帖子", + ExchangeCodeInvalid: "PayType Error", + PrizeIsExistError: "奖品名称重复", + SignConfigIsExistError: "同类型配置;达成条件已存在", + ActivityDateError: "活动开始时间或者结束时间配置不正确", + PhoneNotBoundOrNotRecharge: "绑定手机号或充值才可签到", + UserIsNotMerchant: "用户不是商家", + UserActiveNotEnough: "活跃度不足", + NudeChatRepeatFeedbackErr: "本条信息已举报,请耐心等待平台处理!", + NotBuyBanFeedbackErr: "未购买不能举报!", + ResolutionNotEnough: "分辨率不足", + WithdrawBusy: "提现操作过于频繁,请稍后再试", + WithDrawIllegalAmount: "提现金额不满足要求,请阅读提现说明", + ModuleConfLimitExceed: "模块下专题数量超过限制", + GetLikesBusy: "操作太过频繁,请稍后再试", + GetBuyVidBusy: "操作太过频繁,请稍后再试", + CommentNotRchg: "非充值用户不可评论", + CommentOnlyMerchant: "该商家已开启禁止评论权限", + AddTaskRepeatedError: "已有相同类型任务. 请在该任务上编辑即可, 不需要新增", + UserHasDoTask: "用户重复领取任务", + UserHasReceivedIntegral: "用户重复领取积分", + InsufficientIntegral: "用户积分不足", + CommentOneMinuteOverLimit: "非VIP一分钟只能评论一次,升级会员,可无限评论", + DownloadCountIsNotEnough: "下载次数不足,请您充值VIP获取次数", + AiGenningDelForbidden: AiGenningDelForbiddenMessage, + ChangeFaceSuccessWithoutUrl: "通过ai换脸订单时生成的视频链接不可为空", + ChangeFaceNoModule: "ai换脸模版不存在,请检查模版", + NoSelfAddMsg: "不能给自己发消息", + AiUnDressStatusIsErr: "AI脱衣订单状态异常", + AiChangeFaceImgStatusIsErr: "AI图片换脸订单状态异常", + PrizeRecordNoExists: "抽奖信息不存在", + VideoInDiscountArea: "视频已经在其他折扣区", + NotAVideo: "不是视频", + ContentSensitiveHit: "内容敏感词命中,不予上架", + ErrDataInvalid: "数据不合法", + AiMateIsNotExists: "AI伴侣服务用户未开通!", + FunctionUpgradeToLatest: "功能已升级,请重新下载最新版本安装使用", + VideoTypeDissatisfy: "帖子类型不符合", + InsufficientGold: "金币不足", + InsufficientPoint: "积分不足", + InsufficientLotteryFreeTimes: "免费次数不足", +} diff --git a/common/stderr/stderr.go b/common/stderr/stderr.go new file mode 100644 index 0000000..6f5996e --- /dev/null +++ b/common/stderr/stderr.go @@ -0,0 +1,47 @@ +package stderr + +import "fmt" + +type Code int + +type CodeMsg struct { + Code int `json:"code"` + Msg string `json:"msg"` + Tip string `json:"tip,omitempty"` +} + +func (c Code) Msg() string { + msg, ok := codeAndMsg[c] + if ok { + return msg + } + return "" +} + +func (c Code) Tip() string { + tip, ok := codeAndTip[c] + if ok { + return tip + } + return "" +} + +func (c Code) Error() string { + return fmt.Sprintf("code=%d,msg=%v,tip=%v", c, c.Msg(), c.Tip()) +} + +func (c Code) Struct() CodeMsg { + return CodeMsg{Code: int(c), Msg: c.Msg(), Tip: c.Tip()} +} + +func New(code int, msg string) error { + return fmt.Errorf("code:%d,msg:%s", code, msg) +} + +func (c Code) SetMsg(msg string) CodeMsg { + return CodeMsg{Code: int(c), Msg: msg, Tip: c.Tip()} +} + +func (c Code) SetTip(tip string) CodeMsg { + return CodeMsg{Code: int(c), Msg: c.Msg(), Tip: tip} +} diff --git a/common/stderr/tip.go b/common/stderr/tip.go new file mode 100644 index 0000000..ef56058 --- /dev/null +++ b/common/stderr/tip.go @@ -0,0 +1,192 @@ +package stderr + +var codeAndTip = map[Code]string{ + FunctionNotEnabled: "功能未开启,敬请期待!", + FunctionUpgrade: "功能维护升级中,敬请期待最新版本!", + UserActiveNotEnough: "活跃度不足", + BadUA: "请至官网下载最新版本", + ErrParamError: "参数错误", + InvalidToken: "用户信息已经过期,请重新登陆", + UserIsNotExists: "用户不存在", + UserIsException: "异常用户", + ErrUserBeBanned: "用户已被禁言", + ErrAccessForbid: "没有访问权限", + ErrNamePwdIncorrect: "用户名或密码错误", + InsufficientBalance: "余额不足", + RechargeFaile: "支付失败,稍后再试", + UnkonwErr: "订单异常,请与第三方核对", + RepeatPurchase: "已经购买过了", + GoldVideoCoupleNotExist: "金币视频抵用券不存在", + GoldVideoCoupleAmountErr: "金币视频抵用券面值必须大于视频金额", + BuyFailed: "购买失败,请稍后尝试", + PayBusy: "支付繁忙,请稍后再试!", + ErrNetWorkBusy: "网络异常,请稍后尝试", + CanNotTransfer: "划转功能已关闭,请在游戏钱包充值", + DifferentBankActName: "新卡姓名必须与您以前的提现卡姓名(张三)一致,请填写同名银行卡后再提现。如需更改姓名请联系客服。", + WithdrawBusy: "提现操作过于频繁,请稍后再试", + WithDrawIllegalAmount: "提现金额不满足要求,请阅读提现说明", + GetLikesBusy: "操作太过频繁,请稍后再试", + GetBuyVidBusy: "操作太过频繁,请稍后再试", + CommentNotRchg: "非充值用户不可评论", + CommentOnlyMerchant: "该商家已开启禁止评论权限", + InvalidPromotionCode: "无效的推广码", + PromoteLoop: "您已绑定下级,无法绑定上级", + PromoteHasBind: "您已经绑定了该推广码", + ErrWithDrawAccountHasBind: "当前提现账号已经被绑定,请添加新账号", + ErrQrInvalid: "二维码失效", + ErrCustomerBanned: "客服已经休息啦,请稍后再联系", + SelfPromotionCode: "不能填写自己的邀请码", + ErrCaptchaExists: "短信验证码已经发出,请不要重复发送", + ErrVersionUpdate: "请更新最新版本", + ErrVersionRange: "设置版本更新范围错误,上限版本不能小于下限版本", + CollectHasCollected: "您已经收藏过了", + CollectHasNotCollected: "您还没有收藏它", + LikeHasAlreadyThumbs: "您已经点过赞了", + LikeHasNotThumbs: "您还没有对它点赞", + CommentUserNotBind: "您暂未绑定手机,无法发表评论", + VisitLimit: "操作太频繁,请稍后再试!", + NotInvitedUsers: "您不是推广用户", + CommentPublishTooFrequent: "您发言太频繁了,请稍后再试~", + CommentErrSendFail: "评论发送失败", + ActClose: "活动已关闭!", + ActInvitReceiveCoinsLessThan10: "推广活动,至少领取10金币", + ErrReqForbidden: "该请求被拒绝", + ErrInvalidRequest: "非法请求", + ErrLoginMobileNotExist: "该手机号未绑定", + AdvanceOrderStatusIsErr: "预售订单异常", + AdvanceOrderPayTimeIsErr: "预售订单支付时间异常", + ErrMobileInvalid: "手机号不合法", + ErrUserNameTooLong: "用户名过长,请限制在20个字符", + CommentNotVip: "对不起!您为非vip用户,不能发表评论!", + ErrMobileHasBind: "您已经绑定了此号码", + ErrMobileHasBindByOther: "当前手机号码,已被使用,请输入新号码", + ErrCaptcha: "验证码已过期,请重新获取", + ErrSmsSendFail: "验证码发送失败", + ErrCaptchaInvalid: "验证码不正确,请重新发送", + ErrFsServerFile: "文件服务繁忙,请稍后再试", + ErrPlayTimeTooShort: "上传视频应大于10秒", + ErrUploadExistVideo: "请勿重复上传视频", + ForbidUploadVideo: "您因违反规定,上传违规内容视频,已经被禁止上传,请联系客服解封!", + ErrConnectToFs: "网络繁忙,请稍后再试", + AgentNotInvitedUID: "代理没有邀请该用户", + DistrictActIsNotExist: "账户不存在", + DistrictInsufficientBalance: "余额不足", + DistrictWithdrawAmountTooLittleError: "提现金额太少, 单次不能少于100元", + DistrictWithdrawAmountTooMuchError: "提现金额太多, 单次不能超过10000元", + DistrictWithdrawMethodInvalidError: "提现方法失效", + DistrictWithdrawFailed: "提现失败", + DistrictWithdrawTooFast: "提现太快", + DistrictPasswordError: "密码错误", + DistrictMobileHasBinded: "手机号已被绑定", + DistrictNotHaveMobile: "用户没有绑定手机", + DistrictTimeOutOfRange7: "时间超过7天", + DistrictLessThanDividendRatio: "优化分红比例不能小于实际分红比例", + DistrictTypeIsNotExist: "不存在的商区类型", + DistrictDedTypeIsNotExist: "不存在的扣量类型", + DistrictConDedSetIsNotComplete: "用户消费扣量设置不完整", + DistrictNUDedSetIsNotComplete: "用户新增扣量设置不完整", + DistrictCodeTooLong: "商区码不能超过8个字符", + UserNotDistrictAgent: "该用户不是代理用户", + DistrictAgentNotBind: "该手机号码未绑定用户", + DiscNameExists: "账号已存在", + DiscCodeExists: "商区码已存在", + DiscOptDividendRatioIsNotExist: "优化分红比例不存在", + DiscDividendRatioIsNotExist: "分红比例不存在", + DiscAuthInUsing: "权限被占用", + DiscAuthIsNotExist: "权限不存在", + DistrictDividendBreakdown: "分红比例击穿", + DistrictRoleIsNotExist: "角色不存在", + DistrictCPACantAddCPS: "CPA商区不能添加CPS商区", + DistrictAddAuthFailed: "添加权限失败", + DistrictAddChildGT5: "最多添加5个子商区", + DistrictAddAuthGT10: "最多添加10个权限", + DistrictAddRoleGT20: "最多添加20个角色", + DistrictInviteLinkGT5: "最多添加5个推广链接", + DiscHasExists: "商区已经存在", + DiscNoExists: "商区不存在", + DiscUserUnitError: "用户单价错误", + DistrictLock: "系统升级中,无法登陆", + DiscDisTrans: "暂时无法提现", + DiscParentInvalid: "父商区无效", + DistrictInviteLinkGT50: "最多添加50个子商区", + DiscIsUsed: "商区被使用", + DiscAndSeqeNoExists: "商区码或序号不存在", + DiscAuthNoExists: "帐号没有配置权限, 请联系客服获取权限", + ErrEmailHasBinded: "邮箱已被绑定,请直接登录", + ErrEmailBindedByOther: "该邮箱已被其他用户使用,请换一个", + ErrAccountHasBinded: "账号已被绑定,请直接登录", + ErrAccountBindedByOther: "改账号已被其他用户使用,请换一个", + ErrAccountHasIllegalContent: "您的账号有非法内容,请换一个", + ErrInvalidAccount: "系统不存在此账号,请确认输入的账号已经注册", + ErrInterServerError: "系统内部错误,请联系客服", + ErrInvalidEmail: "邮箱格式有误,请重试", + ErrDbTooMuchRecords: "查询数据量太大,请缩小查询范围", + OrderDoesNotExist: "订单不存在", + OrderHasReceipted: "订单已回执", + ErrVersionTooLow: "指定的升级版本太低", + ExchangeCodeProduceCodeFailed: "兑换码生成失败", + ExchangeCodeExchangeFailed: "兑换失败,请重试", + ExchangeCodeInvalidCode: "无效兑换码", + ExchangeCodeIsNotEffective: "该兑换码未到兑换时间", + ExchangeCodeIsExpired: "该兑换码已经过期", + ExchangeCodeIsUsed: "兑换码已经使用", + ExchangeCodeInvalidApp: "无效的app类型", + ExchangeCodeInvalidAuthority: "无效的权限类型", + ExchangeCodeNullChannelForbidden: "渠道不能为空", + ExchangeCodeOperateDenied: "权限不够,操作被拒绝", + ExchangeCodeInvalidBatchNumber: "无效的批次号", + ExchangeCodeForbidUpdate: "该兑换码已使用或者已过期,不能修改", + ExchangeCodeRepeatExchange: "您已兑换过", + ErrLoginTooFrequently: "官人,您进进出出太快了,小娘子受不了了,一会儿再来哦", + RobotTypeInvalid: "无效的机器人类型", + RobotTypeExisted: "机器人类型已经存在", + RobotContentDelParamIsNull: "删除机器人聊天内容的参数不能为空", + RobotContentIsNull: "增加的内容不能为空", + RobotContentDelFailed: "删除内容失败,没有改内容", + TagAddTagNameEmptyErr: "标签名称不能为空", + TagAddTagDescInvalidErr: "标签描述无效", + TagAddTagNameInvalidErr: "输入的标签名称存在违规词汇,请重新输入新名称", + TagAddTagNameExistedErr: "当前标签已经存在,请输入新标签", + NotSystemAds: "不是系统广告", + ADSNotExist: "广告不存在", + ADReviewNotExist: "广告审核不存在", + URLInvalid: "URL非法", + HostNameInvalid: "域名错误", + ErrADScant: "尺寸错误", + NoVipNoUpload: "非vip限制3次上传", + ExpiredVip: "金币月卡时间已经过期", + AiGenningDelForbidden: "不可删除未完成的ai订单", + ChangeFaceSuccessWithoutUrl: "通过ai换脸订单时生成的视频链接不可为空", + ChangeFaceNoModule: "ai换脸模版不存在,请检查模版", + RecmdTagIsExisted: "推荐标签已存在", + TagNotExist: "标签不存在", + NoVipPrivilege: "非充值用户不能进行操作", + RepeatUpload: "Repeat Upload", + GetLockTimeout: "获取抽奖状态锁超时", + OverTargetCoin: "活动当前金额已经达目标金额,无法再投注", + RepeatFeedback: "不可重复提交楼凤举报", + LouFengNotExists: "该楼凤数据异常,暂时无法举报", + ExchangeCodeInvalid: "PayType Error", + ModuleConfLimitExceed: "模块下专题数量超过限制", + ResolutionNotEnough: "分辨率不足", + CommentUserHasBanned: "您已被禁言,请联系客服!", + PhoneNotBoundOrNotRecharge: "绑定手机或充值才可签到", + UserHasDoTask: "用户重复领取任务", + UserHasReceivedIntegral: "用户重复领取积分", + InsufficientIntegral: "用户积分不足", + CommentOneMinuteOverLimit: "非VIP一分钟只能评论一次,升级会员,可无限评论", + DownloadCountIsNotEnough: "下载次数不足,请您充值VIP获取次数", + NoSelfAddMsg: "不能给自己发消息", + AiUnDressStatusIsErr: "AI脱衣订单状态异常", + AiChangeFaceImgStatusIsErr: "AI图片换脸订单状态异常", + PrizeRecordNoExists: "抽奖信息不存在", + ErrTimeIsEmpty: "时间范围不能为空", + ErrTimeWithinWeek: "时间跨度不能超过一周", + OrganizeInsufficientIntegral: "组织积分不足", + ErrDataInvalid: "数据不合法", + AiMateIsNotExists: "AI伴侣服务用户未开通!", + FunctionUpgradeToLatest: "功能已升级,请重新下载最新版本安装使用", + InsufficientGold: "金币不足", + InsufficientPoint: "积分不足", + InsufficientLotteryFreeTimes: "免费次数不足", +} diff --git a/common/store/store.go b/common/store/store.go new file mode 100644 index 0000000..407ce4f --- /dev/null +++ b/common/store/store.go @@ -0,0 +1,65 @@ +package store + +import ( + "91porn-server/app/appg" + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "encoding/json" + "fmt" + "net/url" +) + +func GetStoreLink(u *UserData) string { + if u == nil { + return "" + } + + sign, err := encryptUserData(u, appg.Conf.Base.StoreEncryptKey) + if err != nil { + return "" + } + urlStr := fmt.Sprintf("%v?sign=%v", appg.Conf.URL.StoreUrl, url.QueryEscape(sign)) + return urlStr +} + +type UserData struct { + AppUid uint64 `json:"uid"` // uid + AppId int `json:"appId"` + Name string `json:"name"` // 姓名 + Portrait string `json:"portrait"` // 头像 + ExpireTime int64 `json:"expireTime"` // 失效时间的时间戳,单位秒 + Balance int64 `json:"balance"` +} + +func encryptUserData(u *UserData, key string) (sign string, err error) { + b, err := json.Marshal(u) + if err != nil { + return + } + encryptBytes, err := Encrypt(b, key) + if err != nil { + return + } + sign = string(encryptBytes) + return +} + +// TODO 这里的别动 +var commonIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f} + +func Encrypt(plainByte []byte, keyText string) (res string, err error) { + // 转换成字节数据, 方便加密 + keyByte := []byte(keyText) + // 创建加密算法aes + c, err := aes.NewCipher(keyByte) + if err != nil { + return "", err + } + //加密字符串 + cfb := cipher.NewCFBEncrypter(c, commonIV) + cipherByte := make([]byte, len(plainByte)) + cfb.XORKeyStream(cipherByte, plainByte) + res = base64.StdEncoding.EncodeToString(cipherByte) + return +} diff --git a/common/synclock/lock.go b/common/synclock/lock.go new file mode 100644 index 0000000..d88ef63 --- /dev/null +++ b/common/synclock/lock.go @@ -0,0 +1,80 @@ +package synclock + +import ( + "errors" + "fmt" + "time" + + "91porn-server/common/redis" +) + +const ( + UserExtSyncLockFmt = "userExtSyncLock:%d" // 用户账户扩展信息 + UserExtSyncLockExpire = 5 * time.Minute // 用户账户扩展信息 过期时间 + UserAccountLockFmt = "UserAccountLockFmt:%d" // 用户账户 + SpinLockExpire = 3 * time.Second +) + +type Lock struct { + Lock *redis.Client +} + +// UserExtSyncLock 通过redis按照用户ID加锁 +func (l *Lock) UserExtcLock(userId uint32) bool { + v, e := l.Lock.Setnx_NewOK(fmt.Sprintf(UserExtSyncLockFmt, userId), true, UserExtSyncLockExpire) + if !v || e != nil { + return false + } + return true +} + +// UserExtSpinLock 通过redis按照用户ID加锁 +func (l *Lock) UserExtSpinLock(userId uint32, lockTimeOut time.Duration) (bool, error) { + endTime := time.Now().Add(lockTimeOut) + for time.Now().Before(endTime) { + value, err := l.Lock.Setnx_NewOK(fmt.Sprintf(UserExtSyncLockFmt, userId), true, UserExtSyncLockExpire) + if err != nil { + return false, errors.New("redis未启动") + } + if value { + return true, nil + } + time.Sleep(time.Millisecond) + } + return false, errors.New("用户操作繁忙") +} + +// UserExtUnlock 按照用户ID释放锁 +func (l *Lock) UserExtUnlock(userId uint32) { + _, _ = l.Lock.Del(fmt.Sprintf(UserExtSyncLockFmt, userId)) +} + +// UserAccountLock 通过redis按照用户ID加锁 +func (l *Lock) UserAccountLock(userId uint32) bool { + v, e := l.Lock.Setnx_NewOK(fmt.Sprintf(UserAccountLockFmt, userId), true, UserExtSyncLockExpire) + if !v || e != nil { + return false + } + return true +} + +// UserAccountSpinLock 通过redis按照用户ID加锁 +func (l *Lock) UserAccountSpinLock(userId uint32, lockTimeOut time.Duration) (bool, error) { + endTime := time.Now().Add(lockTimeOut) + for time.Now().Before(endTime) { + value, err := l.Lock.Setnx_NewOK(fmt.Sprintf(UserAccountLockFmt, userId), true, UserExtSyncLockExpire) + if err != nil { + return false, errors.New("redis未启动") + } + if value { + return true, nil + } + time.Sleep(time.Millisecond) + } + return false, errors.New("用户操作繁忙") +} + +// UserAccountUnlock 按照用户ID释放锁 +func (l *Lock) UserAccountUnlock(userId uint32) { + _, _ = l.Lock.Del(fmt.Sprintf(UserAccountLockFmt, userId)) +} diff --git a/common/tf/tf.go b/common/tf/tf.go new file mode 100644 index 0000000..8fc2e9e --- /dev/null +++ b/common/tf/tf.go @@ -0,0 +1,73 @@ +package tf + +import ( + "fmt" + "net/http" + "strings" + + "91porn-server/common/log" + + "github.com/PuerkitoBio/goquery" +) + +// address: https://testflight.apple.com/join/whsnQnSb +func CheckTFValid(address string) (bool, string, error) { + req, err := http.NewRequest("GET", address, nil) + if err != nil { + log.Warn("CheckTFValid newRequest", log.Any("address", address), log.E(err)) + return false, "", err + } + req.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9") + req.Header.Add("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6") + req.Header.Add("Cache-Control", "no-cache") + req.Header.Add("Connection", "keep-alive") + req.Header.Add("Pragma", "no-cache") + req.Header.Add("Sec-Fetch-Dest", "document") + req.Header.Add("Sec-Fetch-Mode", "navigate") + req.Header.Add("Sec-Fetch-Site", "none") + req.Header.Add("Sec-Fetch-User", "?1") + req.Header.Add("Upgrade-Insecure-Requests", "1") + req.Header.Add("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Mobile/15E148 Safari/604.1") + req.Header.Add("Referer", "https://ipf.live/iosBeta") + res, err := http.DefaultClient.Do(req) + if err != nil { + log.Warn("CheckTFValid Client Do", log.Any("address", address), log.E(err)) + return false, "", err + } + defer func() { _ = res.Body.Close() }() + //不为200 肯定出问题了 + if res.StatusCode != http.StatusOK { + return false, fmt.Sprintf("HttpStatus:%d", res.StatusCode), nil + } + //状态为200,解析内容 + doc, err := goquery.NewDocumentFromReader(res.Body) + if err != nil { + log.Warn("CheckTFValid NewDocumentFromReader err", log.Any("address", address), log.E(err)) + return false, "", err + } + foundStartTesting := false + doc.Find("a").EachWithBreak(func(i int, s *goquery.Selection) bool { + text := strings.TrimSpace(s.Text()) + if text == "Start Testing" || text == "开始测试" { + foundStartTesting = true + return false + } + return true + }) + if foundStartTesting { + return true, "", nil //TF包有效 + } + betaStatus := "" + doc.Find(".beta-status").EachWithBreak(func(i int, s *goquery.Selection) bool { + betaStatus = strings.TrimSpace(s.Text()) + return false + }) + if betaStatus == "" { + return true, "", nil + } + if strings.Contains(betaStatus, "install TestFlight") || strings.Contains(betaStatus, "安装 TestFlight") { + return true, "", nil + } + //TODO: 这里还需要测试,目前来看,出现betaStatus就是无效的 + return false, betaStatus, nil +} diff --git a/common/tg/sendOpt.go b/common/tg/sendOpt.go new file mode 100644 index 0000000..c336edf --- /dev/null +++ b/common/tg/sendOpt.go @@ -0,0 +1,27 @@ +package tg + +type ParseMode string + +const ( + HTML ParseMode = "HTML" + MARKDOWN ParseMode = "Markdown" +) + +type SendOpt struct { + ParseMode *ParseMode +} + +func (s *SendOpt) SetParseMode(mdoe ParseMode) *SendOpt { + s.ParseMode = &mdoe + return s +} + +func mergeSendOpt(opts []*SendOpt) *SendOpt { + sendOpt := SendOpt{} + for _, v := range opts { + if v.ParseMode != nil { + sendOpt.ParseMode = v.ParseMode + } + } + return &sendOpt +} diff --git a/common/tg/tg.go b/common/tg/tg.go new file mode 100644 index 0000000..d77c319 --- /dev/null +++ b/common/tg/tg.go @@ -0,0 +1,76 @@ +package tg + +import ( + "fmt" + + "91porn-server/common/httputil" +) + +const ( + botApi = "https://api.telegram.org" + + TianTian_Bot = "1653674118:AAFXJVpLD2Vc0jHL59ZrqKAUbGRRr1vhUt8" //yinse_zabbix + + ChatIdServerTeam = -500730570 //服务器小组 + ChatIdRechargeTeam = -1001467392117 //业务监控告警群 + ChatIdOPSTeam = -392146367 //运维群 + ChatIdBILLTeam = -354751425 //支付报警群 + + getMe botMethod = "getMe" + sendMessage botMethod = "sendMessage" +) + +type botMethod = string + +type Bot struct { + token string + msgPayload string + sendRetry int +} + +type Msg struct { + ChatId int64 + Text string +} + +type Panic struct { + ChatId int64 + Err interface{} +} + +func New(token string, msgPayload string) *Bot { + return &Bot{token: token, msgPayload: msgPayload, sendRetry: 3} +} + +func (c *Bot) makeUrl(m botMethod) string { + return fmt.Sprintf("%s/bot%s/%s", botApi, c.token, m) +} + +func (c *Bot) Send(t Msg, opts ...*SendOpt) error { + req := map[string]interface{}{ + "chat_id": t.ChatId, + "text": c.msgPayload + t.Text, + } + opt := mergeSendOpt(opts) + if opt.ParseMode != nil { + if *opt.ParseMode == MARKDOWN { + req["text"] = "```" + c.msgPayload + "```" + t.Text + } + req["parse_mode"] = *opt.ParseMode + } + retry := c.sendRetry + url := c.makeUrl(sendMessage) + var err error + for retry != 0 { + retry-- + resp := struct { + Description string `json:"description"` + OK bool `json:"ok"` + ErrorCode int `json:"error_code"` + }{} + if _, err = httputil.DefaultClientPostJsonWithResp(&resp, url, nil, req); err == nil { + return nil + } + } + return err +} diff --git a/common/timeutil/timerange/timerange.go b/common/timeutil/timerange/timerange.go new file mode 100644 index 0000000..2911df4 --- /dev/null +++ b/common/timeutil/timerange/timerange.go @@ -0,0 +1,82 @@ +package timerange + +import ( + "math" + "time" +) + +type TimeRange struct { + Head time.Time + Tail time.Time +} + +func (t *TimeRange) HeadAddDay(d time.Duration) TimeRange { + return TimeRange{ + Head: t.Head.Add(d), + Tail: t.Tail, + } +} + +func (t *TimeRange) TailAddDay(d time.Duration) TimeRange { + return TimeRange{ + Head: t.Head, + Tail: t.Tail.Add(d), + } +} + +func (t *TimeRange) TotalDays() int64 { + return int64(math.Floor(float64(t.Tail.Sub(t.Head).Hours() / 24))) +} + +func (t *TimeRange) SplitByMinute(minute int64) []TimeRange { + count := int(t.Tail.Sub(t.Head)/time.Minute) / int(minute) + var split = make([]TimeRange, 0, count) + temp := t.Head + if t.Tail.After(temp) { + sub := temp.Add(time.Duration(minute) * time.Minute) + if sub.After(t.Tail) { //防止tail超出设定值 + sub = t.Tail + } + subRange := TimeRange{ //一个切片 + Head: temp, + Tail: sub, + } + split = append(split, subRange) + temp = sub + } + return split +} + +// LocDayRange 本地当日时间范围 +func LocDayRange(position time.Time) TimeRange { + head := time.Date(position.Year(), position.Month(), position.Day(), 0, 0, 0, 0, position.Location()).In(time.Local) + tail := head.AddDate(0, 0, 1) + return TimeRange{ + Head: head, + Tail: tail, + } +} + +// LocDayRange 本地当月时间范围 +func LocMonthRange(position time.Time) TimeRange { + head := time.Date(position.Year(), position.Month(), 1, 0, 0, 0, 0, position.Location()).In(time.Local) + tail := head.AddDate(0, 1, 0) + return TimeRange{ + Head: head, + Tail: tail, + } +} + +func RecentMinute(tim time.Time, scaleMinute int64) time.Time { + minute := int64(tim.Minute()) + lave := minute % scaleMinute + formatMinute := minute - lave + return time.Date(tim.Year(), tim.Month(), tim.Day(), tim.Hour(), int(formatMinute), 0, 0, tim.Location()) +} + +func RecentSecond(tim time.Time, scaleSecond int64) time.Time { + second := int64(tim.Second()) + lave := second % scaleSecond + formatSecond := second - lave + return time.Date(tim.Year(), tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), int(formatSecond), 0, tim.Location()) +} diff --git a/common/timeutil/timeslice/timeSlice.go b/common/timeutil/timeslice/timeSlice.go new file mode 100644 index 0000000..731a721 --- /dev/null +++ b/common/timeutil/timeslice/timeSlice.go @@ -0,0 +1,16 @@ +package timeslice + +import "time" + +// TimeSlice +type TimeSlice []time.Time + +func (t TimeSlice) Len() int { + return len(t) +} +func (t TimeSlice) Swap(i, j int) { + t[i], t[j] = t[j], t[i] +} +func (t TimeSlice) Less(i, j int) bool { + return t[i].Before(t[j]) +} diff --git a/common/timeutil/timeutil.go b/common/timeutil/timeutil.go new file mode 100644 index 0000000..1b7448f --- /dev/null +++ b/common/timeutil/timeutil.go @@ -0,0 +1,183 @@ +package timeutil + +import ( + "math" + "math/rand" + "strconv" + "time" + + "github.com/jinzhu/now" +) + +const ( + Day = time.Hour * 24 + + MinuteSeconds = 60 + HourSeconds = 60 * MinuteSeconds + DaySeconds = 24 * HourSeconds +) + +// MonthStartEndTime 返回这月1号的0点和下月1号的0点 +// 2006-02-02 12:34:56 => 2016-01-01 00:00:00, 2016-02-01 00:00:00 +func MonthStartEndTime(t time.Time) (time.Time, time.Time) { + start := now.New(t).BeginningOfMonth() + end := start.AddDate(0, 1, 0) + return start, end +} + +// Age 计算年龄 +func Age(birthday time.Time, t time.Time) int { + btm := birthday + now_ := t + if btm.After(now_) { + return 0 + } + age := now_.Year() - btm.Year() + if now_.Month() < btm.Month() { + age-- + } else if now_.Month() == btm.Month() { + if now_.Day() < btm.Day() { + age-- + } + } + return age +} + +// MidnoonOfDay 一天正午12点 +func MidnoonOfDay(t time.Time) time.Time { + return BeginningOfDay(t).Add(12 * time.Hour) +} + +func Now() time.Time { + return time.Now().Local() +} + +func BeginOfToday() time.Time { + t := Now() + return BeginOfTime(t) +} + +func BeginOfTime(t time.Time) time.Time { + year, month, day := t.Date() + return time.Date(year, month, day, 0, 0, 0, 0, t.Location()) +} + +// BeginningOfDay 一天的开始 +func BeginningOfDay(t time.Time) time.Time { + return now.New(t).BeginningOfDay() +} + +// BeginningOfTomorrow 明天的开始 +func BeginningOfTomorrow(t time.Time) time.Time { + return BeginningOfDay(t).AddDate(0, 0, 1) +} + +// BeginningOfMonth 月的开始 +func BeginningOfMonth(t time.Time) time.Time { + return now.New(t).BeginningOfMonth() +} + +// BeginningOfWeek 周的开始 当前时间的周一零点 +func BeginningOfWeek(t time.Time) time.Time { + return now.New(t.Add(-time.Hour * 24)).BeginningOfWeek().Add(+time.Hour * 24) +} + +// TailOfWeek 周的结束 当前时间的周一零点 +func TailOfWeek(t time.Time) time.Time { + + return now.New(t.Add(-time.Hour * 24)).BeginningOfWeek() +} + +// StrTimeToTime 时间字符串转时间 +func StrTimeToTime(strTime string) (t time.Time) { + timeLayout := "2006-01-02T15:04:05-07:00" + loc, _ := time.LoadLocation("Local") + t, _ = time.ParseInLocation(timeLayout, strTime, loc) + return +} + +// 多少分之前 +func BeforeMinute(min int) time.Time { + negativeM, _ := time.ParseDuration("-" + strconv.Itoa(min) + "m") + return time.Now().Add(negativeM) +} + +// 近几个月 返回时间的上两个月的月初 只支持向前12月 +func NearMonth(t time.Time, m time.Month) time.Time { + tM := t.Month() - m + if tM < 0 { + tM = 12 + tM + return time.Date(t.Year()-1, tM, 1, 0, 0, 0, 0, time.Local) + } + return time.Date(t.Year(), tM, 1, 0, 0, 0, 0, time.Local) +} + +// MillisecondConsistency 毫秒为000时毫秒+1保证毫米精度不丢失 +func MillisecondConsistency(s time.Time) time.Time { + timestamp := s.UnixNano() / 1e6 + if timestamp%1000 == 0 { + return time.Unix(0, (timestamp+1)*1e6) + } + return s +} + +// MonthStartEndTime 返回这月1号的0点和下月1号的0点 +// 2006-02-02 12:34:56 => 2016-01-01 00:00:00, 2016-02-01 00:00:00 +func EarlyLastMonth(t time.Time) (time.Time, time.Time) { + start := now.New(t).BeginningOfMonth() + end := start.AddDate(0, 1, 0) + return start, end +} + +// MonthStartEndTime 返回当天的0点和后一天的0点 +// 2006-02-02 12:34:56 => 2016-02-02 00:00:00, 2016-02-03 00:00:00 +func EarlyLastDay(t time.Time) (time.Time, time.Time) { + start := now.New(t).BeginningOfDay() + end := start.AddDate(0, 0, 1) + return start, end +} + +// MonthStartEndTime 返回前一天的0点和今天的0点 +// 2006-02-02 12:34:56 => 2016-02-02 00:00:00, 2016-02-03 00:00:00 +func YesterdayDay(t time.Time) (time.Time, time.Time) { + tmp := t.AddDate(0, 0, -1) + start := BeginningOfDay(tmp) + end := start.AddDate(0, 0, 1) + return start, end +} + +// 按指定的基础时间,指定的范围随机一个时间 向前随机一个时间 +func RandAddTime(baseTime time.Time, max time.Duration) time.Time { + sum := int64(rand.Intn(int(math.Ceil(max.Seconds())))) + return time.Unix(baseTime.Unix()+sum, 0) +} + +// 按指定的基础时间,指定的范围随机一个时间 向后随机一个时间 +func RandReduceTime(baseTime time.Time, max time.Duration) time.Time { + sum := int64(rand.Intn(int(math.Ceil(max.Seconds())))) + return time.Unix(baseTime.Unix()-sum, 0) +} + +// 根据月份和年返回当月0点和下月0点 +func GetTimeByMonthAndYear(m int, y int) (time.Time, time.Time) { + t := time.Date(y, time.Month(m), 1, 0, 0, 0, 0, time.Local) + nextM := t.AddDate(0, 1, 0) + return t, nextM +} + +// 增加时间, 年、月、日 +func AddTimeByCombo(tm *time.Time, typ string, duration int) *time.Time { + if tm == nil { + return tm + } + date := time.Time{} + switch typ { + case "day": + date = tm.AddDate(0, 0, duration) + case "month": + date = tm.AddDate(0, duration, 0) + case "year": + date = tm.AddDate(duration, 0, 0) + } + return &date +} diff --git a/common/top/asistant/asistant.go b/common/top/asistant/asistant.go new file mode 100644 index 0000000..f487690 --- /dev/null +++ b/common/top/asistant/asistant.go @@ -0,0 +1,54 @@ +package topasist + +import ( + topser "91porn-server/common/top" + "91porn-server/common/top/dailytop" + "91porn-server/common/top/monthtop" + "91porn-server/common/top/weektop" + "91porn-server/common/top/yeartop" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Incr 增加 +func Incr(tp topser.TopName, id string, count float64) { + // 如果是一个空的id + if primitive.NilObjectID.Hex() == id { + return + } + // 日榜 + dailytop.Incr(tp, id, count) + // 周榜 + weektop.Incr(tp, id, count) + // 月榜 + monthtop.Incr(tp, id, count) + // 年榜 + yeartop.Incr(tp, id, count) +} + +// Decr 减少 +func Decr(tp topser.TopName, id string, count float64) { + // 如果是一个空的id + if primitive.NilObjectID.Hex() == id { + return + } + // 日榜 + dailytop.Decr(tp, id, count) + // 周榜 + weektop.Decr(tp, id, count) + // 月榜 + monthtop.Decr(tp, id, count) + // 年榜 + yeartop.Decr(tp, id, count) +} + +// Remove 移除排行榜 +func Remove(tp topser.TopName, id string) { + // 日榜 + dailytop.Remove(tp, id) + // 周榜 + weektop.Remove(tp, id) + // 月榜 + monthtop.Remove(tp, id) + // 年榜 + yeartop.Remove(tp, id) +} diff --git a/common/top/dailytop/grow.go b/common/top/dailytop/grow.go new file mode 100644 index 0000000..6f7539e --- /dev/null +++ b/common/top/dailytop/grow.go @@ -0,0 +1,113 @@ +package dailytop + +import ( + "91porn-server/common/log" + "91porn-server/common/redis" + services "91porn-server/common/top" + "fmt" + "time" +) + +// getCacheKeys 获取需要操作的key +func getCacheKeys(tp services.TopName) []string { + t := time.Now().Unix() + d := t / 86400 + var keys []string + var i int64 + for i = 0; i < 2; i++ { + keys = append(keys, fmt.Sprintf("daliy_top_%s_%d", tp, d-i)) + } + + return keys +} + +// Incr 增加 +func Incr(tp services.TopName, id string, count float64) { + keys := getCacheKeys(tp) + for _, key := range keys { + if !redis.Handler.Exists(key) { + _, _ = redis.Handler.ZIncrBy(key, count, id) + _, _ = redis.Handler.ExpireKey(key, time.Hour*24*3) + continue + } + _, _ = redis.Handler.ZIncrBy(key, count, id) + } +} + +// Decr 减少 +func Decr(tp services.TopName, id string, count float64) { + keys := getCacheKeys(tp) + for _, key := range keys { + if !redis.Handler.Exists(key) { + _, _ = redis.Handler.ZIncrBy(key, -count, id) + _, _ = redis.Handler.ExpireKey(key, time.Hour*24*3) + continue + } + _, _ = redis.Handler.ZIncrBy(key, -count, id) + } +} + +// Remove 移除 +func Remove(tp services.TopName, id string) { + keys := getCacheKeys(tp) + for _, key := range keys { + log.Info("Remove key", log.Any("key", key), log.Any("id", id)) + redis.Handler.ZRem(key, id) + } +} + +// GetTop 获取排行榜头部 +func GetTop(tp services.TopName, count int64) ([]string, map[string]int64) { + keys := getCacheKeys(tp) + key := keys[len(keys)-1] + srt, topVal := redis.Handler.ZRevRangeWithScores2(key, 0, count) + var ids []string + scores := make(map[string]int64) + for _, id := range srt { + score, ok := topVal[id] + if !ok { + continue + } + + ids = append(ids, id) + scores[id] = int64(score) + } + + return ids, scores +} + +// GetTopByPage 获取排行榜头部(改成获取前一天的) +func GetTopByPage(tp services.TopName, skip, size int64) ([]string, map[string]int64, bool) { + keys := getCacheKeys(tp) + k := len(keys) - 2 // 获取倒数第二个,也就是前一天 + if k < 0 { + return []string{}, map[string]int64{}, false + } + key := keys[k] + hasNext := false + count, err := redis.Handler.ZCard(key) + if err != nil { + return nil, nil, false + } + if skip >= count { + return nil, nil, false + } + if count > skip+size { + hasNext = true + } + + srt, topVal := redis.Handler.ZRevRangeWithScores2(key, skip, size) + var ids []string + scores := make(map[string]int64) + for _, id := range srt { + score, ok := topVal[id] + if !ok { + continue + } + + ids = append(ids, id) + scores[id] = int64(score) + } + + return ids, scores, hasNext +} diff --git a/common/top/monthtop/grow.go b/common/top/monthtop/grow.go new file mode 100644 index 0000000..fdfcd8c --- /dev/null +++ b/common/top/monthtop/grow.go @@ -0,0 +1,109 @@ +package monthtop + +import ( + "91porn-server/common/log" + "91porn-server/common/redis" + services "91porn-server/common/top" + "fmt" + "time" +) + +// getCacheKeys 获取需要操作的key +func getCacheKeys(tp services.TopName) []string { + t := time.Now().Unix() + m := t / 2592000 + var keys []string + var i int64 + for i = 0; i < 2; i++ { + keys = append(keys, fmt.Sprintf("month_top_%s_%d", tp, m-i)) + } + + return keys +} + +// Incr 增加 +func Incr(tp services.TopName, id string, count float64) { + keys := getCacheKeys(tp) + for _, key := range keys { + if !redis.Handler.Exists(key) { + _, _ = redis.Handler.ZIncrBy(key, count, id) + _, _ = redis.Handler.ExpireKey(key, time.Hour*24*61) + continue + } + _, _ = redis.Handler.ZIncrBy(key, count, id) + } +} + +// Decr 减少 +func Decr(tp services.TopName, id string, count float64) { + keys := getCacheKeys(tp) + for _, key := range keys { + if !redis.Handler.Exists(key) { + _, _ = redis.Handler.ZIncrBy(key, -count, id) + _, _ = redis.Handler.ExpireKey(key, time.Hour*24*61) + continue + } + _, _ = redis.Handler.ZIncrBy(key, -count, id) + } +} + +// Remove 移除 +func Remove(tp services.TopName, id string) { + keys := getCacheKeys(tp) + for _, key := range keys { + log.Info("Remove key", log.Any("key", key), log.Any("id", id)) + redis.Handler.ZRem(key, id) + } +} + +// GetTop 获取排行榜头部 +func GetTop(tp services.TopName, count int64) ([]string, map[string]int64) { + keys := getCacheKeys(tp) + key := keys[len(keys)-1] + srt, topVal := redis.Handler.ZRevRangeWithScores2(key, 0, count) + var ids []string + scores := make(map[string]int64) + for _, id := range srt { + score, ok := topVal[id] + if !ok { + continue + } + + ids = append(ids, id) + scores[id] = int64(score) + } + + return ids, scores +} + +// GetTopByPage 获取排行榜头部 +func GetTopByPage(tp services.TopName, skip, size int64) ([]string, map[string]int64, bool) { + keys := getCacheKeys(tp) + key := keys[len(keys)-1] + hasNext := false + count, err := redis.Handler.ZCard(key) + if err != nil { + return nil, nil, false + } + if skip >= count { + return nil, nil, false + } + if count > skip+size { + hasNext = true + } + + srt, topVal := redis.Handler.ZRevRangeWithScores2(key, skip, size) + var ids []string + scores := make(map[string]int64) + for _, id := range srt { + score, ok := topVal[id] + if !ok { + continue + } + + ids = append(ids, id) + scores[id] = int64(score) + } + + return ids, scores, hasNext +} diff --git a/common/top/top.go b/common/top/top.go new file mode 100644 index 0000000..940cf6e --- /dev/null +++ b/common/top/top.go @@ -0,0 +1,24 @@ +package topser + +import "fmt" + +type TopName string + +const ( + //TypeMovie TopName = "movie" + //TypePics TopName = "pics" + //TypePost TopName = "post" + //TypeCollections TopName = "collections" + //TypeActress TopName = "actress" + typeMedia = "media:%v" + typeVideo = "video:%v" + TypeRecommendUser TopName = "RecommendUser" +) + +func TypeMedia(mediaType string) TopName { + return TopName(fmt.Sprintf(typeMedia, mediaType)) +} + +func TypeVideo(newsType string) TopName { + return TopName(fmt.Sprintf(typeVideo, newsType)) +} diff --git a/common/top/weektop/grow.go b/common/top/weektop/grow.go new file mode 100644 index 0000000..10d9c0a --- /dev/null +++ b/common/top/weektop/grow.go @@ -0,0 +1,102 @@ +package weektop + +import ( + "91porn-server/common/log" + "91porn-server/common/redis" + services "91porn-server/common/top" + "fmt" + "time" +) + +// getCacheKeys 获取需要操作的key +func getCacheKeys(tp services.TopName) []string { + t := time.Now().Unix() + d := t / 86400 + var keys []string + var i int64 + for i = 0; i < 7; i++ { + keys = append(keys, fmt.Sprintf("week_top_%s_%d", tp, d-i)) + } + + return keys +} + +// Incr 增加 +func Incr(tp services.TopName, id string, count float64) { + keys := getCacheKeys(tp) + for _, key := range keys { + if !redis.Handler.Exists(key) { + _, _ = redis.Handler.ZIncrBy(key, count, id) + _, _ = redis.Handler.ExpireKey(key, time.Hour*24*8) + continue + } + _, _ = redis.Handler.ZIncrBy(key, count, id) + } +} + +// Decr 减少 +func Decr(tp services.TopName, id string, count float64) { + keys := getCacheKeys(tp) + for _, key := range keys { + if !redis.Handler.Exists(key) { + _, _ = redis.Handler.ZIncrBy(key, -count, id) + _, _ = redis.Handler.ExpireKey(key, time.Hour*24*8) + continue + } + _, _ = redis.Handler.ZIncrBy(key, -count, id) + } +} + +// Remove 移除 +func Remove(tp services.TopName, id string) { + keys := getCacheKeys(tp) + for _, key := range keys { + log.Info("Remove key", log.Any("key", key), log.Any("id", id)) + redis.Handler.ZRem(key, id) + } +} + +// GetTop 获取排行榜头部 +func GetTop(tp services.TopName, count int64) ([]string, map[string]int64) { + keys := getCacheKeys(tp) + key := keys[len(keys)-1] + srt, topVal := redis.Handler.ZRevRangeWithScores2(key, 0, count) + var ids []string + scores := make(map[string]int64) + for _, id := range srt { + score, ok := topVal[id] + if !ok { + continue + } + + ids = append(ids, id) + scores[id] = int64(score) + } + + return ids, scores +} + +// GetTopByPage 获取排行榜头部 +func GetTopByPage(tp services.TopName, skip, size int64) ([]string, map[string]int64, bool) { + keys := getCacheKeys(tp) + key := keys[len(keys)-1] + hasNext := false + srt, topVal := redis.Handler.ZRevRangeWithScores2(key, skip, size+1) + var ids []string + if len(srt) > int(size) { + hasNext = true + srt = srt[:size] + } + scores := make(map[string]int64) + for _, id := range srt { + score, ok := topVal[id] + if !ok { + continue + } + + ids = append(ids, id) + scores[id] = int64(score) + } + + return ids, scores, hasNext +} diff --git a/common/top/yeartop/grow.go b/common/top/yeartop/grow.go new file mode 100644 index 0000000..72ee1ea --- /dev/null +++ b/common/top/yeartop/grow.go @@ -0,0 +1,106 @@ +package yeartop + +import ( + "91porn-server/common/log" + "91porn-server/common/redis" + services "91porn-server/common/top" + "fmt" + "time" +) + +// getCacheKeys 获取需要操作的key +func getCacheKeys(tp services.TopName) []string { + y := time.Now().Year() + var keys []string + var i int + for i = 0; i < 2; i++ { + keys = append(keys, fmt.Sprintf("year_top_%s_%d", tp, y-i)) + } + + return keys +} + +// Incr 增加 +func Incr(tp services.TopName, id string, count float64) { + keys := getCacheKeys(tp) + for _, key := range keys { + if !redis.Handler.Exists(key) { + _, _ = redis.Handler.ZIncrBy(key, count, id) + continue + } + _, _ = redis.Handler.ZIncrBy(key, count, id) + } +} + +// Decr 减少 +func Decr(tp services.TopName, id string, count float64) { + keys := getCacheKeys(tp) + for _, key := range keys { + if !redis.Handler.Exists(key) { + _, _ = redis.Handler.ZIncrBy(key, -count, id) + continue + } + _, _ = redis.Handler.ZIncrBy(key, -count, id) + } +} + +// Remove 移除 +func Remove(tp services.TopName, id string) { + keys := getCacheKeys(tp) + for _, key := range keys { + log.Info("Remove key", log.Any("key", key), log.Any("id", id)) + redis.Handler.ZRem(key, id) + } +} + +// GetTop 获取排行榜头部 +func GetTop(tp services.TopName, count int64) ([]string, map[string]int64) { + keys := getCacheKeys(tp) + key := keys[len(keys)-1] + srt, topVal := redis.Handler.ZRevRangeWithScores2(key, 0, count) + var ids []string + scores := make(map[string]int64) + for _, id := range srt { + score, ok := topVal[id] + if !ok { + continue + } + + ids = append(ids, id) + scores[id] = int64(score) + } + + return ids, scores +} + +// GetTopByPage 获取排行榜头部 +func GetTopByPage(tp services.TopName, skip, size int64) ([]string, map[string]int64, bool) { + keys := getCacheKeys(tp) + key := keys[len(keys)-1] + hasNext := false + count, err := redis.Handler.ZCard(key) + if err != nil { + return nil, nil, false + } + if skip >= count { + return nil, nil, false + } + if count > skip+size { + hasNext = true + } + + srt, topVal := redis.Handler.ZRevRangeWithScores2(key, skip, size) + var ids []string + scores := make(map[string]int64) + for _, id := range srt { + score, ok := topVal[id] + if !ok { + continue + } + + ids = append(ids, id) + scores[id] = int64(score) + } + + return ids, scores, hasNext +} diff --git a/common/truthutil/truthutil.go b/common/truthutil/truthutil.go new file mode 100644 index 0000000..8cb9f3e --- /dev/null +++ b/common/truthutil/truthutil.go @@ -0,0 +1,359 @@ +package truthutil + +import ( + "encoding/json" + "fmt" + "regexp" + "strings" + "time" + "unicode" + + "github.com/mozillazg/go-pinyin" + + "91porn-server/app/appg" + "91porn-server/common/constant/redisconst" + "91porn-server/common/localcache" + "91porn-server/common/log" + "91porn-server/models/v/filtermod" +) + +// CheckIsValid 判断评论、私信等内容是否合法 +func CheckIsValid(a string, checkType int) bool { + // 忽略emo表情 参考:https://apps.timwhitlock.info/emoji/tables/unicode#block-6c-other-additional-symbols + reg := regexp.MustCompile(`[\x{1F600}-\x{1F64F}\x{1F444}-\x{1F445}\x{1F488}-\x{1F48B}]+`) + a = reg.ReplaceAllString(a, "") + + // 检查白名单 + if valid := checkWhiteListChars(a); !valid { + return false + } + // 检查后台敏感词 + if valid := checkSensitiveWords(a, checkType); !valid { + return false + } + // 检查url和数字 + if valid := checkURIAndNumber(a); !valid { + return false + } + // 检查评论频率 + if valid := checkRepeat(a); !valid { + return false + } + + return true +} + +// 仅允许常规字符 +func checkWhiteListChars(s string) bool { + // 全符号文本 + cv, err := NewCommentValidator(`^[\d\p{P}\p{S}\s]*$`) + if err != nil { + return false + } + valid := cv.Validate(s) + if valid { + return false + } + + cv, err = NewCommentValidator("^[A-Za-z0-9*!,.?,。?!、 \u4e00-\u9fa5]+$") + if err != nil { + log.Error(fmt.Sprintf("checkWhiteListChars Invalid pattern err:%v", err)) + return false + } + valid = cv.Validate(s) + if !valid { + return false + } + + return true +} + +func checkSensitiveWords(a string, checkType int) bool { + // 获取缓存数据 + str, err := appg.Redis.Get(redisconst.SensitiveWordsCache) + if err != nil { + log.Warn(fmt.Sprintf("redis get SensitiveWords:%v", err)) + } + + var sensitiveWords []string + + if str != nil { + if err = json.Unmarshal([]byte(*str), &sensitiveWords); err != nil { + log.Warn(fmt.Sprintf("redis get SensitiveWords parse err:%v", err)) + } + } else { + words, err := filtermod.GetAllFilterWords() + if err != nil || len(words) <= 0 { + return true + } + + for _, w := range words { + sensitiveWords = append(sensitiveWords, w.Word) + } + + if len(sensitiveWords) > 0 { + // 放入缓存 + bytes, _ := json.Marshal(sensitiveWords) + err := appg.Redis.Set(redisconst.SensitiveWordsCache, bytes, 10*time.Minute) + if err != nil { + log.Warn(fmt.Sprintf("redis set SensitiveWords err:%v", err)) + } + } + } + + a = strings.ReplaceAll(a, " ", "") + a = strings.ReplaceAll(a, "\n", "") + a = strings.ToUpper(a) + for _, s := range sensitiveWords { + filterStr := strings.ReplaceAll(s, " ", "") + filterStr = strings.ReplaceAll(filterStr, "\n", "") + filterStr = strings.ToUpper(filterStr) + if checkType == 0 { + // 个人用户名,简介等 + if strings.Contains(a, filterStr) { + return false + } + } else { + // 用于评论 + // 2026-3 改为精确匹配 + if a == filterStr { + return false + } + } + } + + return true +} + +// checkURIAndNumber 检查qq号或网址 +func checkURIAndNumber(a string) bool { + a = strings.ReplaceAll(a, "匚", "c") + a = strings.ReplaceAll(a, "匸", "c") + a = strings.ReplaceAll(a, "丅", "t") + a = strings.ReplaceAll(a, "卩", "p") + a = strings.ReplaceAll(a, "卪", "p") + a = strings.ReplaceAll(a, "讠", "i") + a = strings.ReplaceAll(a, "丶", ".") + a = strings.ReplaceAll(a, "、", ".") + a = strings.ReplaceAll(a, ",", ".") + a = strings.ReplaceAll(a, ",", ".") + a = strings.ReplaceAll(a, "。", ".") + a = strings.ReplaceAll(a, "俛", "免") + eRegexp := regexp.MustCompile("[^\u4e00-\u9fa50-9a-zA-Z.。+]+") + effective := eRegexp.ReplaceAllString(a, "") + + ra := []rune(effective) + final := "" + + args := pinyin.NewArgs() + args.Fallback = func(r rune, a pinyin.Args) []string { + return []string{string(r)} + } + + pys := pinyin.LazyConvert(effective, &args) + numMap := make(map[int]bool) + var diffNumCount int + var haveJia bool + var haveWei bool + var yue bool + var pao bool + var wang bool + var mian bool + var fei bool + for i, py := range pys { + c := string(ra[i]) + n := -1 + switch py { + case "ling", "ning", "lin", "nin", "O", "0": + n = 0 + if py == "0" || py == "o" || py == "O" { + c = "o" + } else { + c = "0" + } + case "yi", "1": + n = 1 + if py == "yi" { + c = "e" + } + case "er", "2": + n = 2 + c = "2" + case "san", "sa", "shan", "3": + n = 3 + c = "3" + case "si", "shi", "4": + n = 4 + c = "4" + case "wu", "5": + n = 5 + c = "5" + case "liu", "lu", "niu", "nu", "6": + n = 6 + c = "6" + case "qi", "7": + n = 7 + c = "7" + case "ba", "pa", "8": + n = 8 + c = "8" + case "jiu", "9": + n = 9 + c = "9" + case "dian": + c = "." + // "xi" 是常见汉字读音,不转成 "c",避免“嘻嘻”等正常内容被误判为 .cc 域名。 + case "se": + c = "c" + case "ti": + c = "t" + case "en", "eng": + c = "n" + case "ou": + c = "o" + case "pi": + c = "p" + case "jia", "+": + haveJia = true + case "wei", "v", "V", "w", "W", "qun", "qiong", "kou", "qiu", "q", "Q": + haveWei = true + case "wang": + wang = true + case "yue", "yo": + yue = true + case "pao", "bao": + pao = true + case "mian": + mian = true + case "fei": + fei = true + } + + if n >= 0 { + if _, ok := numMap[n]; !ok { + numMap[n] = true + diffNumCount++ + } + } + + final += c + } + + lRegexp := regexp.MustCompile(`[^a-zA-Z0-57-9]+`) + letter := lRegexp.ReplaceAllString(final, "") + //fmt.Println(effective) + //fmt.Println(final) + //fmt.Println(alpha) + // 数字检查 + if diffNumCount >= 4 || len(letter) >= 5 { + return false + } + if haveJia && haveWei { + return false + } + + alphaRegexp := regexp.MustCompile(`[^a-zA-Z0-9.]+`) + alpha := alphaRegexp.ReplaceAllString(final, "") + // 网址检查 + if r := regexp.MustCompile(`(?i)\w*(aaa|abb|abc|ac|aco|ad|ads|ae|aeg|af|afl|ag|ai|aig|al|am|anz|ao|aol|app|aq|ar|art|as|at|au|aw|aws|ax|axa|az|ba|bar|bb|bbc|bbt|bcg|bcn|bd|be|bet|bf|bg|bh|bi|bid|bio|biz|bj|bm|bms|bmw|bn|bo|bom|boo|bot|box|br|bs|bt|buy|bv|bw|by|bz|bzh|ca|cab|cal|cam|car|cat|cba|cbn|cc|cd|ceo|cf|cfa|cfd|cg|ch|ci|ck|cl|cm|cn|co|com|cpa|cr|crs|cu|cv|cw|cx|cy|cz|dad|day|dds|de|dev|dhl|diy|dj|dk|dm|dnp|do|dog|dot|dtv|dvr|dz|eat|ec|eco|edu|ee|eg|er|es|esq|et|eu|eus|fan|fi|fit|fj|fk|fly|fm|fo|foo|fox|fr|frl|ftr|fun|fyi|hbo|hiv|hk|hkt|hm|hn|hot|how|hr|ht|hu|ibm|ice|icu|id|ie|ifm|il|im|in|inc|ing|ink|int|io|iq|ir|is|ist|it|itv|jcb|je|jio|jll|jm|jmp|jnj|jo|jot|joy|jp|ke|kfh|kg|kh|ki|kia|kim|km|kn|kp|kpn|kr|krd|kw|ky|kz|la|lat|law|lb|lc|lds|li|lk|llc|llp|lol|lpl|lr|ls|lt|ltd|lu|lv|ly|ma|man|map|mba|mc|md|me|med|men|mg|mh|mil|mit|mk|ml|mlb|mls|mm|mma|mn|mo|moe|moi|mom|mov|mp|mq|mr|ms|msd|mt|mtn|mtr|mu|mv|mw|mx|my|mz|na|nab|nba|nc|ne|nec|net|new|nf|nfl|ng|ngo|nhk|ni|nl|no|now|np|nr|nra|nrw|ntt|nu|nyc|nz|obi|om|one|ong|onl|ooo|org|ott|ovh|pa|pay|pe|pet|pf|pg|ph|phd|pid|pin|pk|pl|pm|pn|pnc|pr|pro|pru|ps|pt|pub|pw|pwc|py|qa|re|red|ren|ril|rio|rip|ro|rs|ru|run|rw|rwe|sa|sap|sas|sb|sbi|sbs|sc|scb|sd|se|sew|sex|sfr|sg|sh|si|sj|sk|ski|sky|sl|sm|sn|so|soy|spa|sr|srl|ss|st|stc|su|sv|sx|sy|sz|tab|tax|tc|tci|td|tdk|tel|tf|tg|th|thd|tj|tjx|tk|tl|tm|tn|to|top|tr|trv|tt|tui|tv|tvs|tw|tz|ua|ubs|ug|uk|uno|uol|ups|us|uy|uz|va|vc|ve|vet|vg|vi|vig|vin|vip|vn|vu|wed|wf|win|wme|wow|ws|wtc|wtf|xin|xxx|xyz|ye|you|yt|yun|za|zip|zm|zw)`); r != nil { + // 匹配成功 + if r.MatchString(alpha) { + return false + } + } + if r := regexp.MustCompile(`(?i)\w+\.[0-1a-z]{2,}`); r != nil { + // 匹配成功 + if r.MatchString(alpha) { + return false + } + } + + // 综合检查 + if (wang || yue || pao || (mian && fei)) && len(letter) >= 3 { + return false + } + + return true +} + +// checkRepeat 30分钟内超过5条一样的 拉入12小时黑名单 +func checkRepeat(a string) bool { + numRegexp := regexp.MustCompile("[^\u4e00-\u9fa5a-zA-Z]+") + a = numRegexp.ReplaceAllString(a, "") + + var mLimit, cLimit = 30, 5 + var black = 12 + var hasHan bool + //var hasAlpha bool + rmap := make(map[int32]bool) + var rc int + for _, r := range a { + if unicode.Is(unicode.Han, r) { + hasHan = true + } + //if unicode.IsDigit(r) || isEnLetter(r) { + // hasAlpha = true + //} + if _, ok := rmap[r]; !ok { + rmap[r] = true + rc++ + } + } + + // 必须包含汉字并且字母或数字 并且5个以上不重复字符 + if !hasHan || rc < 5 { + return true + } + + // 记录评论map + r := []rune(a) + if len(r) > 30 { + s1 := r[:30] + a = string(s1) + } + + bKey := "cmt-b-" + a + key := "cmt-" + a + // 查看黑名单 + _, ok := localcache.C.Get(bKey) + if ok { + return false + } + // 记录次数 + c, ok2 := localcache.C.Get(key) + if !ok2 { + localcache.C.Set(key, 1, time.Duration(mLimit)*time.Minute) + } else { + // 超过次数加入黑名单 + ci := c.(int) + if ci+1 >= cLimit { + localcache.C.Set(bKey, true, time.Duration(black)*time.Hour) + localcache.C.Delete(key) + return false + } + _ = localcache.C.Increment(key, 1) + } + + return true +} + +// 判断英文字母 +func isEnLetter(s rune) bool { + return (s >= 97 && s <= 122) || (s >= 65 && s <= 90) +} + +type CommentValidator struct { + pattern *regexp.Regexp +} + +func NewCommentValidator(pattern string) (*CommentValidator, error) { + re, err := regexp.Compile(pattern) + if err != nil { + return nil, err + } + return &CommentValidator{pattern: re}, nil +} + +func (cv *CommentValidator) Validate(comment string) bool { + return cv.pattern.MatchString(comment) +} diff --git a/common/truthutil/truthutil_test.go b/common/truthutil/truthutil_test.go new file mode 100644 index 0000000..f143c63 --- /dev/null +++ b/common/truthutil/truthutil_test.go @@ -0,0 +1,38 @@ +package truthutil + +import "testing" + +func TestCheckURIAndNumberAllowsXiGreeting(t *testing.T) { + t.Parallel() + + for _, content := range []string{ + "嘻嘻", + "嘻嘻,我在这里", + "西西", + } { + content := content + t.Run(content, func(t *testing.T) { + t.Parallel() + if !checkURIAndNumber(content) { + t.Fatalf("checkURIAndNumber(%q) = false, want true", content) + } + }) + } +} + +func TestCheckURIAndNumberStillRejectsURLs(t *testing.T) { + t.Parallel() + + for _, content := range []string{ + "abc.com", + "abc点com", + } { + content := content + t.Run(content, func(t *testing.T) { + t.Parallel() + if checkURIAndNumber(content) { + t.Fatalf("checkURIAndNumber(%q) = true, want false", content) + } + }) + } +} diff --git a/common/typeConv.go b/common/typeConv.go new file mode 100644 index 0000000..98a6879 --- /dev/null +++ b/common/typeConv.go @@ -0,0 +1,102 @@ +package common + +import ( + "fmt" + "reflect" + "unicode" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// StructToMap2 对象转map,去掉无效值 +func StructToMap2(u interface{}) []bson.M { + t := reflect.TypeOf(u) + v := reflect.ValueOf(u) + p := make([]bson.M, 0) + for i := 0; i < t.NumField(); i++ { + fv := v.Field(i).Type() + vv := v.Field(i) + k, _ := t.Field(i).Tag.Lookup("bson") + switch fv.Kind() { + case reflect.String: + if vv.String() != "" { + if k == "_id" { + idv, _ := primitive.ObjectIDFromHex(vv.String()) + p = append(p, bson.M{"$match": bson.M{k: idv}}) + } else { + p = append(p, bson.M{"$match": bson.M{k: vv.String()}}) + } + fmt.Println(vv.String()) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if vv.Int() != 0 { + p = append(p, bson.M{"$match": bson.M{k: vv.Interface()}}) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + if vv.Uint() != 0 { + p = append(p, bson.M{"$match": bson.M{k: vv.Interface()}}) + } + case reflect.Interface, reflect.Ptr: + if vv.Uint() != 0 { + p = append(p, bson.M{"$match": bson.M{k: vv.Interface()}}) + } + } + } + return p +} + +// StructToMap 对象转map,去掉无效值 +func StructToMap(u interface{}) (m map[string]interface{}) { + t := reflect.TypeOf(u) + v := reflect.ValueOf(u) + p := make(map[string]interface{}) + for i := 0; i < t.NumField(); i++ { + fv := v.Field(i) + ft := t.Field(i) + jsonTag, _ := ft.Tag.Lookup("json") + switch fv.Kind() { + case reflect.String: + if fv.String() != "" { + p[jsonTag] = fv.Interface() + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if fv.Int() != 0 { + p[jsonTag] = fv.Interface() + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if fv.Uint() != 0 { + p[jsonTag] = fv.Interface() + } + case reflect.Float32, reflect.Float64: + if fv.Float() != 0 { + p[jsonTag] = fv.Interface() + } + default: + p[jsonTag] = fv.Interface() + + } + } + return p +} + +// Ucfirst () 字符串首字母大写 +func Ucfirst(str string) string { + for i, v := range str { + return string(unicode.ToUpper(v)) + str[i+1:] + } + return "" +} + +// Lcfirst () 字符串首字母小写 +func Lcfirst(str string) string { + for i, v := range str { + return string(unicode.ToLower(v)) + str[i+1:] + } + return "" +} + +// MobileBlurry () +func MobileBlurry(str string) string { + return str[0:3] + "****" + str[7:] +} diff --git a/common/usertruth/scode.jpg b/common/usertruth/scode.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4e639e791286515951b7520206dba41596554b93 GIT binary patch literal 16075 zcmeHu2Urtpx9)_{6cmw;LQqf;M5RfUfC`~Vm)=A`nt(Lv26bZz9R-vwMLLL7rGs>k zBE2`Mf|Sq#N$z0Vz%Kv4&pG$r^E~Io=ab2NnX}W@0>koQ3TiJIl$ltjCXY z2%b56frp=;AI2_xNr+eM93MaL&PgB#4jdpSBd0%fh@SVv@e{oN_=7J8C`lm+1Q(!? zlK=rF1WF0Pe*j>hfJBgQAK=FaLI5QsA|@d{aF7iALEaI700M;)5JHKF2noSodxG-- zAtlk#6Fic{R4PU!C+(6lbe@cQ26eBWz~o3n%cVhhW1Y#on75My?vu&;}f4Jr>19SmzG!1 zt842Uo0uKBAOQ5cSm6KP1=}YVB`6mGAt98IWJfLtfipOul!Qblc!-Zms*o7jQJv(y zPfC3`=v8*b0aiZMMVg!TZ3k)D_(xAI?TGeGvVTvo2mc|-ehK!wTzvpJ6ap#_N(sP$ zbqq(G*TMgGBR%8pGNxfjn49?{>HS-UMa9DYgiv8l23fWdr!g-9^GpUYqqOFm7_XfC z2V|n|t~R3~hD{Fa+;1kVD@S}EDB4wD9IpwFN_?*48`>4DGxW6U414%bgypX$q} z9+P#Zp(ngw5IgV=6QF}!%P8AkDaHfk!etxbK#`OxuCo9SL_5c9kO<-dD#X8-zZRyG z^ME~6#p$?tV$@K(GWB6uc3T23evVYbzI;5e@^J+Z0F!uN%zW>8CFsom`7bOU>g4d| zO6W7tT;U~?wTILZ`js4}m@tQ4{?D#YnC@F|Xye+8j<>PrEjWl?c##aw$^!1;|JmjF z!}2LB+-#S8uA2`81(}_hI8M3lPKk2<7i;{7lkC&cHBXxcK|k0$XM28Km5v-){(N z)R0&I)NKABl3A2xlUz2XBb({jWb0G)P$2LkngRAA2@hEIx{HcSf!fe6C5D7-=e|VyQaUlVLya7SWc}iqZ{sos_ zQX!Ifz>RXVl@P5W1yJuBG|w`o&t&99T$*pr&I@damhroP`^M3?tON+5ao?^yYDkOT zh8$UaQ_igBms47y^L+HWS=?`n79*W1dJ9+|Dwo|y&s+^saB}C)>vX_+GBW!z z^+ktIc0%*|m#85TiRLqjB~8uF4z3RN4ec#<1rFBqD!-{K18AmZ`9x%GWX$Rc9Ovg& zbUim~W91pH)C>=_Wr0o^hXOdjk-nxCayBVOeX1MXQ?Qv)he--o(muAjhxEJ<5lt@x++#Q^WR+Om&G zx$NJUL!(j_&#RkcMJuySTDaGV zKjt{|nx8vs(mIF=S;_jgd9xDv+C**{wTz>3em_5rK~4K5xKjSxCpie)qaVCK${qw5 z*2mISB#5ZL51OHN!HyFpr;6+1f=>a9SDv$r;{g^dD8>&d^T;?>xp`+RZ#khR>AV|` z&~p(S<{J**eG$1@99#g7*@R&@MT)McBga?N@IY1&9*Bzvt#zN~zgvR#`-OLAqw}2B z(p$~T#)M-WjX5`{yhuK!l&_xj;!jRjKzvwpyA^l)^Mjc+S80oQ300Ut!|5-9idwMu zhegNI1pRXBKD<>pG@wyoYM#l+^LD&Z=*A1SXVEXAmvSVSPN}w+jek_aY{uXL=@LZZ z9ObN^9jrH#+lKqa**AKekM*lK@2N@>x-r+bC+z+ypdW|g0RD2kqoDDSZWm)sMI;UP z8!GSzKCLLY%1NEW@p1C_X9XiXQ2BLn3JYCDZmrVcf#VtRkJAL2>GQqN@41@cqs|)+ zxcitgEXD^9_^aRnva+feG({&K_}XE(ZJ#z9f(L@IHCXmL+CTYD^N$rt z6EVgluCH6=799mW+3aM+#?gM1n(qu$h;Tecu0766s2N%5{66UkGv-vTXCv@nQaj4@ zEzi4~DwhL$Ke}?Y=%Lc=Mw$(lYq&4UrpM=JQ{^Qzd}-k-G8N2hr3Pe#xMM88n#Yps z(^lfRun3io&^}S09L6Ws6Baa-bkIr8gkX8*=#bMs#^iAG_nL*gvUYb!-Jdd$e2s{s z?Gw36lkpeMz2jH=$0FP>W?4)J5AWOowoW8UY^SvV9AOqg_(&3(cHW;$a{$ic+r?N;W`Qq-D_ zr28-T>wD_^&_uHYEqPC*NQE`6U|o42uLRSjx^#*SZRR6*Aa($@UWEs|Rewu-3r&f8 zXS;VdZ1Jtw2|OTK2><#R4;;ML;Q4|{fu<2zS-1EYOFV)HN@3e)C_coHat1pUx-g6h zvV%X4?`_F}rpBQ-|F8%$a7(HqZ*_Y&TvZWb# z;5HI%so5Ozbpj6%mPmsabsM3MPfkc=l9DpKkAbYwP^%cyv;0{gN!5TaZKmLX?m95x zsC4!v8sh<2cnsDf087L4=kX`gwzey7s=V*xt!OysM{PvN355?QMzcFT5pFXau@o1V zuC_~!&81{Aw?z}>S&>;h-2C+U<0wSqn67X=B&S@=skSJT=f)wQvbB2o;rW150g?Bnm*<|7U6w^6UU31L3)+XH+Sp zvQiq$2IE(@dn!>JnXLy`VfXRCGOwlK>I@#p$#8XY3n<3}n+yWyF=#x{+f|qYTet8i z<0pBiB1N-Yxre!?Ayc$w?i^Sp>$j4TzkO6sE<45cS}?H-JVYl;WCIL^C$GU*u`t|& zRmWE?LRCd?m^8Ofi-YO$)iVT0x~9uEQsJXKi)C2ya5#XPE%uOWgXoX?j1BMQfbnQICx$@)b-rEA6-X5qX;V;51^>yL#z zBCVUa3sMBEn8sJY;BaW-V;Uu}>e!m#!||F(y1>4(~aI4=2o@kT_0r=`o?Y zALAH8d(=uch<0q`>5JDj*p)jMs>?2jT*urBGEHDhGY`IXYnUy#X}Ks6U@&8g1b``& z{(n=uUCHu;FSCKE)vyf@OrJOKF>qQ+!d-oVoWsDeL~|a3OQW-87sQCc1d&C*lOTG< zr}2ObQpD+^(tG$2>|>O2so->(GUyEH1katQpT{RBr!B%I#4F=aT;qg)2D~mxpE>qE zUZ&vyXn`6OM?v7N-;$Xgng4q;N7v}0V%}q zyyO>d*acxYLqX`sY>(`2!UGtA+4()__s>(xkIV!u$6Z9$Em`4#8=woJP4cE_Kq2=f zTTEmTwhs<({_ZMSDjP67zmymYc_+FiYN`Lmhgy5U~DuB}vU>D;_(#J<~}xe$QtW zMChF5XAml~av~^H859lu!pRX=dO>oBm+c>W5lUL#ZjcLBd;a7IZ3EV>fX8UjsB79v zt`pK9jeVeA2O6o`Tqah~0%_NsGDNqGFPD818)l!syDT|rJ(y!36lr3{FcS{6bZ%d< zOVp3q2<7XQUQ8%BbgoiIx{uH*Akx=&S!C&%cKgQEmqpJ^PoC=MmA2V+&xl>a`TP>^ zTwItH7IUgLC~37_D3%V7Ez2>FJIGS-F2_|Y(sT0iDP{VpuQY6j#ox7W(8@>HnTe-G za5+VMkqO;!3BMAk?Q>+y4ItQRC0+|Tw#1FwwYw%}^C(CFvq1Mj(`76U*FH|&E?;B>&9c}C zG&1N9`|bD@MJ{g)$6+NvcYbqFr9)LEu#9I~Sb*jp$uvoiqb38$4vi}e(e$?4P>{nu ziO12_J}8^MFuXwm#{-mxurEoyPrgiN^6lM^*yau(Lbe)}EgnYVP6n8YHy!?n8i$YG zcHzQ3JhW3R#RE$;-Tf_0ow=Gv@~0Cerh8a9r6nhy$8#)r1*14pesSM@Fg4|-zf1HP z6%)UXw0Pe~Bo9azxI_^-dD&sAkJ|f=xp=pVo|N#3;S{g*68K|;iY+e5|1{rRJ+V|l zYO2QO=S}nk8k0ic#U7W^nA&sY_|Z!PQZ{Uqm(>eg{oROa4W}WOQa^vI8U4Sb3~EZ4 z3N*YuN&!Ac(uxPRk%}w&b*Ta^#e*@$G9Y8b6!vdT>4a49-1SaW**4dYBbVthqDAYIakMOHVdAd}B z*C4K6M{HGXeoA@k*@j!hInH`7l}TZ|K)iK>x)xsKM>Iu&x+DKoBdPW2ueEa4s`R$AcyinsZ1FLt*=Wbh{KX@#{{1#8-s~{+iFo`dE zCsZs`99Bx|ox@zexag|Wl!KTYvhC5=zamn6mY6z|Mq7f&YwKObYKD^fo3?b5k`9~r z`~qLo=qmvpkd7Fm`gGyLp-!Z^8OIcI3PVmyzju=S+!y!8_C>&WgI`)V3zZkwm1;M~ z@GNfuT@DuQ%v-r3EYjtT&rjS@0wz{t57>%HCeEzSm^wJUz1v$G+LTf_uir#W#dgDn zRp7{y#uL**-{I51V%$)_|a~L_`IJD;TP{HL};o-AjE>6^xF-Gy6$=teF@J@OBw8yH7Ss0^FN@^u!yp+HM+s8 z6fMpol* zBAt+&%XY%iK@?v9@XJ)X8Z6x`h%VH<)J*Webv&>T4kE!- zCEQGM3JCj{c4l^9qGp#4$+nN1A=VA#GO-6YFP6S-g-__VgFK=AJQmP=8i@^vnP!y+ zapjSn;PP$kgL@jU{SqI@v}<}t$vr(nP$x4hNWd$yOu8oU@>RdQIp(>V!GHn0@BWIJ zRuyazQCpV^m-}N@PGyYtNwVUJv!rbli9KQe!C0u}@+Bg}I4|Kj?vqnh&vbQXPE5Ad zFZZ?e*yOh?#q-n~sA}7O(+u))E2c_Zxuv$VLf)4TM zUsA)#NvD0QmZ)mRgCMo|yfg{8Qkv~MSP}r!w$AWEz?|aifYl7#G%j}*mZ>?akqk$YYuI}>qW9e7_PUAFc#jYZZKfXKHy z!4LyvQ3-Fs;H}br)G7unzgXWt=ZHJJk!Grtqr-{5Sk~cG?mCy%jeKh^gu~}2f^yEd zxrF{OXmr$DE0Ha&jl)YtwF2l!- z{c|E8BMUU?X!C1meOvhC>OQ_S9r*C9^*B|{SGj_?l9{c`jkY%Vabx2qPeu8%S0Yls z0M0m83C50nm3x{M+iIoFGmKeSkOjBN8p>)mm(jo{5-iKsYC(>2%1zjCs}g+!i~&-f z9F;pM?Wb0M={Y&6pc!`qREgH#y8D&&+Sc}VSgH7<&_tVZ&{AR#{?M!9cRdU6^)GCf zJ5j4#-+}voi>>;`R5&OsYGXoZk)}JQ?!$dsOyOqZq!e!K>F4$lAVB*j$z;AtyDhu@$;SSY>uDXzL{>~9wekg9;xT5n7Kg`%^eBJR!;Cm5qj$BmG%feA z!#}B0?DNxh^}UKV@YlpGme3yum)lV80_Yczc&rujJv>7s9Nty16_5FpdS-qF`2zF+ z4aiUUA$-OcLX~IB*%^*TtcQI*A=9c?qhb~S~}>|u>|`fZhhW9j8uGW z>Wc@6%NY4pq+Gl}bNr4qcQeC(vyTNHsmYq=l5Yj4lufFdV7oYkh+dK8f=)6oJiIMI zek0#rQ3wc6v>R7YY;WL}mba4Sw!J!Mc631#apkw6UY18tcEEVZOZ5Hj*qRh?N%zUn zlJRc*+TFrjo;RU`43ur%te@EeCCNpiJ5T$0TJW}5(R5JQF;!Q%gi7cmJqE$%h%#}; z{tDFpYW9zsEns^}ZOi7L!bs56bcgzILX6zUb{p%3$&SSQ=reJG5+r_A6ss!h)R3e< zs||qrh?x^d-Sy>`Erw=iso!aJ&62*ntVhU}d^?3T^Kd(_kbufIMUosF$||kb`fk%3 z-JE23TmH|eBd>CzBc9e;&P>OW4{{+CxEF3&6-6{ggldGk`xQl)SNfhz^&*K~u-sEH z``@hle>LkyrOe%RQ?K@=BWmi-A-#E3FEW^Td^$Gh*}^3&o?`3Q(x-yWZN~D+xTNm1 z`GqlSR}ogQMWX8%=EH%ReO0$h=d}W^TRp)w+gaJ&t}uNtdR=6JL2Wshl(ltDJmmE5 z3f=$27@Ye-s;7)&T2zx*lyvFb8Bws&8VfU4F=V=}w2<{#`U7qjr(T5(I6t*bxDUmv zeA^3b(W0ssk=sv2D6kK~1{_+atJ7s(yN6&Obk^L{Ve$IY0w3taBo?)uz!lzlB?V}KE#WA0Lb>4!vh_7LxS79!f2snC} z%&f1dj{{04^`;Dk2UI$oPUZWLeW{5%=E^8_RjPawmBJQWo0KRj^=Z^Q^E}PMwYPeD z#G3Dd;$fvWPqb3eUr3N@x_gQn=sz}tO073QYgveo0ry;oDA*Rq7*1PsbZynI;vT(r_Z)ul zi;dlmaYPowqeOo8Qj!gip$(J2r~mII3X6{KC5yPEax4Ji28|t<22oI2wL(+RS|Za7 zV^fevaYeruSW-Q}n(|4-hhQCg4vD4B%hM{|Y2dO;f-PUCGMtZ2UzShUhY!5wq2?w} z8@-)4<%Feyr78ka@sY_KM_r+Gla5#3Nv2!M!qLtz%Q_L;in3TMjeVw}`nOH9gS%7k zUp*DH!&-I**W@rNW?5Cha0_I_alxg*dx{NKjqb<|g7gyPGv755^<6=hW(9+r zULf%KR;T)p){v+>@PI~{q~hx&OdjT7*wXpAH@}pPNvZ%_gKX3AwZjDp4KHOap#vj_fFb01Rf;;+v`R$kcNd@*siKYG|1wZ?dXFC6&nak7g< zwd)b8H9wFU#{Ol3-lI82yvYvRG+ydHna6*<1ZHkw!`;_0pm*Dk#negM!O41p8GK%c zOA8QD-@|kNq_w)w?Gn=VYQN-o7IxI_f)J>N(&t8?r9%;zE)bj|b)0_IIoYsP375;EIY}@X|f`@4leJ@RbeUl%x3RDafz2op&I}T4I0PoCdRZB_1 zW!!s^xGj(Q@(ZJjr|b)|H#gw8t5GDl3Q!~rx0Pk-IYX)=^;aLUQ}U*JE7mv-K)%S- z)veWGy6*ZH$ev3$H~C2WUWd4O98acL@)oV_lupQQVEvc2@D1|sRYhmtro#t}_ZNe6 z-A6?mf0OF40n|ghU?sM0vz4O;Cl$~1m3tzjzy~K%l94UgZY<7rDSk^$84tv4Wo>N_ zkLwTjtl&&Yx~Vnm@IYVq<86@XBZomnFv9Z$C^o@bD%dV)#_=PKTQ&n>dMbLz%9(_i z(T`zl?PC7dHqCCzVpsfM7ad$SW7Za016Q0=;(s0gzr^tY4b7(l2he4QhlX@%sNx{U zgz7ZdIM|$_!rNRLuZnUD%5uWnXHSI=nVdN(D6Ix^8|lXmZtq{Gyf!cpnCc~0I=+DN zzmg&}bq{#1V~b+N(ZCh7Je_tgHGd(`b;ZKE^`Lkha&<|UO()-8eRjOrYsP%>}jxk6K=1{s!sD?J!Z>0?x4 zVv(^03nU+5i(VU9I4{kZ_3B-lo{~~`kyhPb%3v$d&v;-%!b1}*x^uN`jRox5E{%Py zz^hxq)_#=LPo4*egnSW>-!dpO1&g&WU_!kc$T0!G)P5aAmac?%e)XZICqBVSz(o>v zxStX>WJwg~=JE^=Ku>`Wk*D>IFIk7wL|C|5Si9QWo(`pFk*Tbkym+IQTta1;s-6kq zfa?5|M=GEoLN<4^h9S&;fu6O4vgcvoy^w(J7#+JUFVOC{xW=5f(M#+&VzBg4t+M|b z)2SSob*yN>0LjO)UPG#~siddyMP5T3XO4{+-MDW5%X8=XGvvmcS$Uh}i*5kCvh4X> zGY!=pVhR>Xh61^v>#A(hJYq|+JQb4{sqV_Kvv0+Yi;YD(9FQ8ifVw9SKGY!whF<7c z83tm29HvJ8+N41XJbwCR^^hdD@q-y!HI{&p=zGxS#2Q$5ER~*fy|p*GYr_g zzl;aAS;RpA6iYLOT|(^IHdIzYtd-nrTml2GxIQzmWcEU7?a<@}_`^9xuo*~FjYJa^ zgAc}W%_zvLDDZYDE;jS=rCU&Qk`?ikL)j=k1QHn+_}#wu@h!p>dG`J-*I55k=LtUa zA1~ECE(klrw$^+qyi1>M9CyP7w(GKcZb7k%-}wlF{~EI}waA5ONe1E=rC<6!zgza0 zMK6~e7kH20iwfL9%p(-DwKn=+h$lTXw?@mDmmk4b5F=)L9?HeY!G6}=%*wc%q_sl_ z@gyVfX>me+QT|8rPlGdk;QI(ufCscRE%VX!UPd+cLI7@PW0g zWb=d-%a*upWD^D0`5J{R8DfDf_*WGC_f3rJ;7z}Ty9ZHZe9T^lSG}SyZZ>E+HsxRC#*}h%UUDa9D;Q=BLGoL2*Z5G+;~P;0kUde%E$Z0=h(%NvHEQO8BPv zgn1(B2DE1O&h?R?&7-6zGuyCPhHt;!jbSiV-kJWdLRD&?-!`K`gagu1OVak3 z>RgndWX0%gYs$h~Bmt$&{2uzQuWywRxV z$&Zsm%c4H+95+M6>(BHxcu@uxiure@?0HWqv(3QR*_mMPJ192cLnZ|)gdwW(UTRH# zfq|@&krzmk;UTT8$Zab!keR6WslBVUoZp2W}-FoM-e`!yBde>6^OZrgRS4F6v zvl%p1(iiny&B&<;gB?DxnB`kQ+Eujs46>P%yD72ZV_jpt4;qOolbr^5lRmQ@LA?6n z$tC@lZ=CTtVJ|8eUM|XhB;~KIwr(@dNq_IB;>8@{*J4l;hwT+y2URQj>~>#?yyHnxrJE8bx~!hEmS7nd4uEI?Lz8inaK;^cZqj1X9+#BFm-=2Xv{|Zq=rVZX@r3Iz=+xL z&L}9N(>iPPeW!Tc>#mC1akTAxn=e>$B9r^0(!8gRujq4s7hL1~6UTQ>l;&+S80!u1 zo?N=kgZ2rgx>h#=))gf$aVaj3^y$cpRGj_d6GY8=ZmJczx{-N!yjJIflt&uJEj8e| z*oCmm)dSSzAK%rq3IdX|LMOdWKdyLR{W|L0OX@ZIeuFAPXWn9e#zVg?P-cEAw663G zsS)jV*IQa*6}1QrLcrF%o9ql$em9PIf z{Osb=BQ@-3B*JdJ#@S}34>W>rjlA@Eq*9D}>-lu<*GHP|$fPrSYcKf=1L*N+D`^xk zdRB}*b;kK=a*xhT{`~RJpA>XolAqAw_PrXxOCOkYjHfoG*-0Gs$+X>3)auNH>}iQ+ zR+dfq9NODv`Lfj(KYYLr$F>;Nm6w`xmBqhnyq@kT*-K*&=}lo@Q#PRyDqr^zO(QA5 z-VO0MPgEW&E*hG11q2n)^Vn|^;Jjo2nJsnIogdY$qu z;oJAF3FL}$FH8gcMLyRiF=VQaSA>^*_SL!lOdDZ+lif--q&HggZiB6UEC&#HM_0Ua zs7-?YJ-7F^CdNNLg*(@o?v}uj{= 3 { + code += 0.01 + } + if z >= 2.5 { + code += 0.49 + } + if z >= 2 { + code += 1.66 + } + if z >= 1.5 { + code += 4.4 + } + if z >= 1 { + code += 9.19 + } + if z >= 0.5 { + code += 14.98 + } + if z >= 0 { + code += 19.15 + } + if z >= -0.5 { + code += 19.15 + } + if z >= -1 { + code += 14.98 + } + if z >= -1.5 { + code += 9.19 + } + if z >= -2 { + code += 4.4 + } + if z >= -2.5 { + code += 1.66 + } + if z >= -3 { + code += 0.49 + } + return code / 100 +} + +// Score 返回出现该用户行为的概率 x 100,得分越高用户越接近真实用户,反之越接近异常用户,通常score < 1为异常用户 +func Score(qCountMap map[string]int64) int64 { + return int64(truthPR(qCountMap, routersND)) +} diff --git a/common/utils.go b/common/utils.go new file mode 100644 index 0000000..8e277ac --- /dev/null +++ b/common/utils.go @@ -0,0 +1,19 @@ +package common + +import "strings" + +// JoinUrlPath 域名与uri拼接 +func JoinUrlPath(host string, uri string) (urlpath string) { + if uri == "" { + return + } + switch { + case strings.HasSuffix(host, "/") && strings.HasPrefix(uri, "/"): + urlpath = host[0:len(host)-1] + uri + case !strings.HasSuffix(host, "/") && !strings.HasPrefix(uri, "/"): + urlpath = host + "/" + uri + default: + urlpath = host + uri + } + return +} diff --git a/common/uuid.go b/common/uuid.go new file mode 100644 index 0000000..a2b1be6 --- /dev/null +++ b/common/uuid.go @@ -0,0 +1,7 @@ +package common + +import "github.com/google/uuid" + +func UUID() string { + return uuid.New().String() +} diff --git a/common/v10/upgradev10.go b/common/v10/upgradev10.go new file mode 100644 index 0000000..51d6c19 --- /dev/null +++ b/common/v10/upgradev10.go @@ -0,0 +1,47 @@ +package v10 + +import ( + "reflect" + "sync" + + "github.com/gin-gonic/gin/binding" + "github.com/go-playground/validator/v10" +) + +type DefaultValidator struct { + once sync.Once + validate *validator.Validate +} + +var _ binding.StructValidator = &DefaultValidator{} + +func (v *DefaultValidator) ValidateStruct(obj interface{}) error { + if kindOfData(obj) == reflect.Struct { + v.lazyinit() + if err := v.validate.Struct(obj); err != nil { + return error(err) + } + } + return nil +} + +func (v *DefaultValidator) Engine() interface{} { + v.lazyinit() + return v.validate +} + +func (v *DefaultValidator) lazyinit() { + v.once.Do(func() { + v.validate = validator.New() + v.validate.SetTagName("binding") + }) +} + +func kindOfData(data interface{}) reflect.Kind { + value := reflect.ValueOf(data) + valueType := value.Kind() + if valueType == reflect.Ptr { + valueType = value.Elem().Kind() + } + return valueType +} diff --git a/common/v10/validate.go b/common/v10/validate.go new file mode 100644 index 0000000..724feed --- /dev/null +++ b/common/v10/validate.go @@ -0,0 +1,168 @@ +package v10 + +import ( + "reflect" + "regexp" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/go-playground/validator/v10" + "github.com/nyaruka/phonenumbers" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// IsPureChar 匹配中文、字母、数字 +func IsPureChar(src string) bool { + return regexp.MustCompile("^[a-z0-9A-Z\u4e00-\u9fa5]+$").MatchString(src) +} + +// IsPureChineseChar 匹配中文 +func IsPureChineseChar(src string) bool { + return regexp.MustCompile("[\u4e00-\u9fa5]").MatchString(src) +} + +// 提取文字 包括数字 英文 不包含符号 +func ExtractPureEngChar(src string) string { + pureArray := regexp.MustCompile("[a-z0-9A-Z\u005f]").FindAllString(src, -1) + return strings.Join(pureArray, "") +} + +// 提取文字 包括中文 数字 英文 不包含符号 +func ExtractPureChar(src string) string { + src = strings.TrimSpace(src) + pureArray := regexp.MustCompile("[a-z0-9A-Z\u4e00-\u9fa5]+").FindAllString(src, -1) + return strings.Join(pureArray, "") +} + +// IsPhoneNumber 匹配所有国际化的手机号码(手机卡 + 上网卡) +func IsGlobalizationPhoneNumber(phoneNumber string) bool { + num, _ := phonenumbers.Parse(phoneNumber, "CN") + return phonenumbers.IsValidNumber(num) +} + +// IsEn 是否是英文的正则表达式验证 +func IsEn(data string) bool { + return regexp.MustCompile(`^[a-zA-Z]+$`).MatchString(data) +} + +// IsEnZh 是否是中英文的正则表达式验证 +func IsEnZh(data string) bool { + return regexp.MustCompile("^[a-zA-Z|\u4e00-\u9fa5]+$").MatchString(data) +} + +// IsEnNum 是否是英文数字的正则表达式验证 +func IsEnNum(data string) bool { + return regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(data) +} + +// IsEnNumSpe 是否是英文数字特殊符号的验证 +func IsEnNumSpe(data string) bool { + return regexp.MustCompile(`^[a-zA-Z0-9|~!@#$%^&*()/\|,.<>?"'();:_+-=\[\]{}]+$`).MatchString(data) +} + +// IsObjectID 是否objectID的验证 +func IsObjectID(data string) bool { + if _, err := primitive.ObjectIDFromHex(data); err != nil { + return false + } + return true +} + +// IsLengthBetween 判断data字符串长度是否在范围内 +func IsLengthBetween(data string, min int, max int) bool { + return len(data) >= min && len(data) <= max +} + +// IsNil nil判断 +func IsNil(i interface{}) bool { + vi := reflect.ValueOf(i) + if vi.Kind() == reflect.Ptr { + return vi.IsNil() + } + return false +} + +// Phone 验证手机号码 +func Phone( + v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, + field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, +) bool { + if data, ok := field.Interface().(string); ok { + if len(data) <= 10 || len(data) >= 15 { + return false + } + if !IsGlobalizationPhoneNumber(data) { + return false + } + return true + } + return false +} + +// Time_Utc 转换成UTC时间 +func Time_Utc(fl validator.FieldLevel) bool { + timeParse, err := time.Parse(time.RFC3339, fl.Field().String()) + if err != nil { + return false + } + timevalue := reflect.ValueOf(timeParse) + fl.Field().Set(timevalue) + return true +} + +// StrLength 验证长度 +func StrLength( + v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, + field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, +) bool { + num, err := strconv.Atoi(param) + if err != nil { + return false + } + if data, ok := field.Interface().(string); ok { + if len(data) == num { + return true + } + } + return false +} + +// NeedType 需要类型 +func NeedType( + v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, + field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, +) bool { + if data, ok := field.Interface().(string); ok { + switch param { + case "en": // 英文 + return IsEn(data) + case "enZh": // 中英文 + return IsEnZh(data) + case "enNum": // 英文数字 + return IsEnNum(data) + case "enNumSpe": // 英文数字特殊符号 + return IsEnNumSpe(data) + case "objectID": // 是否为mongo的id + return IsObjectID(data) + } + } + return false +} + +// RegisterValidator 注解自定义参数验证 +func RegisterValidator() gin.HandlerFunc { + return func(c *gin.Context) { + if v, ok := binding.Validator.Engine().(*validator.Validate); ok { + //v.RegisterValidation("phone", Phone) + // v.RegisterValidation("len", StrLength) + // v.RegisterValidation("min", validator.IsGte) + // v.RegisterValidation("max", validator.IsLte) + // v.RegisterValidation("eq", validator.IsEq) + // v.RegisterValidation("type", NeedType) + _ = v.RegisterValidation("time_utc", Time_Utc) + } + } +} diff --git a/common/version/version.go b/common/version/version.go new file mode 100644 index 0000000..fa5590f --- /dev/null +++ b/common/version/version.go @@ -0,0 +1,99 @@ +package version + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +type Version struct { + Major int + Minor int + Revision int +} + +var ErrVersionString = errors.New("bad version string") + +func New(ver string) (*Version, error) { + vs := strings.Split(ver, ".") + if len(vs) != 3 { + return nil, ErrVersionString + } + major, err := strconv.Atoi(vs[0]) + if err != nil { + return nil, ErrVersionString + } + minor, err := strconv.Atoi(vs[1]) + if err != nil || minor > 999 { + return nil, ErrVersionString + } + revision, err := strconv.Atoi(vs[2]) + if err != nil || revision > 999 { + return nil, ErrVersionString + } + return &Version{Major: major, Minor: minor, Revision: revision}, nil +} + +func MustNew(ver string) *Version { + v, err := New(ver) + if err != nil { + panic("bad version") + } + return v +} + +func (v *Version) GetCode() int64 { + return int64(v.Major*1000000) + int64(v.Minor*1000) + int64(v.Revision) +} + +// for print +func (v *Version) String() string { + return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Revision) +} + +// Compare 0相等, -1是 v< other, 1 v > other +func (v *Version) Compare(other *Version) float64 { + if v.String() == other.String() { + return 0 + } + if v.Major > other.Major { + return 1 + } + if v.Major < other.Major { + return -1 + } + if v.Minor > other.Minor { + return 1 + } + if v.Minor < other.Minor { + return -1 + } + if v.Revision > other.Revision { + return 1 + } + if v.Revision < other.Revision { + return -1 + } + return 0 +} + +func (v *Version) GT(other *Version) bool { + return v.Compare(other) > 0 +} + +func (v *Version) LT(other *Version) bool { + return v.Compare(other) < 0 +} + +func (v *Version) EQ(other *Version) bool { + return v.Compare(other) == 0 +} + +func (v *Version) GTE(other *Version) bool { + return v.Compare(other) >= 0 +} + +func (v *Version) LTE(other *Version) bool { + return v.Compare(other) <= 0 +} diff --git a/common/vid.go b/common/vid.go new file mode 100644 index 0000000..5565f62 --- /dev/null +++ b/common/vid.go @@ -0,0 +1,45 @@ +package common + +import "math" + +type Quality string +type Direction string + +const ( + High Quality = "high" //影片质量高 720P以上 + Middle Quality = "middle" //影片质量高 480P-720P以上 + Low Quality = "low" //影片质量高 480P以下 + + Vertical Direction = "vertical" //竖屏 + Horizontal Direction = "horizontal" //横屏 + Square Direction = "square" //方形屏 +) + +type IdentifiInfo struct { + Quality Quality + Direction Direction +} + +// 提取视频信息 +func IdentifiVideoInfo(width, height int64) (info IdentifiInfo) { + //竖屏 + if height > width { + info.Direction = Vertical + } + if height == width { + info.Direction = Square + } + if height < width { + info.Direction = Horizontal + } + max := math.Max(float64(width), float64(height)) + min := math.Min(float64(width), float64(height)) + if min >= 720 && max >= 1280 { + info.Quality = High + } else if min >= 480 && max >= 800 { + info.Quality = Middle + } else { + info.Quality = Low + } + return +} diff --git a/common/worker/worker.go b/common/worker/worker.go new file mode 100644 index 0000000..6413aa8 --- /dev/null +++ b/common/worker/worker.go @@ -0,0 +1,98 @@ +package worker + +import ( + "91porn-server/common/log" + "errors" + "fmt" + "runtime" + "strings" + "sync" + "time" +) + +const ( + stop = 0 + running = 1 +) + +type Worker struct { + num int // 最大工作协程,限制同时执行任务的协程数量,避免同时执行的任务太多,超过其他服务的承载上限 + ch chan struct{} + wg *sync.WaitGroup + state int + lock *sync.RWMutex +} + +func NewWorker(workerNum int) *Worker { + return &Worker{ + num: workerNum, + ch: make(chan struct{}, workerNum), + wg: &sync.WaitGroup{}, + state: running, + lock: &sync.RWMutex{}, + } +} + +func (w *Worker) wait() { + w.wg.Wait() +} + +func (w *Worker) Exec(f func()) (err error) { + defer func() { + if e := recover(); e != nil { + log.Error("worker exec Panic: ", log.Any("e", e)) + } + }() + tick := time.NewTicker(time.Second * 1) + if w.State() != running { + return + } + select { + case w.ch <- struct{}{}: + go func() { + w.wg.Add(1) + defer func() { + <-w.ch + w.wg.Done() + if r := recover(); r != nil { + dep := 0 + t := make([]string, 0, 10) + for i := 1; i < 10; i++ { + _, file, line, ok := runtime.Caller(i) + if !ok { + break + } + if strings.Contains(file, "/runtime/") || strings.Contains(file, "/reflect/") { + continue + } + t = append(t, fmt.Sprintf("%s∟%s:%d", strings.Repeat(" ", dep), file, line)) + dep++ + } + exception := strings.Join(t, "\n") + log.Error("worker exec func Panic: ", log.Any("Exception", exception)) + } + }() + f() + }() + case <-tick.C: + log.Debug("worker exec timeout") + // 等待执行超时,直接返回 + return errors.New("Exec timeout") + } + return nil +} + +func (w *Worker) Stop() { + w.lock.Lock() + w.state = stop + w.lock.Unlock() + // 等待所有的任务执行完毕再退出 + w.wait() + return +} + +func (w *Worker) State() int { + w.lock.RLock() + defer w.lock.RUnlock() + return w.state +} diff --git a/common/ysinterface/disc/disc.go b/common/ysinterface/disc/disc.go new file mode 100644 index 0000000..953aba6 --- /dev/null +++ b/common/ysinterface/disc/disc.go @@ -0,0 +1,8 @@ +package disc + +type DistrictStater interface { + GetDiscCode() string + GetPromSeqe() string + GetSysType() string + String() string +} diff --git a/common/ysphone/phone.go b/common/ysphone/phone.go new file mode 100644 index 0000000..54963dc --- /dev/null +++ b/common/ysphone/phone.go @@ -0,0 +1,11 @@ +package ysphone + +import "github.com/nyaruka/phonenumbers" + +func FormatPhoneNumber(phoneNumber string) string { + if phoneNumber == "" { + return "" + } + num, _ := phonenumbers.Parse(phoneNumber, "CN") + return phonenumbers.Format(num, phonenumbers.E164) +} diff --git a/common/ysqr/error.go b/common/ysqr/error.go new file mode 100644 index 0000000..9d9c012 --- /dev/null +++ b/common/ysqr/error.go @@ -0,0 +1,15 @@ +package ysqr + +import "errors" + +type ErrContentInvalid struct { +} + +func (ErrContentInvalid) Error() string { + return "content invalid" +} + +var ( + ErrInvalidUser = errors.New("user invalid") + ErrContentIllegal = errors.New("content invalid") +) diff --git a/common/ysqr/qr.go b/common/ysqr/qr.go new file mode 100644 index 0000000..324ca85 --- /dev/null +++ b/common/ysqr/qr.go @@ -0,0 +1,60 @@ +package ysqr + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "91porn-server/common/crypt" +) + +type ActType string + +const ( + Login ActType = "||" + + PFV = "pfv" +) + +type LoginClaims struct { + DevID string `json:"devID"` + T ActType `json:"t"` +} + +func sign(devID string, t ActType, secret string) string { + j, _ := json.Marshal(LoginClaims{ + DevID: devID, + T: t, + }) + sha256 := crypt.StrToHmacSha256(string(j), secret) + if len(sha256) > 24 { + return sha256[8:24] + } + return sha256 +} + +type Content struct { + UID uint64 + T ActType + LoginClaims +} + +// String pfv2505938||******** +func (c Content) String(secret string) string { + s := sign(c.DevID, c.T, secret) + return fmt.Sprintf("%s%d%s%s", PFV, c.UID, string(c.T), s) +} + +func GetUIDFromQrCnt(t ActType, qrCnt string) (uint64, error) { + list := strings.Split(qrCnt, string(t)) + if len(list) != 2 { + return 0, ErrContentInvalid{} + } + uidstr := strings.ReplaceAll(list[0], PFV, "") + uid, err := strconv.ParseUint(uidstr, 10, 64) + if err != nil { + return 0, ErrContentInvalid{} + } + return uid, nil +} diff --git a/common/ysurl/yslinks.go b/common/ysurl/yslinks.go new file mode 100644 index 0000000..bc1eb5a --- /dev/null +++ b/common/ysurl/yslinks.go @@ -0,0 +1,47 @@ +package ysurl + +import ( + "net/url" + "regexp" +) + +// 广告跳转类型 +type URLJumpType int + +const ( + ExBrowserJump URLJumpType = iota //外部浏览器跳转 + InBrowserJump //内部浏览器跳转 + AppJump //APP内部跳转 +) + +const ( + Http = "http" //外部http跳转 + Https = "https" //外部https跳转 + Yinselink = "yinselink" //内部http跳转 + Yinselinks = "yinselinks" //内部https跳转 + Yinseinner = "yinseinner" //应用内⻚面跳转 +) + +func IsValidURL(jumpType URLJumpType, rawurl string) bool { + url, err := url.Parse(rawurl) + if err != nil { + return false + } + switch jumpType { + case ExBrowserJump: + return url.Scheme == Http || url.Scheme == Https + case InBrowserJump: + return url.Scheme == Yinselink || url.Scheme == Yinselinks + case AppJump: + return url.Scheme == Yinseinner + } + return false +} + +func IsValidHostName(hostName string) bool { + re, err := regexp.Compile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`) + if err != nil { + return false + } + return re.MatchString(hostName) +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..cca6727 --- /dev/null +++ b/doc.go @@ -0,0 +1,12 @@ +// build doc + +package main + +import ( + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +func init() { + SwagHandler = ginSwagger.WrapHandler(swaggerFiles.Handler) +} diff --git a/generate/common/generate.go b/generate/common/generate.go new file mode 100644 index 0000000..f243145 --- /dev/null +++ b/generate/common/generate.go @@ -0,0 +1,364 @@ +package common + +import ( + "errors" + "fmt" + "os" + "path" + "path/filepath" + "reflect" + "strings" + + "91porn-server/generate/tpl" +) + +func NewGen(moduleName, tblComment string) *G { + rootPath := RootPath() + return &G{ + AppGenerate: true, + WebGenerate: true, + RootPath: rootPath, + ModuleName: moduleName, + TableComment: tblComment, + } +} + +type Field struct { + Name string + TypeName string + Tag string + BsonField string + FormTag string + UpdateTag string + Comment string + SubField []Field + SubStructName string +} + +type G struct { + AppGenerate bool + WebGenerate bool + RootPath string + ModuleName string + StructName string + FileName string + TableName string + TableComment string + Fields []Field +} + +// Generate 生成... +func (o *G) Generate(s interface{}) { + var err error + // 获取model结构 + t := reflect.TypeOf(s) + o.Fields, err = o.explodeStruct(t) + if err != nil { + log(err.Error(), "error") + return + } + o.StructName = FirstUpper(t.Name()) + if o.TableName == "" { + o.TableName = o.StructName + } + o.FileName = strings.ToLower(o.TableName) + + // 生成变量表 + v := make(map[string]string) + v["FileName"] = strings.ToLower(o.TableName) + v["ModuleName"] = o.ModuleName + v["TableComment"] = o.TableComment + v["TableName"] = o.TableName + v["StructName"] = o.StructName + v["PackageTableName"] = strings.ToLower(o.TableName) + v["RouterName"] = FirstLower(o.TableName) + v["StructDefine"] = o.genStructDefine(o.StructName, o.Fields) + v["AppRespStructFields"] = o.genAppRespStructFields(o.Fields) + v["AppRespTransferStatement"] = o.genAppRespTransferStatement(o.Fields) + v["CreateReqFields"] = o.genCreateStructFields(o.Fields) + v["CreateStatement"] = o.genCreateStatement(o.Fields) + v["UpdateReqFields"] = o.genUpdateStructFields(o.Fields) + v["UpdateStatement"] = o.genUpdateStatement(o.Fields) + + // 循环配置进行生成 + for tpDir, c := range tpl.Config { + // 判断是否生成 app web + if !o.AppGenerate && strings.HasPrefix(tpDir, "/app") { + continue + } + if !o.WebGenerate && strings.HasPrefix(tpDir, "/web") { + continue + } + // 遍历目录 + tplPath := path.Join(o.RootPath, "/generate/tpl", tpDir) + var tpls []string + err := filepath.Walk(tplPath, func(path string, info os.FileInfo, err error) error { + if !info.IsDir() && strings.HasSuffix(info.Name(), ".tpl") { + tpls = append(tpls, path) + } + return nil + }) + if err != nil { + log(err.Error(), "error") + return + } + // 创建文件 + for _, tf := range tpls { + err = o.CreateFile(tpDir, tf, c, v) + if err != nil { + log(err.Error(), "error") + } + } + } + + // 执行完成 + log("生成完毕!", "info") +} + +// CreateFile 根据模板创建文件 +func (o *G) CreateFile(dir, tf string, conf tpl.Tpl, val map[string]string) (err error) { + val["VariablePrefix"] = FirstUpper(conf.VariablePrefix) + _, f := path.Split(tf) + f = strings.Replace(TplReplace(f, val), ".tpl", ".go", -1) + fPath := path.Join(o.RootPath, dir) + if conf.SplitDir { + fPath = path.Join(fPath, o.FileName+conf.DirSuffix) + } + // 创建目录 + err = EnsurePath(fPath) + if err != nil { + return err + } + + fPath = path.Join(fPath, f) + // 检查文件是否存在,存在则跳过 + //_, err = os.Stat(fPath) + //if err == nil { + // log("目标文件已存在,不可覆盖!File:"+fPath, "warn") + // return nil + //} + + log("准备写入目标文件:"+fPath, "info") + + // 读取文件并替换内容 + b, err := os.ReadFile(tf) + if err != nil { + return err + } + tplContent := string(b) + tplContent = TplReplace(tplContent, val) + + // 写入目标文件 + if err := os.WriteFile(fPath, []byte(tplContent), 0755); err != nil { + log("写入目标文件:"+fPath+"失败!", "error") + return err + } + + log("写入目标文件:"+fPath+"成功!", "error") + return nil +} + +// genStructDefine 生成结构体定义语句 +func (o *G) genStructDefine(structName string, fields []Field) string { + var s []string + sub := make(map[string][]Field) + for _, f := range fields { + tmp := fmt.Sprintf(" %s\t%s\t`%s`\t// %s", f.Name, f.TypeName, f.Tag, f.Comment) + s = append(s, tmp) + if len(f.SubField) > 0 { + sub[f.SubStructName] = f.SubField + } + } + + ms := fmt.Sprintf("type %s struct {\n%s\n} \n\n", structName, strings.Join(s, "\n")) + if len(sub) == 0 { + return ms + } + var subs []string + for n, v := range sub { + subs = append(subs, o.genStructDefine(n, v)) + } + ss := strings.Join(subs, "\n\n") + + return ms + ss +} + +// genAppRespStructFields 生成app返回结构 +func (o *G) genAppRespStructFields(fields []Field) string { + var s []string + for _, f := range fields { + tmp := fmt.Sprintf(" %s\t%s\t`%s`\t// %s", f.Name, f.TypeName, f.UpdateTag, f.Comment) + s = append(s, tmp) + } + + return strings.Join(s, "\n") +} + +// genAppRespTransferStatement 生成app返回转换过程代码 +func (o *G) genAppRespTransferStatement(fields []Field) string { + var s []string + for _, f := range fields { + tmp := fmt.Sprintf(" %s: item.%s,", f.Name, f.Name) + s = append(s, tmp) + } + str := strings.Join(s, "\n") + + return str +} + +// genCreateStructFields 生成创建数据字段定义 +func (o *G) genCreateStructFields(fields []Field) string { + var s []string + for _, f := range fields { + fn := strings.ToLower(f.Name) + if fn == "id" || fn == "createdat" || fn == "updatedat" { + continue + } + + tmp := fmt.Sprintf(" %s\t%s\t`%s`\t// %s", f.Name, f.TypeName, f.FormTag, f.Comment) + s = append(s, tmp) + } + + return strings.Join(s, "\n") +} + +// genCreateStatement 生成创建过程代码 +func (o *G) genCreateStatement(fields []Field) string { + var s []string + hasCreateTime := false + hasUpdateTime := false + for _, f := range fields { + fn := strings.ToLower(f.Name) + if fn == "id" { + continue + } + if fn == "createdat" { + hasCreateTime = true + continue + } + if fn == "updatedat" { + hasUpdateTime = true + continue + } + + tmp := fmt.Sprintf(" %s: p.%s,", f.Name, f.Name) + s = append(s, tmp) + } + str := strings.Join(s, "\n") + if hasCreateTime { + str += "\n CreatedAt: time.Now()," + } + if hasUpdateTime { + str += "\n UpdatedAt: time.Now()," + } + + return str +} + +// genUpdateStructFields 生成更新数据字段定义 +func (o *G) genUpdateStructFields(fields []Field) string { + var s []string + for _, f := range fields { + fn := strings.ToLower(f.Name) + if fn == "id" || fn == "createdat" || fn == "updatedat" { + continue + } + + tmp := fmt.Sprintf(" %s\t*%s\t`%s`\t// %s", f.Name, f.TypeName, f.UpdateTag, f.Comment) + s = append(s, tmp) + } + + return strings.Join(s, "\n") +} + +// genUpdateStatement 生成更新过程代码 +func (o *G) genUpdateStatement(fields []Field) string { + var s []string + hasUpdateTime := false + for _, f := range fields { + fn := strings.ToLower(f.Name) + if fn == "id" || fn == "createdat" { + continue + } + if fn == "updatedat" { + hasUpdateTime = true + continue + } + + tmp := fmt.Sprintf(" if p.%s != nil {\n\t\tdata[\"%s\"] = *p.%s\n\t}", f.Name, f.BsonField, f.Name) + s = append(s, tmp) + } + str := strings.Join(s, "\n") + if hasUpdateTime { + str += "\n\tdata[\"updatedAt\"] = time.Now()\n" + } + + return str +} + +// SetTableName 自定义表名 +func (o *G) SetTableName(n string) { + o.TableName = FirstUpper(n) +} + +// NoApp 不生成app +func (o *G) NoApp() { + o.AppGenerate = false +} + +// NoWeb 不生成web +func (o *G) NoWeb() { + o.WebGenerate = false +} + +// explodeStruct 解剖struct +func (o *G) explodeStruct(t reflect.Type) (f []Field, err error) { + if t.Kind() != reflect.Struct { + return nil, errors.New("不支持结构体以外的类型!") + } + + // 遍历结构体的字段 + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + jsonTag := field.Tag.Get("json") + if jsonTag == "" { + jsonTag = "-" + } + bsonTag := field.Tag.Get("bson") + if bsonTag == "" { + bsonTag = "-" + } + tag := fmt.Sprintf("json:\"%s\" bson:\"%s\"", jsonTag, bsonTag) + fTag := fmt.Sprintf("json:\"%s\" form:\"%s\" binding:\"required\"", jsonTag, jsonTag) + uTag := fmt.Sprintf("json:\"%s\"", jsonTag) + + isDefinedType := strings.Count(field.Type.String(), "main.") > 0 + typeName := strings.Replace(field.Type.String(), "main.", "", 1) + tmp := Field{ + Name: field.Name, + TypeName: typeName, + Tag: tag, + BsonField: bsonTag, + FormTag: fTag, + UpdateTag: uTag, + Comment: field.Tag.Get("comment"), + } + if isDefinedType { + if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Pointer { + tmp.SubField, _ = o.explodeStruct(field.Type.Elem()) + if field.Type.Kind() == reflect.Slice { + tmp.SubStructName = strings.TrimLeft(typeName, "[]") + } else { + tmp.SubStructName = strings.TrimLeft(typeName, "*") + } + } else if field.Type.Kind() == reflect.Struct { + tmp.SubField, _ = o.explodeStruct(field.Type) + tmp.SubStructName = typeName + } + } + + f = append(f, tmp) + } + + return +} diff --git a/generate/common/helper.go b/generate/common/helper.go new file mode 100644 index 0000000..4ab050f --- /dev/null +++ b/generate/common/helper.go @@ -0,0 +1,52 @@ +package common + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// EnsurePath 创建目标目录 +func EnsurePath(p string) error { + s, err := os.Stat(p) + if err != nil || !s.IsDir() { + return os.MkdirAll(p, 0755) + } + + return nil +} + +// TplReplace 模板变量替换 +func TplReplace(content string, val map[string]string) string { + for k, v := range val { + content = strings.ReplaceAll(content, "{{"+k+"}}", v) + } + + return content +} + +func log(msg, level string) { + fmt.Printf("[%s] %s \n", level, msg) +} + +// FirstUpper 字符串首字母大写 +func FirstUpper(s string) string { + if s == "" { + return "" + } + return strings.ToUpper(s[:1]) + s[1:] +} + +// FirstLower 字符串首字母小写 +func FirstLower(s string) string { + if s == "" { + return "" + } + return strings.ToLower(s[:1]) + s[1:] +} + +func RootPath() string { + p, _ := filepath.Abs("./") + return p +} diff --git a/generate/model/aiplaza/main.go b/generate/model/aiplaza/main.go new file mode 100644 index 0000000..b4c237a --- /dev/null +++ b/generate/model/aiplaza/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "91porn-server/generate/common" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +type AiPlaza struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id" comment:"文档id"` + Type int `json:"type" bson:"type"` // 1-ai图片换脸 2-ai视频换脸 3-ai脱衣 4-ai图生视频 5-ai绘画 + ModuleId string `json:"moduleId" bson:"moduleId" comment:"关联模版id"` // 关联模版id + Uid uint64 `json:"uid" bson:"uid"` + Title string `json:"title" bson:"title" comment:"标题"` + OriginalImage string `json:"originalImage" bson:"originalImage" comment:"原图"` // 原图 + OriginalVideo string `json:"originalVideo" bson:"originalVideo" comment:"原视频"` // 原视频 + OriginContent string `json:"originContent" bson:"originContent" comment:"输入的原文"` // 输入的原文 + GenerateImage string `json:"generateImage" bson:"generateImage" comment:"生成的图"` // 生成的图 + GenerateVideo string `json:"generateVideo" bson:"generateVideo" comment:"生成的视频"` // 生成的视频 + Status int `json:"status" bson:"status" comment:"0-待审核 1-审核通过 2-拒绝 3-下架"` // 0-待审核 1-审核通过 2-拒绝 3-下架 + Reason string `json:"reason" bson:"reason" comment:"原因"` + SortCode int `json:"sortCode" bson:"sortCode" comment:"置顶排序号,大于0就是置顶"` + ReviewAt time.Time `json:"reviewAt" bson:"reviewAt" comment:"审核时间"` // 审核时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt" comment:"文档创建时间"` // 文档创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt" comment:"文档更新时间"` // 文档更新时间 +} + +func main() { + g := common.NewGen("91porn-server", "ai广场帖子") + g.AppGenerate = true + g.Generate(AiPlaza{}) +} diff --git a/generate/model/hotsearch/main.go b/generate/model/hotsearch/main.go new file mode 100644 index 0000000..d676423 --- /dev/null +++ b/generate/model/hotsearch/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "time" + + "91porn-server/generate/common" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type MediaBookshelf struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty" comment:"文档id"` + Type int `json:"type" bson:"type" comment:"类型"` + Name string `json:"name" bson:"name" comment:"名称"` + MID primitive.ObjectID `json:"mid" bson:"mid" comment:"媒体ID"` + ReadHistory ReadHistory `json:"readHistory" bson:"readHistory" comment:"阅读记录"` + UID uint64 `json:"uid" bson:"uid" comment:"用户ID"` + IsDelete bool `json:"isDelete" bson:"isDelete" comment:"是否删除"` + ReadAt time.Time `json:"readAt" bson:"readAt" comment:"阅读时间"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt" comment:"创建时间"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt" comment:"更新时间"` +} + +type ReadHistory struct { + HasRead bool `json:"hasRead" bson:"hasRead"` // 是否已读 + CID primitive.ObjectID `json:"cid" bson:"cid"` // 子集ID + Name string `json:"name" bson:"name"` // 续看子集名称 +} + +func main() { + g := common.NewGen("91porn-server", "媒体书架列表") + g.Generate(MediaBookshelf{}) +} diff --git a/generate/model/imgroup/main.go b/generate/model/imgroup/main.go new file mode 100644 index 0000000..ef06797 --- /dev/null +++ b/generate/model/imgroup/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "91porn-server/generate/common" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +type ImGroup struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + GroupId int64 `json:"groupId" bson:"groupId" comment:"群id"` // 群id + Name string `json:"name" bson:"name" comment:"群组名"` // 群组名 + Cover string `json:"cover" bson:"cover" comment:"封面"` + Summary string `json:"summary" bson:"summary" comment:"简介"` // 简介 + MemberNum int64 `json:"memberNum" bson:"memberNum" comment:"成员数量(真实)"` // 成员数量(真实) + FakeMemberNum int64 `json:"fakeMemberNum" bson:"fakeMemberNum" comment:"成员数量(假的)"` // 成员数量(假的) + Price int64 `bson:"price" json:"price" comment:"加入群聊价格 0-免费"` // 加入群聊价格 0-免费 + CreatedAt time.Time `json:"createdAt" bson:"createdAt" comment:"文档创建时间"` // 文档创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt" comment:"文档更新时间"` // 文档更新时间 +} + +func main() { + g := common.NewGen("91porn-server", "im群组") + g.AppGenerate = true + g.Generate(ImGroup{}) +} diff --git a/generate/model/imgroupmember/main.go b/generate/model/imgroupmember/main.go new file mode 100644 index 0000000..71f0a2c --- /dev/null +++ b/generate/model/imgroupmember/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "91porn-server/generate/common" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +type ImGroupMember struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id" comment:"文档id"` + Uid int64 `json:"uid" bson:"uid" comment:"uid"` + Price int64 `json:"price" bson:"price" comment:"进群花费价格"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt" comment:"创建时间"` +} + +func main() { + g := common.NewGen("91porn-server", "im群组成员") + g.AppGenerate = true + g.Generate(ImGroupMember{}) +} diff --git a/generate/model/immessage/main.go b/generate/model/immessage/main.go new file mode 100644 index 0000000..80bc651 --- /dev/null +++ b/generate/model/immessage/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "91porn-server/generate/common" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +type ImMessage struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id" comment:"文档id"` + GroupId int64 `json:"groupId" bson:"groupId" comment:"群组id"` + Uid int64 `json:"uid" bson:"uid" comment:"用户id"` + Content string `json:"content" bson:"content" comment:"消息内容"` + Image string `json:"image" bson:"image" comment:"图片"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt" comment:"创建时间"` +} + +func main() { + g := common.NewGen("91porn-server", "im消息") + g.AppGenerate = true + g.Generate(ImMessage{}) +} diff --git a/generate/model/nakechatorder/main.go b/generate/model/nakechatorder/main.go new file mode 100644 index 0000000..76a041e --- /dev/null +++ b/generate/model/nakechatorder/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "91porn-server/generate/common" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +type NakedChatOrder struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id" comment:"文档id"` + Nid primitive.ObjectID `json:"nid" bson:"nid" comment:"裸聊id"` + Uid int64 `json:"uid" bson:"uid" comment:"uid"` + UserContact string `json:"userContact " bson:"userContact" comment:"用户联系方式"` + Num int64 `json:"num" bson:"num" comment:"购买数量"` + Price int64 `json:"price" bson:"price" comment:"购买的单价"` + Amount int64 `json:"amount" bson:"amount" comment:"总额"` + Remark string `json:"remark" bson:"remark" comment:"订单备注信息"` + Status int `json:"status" bson:"status" comment:"0-已下单 1-已完成"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt" comment:"文档创建时间"` // 文档创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt" comment:"文档更新时间"` // 文档更新时间 +} + +func main() { + g := common.NewGen("91porn-server", "裸聊订单") + g.AppGenerate = true + g.Generate(NakedChatOrder{}) +} diff --git a/generate/model/nakedchat/main.go b/generate/model/nakedchat/main.go new file mode 100644 index 0000000..6b0ffda --- /dev/null +++ b/generate/model/nakedchat/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "91porn-server/generate/common" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +type NakedChat struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id" comment:"文档id"` + Title string `json:"title" bson:"title" comment:"标题"` + Cover string `json:"cover" bson:"cover" comment:"封面"` + Price int64 `json:"price" bson:"price" comment:"价格 n金币/分钟"` + Options []int64 `json:"options" bson:"options" comment:"可供购买选项 ,多少分钟"` + Images []string `json:"images" bson:"images" comment:"图片列表"` + Video string `json:"video" bson:"video" comment:"展示的视频"` + Contact string `json:"contact " bson:"contact" comment:"联系方式"` + Age int `json:"age" bson:"age" comment:"年龄 单位 岁"` + Weight int `json:"weight" bson:"weight" comment:"体重 单位 kg"` + Cup string `json:"cup" bson:"cup" comment:"罩杯"` + SaleNum int64 `json:"saleNum" bson:"saleNum" comment:"销售数量"` // 销售数量 + FakeSaleNum int64 `json:"fakeSaleNum" bson:"fakeSaleNum" comment:"销售数量(假)"` // 销售数量(假) + BusinessHours string `json:"businessHours" bson:"businessHours" comment:"连线时间"` // 连线时间 + Summary string `json:"summary" bson:"summary" comment:"简介"` // 简介 + CreatedAt time.Time `json:"createdAt" bson:"createdAt" comment:"文档创建时间"` // 文档创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt" comment:"文档更新时间"` // 文档更新时间 +} + +func main() { + g := common.NewGen("91porn-server", "裸聊") + g.AppGenerate = true + g.Generate(NakedChat{}) +} diff --git a/generate/model/sysconf/main.go b/generate/model/sysconf/main.go new file mode 100644 index 0000000..ba54e24 --- /dev/null +++ b/generate/model/sysconf/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "time" + + "91porn-server/generate/common" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type SysConf struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty" comment:"文档id"` + + GroupName string `json:"groupName" bson:"groupName" comment:"分组名"` + GpCode string `json:"gpCode" bson:"gpCode" comment:"分组编码"` + VCode string `json:"vCode" bson:"vCode" comment:"变量名"` + Title string `json:"title" bson:"title" comment:"变量标题"` + Tip string `json:"tip" bson:"tip" comment:"变量描述"` + Type string `json:"type" bson:"type" comment:"类型:text,string,img,int,bool,object,text-array,string-array"` + Value string `json:"value" bson:"value" comment:"变量值"` + IsRequired bool `json:"is_required" bson:"is_required" comment:"是否必填"` + SortOrder int `json:"sort_order" bson:"sort_order" comment:"排序值"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt" comment:"创建时间"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt" comment:"更新时间"` +} + +func main() { + g := common.NewGen("91porn-server", "系统通用配置") + g.AppGenerate = false + g.Generate(SysConf{}) +} diff --git a/generate/tpl/app/api/{{PackageTableName}}.tpl b/generate/tpl/app/api/{{PackageTableName}}.tpl new file mode 100644 index 0000000..bf9a643 --- /dev/null +++ b/generate/tpl/app/api/{{PackageTableName}}.tpl @@ -0,0 +1,66 @@ +package {{PackageTableName}}ctrl + +import ( + "{{ModuleName}}/app/service/{{PackageTableName}}ser" + "{{ModuleName}}/common" + "{{ModuleName}}/common/stderr" + + "github.com/gin-gonic/gin" +) + +// List doc +// +// @Summary 获取{{TableComment}}列表接口 +// @Description 获取{{TableComment}}列表 +// @Tags 移动端-{{TableComment}} +// @Accept mpfd,json +// @Produce json +// @Param q query {{PackageTableName}}ser.{{VariablePrefix}}QueryListReq false "请求参数" +// @Success 200 object {{PackageTableName}}ser.{{VariablePrefix}}ListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/{{PackageTableName}}/list [get] +func List(ctx *gin.Context) { + p := &{{PackageTableName}}ser.{{VariablePrefix}}QueryListReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + // 获取用户当前配置 + // var err error + // p.Uid, err = common.GetUID(ctx) + // if err != nil { + // common.ServeJSON(ctx, stderr.ErrNoToken, err) + // return + // } + + list := p.GetList() + common.ServeJSON(ctx, stderr.Success, list) +} + +// Info doc +// +// @Summary 获取{{TableComment}}详情接口 +// @Description 获取{{TableComment}}详情 +// @Tags 移动端-{{TableComment}} +// @Accept mpfd,json +// @Produce json +// @Param q query {{PackageTableName}}ser.{{VariablePrefix}}QueryInfoReq false "请求参数" +// @Success 200 object {{PackageTableName}}ser.{{VariablePrefix}}QueryInfoRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/app/{{PackageTableName}}/info [get] +func Info(ctx *gin.Context) { + p := &{{PackageTableName}}ser.{{VariablePrefix}}QueryInfoReq{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + data, err := p.GetInfo() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/generate/tpl/app/router/{{PackageTableName}}.tpl b/generate/tpl/app/router/{{PackageTableName}}.tpl new file mode 100644 index 0000000..a5c8f69 --- /dev/null +++ b/generate/tpl/app/router/{{PackageTableName}}.tpl @@ -0,0 +1,15 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "{{ModuleName}}/app/api/{{PackageTableName}}ctrl" +) + +// {{RouterName}} 移动端接口 todo: 在 app/router/base.go 中新增路由 +func {{RouterName}}Router(router *gin.RouterGroup) { + group := router.Group("/{{PackageTableName}}") + { + group.GET("/list", {{PackageTableName}}ctrl.List) + group.GET("/info", {{PackageTableName}}ctrl.Info) + } +} diff --git a/generate/tpl/app/service/{{PackageTableName}}.tpl b/generate/tpl/app/service/{{PackageTableName}}.tpl new file mode 100644 index 0000000..030fbd0 --- /dev/null +++ b/generate/tpl/app/service/{{PackageTableName}}.tpl @@ -0,0 +1,63 @@ +package {{PackageTableName}}ser + +import ( + "{{ModuleName}}/common/log" + "{{ModuleName}}/models/cache/{{PackageTableName}}data" + "{{ModuleName}}/models/commod" + "{{ModuleName}}/models/v/{{PackageTableName}}mod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type {{VariablePrefix}}QueryListReq struct { + commod.Page +} + +type {{VariablePrefix}}ListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []*{{PackageTableName}}mod.{{StructName}}Info `json:"list"` +} + +// GetList 获取列表 +func (p *{{VariablePrefix}}QueryListReq) GetList() {{VariablePrefix}}ListRes { + var res {{VariablePrefix}}ListRes + var err error + filter := bson.M{} + + sort := bson.D{{"_id", -1}} + // 获取列表 + var data []{{PackageTableName}}mod.{{StructName}} + data, res.Total, res.HasNext, err = {{PackageTableName}}data.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort) + if err != nil { + log.Error("获取{{TableComment}}列表数据错误", log.Any("Params", *p), log.E(err)) + return res + } + res.List = {{PackageTableName}}data.FormatAppDataList(data) + + return res +} + +type {{VariablePrefix}}QueryInfoReq struct { + ID string `json:"id" form:"id"` // id +} +type {{VariablePrefix}}QueryInfoRes = *{{PackageTableName}}mod.{{StructName}}Info + +// GetInfo 获取详情 +func (p *{{VariablePrefix}}QueryInfoReq) GetInfo() (res {{VariablePrefix}}QueryInfoRes, err error) { + oid, err := primitive.ObjectIDFromHex(p.ID) + if err != nil { + return res, err + } + + var item {{PackageTableName}}mod.{{StructName}} + item, err = {{PackageTableName}}data.GetInfoFromCache(oid) + if err != nil { + log.Error("获取{{TableComment}}详情数据错误", log.Any("ID", p.ID), log.E(err)) + return + } + + res = {{PackageTableName}}data.FormatAppData(item) + return +} diff --git a/generate/tpl/config.go b/generate/tpl/config.go new file mode 100644 index 0000000..105ab98 --- /dev/null +++ b/generate/tpl/config.go @@ -0,0 +1,44 @@ +package tpl + +type Tpl struct { + SplitDir bool + DirSuffix string + VariablePrefix string +} + +var Config = map[string]Tpl{ + "/app/api": { + SplitDir: true, + DirSuffix: "ctrl", + VariablePrefix: "App", + }, + "/app/router": { + SplitDir: false, + }, + "/app/service": { + SplitDir: true, + DirSuffix: "ser", + VariablePrefix: "App", + }, + "/models/cache": { + SplitDir: true, + DirSuffix: "data", + }, + "/models/v": { + SplitDir: true, + DirSuffix: "mod", + }, + "/web/api": { + SplitDir: true, + DirSuffix: "ctrl", + VariablePrefix: "Web", + }, + "/web/router": { + SplitDir: false, + }, + "/web/service": { + SplitDir: true, + DirSuffix: "ser", + VariablePrefix: "Web", + }, +} diff --git a/generate/tpl/models/cache/data.tpl b/generate/tpl/models/cache/data.tpl new file mode 100644 index 0000000..7a43e1e --- /dev/null +++ b/generate/tpl/models/cache/data.tpl @@ -0,0 +1,34 @@ +package {{PackageTableName}}data + +import ( + "{{ModuleName}}/models/v/{{PackageTableName}}mod" +) + +// FormatAppDataList 格式化app列表数据 +func FormatAppDataList(origin []{{PackageTableName}}mod.{{StructName}}) (res []*{{PackageTableName}}mod.{{StructName}}Info) { + res = make([]*{{PackageTableName}}mod.{{StructName}}Info, len(origin)) + if len(origin) == 0 { + return + } + + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppData(item) + } + + return +} + +// FormatAppData 格式化app数据 +func FormatAppData(item {{PackageTableName}}mod.{{StructName}}) (res *{{PackageTableName}}mod.{{StructName}}Info) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res = &{{PackageTableName}}mod.{{StructName}}Info{ + {{AppRespTransferStatement}} + } + + return +} diff --git a/generate/tpl/models/cache/{{PackageTableName}}.tpl b/generate/tpl/models/cache/{{PackageTableName}}.tpl new file mode 100644 index 0000000..564803e --- /dev/null +++ b/generate/tpl/models/cache/{{PackageTableName}}.tpl @@ -0,0 +1,89 @@ +package {{PackageTableName}}data + +import ( + "time" + + "{{ModuleName}}/common/cachev2" + "{{ModuleName}}/common/db" + "{{ModuleName}}/models" + "{{ModuleName}}/models/v/{{PackageTableName}}mod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []{{PackageTableName}}mod.{{StructName}} + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]{{PackageTableName}}mod.{{StructName}}, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(10*time.Minute).AutoListKey(models.{{TableName}}).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = {{PackageTableName}}mod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res {{PackageTableName}}mod.{{StructName}}, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoInfoKey(models.{{TableName}}, id.Hex()).ResBind(&res).Cache({{PackageTableName}}mod.GetInfo, id) + return +} + +// GetAllFromCache 获取所有数据 +func GetAllFromCache(cond bson.M, sort bson.D) (res []{{PackageTableName}}mod.{{StructName}}, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoListKey(models.{{TableName}}+"-all").ResBind(&res).Cache({{PackageTableName}}mod.GetAll, cond, sort) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data {{PackageTableName}}mod.{{StructName}}) (primitive.ObjectID, error) { + newID, err := {{PackageTableName}}mod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.{{TableName}}).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := {{PackageTableName}}mod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.{{TableName}}).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = {{PackageTableName}}mod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.{{TableName}}).AutoClear(true, &all) + return +} diff --git a/generate/tpl/models/v/app.tpl b/generate/tpl/models/v/app.tpl new file mode 100644 index 0000000..30a7211 --- /dev/null +++ b/generate/tpl/models/v/app.tpl @@ -0,0 +1,12 @@ +package {{PackageTableName}}mod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// {{StructName}}Info 移动端返回内容 +type {{StructName}}Info struct { + {{AppRespStructFields}} +} diff --git a/generate/tpl/models/v/type.tpl b/generate/tpl/models/v/type.tpl new file mode 100644 index 0000000..dda2753 --- /dev/null +++ b/generate/tpl/models/v/type.tpl @@ -0,0 +1,44 @@ +package {{PackageTableName}}mod + +import ( + "fmt" + "time" + + "{{ModuleName}}/common/db" + "{{ModuleName}}/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +// todo: 在models/table.go中创建表名 +const table = models.{{TableName}} + +{{StructDefine}} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 todo: 在models/Init/init.go中调用初始化 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "_id", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} diff --git a/generate/tpl/models/v/{{PackageTableName}}.tpl b/generate/tpl/models/v/{{PackageTableName}}.tpl new file mode 100644 index 0000000..cab15df --- /dev/null +++ b/generate/tpl/models/v/{{PackageTableName}}.tpl @@ -0,0 +1,144 @@ +package {{PackageTableName}}mod + +import ( + "encoding/json" + "errors" + "fmt" + + "{{ModuleName}}/common/db" + "{{ModuleName}}/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []{{StructName}}, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + + +// GetAll 查询全部文档 +func GetAll(filter primitive.M, sort bson.D) (out []*{{StructName}}, err error) { + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAll", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) ({{StructName}}, error) { + v := {{StructName}}{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d {{StructName}}) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} diff --git a/generate/tpl/web/api/{{PackageTableName}}.tpl b/generate/tpl/web/api/{{PackageTableName}}.tpl new file mode 100644 index 0000000..90b7246 --- /dev/null +++ b/generate/tpl/web/api/{{PackageTableName}}.tpl @@ -0,0 +1,142 @@ +package {{PackageTableName}}ctrl + +import ( + "encoding/json" + + "github.com/gin-gonic/gin" + "{{ModuleName}}/common" + "{{ModuleName}}/common/stderr" + "{{ModuleName}}/models/l/operatorlgmod" + "{{ModuleName}}/web/service/{{PackageTableName}}ser" +) + +// List doc +// @Summary 获取{{TableComment}}列表 +// @Description 获取{{TableComment}}列表 +// @Tags 后台-{{TableComment}} +// @Accept mpfd,json +// @Produce json +// @Param q query {{PackageTableName}}ser.{{VariablePrefix}}ListReq false "请求参数" +// @Success 200 object {{PackageTableName}}ser.{{VariablePrefix}}ListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/{{PackageTableName}}/list [get] +func List(ctx *gin.Context) { + var req = &{{PackageTableName}}ser.{{VariablePrefix}}ListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Create doc +// @Summary 新增{{TableComment}} +// @Description 新增{{TableComment}} +// @Tags 后台-{{TableComment}} +// @Accept mpfd,json +// @Produce json +// @Param q body {{PackageTableName}}ser.{{VariablePrefix}}CreateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/{{PackageTableName}}/create [post] +func Create(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &{{PackageTableName}}ser.{{VariablePrefix}}CreateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 创建 + err = p.Create() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "{{TableComment}}管理", "创建", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 更新{{TableComment}} +// @Description 更新{{TableComment}} +// @Tags 后台-{{TableComment}} +// @Accept mpfd,json +// @Produce json +// @Param q body {{PackageTableName}}ser.{{VariablePrefix}}UpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/{{PackageTableName}}/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &{{PackageTableName}}ser.{{VariablePrefix}}UpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "{{TableComment}}管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除{{TableComment}} +// @Description 删除{{TableComment}} +// @Tags 后台-{{TableComment}} +// @Accept mpfd,json +// @Produce json +// @Param q body {{PackageTableName}}ser.{{VariablePrefix}}DeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/{{PackageTableName}}/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &{{PackageTableName}}ser.{{VariablePrefix}}DeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = p.Delete() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "{{TableComment}}管理", "删除", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/generate/tpl/web/router/{{PackageTableName}}.tpl b/generate/tpl/web/router/{{PackageTableName}}.tpl new file mode 100644 index 0000000..6e30cfa --- /dev/null +++ b/generate/tpl/web/router/{{PackageTableName}}.tpl @@ -0,0 +1,17 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "{{ModuleName}}/web/api/{{PackageTableName}}ctrl" +) + +// {{RouterName}}Router {{TableComment}}管理 todo: 在 web/router/base.go 中新增路由 +func {{RouterName}}Router(router *gin.RouterGroup) { + group := router.Group("/{{PackageTableName}}") + { + group.GET("/list", {{PackageTableName}}ctrl.List) + group.POST("/create", {{PackageTableName}}ctrl.Create) + group.POST("/update", {{PackageTableName}}ctrl.Update) + group.POST("/delete", {{PackageTableName}}ctrl.Delete) + } +} diff --git a/generate/tpl/web/service/{{PackageTableName}}.tpl b/generate/tpl/web/service/{{PackageTableName}}.tpl new file mode 100644 index 0000000..023a7e8 --- /dev/null +++ b/generate/tpl/web/service/{{PackageTableName}}.tpl @@ -0,0 +1,81 @@ +package {{PackageTableName}}ser + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "{{ModuleName}}/models/commod" + "{{ModuleName}}/models/cache/{{PackageTableName}}data" + "{{ModuleName}}/models/v/{{PackageTableName}}mod" +) + +type {{VariablePrefix}}ListReq struct { + commod.Page +} +type {{VariablePrefix}}ListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []{{PackageTableName}}mod.{{StructName}} `json:"list"` +} + +// GetList 获取列表 +func (q *{{VariablePrefix}}ListReq) GetList() (res {{VariablePrefix}}ListRes, err error) { + filter := bson.M{} + res.List, res.Total, res.HasNext, err = {{PackageTableName}}mod.GetList(filter, int64(q.Skip()), int64(q.Limit()), q.GetSort()) + + return +} + +type {{VariablePrefix}}CreateReq struct { + {{CreateReqFields}} +} + +// Create 发布数据 +func (p *{{VariablePrefix}}CreateReq) Create() error { + data := {{PackageTableName}}mod.{{StructName}}{ + {{CreateStatement}} + } + + // 创建数据 + if _, err := {{PackageTableName}}data.InsertData(nil, data); err != nil { + return err + } + + return nil +} + +type {{VariablePrefix}}UpdateReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` + {{UpdateReqFields}} +} + +// Update 更新数据 +func (p *{{VariablePrefix}}UpdateReq) Update() error { + _, err := {{PackageTableName}}mod.GetInfo(p.ID) + if err != nil { + return err + } + + data := make(map[string]interface{}) + {{UpdateStatement}} + + if _, err = {{PackageTableName}}data.UpdateData(nil, p.ID.Hex(), data); err != nil { + return err + } + + return nil +} + +type {{VariablePrefix}}DeleteReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` +} + +// Delete 删除数据 +func (p *{{VariablePrefix}}DeleteReq) Delete() error { + if err := {{PackageTableName}}data.DeleteData(nil, p.ID); err != nil { + return err + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..43a7c24 --- /dev/null +++ b/go.mod @@ -0,0 +1,134 @@ +module 91porn-server + +go 1.23.0 + +require ( + github.com/IBM/sarama v1.45.2 + github.com/PuerkitoBio/goquery v1.8.1 + github.com/Shopify/sarama v1.38.1 + github.com/aws/aws-sdk-go v1.44.260 + github.com/badoux/checkmail v1.2.1 + github.com/bxcodec/faker/v3 v3.1.0 + github.com/ddliu/go-httpclient v0.7.1 + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 + github.com/didip/tollbooth v4.0.2+incompatible + github.com/didip/tollbooth_gin v0.0.0-20170928041415-5752492be505 + github.com/elastic/go-elasticsearch/v8 v8.8.0 + github.com/fatih/structs v1.1.0 + github.com/gin-contrib/pprof v1.4.0 + github.com/gin-gonic/gin v1.9.1 + github.com/go-ego/gpy v0.42.1 + github.com/go-playground/form v3.1.4+incompatible + github.com/go-playground/validator/v10 v10.14.0 + github.com/go-redis/redis v6.15.9+incompatible + github.com/go-redsync/redsync/v4 v4.8.1 + github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible + github.com/golang-module/carbon/v2 v2.3.10 + github.com/google/uuid v1.3.0 + github.com/grafov/m3u8 v0.12.0 + github.com/jinzhu/copier v0.3.5 + github.com/jinzhu/now v1.1.5 + github.com/makiuchi-d/gozxing v0.1.1 + github.com/moul/http2curl v1.0.0 + github.com/mozillazg/go-pinyin v0.19.0 + github.com/nyaruka/phonenumbers v1.1.6 + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/pkg/errors v0.9.1 + github.com/robfig/cron/v3 v3.0.1 + github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 + github.com/satori/go.uuid v1.2.0 + github.com/shopspring/decimal v1.3.1 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e + github.com/smallnest/weighted v0.0.0-20230419055410-36b780e40a7a + github.com/storyicon/grbac v0.0.0-20200224041032-a0461737df7e + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/tealeg/xlsx v1.0.5 + github.com/vmihailenco/msgpack/v5 v5.4.1 + github.com/xuri/excelize/v2 v2.7.1 + go.mongodb.org/mongo-driver v1.11.7 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.38.0 + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/andybalholm/cascadia v1.3.2 // indirect + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/eapache/go-resiliency v1.7.0 // indirect + github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect + github.com/eapache/queue v1.1.0 // indirect + github.com/elastic/elastic-transport-go/v8 v8.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-ego/gse v0.80.2 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/spec v0.20.9 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/golang-lru v0.6.0 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/montanaflynn/stats v0.7.0 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.3 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/smartystreets/goconvey v1.8.1 // indirect + github.com/swaggo/swag v1.16.1 // indirect + github.com/technoweenie/multipartstreamer v1.0.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + github.com/vcaesar/cedar v0.20.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 // indirect + github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83 // indirect + github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/go-playground/assert.v1 v1.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..985db1d --- /dev/null +++ b/go.sum @@ -0,0 +1,540 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/IBM/sarama v1.45.2 h1:8m8LcMCu3REcwpa7fCP6v2fuPuzVwXDAM2DOv3CBrKw= +github.com/IBM/sarama v1.45.2/go.mod h1:ppaoTcVdGv186/z6MEKsMm70A5fwJfRTpstI37kVn3Y= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= +github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= +github.com/Shopify/sarama v1.38.1 h1:lqqPUPQZ7zPqYlWpTh+LQ9bhYNu2xJL6k1SJN4WVe2A= +github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSdZ4X2o5g= +github.com/Shopify/toxiproxy/v2 v2.5.0 h1:i4LPT+qrSlKNtQf5QliVjdP08GyAH8+BUIc9gT0eahc= +github.com/Shopify/toxiproxy/v2 v2.5.0/go.mod h1:yhM2epWtAmel9CB8r2+L+PCmhH6yH2pITaPAo7jxJl0= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/aws/aws-sdk-go v1.44.260 h1:78IJkDpDPXvLXvIkNAKDP/i3z8Vj+3sTAtQYw/v/2o8= +github.com/aws/aws-sdk-go v1.44.260/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/badoux/checkmail v1.2.1 h1:TzwYx5pnsV6anJweMx2auXdekBwGr/yt1GgalIx9nBQ= +github.com/badoux/checkmail v1.2.1/go.mod h1:XroCOBU5zzZJcLvgwU15I+2xXyCdTWXyR9MGfRhBYy0= +github.com/bsm/ginkgo/v2 v2.5.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= +github.com/bsm/gomega v1.20.0/go.mod h1:JifAceMQ4crZIWYUKrlGcmbN3bqHogVTADMD2ATsbwk= +github.com/bxcodec/faker/v3 v3.1.0 h1:VCCPusvvk1My6RjWFnqVbh6EdHDqjWmrHJCHduUksV0= +github.com/bxcodec/faker/v3 v3.1.0/go.mod h1:gF31YgnMSMKgkvl+fyEo1xuSMbEuieyqfeslGYFjneM= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ddliu/go-httpclient v0.7.1 h1:COWYBalfbaFNe6e0eQU38++vCD5kzLh1H1RFs3xcn9g= +github.com/ddliu/go-httpclient v0.7.1/go.mod h1:uwipe9x9SYGk4JhBemO7+dD87QbiY224y0DLB9OY0Ik= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 h1:AqeKSZIG/NIC75MNQlPy/LM3LxfpLwahICJBHwSMFNc= +github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFauPHz7+NnjR/yHJGhrKo1Za+zStgwUETx3yzqgY= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M= +github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY= +github.com/didip/tollbooth_gin v0.0.0-20170928041415-5752492be505 h1:VkJBA707rG0mOUM5nuqTs53hlJEb6peXnY7elFDWh88= +github.com/didip/tollbooth_gin v0.0.0-20170928041415-5752492be505/go.mod h1:ieayd+rxBVaj62fhAdF5p1U70Y4ZCcfpk0+4jesd0f8= +github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= +github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/elastic/elastic-transport-go/v8 v8.0.0-20230329154755-1a3c63de0db6/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI= +github.com/elastic/elastic-transport-go/v8 v8.2.0 h1:hkK5IIs/15mpSXzd5THWVlWTKJyMw6cbCWM3T/B2S5E= +github.com/elastic/elastic-transport-go/v8 v8.2.0/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI= +github.com/elastic/go-elasticsearch/v8 v8.8.0 h1:yNBPlXNo6wstMG7I3KiZPbLFgA82RMryYqkh1xBMV3A= +github.com/elastic/go-elasticsearch/v8 v8.8.0/go.mod h1:NGmpvohKiRHXI0Sw4fuUGn6hYOmAXlyCphKpzVBiqDE= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg= +github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-ego/gpy v0.42.1 h1:P/doarxZi2J5qwNzLIM0ESd3VXLXMpdhwO/GNontLdE= +github.com/go-ego/gpy v0.42.1/go.mod h1:eBtY3/uCDqBFog4ES9L00xytTpXar/0mGf45Rb7zcgg= +github.com/go-ego/gse v0.80.2 h1:3LRfkaBuwlsHsmkOZvnhTcsYPXUAhiP06Sqcid7mO1M= +github.com/go-ego/gse v0.80.2/go.mod h1:kesekpZfcFQ/kwd9b27VZHUOH5dQUjaaQUZ4OGt4Hj4= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= +github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/form v3.1.4+incompatible h1:lvKiHVxE2WvzDIoyMnWcjyiBxKt2+uFJyZcPYWsLnjI= +github.com/go-playground/form v3.1.4+incompatible/go.mod h1:lhcKXfTuhRtIZCIKUeJ0b5F207aeQCPbZU09ScKjwWg= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4= +github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= +github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= +github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= +github.com/go-redsync/redsync/v4 v4.8.1 h1:rq2RvdTI0obznMdxKUWGdmmulo7lS9yCzb8fgDKOlbM= +github.com/go-redsync/redsync/v4 v4.8.1/go.mod h1:LmUAsQuQxhzZAoGY7JS6+dNhNmZyonMZiiEDY9plotM= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU= +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-module/carbon/v2 v2.3.10 h1:C25x4A4UrIch6bisV3j37eU+op5+cp4gw/Fffv5c/FA= +github.com/golang-module/carbon/v2 v2.3.10/go.mod h1:XDALX7KgqmHk95xyLeaqX9/LJGbfLATyruTziq68SZ8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= +github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/grafov/m3u8 v0.12.0 h1:T6iTwTsSEtMcwkayef+FJO8kj+Sglr4Lh81Zj8Ked/4= +github.com/grafov/m3u8 v0.12.0/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= +github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= +github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I= +github.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= +github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/mozillazg/go-pinyin v0.19.0 h1:p+J8/kjJ558KPvVGYLvqBhxf8jbZA2exSLCs2uUVN8c= +github.com/mozillazg/go-pinyin v0.19.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/nyaruka/phonenumbers v1.1.6 h1:DcueYq7QrOArAprAYNoQfDgp0KetO4LqtnBtQC6Wyes= +github.com/nyaruka/phonenumbers v1.1.6/go.mod h1:yShPJHDSH3aTKzCbXyVxNpbl2kA+F+Ne5Pun/MvFRos= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= +github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/redis/go-redis/v9 v9.0.2 h1:BA426Zqe/7r56kCcvxYLWe1mkaz71LKF77GwgFzSxfE= +github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM= +github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/robfig/cron v1.1.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko= +github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/smallnest/weighted v0.0.0-20230419055410-36b780e40a7a h1:eieNTZmrnPzIBWd/tAc2+60qroyOzAoM/Q3FiTwHG1o= +github.com/smallnest/weighted v0.0.0-20230419055410-36b780e40a7a/go.mod h1:xc9CoZ+ZBGwajnWto5Aqw/wWg8euy4HtOr6K9Fxp9iw= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/storyicon/grbac v0.0.0-20200224041032-a0461737df7e h1:v/SEKCJ/DsYAl51ROfVsoEm76SegQiAh8RPOPGGZ580= +github.com/storyicon/grbac v0.0.0-20200224041032-a0461737df7e/go.mod h1:bkwjmFAuumy2DsLyhHzgPWC6hEFjevSmI7iabVzBJ7A= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= +github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg= +github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto= +github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE= +github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM= +github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= +github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/vcaesar/cedar v0.20.1 h1:cDOmYWdprO7ZW8cngJrDi8Zivnscj9dA/y8Y+2SB1P0= +github.com/vcaesar/cedar v0.20.1/go.mod h1:iMDweyuW76RvSrCkQeZeQk4iCbshiPzcCvcGCtpM7iI= +github.com/vcaesar/tt v0.20.0 h1:9t2Ycb9RNHcP0WgQgIaRKJBB+FrRdejuaL6uWIHuoBA= +github.com/vcaesar/tt v0.20.0/go.mod h1:GHPxQYhn+7OgKakRusH7KJ0M5MhywoeLb8Fcffs/Gtg= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 h1:ge5g8vsTQclA5lXDi+PuiAFw5GMIlMHOB/5e1hsf96E= +github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.7.1 h1:gm8q0UCAyaTt3MEF5wWMjVdmthm2EHAWesGSKS9tdVI= +github.com/xuri/excelize/v2 v2.7.1/go.mod h1:qc0+2j4TvAUrBw36ATtcTeC1VCM0fFdAXZOmcF4nTpY= +github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83 h1:xVwnvkzzi+OiwhIkWOXvh1skFI6bagk8OvGuazM80Rw= +github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= +github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.11.7 h1:LIwYxASDLGUg/8wOhgOOZhX8tQa/9tgZPgzZoVqJvcs= +go.mongodb.org/mongo-driver v1.11.7/go.mod h1:G9TgswdsWjX4tmDA5zfs2+6AEPpYJwqblyjsfuh8oXY= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= +golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..1057f7a --- /dev/null +++ b/main.go @@ -0,0 +1,38 @@ +package main + +import ( + _ "91porn-server/docs" + + "github.com/gin-gonic/gin" +) + +var SwagHandler gin.HandlerFunc + +// @title 91porn-server 项目API文档 +// @version 1.0 +// @description 项目接口文档 +// @termsOfService 91porn-server + +// @contact.name 91porn +// @contact.url 91porn@gamil.com +// @contact.email 91porn@gamil.com + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @Param Authorization header string true "token" default(A) +// @host 192.168.2.195:8182 +// @BasePath /api +// @securityDefinitions.apikey ApiKeyAuth +// @in header +// @name Authorization + +func main() { + gin.SetMode(gin.ReleaseMode) + r := gin.Default() + if SwagHandler != nil { + r.GET("/swagger/*any", SwagHandler) + } + println("swag server start list port:[20114]\nAPI URL: https://91pornht.cestalt.com/swagger/index.html") + _ = r.Run(":20114") +} diff --git a/middleware/cors/cors.go b/middleware/cors/cors.go new file mode 100644 index 0000000..eae2f75 --- /dev/null +++ b/middleware/cors/cors.go @@ -0,0 +1,133 @@ +package cors + +import ( + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" +) + +type Options struct { + Origin *[]string //传入空指针,表示允许"*", 传入空数组表示禁止跨域 + Methods []string //Methods + AllowedHeaders []string //AllowHeaders + ExposedHeaders []string //exposeHeader + Credentials bool //cookie + MaxAge int64 //缓存MaxAge + PreflightContinue bool //在遇到Options的时候,继续,而不是返回请求 + OptionsSuccessStatus int //Options时,返回的状态码,默认204 +} + +func configureOrigin(o *Options, c *gin.Context) map[string]string { + reqOrigin := c.GetHeader("Origin") + if o.Origin == nil { + return map[string]string{"Access-Control-Allow-Origin": "*"} + } + headers := make(map[string]string) + isAllowed := false + for _, allowed := range *o.Origin { + if allowed == reqOrigin { + isAllowed = true + break + } + } + if isAllowed { + headers["Access-Control-Allow-Origin"] = reqOrigin + } else { + headers["Access-Control-Allow-Origin"] = strconv.FormatBool(false) + } + headers["Vary"] = "Origin" + return headers +} + +func configureMethods(o *Options) map[string]string { + headers := make(map[string]string) + if len(o.Methods) > 0 { + headers["Access-Control-Allow-Methods"] = strings.ToUpper(strings.Join(o.Methods, ",")) + } + return headers +} + +func configureCredentials(o *Options) map[string]string { + headers := make(map[string]string) + if o.Credentials { + headers["Access-Control-Allow-Credentials"] = strconv.FormatBool(true) + } + return headers +} + +func configureAllowedHeaders(o *Options, c *gin.Context) map[string]string { + headers := make(map[string]string) + if len(o.AllowedHeaders) > 0 { + headers["Access-Control-Allow-Headers"] = strings.Join(o.AllowedHeaders, ",") + } + return headers +} + +func configureExposedHeaders(o *Options) map[string]string { + var headers = make(map[string]string) + if len(o.ExposedHeaders) > 0 { + headers["Access-Control-Expose-Headers"] = strings.Join(o.ExposedHeaders, ",") + } + return headers +} + +func configureMaxAge(o *Options) map[string]string { + var headers = make(map[string]string) + if o.MaxAge >= 0 { + headers["Access-Control-Max-Age"] = strconv.FormatInt(o.MaxAge, 10) + } + return headers +} + +func mergeMap(m ...map[string]string) map[string]string { + r := make(map[string]string) + for _, t := range m { + for k, v := range t { + r[k] = v + } + } + return r +} + +func applyHeader(c *gin.Context, h map[string]string) { + for k, v := range h { + c.Header(k, v) + } +} + +// Cors 跨域处理 +func Cors(o *Options) gin.HandlerFunc { + if o.OptionsSuccessStatus == 0 { + o.OptionsSuccessStatus = http.StatusNoContent + } + return func(c *gin.Context) { + method := c.Request.Method + //c.Header("Access-Control-Allow-Origin", "*") + //c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token,Authorization,Token,Content-Length,Etag,Content-Range,Accept-Ranges,User-Agent,Range,Bucket,Content-Disposition,Signature,X-Forwarded-For,X-Real-Ip") + if method == "OPTIONS" { + h := mergeMap( + configureOrigin(o, c), + configureCredentials(o), + configureMethods(o), + configureAllowedHeaders(o, c), + configureMaxAge(o), + configureExposedHeaders(o), + ) + applyHeader(c, h) + if o.PreflightContinue { + return + } + c.Header("Content-Length", "0") + c.AbortWithStatus(o.OptionsSuccessStatus) + return + } + h := mergeMap( + configureOrigin(o, c), + configureCredentials(o), + configureExposedHeaders(o), + ) + applyHeader(c, h) + } +} diff --git a/middleware/ginzap/ginzap.go b/middleware/ginzap/ginzap.go new file mode 100644 index 0000000..e009241 --- /dev/null +++ b/middleware/ginzap/ginzap.go @@ -0,0 +1,100 @@ +package ginzap + +import ( + "net" + "net/http" + "os" + "runtime/debug" + "strings" + "time" + + "91porn-server/common/hevcpull" + "91porn-server/common/log" + + "github.com/gin-gonic/gin" +) + +func Logger(ctxKeys []string) gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + query := hevcpull.RedactText(c.Request.URL.RawQuery) + c.Next() + end := time.Now() + latency := end.Sub(start) + if len(c.Errors) > 0 { + for _, e := range c.Errors.Errors() { + log.Error(e, log.R(c)) + } + } else { + l := []log.Field{ + log.R(c), + log.Any("status", c.Writer.Status()), + log.Any("method", c.Request.Method), + log.Any("path", path), + log.Any("query", query), + log.Any("user-agent", c.Request.UserAgent()), + log.Any("time", end.Format("2006-01-02 15:04:05")), + log.Any("latency", latency), + } + for _, k := range ctxKeys { + v, e := c.Get(k) + if e { + l = append(l, log.Any(k, v)) + } + } + log.Info(path, l...) + } + } +} + +// RecoveryWithZap returns a gin.HandlerFunc (middleware) +// that recovers from any panics and logs requests using uber-go/zap. +// All errors are logged using zap.Error(). +// stack means whether output the stack info. +// The stack info is easy to find where the error occurs but the stack info is too large. +func Recovery(stack bool, do func(err interface{})) gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + if do != nil { + do(err) + } + // Check for a broken connection, as it is not really a + // condition that warrants a panic stack trace. + var brokenPipe bool + if ne, ok := err.(*net.OpError); ok { + if se, ok := ne.Err.(*os.SyscallError); ok { + if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { + brokenPipe = true + } + } + } + + // Record request identity without exposing authentication headers or body. + requestFields := []log.Field{ + log.R(c), + log.Any("method", c.Request.Method), + log.Any("path", c.Request.URL.Path), + log.Any("panic", err), + } + if brokenPipe { + log.Error(c.Request.URL.Path, requestFields...) + // If the connection is dead, we can't write a status to it. + c.Error(err.(error)) // nolint: errcheck + c.Abort() + return + } + if stack { + requestFields = append(requestFields, log.Any("stack", string(debug.Stack()))) + } + log.Error("[Recovery from panic]", requestFields...) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ + "result": false, + "error": http.StatusText(http.StatusInternalServerError), + }) + } + }() + c.Next() + } +} diff --git a/middleware/ginzap/ginzap_test.go b/middleware/ginzap/ginzap_test.go new file mode 100644 index 0000000..0ce4406 --- /dev/null +++ b/middleware/ginzap/ginzap_test.go @@ -0,0 +1,17 @@ +package ginzap + +import ( + "strings" + "testing" + + "91porn-server/common/hevcpull" +) + +func TestSignedPullQueryIsRedactedBeforeLogging(t *testing.T) { + const signature = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + query := "hevc_exp=1&hevc_sig=" + signature + safe := hevcpull.RedactText(query) + if strings.Contains(safe, signature) || !strings.Contains(safe, "[REDACTED]") { + t.Fatalf("signed pull query was not redacted: %s", safe) + } +} diff --git a/middleware/ip/ip.go b/middleware/ip/ip.go new file mode 100644 index 0000000..f0f162f --- /dev/null +++ b/middleware/ip/ip.go @@ -0,0 +1,37 @@ +package ip + +import ( + "net/http" + + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/v/ipwhitemod" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" +) + +// RealIP 获取真实IP +func RealIP(c *gin.Context) { + ip := c.ClientIP() + c.Set(constant.CtxIP, ip) +} + +var accessForbidMsg = gin.H{ + "code": stderr.ErrAccessForbid, + "msg": stderr.ErrAccessForbid.Msg(), +} + +// RealIP 获取真实IP +func CheckWhiteIP(ctx *gin.Context) { + //检查ip是否在白名单中 + // ipFlag, err := webg.Redis.SISMember(constant.IPWhiteRedisKey, constant.CtxIP) + // if webg.Conf.EnableIPWhite.IsEnable && (err != nil || !ipFlag) { + if webg.Conf.EnableIPWhite.IsEnable { + iPWhite, err := ipwhitemod.FindOneByIp(ctx.ClientIP()) + if err != nil || iPWhite.IP == "" { + ctx.AbortWithStatusJSON(http.StatusOK, accessForbidMsg) + return + } + } +} diff --git a/middleware/requestid/requestid.go b/middleware/requestid/requestid.go new file mode 100644 index 0000000..998a27d --- /dev/null +++ b/middleware/requestid/requestid.go @@ -0,0 +1,39 @@ +package requestid + +import ( + "strings" + + "github.com/gin-gonic/gin" +) + +func New(opts ...Option) gin.HandlerFunc { + var cfg config = config{ + generator: DefaultFenerator, + } + for _, opt := range opts { + opt(&cfg) + } + return func(c *gin.Context) { + rid := strings.TrimSpace(c.GetHeader(HeaderKey)) + clientProvided := rid != "" && len(rid) <= 128 + if !clientProvided { + rid = cfg.generator() + } + c.Request.Header.Set(HeaderKey, rid) + c.Header(HeaderKey, rid) + c.Set(ContextKey, rid) + c.Set(ClientProvidedContextKey, clientProvided) + } +} + +// FromClient 仅返回客户端显式提供且长度合规的请求ID。 +func FromClient(c *gin.Context) (string, bool) { + if c == nil { + return "", false + } + provided, ok := c.Get(ClientProvidedContextKey) + if !ok || provided != true { + return "", false + } + return c.GetHeader(HeaderKey), true +} diff --git a/middleware/requestid/requestid_test.go b/middleware/requestid/requestid_test.go new file mode 100644 index 0000000..c869237 --- /dev/null +++ b/middleware/requestid/requestid_test.go @@ -0,0 +1,82 @@ +package requestid + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestMiddlewareClientProvidedRequestID(t *testing.T) { + gin.SetMode(gin.TestMode) + + var ( + gotID string + gotProvided bool + ) + router := gin.New() + router.Use(New(WithGenerator(func() string { return "generated-id" }))) + router.GET("/", func(c *gin.Context) { + gotID, gotProvided = FromClient(c) + c.Status(http.StatusNoContent) + }) + + request := httptest.NewRequest(http.MethodGet, "/", nil) + request.Header.Set(HeaderKey, " client-id ") + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + if gotID != "client-id" || !gotProvided { + t.Fatalf("FromClient() = (%q, %v), want (client-id, true)", gotID, gotProvided) + } + if got := response.Header().Get(HeaderKey); got != "client-id" { + t.Fatalf("response %s = %q, want client-id", HeaderKey, got) + } +} + +func TestMiddlewareGeneratedRequestIDIsTracingOnly(t *testing.T) { + gin.SetMode(gin.TestMode) + + tests := []struct { + name string + requestID string + }{ + {name: "missing"}, + {name: "blank", requestID: " "}, + {name: "too long", requestID: strings.Repeat("x", 129)}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + gotID string + gotProvided bool + ) + router := gin.New() + router.Use(New(WithGenerator(func() string { return "generated-id" }))) + router.GET("/", func(c *gin.Context) { + gotID, gotProvided = FromClient(c) + c.Status(http.StatusNoContent) + }) + + request := httptest.NewRequest(http.MethodGet, "/", nil) + if tt.requestID != "" { + request.Header.Set(HeaderKey, tt.requestID) + } + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + + if gotID != "" || gotProvided { + t.Fatalf("FromClient() = (%q, %v), want empty and false", gotID, gotProvided) + } + if got := request.Header.Get(HeaderKey); got != "generated-id" { + t.Fatalf("request %s = %q, want generated-id", HeaderKey, got) + } + if got := response.Header().Get(HeaderKey); got != "generated-id" { + t.Fatalf("response %s = %q, want generated-id", HeaderKey, got) + } + }) + } +} diff --git a/middleware/requestid/type.go b/middleware/requestid/type.go new file mode 100644 index 0000000..66b4f26 --- /dev/null +++ b/middleware/requestid/type.go @@ -0,0 +1,29 @@ +package requestid + +import uuid "github.com/satori/go.uuid" + +const ( + HeaderKey = "X-Request-ID" + ContextKey = "Ctx-Request-ID" + // ClientProvidedContextKey 标记请求ID是否由客户端显式提供; + // 服务端自动生成的ID仅用于链路追踪,不应自动开启业务幂等缓存。 + ClientProvidedContextKey = "Ctx-Request-ID-Client-Provided" +) + +type RequestIDGenerator func() string + +type config struct { + generator RequestIDGenerator +} + +type Option func(*config) + +func WithGenerator(g RequestIDGenerator) Option { + return func(c *config) { + c.generator = g + } +} + +var DefaultFenerator = func() string { + return uuid.NewV4().String() +} diff --git a/middleware/ua/ua.go b/middleware/ua/ua.go new file mode 100644 index 0000000..2e0e692 --- /dev/null +++ b/middleware/ua/ua.go @@ -0,0 +1,101 @@ +package ua + +import ( + "net/url" + "strings" + + "91porn-server/common/constant" + "91porn-server/common/log" + + "github.com/gin-gonic/gin" +) + +type UA struct { + UserAgent string + DevID string //设备id 唯一标识 + DevType string //设备类型 华为nova7 + SysType string //pc + Ver string //1.0 + BuildID string // + Mac string // + GlobalDevID string //全局唯一表示 + Terminal string //终端 0-客户端;1-h5(ios)端;2-web端 + IsH5 string + SystemVersion string + SystemName string + DeviceBrand string + DeviceModel string + SID string +} + +func UAer(c *gin.Context) { + userAgent := c.GetHeader("X-User-Agent") //兼容H5 UserAgent + if userAgent == "" { + userAgent = c.Request.UserAgent() + } + if userAgent == "" { + log.Warn("user-agent empty") + return + } + ua := Parse(userAgent) + if sid := strings.TrimSpace(c.GetHeader("sid")); sid != "" { + ua.SID = sid + } + c.Set(constant.CtxUA, ua) +} + +func Parse(userAgent string) UA { + var uastr string + ua := UA{} + decodeUa, err := url.QueryUnescape(userAgent) + if err != nil { + log.Warn("URLDecode user-agent error", log.Any("ua", userAgent)) + uastr = userAgent + } else { + uastr = decodeUa + } + uas := strings.Split(uastr, ";") + for _, v := range uas { + vss := strings.SplitN(strings.TrimSpace(v), "=", 2) + if len(vss) < 2 { + log.Warn("user-agent miss", log.Any("ua", userAgent)) + continue + } + key := strings.TrimSpace(vss[0]) + value := strings.TrimSpace(vss[1]) + switch key { + case "DevID": + ua.DevID = value + case "DevType": + ua.DevType = value + case "SysType": + ua.SysType = value + case "Ver": + ua.Ver = value + case "BuildID": + ua.BuildID = value + case "Mac": + ua.Mac = value + case "GlobalDevID": + ua.GlobalDevID = value + case "Terminal": + ua.Terminal = value + case "IsH5": + ua.IsH5 = value + case "SystemVersion": + ua.SystemVersion = value + case "SystemName": + ua.SystemName = value + case "DeviceBrand": + ua.DeviceBrand = value + case "DeviceModel", "device_model": + ua.DeviceModel = value + case "SID", "Sid", "sid": + ua.SID = value + default: + log.Warn("user-agent unknown", log.Any("key", key), log.Any("ua", userAgent)) + } + } + ua.UserAgent = uastr + return ua +} diff --git a/middleware/ua/ua_test.go b/middleware/ua/ua_test.go new file mode 100644 index 0000000..511205b --- /dev/null +++ b/middleware/ua/ua_test.go @@ -0,0 +1,89 @@ +package ua + +import ( + "net/http/httptest" + "strings" + "testing" + + "91porn-server/common/constant" + + "github.com/gin-gonic/gin" +) + +func TestUAerParsesSIDFromHeaderAndUserAgent(t *testing.T) { + gin.SetMode(gin.TestMode) + + body := `{"devID":"91porn_google_3695e94c7226d787","qrCnt":"","devType":"lynx:36:id=CP1A.260305.018","sysType":"android","ver":"1.0.6","buildID":"com.noporn.newchatone_zero_six","devToken":"Qp6IluBGdByscQvXczvffmfC7YQxO6zm3Rr/A52FBpk=","cutInfos":""}` + req := httptest.NewRequest("POST", "/api/app/mine/login", strings.NewReader(body)) + req.Header.Set("sid", "3a73a39a03cf450893f89909e31d4bea") + req.Header.Set("user-agent", "DevID%3D91porn_google_3695e94c7226d787%3BDevType%3Dlynx%3A36%3Aid%3DCP1A.260305.018%3BSysType%3Dandroid%3BVer%3D1.0.6%3BBuildID%3Dcom.noporn.newchatone_zero_six%3BDeviceBrand%3Dgoogle%3BSystemName%3DAndroid%3BSystemVersion%3D16") + req.Header.Set("x-api-key", "timestamp=1776140330;sign=f8c0476aa413e3e21ae6c8e2ad02b0c0f6af6d25;nonce=d8b1f060-6c3b-458b-b059-7ef87d2e9f79") + req.Header.Set("accept-encoding", "*") + req.Header.Set("device", "android") + req.Header.Set("host", "91pornht.remoces.com") + req.Header.Set("authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzYxNDAyNTc3ODk4NjUyMDAsInR5cGUiOjAsInVpZCI6MzAzNzQzfQ.1tXpLY-FsDu9HzkT5SVVbMaOMtzkgLU5PBZzvHm-X0I") + req.Header.Set("api_version", "1.0.0") + req.Header.Set("content-type", "application/json;charset=UTF-8") + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = req + + UAer(c) + + value, ok := c.Get(constant.CtxUA) + if !ok { + t.Fatal("UAer did not set UA in context") + } + got, ok := value.(UA) + if !ok { + t.Fatalf("context UA type = %T, want ua.UA", value) + } + + if got.SID != "3a73a39a03cf450893f89909e31d4bea" { + t.Fatalf("SID = %q, want header sid", got.SID) + } + if got.DevID != "91porn_google_3695e94c7226d787" { + t.Fatalf("DevID = %q", got.DevID) + } + if got.DevType != "lynx:36:id=CP1A.260305.018" { + t.Fatalf("DevType = %q", got.DevType) + } + if got.SysType != "android" { + t.Fatalf("SysType = %q", got.SysType) + } + if got.Ver != "1.0.6" { + t.Fatalf("Ver = %q", got.Ver) + } + if got.BuildID != "com.noporn.newchatone_zero_six" { + t.Fatalf("BuildID = %q", got.BuildID) + } + if got.DeviceBrand != "google" { + t.Fatalf("DeviceBrand = %q", got.DeviceBrand) + } + if got.SystemName != "Android" { + t.Fatalf("SystemName = %q", got.SystemName) + } + if got.SystemVersion != "16" { + t.Fatalf("SystemVersion = %q", got.SystemVersion) + } +} + +func TestParseAcceptsSIDKeyVariants(t *testing.T) { + for _, tc := range []struct { + name string + userAgent string + want string + }{ + {name: "sid", userAgent: "DevID=device;sid=lower", want: "lower"}, + {name: "Sid", userAgent: "DevID=device;Sid=title", want: "title"}, + {name: "SID", userAgent: "DevID=device;SID=upper", want: "upper"}, + } { + t.Run(tc.name, func(t *testing.T) { + got := Parse(tc.userAgent) + if got.SID != tc.want { + t.Fatalf("SID = %q, want %q", got.SID, tc.want) + } + }) + } +} diff --git a/models/Init/init.go b/models/Init/init.go new file mode 100644 index 0000000..d02a8d0 --- /dev/null +++ b/models/Init/init.go @@ -0,0 +1,423 @@ +package Init + +import ( + "91porn-server/models/s/dailyretentionmod" + "91porn-server/models/s/useradverstatmod" + "91porn-server/models/v/advanceconfigmod" + "91porn-server/models/v/advanceordermod" + "91porn-server/models/v/aichangefaceimgmod" + "91porn-server/models/v/aichangefacemod" + "91porn-server/models/v/aichangefacevidmod" + "91porn-server/models/v/aiimagetovideomod" + "91porn-server/models/v/aiplazamod" + "91porn-server/models/v/aitemplatemodulemod" + "91porn-server/models/v/aitexttoimagemod" + "91porn-server/models/v/aitexttonovelmod" + "91porn-server/models/v/bannerjumpmod" + "91porn-server/models/v/checkinconfigmod" + "91porn-server/models/v/checkinprizemod" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/collectmod" + "91porn-server/models/v/coupon_record_mod" + "91porn-server/models/v/discount_area_mod" + "91porn-server/models/v/discount_area_video_mod" + "91porn-server/models/v/eventmod" + "91porn-server/models/v/export_task_mod" + "91porn-server/models/v/followmod" + "91porn-server/models/v/fundtransferlogmod" + "91porn-server/models/v/imgroupmembermod" + "91porn-server/models/v/imgroupmod" + "91porn-server/models/v/immessagemod" + "91porn-server/models/v/integralconfigmod" + "91porn-server/models/v/integralexcangemod" + "91porn-server/models/v/jingangmod" + "91porn-server/models/v/likemod" + "91porn-server/models/v/media_buy_record_mod" + "91porn-server/models/v/mediabookshelfmod" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/mediatagdimensionmod" + "91porn-server/models/v/mediatagmod" + "91porn-server/models/v/messagemod" + "91porn-server/models/v/nakedchatmod" + "91porn-server/models/v/nakedchatordermod" + "91porn-server/models/v/officialWebsitemod" + "91porn-server/models/v/officialmod" + "91porn-server/models/v/paymentguidemod" + "91porn-server/models/v/sessionmod" + "91porn-server/models/v/signrecordmod" + "91porn-server/models/v/signtaskmod" + "91porn-server/models/v/sysconfmod" + "91porn-server/models/v/taskrecordmod" + "91porn-server/models/v/userResourcemod" + "91porn-server/models/v/usercheckinmod" + "91porn-server/models/v/videogoldcoinmod" + "91porn-server/models/v/vidtimeonlinemod" + "91porn-server/models/v/vipcardexperimentmod" + "os" + "time" + + "91porn-server/common/log" + "91porn-server/models/l/adsclicklogmod" + "91porn-server/models/l/exchlogmod" + "91porn-server/models/l/loginlgmod" + "91porn-server/models/l/lotterylgmod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/l/payvidlgmod" + "91porn-server/models/l/playlgmod" + "91porn-server/models/l/pullgmod" + "91porn-server/models/l/registermod" + "91porn-server/models/l/searchlogmod" + "91porn-server/models/l/synccdnmod" + "91porn-server/models/l/userdailytasklogmod" + "91porn-server/models/l/visitlogmod" + "91porn-server/models/l/welfarelgmod" + "91porn-server/models/s/hotstatmod" + "91porn-server/models/s/kwrankmod" + "91porn-server/models/s/kwstatmod" + "91porn-server/models/s/sectionstatmod" + "91porn-server/models/s/statordermod" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/s/stattagplaymod" + "91porn-server/models/s/statusermod" + "91porn-server/models/s/statvidmod" + "91porn-server/models/s/statvidtotalmod" + "91porn-server/models/v/active2023mod" + "91porn-server/models/v/activitymod" + "91porn-server/models/v/actmod" + "91porn-server/models/v/adminmod" + "91porn-server/models/v/adreviewmod" + "91porn-server/models/v/adsmod" + "91porn-server/models/v/aiUnDressmod" + "91porn-server/models/v/annoumod" + "91porn-server/models/v/audioanchormod" + "91porn-server/models/v/audiobookmod" + "91porn-server/models/v/authoritymod" + "91porn-server/models/v/backpackmod" + "91porn-server/models/v/chatrobotmod" + "91porn-server/models/v/contentlibmod" + "91porn-server/models/v/contentreviewmod" + "91porn-server/models/v/currencymod" + "91porn-server/models/v/dailytaskmod" + "91porn-server/models/v/errfedbkmod" + "91porn-server/models/v/exchcodemod" + "91porn-server/models/v/faqmod" + "91porn-server/models/v/feedbackmod" + "91porn-server/models/v/fictionmod" + "91porn-server/models/v/filtermod" + "91porn-server/models/v/freeVidmod" + "91porn-server/models/v/fsuidmod" + "91porn-server/models/v/goldcfgmod" + "91porn-server/models/v/goldextramod" + "91porn-server/models/v/hevctaskmod" + "91porn-server/models/v/idmod" + "91porn-server/models/v/imagetopmod" + "91porn-server/models/v/immod" + "91porn-server/models/v/imusermod" + "91porn-server/models/v/ipblockmod" + "91porn-server/models/v/ipwhitemod" + "91porn-server/models/v/ldycfgmod" + "91porn-server/models/v/locmod" + "91porn-server/models/v/marqueemod" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/modulesectionmod" + "91porn-server/models/v/modulevidmod" + "91porn-server/models/v/newactivity" + "91porn-server/models/v/noticefmtmod" + "91porn-server/models/v/noticerecdmod" + "91porn-server/models/v/oncetaskmod" + "91porn-server/models/v/operationlogmod" + "91porn-server/models/v/payaudiobookmod" + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/prizemod" + "91porn-server/models/v/productbenefitmod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/productposimod" + "91porn-server/models/v/proxyincomemod" + "91porn-server/models/v/proxymod" + "91porn-server/models/v/proxyrecordmod" + "91porn-server/models/v/pushmod" + "91porn-server/models/v/questionnremod" + "91porn-server/models/v/quicksearchmod" + "91porn-server/models/v/rchgamegoldmod" + "91porn-server/models/v/rchgamtmod" + "91porn-server/models/v/rchgchanmod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/recmdtag" + "91porn-server/models/v/recommod" + "91porn-server/models/v/reptmod" + "91porn-server/models/v/repttypemod" + "91porn-server/models/v/rjctmpltmod" + "91porn-server/models/v/rolemod" + "91porn-server/models/v/scenebannermod" + "91porn-server/models/v/sensitivewordmod" + "91porn-server/models/v/settingmod" + "91porn-server/models/v/sourcemod" + "91porn-server/models/v/systemmod" + "91porn-server/models/v/tagconfmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/taskmod" + "91porn-server/models/v/tonemod" + "91porn-server/models/v/tonerecomod" + "91porn-server/models/v/txnactmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/useractmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/userrecomod" + "91porn-server/models/v/usertagmod" + "91porn-server/models/v/userwatchrecordmod" + "91porn-server/models/v/verifyreportmod" + "91porn-server/models/v/versionmod" + "91porn-server/models/v/videoactivitymod" + "91porn-server/models/v/videocoupon" + "91porn-server/models/v/videodiscountmod" + "91porn-server/models/v/vidmod" + "91porn-server/models/v/vidpopmod" + "91porn-server/models/v/vipconfigmod" + "91porn-server/models/v/walletmod" + "91porn-server/models/v/wdchannmod" + "91porn-server/models/v/wdordmod" + "91porn-server/models/v/wdtaxmod" +) + +// initVideoDB 初始化主业务数据库 +func initVideoDB() { + defer func() { + if r := recover(); r != nil { + log.Warn("[Init Index Error]===>", log.Any("Detail ", r)) + os.Exit(-1) + } + }() + log.Info("【DB-Video】init start..") + filtermod.Init() + repttypemod.Init() + reptmod.Init() + tagmod.Init() + tonemod.Init() + tonerecomod.Init() + locmod.Init() + vidmod.Init() + hevctaskmod.Init() + adminmod.Init() + adsmod.Init() + adreviewmod.Init() + annoumod.Init() + pushmod.Init() + activitymod.Init() + sourcemod.Init() + productmod.Init() + productposimod.Init() + proxyincomemod.Init() + proxymod.Init() + proxyrecordmod.Init() + idmod.Init() + usermod.Init() + rchgamtmod.Init() + rchgamegoldmod.Init() + goldcfgmod.Init() + rchgchanmod.Init() + rchgordmod.Init() + prdcthsomod.Init() + settingmod.Init() + versionmod.Init() + walletmod.Init() + fundtransferlogmod.Init() + wdchannmod.Init() + wdordmod.Init() + wdtaxmod.Init() + authoritymod.Init() + rolemod.Init() + faqmod.Init() + feedbackmod.Init() + txnactmod.Init() + txnmod.Init() + useractmod.Init() + usertagmod.Init() + immod.Init() + imusermod.Init() + ipblockmod.Init() + ipwhitemod.Init() + noticefmtmod.Init() + noticerecdmod.Init() + officialmod.Init() + userrecomod.Init() + exchcodemod.Init() + chatrobotmod.Init() + contentlibmod.Init() + fsuidmod.Init() + recmdtag.Init() + freeVidmod.Init() + newactivity.Init() + ldycfgmod.Init() + questionnremod.Init() + verifyreportmod.Init() + fictionmod.Init() + audiobookmod.Init() + audioanchormod.Init() + errfedbkmod.Init() + payaudiobookmod.Init() + vipconfigmod.Init() + taskmod.Init() + taskrecordmod.Init() + backpackmod.Init() + prizemod.Init() + currencymod.Init() + systemmod.Init() + videodiscountmod.Init() + moduleconfmod.Init() + modulesectionmod.Init() + recommod.Init() + modulevidmod.Init() + tagconfmod.Init() + operationlogmod.Init() + videocoupon.Init() + marqueemod.Init() + videoactivitymod.Init() + vidpopmod.Init() + userwatchrecordmod.Index() + productbenefitmod.Init() + imagetopmod.Init() + active2023mod.Init() + goldextramod.Init() + actmod.Init() + dailytaskmod.Init() + oncetaskmod.Init() + rjctmpltmod.Init() + aiUnDressmod.Init() + likemod.Init() + integralconfigmod.Init() + integralexcangemod.Init() + followmod.Init() + cmtmod.Init() + collectmod.Init() + userResourcemod.Init() + aichangefacevidmod.Init() + aichangefacemod.Init() + sessionmod.Init() + messagemod.Init() + aichangefaceimgmod.Init() + advanceordermod.Init() + videogoldcoinmod.Init() + advanceconfigmod.Init() + coupon_record_mod.Init() + signrecordmod.Init() + vidtimeonlinemod.Init() + discount_area_mod.Init() + discount_area_video_mod.Init() + export_task_mod.Init() + quicksearchmod.Init() + sysconfmod.Init() + bannerjumpmod.Init() + paymentguidemod.Init() + scenebannermod.Init() + vipcardexperimentmod.Init() + mediamod.Init() + mediacontentmod.Init() + mediatagmod.Init() + mediatagdimensionmod.Init() + mediabookshelfmod.Init() + media_buy_record_mod.Init() + imgroupmod.Init() + imgroupmembermod.Init() + immessagemod.Init() + nakedchatmod.Init() + nakedchatordermod.Init() + aitexttoimagemod.Init() + aiimagetovideomod.Init() + aitemplatemodulemod.Init() + aiplazamod.Init() + signtaskmod.Init() + aitexttonovelmod.Init() + eventmod.Init() + checkinprizemod.Init() + usercheckinmod.Init() + checkinconfigmod.Init() + sensitivewordmod.Init() + contentreviewmod.Init() + officialWebsitemod.Init() + log.Info("【DB-Video】init end") +} + +// initStatDB 初始化统计数据库 +func initStatDB() { + defer func() { + if r := recover(); r != nil { + log.Warn("[Init Index Error]===>", log.Any("Detail ", r)) + os.Exit(-1) + } + }() + log.Info("【DB-Stat】init start..") + statrecordmod.Init() + sectionstatmod.Init() + statvidmod.Init() + statordermod.Init() + kwstatmod.Init() + kwrankmod.Init() + stattagplaymod.Init() + statvidtotalmod.Init() + statusermod.Init() + hotstatmod.Init() + dailyretentionmod.Init() + useradverstatmod.Init() + log.Info("【DB-Stat】init end") +} + +// initLogDB 初始化日志数据库 +func initLogDB() { + defer func() { + if r := recover(); r != nil { + log.Warn("[Init Index Error]===>", log.Any("Detail ", r)) + os.Exit(-1) + } + }() + log.Info("【DB-Log】 db init start..") + loginlgmod.Init() + payvidlgmod.Init() + playlgmod.Init() + pullgmod.Init() + searchlogmod.Init() + visitlogmod.Init() + welfarelgmod.Init() + synccdnmod.Init() + operatorlgmod.Init() + registermod.Init() + exchlogmod.Init() + lotterylgmod.Init() + adsclicklogmod.Init() + userdailytasklogmod.Init() + jingangmod.Init() + log.Info("【DB-Log】db init end") +} + +// Init 设置索引// +func InitMongo() { + log.Info("DB init start..") + initLogDB() + initVideoDB() + initStatDB() + log.Info("DB init end") +} + +func ESInit() { + log.Info("ES init start...") + initESIndex() + log.Info("ES init end") +} + +// 初始化ES index +func initESIndex() { + vidmod.InitESIndex() + tagmod.InitESIndex() + usermod.InitESIndex() + audiobookmod.InitESIndex() +} + +func InitCache() { + go func() { + ticker := time.NewTicker(29 * time.Minute) + for { + <-ticker.C + vidmod.SetAllVidWithPopCache() + } + }() +} diff --git a/models/cache/aiimagetovideodata/aiimagetovideo.go b/models/cache/aiimagetovideodata/aiimagetovideo.go new file mode 100755 index 0000000..7ed9cfc --- /dev/null +++ b/models/cache/aiimagetovideodata/aiimagetovideo.go @@ -0,0 +1,88 @@ +package aiimagetovideodata + +import ( + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/aiimagetovideomod" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []aiimagetovideomod.AiImageToVideo + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]aiimagetovideomod.AiImageToVideo, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(10*time.Minute).AutoListKey(models.AiImageToVideo).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = aiimagetovideomod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res aiimagetovideomod.AiImageToVideo, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoInfoKey(models.AiImageToVideo, id.Hex()).ResBind(&res).Cache(aiimagetovideomod.GetInfo, id) + return +} + +// GetAllFromCache 获取所有数据 +func GetAllFromCache(cond bson.M, sort bson.D) (res []aiimagetovideomod.AiImageToVideo, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoListKey(models.AiImageToVideo+"-all").ResBind(&res).Cache(aiimagetovideomod.GetAll, cond, sort) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data aiimagetovideomod.AiImageToVideo) (primitive.ObjectID, error) { + newID, err := aiimagetovideomod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.AiImageToVideo).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := aiimagetovideomod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.AiImageToVideo).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = aiimagetovideomod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.AiImageToVideo).AutoClear(true, &all) + return +} diff --git a/models/cache/aiimagetovideodata/data.go b/models/cache/aiimagetovideodata/data.go new file mode 100755 index 0000000..fa29b68 --- /dev/null +++ b/models/cache/aiimagetovideodata/data.go @@ -0,0 +1,43 @@ +package aiimagetovideodata + +import "91porn-server/models/v/aiimagetovideomod" + +// FormatAppDataList 格式化app列表数据 +func FormatAppDataList(origin []aiimagetovideomod.AiImageToVideo) (res []*aiimagetovideomod.AiImageToVideoInfo) { + res = make([]*aiimagetovideomod.AiImageToVideoInfo, len(origin)) + if len(origin) == 0 { + return + } + + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppData(item) + } + + return +} + +// FormatAppData 格式化app数据 +func FormatAppData(item aiimagetovideomod.AiImageToVideo) (res *aiimagetovideomod.AiImageToVideoInfo) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res = &aiimagetovideomod.AiImageToVideoInfo{ + ID: item.ID, + UID: item.UID, + ImgUrl: item.ImgUrl, + NewImgUrl: item.NewImgUrl, + Status: item.Status, + Coin: item.Coin, + IsFreeTimes: item.IsFreeTimes, + IsHide: item.IsHide, + Remark: item.Remark, + IsDelete: item.IsDelete, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + + return +} diff --git a/models/cache/aiplazadata/aiplaza.go b/models/cache/aiplazadata/aiplaza.go new file mode 100755 index 0000000..6e83499 --- /dev/null +++ b/models/cache/aiplazadata/aiplaza.go @@ -0,0 +1,89 @@ +package aiplazadata + +import ( + "time" + + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/aiplazamod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []aiplazamod.AiPlaza + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]aiplazamod.AiPlaza, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(10*time.Minute).AutoListKey(models.AiPlaza).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = aiplazamod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res aiplazamod.AiPlaza, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoInfoKey(models.AiPlaza, id.Hex()).ResBind(&res).Cache(aiplazamod.GetInfo, id) + return +} + +// GetAllFromCache 获取所有数据 +func GetAllFromCache(cond bson.M, sort bson.D) (res []aiplazamod.AiPlaza, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoListKey(models.AiPlaza+"-all").ResBind(&res).Cache(aiplazamod.GetAll, cond, sort) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data aiplazamod.AiPlaza) (primitive.ObjectID, error) { + newID, err := aiplazamod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.AiPlaza).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := aiplazamod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.AiPlaza).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id ...primitive.ObjectID) (err error) { + err = aiplazamod.DeleteByID(t, id...) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.AiPlaza).AutoClear(true, &all) + return +} diff --git a/models/cache/aiplazadata/data.go b/models/cache/aiplazadata/data.go new file mode 100755 index 0000000..03a56a5 --- /dev/null +++ b/models/cache/aiplazadata/data.go @@ -0,0 +1,153 @@ +package aiplazadata + +import ( + "91porn-server/app/service/vidhelpser" + "91porn-server/common" + "91porn-server/models/v/aiplazamod" + "91porn-server/models/v/collectmod" + "91porn-server/models/v/likemod" + "91porn-server/models/v/vidmod" + "go.mongodb.org/mongo-driver/bson/primitive" + "sync" +) + +// FormatAppDataList 格式化app列表数据 +func FormatAppDataList(origin []aiplazamod.AiPlaza) (res []*aiplazamod.AiPlazaInfo) { + res = make([]*aiplazamod.AiPlazaInfo, len(origin)) + if len(origin) == 0 { + return + } + // 获取用户数据 + uids := []uint64{} + for _, item := range origin { + uids = append(uids, item.Uid) + } + userMap := vidhelpser.GetSimpleUserMapNoUID(uids) + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppSimpleData(item, userMap) + } + + return +} + +// FormatAppSimpleData 格式化app数据 +func FormatAppSimpleData(item aiplazamod.AiPlaza, userMap map[uint64]*vidmod.Publisher) (res *aiplazamod.AiPlazaInfo) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res = &aiplazamod.AiPlazaInfo{ + ID: item.ID, + Type: item.Type, + Template: item.Template, + Uid: item.Uid, + Title: item.Title, + OriginalImage: item.OriginalImage, + OriginalVideo: item.OriginalVideo, + OriginalVideoCover: item.OriginalVideoCover, + OriginContent: item.OriginContent, + GenerateImage: item.GenerateImage, + GenerateVideo: item.GenerateVideo, + GenerateVideoCover: item.GenerateVideoCover, + Status: item.Status, + Reason: item.Reason, + SortCode: item.SortCode, + LikeCount: item.LikeCount, + CollectCount: item.CollectCount, + CommentCount: item.CommentCount, + ViewCount: item.ViewCount, + ReviewAt: item.ReviewAt, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + if userMap != nil { + user, ok := userMap[item.Uid] + if ok && user != nil { + res.Name = user.Name + res.Gender = user.Gender + res.Portrait = user.Portrait + } + } + return +} + +// FormatAppData 格式化app列表数据 +func FormatAppData(uid uint64, item aiplazamod.AiPlaza) (res *aiplazamod.AiPlazaInfo) { + if item.ID.IsZero() { + return + } + + userMap := vidhelpser.GetSimpleUserMapNoUID([]uint64{item.Uid}) + // 获取是否点赞,收藏数据 + dataStatus := GetDataStatus(uid, []primitive.ObjectID{item.ID}) + // 组装返回数据 + res = &aiplazamod.AiPlazaInfo{ + ID: item.ID, + Type: item.Type, + Template: item.Template, + Uid: item.Uid, + Title: item.Title, + OriginalImage: item.OriginalImage, + OriginalVideo: item.OriginalVideo, + OriginalVideoCover: item.OriginalVideoCover, + OriginContent: item.OriginContent, + GenerateImage: item.GenerateImage, + GenerateVideo: item.GenerateVideo, + GenerateVideoCover: item.GenerateVideoCover, + Status: item.Status, + Reason: item.Reason, + SortCode: item.SortCode, + ReviewAt: item.ReviewAt, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + if userMap != nil { + user, ok := userMap[item.Uid] + if ok && user != nil { + res.Name = user.Name + res.Gender = user.Gender + res.Portrait = user.Portrait + } + } + if dataStatus != nil { + res.HasLiked = dataStatus[item.ID].HasLiked + res.HasCollected = dataStatus[item.ID].HasCollected + } + return +} + +// DataStatus 数据状态 +type DataStatus struct { + //已点赞 + HasLiked bool `json:"hasLiked" bson:"hasLiked"` + //已收藏 + HasCollected bool `json:"hasCollected" bson:"hasCollected"` +} + +// GetDataStatus 获取数据状态信息 +func GetDataStatus(uid uint64, objIds []primitive.ObjectID) map[primitive.ObjectID]DataStatus { + mLike := make(map[primitive.ObjectID]bool) + mCollect := make(map[primitive.ObjectID]bool) + var wg sync.WaitGroup + wg.Add(2) + common.Go(func() { + defer wg.Done() + mLike, _ = likemod.IsLikeVideos(uid, objIds) + }) + common.Go(func() { + defer wg.Done() + mCollect, _ = collectmod.IsCollectVideos(uid, objIds) + }) + wg.Wait() + m := make(map[primitive.ObjectID]DataStatus) + for _, v := range objIds { + info := DataStatus{ + HasLiked: mLike[v], + HasCollected: mCollect[v], + } + m[v] = info + } + return m +} diff --git a/models/cache/aitemplatemoduledata/aitemplatemodule.go b/models/cache/aitemplatemoduledata/aitemplatemodule.go new file mode 100755 index 0000000..fdaf314 --- /dev/null +++ b/models/cache/aitemplatemoduledata/aitemplatemodule.go @@ -0,0 +1,89 @@ +package aitemplatemoduledata + +import ( + "time" + + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/aitemplatemodulemod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []aitemplatemodulemod.AiTemplateModule + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]aitemplatemodulemod.AiTemplateModule, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(3*time.Minute).AutoListKey(models.AiTemplateModule).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = aitemplatemodulemod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res aitemplatemodulemod.AiTemplateModule, err error) { + _, err = cachev2.Classes().CacheTime(5*time.Minute).AutoInfoKey(models.AiTemplateModule, id.Hex()).ResBind(&res).Cache(aitemplatemodulemod.GetInfo, id) + return +} + +// GetAllFromCache 获取所有数据 +func GetAllFromCache(cond bson.M, sort bson.D) (res []aitemplatemodulemod.AiTemplateModule, err error) { + _, err = cachev2.Classes().CacheTime(8*time.Minute).AutoListKey(models.AiTemplateModule+"-all").ResBind(&res).Cache(aitemplatemodulemod.GetAll, cond, sort) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data aitemplatemodulemod.AiTemplateModule) (primitive.ObjectID, error) { + newID, err := aitemplatemodulemod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.AiTemplateModule).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := aitemplatemodulemod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.AiTemplateModule).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = aitemplatemodulemod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.AiTemplateModule).AutoClear(true, &all) + return +} diff --git a/models/cache/aitemplatemoduledata/data.go b/models/cache/aitemplatemoduledata/data.go new file mode 100755 index 0000000..a45d700 --- /dev/null +++ b/models/cache/aitemplatemoduledata/data.go @@ -0,0 +1,36 @@ +package aitemplatemoduledata + +import ( + "91porn-server/models/v/aitemplatemodulemod" +) + +// FormatAppDataList 格式化app列表数据 +func FormatAppDataList(origin []aitemplatemodulemod.AiTemplateModule) (res []*aitemplatemodulemod.AiTemplateModuleInfo) { + res = make([]*aitemplatemodulemod.AiTemplateModuleInfo, len(origin)) + if len(origin) == 0 { + return + } + + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppData(item) + } + + return +} + +// FormatAppData 格式化app数据 +func FormatAppData(item aitemplatemodulemod.AiTemplateModule) (res *aitemplatemodulemod.AiTemplateModuleInfo) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res = &aitemplatemodulemod.AiTemplateModuleInfo{ + ID: item.ID, + Type: item.Type, + Name: item.Name, + } + + return +} diff --git a/models/cache/aitexttoimagedata/aitexttoimage.go b/models/cache/aitexttoimagedata/aitexttoimage.go new file mode 100755 index 0000000..cbdb123 --- /dev/null +++ b/models/cache/aitexttoimagedata/aitexttoimage.go @@ -0,0 +1,89 @@ +package aitexttoimagedata + +import ( + "time" + + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/aitexttoimagemod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []aitexttoimagemod.AiTextToImage + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]aitexttoimagemod.AiTextToImage, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(3*time.Minute).AutoListKey(models.AiTextToImage).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = aitexttoimagemod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res aitexttoimagemod.AiTextToImage, err error) { + _, err = cachev2.Classes().CacheTime(5*time.Minute).AutoInfoKey(models.AiTextToImage, id.Hex()).ResBind(&res).Cache(aitexttoimagemod.GetInfo, id) + return +} + +// GetAllFromCache 获取所有数据 +func GetAllFromCache(cond bson.M, sort bson.D) (res []aitexttoimagemod.AiTextToImage, err error) { + _, err = cachev2.Classes().CacheTime(8*time.Minute).AutoListKey(models.AiTextToImage+"-all").ResBind(&res).Cache(aitexttoimagemod.GetAll, cond, sort) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data aitexttoimagemod.AiTextToImage) (primitive.ObjectID, error) { + newID, err := aitexttoimagemod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.AiTextToImage).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := aitexttoimagemod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.AiTextToImage).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = aitexttoimagemod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.AiTextToImage).AutoClear(true, &all) + return +} diff --git a/models/cache/aitexttoimagedata/data.go b/models/cache/aitexttoimagedata/data.go new file mode 100755 index 0000000..b88220f --- /dev/null +++ b/models/cache/aitexttoimagedata/data.go @@ -0,0 +1,49 @@ +package aitexttoimagedata + +import ( + "91porn-server/models/v/aitexttoimagemod" +) + +// FormatAppDataList 格式化app列表数据 +func FormatAppDataList(origin []aitexttoimagemod.AiTextToImage) (res []*aitexttoimagemod.AiTextToImageInfo) { + res = make([]*aitexttoimagemod.AiTextToImageInfo, len(origin)) + if len(origin) == 0 { + return + } + + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppData(item) + } + + return +} + +// FormatAppData 格式化app数据 +func FormatAppData(item aitexttoimagemod.AiTextToImage) (res *aitexttoimagemod.AiTextToImageInfo) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res = &aitexttoimagemod.AiTextToImageInfo{ + ID: item.ID, + UID: item.UID, + Text: item.Text, + NewImgUrl: item.NewImgUrl, + Status: item.Status, + AspectRatio: item.AspectRatio, + StyleType: item.StyleType, + Coin: item.Coin, + DebitAmountCoin: item.DebitAmountCoin, + DebitIncomeCoin: item.DebitIncomeCoin, + IsFreeTimes: item.IsFreeTimes, + IsHide: item.IsHide, + Remark: item.Remark, + IsDelete: item.IsDelete, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + + return +} diff --git a/models/cache/aitexttonoveldata/aitexttonovel.go b/models/cache/aitexttonoveldata/aitexttonovel.go new file mode 100755 index 0000000..6e38c6c --- /dev/null +++ b/models/cache/aitexttonoveldata/aitexttonovel.go @@ -0,0 +1,88 @@ +package aitexttonoveldata + +import ( + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/aitexttonovelmod" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []aitexttonovelmod.AiTextToNovel + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]aitexttonovelmod.AiTextToNovel, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(10*time.Minute).AutoListKey(models.AiTextToNovel).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = aitexttonovelmod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res aitexttonovelmod.AiTextToNovel, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoInfoKey(models.AiTextToNovel, id.Hex()).ResBind(&res).Cache(aitexttonovelmod.GetInfo, id) + return +} + +// GetAllFromCache 获取所有数据 +func GetAllFromCache(cond bson.M, sort bson.D) (res []aitexttonovelmod.AiTextToNovel, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoListKey(models.AiTextToNovel+"-all").ResBind(&res).Cache(aitexttonovelmod.GetAll, cond, sort) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data aitexttonovelmod.AiTextToNovel) (primitive.ObjectID, error) { + newID, err := aitexttonovelmod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.AiTextToNovel).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := aitexttonovelmod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.AiTextToNovel).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = aitexttonovelmod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.AiTextToNovel).AutoClear(true, &all) + return +} diff --git a/models/cache/aitexttonoveldata/data.go b/models/cache/aitexttonoveldata/data.go new file mode 100755 index 0000000..c79cda0 --- /dev/null +++ b/models/cache/aitexttonoveldata/data.go @@ -0,0 +1,49 @@ +package aitexttonoveldata + +import "91porn-server/models/v/aitexttonovelmod" + +// FormatAppDataList 格式化app列表数据 +func FormatAppDataList(origin []aitexttonovelmod.AiTextToNovel) (res []*aitexttonovelmod.AiTextToNovel) { + res = make([]*aitexttonovelmod.AiTextToNovel, len(origin)) + if len(origin) == 0 { + return + } + + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppData(item) + } + + return +} + +// FormatAppData 格式化app数据 +func FormatAppData(item aitexttonovelmod.AiTextToNovel) (res *aitexttonovelmod.AiTextToNovel) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res = &aitexttonovelmod.AiTextToNovel{ + ID: item.ID, + UID: item.UID, + Description: item.Description, + Content: item.Content, + Status: item.Status, + CharacterSetting: item.CharacterSetting, + LocationScene: item.LocationScene, + Details: item.Details, + ModelType: item.ModelType, + Coin: item.Coin, + DebitAmountCoin: item.DebitAmountCoin, + DebitIncomeCoin: item.DebitIncomeCoin, + IsFreeTimes: item.IsFreeTimes, + IsHide: item.IsHide, + Remark: item.Remark, + IsDelete: item.IsDelete, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + + return +} diff --git a/models/cache/bannerjumpdata/bannerjump.go b/models/cache/bannerjumpdata/bannerjump.go new file mode 100755 index 0000000..5619795 --- /dev/null +++ b/models/cache/bannerjumpdata/bannerjump.go @@ -0,0 +1,73 @@ +package bannerjumpdata + +import ( + "time" + + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/bannerjumpmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []bannerjumpmod.BannerJump + Count int64 + HasNext bool +} + +// GetAllFromCache 获取所有数据 +func GetAllFromCache() (res []bannerjumpmod.BannerJump, err error) { + t := time.Now() + cond := bson.M{"enable": true, "startAt": bson.M{"$lte": t}, "endAt": bson.M{"$gt": t}} + sort := bson.D{{"weight", -1}, {"createdAt", -1}} + _, err = cachev2.Classes().CacheTime(8*time.Minute).AutoListKey(models.BannerJump+"-all").ResBind(&res).Cache(bannerjumpmod.GetAll, cond, sort) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data bannerjumpmod.BannerJump) (primitive.ObjectID, error) { + newID, err := bannerjumpmod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.BannerJump).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := bannerjumpmod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.BannerJump).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = bannerjumpmod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.BannerJump).AutoClear(true, &all) + return +} diff --git a/models/cache/imgroupdata/data.go b/models/cache/imgroupdata/data.go new file mode 100755 index 0000000..147875a --- /dev/null +++ b/models/cache/imgroupdata/data.go @@ -0,0 +1,43 @@ +package imgroupdata + +import ( + "91porn-server/models/v/imgroupmod" +) + +// FormatAppDataList 格式化app列表数据 +func FormatAppDataList(origin []imgroupmod.ImGroup) (res []*imgroupmod.ImGroupInfo) { + res = make([]*imgroupmod.ImGroupInfo, len(origin)) + if len(origin) == 0 { + return + } + + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppData(item) + } + + return +} + +// FormatAppData 格式化app数据 +func FormatAppData(item imgroupmod.ImGroup) (res *imgroupmod.ImGroupInfo) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res = &imgroupmod.ImGroupInfo{ + ID: item.ID, + GroupId: item.GroupId, + Name: item.Name, + Cover: item.Cover, + Summary: item.Summary, + MemberNum: item.MemberNum, + FakeMemberNum: item.FakeMemberNum, + Price: item.Price, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + + return +} diff --git a/models/cache/imgroupdata/imgroup.go b/models/cache/imgroupdata/imgroup.go new file mode 100755 index 0000000..a56d094 --- /dev/null +++ b/models/cache/imgroupdata/imgroup.go @@ -0,0 +1,89 @@ +package imgroupdata + +import ( + "time" + + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/imgroupmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []imgroupmod.ImGroup + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]imgroupmod.ImGroup, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(10*time.Minute).AutoListKey(models.ImGroup).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = imgroupmod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res imgroupmod.ImGroup, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoInfoKey(models.ImGroup, id.Hex()).ResBind(&res).Cache(imgroupmod.GetInfo, id) + return +} + +// GetAllFromCache 获取所有数据 +func GetAllFromCache(cond bson.M, sort bson.D) (res []imgroupmod.ImGroup, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoListKey(models.ImGroup+"-all").ResBind(&res).Cache(imgroupmod.GetAll, cond, sort) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data imgroupmod.ImGroup) (primitive.ObjectID, error) { + newID, err := imgroupmod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.ImGroup).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := imgroupmod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.ImGroup).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = imgroupmod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.ImGroup).AutoClear(true, &all) + return +} diff --git a/models/cache/imgroupmemberdata/data.go b/models/cache/imgroupmemberdata/data.go new file mode 100755 index 0000000..aeec7d9 --- /dev/null +++ b/models/cache/imgroupmemberdata/data.go @@ -0,0 +1,37 @@ +package imgroupmemberdata + +import ( + "91porn-server/models/v/imgroupmembermod" +) + +// FormatAppDataList 格式化app列表数据 +func FormatAppDataList(origin []imgroupmembermod.ImGroupMember) (res []*imgroupmembermod.ImGroupMemberInfo) { + res = make([]*imgroupmembermod.ImGroupMemberInfo, len(origin)) + if len(origin) == 0 { + return + } + + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppData(item) + } + + return +} + +// FormatAppData 格式化app数据 +func FormatAppData(item imgroupmembermod.ImGroupMember) (res *imgroupmembermod.ImGroupMemberInfo) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res = &imgroupmembermod.ImGroupMemberInfo{ + ID: item.ID, + Uid: item.Uid, + Price: item.Price, + CreatedAt: item.CreatedAt, + } + + return +} diff --git a/models/cache/imgroupmemberdata/imgroupmember.go b/models/cache/imgroupmemberdata/imgroupmember.go new file mode 100755 index 0000000..35cb169 --- /dev/null +++ b/models/cache/imgroupmemberdata/imgroupmember.go @@ -0,0 +1,89 @@ +package imgroupmemberdata + +import ( + "time" + + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/imgroupmembermod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []imgroupmembermod.ImGroupMember + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]imgroupmembermod.ImGroupMember, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(10*time.Minute).AutoListKey(models.ImGroupMember).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = imgroupmembermod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res imgroupmembermod.ImGroupMember, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoInfoKey(models.ImGroupMember, id.Hex()).ResBind(&res).Cache(imgroupmembermod.GetInfo, id) + return +} + +// GetAllFromCache 获取所有数据 +func GetAllFromCache(cond bson.M, sort bson.D) (res []imgroupmembermod.ImGroupMember, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoListKey(models.ImGroupMember+"-all").ResBind(&res).Cache(imgroupmembermod.GetAll, cond, sort) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data imgroupmembermod.ImGroupMember) (primitive.ObjectID, error) { + newID, err := imgroupmembermod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.ImGroupMember).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := imgroupmembermod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.ImGroupMember).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = imgroupmembermod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.ImGroupMember).AutoClear(true, &all) + return +} diff --git a/models/cache/immessagedata/data.go b/models/cache/immessagedata/data.go new file mode 100755 index 0000000..db91735 --- /dev/null +++ b/models/cache/immessagedata/data.go @@ -0,0 +1,52 @@ +package immessagedata + +import ( + "91porn-server/app/service/vidhelpser" + "91porn-server/models/v/immessagemod" + "91porn-server/models/v/vidmod" +) + +// FormatAppDataList 格式化app列表数据 +func FormatAppDataList(origin []immessagemod.ImMessage) (res []*immessagemod.ImMessageInfo) { + res = make([]*immessagemod.ImMessageInfo, len(origin)) + if len(origin) == 0 { + return + } + // 获取用户数据 + uids := []uint64{} + for _, item := range origin { + uids = append(uids, item.Uid) + } + userMap := vidhelpser.GetSimpleUserMapNoUID(uids) + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppData(item, userMap) + } + + return +} + +// FormatAppData 格式化app数据 +func FormatAppData(item immessagemod.ImMessage, userMap map[uint64]*vidmod.Publisher) (res *immessagemod.ImMessageInfo) { + if item.ID.IsZero() { + return + } + // 组装返回数据 + res = &immessagemod.ImMessageInfo{ + ID: item.ID, + GroupId: item.GroupId, + Uid: item.Uid, + Content: item.Content, + Image: item.Image, + CreatedAt: item.CreatedAt, + } + if userMap != nil { + user, ok := userMap[item.Uid] + if ok && user != nil { + res.Name = user.Name + res.Gender = user.Gender + res.Portrait = user.Portrait + } + } + return +} diff --git a/models/cache/immessagedata/immessage.go b/models/cache/immessagedata/immessage.go new file mode 100755 index 0000000..1aff593 --- /dev/null +++ b/models/cache/immessagedata/immessage.go @@ -0,0 +1,89 @@ +package immessagedata + +import ( + "time" + + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/immessagemod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []immessagemod.ImMessage + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]immessagemod.ImMessage, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(10*time.Minute).AutoListKey(models.ImMessage).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = immessagemod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res immessagemod.ImMessage, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoInfoKey(models.ImMessage, id.Hex()).ResBind(&res).Cache(immessagemod.GetInfo, id) + return +} + +// GetAllFromCache 获取所有数据 +func GetAllFromCache(cond bson.M, sort bson.D) (res []immessagemod.ImMessage, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoListKey(models.ImMessage+"-all").ResBind(&res).Cache(immessagemod.GetAll, cond, sort) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data immessagemod.ImMessage) (primitive.ObjectID, error) { + newID, err := immessagemod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.ImMessage).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := immessagemod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.ImMessage).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = immessagemod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.ImMessage).AutoClear(true, &all) + return +} diff --git a/models/cache/mediabookshelfdata/data.go b/models/cache/mediabookshelfdata/data.go new file mode 100755 index 0000000..b939922 --- /dev/null +++ b/models/cache/mediabookshelfdata/data.go @@ -0,0 +1,116 @@ +package mediabookshelfdata + +import ( + "91porn-server/models/v/mediabookshelfmod" + "91porn-server/models/v/mediamod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +// AppData 需要组装返回的数据 +type AppData struct { + BaseInfo *mediabookshelfmod.MediaBookshelfInfo `json:"baseInfo"` // 基础信息 +} + +// FormatAppDataList 格式化app列表数据 +func FormatAppDataList(origin []mediabookshelfmod.MediaBookshelf) (res []AppData) { + res = make([]AppData, len(origin)) + if len(origin) == 0 { + return + } + + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppData(item) + } + + return +} + +// FormatAppData 格式化app数据 +func FormatAppData(item mediabookshelfmod.MediaBookshelf) (res AppData) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res.BaseInfo = &mediabookshelfmod.MediaBookshelfInfo{ + ID: item.ID, + Name: item.Name, + UID: item.UID, + ReadAt: item.ReadAt, + CreatedAt: item.CreatedAt, + UpdateTime: item.UpdateTime, + } + + return +} + +// AppSimpleData 仅返回基础数据 +type AppSimpleData *mediabookshelfmod.MediaBookshelfInfo + +// FormatAppSimpleDataList 格式化app列表数据 +func FormatAppSimpleDataList(origin []mediabookshelfmod.MediaBookshelf, skip, limit int64, sort bson.D) (res []AppSimpleData) { + res = make([]AppSimpleData, len(origin)) + if len(origin) == 0 { + return + } + + var mids []primitive.ObjectID + for i := range origin { + mids = append(mids, origin[i].MID) + } + + filter := bson.M{"_id": bson.M{"$in": mids}} + opts := options.Find().SetSkip(skip).SetLimit(limit) + if len(sort) > 0 { + opts = opts.SetSort(sort) + } + media, err := mediamod.QueryMediaByCond(filter, opts) + if err != nil { + return + } + m := make(map[primitive.ObjectID]*mediamod.Media) + for i := range media { + m[media[i].ID] = media[i] + } + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppSimpleData(item, m[item.MID]) + } + + return +} + +// FormatAppSimpleData 格式化app数据 +func FormatAppSimpleData(item mediabookshelfmod.MediaBookshelf, media *mediamod.Media) (res AppSimpleData) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res = &mediabookshelfmod.MediaBookshelfInfo{ + ID: item.MID, + //MID: item.MID, + Name: media.Title, + VerticalCover: media.VerticalCover, + TotalEpisode: media.TotalEpisode, + CurrentEpisode: media.CurrentEpisode, + MediaType: media.MediaType, + MediaSubType: int(media.MediaSubType), + UpdateStatus: media.UpdateStatus, + CountBrowse: media.CountBrowse, + Permission: media.Permission, + Price: media.Price, + UID: item.UID, + ReadAt: item.ReadAt, + CreatedAt: item.CreatedAt, + UpdateTime: item.UpdateTime, + } + if time.Now().Add(-time.Hour*24*7).Unix() < media.ContentUpdateTime.Unix() { + res.IsNew = true + } + return +} diff --git a/models/cache/mediabookshelfdata/mediabookshelf.go b/models/cache/mediabookshelfdata/mediabookshelf.go new file mode 100755 index 0000000..d24a86b --- /dev/null +++ b/models/cache/mediabookshelfdata/mediabookshelf.go @@ -0,0 +1,110 @@ +package mediabookshelfdata + +import ( + "fmt" + "time" + + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/mediabookshelfmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func getKey(uid uint64) string { + return fmt.Sprintf("%s-%d", models.MediaBookshelf, uid) +} + +type ListRes struct { + List []mediabookshelfmod.MediaBookshelf + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(uid uint64, cond bson.M, skip, limit int64, sort bson.D) ([]mediabookshelfmod.MediaBookshelf, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(10*time.Minute).AutoListKey(getKey(uid)).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = mediabookshelfmod.GetList(cond, skip, limit, sort) + return +} + +func GetInfoByCond(cond bson.M) (mediabookshelfmod.MediaBookshelf, error) { + return mediabookshelfmod.GetInfoByCond(cond) +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, uid uint64, data mediabookshelfmod.MediaBookshelf) (primitive.ObjectID, error) { + newID, err := mediabookshelfmod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(getKey(uid)).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := mediabookshelfmod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.MediaBookshelf).AutoClear(true, nil) + + return affect, err +} + +// DeleteDataByCond 删除数据 +func DeleteDataByCond(t *db.MongoTool, uid uint64, cond bson.M) (err error) { + err = mediabookshelfmod.DeleteByCond(t, cond) + if err != nil { + return + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(getKey(uid)).AutoClear(true, nil) + return +} + +// DeleteDataByIds 删除数据 +func DeleteDataByIds(t *db.MongoTool, uid uint64, ids []primitive.ObjectID) (err error) { + err = mediabookshelfmod.DeleteByIDS(t, uid, ids) + if err != nil { + return + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(getKey(uid)).AutoClear(true, nil) + return +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = mediabookshelfmod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.MediaBookshelf).AutoClear(true, &all) + return +} diff --git a/models/cache/mediacontentdata/mediacontent.go b/models/cache/mediacontentdata/mediacontent.go new file mode 100755 index 0000000..a79cf4a --- /dev/null +++ b/models/cache/mediacontentdata/mediacontent.go @@ -0,0 +1,37 @@ +package mediacontentdata + +import ( + "time" + + "91porn-server/common/cachev2" + "91porn-server/models" + "91porn-server/models/v/mediacontentmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []mediacontentmod.MediaContent + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]mediacontentmod.MediaContent, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(10*time.Minute).AutoListKey(models.MediaContent).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = mediacontentmod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res mediacontentmod.MediaContent, err error) { + _, err = cachev2.Classes().CacheTime(10*time.Minute).AutoInfoKey(models.MediaContent, id.Hex()).ResBind(&res).Cache(mediacontentmod.GetInfo, id, true) + return +} diff --git a/models/cache/mediadata/media.go b/models/cache/mediadata/media.go new file mode 100755 index 0000000..fd9b2fb --- /dev/null +++ b/models/cache/mediadata/media.go @@ -0,0 +1,37 @@ +package mediadata + +import ( + "time" + + "91porn-server/common/cachev2" + "91porn-server/models" + "91porn-server/models/v/mediamod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []*mediamod.Media + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]*mediamod.Media, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(10*time.Minute).AutoListKey(models.Media).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.HasNext, err = mediamod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res mediamod.Media, err error) { + _, err = cachev2.Classes().CacheTime(5*time.Minute).AutoInfoKey(models.Media, id.Hex()).ResBind(&res).Cache(mediamod.GetInfo, id) + return +} diff --git a/models/cache/mediatagdata/mediatag.go b/models/cache/mediatagdata/mediatag.go new file mode 100755 index 0000000..7901201 --- /dev/null +++ b/models/cache/mediatagdata/mediatag.go @@ -0,0 +1,94 @@ +package mediatagdata + +import ( + "time" + + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/mediatagmod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []mediatagmod.MediaTag + Count int64 + HasNext bool +} + +// GetTagIdsByNameFromCache 通过名字获取标签id +func GetTagIdsByNameFromCache(name string) (ids []primitive.ObjectID, err error) { + _, err = cachev2.Classes().CacheTime(300*time.Second).AutoListKey(models.MediaTag).ResBind(&ids).Cache(mediatagmod.FindTagIdsByName, name) + return +} + +// GetTagByNameFromCache 通过名字获取标签id +func GetTagByNameFromCache(name string) (tag mediatagmod.MediaTag, err error) { + _, err = cachev2.Classes().CacheTime(300*time.Second).AutoListKey(models.MediaTag).ResBind(&tag).Cache(mediatagmod.FindOneTagByName, name) + return tag, err +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]mediatagmod.MediaTag, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(3600*time.Second).AutoListKey(models.MediaTag).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = mediatagmod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res mediatagmod.MediaTag, err error) { + _, err = cachev2.Classes().CacheTime(7200*time.Second).AutoInfoKey(models.MediaTag, id.Hex()).ResBind(&res).Cache(mediatagmod.GetInfo, id) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data mediatagmod.MediaTag) (primitive.ObjectID, error) { + newID, err := mediatagmod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.MediaTag).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := mediatagmod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.MediaTag).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = mediatagmod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.MediaTag).AutoClear(true, &all) + return +} diff --git a/models/cache/mediatagdimensiondata/mediatagdimension.go b/models/cache/mediatagdimensiondata/mediatagdimension.go new file mode 100755 index 0000000..6e909bc --- /dev/null +++ b/models/cache/mediatagdimensiondata/mediatagdimension.go @@ -0,0 +1,82 @@ +package mediatagdimensiondata + +import ( + "time" + + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/mediatagdimensionmod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []mediatagdimensionmod.MediaTagDimension + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]mediatagdimensionmod.MediaTagDimension, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(3600*time.Second).AutoListKey(models.MediaTagDimension).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = mediatagdimensionmod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res mediatagdimensionmod.MediaTagDimension, err error) { + _, err = cachev2.Classes().CacheTime(7200*time.Second).AutoInfoKey(models.MediaTagDimension, id.Hex()).ResBind(&res).Cache(mediatagdimensionmod.GetInfo, id) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data mediatagdimensionmod.MediaTagDimension) (primitive.ObjectID, error) { + newID, err := mediatagdimensionmod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.MediaTagDimension).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := mediatagdimensionmod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.MediaTagDimension).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = mediatagdimensionmod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.MediaTagDimension).AutoClear(true, &all) + return +} diff --git a/models/cache/nakedchatdata/data.go b/models/cache/nakedchatdata/data.go new file mode 100755 index 0000000..1a20a37 --- /dev/null +++ b/models/cache/nakedchatdata/data.go @@ -0,0 +1,74 @@ +package nakedchatdata + +import ( + "91porn-server/models/v/nakedchatmod" +) + +// FormatAppDataList 格式化app列表数据 +func FormatAppDataList(origin []nakedchatmod.NakedChat) (res []*nakedchatmod.NakedChatSimple) { + res = make([]*nakedchatmod.NakedChatSimple, len(origin)) + if len(origin) == 0 { + return + } + + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppSimpleData(item) + } + + return +} + +// FormatAppSimpleData 格式化app数据 +func FormatAppSimpleData(item nakedchatmod.NakedChat) (res *nakedchatmod.NakedChatSimple) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res = &nakedchatmod.NakedChatSimple{ + ID: item.ID, + Title: item.Title, + Cover: item.Cover, + Price: item.Price, + Age: item.Age, + Weight: item.Weight, + Height: item.Height, + Cup: item.Cup, + SaleNum: item.SaleNum + item.FakeSaleNum, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + + return +} + +// FormatAppData 格式化app数据 +func FormatAppData(item nakedchatmod.NakedChat) (res *nakedchatmod.NakedChatInfo) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res = &nakedchatmod.NakedChatInfo{ + ID: item.ID, + Title: item.Title, + Cover: item.Cover, + Price: item.Price, + Options: item.Options, + Images: item.Images, + Video: item.Video, + Contact: item.Contact, + Age: item.Age, + Weight: item.Weight, + Height: item.Height, + Cup: item.Cup, + SaleNum: item.SaleNum + item.FakeSaleNum, + BusinessHours: item.BusinessHours, + Summary: item.Summary, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + + return +} diff --git a/models/cache/nakedchatdata/nakedchat.go b/models/cache/nakedchatdata/nakedchat.go new file mode 100755 index 0000000..a5ac0e3 --- /dev/null +++ b/models/cache/nakedchatdata/nakedchat.go @@ -0,0 +1,89 @@ +package nakedchatdata + +import ( + "time" + + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/nakedchatmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []nakedchatmod.NakedChat + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]nakedchatmod.NakedChat, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(10*time.Minute).AutoListKey(models.NakedChat).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = nakedchatmod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res nakedchatmod.NakedChat, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoInfoKey(models.NakedChat, id.Hex()).ResBind(&res).Cache(nakedchatmod.GetInfo, id) + return +} + +// GetAllFromCache 获取所有数据 +func GetAllFromCache(cond bson.M, sort bson.D) (res []nakedchatmod.NakedChat, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoListKey(models.NakedChat+"-all").ResBind(&res).Cache(nakedchatmod.GetAll, cond, sort) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data nakedchatmod.NakedChat) (primitive.ObjectID, error) { + newID, err := nakedchatmod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.NakedChat).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := nakedchatmod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.NakedChat).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = nakedchatmod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.NakedChat).AutoClear(true, &all) + return +} diff --git a/models/cache/nakedchatorderdata/data.go b/models/cache/nakedchatorderdata/data.go new file mode 100755 index 0000000..b5514f2 --- /dev/null +++ b/models/cache/nakedchatorderdata/data.go @@ -0,0 +1,44 @@ +package nakedchatorderdata + +import ( + "91porn-server/models/v/nakedchatordermod" +) + +// FormatAppDataList 格式化app列表数据 +func FormatAppDataList(origin []nakedchatordermod.NakedChatOrder) (res []*nakedchatordermod.NakedChatOrderInfo) { + res = make([]*nakedchatordermod.NakedChatOrderInfo, len(origin)) + if len(origin) == 0 { + return + } + + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppData(item) + } + + return +} + +// FormatAppData 格式化app数据 +func FormatAppData(item nakedchatordermod.NakedChatOrder) (res *nakedchatordermod.NakedChatOrderInfo) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res = &nakedchatordermod.NakedChatOrderInfo{ + ID: item.ID, + Nid: item.Nid, + Uid: item.Uid, + UserContact: item.UserContact, + Num: item.Num, + Price: item.Price, + Amount: item.Amount, + Remark: item.Remark, + Status: item.Status, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + + return +} diff --git a/models/cache/nakedchatorderdata/nakedchatorder.go b/models/cache/nakedchatorderdata/nakedchatorder.go new file mode 100755 index 0000000..16443dd --- /dev/null +++ b/models/cache/nakedchatorderdata/nakedchatorder.go @@ -0,0 +1,89 @@ +package nakedchatorderdata + +import ( + "time" + + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/nakedchatordermod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []nakedchatordermod.NakedChatOrder + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]nakedchatordermod.NakedChatOrder, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(10*time.Minute).AutoListKey(models.NakedChatOrder).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = nakedchatordermod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res nakedchatordermod.NakedChatOrder, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoInfoKey(models.NakedChatOrder, id.Hex()).ResBind(&res).Cache(nakedchatordermod.GetInfo, id) + return +} + +// GetAllFromCache 获取所有数据 +func GetAllFromCache(cond bson.M, sort bson.D) (res []nakedchatordermod.NakedChatOrder, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoListKey(models.NakedChatOrder+"-all").ResBind(&res).Cache(nakedchatordermod.GetAll, cond, sort) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data nakedchatordermod.NakedChatOrder) (primitive.ObjectID, error) { + newID, err := nakedchatordermod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.NakedChatOrder).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := nakedchatordermod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.NakedChatOrder).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = nakedchatordermod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.NakedChatOrder).AutoClear(true, &all) + return +} diff --git a/models/cache/quicksearchdata/data.go b/models/cache/quicksearchdata/data.go new file mode 100755 index 0000000..106b5e6 --- /dev/null +++ b/models/cache/quicksearchdata/data.go @@ -0,0 +1,85 @@ +package quicksearchdata + +import ( + "91porn-server/models/v/quicksearchmod" +) + +// AppData 需要组装返回的数据 +type AppData struct { + BaseInfo *quicksearchmod.QuickSearchInfo `json:"baseInfo"` // 基础信息 +} + +// FormatAppDataList 格式化app列表数据 +func FormatAppDataList(origin []quicksearchmod.QuickSearch) (res []AppData) { + res = make([]AppData, len(origin)) + if len(origin) == 0 { + return + } + + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppData(item) + } + + return +} + +// FormatAppData 格式化app数据 +func FormatAppData(item quicksearchmod.QuickSearch) (res AppData) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res.BaseInfo = &quicksearchmod.QuickSearchInfo{ + ID: item.ID, + Enabled: item.Enabled, + Title: item.Title, + SearchKeyword: item.SearchKeyword, + Link: item.Link, + Type: item.Type, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + + return +} + +// AppSimpleData 仅返回基础数据 +type AppSimpleData *quicksearchmod.QuickSearchInfo + +// FormatAppSimpleDataList 格式化app列表数据 +func FormatAppSimpleDataList(origin []quicksearchmod.QuickSearch) (res []AppSimpleData) { + res = make([]AppSimpleData, len(origin)) + if len(origin) == 0 { + return + } + + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppSimpleData(item) + } + + return +} + +// FormatAppSimpleData 格式化app数据 +func FormatAppSimpleData(item quicksearchmod.QuickSearch) (res AppSimpleData) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res = &quicksearchmod.QuickSearchInfo{ + ID: item.ID, + Enabled: item.Enabled, + Title: item.Title, + SearchKeyword: item.SearchKeyword, + Link: item.Link, + Type: item.Type, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + + return +} diff --git a/models/cache/quicksearchdata/quicksearch.go b/models/cache/quicksearchdata/quicksearch.go new file mode 100755 index 0000000..8b3861a --- /dev/null +++ b/models/cache/quicksearchdata/quicksearch.go @@ -0,0 +1,89 @@ +package quicksearchdata + +import ( + "time" + + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/quicksearchmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []quicksearchmod.QuickSearch + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]quicksearchmod.QuickSearch, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(10*time.Minute).AutoListKey(models.QuickSearch).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = quicksearchmod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res quicksearchmod.QuickSearch, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoInfoKey(models.QuickSearch, id.Hex()).ResBind(&res).Cache(quicksearchmod.GetInfo, id) + return +} + +// GetAllFromCache 获取所有数据 +func GetAllFromCache(cond bson.M) (res []quicksearchmod.QuickSearch, err error) { + _, err = cachev2.Classes().CacheTime(3*time.Minute).AutoListKey(models.QuickSearch+"-all").ResBind(&res).Cache(quicksearchmod.GetAll, cond) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data quicksearchmod.QuickSearch) (primitive.ObjectID, error) { + newID, err := quicksearchmod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.QuickSearch).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := quicksearchmod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.QuickSearch).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = quicksearchmod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.QuickSearch).AutoClear(true, &all) + return +} diff --git a/models/cache/signrecorddata/data.go b/models/cache/signrecorddata/data.go new file mode 100755 index 0000000..1ac7a8c --- /dev/null +++ b/models/cache/signrecorddata/data.go @@ -0,0 +1,89 @@ +package signrecorddata + +import ( + "91porn-server/models/v/signrecordmod" +) + +// AppData 需要组装返回的数据 +type AppData struct { + BaseInfo *signrecordmod.SignRecordInfo `json:"baseInfo"` // 基础信息 +} + +// FormatAppDataList 格式化app列表数据 +func FormatAppDataList(origin []signrecordmod.SignRecord) (res []AppData) { + res = make([]AppData, len(origin)) + if len(origin) == 0 { + return + } + + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppData(item) + } + + return +} + +// FormatAppData 格式化app数据 +func FormatAppData(item signrecordmod.SignRecord) (res AppData) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res.BaseInfo = &signrecordmod.SignRecordInfo{ + ID: item.ID, + PID: item.PID, + UID: item.UID, + TotalDays: item.TotalDays, + CurrentSignDays: item.CurrentSignDays, + ForgetSignDays: item.ForgetSignDays, + RenewalSignDays: item.RenewalSignDays, + SignTime: item.SignTime, + CreatedAt: item.CreatedAt, + UpdateTime: item.UpdateTime, + } + + return +} + +// AppSimpleData 仅返回基础数据 +type AppSimpleData *signrecordmod.SignRecordInfo + +// FormatAppSimpleDataList 格式化app列表数据 +func FormatAppSimpleDataList(origin []signrecordmod.SignRecord) (res []AppSimpleData) { + res = make([]AppSimpleData, len(origin)) + if len(origin) == 0 { + return + } + + // 组装返回数据 + for i, item := range origin { + res[i] = FormatAppSimpleData(item) + } + + return +} + +// FormatAppSimpleData 格式化app数据 +func FormatAppSimpleData(item signrecordmod.SignRecord) (res AppSimpleData) { + if item.ID.IsZero() { + return + } + + // 组装返回数据 + res = &signrecordmod.SignRecordInfo{ + ID: item.ID, + PID: item.PID, + UID: item.UID, + TotalDays: item.TotalDays, + CurrentSignDays: item.CurrentSignDays, + ForgetSignDays: item.ForgetSignDays, + RenewalSignDays: item.RenewalSignDays, + SignTime: item.SignTime, + CreatedAt: item.CreatedAt, + UpdateTime: item.UpdateTime, + } + + return +} diff --git a/models/cache/signrecorddata/signrecord.go b/models/cache/signrecorddata/signrecord.go new file mode 100755 index 0000000..375dd4e --- /dev/null +++ b/models/cache/signrecorddata/signrecord.go @@ -0,0 +1,88 @@ +package signrecorddata + +import ( + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/signrecordmod" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRes struct { + List []signrecordmod.SignRecord + Count int64 + HasNext bool +} + +// GetListFromCache 从缓存获取列表数据 +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]signrecordmod.SignRecord, int64, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(10*time.Minute).AutoListKey(models.SignRecord).ResBind(&data).Cache(GetListData, cond, skip, limit, sort) + return data.List, data.Count, data.HasNext, err +} + +// GetListData 获取列表数据并组装适合缓存格式返回 +func GetListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + res.List, res.Count, res.HasNext, err = signrecordmod.GetList(cond, skip, limit, sort) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res signrecordmod.SignRecord, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoInfoKey(models.SignRecord, id.Hex()).ResBind(&res).Cache(signrecordmod.GetInfo, id) + return +} + +// GetAllFromCache 获取所有数据 +func GetAllFromCache(cond bson.M, sort bson.D) (res []signrecordmod.SignRecord, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoListKey(models.SignRecord+"-all").ResBind(&res).Cache(signrecordmod.GetAll, cond, sort) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data signrecordmod.SignRecord) (primitive.ObjectID, error) { + newID, err := signrecordmod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + _, err = cachev2.Classes().Table(models.SignRecord).AutoClear(true, nil) + if err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := signrecordmod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + // 清理缓存 + _, _ = cachev2.Classes().Table(models.SignRecord).AutoClear(true, &id) + + return affect, err +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = signrecordmod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + all := "*" + _, _ = cachev2.Classes().Table(models.SignRecord).AutoClear(true, &all) + return +} diff --git a/models/cache/sysconfdata/sysconf.go b/models/cache/sysconfdata/sysconf.go new file mode 100755 index 0000000..663142f --- /dev/null +++ b/models/cache/sysconfdata/sysconf.go @@ -0,0 +1,161 @@ +package sysconfdata + +import ( + "strconv" + "time" + + "91porn-server/common/cachev2" + "91porn-server/common/db" + "91porn-server/common/localcache" + "91porn-server/models" + "91porn-server/models/v/sysconfmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetIntsFromSharedCache reads integer settings from the shared cache without +// the process-local one-minute layer, so management changes take effect at once. +func GetIntsFromSharedCache(defaults map[sysconfmod.VCode]int) (map[sysconfmod.VCode]int, error) { + configure, err := getAllFromSharedCache() + if err != nil { + return nil, err + } + values := make(map[sysconfmod.VCode]int, len(defaults)) + for code, fallback := range defaults { + values[code] = fallback + if raw, ok := configure[string(code)]; ok { + if parsed, parseErr := strconv.Atoi(raw); parseErr == nil { + values[code] = parsed + } + } + } + return values, nil +} + +const ( + tmpLocalCacheKey = "AllSysConfTmpKey" + // DefaultTotalWatchCount preserves the legacy free-trial behavior when the + // shared configuration is absent or temporarily unavailable. + DefaultTotalWatchCount uint64 = 3 +) + +// GetAllFromCache 获取全部配置项 +func GetAllFromCache() (res sysconfmod.ConfMap, err error) { + // 频繁访问 从本地进程内缓存中获取,对象不存在则从redis中获取 + r, _ := localcache.C.Get(tmpLocalCacheKey) + if r != nil { + return r.(sysconfmod.ConfMap), nil + } + defer func() { + localcache.C.Set(tmpLocalCacheKey, res, 1*time.Minute) + }() + return getAllFromSharedCache() +} + +// GetTotalWatchCount returns the configured non-VIP free-trial count from the +// existing bounded configuration cache and falls back to the legacy default. +func GetTotalWatchCount() uint64 { + configure, err := GetAllFromCache() + if err != nil { + return DefaultTotalWatchCount + } + return totalWatchCount(configure) +} + +func totalWatchCount(configure sysconfmod.ConfMap) uint64 { + raw, ok := configure[string(sysconfmod.VCodeTotalWatchCount)] + if !ok { + return DefaultTotalWatchCount + } + value, err := strconv.ParseUint(raw, 10, 64) + if err != nil { + return DefaultTotalWatchCount + } + return value +} + +func getAllFromSharedCache() (res sysconfmod.ConfMap, err error) { + _, err = cachev2.Classes().CacheTime(5 * time.Minute).AutoListKey(models.SysConf).ResBind(&res).Cache(sysconfmod.GetAllConfig) + return +} + +// GetBoolFromSharedCache 绕过进程内缓存读取布尔配置,后台修改并清理 Redis 后可立即生效。 +func GetBoolFromSharedCache(code sysconfmod.VCode) (bool, error) { + values, err := GetBoolsFromSharedCache(code) + if err != nil { + return false, err + } + return values[code], nil +} + +// GetBoolsFromSharedCache reads several switches from one shared-cache +// snapshot. This keeps request paths that need related switches from fetching +// and decoding the same configuration map repeatedly. +func GetBoolsFromSharedCache(codes ...sysconfmod.VCode) (map[sysconfmod.VCode]bool, error) { + configure, err := getAllFromSharedCache() + if err != nil { + return nil, err + } + values := make(map[sysconfmod.VCode]bool, len(codes)) + for _, code := range codes { + if _, exists := configure[string(code)]; !exists { + continue + } + values[code] = configure.GetBool(code) + } + return values, nil +} + +func clearAllCache() error { + localcache.C.Delete(tmpLocalCacheKey) + _, err := cachev2.Classes().AutoListKey(models.SysConf).DeleteCurrent() + return err +} + +// GetByGpCodeFromCache 获取分组配置数据 +func GetByGpCodeFromCache(code sysconfmod.GPCode) (res sysconfmod.ConfMap, err error) { + _, err = cachev2.Classes().CacheTime(30*time.Minute).AutoListKey(models.SysConf).ResBind(&res).Cache(sysconfmod.GetByGpCode, code) + return +} + +// InsertData 插入数据 +func InsertData(t *db.MongoTool, data sysconfmod.SysConf) (primitive.ObjectID, error) { + newID, err := sysconfmod.Insert(t, data) + if err != nil { + return newID, err + } + // 清除缓存数据 + if err = clearAllCache(); err != nil { + return newID, err + } + return newID, nil +} + +// UpdateData 更新数据 +func UpdateData(t *db.MongoTool, id string, data map[string]interface{}) (int64, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return 0, err + } + + affect, err := sysconfmod.UpdateByID(t, objID, data) + if err != nil { + return 0, err + } + + if err = clearAllCache(); err != nil { + return affect, err + } + return affect, nil +} + +// DeleteData 删除数据 +func DeleteData(t *db.MongoTool, id primitive.ObjectID) (err error) { + err = sysconfmod.DeleteByID(t, id) + if err != nil { + return + } + + // 清理缓存 + return clearAllCache() +} diff --git a/models/cache/sysconfdata/sysconf_test.go b/models/cache/sysconfdata/sysconf_test.go new file mode 100644 index 0000000..45bee6e --- /dev/null +++ b/models/cache/sysconfdata/sysconf_test.go @@ -0,0 +1,27 @@ +package sysconfdata + +import ( + "testing" + + "91porn-server/models/v/sysconfmod" +) + +func TestTotalWatchCount(t *testing.T) { + tests := []struct { + name string + config sysconfmod.ConfMap + want uint64 + }{ + {name: "configured", config: sysconfmod.ConfMap{string(sysconfmod.VCodeTotalWatchCount): "5"}, want: 5}, + {name: "zero is allowed", config: sysconfmod.ConfMap{string(sysconfmod.VCodeTotalWatchCount): "0"}, want: 0}, + {name: "missing", config: sysconfmod.ConfMap{}, want: DefaultTotalWatchCount}, + {name: "invalid", config: sysconfmod.ConfMap{string(sysconfmod.VCodeTotalWatchCount): "invalid"}, want: DefaultTotalWatchCount}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := totalWatchCount(tt.config); got != tt.want { + t.Fatalf("totalWatchCount() = %d, want %d", got, tt.want) + } + }) + } +} diff --git a/models/cache/viddata/vid.go b/models/cache/viddata/vid.go new file mode 100755 index 0000000..7316a68 --- /dev/null +++ b/models/cache/viddata/vid.go @@ -0,0 +1,36 @@ +package viddata + +import ( + "91porn-server/common/cachev2" + "91porn-server/models" + "91porn-server/models/v/vidmod" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "time" + + "go.mongodb.org/mongo-driver/bson" +) + +type ListRes struct { + List []*vidmod.VideoModel + HasNext bool +} + +func GetListFromCache(cond bson.M, skip, limit int64, sort bson.D) ([]*vidmod.VideoModel, bool, error) { + var data ListRes + _, err := cachev2.Classes().CacheTime(10*time.Minute).AutoListKey(models.VideoInfo).ResBind(&data).Cache(getListData, cond, skip, limit, sort) + return data.List, data.HasNext, err +} + +// getListData 获取列表数据并组装适合缓存格式返回 +func getListData(cond bson.M, skip, limit int64, sort bson.D) (res ListRes, err error) { + opt := options.Find().SetLimit(limit).SetSkip(skip).SetSort(sort) + res.List, res.HasNext, err = vidmod.FindList(cond, opt) + return +} + +// GetInfoFromCache 获取数据详情 +func GetInfoFromCache(id primitive.ObjectID) (res vidmod.VideoModel, err error) { + _, err = cachev2.Classes().CacheTime(5*time.Minute).AutoInfoKey(models.VideoInfo, id.Hex()).ResBind(&res).Cache(vidmod.GetVideoInfo, id.Hex()) + return +} diff --git a/models/commod/commod.go b/models/commod/commod.go new file mode 100644 index 0000000..5feaeec --- /dev/null +++ b/models/commod/commod.go @@ -0,0 +1,156 @@ +package commod + +import ( + "reflect" + + "91porn-server/common/stderr" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// Page 客户端传低过来的分页信息 对外暴露 +type Page struct { + PageNumber uint64 `form:"pageNumber" json:"pageNumber" binding:"required,min=1"` // 当前页 + PageSize uint64 `form:"pageSize" json:"pageSize" binding:"required,min=1,max=100"` // 每页条数 + Sort []Sorts `form:"sort" json:"sort"` +} + +type Sorts struct { + SortKey string `json:"sortKey"` + SortVal int `json:"sortVal"` +} + +func (p Page) Skip() uint64 { + return (p.PageNumber - 1) * p.PageSize +} + +func (p Page) Skip64() int64 { + return int64((p.PageNumber - 1) * p.PageSize) +} + +func (p Page) Limit64() int64 { + return int64(p.PageSize) +} + +func (p Page) Limit() uint64 { + return p.PageSize +} + +// 注意:Page.Sort内元素顺序决定排序顺序 +func (p Page) GetSort() bson.D { + sorts := make(bson.D, len(p.Sort)) + for i, v := range p.Sort { + sorts[i] = bson.E{Key: v.SortKey, Value: v.SortVal} + } + return sorts +} + +// Sig 签名结构体 +type Sig struct { + AccessKey string `form:"accessKey" json:"accessKey" binding:"required"` + BucketName string `form:"bucketName" json:"bucketName" binding:"required"` +} + +// Resp 返回结构体 +type Resp struct { + Code stderr.Code `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` +} + +// ListResp 分页列表 公共返回结构体 +type ListResp struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List interface{} `json:"list"` +} + +// Page 客户端传低过来的分页信息 内部使用 +type PageBy struct { + CheckNext bool + Num uint64 + Size uint64 +} + +// OrderBy 排序 +type OrderBy struct { + Key string // 排序的字段 + Desc bool // 排序方式 是否倒序 +} + +// StdQuery 客户端传过来的时间条件信息 +type StdQuery struct { + Page *PageBy + Order *[]OrderBy +} + +// ConvertToListQuery 分页列表 通用条件查询条件组装 +func ConvertToListQuery(s StdQuery) (opts *options.FindOptions) { + opts = &options.FindOptions{} + if s.Page != nil { + skip := int64((s.Page.Num - 1) * s.Page.Size) + limit := int64(s.Page.Size) + if s.Page.CheckNext { + limit++ + } + opts.Skip = &skip + opts.Limit = &limit + } + if s.Order != nil && len(*s.Order) > 0 { + sort := bson.D{} + for _, v := range *s.Order { + e := bson.E{} + if v.Desc { + e.Key = v.Key + e.Value = -1 + + } else { + e.Key = v.Key + e.Value = 1 + } + sort = append(sort, e) + } + opts.Sort = sort + } + return +} + +// StructToMap2 对象转map,去掉无效值 +func StructToMap3(u interface{}) map[string]interface{} { + t := reflect.TypeOf(u) + v := reflect.ValueOf(u) + m := make(map[string]interface{}) + for i := 0; i < t.NumField(); i++ { + fv := v.Field(i).Type() + vv := v.Field(i) + k, _ := t.Field(i).Tag.Lookup("bson") + switch fv.Kind() { + case reflect.String: + if vv.String() != "" { + if k == "_id" { + idv, _ := primitive.ObjectIDFromHex(vv.String()) + m["_id"] = idv + + } else { + m[k] = vv.String() + } + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if vv.Int() != 0 { + m[k] = vv.Interface() + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + if vv.Uint() != 0 { + m[k] = vv.Interface() + } + case reflect.Interface, reflect.Ptr: + if vv.Interface() != nil { + m[k] = vv.Interface() + } + } + + } + return m +} diff --git a/models/commod/consts.go b/models/commod/consts.go new file mode 100644 index 0000000..b936b41 --- /dev/null +++ b/models/commod/consts.go @@ -0,0 +1,44 @@ +package commod + +const ( + LouFengCostNotify = "LouFengCostNotify:%d" //用户楼凤消费通知 + GameRechargePoliteNotify = "GameRechargePoliteNotify:%d" //用户游戏充值有礼通知 +) + +// SystemConfigType 系统配置类型 +type SystemConfigType int + +const ( + NudeChatSpecialArea SystemConfigType = iota // 裸聊专区 +) + +// SystemLocationCode 系统位置编号 +type SystemLocationCode int + +const ( + OfficialBulletin SystemLocationCode = iota // 官方公告 +) + +// BuyType 充值购买类型 +type BuyType int + +const ( + BuyGold BuyType = iota + 1 // 购买金币 + BuyGameCoin // 购买游戏币 + BuyFruitCoin // 购买果币 + BuyProduct // 购买商品 + //BuyNudeChatService // 购买裸聊服务 +) + +type AdGroup string + +const ( + AdGroupNone AdGroup = "" // 无组 + AdGroupA AdGroup = "A" // A组 + AdGroupB AdGroup = "B" // B组 + AdGroupC AdGroup = "C" // C组 +) + +func (ag AdGroup) InABC() bool { + return ag == AdGroupA || ag == AdGroupB || ag == AdGroupC +} diff --git a/models/commod/dedType.go b/models/commod/dedType.go new file mode 100644 index 0000000..0d475fa --- /dev/null +++ b/models/commod/dedType.go @@ -0,0 +1,52 @@ +package commod + +import ( + "errors" + "math" +) + +// 扣量类型 +type DeductType int + +const ( + NotDed DeductType = iota +) + +const ( + DedConsume DeductType = 1 << iota + DedNewUser + DedBooker + DedRecharge +) + +func (d DeductType) Has(t DeductType) bool { + return int(t)&int(d) == int(t) +} + +// BitSet 位集合 +func (d DeductType) BitSet() []int { + return []int{int(math.Log2(float64(d)))} +} + +func (d DeductType) String() string { + switch d { + case DedConsume: + return "dedConsume" + case DedNewUser: + return "dedNewUser" + case DedBooker: + return "dedBooker" + case DedConsume | DedNewUser: + return "dedConsumeAndNewUser" + default: + return "unknow" + } +} + +func ToDeductType(v int) (DeductType, error) { + all := DedConsume | DedNewUser | DedBooker + if all.Has(DeductType(v)) { + return DeductType(v), nil + } + return NotDed, errors.New(NotDed.String()) +} diff --git a/models/commod/disc.go b/models/commod/disc.go new file mode 100644 index 0000000..e323e09 --- /dev/null +++ b/models/commod/disc.go @@ -0,0 +1,93 @@ +package commod + +import ( + "strings" + "time" + + "91porn-server/common/pageopt" +) + +type DiscSeqe struct { + DistrictCode string `json:"districtCode" bson:"districtCode"` //商区码 + PromSeqe string `json:"promSeqe" bson:"promSeqe"` //推广序列 +} + +func (d *DiscSeqe) String() string { + if d.PromSeqe == "" { + return d.DistrictCode + } + return strings.ToUpper(strings.Join([]string{d.DistrictCode, d.PromSeqe}, "-")) +} + +func (p *DiscSeqe) From(s string) *DiscSeqe { + p.Clean() + list := strings.Split(s, "-") + if len(list) > 0 { + p.DistrictCode = list[0] + } + if len(list) > 1 { + p.PromSeqe = list[1] + } + return p +} + +func (p *DiscSeqe) Clean() { + p.DistrictCode = "" + p.PromSeqe = "" +} + +type DiscDoc struct { + //商区码和推广序列 + DiscSeqe `bson:",inline"` + //true:是直推用户 + IsDirect bool `json:"isDirect" bson:"isDirect"` + //商区绑定时间 + DiscBindAt time.Time `bson:"discBindAt"` +} + +func NewDiscDoc(discSeqe DiscSeqe, isDirect bool, discBindAt time.Time) DiscDoc { + return DiscDoc{ + discSeqe, + isDirect, //直推:推广链接没有推广码, 否则是分裂, + discBindAt, + } +} + +type Matcher = pageopt.Matcher + +// IsDirectMatch +type IsDirectMatch struct { + IsDirect *bool +} + +func (b *IsDirectMatch) New() Matcher { + return pageopt.NewAssignMatch("isDirect", b.IsDirect) +} + +// DistrictCodeMatch +type DistrictCodeMatch struct { + DistrictCode *string +} + +func (s *DistrictCodeMatch) New() Matcher { + return pageopt.NewAssignMatch("districtCode", s.DistrictCode) +} + +// DistrictCodeInMatch +type DistrictCodeInMatch struct { + Codes []string +} + +func (s *DistrictCodeInMatch) New() Matcher { + return pageopt.NewInMatch("districtCode", s.Codes) +} + +// DiscBindAtGTEAndLTMatch +type DiscBindAtGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (c *DiscBindAtGTEAndLTMatch) New() Matcher { + return pageopt.NewGTEAndLTMatch("discBindAt", c.GTE, c.LT) +} diff --git a/models/commod/ldy.go b/models/commod/ldy.go new file mode 100644 index 0000000..53d365c --- /dev/null +++ b/models/commod/ldy.go @@ -0,0 +1,14 @@ +package commod + +import "91porn-server/common/constant" + +type ButtonProp struct { + Style constant.SwitchStyle `form:"style" json:"style" bson:"style"` //开关样式 + Enable bool `form:"enable" json:"enable" bson:"enable"` //true:显示按钮 +} + +type LdyButton struct { + Name string `form:"name" json:"name" bson:"name"` //开关名 + Type constant.SwitchAct `form:"type" json:"type" bson:"type"` //开关类型 + ButtonProp `bson:",inline"` +} diff --git a/models/commod/product.go b/models/commod/product.go new file mode 100644 index 0000000..9c205eb --- /dev/null +++ b/models/commod/product.go @@ -0,0 +1,34 @@ +package commod + +type ProductType int + +const ( + VIP ProductType = 0 // product 0 + VIDEO ProductType = 1 // VIDEO视频1 + MODEL ProductType = 2 // MODEL嫩模2 + MeetingCard ProductType = 3 // 约会卡 3 + GAME ProductType = 4 // 游戏币 4 + NEWUSERCard ProductType = 5 // 新手卡 5 + PhysicalGoods ProductType = 6 // 实体商品 6 + AudioBook ProductType = 8 // 语音小说 8 + VideoDiscount ProductType = 13 // 视频折扣卡 13 + VideoFreeCard ProductType = 14 // 视频免费卡 14 + CoinMonthCard ProductType = 18 // 金币月卡 18 + Media ProductType = 19 // 动漫整本 19 + AdvanceCard ProductType = 21 // 预售卡 21 + GameAdvanceCard ProductType = 22 // 游戏预售卡 22 + WhoringCard ProductType = 24 // 白嫖卡 24 + ImGroup ProductType = 101 // 加入群 101 + NakedChat ProductType = 102 // 裸聊 102 +) +const OTHER = 20 //OTHER 20 + +// CurrencyType 货币类型 +type CurrencyType int + +const ( + Gold CurrencyType = iota + 1 // 金币 + GameCoin // 游戏币 + FruitCoin // 果币 + AiMateCoin // AI伴侣币 +) diff --git a/models/commod/sort.go b/models/commod/sort.go new file mode 100644 index 0000000..8b7d709 --- /dev/null +++ b/models/commod/sort.go @@ -0,0 +1,65 @@ +package commod + +type SortType int + +// 1、最新,2、最热/推荐,3、最多播放,4、十分钟以上视频, 5、精华/精选,6、视频 7-最多收藏 8、解锁次数 9、最新热评 +const ( + New SortType = 1 // 最新上架 + MostHot SortType = 2 // 热门推荐 + MostWatch SortType = 3 // 最多观看 + MostCollect SortType = 7 // 最多收藏 + HotComment SortType = 9 // 最新热评 +) + +var nameMap = map[SortType]string{ + New: "最新上架", + MostHot: "热门推荐", + MostWatch: "最多观看", + HotComment: "最新热评", + MostCollect: "最多收藏", +} + +func (s SortType) Name() string { + name, ok := nameMap[s] + if !ok { + return "未知排序" + } + + return name +} + +func (s SortType) Item() SortItemData { + item := SortItemData{ + Value: s, + Name: "未知排序", + } + name, ok := nameMap[s] + if !ok { + return item + } + item.Name = name + return item +} + +type SortItemData struct { + Value SortType `json:"value"` + Name string `json:"name"` +} + +// WebSortRules 后台支持修改的排序规则 +var WebSortRules = map[string][]SortItemData{ + "acgSort": { + New.Item(), + MostHot.Item(), + MostWatch.Item(), + MostCollect.Item(), + HotComment.Item(), + }, + "videoSort": { + New.Item(), + MostHot.Item(), + MostWatch.Item(), + MostCollect.Item(), + HotComment.Item(), + }, +} diff --git a/models/commod/stat-center.go b/models/commod/stat-center.go new file mode 100644 index 0000000..e1392f8 --- /dev/null +++ b/models/commod/stat-center.go @@ -0,0 +1,165 @@ +package commod + +import ( + "time" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +//统计中心Model + +type KFKTopic string + +const ( + KFK_APPID int32 = 204 // 205:91PORN + KFK_APP_NAME = "91PORN" // app名字 + USER_REG KFKTopic = "user_register" // 用户注册 + USER_RECH KFKTopic = "user_recharge" // 用户充值 + USER_RECH_ALL KFKTopic = "user_recharge_all" // 全部订单 + USER_ACCE KFKTopic = "user_access" // 用户访问 + USER_WOTHDRAW KFKTopic = "user_withdraw" // 用户提现 + USER_BINDING KFKTopic = "user_binding" // 用户绑定 + USER_INVITE KFKTopic = "user_invite" // 用户邀请 + ConsumeRecordJob = "consume_record" // 产品消费流水 + CardSellJob KFKTopic = "card_sell" // 会员卡特权卡销售流水 + AiSellJob = "ai_sell" // AI销售流水 + AdsTimeLongVideo = 15 // 长视频广告时间(单位: 秒) +) + +type AiSellMsg struct { + AppID int32 `json:"appID" bson:"appID"` // AppID + UID uint64 `json:"uid" bson:"uid"` // UID + UniqID string `json:"uniqID" bson:"uniqID"` // 唯一ID,各个app内部交易ID + Amount int64 `json:"amount" bson:"amount"` // 金币数 + TranType string `json:"tranType" bson:"tranType"` // 交易类型 + SysType string `json:"sysType" bson:"sysType"` // 系统类型 + CurrencyType string `json:"currencyType" bson:"currencyType"` // 交易类型,pay、free + TranCreatedAt time.Time `json:"tranCreatedAt" bson:"tranCreatedAt"` // 交易完成时间 + IsRepurchase string `json:"isRepurchase" bson:"isRepurchase"` // 是否复购,yes、no + +} + +// UserRegisterMsg 用户注册 +type UserRegisterMsg struct { + UserId uint64 `json:"userId"` + AppId int32 `json:"appId"` + PlatformId string `json:"platformId"` // 原始平台流水Id + SysType string `json:"sysType"` + DevType string `json:"devType"` + Mobile string `json:"mobile"` + Name string `json:"name"` + IP string `json:"ip"` + IsDirect bool `json:"isDirect"` + DistrictCode string `json:"districtCode"` // 渠道推广 dc + PromSeqe string `json:"promSeqe"` // 代理推广 pc + PUC string `json:"puc" bson:"puc"` // + PromCode string `json:"promCode"` // 用户推广序列(全名代理) + RegisterTime time.Time `json:"registerTime"` +} + +// UserAccessMsg 用户访问 用户每天第一访问 +type UserAccessMsg struct { + UserId uint64 `json:"userId"` + PlatformId string `json:"platformId"` // 原始平台流水Id + AppId int32 `json:"appId"` + SysType string `json:"sysType"` + DevType string `json:"devType"` + IP string `json:"ip"` + Version string `json:"version"` + DevID string `json:"devID"` + VisitAt time.Time `json:"visitAt"` + + IsDirect bool `json:"isDirect,omitempty"` // true:是直推用户 + DistrictCode string `json:"districtCode,omitempty"` // 渠道码 + RegisterTime time.Time `json:"registerTime,omitempty"` //用户注册时间 + IsDeduction bool `json:"isDeduction,omitempty"` // true:CPA扣量用户 +} + +// UserBindingMsg 用户绑定 +type UserBindingMsg struct { + UserId uint64 `json:"userId"` // 用户ID + AppId int32 `json:"appId"` // appID + PlatformId string `json:"platformId"` // 原始平台流水Id + SysType string `json:"sysType"` // 操作系统类型 安卓 IOS + DevType string `json:"devType"` // 设备类型 + Mobile string `json:"mobile"` // 手机号 + BindingTime *time.Time `json:"bindingTime"` // 绑定时间 +} + +type UserInviteBindMsg struct { + UserId uint64 `json:"userId" bson:"userId" binding:"required"` // 用户ID + AppId int32 `json:"appId" bson:"appId" binding:"required"` // appID + ParentPromCode string `json:"parentPromCode,omitempty" bson:"promSeqe"` // 邀请人推广码 + InviteTime time.Time `json:"inviteTime,omitempty" bson:"inviteTime" binding:"required"` // 邀请码绑定时间 +} + +const ( + StatVipCard = iota //会员卡 + StatLouFeng //楼凤 + StatValueAddSer //增值服务 +) + +const ( + CurrencyTypeGold = iota //金币 + CurrencyTypeCash //现金 +) + +type ConsumeRecordMsg struct { + AppID int32 `json:"appID" bson:"appID"` + UID uint64 `json:"uid" bson:"uid"` // + Type int `json:"type" bson:"type"` //消费类型 + CurrencyType int `json:"currencyType" bson:"currencyType"` //货币类型 0金币 1人民币 + Money decimal.Decimal `json:"money" bson:"money"` //人民币 元 + Amount decimal.Decimal `json:"amount" bson:"amount"` //金币(角) + Uniq string `json:"uniq" bson:"uniq"` //唯一号码 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //记录创建时间 +} + +// 用户充值消息 +type UserRechargeMsg struct { + UserId uint64 `json:"userId" bson:"userId" binding:"required"` // 用户ID + AppId int32 `json:"appId" bson:"appId" binding:"required"` // appID + PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id + SysType string `json:"sysType" bson:"sysType" binding:"required"` // 操作系统类型 安卓 IOS + DevType string `json:"devType" bson:"devType"` // 设备类型 + ChannelName string `json:"channelName" bson:"channelName"` // 支付渠道名字 + CID string `json:"cid" bson:"cid"` // 渠道id + Type string `bson:"type" json:"type"` // 充值方式 + OrderId string `json:"orderId" bson:"orderId" binding:"required"` // 流水id + OID string `json:"oid" bson:"oid"` //第三方支付流水id + Money int64 `json:"money" bson:"money" binding:"required"` // 充值金额 订单金额 + PayMoney int64 `json:"payMoney" bson:"payMoney" binding:"required"` // 实际到账金额 用户实际支付金额 + Status int `json:"status" bson:"status"` // 2付款失败 3付款成功(目前只有成功才发送) + Rate string `json:"rate"` //渠道费率 + SuccessAt time.Time `json:"successAt,omitempty" bson:"successAt"` // 成功时间 + ProductType int `json:"productType" bson:"productType"` + ChanShareMod int `json:"chanShareMod" bson:"chanShareMod"` //渠道分成 0消费分成 1金币分成 2不分成 + /*......*/ +} + +type CardSellMsg struct { + AppID int32 `json:"appID" bson:"appID"` // AppID + UID uint64 `json:"uid" bson:"uid"` // UID + UniqID string `json:"uniqID" bson:"uniqID"` // 唯一ID,各个app内部交易ID + Amount int64 `json:"amount" bson:"amount"` // 金币数 + TranTypeInt int64 `json:"tranTypeInt" bson:"tranTypeInt"` // 交易类型码 + TranType string `json:"tranType" bson:"tranType"` // 交易类型文字描述 + SysType string `json:"sysType" bson:"sysType"` // 系统类型 + CurrencyType int `json:"currencyType" bson:"currencyType"` // 交易类型(0 金币 1 现金) + TranCreatedAt time.Time `json:"tranCreatedAt" bson:"tranCreatedAt"` // 交易完成时间 + Product Product `json:"product" bson:"product"` // 产品信息 +} + +type Position struct { + ID primitive.ObjectID `json:"id" bson:"id"` // 位置ID + Name string `json:"name" bson:"name"` // 位置名称 +} + +type Product struct { + ID string `json:"id"` // ID + Name string `json:"name"` // 产品名称 + DiscountedPrice int64 `bson:"discountedPrice" json:"discountedPrice"` // 现价 单位角(金币) + ProductType ProductType `bson:"productType" json:"productType"` // 产品类型 + Position Position `json:"position"` // 产品位置 +} diff --git a/models/e/daichongmod/app.go b/models/e/daichongmod/app.go new file mode 100644 index 0000000..f2d256c --- /dev/null +++ b/models/e/daichongmod/app.go @@ -0,0 +1,18 @@ +package daichongmod + +// 获取订单信息 +type GetOrderReq struct { + TraderId int64 `form:"traderId" json:"traderId"` + PlayerId int64 `form:"playerId" json:"playerId"` + Proof string `form:"proof" json:"proof"` //玩家上传的图片名称 + SessionId string `form:"sessionId" json:"sessionId"` //当前会话的sessionId + ProT int `form:"productType" json:"productType" ` //0 站群 1棋牌 +} + +// 聊天信息请求 +type PayInfoReq struct { + TraderId int64 `form:"traderId" json:"traderId" binding:"required"` + PlayerId int64 `form:"playerId" json:"playerId" binding:"required"` + SessionId string `form:"sessionId" json:"sessionId" binding:"required"` //当前会话的sessionId + ProT int `form:"productType" json:"productType" ` //0 站群 1棋牌 +} diff --git a/models/e/daichongmod/web.go b/models/e/daichongmod/web.go new file mode 100644 index 0000000..32be233 --- /dev/null +++ b/models/e/daichongmod/web.go @@ -0,0 +1,34 @@ +package daichongmod + +import "time" + +// 代充回调公用请求结构体 +type CommnReq struct { + AppId string `json:"appId" binding:"required"` //代充平台提供的appId + Data string `json:"data" binding:"required"` //加密字符串 +} + +// 商人登陆 加密结构体 +type LoginSign struct { + Uid string `json:"uid"` + Password string `json:"password"` +} + +// 商人余额 加密结构体 +type BalanceSign struct { + Uids string `json:"uids"` + Time int64 `json:"time"` //时间戳 +} + +// 上分 通知新订单生成 加密结构体 +type NewOrderSign struct { + FromId string `json:"fromId"` //商人uid + ToId string `json:"toId"` //用户uid + Amount int64 `json:"amount"` //上分数量 + OrderId string `json:"orderId"` //订单号 + ProductInfo string `json:"productInfo"` //商品信息 + Time int64 `json:"time"` //本次通知发起的时间 + TransNo string `json:"transNo"` //请求订单 + ProT int `json:"productType"` //0站群 1棋牌 + SuccessAt time.Time `json:"successAt"` //回调成功时间 +} diff --git a/models/e/sharemod/app.go b/models/e/sharemod/app.go new file mode 100644 index 0000000..787c64b --- /dev/null +++ b/models/e/sharemod/app.go @@ -0,0 +1,41 @@ +package sharemod + +import "91porn-server/models/v/vidmod" + +// VShareReq 内容分享请求 +type VShareReq struct { + Content string `form:"content" json:"content"` + VideoID string `form:"videoID" json:"videoID"` // 可选;视频分享成功时用于累计真实推荐分 + EventID string `form:"eventId" json:"eventId"` // 可选;同一次真实分享事件重试时保持不变,用于推荐分幂等 + ObjType string `form:"objType" json:"objType"` // drama 表示短剧分享 + MediaID string `form:"mediaID" json:"mediaID"` + ContentID string `form:"contentID" json:"contentID"` +} + +// VShareCntReq 视频分享次数 +type VShareCntReq struct { + VideoID string `form:"videoID" json:"videoID"` +} + +// VShareResp 内容分享应答 +type VShareResp struct { + QrCode []byte `json:"qrCode"` +} + +// VShareCntResp 分享次数应答 +type VShareCntResp struct { + VidelID string `json:"videoID"` + Cnt int `json:"cnt"` +} + +// List 分享推荐列表 +type List struct { + Title string `json:"title"` + Cover string `json:"cover"` + SourceUrl string `json:"sourceUrl"` + LandUrl string `json:"landUrl"` + PicUrl string `json:"picUrl"` + Tags []string `json:"tags"` + DownloadUrl string `json:"downloadUrl"` + List []vidmod.ShareInfo `json:"list"` +} diff --git a/models/e/sharemod/type.go b/models/e/sharemod/type.go new file mode 100644 index 0000000..445cfad --- /dev/null +++ b/models/e/sharemod/type.go @@ -0,0 +1,9 @@ +package sharemod + +// VShareModel 分享统计 +type VShareModel struct { + UID int64 `json:"uid" bson:"uid"` + Title int `json:"title" bson:"title"` + ClickCnt int64 `json:"clickCnt" bson:"clickCnt"` + ShareTime int64 `json:"shareTime" bson:"shareTime"` +} diff --git a/models/esTable.go b/models/esTable.go new file mode 100644 index 0000000..12384de --- /dev/null +++ b/models/esTable.go @@ -0,0 +1,15 @@ +package models + +const ( + ESInfoVideoTable = "91porn_video_info" + ESMediaTable = "91porn_media_info" + ESInfoTagTable = "91porn_tag_info" + ESInfoUserTable = "91porn_user_info" + ESInfoLouFengTable = "91porn_loufeng_info" + ESInfoFictionTable = "91porn_fiction_info" + ESInfoAudioBookTable = "91porn_audiobook" + + ESLogSearchLogTable = "91porn_search_log" + + ESStatKeywordTable = "91porn_keyword_stat" +) diff --git a/models/l/adsclicklogmod/type.go b/models/l/adsclicklogmod/type.go new file mode 100644 index 0000000..6f7f16a --- /dev/null +++ b/models/l/adsclicklogmod/type.go @@ -0,0 +1,74 @@ +package adsclicklogmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.AdsClickLog + +func Init() { + mdb = db.Init(table) + initIndex() +} + +// InitIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "adsType", Value: 1}, {Key: "createdAt", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +type AdsClick struct { + ID primitive.ObjectID `json:"-" bson:"_id,omitempty"` + UID uint64 `json:"uid" bson:"uid"` + ClickId primitive.ObjectID `json:"clickId" bson:"clickId"` + ObjType string `json:"objType" bson:"objType"` + AdsType int64 `json:"adsType" bson:"adsType"` // 0 默认; 1 金主楼凤广告 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +func AddClick(uid uint64, clickId primitive.ObjectID, objType string, adsType int64) error { + now := time.Now() + _, err := coll(nil).InsertOne(AdsClick{ + UID: uid, + ClickId: clickId, + ObjType: objType, + AdsType: adsType, + CreatedAt: now, + UpdatedAt: now, + }) + return err +} + +func GetDailyClickCount(uid uint64) (uint64, error) { + year, month, day := time.Now().Date() + start := time.Date(year, month, day, 0, 0, 0, 0, time.Local) + end := time.Date(year, month, day+1, 0, 0, 0, 0, time.Local) + res, err := coll(nil).Distinct("clickId", bson.M{"uid": uid, "adsType": 1, "createdAt": bson.M{"$gte": start, "$lt": end}}) + return uint64(len(res)), err +} diff --git a/models/l/exchlogmod/exchangecode.go b/models/l/exchlogmod/exchangecode.go new file mode 100644 index 0000000..2b9dd76 --- /dev/null +++ b/models/l/exchlogmod/exchangecode.go @@ -0,0 +1,121 @@ +package exchlogmod + +import ( + "fmt" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.ExchLog + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "code", Value: 1}, {Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "userID", Value: 1}, {Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "userID", Value: 1}, {Key: "batchNum", Value: 1}}, + }, + { + Keys: bson.D{{Key: "channel", Value: 1}, {Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "authority", Value: 1}, {Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func Insert(doc ExchangeLog) error { + if _, err := coll(nil).InsertOne(doc); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err), + log.Any("doc", doc), + ) + return err + } + return nil +} + +func GetExchangeLogList(filterParams FilterDoc, page commod.Page) (total int64, data []ExchangeLog, err error) { + filter, _ := common.ToBsonM(filterParams) + var skip = int64((page.PageNumber - 1) * page.PageSize) + var limit = int64(page.PageSize) + var opts = options.Find() + opts.SetSort(bson.D{{Key: "createdAt", Value: -1}}).SetSkip(skip).SetLimit(limit) + if err = coll(nil).Find(&data, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetExchangeLogList", table, "Find", err), + log.Any("filterParams", filterParams), + log.Any("page", page), + ) + return + } + if total, err = coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetExchangeLogList", table, "Count", err), + log.Any("filterParams", filterParams), + log.Any("page", page), + ) + return + } + return +} + +// 获取用户兑换日志 +func GetLogByUIDAndBatchNum(uid uint64, batchNum string) (data ExchangeLog, err error) { + if err = coll(nil).FindOne(&data, bson.M{"userID": uid, "batchNum": batchNum}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetExchangeLogList", table, "Find", err), + log.Any("uid", uid), + log.Any("batchNum", batchNum), + ) + return + } + return +} + +// 获取用户兑换日志 +func GetLogByUIDAndAuthority(uid uint64, authority string) (data ExchangeLog, err error) { + if err = coll(nil).FindOne(&data, bson.M{"userID": uid, "authority": authority}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetExchangeLogList", table, "Find", err), + log.Any("uid", uid), + log.Any("authority", authority), + ) + return + } + return +} + +// 获取用户兑换 +func GetLogByUIDAndCode(uid uint64, code string) (data ExchangeLog, err error) { + if err = coll(nil).FindOne(&data, bson.M{"userID": uid, "code": code}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetLogByUIDAndCode", table, "Find", err), + log.Any("uid", uid), + log.Any("code", code), + ) + return + } + return +} diff --git a/models/l/exchlogmod/type.go b/models/l/exchlogmod/type.go new file mode 100644 index 0000000..49714f1 --- /dev/null +++ b/models/l/exchlogmod/type.go @@ -0,0 +1,37 @@ +package exchlogmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +type ExchangeLog struct { + ID ObjectID `json:"id" bson:"_id,omitempty"` + Code string `json:"code" bson:"code"` // 兑换码 + UserID uint64 `json:"userID" bson:"userID"` // 兑换者 + BatchNum string `json:"batchNum" bson:"batchNum"` // 批次号 + Channel string `json:"channel" bson:"channel"` // 所属渠道 + Authority string `json:"authority" bson:"authority"` // 兑换权限 + Desc string `json:"desc" bson:"-"` // 说明 + Reward int `json:"reward" bson:"reward"` // 奖励值 + RewardCount int `json:"rewardCount" bson:"rewardCount"` // 奖励量 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间/兑换时间 +} + +type FilterDoc struct { + Code *string `bson:"code,omitempty"` // 兑换码 + UserID *uint64 `bson:"userID,omitempty"` // 兑换者 + Channel *string `bson:"channel,omitempty"` // 所属渠道 + Authority *string `bson:"authority,omitempty"` // 兑换权限 +} diff --git a/models/l/exchlogmod/web.go b/models/l/exchlogmod/web.go new file mode 100644 index 0000000..a048ede --- /dev/null +++ b/models/l/exchlogmod/web.go @@ -0,0 +1,25 @@ +package exchlogmod + +import ( + "time" + + "91porn-server/models/commod" +) + +type ListReqParam struct { + Code *string `form:"code" json:"code"` // 兑换码 + UserID *uint64 `form:"userID" json:"userID"` // 兑换者 + Channel *string `form:"channel" json:"channel"` // 所属渠道 + Authority *string `form:"authority" json:"authority"` // 兑换权限 + Page commod.Page +} + +type ListResp struct { + Code string `json:"code"` // 兑换码 + UserID uint64 `json:"userID"` // 兑换者 + Channel string `json:"channel"` // 所属渠道 + Authority string `json:"authority"` // 兑换权限 + Reward int `json:"reward"` // 奖励值 + RewardCount int `json:"rewardCount"` // 奖励量 + CreatedAt time.Time `json:"createdAt"` // 创建时间/兑换时间 +} diff --git a/models/l/loginlgmod/login.go b/models/l/loginlgmod/login.go new file mode 100644 index 0000000..51e4b6a --- /dev/null +++ b/models/l/loginlgmod/login.go @@ -0,0 +1,127 @@ +package loginlgmod + +import ( + "fmt" + "math" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.LoginLog + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "devID", Value: 1}}, + }, + { + Keys: bson.D{{Key: "loginTime", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func InsertLoginLog(l *LoginLog) error { + if _, err := coll(nil).InsertOne(l); err != nil { + log.Error("loginlog insert err", log.E(err)) + return err + } + return nil +} + +func WebGetDayActivity(uids []uint64, start time.Time, end time.Time) (data []LoginLog, err error) { + if err = coll(nil).Find( + &data, + bson.M{ + "loginTime": bson.M{ + "$and": []bson.M{ + bson.M{"$gte": start}, + bson.M{"$lte": end}, + }, + }, + "uid": bson.M{ + "$in": uids, + }, + }, + ); err != nil { + log.ZapLog.Error(fmt.Sprintf("model record login WebGetDayActivity fail error:%+v:", err)) + return + } + return +} + +// GetTotalCnt 获取查询总数总数 +func getLoginLogTotalCnt(cond bson.M) (int64, error) { + total, err := coll(nil).Count(cond) + if err != nil { + e := fmt.Sprintf("get total Cnt error: %+v\n", err) + log.ZapLog.Error(e) + return 0, err + } + return total, nil +} + +// GetSkipSize 计算跳转 +func getLoginLogSkipSize(page int, size int, cond bson.M) (int, int, int64, error) { + total, err := getLoginLogTotalCnt(cond) + if err != nil { + return 0, 0, 0, err + } + totalpages := int(math.Ceil(float64(total) / float64(size))) + if page > totalpages { + page = totalpages + } + if page < 1 { + page = 1 + } + return (page - 1) * size, totalpages, total, nil +} + +// FindLoginLog 查找登陆日志 +func FindLoginLogList(page int, size int, cond bson.M, sort bson.D) ([]*LoginLog, int, int64, error) { + skip, totalPages, total, err := getLoginLogSkipSize(page, size, cond) + if err != nil { + return nil, 0, 0, err + } + var back []*LoginLog + if err = coll(nil).Aggregate( + &back, + []bson.M{ + bson.M{"$match": cond}, + bson.M{"$sort": sort}, + bson.M{"$skip": skip}, + bson.M{"$limit": size}, + }, + ); err != nil { + e := fmt.Sprintf("get login logs error: %+v\n", err) + log.ZapLog.Error(e) + return nil, totalPages, total, err + } + return back, totalPages, total, nil +} + +func DeleteBeforeLoginTime(t *db.MongoTool, tm time.Time) error { + _, err := coll(t).DeleteMany(bson.M{"loginTime": bson.M{"$lt": tm}}) + return err +} diff --git a/models/l/loginlgmod/type.go b/models/l/loginlgmod/type.go new file mode 100644 index 0000000..02bf05d --- /dev/null +++ b/models/l/loginlgmod/type.go @@ -0,0 +1,43 @@ +package loginlgmod + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models/commod" +) + +// LoginLog 登陆记录 +type LoginLog struct { + UID uint64 `json:"uid" bson:"uid"` + IP string `json:"ip" bson:"ip"` + SysType string `json:"sysType" bson:"sysType"` //操作系统类型 安卓 IOS + Ver string `json:"ver" bson:"ver"` //APP版本号 + DevType string `json:"devType" bson:"devType"` //设备型号 + DevID string `json:"devID" bson:"devID"` + BuildID string `json:"buildID" bson:"buildID"` //app构建ID(包ID) + LoginTime time.Time `json:"loginTime" bson:"loginTime,omitempty"` + LogoutTime time.Time `json:"logoutTime" bson:"logoutTime,omitempty"` + Logout bool `json:"logout" bson:"logout"` //是否退出登陆 +} + +// LogListResp 登陆日志查询列表 +type LogListResp struct { + Logs []*LoginLog `json:"logs"` + Total int64 `json:"total"` +} + +// QueryReq 登陆日志查询请求参数 +type QueryReq struct { + commod.Page + UID uint64 `form:"uid" json:"uid"` + Start time.Time `form:"start" json:"start"` + End time.Time `form:"end" json:"end"` + Sort string `form:"sort" json:"sort"` + Desc int `form:"desc" json:"desc"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/l/lotterylgmod/lottery.go b/models/l/lotterylgmod/lottery.go new file mode 100644 index 0000000..e07c61b --- /dev/null +++ b/models/l/lotterylgmod/lottery.go @@ -0,0 +1,108 @@ +package lotterylgmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.LotteryLog + +var mdb *db.MongoDB + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndexAds 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "code", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// InsertLog 记录一个抽奖号行为 +func InsertLog(uid uint64, code int, gateName string) error { + ll := LotteryLog{ + UID: uid, + Code: code, + GateName: gateName, + CreatedAt: time.Now(), + } + if _, err := coll(nil).InsertOne(ll); err != nil { + log.Error("InsertClickLog error", log.Any("ll", ll), log.E(err)) + return err + } + return nil +} + +// UserTodayChances 获取用户当日抽奖号数量 +func UserTodayChances(uid uint64) (int, error) { + sumDate := timerange.LocDayRange(time.Now()).Head + ll := []LotteryLog{} + cond := bson.M{"uid": uid, "createdAt": bson.M{"$gte": sumDate}} + if err := coll(nil).Find(&ll, cond); err != nil { + log.Error("UserTodayChances error", log.Any("uid", uid), log.E(err)) + return 0, err + } + return len(ll), nil +} + +// LottnumList 获取摇号列表 +func LottnumList(page, size uint64, start *time.Time, end *time.Time, uid *uint64, code *int) ([]LotteryLog, int64, error) { + back := []LotteryLog{} + cond := bson.M{} + if uid != nil { + cond["uid"] = *uid + } + if code != nil { + cond["code"] = *code + } + if start != nil && end != nil { + cond["createdAt"] = bson.M{"$gte": *start, "$lt": *end} + } + sort := bson.D{{Key: "createdAt", Value: -1}} + skip := (page - 1) * size + opts := options.FindOptions{} + opts.SetSort(sort).SetSkip(int64(skip)).SetLimit(int64(size)) + if err := coll(nil).Find(&back, bson.M(cond), &opts); err != nil { + log.Error("LottnumList error", log.Any("page", page), log.Any("size", size), log.Any("cond", cond), log.E(err)) + return back, 0, err + } + total, _ := coll(nil).Count(cond) + return back, total, nil +} + +// UserTodayNum 获取用户当日抽奖号 +func UserTodayNum(uid uint64) ([]LotteryLog, error) { + sumDate := timerange.LocDayRange(time.Now()).Head + ll := []LotteryLog{} + cond := bson.M{"uid": uid, "createdAt": bson.M{"$gte": sumDate}} + if err := coll(nil).Find(&ll, cond); err != nil { + log.Error("UserTodayChances error", log.Any("uid", uid), log.E(err)) + return ll, err + } + return ll, nil +} diff --git a/models/l/lotterylgmod/type.go b/models/l/lotterylgmod/type.go new file mode 100644 index 0000000..5e347ad --- /dev/null +++ b/models/l/lotterylgmod/type.go @@ -0,0 +1,20 @@ +package lotterylgmod + +import ( + "time" + + "91porn-server/common/db" +) + +// LotteryLog 活动摇号日志 +type LotteryLog struct { + UID uint64 `json:"uid" bson:"uid"` + Code int `json:"code" bson:"code"` + GateName string `json:"gateName" bson:"gateName"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/l/lotterylgmod/web.go b/models/l/lotterylgmod/web.go new file mode 100644 index 0000000..e8e8af2 --- /dev/null +++ b/models/l/lotterylgmod/web.go @@ -0,0 +1,50 @@ +package lotterylgmod + +import ( + "time" + + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// AdsClickResp 广告点击日志应答 +type AdsClickResp struct { + Logs int `json:"logs"` + Total int64 `json:"total"` +} + +// AdsCountResp 广告点击统计应答 +type AdsCountResp struct { + ID string `json:"id"` + TodayCnt int64 `json:"todayCnt"` + YesterdayCnt int64 `json:"yesterdayCnt"` + WeekCnt int64 `json:"weekCnt"` + MonthCnt int64 `json:"monthCnt"` + TotalCnt int64 `json:"totalCnt"` +} + +type AdsClickReq struct { + UID uint64 `form:"uid" json:"uid"` + Start time.Time `form:"start" json:"start"` + End time.Time `form:"end" json:"end"` + Sort string `form:"sort" json:"sort"` + Desc int `form:"desc" json:"desc"` + commod.Page +} + +// AdsClickInfo +type AdsClickInfo struct { + AID primitive.ObjectID `json:"aid"` //广告id + TodayCount int64 `json:"todayCount"` //今日点击数 + YesterdayCount int64 `json:"yesterdayCount"` //昨日点击数 + WeekCount int64 `json:"weekCount"` //本周点击数 + MonthCount int64 `json:"monthCount"` //本月点击数 + TotalCount int64 `json:"totalCount"` //总点击数 +} + +// AdsStatisticResp 广告点击统计应答 +type AdsStatisticResp struct { + Logs []AdsClickInfo `json:"logs"` + Total int64 `json:"total"` +} diff --git a/models/l/operatorlgmod/operator.go b/models/l/operatorlgmod/operator.go new file mode 100644 index 0000000..6691b95 --- /dev/null +++ b/models/l/operatorlgmod/operator.go @@ -0,0 +1,148 @@ +package operatorlgmod + +import ( + "fmt" + "math" + "strings" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.WebOperatorLog + +var mdb *db.MongoDB + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndexAds 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "manager", Value: 1}}, + }, + { + Keys: bson.D{{Key: "uri", Value: 1}}, + }, + { + Keys: bson.D{{Key: "position", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// RecordOperation 记录操作日志 +func RecordOperation(manager string, position string, operator string, content string, uri string) error { + record := WebOperatorLog{ + Manager: manager, + Position: position, + Operator: operator, + Content: content, + URI: uri, + CreatedAt: time.Now(), + } + if _, err := coll(nil).InsertOne(&record); err != nil { + log.Error("RecordOperation error", log.Any("manager", manager), log.Any("position", position), + log.Any("operator", operator), log.Any("content", content), log.Any("url", uri), log.E(err)) + return err + } + return nil +} + +func backOperatorParam(param OperatorListReq) map[string]interface{} { + m := make(map[string]interface{}) + if len(param.Manager) != 0 { + m["manager"] = param.Manager + } + if len(param.Position) != 0 { + m["position"] = param.Position + } + if len(param.URI) != 0 { + m["uri"] = param.URI + } + if param.Content != "" { + param.Content = strings.Trim(param.Content, " ") + m["content"] = bson.M{"$regex": primitive.Regex{ + Pattern: param.Content, + Options: "i", + }} + } + if !param.End.IsZero() { + i := make(map[string]time.Time) + i["$gte"] = param.Start + i["$lt"] = param.End + m["createdAt"] = i + } + return m +} + +// getTotalCnt 获取后台操作日志总数 +func getTotalCnt(cond bson.M) (int64, error) { + total, err := coll(nil).Count(cond) + if err != nil { + log.Error("getTotalCnt", log.Any("cond", cond), log.E(err)) + return 0, err + } + return total, nil +} + +// getSkipSize 计算跳转 +func getSkipSize(page, size uint64, cond bson.M) (uint64, uint64, int64, error) { + total, err := getTotalCnt(cond) + if err != nil { + return 0, 0, 0, err + } + totalpages := uint64(math.Ceil(float64(total) / float64(size))) + if page > totalpages { + page = totalpages + } + if page < 1 { + page = 1 + } + return (page - 1) * size, totalpages, total, nil +} + +// GetOperatorLog 获取操作日志 +func GetOperatorLog(req OperatorListReq) ([]*WebOperatorLog, int64, error) { + cond := backOperatorParam(req) + sort := bson.D{{Key: "createdAt", Value: -1}} + skip, _, total, err := getSkipSize(req.PageNumber, req.PageSize, bson.M(cond)) + if err != nil { + return nil, 0, err + } + opts := options.FindOptions{} + opts.SetSort(sort).SetSkip(int64(skip)).SetLimit(int64(req.PageSize)) + var back []*WebOperatorLog + if err = coll(nil).Find(&back, bson.M(cond), &opts); err != nil { + log.Error("GetOperatorLog error", log.Any("req", req), log.E(err)) + } + return back, total, err +} + +// EditRemarks 更新备注 +func EditRemarks(id primitive.ObjectID, remarks string) error { + cond := bson.M{"_id": id} + update := bson.M{"$set": bson.M{"remarks": remarks}} + if _, err := coll(nil).UpdateOne(cond, update); err != nil { + log.Error("EditRemarks error", log.Any("id", id), log.Any("remarks", remarks), log.E(err)) + return err + } + return nil +} diff --git a/models/l/operatorlgmod/type.go b/models/l/operatorlgmod/type.go new file mode 100644 index 0000000..6797b27 --- /dev/null +++ b/models/l/operatorlgmod/type.go @@ -0,0 +1,27 @@ +package operatorlgmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// WebOperatorLog 后台操作日志 +type WebOperatorLog struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Manager string `json:"manager" bson:"manager"` //管理员账号 + Position string `json:"position" bson:"position"` //操作位置 + Operator string `json:"operator" bson:"operator"` //操作动作 + Content string `json:"content" bson:"content"` //操作内容 + URI string `json:"uri" bson:"uri"` //操作uri + Remarks string `json:"remarks" bson:"remarks"` //备注 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/l/operatorlgmod/web.go b/models/l/operatorlgmod/web.go new file mode 100644 index 0000000..c6ded63 --- /dev/null +++ b/models/l/operatorlgmod/web.go @@ -0,0 +1,32 @@ +package operatorlgmod + +import ( + "time" + + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// OperatorListReq 操作日志查询 +type OperatorListReq struct { + Manager string `form:"manager" json:"manager"` + Position string `form:"position" json:"position"` + Content string `form:"content" json:"content" binding:"omitempty,min=2,max=100"` + URI string `form:"uri" json:"uri"` + Start time.Time `form:"start" json:"start"` + End time.Time `form:"end" json:"end"` + commod.Page +} + +// OperatorListResp 操作日志查询应答 +type OperatorListResp struct { + Infos []*WebOperatorLog `json:"infos"` + Total int64 `json:"total"` +} + +// OperatorModifyReq 操作日志修改 +type OperatorModifyReq struct { + ID primitive.ObjectID `json:"id"` + Remarks string `json:"remarks"` +} diff --git a/models/l/payvidlgmod/app.go b/models/l/payvidlgmod/app.go new file mode 100644 index 0000000..e36fe94 --- /dev/null +++ b/models/l/payvidlgmod/app.go @@ -0,0 +1,6 @@ +package payvidlgmod + +// Pay4VidLogQueryReq Pay4VidLogQueryReq +type Pay4VidLogQueryReq struct { + PublisherID *uint64 `json:"publisherID,omitempty" bson:"publisherID"` //上传者ID +} diff --git a/models/l/payvidlgmod/discStat.go b/models/l/payvidlgmod/discStat.go new file mode 100644 index 0000000..8aeea9a --- /dev/null +++ b/models/l/payvidlgmod/discStat.go @@ -0,0 +1,114 @@ +package payvidlgmod + +import ( + "fmt" + "strings" + "time" + + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/common/ysinterface/disc" + + "go.mongodb.org/mongo-driver/mongo/options" +) + +type DistrictStatKey struct { + DiscSeqe `bson:",inline"` //商区码 + SysType string `bson:"sysType"` //系统类型 iOS Android +} + +func (d DistrictStatKey) GetDiscCode() string { + return d.DistrictCode +} + +func (d DistrictStatKey) GetPromSeqe() string { + return d.PromSeqe +} + +func (d DistrictStatKey) GetSysType() string { + return d.SysType +} + +func (d DistrictStatKey) String() string { + if d.DiscSeqe.String() == "" { + return "" + } + return strings.ToUpper(strings.Join([]string{d.DiscSeqe.String(), d.SysType}, "-")) +} + +type DistrictStater = disc.DistrictStater + +// DiscSeqeTransCount +func DiscSeqeBuyVidCount(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + filter := pageopt.MergeM(mats) + list := []DistrictStatKey{} + opt := (&options.FindOptions{}).SetProjection(M{ + "districtCode": 1, + "promSeqe": 1, + "sysType": 1, + }) + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DiscSeqeTransCount", table, "Find", err)) + return nil, err + } + m := make(map[DistrictStater]int64) + for _, v := range list { + if v.String() != "" { + m[v] += 1 + } + } + return m, nil +} + +// DiscSeqeBuyVidCoins +func DiscSeqeBuyVidCoins(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + filter := pageopt.MergeM(mats) + list := []struct { + DistrictStatKey `bson:",inline"` //商区码 + Coins int64 `bson:"coins"` + }{} + opt := (&options.FindOptions{}).SetProjection(M{ + "districtCode": 1, + "promSeqe": 1, + "sysType": 1, + "coins": 1, + }) + if err := coll(nil).Find(&list, filter, opt); err != nil { + return nil, err + } + m := make(map[DistrictStater]int64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v.DistrictStatKey] += v.Coins + } + } + return m, nil +} + +// DiscSeqeTaxAmount +func DiscSeqeTaxAmount(start, end time.Time, mats ...Matcher) (map[DistrictStater]float64, error) { + mats = append(mats, (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + filter := pageopt.MergeM(mats) + list := []struct { + DistrictStatKey `bson:",inline"` //商区码 + TaxAmount float64 `bson:"taxAmount"` //税额 + }{} + opt := (&options.FindOptions{}).SetProjection(M{ + "districtCode": 1, + "promSeqe": 1, + "sysType": 1, + "taxAmount": 1, + }) + if err := coll(nil).Find(&list, filter, opt); err != nil { + return nil, err + } + m := make(map[DistrictStater]float64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v.DistrictStatKey] += v.TaxAmount + } + } + return m, nil +} diff --git a/models/l/payvidlgmod/list.go b/models/l/payvidlgmod/list.go new file mode 100644 index 0000000..13a3c19 --- /dev/null +++ b/models/l/payvidlgmod/list.go @@ -0,0 +1,84 @@ +package payvidlgmod + +import ( + "91porn-server/common/log" + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Matcher = pageopt.Matcher + +// DistrictCodeMatch +type DistrictCodeMatch struct { + DistrictCode *string +} + +func (s *DistrictCodeMatch) New() Matcher { + return pageopt.NewAssignMatch("districtCode", s.DistrictCode) +} + +// IsDirectMatch +type IsDirectMatch struct { + IsDirect *bool +} + +func (b *IsDirectMatch) New() Matcher { + return pageopt.NewAssignMatch("isDirect", b.IsDirect) +} + +// PromSeqeMatch +type PromSeqeMatch struct { + Seqe *string +} + +func (d *PromSeqeMatch) New() Matcher { + return pageopt.NewAssignMatch("promSeqe", d.Seqe) +} + +// SysTypeMatch +type SysTypeMatch struct { + SysType *string +} + +func (d *SysTypeMatch) New() Matcher { + return pageopt.NewAssignMatch("sysType", d.SysType) +} + +// CreatedAtGTEAndLTMatch +type CreatedAtGTEAndLTMatch = pageopt.CreatedAtGTEAndLTMatch + +type Sort = bson.D + +var Sort_CreatedAt_n1 = Sort{{Key: "createdAt", Value: -1}} + +func List(sort Sort, skip, limit *int64, matchers ...Matcher) ([]Pay4VidLog, error) { + filter := pageopt.MergeM(matchers) + opt := (&options.FindOptions{}) + if len(sort) != 0 { + opt.SetSort(sort) + } + if skip != nil { + opt.SetSkip(*skip) + } + if limit != nil { + opt.SetLimit(*limit) + } + list := []Pay4VidLog{} + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.Error("payvidlgmod List error", log.E(err)) + return nil, err + } + return list, nil +} + +func Count(matchers ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchers) + count, err := coll(nil).Count(filter) + if err != nil { + log.Error("payvidlgmod Count error", log.E(err)) + return 0, err + } + return count, nil +} diff --git a/models/l/payvidlgmod/pay.go b/models/l/payvidlgmod/pay.go new file mode 100644 index 0000000..a822480 --- /dev/null +++ b/models/l/payvidlgmod/pay.go @@ -0,0 +1,602 @@ +package payvidlgmod + +import ( + "91porn-server/models/commod" + "fmt" + "math" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/common/timeutil" + "91porn-server/models" + + "github.com/pkg/errors" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.PayVideoLog + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "videoID", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "uniq", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "deductType", Value: 1}, {Key: "createdAt", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{{Key: "videoID", Value: 1}}, + }, + { + Keys: bson.D{{Key: "publisherID", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InsertVideoPayRecord 插入一条购买记录 +func InsertVideoPayRecord(t *db.MongoTool, p Pay4VidLog) error { + p.Uniq = Unique(p.UID, p.VideoID) + p.CreatedAt = time.Now() + res, err := coll(t).InsertOne(&p) + if err != nil { + log.Error("InsertVideoPayRecord error", log.Any("p", p), log.E(err)) + return err + } + p.ID = res.InsertedID.(ObjectID) + return nil +} + +// IsPay4Video 用户是否购买了视频 +func IsPay4Video(uid uint64, videoID ObjectID) (bool, error) { + cnt, err := coll(nil).Count(bson.M{"uid": uid, "videoID": videoID}) + if err != nil { + log.Error("IsPay4Video error", log.Any("uid", uid), log.Any("videoID", videoID), log.E(err)) + return false, err + } + return cnt != 0, nil +} + +func FindManyPay4VidLogByUID(uid uint64, newsType string) ([]*Pay4VidLog, error) { + vl := make([]*Pay4VidLog, 0) + err := coll(nil).Find(&vl, bson.M{"uid": uid, "newsType": newsType}) + if err != nil { + log.Error("FindManyPay4VidLogByUID error", log.Any("uid", uid), log.Any("err", err.Error())) + return nil, err + } + return vl, nil +} + +// IsPay4Videos 用户是否购买了视频 +func IsPay4Videos(uid uint64, videoIDs []ObjectID) (map[ObjectID]bool, error) { + if videoIDs == nil { + videoIDs = []ObjectID{} + } + m := make(map[ObjectID]bool) + var infos []Pay4VidLog + query := bson.M{"uid": uid, "videoID": bson.M{"$in": videoIDs}} + if err := coll(nil).Find(&infos, query); err != nil { + log.Error("IsPay4Videos error", log.Any("uid", uid), log.Any("videoIDs", videoIDs), log.E(err)) + return m, err + } + for _, i := range infos { + m[i.VideoID] = true + } + return m, nil +} + +// 判断当前视频是否有人购买过 +func IsVidIfBePay(id primitive.ObjectID) bool { + var pay *Pay4VidLog + if err := coll(nil).FindOne(&pay, bson.M{"videoID": id}); err != nil { + log.Error("IsVidIfBePay error", log.Any("videoID", id), log.E(err)) + return false + } + return pay != nil +} + +// DelVideoPayRecord 删除一条购买记录 +func DelVideoPayRecord(videoID primitive.ObjectID, uid uint64) error { + if _, err := coll(nil).DeleteOne(bson.M{"videoID": videoID, "uid": uid}); err != nil { + log.Error("DelVideoPayRecord DeleteOne error", log.Any("videoID", videoID), log.Any("uid", uid)) + return err + } + return nil +} + +// 从给定的hash列表中获取购买状态映射:uniq->Statue +// uniq通过Unique()获取 +func PayStatueMap(uniqList []string) (map[string]bool, error) { + filter := bson.M{ + "uniq": bson.M{"$in": uniqList}, + } + payLogList := make([]Pay4VidLog, 0, len(uniqList)) + if err := coll(nil).Find(&payLogList, filter); err != nil { + return nil, err + } + m := make(map[string]bool, len(payLogList)) + //初始化 + for _, uniq := range uniqList { + m[uniq] = false + } + //已经收藏的 + for _, v := range payLogList { + uniq := v.Uniq + m[uniq] = true + } + return m, nil +} + +func GetUIDListByPayTime(start time.Time, end time.Time) ([]uint64, error) { + filter := bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + list := make([]struct { + UID uint64 `bson:"uid"` + }, 0) + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + uidList := make([]uint64, len(list)) + for i, v := range list { + uidList[i] = v.UID + } + return uidList, nil +} + +func GetPayMoneyByTime(start time.Time, end time.Time) (int64, error) { + filter := bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + logs := make([]Pay4VidLog, 0) + if err := coll(nil).Find(&logs, filter); err != nil { + return 0, fmt.Errorf("table:%s GetPayMoneyByTime err: %s", table, err.Error()) + } + total := int64(0) + for _, v := range logs { + total += v.PayMoney + } + return total, nil +} + +// FindManyVideoPayRecord 查询所有 +func FindThreeMonthVideoPayRecord(publisherID uint64, pageNumber, pageSize uint64) (total int64, totalAmount int64, data []*Pay4VidLog, hasNext bool, err error) { + data = make([]*Pay4VidLog, 0) + startTime := timeutil.NearMonth(time.Now(), 2) + f := bson.M{"publisherID": publisherID, "createdAt": bson.M{"$gte": startTime}} + skip := int64(pageSize * (pageNumber - 1)) + limit := int64(pageSize + 1) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Error("FindManyVideoPayRecord error", log.Any("filter", f), log.E(err)) + } + type Res struct { + TotalAmount float64 `json:"totalAmount" bson:"totalAmount"` + } + totalIncome := []bson.M{ + {"$match": f}, + { + "$group": bson.M{ + "_id": nil, + "totalAmount": bson.M{ + "$sum": "$publisherIncome", + }, + }, + }, + } + ress := make([]Res, 0) + if err = coll(nil).Aggregate(&ress, totalIncome); err != nil { + log.Error("FindManyVideoPayRecord error", log.Any("filter", f), log.E(err)) + } + if len(ress) > 0 { + totalAmount = int64(math.Floor(ress[0].TotalAmount)) + } + total, err = coll(nil).Count(f) + if err != nil { + log.Error("FindManyVideoPayRecord Count error", log.Any("filter", f), log.E(err)) + } + if len(data) > int(pageSize) { + hasNext = true + data = data[:pageSize] + } + return +} + +// GetWorksIncomeList 查询收益列表 +func GetWorksIncomeList(publisherID uint64, page commod.Page) (data []*Pay4VidLog, hasNext bool, err error) { + data = make([]*Pay4VidLog, 0) + opt := options.Find(). + SetLimit(page.Limit64() + 1). + SetSkip(page.Skip64()). + SetSort(bson.D{{"createdAt", -1}}) + + filter := bson.M{ + "publisherID": publisherID, + } + + if err = coll(nil).Find(&data, filter, opt); err != nil { + log.Error("FindManyVideoPayRecord error", log.Any("filter", filter), log.E(err)) + return nil, false, err + } + hasNext = len(data) > int(page.Limit64()) + if hasNext { + data = data[:page.Limit64()] + } + return +} + +// PayCoinByCreatedTime 金币 +func PayCoinByCreatedTime(start time.Time, end time.Time) (int64, error) { + pipeline := []bson.M{ + { + "$match": bson.M{ + // createdAt ∈ [startTime, endTime) + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + }, + }, + { + "$group": bson.M{ + "_id": nil, + "totalAmount": bson.M{"$sum": "$payMoney"}, + }, + }, + } + var ret struct { + TotalAmount int64 `bson:"totalAmount"` + } + if err := coll(nil).AggregateDecode(&ret, pipeline); err != nil { + return 0, fmt.Errorf("table:%s PayCoinByCreatedTime err: %s", table, err.Error()) + } + return ret.TotalAmount, nil +} + +// PayCoinMapByTime vid->金币 map +func PayCoinMapByTime(start time.Time, end time.Time) (map[ObjectID]int64, error) { + pipeline := []bson.M{ + { + "$match": bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + }, + }, + { + "$group": bson.M{ + "_id": "$videoID", + "income": bson.M{ + "$sum": "$payMoney", + }, + }, + }, + } + var docList []struct { + VID ObjectID `bson:"_id"` + Income int64 `bson:"income"` + } + if err := coll(nil).Aggregate(&docList, pipeline); err != nil { + return nil, fmt.Errorf("table:%s PayCoinMapByTime err: %s", table, err.Error()) + } + ret := make(map[ObjectID]int64) + for _, doc := range docList { + ret[doc.VID] = doc.Income + } + return ret, nil +} + +func VideoCoinIncomeGross(start, end time.Time) (int64, error) { + matchStage := bson.M{ + "$match": bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + }, + } + groupStage := bson.M{ + "$group": bson.M{ + "_id": nil, + "gross": bson.M{ + "$sum": "$payMoney", + }, + }, + } + var gross struct { + Gross int64 `bson:"gross"` + } + pipeline := []bson.M{matchStage, groupStage} + // opts := options.Aggregate() + if err := coll(nil).AggregateDecode(&gross, pipeline); err != nil { + log.Error("VideoCoinIncomeGross err: " + err.Error()) + return 0, errors.Wrapf(err, "table:%s VideoCoinIncomeGross", table) + } + return gross.Gross, nil +} + +// VideoCoinIncomeAggregateByVideoIDs 根据视频ID统计视频的售卖总数以及总金币数 +func VideoCoinIncomeAggregateByVideoIDs(ids []primitive.ObjectID) (list []VideoCoinIncome, err error) { + if len(ids) == 0 { + return + } + matchStage := bson.M{ + "$match": bson.M{ + "videoID": bson.M{"$in": ids}, + }, + } + groupStage := bson.M{ + "$group": bson.M{ + "_id": "$videoID", + "total": bson.M{ + "$sum": "$payMoney", + }, + "count": bson.M{ + "$sum": 1, + }, + }, + } + pipeline := []bson.M{matchStage, groupStage} + opts := options.Aggregate().SetMaxTime(10 * time.Second) + if err := coll(nil).Aggregate(&list, pipeline, opts); err != nil { + return nil, errors.Wrapf(err, "table:%s VideoCoinIncomeAggregateByVideoIDs", table) + } + return +} + +// VideoCoinIncomeAggregate 聚合统计在给定时间段内视频收入排名前N的视频 +func VideoCoinIncomeAggregateTopN(topN int, start, end time.Time) (list []VideoCoinIncome, err error) { + matchStage := bson.M{ + "$match": bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + }, + } + groupStage := bson.M{ + "$group": bson.M{ + "_id": "$videoID", + "total": bson.M{ + "$sum": "$payMoney", + }, + "count": bson.M{ + "$sum": 1, + }, + }, + } + sortStage := bson.M{ + "$sort": bson.M{ + "total": -1, + "count": -1, + }, + } + limitStage := bson.M{ + "$limit": topN, + } + pipeline := []bson.M{matchStage, groupStage, sortStage, limitStage} + opts := options.Aggregate().SetMaxTime(10 * time.Second) + if err := coll(nil).Aggregate(&list, pipeline, opts); err != nil { + return nil, errors.Wrapf(err, "table:%s VideoCoinIncomeAggregateTopN", table) + } + return +} + +// PayCountMapByTime vid->金币 map +func PayCountMapByTime(start time.Time, end time.Time) (map[ObjectID]int64, error) { + pipeline := []bson.M{ + { + "$match": bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + }, + }, + { + "$group": bson.M{ + "_id": "$videoID", + "count": bson.M{ + "$sum": 1, + }, + }, + }, + } + var docList []struct { + VID ObjectID `bson:"_id"` + Count int64 `bson:"count"` + } + if err := coll(nil).Aggregate(&docList, pipeline); err != nil { + return nil, fmt.Errorf("table:%s PayCoinMapByTime err: %s", table, err.Error()) + } + ret := make(map[ObjectID]int64) + for _, doc := range docList { + ret[doc.VID] = doc.Count + } + return ret, nil +} + +// TaxAmountMapByTime vid->TaxAmount map +func TaxAmountMapByTime(start time.Time, end time.Time) (map[ObjectID]float64, error) { + pipeline := []bson.M{ + { + "$match": bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + }, + }, + { + "$group": bson.M{ + "_id": "$videoID", + "taxAmount": bson.M{ + "$sum": "$taxAmount", + }, + }, + }, + } + var docList []struct { + VID ObjectID `bson:"_id"` + TaxAmount float64 `bson:"taxAmount"` + } + if err := coll(nil).Aggregate(&docList, pipeline); err != nil { + return nil, fmt.Errorf("table:%s PayCoinMapByTime err: %s", table, err.Error()) + } + ret := make(map[ObjectID]float64) + for _, doc := range docList { + ret[doc.VID] += doc.TaxAmount + } + return ret, nil +} + +func PublisherIncomeMapByTime(start time.Time, end time.Time) (map[uint64]int64, error) { + pipeline := []bson.M{ + { + "$match": bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + }, + }, + { + "$group": bson.M{ + "_id": "$publisherID", + "income": bson.M{ + "$sum": "$payMoney", + }, + }, + }, + } + var docList []struct { + PublisherID uint64 `bson:"_id"` + Income int64 `bson:"income"` + } + if err := coll(nil).Aggregate(&docList, pipeline); err != nil { + return nil, fmt.Errorf("table:%s PublisherIncomeMapByTime err: %s", table, err.Error()) + } + ret := make(map[uint64]int64) + for _, doc := range docList { + ret[doc.PublisherID] = doc.Income + } + return ret, nil +} + +// TaxAmountByTime +func TaxAmountByTime(start, end time.Time, mats ...Matcher) (float64, error) { + mats = append(mats, (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + filter := pageopt.MergeM(mats) + var list []struct { + TaxAmount float64 `bson:"taxAmount"` //税额 + } + opt := (&options.FindOptions{}).SetProjection(M{ + "taxAmount": 1, + }) + if err := coll(nil).Find(&list, filter, opt); err != nil { + return 0, err + } + var taxAmount float64 + for _, v := range list { + taxAmount += v.TaxAmount + } + return taxAmount, nil +} + +func FindByUID(uid uint64, newsType string, pageNumber int64, pageSize int64) (data []*Pay4VidLog, hasNext bool, err error) { + data = make([]*Pay4VidLog, 0) + opt := options.Find(). + SetLimit(pageSize + 1). + SetSkip((pageNumber - 1) * pageSize). + SetSort(bson.D{{"createdAt", -1}}) + + filter := bson.M{ + "uid": uid, + "newsType": newsType, + } + err = coll(nil).Find(&data, filter, opt) + if err != nil { + log.Error("FindByUID error", log.Any("uid", uid), log.Any("err", err.Error())) + return nil, false, err + } + if len(data) > int(pageSize) { + hasNext = true + data = data[:pageSize] + } + return +} + +func PublisherVideoDeductionRange(uid uint64, start, end time.Time) ([]PublisherDeductionStat, error) { + pipeline := []bson.M{ + { + "$match": bson.M{ + // createdAt ∈ [startTime, endTime) + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + "publisherID": uid, + }, + }, + { + "$group": bson.M{ + "_id": "$isVideoDeduction", + "totalAmount": bson.M{"$sum": "$coins"}, + "videoCount": bson.M{"$sum": 1}, + }, + }, + } + docList := []PublisherDeductionStat{} + return docList, coll(nil).Aggregate(&docList, pipeline) +} + +func GetPayVidLogsByTime(start time.Time, end time.Time) ([]Pay4VidLogShort, error) { + filter := bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + logs := make([]Pay4VidLogShort, 0) + if err := coll(nil).Find(&logs, filter); err != nil { + return logs, fmt.Errorf("table:%s GetPayMoneyByTime err: %s", table, err.Error()) + } + return logs, nil +} diff --git a/models/l/payvidlgmod/type.go b/models/l/payvidlgmod/type.go new file mode 100644 index 0000000..a6b4475 --- /dev/null +++ b/models/l/payvidlgmod/type.go @@ -0,0 +1,78 @@ +package payvidlgmod + +import ( + "strconv" + "strings" + "time" + + "91porn-server/common/db" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ( + ObjectID = primitive.ObjectID + + M = bson.M + + DiscSeqe = commod.DiscSeqe + + DiscDoc = commod.DiscDoc +) + +// Pay4VidLog 购买影片记录 +type Pay4VidLog struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + UID uint64 `json:"uid" bson:"uid"` // 用户ID + Title string `json:"title" bson:"title,required"` // 视频标题 + NewsType string `json:"newsType" bson:"newsType"` // 类型 + VideoID primitive.ObjectID `json:"videoID" bson:"videoID"` // 视频id + PlayTime uint `json:"playTime" bson:"playTime"` // 影片长度 + Coins int64 `json:"coins" bson:"coins"` // 定价 + Tax int64 `json:"tax" bson:"tax"` // 税率 + TaxAmount float64 `json:"taxAmount" bson:"taxAmount"` // 系统收取的税额 税率*定价 + PayMoney int64 `json:"payMoney" bson:"payMoney"` // 金币 + PublisherIncome float64 `json:"publisherIncome" bson:"publisherIncome"` // 上传者实际收益 定价-系统收取的税额 + PublisherID uint64 `json:"publisherID" bson:"publisherID"` // 上传者ID + Uniq string `json:"uniq" bson:"uniq"` // UID.VideoID + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + SysType string `json:"sysType" bson:"sysType"` // 系统类型 + IsVideoDeduction bool `json:"isVideoDeduction" bson:"isVideoDeduction"` // 是否扣量 + DiscDoc `bson:",inline"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func Unique(uid uint64, videoID primitive.ObjectID) string { + list := []string{ + strconv.FormatInt(int64(uid), 10), + videoID.Hex(), + } + s := strings.Join(list, ".") + return s +} + +type VideoCoinIncome struct { + VID ObjectID `bson:"_id" json:"videoID"` //video id + Total int64 `bson:"total" json:"total"` //视频总金币 + Count int64 `bson:"count" json:"count"` //视频购买总次数 +} + +type PublisherDeductionStat struct { + IsVideoDeduction bool `bson:"_id"` // 是否扣量 + TotalAmount int64 `bson:"totalAmount"` // 视频总售卖(金币) + VideoCount int64 `bson:"videoCount"` // 总计视频数 +} + +// Pay4VidLogShort 购买影片短结构 +type Pay4VidLogShort struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + VideoID primitive.ObjectID `json:"videoID" bson:"videoID"` + PayMoney int64 `json:"payMoney" bson:"payMoney"` //金币 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} diff --git a/models/l/playlgmod/play.go b/models/l/playlgmod/play.go new file mode 100644 index 0000000..101b240 --- /dev/null +++ b/models/l/playlgmod/play.go @@ -0,0 +1,426 @@ +package playlgmod + +import ( + "fmt" + "math" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "github.com/jinzhu/now" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.PlayLog +const maxReadVideos = 200 + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "videoID", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InsertPlayRecord 记录一个播放行为 +func InsertPlayRecord(p PlayLog) error { + if _, err := coll(nil).InsertOne(&p); err != nil { + log.Error("InsertPlayRecord error", log.Any("p", p), log.E(err)) + return err + } + return nil +} + +// HasWatchedVideo 已经观看的视频 +func HasWatchedVideo(uid uint64) ([]primitive.ObjectID, error) { + now := time.Now() + begin := now.AddDate(0, 0, -7) + var logs []*PlayLog + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "createdAt", Value: -1}}).SetLimit(maxReadVideos) + if err := coll(nil).Find(&logs, bson.M{"uid": uid, "createdAt": bson.M{"$gt": begin}}, &opts); err != nil { + log.Error("HasWatchedVideo error", log.Any("uid", uid), log.E(err)) + return nil, err + } + vids := make([]primitive.ObjectID, len(logs)) + for i, l := range logs { + vids[i] = l.VideoID + } + return vids, nil +} + +// HasWatchedVideoCnt 已经观看的视频数 +func HasWatchedVideoCnt(uids []uint64) ([]UIDCount, error) { + now := time.Now() + begin := now.AddDate(0, 0, -4) + var datas []UIDCount + cond := bson.M{"uid": bson.M{"$in": uids}, "createdAt": bson.M{"$gt": begin}} + if err := coll(nil).Aggregate(&datas, []bson.M{ + bson.M{"$match": cond}, + bson.M{"$group": bson.M{"_id": "$uid", "count": bson.M{"$sum": 1}}}, + }); err != nil { + log.Error("HasWatchedVideoCnt error", log.Any("uids", uids), log.E(err)) + } + return datas, nil +} + +// HasWatchedTags 当天已经观看过的标签id +func HasWatchedTags(uid uint64, top int) ([]string, error) { + tagIDs := []string{} + var data []TagIDCount + if err := coll(nil).Aggregate(&data, []bson.M{ + bson.M{"$match": bson.M{"uid": uid, "tagID": bson.M{"$exists": true}, "createdAt": bson.M{"$gte": now.BeginningOfDay()}}}, + bson.M{"$group": bson.M{"_id": "$tagID", "count": bson.M{"$sum": 1}}}, + bson.M{"$sort": bson.D{{Key: "count", Value: -1}}}, + bson.M{"$limit": top}, + }); err != nil { + log.Error("HasWatchedTags error", log.Any("uid", uid), log.Any("top", top), log.E(err)) + return tagIDs, err + } + tagIDs = make([]string, len(data)) + for i, d := range data { + tagIDs[i] = d.TagID.Hex() + } + return tagIDs, nil +} + +// getPlayRecordSkipSize 计算跳转 +func getPlayRecordSkipSize(page, size uint64, cond bson.M) (uint64, uint64, uint64, error) { + var total uint64 = 10000 + totalpages := uint64(math.Ceil(float64(total) / float64(size))) + if page > totalpages { + page = totalpages + } + if page < 1 { + page = 1 + } + return (page - 1) * size, totalpages, total, nil +} + +// GetPlayRecordList 获取播放记录 +func getPlayRecordList(page, size uint64, cond bson.M, sort bson.D) ([]*PlayLog, uint64, uint64, error) { + skip, totalPages, total, err := getPlayRecordSkipSize(page, size, cond) + if err != nil { + return nil, 0, 0, err + } + opts := options.FindOptions{} + if sort != nil { + opts.SetSort(sort) + } + opts.SetSkip(int64(skip)).SetLimit(int64(size)) + var back []*PlayLog + if err = coll(nil).Find(&back, cond, &opts); err != nil { + log.Error("GetPlayRecordList error", log.Any("page", page), log.Any("size", size), + log.Any("cond", cond), log.Any("sort", sort), log.E(err)) + return nil, totalPages, total, err + } + return back, totalPages, total, nil +} + +// GetNearPlayLog 获取用户最近的观影记录 +func GetNearPlayLog(uid uint64) error { + var p PlayLog + opts := options.FindOne().SetSort(bson.D{{Key: "createdAt", Value: -1}}) + return coll(nil).FindOne(&p, bson.M{"uid": uid}, opts) +} + +// GetViewLogToday 当天是否看过此视频 +func GetViewLogToday(uid uint64, vid primitive.ObjectID) bool { + cond := bson.M{"uid": uid, "videoID": vid, "createdAt": bson.M{"$gte": now.BeginningOfDay(), "$lte": now.EndOfDay()}} + cnt, _ := coll(nil).Count(cond) + return cnt > 0 +} + +// 当天是否观看过视频 +func IsViewToday(uid uint64) bool { + cond := bson.M{"uid": uid, "createdAt": bson.M{"$gte": now.BeginningOfDay(), "$lte": now.EndOfDay()}} + cnt, _ := coll(nil).Count(cond) + return cnt > 0 +} + +// PlayCountMapByTime PlayCountMapByTime playWays 0免费 1付费 2.试看 +func PlayCountMapByTime(start time.Time, end time.Time, playWays []int) (map[primitive.ObjectID]int64, error) { + if len(playWays) == 0 { + return make(map[primitive.ObjectID]int64), nil + } + filter := bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + "playWay": bson.M{"$in": playWays}, //0免费 1付费 2.试看 + } + pipeline := []bson.M{ + { + "$match": filter, + }, + { + "$group": bson.M{ + "_id": "$videoID", + "count": bson.M{ + "$sum": 1, + }, + }, + }, + } + list := make([]struct { + VID primitive.ObjectID `bson:"_id"` + Count int64 `bson:"count"` + }, 0) + if err := coll(nil).Aggregate(&list, pipeline); err != nil { + return nil, fmt.Errorf("PlayLog PlayCountMapByTime err: %s", err.Error()) + } + ret := make(map[primitive.ObjectID]int64) + for _, v := range list { + ret[v.VID] = v.Count + } + return ret, nil +} + +func GetPayUserCount(vid []primitive.ObjectID) (int64, error) { + if len(vid) == 0 { + return 0, nil + } + pipeline := []bson.M{ + { + "$match": bson.M{ + "videoID": bson.M{ + "$in": vid, + }, + }, + }, + { + "$group": bson.M{ + "_id": "$uid", + }, + }, + } + count, err := coll(nil).Count(pipeline) + if err != nil { + return 0, fmt.Errorf("GetPatUserCount err: %s", err.Error()) + } + return count, nil +} + +// PlayUIDListByTime +// playWay 0,免费 1付费 2.试看 ,nil All +func PlayUIDListByTime(start time.Time, end time.Time, playWay *int) ([]uint64, error) { + match := bson.M{ + "$match": bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + }, + } + if playWay != nil { + match["playWay"] = *playWay //0,免费 1付费 2.试看 + } + pipeline := []bson.M{match} + pipeline = append(pipeline, + bson.M{ + "$group": bson.M{ + "_id": "$uid", + }, + }) + list := []struct { + UID uint64 `bson:"_id"` + }{} + if err := coll(nil).Aggregate(&list, pipeline); err != nil { + return nil, fmt.Errorf("PlayUIDListByTime err: %s", err.Error()) + } + uidList := make([]uint64, len(list)) + for i, v := range list { + uidList[i] = v.UID + } + return uidList, nil +} + +func backWatchParam(param WatchReq) map[string]interface{} { + m := make(map[string]interface{}) + if param.UID > 0 { + m["uid"] = param.UID + } + if param.PlayWay == 1 { + m["playWay"] = 0 + } + if param.PlayWay == 2 { + m["playWay"] = 1 + } + if param.PlayWay == 3 { + m["playWay"] = 2 + } + if !param.End.IsZero() { + i := make(map[string]time.Time) + i["$gte"] = param.Start + i["$lt"] = param.End + m["createdAt"] = i + } + return m +} + +// GetPlayLog 获取播放日志 +func GetPlayLog(param WatchReq) ([]*PlayLog, uint64, error) { + cond := backWatchParam(param) + sort := bson.D{{Key: "createdAt", Value: -1}} + infos, _, total, err := getPlayRecordList(param.PageNumber, param.PageSize, bson.M(cond), sort) + return infos, total, err +} + +// GetUIDPlaySecondMap 用户播放时长(秒)map +func GetUIDPlaySecondMap(uidList []uint64) (map[uint64]int64, error) { + pipeline := []bson.M{ + { + "$match": bson.M{ + "uid": bson.M{ + "$in": uidList, + }, + }, + }, + { + "$group": bson.M{ + "_id": "$uid", + "playSecond": bson.M{ + "$sum": "$longer", + }, + }, + }, + } + list := []struct { + UID uint64 `bson:"_id"` + PlaySecond int64 `bson:"playSecond"` + }{} + if err := coll(nil).Aggregate(&list, pipeline); err != nil { + return nil, fmt.Errorf("GetUIDPlayTimeMap err: %s", err.Error()) + } + playSecondMap := make(map[uint64]int64, len(list)) + for _, v := range list { + playSecondMap[v.UID] = v.PlaySecond + } + return playSecondMap, nil +} + +// 根据时间范围获取标签下的视频的播放次数 +func GetTagPlayByTimeRange(start time.Time, end time.Time) ([]TagPlayCount, error) { + list := make([]TagPlayCount, 0) + pipeline := []bson.M{ + { + "$match": bson.M{ + "tagID": bson.M{"$exists": true}, + "createdAt": bson.M{"$gte": start, "$lt": end}, + }, + }, + { + "$group": bson.M{ + "_id": bson.M{"videoID": "$videoID", "tagID": "$tagID"}, + "playCount": bson.M{"$sum": 1}, + }, + }, + } + if err := coll(nil).Aggregate(&list, pipeline); err != nil { + log.Error("models log play log model GetTagPlayByTimeRange error", log.E(err), log.Any("start", start), log.Any("end", end)) + return nil, err + } + return list, nil +} + +// 获取播放量最多的视频 +func GetHotVid(start time.Time, end time.Time) ([]TagPlayCount, error) { + list := make([]TagPlayCount, 0) + pipeline := []bson.M{ + { + "$match": bson.M{ + "tagID": bson.M{"$exists": true}, + "createdAt": bson.M{"$gte": start, "$lt": end}, + }, + }, + { + "$group": bson.M{ + "_id": bson.M{"videoID": "$videoID", "tagID": "$tagID"}, + "playCount": bson.M{"$sum": 1}, + }, + }, + } + if err := coll(nil).Aggregate(&list, pipeline); err != nil { + log.Error("models log play log model GetTagPlayByTimeRange error", log.E(err), log.Any("start", start), log.Any("end", end)) + return nil, err + } + return list, nil +} + +// 获取播放量最多的视频 +func GetHotVidIDs() ([]VidCount, error) { + list := make([]VidCount, 0) + pipeline := []bson.M{ + { + "$match": bson.M{ + "createdAt": bson.M{ + "$gte": time.Now().AddDate(0, 0, -1), + }, + }, + }, + { + "$group": bson.M{ + "_id": "$videoID", + "count": bson.M{"$sum": 1}, + }, + }, + {"$sort": bson.M{ + "count": -1, + }}, + { + "$limit": 20, + }, + } + return list, coll(nil).Aggregate(&list, pipeline) +} + +// 获取播放量最多的视频 +func GetHotPublishesr() ([]PublisherCount, error) { + list := make([]PublisherCount, 0) + pipeline := []bson.M{ + { + "$group": bson.M{ + "_id": "$publisher", + "count": bson.M{"$sum": 1}, + }, + }, + {"$match": bson.M{ + "_id": bson.M{"$gt": 0}, + }}, + {"$sort": bson.M{ + "count": -1, + }}, + { + "$limit": 20, + }, + } + return list, coll(nil).Aggregate(&list, pipeline) +} + +func DeleteBeforeCreatedAt(t *db.MongoTool, tm time.Time) error { + _, err := coll(t).DeleteMany(bson.M{"createdAt": bson.M{"$lt": tm}}) + return err +} diff --git a/models/l/playlgmod/type.go b/models/l/playlgmod/type.go new file mode 100644 index 0000000..fd94e19 --- /dev/null +++ b/models/l/playlgmod/type.go @@ -0,0 +1,73 @@ +package playlgmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +// PlayLog 观看记录 +type PlayLog struct { + UID uint64 `json:"uid" bson:"uid"` + VideoID primitive.ObjectID `json:"videoID" bson:"videoID"` + Longer int `json:"longer" bson:"longer"` //播放时长 单位:秒 + Progress int `json:"progress" bson:"progress"` + Via int `json:"via" bson:"via"` + PlayWay int `json:"playWay" bson:"playWay"` //0,免费 1付费 2.试看 + TagID primitive.ObjectID `json:"tagID" bson:"tagID,omitempty"` + Publisher uint64 `json:"publisher" bson:"publisher,omitempty"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +type GroupKey struct { + VideoID primitive.ObjectID `bson:"videoID"` + TagID primitive.ObjectID `bson:"tagID"` +} + +type TagPlayCount struct { + GroupID GroupKey `bson:"_id"` + PlayCount int64 `bson:"playCount"` +} + +type TagPlayInfo struct { + VideoID primitive.ObjectID + TagID primitive.ObjectID + PlayCount int64 +} + +// TagIDCount 观看过的tagID统计 +type TagIDCount struct { + TagID primitive.ObjectID `bson:"_id"` + Count int `bson:"count"` +} + +// UIDCount 用户观看次数 +type UIDCount struct { + UID uint64 `bson:"_id"` + Count int `bson:"count"` +} + +// VidCount 视频观看次数 +type VidCount struct { + //视频id + VideoID primitive.ObjectID `json:"videoID" bson:"_id"` + //次数 + Count int `bson:"count"` +} + +// PublisherCount 博主被观看次数 +type PublisherCount struct { + //上传者id + Publisher uint64 `json:"publisher" bson:"_id,omitempty"` + //次数 + Count int `bson:"count"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/l/playlgmod/web.go b/models/l/playlgmod/web.go new file mode 100644 index 0000000..8644c80 --- /dev/null +++ b/models/l/playlgmod/web.go @@ -0,0 +1,37 @@ +package playlgmod + +import ( + "time" + + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// WatchReq 观看日志查询请求参数 +type WatchReq struct { + PlayWay int `form:"playWay" json:"playWay"` + UID uint64 `form:"uid" json:"uid"` + Start time.Time `form:"start" json:"start"` + End time.Time `form:"end" json:"end"` + commod.Page +} + +// LogInfo 返回数据 +type LogInfo struct { + UID uint64 `json:"uid"` + VideoID primitive.ObjectID `json:"videoID"` + Title string `json:"title"` + PlayTime uint `json:"playTime"` + Longer int `json:"longer" ` + Progress int `json:"progress"` + Via int `json:"via"` + PlayWay int `json:"playWay"` //0,免费 1付费 2.试看 + CreatedAt time.Time `json:"createdAt"` +} + +// WatchResp 观看日志查询请求应答 +type WatchResp struct { + Logs []*LogInfo `json:"logs"` + Total uint64 `json:"total"` +} diff --git a/models/l/pullgmod/pull.go b/models/l/pullgmod/pull.go new file mode 100644 index 0000000..db19c7c --- /dev/null +++ b/models/l/pullgmod/pull.go @@ -0,0 +1,62 @@ +package pullgmod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.PullLog + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "newUpdatedAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// InsertPullLog InsertPullLog +func InsertPullLog(p PullLog) error { + _, err := coll(nil).InsertOne(&p) + return err +} + +// FindPullRecord 查询 +func FindPullRecord(vidType, newUpdateAt string) (pr *PullLog, err error) { + opts := options.FindOne().SetSort(bson.D{{Key: "createdAt", Value: -1}}) + cond := bson.M{} + if vidType != "" { + cond["vidType"] = vidType + } + if newUpdateAt != "" { + cond["newUpdatedAt"] = newUpdateAt + } + if err = coll(nil).FindOne(&pr, cond, opts); err != nil { + log.Error("FindPullRecord error", log.E(err)) + return + } + return +} diff --git a/models/l/pullgmod/type.go b/models/l/pullgmod/type.go new file mode 100644 index 0000000..8c43788 --- /dev/null +++ b/models/l/pullgmod/type.go @@ -0,0 +1,23 @@ +package pullgmod + +import ( + "time" + + "91porn-server/common/db" +) + +// PullLog 从aws同步视频记录 +type PullLog struct { + LastID string `json:"lastId" bson:"lastId"` + Count int `json:"count" bson:"count"` + FailCount int `json:"failCount" bson:"failCount"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + FailIDs []string `json:"failIDs" bson:"failIDs,omitempty"` + VidType string `json:"vidType" bson:"vidType"` + NewUpdatedAt string `json:"newUpdatedAt" bson:"newUpdatedAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/l/registermod/register.go b/models/l/registermod/register.go new file mode 100644 index 0000000..93ce8be --- /dev/null +++ b/models/l/registermod/register.go @@ -0,0 +1,57 @@ +package registermod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.NewRegister + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "ip", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func InsertRegisterLog(l *NewRegisterLog) error { + if _, err := coll(nil).InsertOne(l); err != nil { + log.Error("loginlog insert err", log.E(err)) + return err + } + return nil +} + +func DeleteBeforeCreatedAt(t *db.MongoTool, tm time.Time) error { + _, err := coll(t).DeleteMany(bson.M{"createdAt": bson.M{"$lt": tm}}) + return err +} diff --git a/models/l/registermod/type.go b/models/l/registermod/type.go new file mode 100644 index 0000000..f5affb4 --- /dev/null +++ b/models/l/registermod/type.go @@ -0,0 +1,25 @@ +package registermod + +import ( + "time" + + "91porn-server/common/db" +) + +// 新注册用户Log 接口调用记录 +type NewRegisterLog struct { + UID uint64 `json:"uid" bson:"uid"` + IP string `json:"ip" bson:"ip"` + Query []QueryStat `json:"query" bson:"query"` //接口调用次数记录表 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +type QueryStat struct { + QueryUrl string `json:"key" bson:"key"` + Count int64 `json:"count" bson:"count"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/l/searchlogmod/searchLog.go b/models/l/searchlogmod/searchLog.go new file mode 100644 index 0000000..264d437 --- /dev/null +++ b/models/l/searchlogmod/searchLog.go @@ -0,0 +1,148 @@ +package searchlogmod + +import ( + "fmt" + "time" + + "91porn-server/common/constant" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.SearchLog + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "realm", Value: 1}}, //不会新建 realm 索引 + }, + { + Keys: bson.D{{Key: "realm", Value: 1}, {Key: "keyword", Value: 1}}, //不会新建 realm 索引 + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// FindHotSearch 根据搜索量获取热搜排行榜 +func FindHotSearch(realm constant.RealmType, page commod.Page) ([]SearchCount, error) { + p := []M{ + { + "$match": M{"realm": realm}, + }, + { + "$group": M{"_id": "$keyword", "count": M{"$sum": 1}}, + }, + { + "$match": M{"_id": M{"$ne": ""}}, + }, + { + "$sort": bson.D{{Key: "count", Value: -1}}, + }, + { + "$skip": (page.PageNumber - 1) * page.PageSize, + }, + { + "$limit": page.PageSize, + }, + } + data := make([]SearchCount, 0, page.PageSize) + if err := coll(nil).Aggregate(&data, p); err != nil { + log.ZapLog.Error(fmt.Sprintf("models record SearchLog FindSearchCountVid fail error:%+v:", err)) + return nil, err + } + return data, nil +} + +// InsertMany InsertMany +func InsertMany(uid uint64, realm constant.RealmType, keyWords []string) error { + now := time.Now() + searchLogDoc := make([]SearchLogDoc, 0, len(keyWords)) + for _, keyword := range keyWords { + if keyword == "" { + continue + } + searchLogDoc = append(searchLogDoc, SearchLogDoc{ + Realm: realm, + UID: uid, + Keyword: keyword, + CreatedAt: now, + }) + } + if len(searchLogDoc) == 0 { + return nil + } + if _, err := coll(nil).InsertMany(&searchLogDoc); err != nil { + log.ZapLog.Error("SearchLog InsertMany error", log.E(err)) + return err + } + return nil +} + +func GetKeywordCountMapByTime(start time.Time, end time.Time, realm constant.RealmType) (map[string]int64, error) { + pipeline := []M{ + { + "$match": M{ + "createdAt": M{ + "$gte": start, + "$lt": end, + }, + "realm": realm, + }, + }, + { + "$sortByCount": "$keyword", + }, + } + list := []struct { + Keyword string `bson:"_id"` + Count int64 `bson:"count"` + }{} + if err := coll(nil).Aggregate(&list, pipeline); err != nil { + return nil, fmt.Errorf("GetKeywordCountMapByTime err: %s", err.Error()) + } + m := make(map[string]int64, len(list)) + for _, v := range list { + m[v.Keyword] = v.Count + } + return m, nil +} + +// 根据时间段获取搜索日志 +func GetSearchLogByTimeRange(start time.Time, end time.Time) (data []SearchLog, err error) { + var query = bson.M{ + "createdAt": bson.M{"$gte": start, "$lt": end}, + } + if err = coll(nil).Find(&data, query); err != nil { + log.Error("models SearchLog model GetSearchLogByTimeRange error", log.E(err)) + return + } + return +} + +func DeleteBeforeCreatedAt(t *db.MongoTool, tm time.Time) error { + _, err := coll(t).DeleteMany(bson.M{"createdAt": bson.M{"$lt": tm}}) + return err +} diff --git a/models/l/searchlogmod/type.go b/models/l/searchlogmod/type.go new file mode 100644 index 0000000..1d51fa3 --- /dev/null +++ b/models/l/searchlogmod/type.go @@ -0,0 +1,54 @@ +package searchlogmod + +import ( + "time" + + "91porn-server/common/constant" + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ( + SearchLogDoc = SearchLog + + ObjectID = primitive.ObjectID + + M = bson.M +) + +// SearchLog 搜索记录 +type SearchLog struct { + ID ObjectID `json:"id" bson:"_id,omitempty"` + Realm constant.RealmType `json:"realm" bson:"realm"` //搜索区域 综合:complex 视频:video 用户:user 话题:tag 地点:site + UID uint64 `json:"uid" bson:"uid"` //搜索人ID + Keyword string `json:"keyword" bson:"keyword"` //搜索的关键字 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //搜索的时间 +} + +// SearchLog 搜索记录 +type ESSearchLogSource struct { + ID ObjectID `json:"id"` + Realm constant.RealmType `json:"realm"` //搜索区域 综合:complex 视频:video 用户:user 话题:tag 地点:site + UID uint64 `json:"uid"` //搜索人ID + Keyword string `json:"keyword"` //搜索的关键字 + CreatedAt time.Time `json:"createdAt"` //搜索的时间 +} + +// SearchCount 热搜视频统计最高多搜索次数的视频 +type SearchCount struct { + KeyWord string `json:"keyWord" bson:"_id"` //关键字 + Count int `json:"count" bson:"count"` //关键字搜索次数 +} + +// SearchResp SearchResp +type SearchResp struct { + TagName string `json:"id" bson:"_id,omitempty"` + Count int `json:"count" bson:"count,omitempty"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/l/synccdnmod/synccdn.go b/models/l/synccdnmod/synccdn.go new file mode 100644 index 0000000..064a2d8 --- /dev/null +++ b/models/l/synccdnmod/synccdn.go @@ -0,0 +1,57 @@ +package synccdnmod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.SyncCdnLog + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// InsertSyncCdnLog InsertSyncCdnLog +func InsertSyncCdnLog(p SyncCdnLog) error { + _, err := coll(nil).InsertOne(&p) + return err +} + +// FindSyncRecord 查询 +func FindSyncRecord() (pr []SyncCdnLog, err error) { + pr = []SyncCdnLog{} + cond := []bson.M{ + bson.M{ + "$sort": bson.M{"createdAt": -1}, + }, + bson.M{ + "$limit": 1}} + if err = coll(nil).Aggregate(&pr, cond); err != nil { + log.Error("FindSyncRecord error", log.E(err)) + return + } + return +} diff --git a/models/l/synccdnmod/type.go b/models/l/synccdnmod/type.go new file mode 100644 index 0000000..d503154 --- /dev/null +++ b/models/l/synccdnmod/type.go @@ -0,0 +1,51 @@ +package synccdnmod + +import ( + "91porn-server/common/db" + "91porn-server/common/stderr" + "time" +) + +const ( + AccessKey = "4qQTNUrg9MloxqtykJbkf9BTeJ3liBo5fS7Qg6Nu" + SecretKey = "axcamr082kQaJx87ebqkGHNPy-9EBeoTT2XbU6A2" + //AccessKey = "MY_ACCESS_KEY" + //SecretKey = "MY_SECRET_KEY" + URl = "http://rs.qiniu.com/move/bmV3ZG9jczpmaW5kX21hbi50eHQ=/bmV3ZG9jczpmaW5kLm1hbi50eHQ=" + + Host string = "http://fusion.qiniuapi.com" //域名 + FetchUrl string = "/v2/tune/prefetch" //预取接口 + PreFetchUrl string = "/v2/tune/prefetch/list" //预期查询接口地址 + +) + +// SyncHttpResp 返回结构体 +type SyncHttpResp struct { + Code stderr.Code `json:"code"` + Error string `json:"error"` + RequestId string `json:"requestId"` + InvalidUrls []string `json:"invalidUrls"` + QuotaDay int `json:"quotaDay"` + SurplusDay int `json:"surplusDay"` +} + +type SyncCdnLog struct { + RequestIds []string `json:"requestIds" bson:"requestIds"` + LastID string `json:"lastId" bson:"lastId"` + Count int `json:"count" bson:"count"` + FailCount int `json:"failCount" bson:"failCount"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + FailIDs []string `json:"failIDs" bson:"failIDs,omitempty"` +} + +// SyncReq 前端参数传递 +type SyncReq struct { + Token string `json:"token" binding:"required"` + Cdn string `json:"cdn"` + FilePath string `json:"filePath"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/l/userdailytasklogmod/type.go b/models/l/userdailytasklogmod/type.go new file mode 100644 index 0000000..4755668 --- /dev/null +++ b/models/l/userdailytasklogmod/type.go @@ -0,0 +1,71 @@ +package userdailytasklogmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/v/dailytaskmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.UserDailyTaskLog + +// InitIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "type", Value: 1}, {Key: "date", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type UserDailyTask struct { + ID primitive.ObjectID `json:"_id" bson:"_id"` + UID uint64 `json:"uid" bson:"uid"` + Type int `json:"type" bson:"type"` // 0 每日广告点击; 1 每日任务 + Date string `json:"date" bson:"date"` // 日期. 格式 2006-01-02 + FinishCount uint64 `json:"finishCount" bson:"finishCount"` // 当日完成次数 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +func CompleteDailyTask(uid uint64, taskType int64, completeTime uint64) error { + _, err := coll(nil).UpsertOne(bson.M{"uid": uid, "type": taskType, "date": time.Now().Format("2006-01-02")}, bson.M{"$inc": bson.M{"finishCount": completeTime}}) + return err +} + +func GetUserFinishCount(uid uint64, taskType dailytaskmod.DailyTaskTypeEnum) (uint64, error) { + var udk UserDailyTask + if err := coll(nil).FindOne(&udk, bson.M{"uid": uid, "type": taskType, "date": time.Now().Format("2006-01-02")}); err != nil { + return 0, err + } + if udk.ID.IsZero() { + return 0, nil + } + return udk.FinishCount, nil +} diff --git a/models/l/visitlogmod/list.go b/models/l/visitlogmod/list.go new file mode 100644 index 0000000..1373853 --- /dev/null +++ b/models/l/visitlogmod/list.go @@ -0,0 +1,44 @@ +package visitlogmod + +import ( + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type VisitLogSlice []VisitLog + +func (v VisitLogSlice) ToUIDs() []uint64 { + uids := make([]uint64, len(v)) + for i, l := range v { + uids[i] = l.UID + } + return uids +} + +type Matcher = pageopt.Matcher + +// CreatedAtGTEAndLTMatch +type CreatedAtGTEAndLTMatch = pageopt.CreatedAtGTEAndLTMatch + +func List(sort bson.D, skip, limit *int64, mats ...Matcher) (VisitLogSlice, error) { + opt := (&options.FindOptions{}) + if len(sort) != 0 { + opt.SetSort(sort) + } + if skip != nil { + opt.SetSkip(*skip) + } + if limit != nil { + opt.SetLimit(*limit) + } + filter := pageopt.MergeM(mats) + list := VisitLogSlice{} + return list, coll(nil).Find(&list, filter, opt) +} + +func Count(matchs ...pageopt.Matcher) (int64, error) { + filter := pageopt.MergeM(matchs) + return coll(nil).Count(filter) +} diff --git a/models/l/visitlogmod/type.go b/models/l/visitlogmod/type.go new file mode 100644 index 0000000..af0f07e --- /dev/null +++ b/models/l/visitlogmod/type.go @@ -0,0 +1,38 @@ +package visitlogmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 以天为单位,统计用户的首次访问的日志 +type VisitLog struct { + ID *primitive.ObjectID `json:"id" bson:"_id,omitempty"` //记录Id + SumDate time.Time `json:"sumDate" bson:"sumDate"` //每日零点 + UID uint64 `json:"uid" bson:"uid"` //用户ID + IP string `json:"ip" bson:"ip"` //IP + SysType string `json:"sysType" bson:"sysType"` //操作系统类型 安卓 IOS + Ver string `json:"ver" bson:"ver"` //APP版本号 + DevType string `json:"devType" bson:"devType"` //设备型号 + DevID string `json:"devID" bson:"devID"` //设备ID + BuildID string `json:"buildID" bson:"buildID"` //app构建ID(包ID) + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 + + IsDirect bool `json:"isDirect" bson:"isDirect"` // true:是直推用户 + DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码 + RegisterTime time.Time `json:"registerTime" bson:"registerTime"` //用户注册时间 + IsDeduction bool `json:"isDeduction" bson:"isDeduction"` // true:CPA扣量用户 +} + +type FilterDoc struct { + SumDate time.Time `bson:"sumDate"` //每日零点 + UID uint64 `bson:"uid"` //用户ID +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/l/visitlogmod/visitLog.go b/models/l/visitlogmod/visitLog.go new file mode 100644 index 0000000..ddce9a2 --- /dev/null +++ b/models/l/visitlogmod/visitLog.go @@ -0,0 +1,187 @@ +package visitlogmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/models" + "91porn-server/models/v/usermod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.VisitLog + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "sumDate", Value: -1}, {Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func DataClean(deleteAt time.Time) (int64, error) { + res, err := coll(nil).DeleteMany(bson.M{"createdAt": bson.M{"$lt": deleteAt}}) + if err != nil { + return 0, err + } + return res.DeletedCount, nil +} + +func UpsertUserVisit(l *VisitLog) (*mongo.UpdateResult, error) { + l.CreatedAt = time.Now() + b, _ := bson.Marshal(l) + m := bson.M{} + _ = bson.Unmarshal(b, &m) + return coll(nil).UpsertOne(bson.M{"sumDate": l.SumDate, "uid": l.UID}, bson.M{"$setOnInsert": m}) +} + +func UIDListByCreatedTime(start time.Time, end time.Time, mats ...Matcher) ([]uint64, error) { + list := []struct { + UID uint64 `bson:"uid"` //用户ID + }{} + mats = append(mats, (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + filter := pageopt.MergeM(mats) + opt := (&options.FindOptions{}).SetProjection(bson.M{ + "uid": 1, + }) + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.Error("UIDListByCreatedTime failed", log.Any("filter", filter), log.E(err)) + return nil, err + } + uids := make([]uint64, len(list)) + for i, v := range list { + uids[i] = v.UID + } + return uids, nil +} + +func GetSumDateCountMap(uidList []uint64) (map[uint64]int64, error) { + if len(uidList) == 0 { + return make(map[uint64]int64), nil + } + pipeLine := []bson.M{ + { + "$match": bson.M{ + "uid": bson.M{ + "$in": uidList, + }, + }, + }, + { + "$group": bson.M{ + "_id": "$uid", + "count": bson.M{ + "$sum": 1, + }, + }, + }, + } + list := make([]struct { + UID uint64 `bson:"_id"` + Count int64 `bson:"count"` + }, 0, len(uidList)) + if err := coll(nil).Aggregate(&list, pipeLine); err != nil { + return nil, err + } + countMap := make(map[uint64]int64, len(list)) + for _, v := range list { + countMap[v.UID] = v.Count + } + return countMap, nil +} + +// RetainUserListByTime 存留user +func RetainUserListByTime(createUserStart, createUserEnd, visitStart, visitEnd time.Time, mats ...usermod.Matcher) ([]usermod.User, error) { + visitFilter := (&CreatedAtGTEAndLTMatch{GTE: &visitStart, LT: &visitEnd}).New().Filter() + visitList := make([]struct { + UID uint64 `bson:"uid"` + }, 0) + visitOpt := (&options.FindOptions{}).SetProjection(bson.M{ + "uid": 1, + }) + if err := coll(nil).Find(&visitList, visitFilter, visitOpt); err != nil { + return nil, err + } + visitUIDList := make([]uint64, len(visitList)) + for i, v := range visitList { + visitUIDList[i] = v.UID + } + return usermod.UserListByCreateTimeAndUIDS(createUserStart, createUserEnd, visitUIDList, mats...) +} + +// RetainUIDListByTime 存留UID +func RetainUIDListByTime(createStart, createEnd, visitStart, visitEnd time.Time, mats ...usermod.Matcher) ([]uint64, error) { + visitList := make([]struct { + UID uint64 `bson:"uid"` + }, 0) + visitFilter := (&CreatedAtGTEAndLTMatch{GTE: &visitStart, LT: &visitEnd}).New().Filter() + visitOpt := (&options.FindOptions{}).SetProjection(bson.M{ + "uid": 1, + }) + if err := coll(nil).Find(&visitList, visitFilter, visitOpt); err != nil { + return nil, err + } + visitUIDList := make([]uint64, len(visitList)) + for i, v := range visitList { + visitUIDList[i] = v.UID + } + return usermod.UIDListByCreateTimeAndUIDS(createStart, createEnd, visitUIDList, mats...) +} + +// AccessSyncById 每日访问数据同步 +func AccessSyncById(Id string, size int64) (data []VisitLog, err error) { + id, err := primitive.ObjectIDFromHex(Id) + if err != nil { + return + } + endTime := time.Now().Add(-time.Minute * 5) + visitFilter := bson.M{"_id": bson.M{"$gt": id}, "createdAt": bson.M{"$lt": endTime}} + visitOpt := (&options.FindOptions{}).SetLimit(size).SetSort(bson.M{"_id": 1}) + err = coll(nil).Find(&data, visitFilter, visitOpt) + return +} + +func AccessSyncByTime(visitTime time.Time, size int64) (data []VisitLog, err error) { + endTime := time.Now().Add(-time.Minute * 5) + visitFilter := bson.M{"createdAt": bson.M{"$gt": visitTime, "$lt": endTime}} + visitOpt := (&options.FindOptions{}).SetLimit(size).SetSort(bson.M{"createdAt": 1}) + err = coll(nil).Find(&data, visitFilter, visitOpt) + return +} + +func GetInfoByCond(cond bson.M, opts ...*options.FindOneOptions) (VisitLog, error) { + v := VisitLog{} + if err := coll(nil).FindOne(&v, cond, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("cond", cond), + ) + return v, err + } + return v, nil +} diff --git a/models/l/welfarelgmod/pull.go b/models/l/welfarelgmod/pull.go new file mode 100644 index 0000000..9a34946 --- /dev/null +++ b/models/l/welfarelgmod/pull.go @@ -0,0 +1,54 @@ +package welfarelgmod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.WelfareLog + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: -1}, {Key: "fareType", Value: -1}}, + Options: options.Index().SetUnique(true), + }, { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// InsertWelFareLog InsertWelFareLog +func InsertWelFareLog(p WelfareLog) error { + _, err := coll(nil).InsertOne(&p) + return err +} + +// FindWelFareByUid 查询 +func FindWelFareByUid(uid uint64) (pr *WelfareLog, err error) { + if err = coll(nil).FindOne(&pr, bson.M{"uid": uid}); err != nil { + log.Error("FindWelFareByUid error", log.E(err)) + return + } + return +} diff --git a/models/l/welfarelgmod/type.go b/models/l/welfarelgmod/type.go new file mode 100644 index 0000000..47631c9 --- /dev/null +++ b/models/l/welfarelgmod/type.go @@ -0,0 +1,31 @@ +package welfarelgmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WelFareType int64 + +const ( + Fare_Vip WelFareType = iota //vip + Fare_Coin //金币 +) + +// 福利添加记录 +type WelfareLog struct { + Id primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + Uid uint64 `json:"uid" bson:"uid"` + FareType WelFareType `json:"fareType" bson:"fareType"` + FareNum int64 `json:"fareNum" bson:"fareNum"` + DailyDate time.Time `json:"dailyDate" bson:"dailyDate"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/s/dailyretentionmod/dailyRetention.go b/models/s/dailyretentionmod/dailyRetention.go new file mode 100644 index 0000000..dc15c0d --- /dev/null +++ b/models/s/dailyretentionmod/dailyRetention.go @@ -0,0 +1,121 @@ +package dailyretentionmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "errors" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.DailyRetention + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{ + {Key: "date", Value: -1}, + }, + }, + { + Keys: bson.D{ + {Key: "createdAt", Value: -1}, + }, + }, + { + Keys: bson.D{ + {Key: "adGroup", Value: 1}, + {Key: "date", Value: -1}, + }, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +// QueryAllList 分页查询文档 +func QueryAllList(filter primitive.M, opts ...*options.FindOptions) (out []*DailyRetention, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryOne +func QueryOne(filter bson.M) (*DailyRetention, error) { + v := DailyRetention{} + if err := coll(nil).FindOne(&v, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryOne", table, "FindOne", err), + log.Any("filter", filter), + ) + return &v, err + } + + if v.Id.IsZero() { + return nil, errors.New("record not found") + } + + return &v, nil +} + +// UpsertOne +func UpsertOne(filter bson.M, updateData bson.M) (err error) { + if _, err = coll(nil).UpsertOne(filter, updateData); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpsertOne", table, "UpdateOne", err), + log.Any("filter", filter), + log.Any("updateData", updateData), + ) + return + } + return +} + +func BulkWrite(models []mongo.WriteModel, opts ...*options.BulkWriteOptions) (err error) { + if len(models) == 0 { + return nil + } + if _, err = coll(nil).Bulk(models, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "BulkWrite", table, "BulkWrite", err), + log.Any("modelsCount", len(models)), + ) + return + } + return +} + +func Distinct(filter bson.M, fieldName string, opts ...*options.DistinctOptions) (out []*DailyRetention, err error) { + raw, err := coll(nil).Distinct(fieldName, filter, opts...) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Distinct", table, "UpdateOne", err), + log.Any("fieldName", fieldName), + log.Any("filter", filter), + ) + return + } + out = make([]*DailyRetention, len(raw)) + return +} diff --git a/models/s/dailyretentionmod/type.go b/models/s/dailyretentionmod/type.go new file mode 100644 index 0000000..0ce7791 --- /dev/null +++ b/models/s/dailyretentionmod/type.go @@ -0,0 +1,33 @@ +package dailyretentionmod + +import ( + "91porn-server/common/db" + "91porn-server/models/commod" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +const ( + MaxRetentionDays = 30 // 最大留存天数 +) + +// DailyRetention 每日用户留存统计 +// @table:daily_retention +// @model:DailyRetentionModel +type DailyRetention struct { + Id primitive.ObjectID `json:"id" bson:"_id"` // 自增ID + Date time.Time `json:"date" bson:"date" index:"date_idx, date_adgroup_idx:unique"` // 日期,当日0点 + AdGroup commod.AdGroup `json:"adGroup" bson:"adGroup" index:"adgroup_idx, date_adgroup_idx:unique"` // 广告组 + NewUsers int64 `json:"newUsers" bson:"newUsers"` // 当天新增用户 + UserRetained map[int]int64 `json:"userRetained" bson:"userRetained"` // key = 第几天, value = 日活留存人数 + UserRates map[int]float64 `json:"userRates" bson:"userRates"` // key = 第几天, value = 日活留存率 + AdClickAcc map[int]int64 `json:"adClickAcc" bson:"adClickAcc"` // key = 第几天, value = 广告点击累计 + TotalClickAcc map[int]int64 `json:"totalClickAcc" bson:"totalClickAcc"` // key = 第几天, value = 总点击累计 + PayCountAcc map[int]int64 `json:"payCountAcc" bson:"payCountAcc"` // key = 第几天, value = 充值次数累计 + PayTotalAcc map[int]int64 `json:"payTotalAcc" bson:"payTotalAcc"` // key = 第几天, value = 充值总金额累计 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt" index:"-created_at_idx"` // 创建时间 +} diff --git a/models/s/hotstatmod/hotstat.go b/models/s/hotstatmod/hotstat.go new file mode 100644 index 0000000..71e5626 --- /dev/null +++ b/models/s/hotstatmod/hotstat.go @@ -0,0 +1,203 @@ +package hotstatmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.NewsHot + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "sumDate", Value: 1}, {Key: "videoID", Value: 1}}, + Options: options.Index(), + }, + { + Keys: bson.D{{Key: "sumDate", Value: 1}, {Key: "todayHot", Value: -1}}, + Options: options.Index(), + }, + { + Keys: bson.D{{Key: "sumDate", Value: 1}, {Key: "weekHot", Value: -1}}, + Options: options.Index(), + }, + { + Keys: bson.D{{Key: "sumDate", Value: 1}, {Key: "monthHot", Value: -1}}, + Options: options.Index(), + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + Options: options.Index(), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// UpdateInsertHotValue 更新热度数值,没有就插入 +func UpdateInsertHotValue(sumDate time.Time, mDayli map[primitive.ObjectID]int, mWeek map[primitive.ObjectID]int, mMonth map[primitive.ObjectID]int, mNow map[primitive.ObjectID]int) error { + writes := make([]mongo.WriteModel, len(mNow)) + i := 0 + for vid, v := range mNow { + filter := bson.M{ + "sumDate": sumDate, + "videoID": vid, + } + update := bson.M{ + "$setOnInsert": bson.M{ + "sumDate": sumDate, + "videoID": vid, + "createdAt": time.Now(), + }, + "$set": bson.M{ + "todayHot": mDayli[vid], + "weekHot": mWeek[vid], + "monthHot": mMonth[vid], + "totalHot": v, + "updatedAt": time.Now(), + }, + } + writes[i] = mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + i++ + log.Debug(fmt.Sprintf("sumDate:%s table:%s [filter:%+v update:%+v ]\n", sumDate, table, filter, update)) + } + if len(writes) == 0 { + return nil + } + //bulkWrite 不是原子操作 不具备事务性 + opt := (&options.BulkWriteOptions{}).SetOrdered(false) //设为无序,触发并行写,提升写效率 + _, err := coll(nil).Bulk(writes, opt) + return err +} + +func getHotValue(page, size uint64, cond bson.M, sort bson.D) ([]*HotStat, bool, error) { + hasNext := false + skip := (page - 1) * size + opts := options.FindOptions{} + if sort != nil { + opts.SetSort(sort) + } + opts.SetSkip(int64(skip)).SetLimit(int64(size + 1)) + var back []*HotStat + if err := coll(nil).Find(&back, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getHotValue", table, "Find", err), + log.Any("page", page), + log.Any("size", size), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return nil, hasNext, err + } + if uint64(len(back)) > size { + hasNext = true + back = back[:size] + } + return back, hasNext, nil +} + +func getSortedVideoID(page, size uint64, cond bson.M, sort bson.D) ([]primitive.ObjectID, bool, error) { + ids := []primitive.ObjectID{} + infos, hasNext, err := getHotValue(page, size, cond, sort) + if err != nil { + return ids, false, err + } + ids = make([]primitive.ObjectID, len(infos)) + for i, v := range infos { + ids[i] = v.VideoID + } + return ids, hasNext, err +} + +// GetHotValueToday 查找热度值 +func GetHotValueToday(page, size uint64) ([]primitive.ObjectID, bool, error) { + sumDate := timerange.LocDayRange(time.Now()).Head + cond := bson.M{"sumDate": sumDate} + sort := bson.D{{Key: "todayHot", Value: -1}} + return getSortedVideoID(page, size, cond, sort) +} + +// GetHotValueWeek 查找热度值 +func GetHotValueWeek(page, size uint64) ([]primitive.ObjectID, bool, error) { + sumDate := timerange.LocDayRange(time.Now()).Head + cond := bson.M{"sumDate": sumDate} + sort := bson.D{{Key: "weekHot", Value: -1}} + return getSortedVideoID(page, size, cond, sort) +} + +// GetHotValueMonth 查找热度值 +func GetHotValueMonth(page, size uint64) ([]primitive.ObjectID, bool, error) { + sumDate := timerange.LocDayRange(time.Now()).Head + cond := bson.M{"sumDate": sumDate} + sort := bson.D{{Key: "monthHot", Value: -1}} + return getSortedVideoID(page, size, cond, sort) +} + +func getHotValueByDate(dt time.Time) (map[primitive.ObjectID]int, error) { + var infos []*HotStat + mValue := make(map[primitive.ObjectID]int) + if err := coll(nil).Find(&infos, bson.M{"sumDate": dt}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getHotValue", table, "Find", err)) + return mValue, err + } + for _, v := range infos { + mValue[v.VideoID] = v.TotalHot + } + return mValue, nil +} + +// GetYesterdayHotValue 获取昨日热度值 +func GetYesterdayHotValue(dt time.Time) (map[primitive.ObjectID]int, error) { + return getHotValueByDate(dt.AddDate(0, 0, -1)) +} + +// GetLastWeekHotValue 获取上个周日热度值 +func GetLastWeekHotValue(dt time.Time) (map[primitive.ObjectID]int, error) { + offset := 0 + wk := dt.Weekday() + switch wk { + case time.Monday: + offset = -1 + case time.Tuesday: + offset = -2 + case time.Wednesday: + offset = -3 + case time.Thursday: + offset = -4 + case time.Friday: + offset = -5 + case time.Saturday: + offset = -6 + } + return getHotValueByDate(dt.AddDate(0, 0, offset)) +} + +// GetLastMonthHotValue 获取上个月末热度值 +func GetLastMonthHotValue(dt time.Time) (map[primitive.ObjectID]int, error) { + return getHotValueByDate(dt.AddDate(0, 0, -dt.Day())) +} + +func DeleteBeforeCreatedAt(t *db.MongoTool, tm time.Time) error { + _, err := coll(t).DeleteMany(bson.M{"createdAt": bson.M{"$lt": tm}}) + return err +} diff --git a/models/s/hotstatmod/type.go b/models/s/hotstatmod/type.go new file mode 100644 index 0000000..5319e10 --- /dev/null +++ b/models/s/hotstatmod/type.go @@ -0,0 +1,27 @@ +package hotstatmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func Init() { + mdb = db.Init(table) + initIndex() +} + +// HotStat 帖子热度统计表 +type HotStat struct { + ID primitive.ObjectID `json:"_id" bson:"_id,omitempty"` + SumDate time.Time `json:"sumDate" bson:"sumDate"` + VideoID primitive.ObjectID `json:"videoID" bson:"videoID"` + TodayHot int `json:"todayHot" bson:"todayHot"` //今日热度 + WeekHot int `json:"weekHot" bson:"weekHot"` //本周热度 + MonthHot int `json:"monthHot" bson:"monthHot"` //本月热度 + TotalHot int `json:"totalHot" bson:"totalHot"` //目前为止总热度 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 +} diff --git a/models/s/kwrankmod/keywordrank.go b/models/s/kwrankmod/keywordrank.go new file mode 100644 index 0000000..f615b2e --- /dev/null +++ b/models/s/kwrankmod/keywordrank.go @@ -0,0 +1,176 @@ +package kwrankmod + +import ( + "fmt" + "math/rand" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.KWRankStat + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "word", Value: 1}}, + }, + { + Keys: bson.D{{Key: "sumDate", Value: -1}}, + }, + { + Keys: bson.D{{Key: "sumDate", Value: -1}, {Key: "word", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "count", Value: -1}}, + }, + { + Keys: bson.D{{Key: "sortKey", Value: -1}}, //大的排前面 + }, + { + Keys: bson.D{{Key: "sumDate", Value: -1}, {Key: "sortKey", Value: -1}, {Key: "count", Value: -1}}, //大的排前面 + }, + { + Keys: bson.D{{Key: "recordAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func Insert(kw Word) error { + now := time.Now() + kw.CreatedAt = now + kw.UpdatedAt = now + if _, err := coll(nil).InsertOne(kw); err != nil { + log.ZapLog.Error(fmt.Sprintf("coll:%s Insert fail error:%+v:", table, err)) + return err + } + return nil +} + +func Update(id ObjectID, doc RankDoc) error { + update, err := common.ToBsonM(doc) + if err != nil { + return fmt.Errorf("coll:%s update ToBsonM fail, error:%+v ", table, err) + } + update["updatedAt"] = time.Now() + if _, err := coll(nil).UpdateOne(M{"_id": id}, M{"$set": update}); err != nil { + log.ZapLog.Error(fmt.Sprintf("coll:%s update fail error:%+v:", table, err)) + return err + } + return nil +} + +// DeleteOne DeleteOne +func Delete(id ObjectID) error { + _, err := coll(nil).DeleteOne(M{"_id": id}) + if err != nil { + log.ZapLog.Error(fmt.Sprintf("coll:%s DeleteOne fail error:%+v:", table, err)) + return err + } + return nil +} + +var rankSort = bson.D{ + {Key: "sumDate", Value: -1}, + {Key: "sortKey", Value: -1}, + {Key: "count", Value: -1}, +} + +var enableIsTrue = true + +// RecentRanking RecentRanking +func RecentRanking(limit int64) ([]Keyword, error) { + kws, err := KwsList(rankSort, 0, limit, (&EnableMatch{&enableIsTrue}).New()) + if err != nil { + log.ZapLog.Error(fmt.Sprintf("coll:%s RecentRanking fail error:%+v:", table, err)) + } + return kws, err +} + +func ChangeStatTrans(trans *db.MongoTool, position time.Time, recordTime time.Time, setList []RankSetDoc) error { + if len(setList) == 0 { + return nil + } + sumDate := timerange.LocDayRange(position).Head + // create the slice of write models + writes := make([]mongo.WriteModel, len(setList)) + for i, doc := range setList { + filter := M{ + "sumDate": sumDate, + "word": doc.Word, + } + update := M{ + "$setOnInsert": M{ + "sumDate": sumDate, + "word": doc.Word, + "enable": true, //首次插入时使能 + "createdAt": time.Now(), + }, + "$set": M{ + "count": doc.Count, //设计为统计表keywordStat聚合word得到 + "sortKey": doc.Count, //控制app端排序 + "recordAt": recordTime, + "updatedAt": time.Now(), + }, + } + writes[i] = mongo.NewUpdateOneModel(). + SetFilter(filter). + SetUpdate(update). + SetUpsert(true) + log.Debug(fmt.Sprintf("sumDate:%s table:%s [ word:%+v filter:%+v update:%+v ]\n", sumDate, table, doc.Word, filter, update)) + } + //bulkWrite 不是原子操作 不具备事务性 + opt := (&options.BulkWriteOptions{}).SetOrdered(false) //设为无序,触发并行写,提升写效率 + _, err := coll(trans).Bulk(writes, opt) + return err +} + +func GetListNewest(limit int64) (WordArray, error) { + sort := bson.D{{Key: "createdAt", Value: -1}} + enable := true + enableMatch := EnableMatch{&enable} + return List(sort, 0, limit, enableMatch.New()) +} + +func GetListRandom(limit int64) (WordArray, error) { + enable := true + enableMatch := EnableMatch{&enable} + count, err := Count(enableMatch.New()) + if err != nil { + return nil, err + } + maxSkip := count - limit + if maxSkip < 0 { + maxSkip = 0 + } + randSkip := rand.Int63n(maxSkip) + sort := bson.D{{Key: "count", Value: -1}} + return List(sort, randSkip, limit) +} + +func DeleteBeforeSumDate(t *db.MongoTool, tm time.Time) error { + _, err := coll(t).DeleteMany(bson.M{"sumDate": bson.M{"$lt": tm}}) + return err +} diff --git a/models/s/kwrankmod/list.go b/models/s/kwrankmod/list.go new file mode 100644 index 0000000..8c5dc45 --- /dev/null +++ b/models/s/kwrankmod/list.go @@ -0,0 +1,78 @@ +package kwrankmod + +import ( + "fmt" + "time" + + "91porn-server/common/log" + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Matcher = pageopt.Matcher + +// SumDateMatch +type SumDateMatch struct { + SumDate *time.Time +} + +func (i *SumDateMatch) New() Matcher { + return pageopt.NewAssignMatch("sumDate", i.SumDate) +} + +// EnableMatch +type EnableMatch struct { + Enable *bool +} + +func (i *EnableMatch) New() Matcher { + return pageopt.NewAssignMatch("enable", i.Enable) +} + +func List(sort bson.D, skip, limit int64, matchers ...Matcher) (WordArray, error) { + filter := pageopt.MergeM(matchers) + opt := &options.FindOptions{} + if len(sort) != 0 { + opt.SetSort(sort) + } + opt.SetSkip(skip) + opt.SetLimit(limit) + wordArray := make(WordArray, 0, limit) + if err := coll(nil).Find(&wordArray, filter, opt); err != nil { + log.ZapLog.Error(fmt.Sprintf("coll:%s List fail error:%+v:", table, err)) + return wordArray, err + } + return wordArray, nil +} + +type Keyword struct { + KeyWord string `bson:"word" json:"keyWord"` //关键字 + Count int64 `bson:"count" json:"count"` //关键字搜索次数 +} + +func KwsList(sort bson.D, skip, limit int64, matchers ...Matcher) ([]Keyword, error) { + filter := pageopt.MergeM(matchers) + opt := &options.FindOptions{} + if len(sort) != 0 { + opt.SetSort(sort) + } + opt.SetSkip(skip) + opt.SetLimit(limit) + opt.SetProjection(M{ + "word": 1, + "count": 1, + }) + list := make([]Keyword, 0, limit) + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.ZapLog.Error(fmt.Sprintf("coll:%s List fail error:%+v:", table, err)) + return list, err + } + return list, nil +} + +func Count(matchers ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchers) + return coll(nil).Count(filter) +} diff --git a/models/s/kwrankmod/type.go b/models/s/kwrankmod/type.go new file mode 100644 index 0000000..eb7ccb0 --- /dev/null +++ b/models/s/kwrankmod/type.go @@ -0,0 +1,66 @@ +package kwrankmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +type M = bson.M + +type Word struct { + ID primitive.ObjectID `bson:"_id,omitempty"` // ID + SumDate time.Time `bson:"sumDate"` + ComplexProportion float64 `bson:"complexProportion"` //综合搜索占比 + VideoProportion float64 `bson:"videoProportion"` //视屏搜索占比 + UserProportion float64 `bson:"userProportion"` //用户搜索占比 + TagProportion float64 `bson:"tagProportion"` //标签(话题)搜索占比 + LocationProportion float64 `bson:"locationProportion"` //地点搜索占比 + Word string `bson:"word"` //词 + Count int64 `bson:"count"` //搜索量 + SortKey int64 `bson:"sortKey"` //排序量 + Enable bool `bson:"enable"` //使能 + RecordAt time.Time `bson:"recordAt"` //数据记录时间 + CreatedAt time.Time `bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `bson:"updatedAt"` // 更新时间 +} + +type RankDoc struct { + Word *string `bson:"word,omitempty"` + SortKey *int64 `bson:"sortKey,omitempty"` + Enable *bool `bson:"enable,omitempty"` +} + +type RankSetDoc struct { + Word *string `bson:"word,omitempty"` + Count *int64 `bson:"count,omitempty"` + ComplexProportion *float64 `bson:"complexProportion"` + VideoProportion *float64 `bson:"videoProportion"` + UserProportion *float64 `bson:"userProportion"` + TagProportion *float64 `bson:"tagProportion"` + LocationProportion *float64 `bson:"locationProportion"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type WordArray []Word + +func (k WordArray) Len() int { + return len(k) +} + +func (k WordArray) Less(i, j int) bool { + return k[i].Count < k[j].Count +} + +func (k WordArray) Swap(i, j int) { + k[i], k[j] = k[j], k[i] +} diff --git a/models/s/kwstatmod/keywordStat.go b/models/s/kwstatmod/keywordStat.go new file mode 100644 index 0000000..3382ca0 --- /dev/null +++ b/models/s/kwstatmod/keywordStat.go @@ -0,0 +1,234 @@ +package kwstatmod + +import ( + "fmt" + "time" + + "91porn-server/common/constant" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models" + + "github.com/jinzhu/now" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.KeywordStat + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "word", Value: 1}}, + }, + { + Keys: bson.D{{Key: "realm", Value: 1}, {Key: "word", Value: 1}}, + }, + { + Keys: bson.D{{Key: "sumDate", Value: -1}, {Key: "realm", Value: 1}}, + }, + { + Keys: bson.D{{Key: "sumDate", Value: -1}, {Key: "realm", Value: 1}, {Key: "word", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "count", Value: -1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func ChangeStatTrans(trans *db.MongoTool, position time.Time, recordTime time.Time, docList []KeywordIncDoc) error { + sumDate := timerange.LocDayRange(position).Head + // create the slice of write models + writes := make([]mongo.WriteModel, len(docList)) + for i, doc := range docList { + filter := M{ + "sumDate": sumDate, + "realm": doc.Realm, + "word": doc.Word, + } + update := M{ + "$setOnInsert": M{ + "sumDate": sumDate, + "realm": doc.Realm, + "word": doc.Word, + "createdAt": time.Now(), + }, + "$set": M{ + "recordAt": recordTime, + "updatedAt": time.Now(), + }, + "$inc": M{ + "count": doc.Count, + }, + } + writes[i] = mongo.NewUpdateOneModel(). + SetFilter(filter). + SetUpdate(update). + SetUpsert(true) + log.Debug(fmt.Sprintf("sumDate:%s table:%s [ realm:%+v word:%+v filter:%+v update:%+v ]\n", sumDate, table, doc.Realm, doc.Word, filter, update)) + } + if len(writes) == 0 { + return nil + } + //bulkWrite 不是原子操作 不具备事务性 + opt := (&options.BulkWriteOptions{}).SetOrdered(false) //设为无序,触发并行写,提升写效率 + _, err := coll(trans).Bulk(writes, opt) + return err +} + +func list(sumDate time.Time, realms []constant.RealmType, enable bool, sort bson.D, skip, limit int64) ([]Keyword, error) { + filter := M{ + "sumDate": sumDate, + "realm": M{"$in": realms}, + } + realmCount := len(realms) + if realmCount == 0 { + return []Keyword{}, nil + } + if realmCount == 1 { //只有一个元素 不使用$in 提升效率 + filter["realm"] = realms[0] + } + opt := (&options.FindOptions{}). + SetSort(sort). + SetSkip(skip). + SetLimit(limit) + keywords := make([]Keyword, 0, limit) + if err := coll(nil).Find(&keywords, filter, opt); err != nil { + log.ZapLog.Error(fmt.Sprintf("coll:%s list fail error:%+v:", table, err)) + return nil, err + } + return keywords, nil +} + +// RealmRanking RealmRanking +func RealmRanking(sumDate time.Time, realm constant.RealmType, skip int64, limit int64) ([]Keyword, error) { + sort := bson.D{ + {Key: "sortKey", Value: -1}, + {Key: "count", Value: -1}, + } + keywords, err := list(sumDate, []constant.RealmType{realm}, true, sort, skip, limit) + if err != nil { + log.ZapLog.Error(fmt.Sprintf("coll:%s RealmRanking fail error:%+v:", table, err)) + return nil, err + } + return keywords, nil +} + +// KeywordCountMapBySumDate +func KeywordCountMapBySumDate(sumDate time.Time) (map[string]int64, error) { + pipeline := []M{ + { + "$match": M{ + "sumDate": sumDate, + }, + }, + { + "$group": M{ + "_id": "$word", + "count": M{"$sum": "$count"}, + }, + }, + { + "$sort": M{ + "count": -1, + }, + }, + } + var list []struct { + Word string `bson:"_id"` + Count int64 `bson:"count"` + } + if err := coll(nil).Aggregate(&list, pipeline); err != nil { + return nil, err + } + m := make(map[string]int64, len(list)) + for _, v := range list { + m[v.Word] = v.Count + } + return m, nil +} + +// HotSearchWordsToday 获取今日搜索较高的关键词 视频关键字 +func HotSearchWordsToday(top int) ([]string, error) { + var kw []Keyword + var words []string + opts := options.FindOptions{} + sort := bson.D{{Key: "count", Value: -1}} + opts.SetSort(sort).SetLimit(int64(top)) + if err := coll(nil).Find(&kw, M{"realm": "video", "sumDate": now.BeginningOfDay()}, &opts); err != nil { + log.Error("keywordStat model HotSearchWordsToday error", log.E(err), log.Any("top", top)) + return words, err + } + words = make([]string, len(kw)) + for i, k := range kw { + words[i] = k.Word + } + return words, nil +} + +func GetKeywordsExcludeWords(sumDate time.Time, realm string, words []string, skip int64, limit int64) (data []Keyword, err error) { + if words == nil { + words = []string{} + } + query := bson.M{"word": bson.M{"$nin": words}} + opts := options.Find() + sort := bson.D{ + {Key: "sortKey", Value: -1}, + {Key: "count", Value: -1}, + } + opts.SetSort(sort).SetSkip(skip).SetLimit(limit) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error("keywordStat model GetKeywordsExcludeWords error", log.E(err), log.Any("sumDate", sumDate), log.Any("words", words)) + return + } + return +} + +// 根据时间获取今日加昨日的关键字 +func GetKeywordsBySumDate(sumDate time.Time, realm string, skip int64, limit int64) (data []Keyword, err error) { + var query = bson.M{"sumDate": bson.M{"$gte": sumDate}, "realm": realm} + var opts = options.Find() + opts.SetSort(bson.D{{Key: "sortKey", Value: -1}, {Key: "count", Value: -1}}).SetSkip(skip).SetLimit(limit) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error("keywordStat model GetKeywordsBySumDate error", log.E(err), log.Any("sumDate", sumDate), log.Any("realm", realm)) + return + } + return +} + +// 根据时间段获取关键字 +func GetKeywordsByTimeRange(start time.Time, end time.Time) (data []Keyword, err error) { + var query = bson.M{"updatedAt": bson.M{"$gte": start, "$lt": end}} + if err = coll(nil).Find(&data, query); err != nil { + log.Error("keywordStat model GetKeywordsByTimeRange error", log.E(err)) + return + } + return +} + +func DeleteBeforeSumDate(t *db.MongoTool, tm time.Time) error { + _, err := coll(t).DeleteMany(bson.M{"sumDate": bson.M{"$lt": tm}}) + return err +} diff --git a/models/s/kwstatmod/type.go b/models/s/kwstatmod/type.go new file mode 100644 index 0000000..8931c49 --- /dev/null +++ b/models/s/kwstatmod/type.go @@ -0,0 +1,51 @@ +package kwstatmod + +import ( + "time" + + "91porn-server/common/constant" + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ( + ObjectID = primitive.ObjectID + + M = bson.M + + KeywordSetDoc = KeywordIncDoc +) +type Keyword struct { + ID primitive.ObjectID `bson:"_id,omitempty"` // ID + SumDate time.Time `bson:"sumDate"` + Realm constant.RealmType `bson:"realm"` + Word string `bson:"word"` + Count int64 `bson:"count"` + RecordAt time.Time `bson:"recordAt"` //数据记录时间 + CreatedAt time.Time `bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `bson:"updatedAt"` // 更新时间 +} + +type ESKeywordSrc struct { + ID primitive.ObjectID `json:"id"` // ID + SumDate time.Time `json:"sumDate"` + Realm constant.RealmType `json:"realm"` + Word string `json:"word"` + Count int64 `json:"count"` + RecordAt time.Time `json:"recordAt"` //数据记录时间 + CreatedAt time.Time `json:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt"` // 更新时间 +} + +type KeywordIncDoc struct { + Realm constant.RealmType `bson:"realm,omitempty"` + Word string `bson:"word,omitempty"` + Count int64 `bson:"count,omitempty"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/s/sectionstatmod/stat.go b/models/s/sectionstatmod/stat.go new file mode 100644 index 0000000..9ed9415 --- /dev/null +++ b/models/s/sectionstatmod/stat.go @@ -0,0 +1,43 @@ +package sectionstatmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// IncSectionHits 增加专题每日点击量 +func IncSectionHits(sectionID primitive.ObjectID, date string) error { + filter := bson.M{"sectionID": sectionID, "date": date} + _, err := coll(nil).UpdateOne(filter, bson.M{"$inc": bson.M{"hitsPerDay": 1}}) + return err +} + +// UpsertSectionHits upsert某一天专题点击量 +func UpsertSectionHits(sectionID primitive.ObjectID, date string) error { + filter := bson.M{"sectionID": sectionID, "date": date} + now := time.Now() + update := bson.M{ + "$set": bson.M{"sectionID": sectionID, "date": date, "updatedAt": now}, + "$setOnInsert": bson.M{"createdAt": now}, + "$inc": bson.M{"hitsPerDay": 1}, + } + _, err := coll(nil).UpsertOne(filter, update) + return err +} + +// ListSectionHits 按页检索专题统计数据 +func ListSectionHits(req ListRequest) (resp ListResponse, err error) { + opts := options.Find().SetSkip(int64(req.PageSize * (req.PageNumber - 1))).SetLimit(int64(req.PageSize + 1)). + SetSort(bson.D{{Key: "date", Value: -1}}) + if err = coll(nil).Find(&resp.List, bson.M{"sectionID": req.SectionID}, opts); err != nil { + return + } + if uint64(len(resp.List)) > req.PageSize { + resp.HasNext = true + resp.List = resp.List[:req.PageSize] + } + return +} diff --git a/models/s/sectionstatmod/type.go b/models/s/sectionstatmod/type.go new file mode 100644 index 0000000..5795eb3 --- /dev/null +++ b/models/s/sectionstatmod/type.go @@ -0,0 +1,63 @@ +package sectionstatmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type SectionStat struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // id + SectionID primitive.ObjectID `json:"sectionID" bson:"sectionID"` // 专题ID + HitsPerday int64 `json:"hitsPerDay" bson:"hitsPerDay"` // 每日点击量 + Date string `json:"date" bson:"date"` // 统计日期 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` // 刷新时间 +} + +var mdb *db.MongoDB + +const table = models.SectionClick + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "sectionID", Value: 1}, {Key: "date", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +type ListRequest struct { + SectionID primitive.ObjectID `json:"sectionID" swaggertype:"string" binding:"required"` // 专题ID + commod.Page +} + +type ListResponse struct { + List []SectionStat // 统计列表 + HasNext bool // 下一页 +} diff --git a/models/s/statordermod/list.go b/models/s/statordermod/list.go new file mode 100644 index 0000000..809f6e9 --- /dev/null +++ b/models/s/statordermod/list.go @@ -0,0 +1,95 @@ +package statordermod + +import ( + "time" + + "91porn-server/common/log" + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/mongo/options" +) + +type ( + Matcher = pageopt.Matcher + + // ChannelMatch + ChannelMatch struct { + Type *string + } +) + +func (t *ChannelMatch) New() Matcher { + return pageopt.NewAssignMatch("channel", t.Type) +} + +// TypeMatch +type TypeMatch struct { + Type *OrderType +} + +func (t *TypeMatch) New() Matcher { + return pageopt.NewAssignMatch("type", t.Type) +} + +// SumDateGTEAndLTMatch +type SumDateGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (s *SumDateGTEAndLTMatch) New() Matcher { + return pageopt.NewGTEAndLTMatch("sumDate", s.GTE, s.LT) +} + +var Sort_SumDate_n1 = Sort{{Key: "sumDate", Value: -1}} + +func List(sort Sort, skip, limit *int64, matchers ...Matcher) ([]OrderState, error) { + filter := pageopt.MergeM(matchers) + opt := (&options.FindOptions{}) + if len(sort) != 0 { + opt.SetSort(sort) + } + if skip != nil { + opt.SetSkip(*skip) + } + if limit != nil { + opt.SetLimit(*limit) + } + list := []OrderState{} + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.Error("statordermod List error", log.E(err)) + return nil, err + } + return list, nil +} + +func Count(matchers ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchers) + count, err := coll(nil).Count(filter) + if err != nil { + log.Error("statordermod Count error", log.E(err)) + return 0, err + } + return count, nil +} + +// Sum +func Sum(matchers ...Matcher) (Items, error) { + filter := pageopt.MergeM(matchers) + pipeline := []M{} + pipeline = append(pipeline, M{"$match": filter}) + pipeline = append(pipeline, M{"$group": M{"_id": nil, + "orderCount": M{"$sum": "$orderCount"}, + "completeOrderCount": M{"$sum": "$completeOrderCount"}, + "orderAmount": M{"$sum": "$orderAmount"}, + "completeOrderAmount": M{"$sum": "$completeOrderAmount"}, + "paidOrderCount": M{"$sum": "$paidOrderCount"}, + "paidAmount": M{"$sum": "$paidAmount"}, + }}) + items := Items{} + if err := coll(nil).AggregateDecode(&items, pipeline); err != nil { + log.Error("statordermod Sum error", log.E(err)) + return Items{}, err + } + return items, nil +} diff --git a/models/s/statordermod/order.go b/models/s/statordermod/order.go new file mode 100644 index 0000000..33ae4c6 --- /dev/null +++ b/models/s/statordermod/order.go @@ -0,0 +1,123 @@ +package statordermod + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.OrderStat + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "sumDate", Value: -1}}, + }, + { + Keys: bson.D{{Key: "channel", Value: 1}, {Key: "type", Value: 1}}, + }, + { + Keys: bson.D{{Key: "sumDate", Value: -1}, {Key: "channel", Value: 1}, {Key: "type", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func ChangeRechargeStatTrans(trans *db.MongoTool, sumDate, recordTime time.Time, stateMap RecOrderStateMap) error { + writes := make([]mongo.WriteModel, len(stateMap)) + i := 0 + for channel, statDoc := range stateMap { + model, err := toMgoModel(sumDate, recordTime, channel, Recharge, statDoc.RechargeOrderStateIncDoc, statDoc.RechargeOrderStateSetDoc) + if err != nil { + return err + } + writes[i] = model + i++ + } + if len(writes) == 0 { + return nil + } + //bulkWrite 不是原子操作 不具备事务性 + opt := (&options.BulkWriteOptions{}).SetOrdered(false) //设为无序,触发并行写,提升写效率 + _, err := coll(trans).Bulk(writes, opt) + return err +} + +func ChangeWithDrawStatTrans(trans *db.MongoTool, sumDate, recordTime time.Time, stateMap WitOrderStateMap) error { + writes := make([]mongo.WriteModel, len(stateMap)) + i := 0 + for channel, statDoc := range stateMap { + model, err := toMgoModel(sumDate, recordTime, channel, WithDraw, statDoc.WithDrawOrderStateIncDoc, statDoc.WithDrawOrderStateSetDoc) + if err != nil { + return err + } + writes[i] = model + i++ + } + if len(writes) == 0 { + return nil + } + //bulkWrite 不是原子操作 不具备事务性 + opt := (&options.BulkWriteOptions{}).SetOrdered(false) //设为无序,触发并行写,提升写效率 + _, err := coll(trans).Bulk(writes, opt) + return err +} + +func toMgoModel(sumDate, recordTime time.Time, channel string, typ OrderType, inc orderStateIncDoc, set orderStateSetDoc) (mongo.WriteModel, error) { + filter := M{ + "sumDate": sumDate, + "channel": channel, + "type": typ, + } + setM, err := common.ToBsonM(set) + if err != nil { + return nil, err + } + setM["recordAt"] = recordTime + setM["updatedAt"] = time.Now() + incM, err := common.ToBsonM(inc) + if err != nil { + return nil, err + } + update := M{ + "$setOnInsert": M{ + "sumDate": sumDate, + "channel": channel, + "type": typ, + "createdAt": time.Now(), + }, + } + if len(incM) != 0 { + update["$inc"] = incM + } + if len(setM) != 0 { + update["$set"] = setM + } + model := mongo.NewUpdateOneModel(). + SetFilter(filter). + SetUpdate(update). + SetUpsert(true) + log.Debug(fmt.Sprintf("sumDate:%s table:%s [ typ:%s channel:%s filter:%+v update:%+v ]\n", sumDate, table, typ.String(), channel, filter, update)) + return model, nil +} diff --git a/models/s/statordermod/type.go b/models/s/statordermod/type.go new file mode 100644 index 0000000..77b7f3e --- /dev/null +++ b/models/s/statordermod/type.go @@ -0,0 +1,90 @@ +package statordermod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ( + OrderType int + + M = bson.M + + Sort = bson.D +) + +const ( + Recharge OrderType = 1 + WithDraw OrderType = 2 +) + +func (o *OrderType) String() string { + switch *o { + case Recharge: + return "recharge" + case WithDraw: + return "withDraw" + default: + return "unknow" + } +} + +type Items struct { + OrderCount int64 `json:"orderCount" bson:"orderCount"` //生产订单数 + CompleteOrderCount int64 `json:"completeOrderCount" bson:"completeOrderCount"` //完成订单数 + OrderAmount int64 `json:"orderAmount" bson:"orderAmount"` //生产订单金额 + CompleteOrderAmount int64 `json:"completeOrderAmount" bson:"completeOrderAmount"` //完成订单的实际支付金额 + PaidOrderCount int64 `json:"paidOrderCount" bson:"paidOrderCount"` //用户已支付金额/三方已出款金额 + PaidAmount int64 `json:"paidAmount" bson:"paidAmount"` //用户已支付订单数/三方已出款订单数 +} + +// OrderState 充值提现统计 +type OrderState struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + SumDate time.Time `json:"sumDate" bson:"sumDate"` + + Items `bson:",inline"` + + Channel string `json:"channel" bson:"channel"` //充值渠道 + Type OrderType `json:"type" bson:"type"` //订单类型, 1,充值;2,提现 + RecordAt time.Time `json:"recordAt" bson:"recordAt"` //数据记录时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //文档创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //文档更新时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type orderStateSetDoc struct { +} + +type orderStateIncDoc struct { + OrderCount *int64 `bson:"orderCount,omitempty"` //订单数 + CompleteOrderCount *int64 `bson:"completeOrderCount,omitempty"` //完成订单数 + OrderAmount *int64 `bson:"orderAmount,omitempty"` //订单金额 + CompleteOrderAmount *int64 `bson:"completeOrderAmount,omitempty"` //实际支付金额 + PaidOrderCount *int64 `bson:"paidOrderCount,omitempty"` //用户已支付订单数/三方已出款订单数 + PaidAmount *int64 `bson:"paidAmount,omitempty"` //用户已支付金额/三方已出款金额 +} + +type RechargeOrderStateIncDoc = orderStateIncDoc +type RechargeOrderStateSetDoc = orderStateSetDoc +type RechargeOrderStateDoc struct { + RechargeOrderStateIncDoc + RechargeOrderStateSetDoc +} +type RecOrderStateMap = map[string]*RechargeOrderStateDoc + +type WithDrawOrderStateIncDoc = orderStateIncDoc +type WithDrawOrderStateSetDoc = orderStateSetDoc +type WithDrawOrderStateDoc struct { + WithDrawOrderStateIncDoc + WithDrawOrderStateSetDoc +} +type WitOrderStateMap = map[string]*WithDrawOrderStateDoc diff --git a/models/s/statrecordmod/statRecord.go b/models/s/statrecordmod/statRecord.go new file mode 100644 index 0000000..532498c --- /dev/null +++ b/models/s/statrecordmod/statRecord.go @@ -0,0 +1,182 @@ +package statrecordmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.StatRecord + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "name", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "job", Value: 1}, {Key: "item", Value: 1}}, + Options: options.Index().SetSparse(true).SetUnique(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// ChangeStatTrans ChangeStatTrans +func UpsertOneTrans(trans *db.MongoTool, name string, recordTime time.Time) error { + filter := bson.M{ + "name": name, + } + update := bson.M{ + "$set": bson.M{ + "recordTime": recordTime, + "updatedAt": time.Now(), + }, + "$setOnInsert": bson.M{ + "name": name, + "fristRecordTime": recordTime, + "createdAt": time.Now(), + }, + } + if _, err := coll(trans).UpsertOne(filter, update); err != nil { + log.Error("statrecordmod UpsertOneTrans error:%v\n", log.E(err)) + return err + } + return nil +} + +type ItemRecordAt struct { + Job string + Item string + RecordTime time.Time +} + +func MergeName(job string, item string) string { + return fmt.Sprintf("%s.%s", job, item) +} + +func UpsertOneItemTrans(trans *db.MongoTool, job string, item string, recordTime time.Time) error { + filter := bson.M{ + "job": job, + "item": item, + } + update := bson.M{ + "$set": bson.M{ + "recordTime": recordTime, + "updatedAt": time.Now(), + }, + "$setOnInsert": bson.M{ + "name": MergeName(job, item), + "job": job, + "item": item, + "fristRecordTime": recordTime, + "createdAt": time.Now(), + }, + } + if _, err := coll(trans).UpsertOne(filter, update); err != nil { + log.Error("statrecordmod UpsertOneItemTrans error:%v\n", log.E(err)) + return err + } + return nil +} + +// FindOne FindOne +func findOneByName(Name string) (Record, error) { + filter := bson.M{ + "name": Name, + } + doc := Record{} + return doc, coll(nil).FindOne(&doc, filter) +} + +// LastRecordTime 最后的记录时间,服务器的时区 +func LastRecordTime(name string, defaultTime time.Time) (time.Time, error) { + //获取上次记录时间 + doc, err := findOneByName(name) + if err != nil { + return time.Time{}, fmt.Errorf("LastRecordTime err:%+v\n", err) + } + //设定为服务器的时区 + var tim = defaultTime + if !doc.RecordTime.IsZero() { + tim = doc.RecordTime + } + //不依赖数据库 保证时间是服务器时间 + localTime := tim.In(time.Local) + log.Debug("LastRecordTime local Info", log.Any("Local", time.Local), log.Any("time", tim), log.Any("returnTime", localTime)) + return localTime, nil +} + +func FristRecordTimeByName(name string) (time.Time, error) { + doc, err := findOneByName(name) + if err != nil { + return time.Time{}, fmt.Errorf("FristRecordTimeByName err:%+v\n", err) + } + return doc.FristRecordTime, nil +} + +func findOneByJob(job string, item string) (Record, error) { + filter := bson.M{ + "job": job, + "item": item, + } + doc := Record{} + return doc, coll(nil).FindOne(&doc, filter) +} + +func FristRecordTimeByJob(job string, item string) (time.Time, error) { + doc, err := findOneByJob(job, item) + if err != nil { + return time.Time{}, fmt.Errorf("FristRecordTimeByName err:%+v\n", err) + } + return doc.FristRecordTime, nil +} + +func findManyByJob(job string) ([]Record, error) { + filter := bson.M{ + "job": job, + } + list := []Record{} + return list, coll(nil).Find(&list, filter) +} + +func LastRecordTimeMap(job string, defaultTimeMap map[string]time.Time) (map[string]time.Time, error) { + list, err := findManyByJob(job) + if err != nil { + return nil, err + } + for _, v := range list { + defaultTimeMap[v.Item] = v.RecordTime + } + return defaultTimeMap, nil +} + +func DeleteStatRecordByName(name string) error { + _, err := coll(nil).DeleteOne(bson.M{"name": name}) + return err +} diff --git a/models/s/statrecordmod/type.go b/models/s/statrecordmod/type.go new file mode 100644 index 0000000..0974857 --- /dev/null +++ b/models/s/statrecordmod/type.go @@ -0,0 +1,84 @@ +package statrecordmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Record struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + + Name string `bson:"name"` //任务名 + + Job string `bson:"job"` //工作 + Item string `bson:"item"` //项目 + + RecordTime time.Time `bson:"recordTime"` //提交时间 + FristRecordTime time.Time `bson:"fristRecordTime"` //第一次提交时间 + CreatedAt time.Time `bson:"createdAt"` //创建时间 + UpdatedAt time.Time `bson:"updatedAt"` //更新时间 +} + +const ( + OneMinuteScale = 1 //对齐时间(分钟) + FiveMinuteScale = 5 //对齐时间(分钟) + FifteenMinuteScale = 15 //对齐时间(分钟) + ThirtyMinuteScale = 30 //对齐时间(分钟) + OneDayScale = 1440 //对齐时间(分钟) +) + +const ( + //stat 基本统计 + DailyStatJob1 = "dailyStatJob1" + ChannelStatJob = "channelStatJob" + VideoStatJob = "videoStatJob" + VideoTotalStatJob = "videoTotalStatJob" + ChannelRetainStatJob = "channelRetainStatJob" + KeywordStatJob = "keywordStatJob" + KeywordRankStatJob = "keywordRankStatJob" + RechargeOrderStatJob = "rechargeOrderStatJob" + WithdrawOrderStatJob = "withdrawOrderStatJob" + TagPlayStatJob = "tagPlayStatJob" + ADSStatJob = "adsStatJob" + TotalJob = "totalJob" + ChTotalJob = "chTotalJob" + InvitStatJob = "InvitStatJob" + DataPointJob = "dataPointJob" + UserStatJob = "userStatJob" + DiscStatJob = "discStatJob" + DiscTotalStatJob = "discTotalStatJob" + Retain1StatJob = "retainStatJob1" + DicsZoomStatJob = "dicsZoomStatJob" + + //act 活动统计,生命周期等于活动时间 + ActInvitCountJob_01_17 = "ActInvitCountJob_01_17" + ActInvitCoinsJob_01_17 = "ActInvitCoinsJob_01_17" + LotteryActID = "LotteryActID" + + // 数据同步 + ElasticSyncVideoJob = "esVideoJob" + ElasticSyncMediaJob = "esMediaJob" + ElasticSyncTagJob = "esTagJob" + ElasticSyncUserJob = "esUserJob" + ElasticSyncKeywordJob = "esKeywordJob" + ElasticSyncSearchLogJob = "esSearchLogJob" + ElasticSyncLoufengJob = "esLouFengJob" + ElasticSyncAudioBookJob = "esAudioBookJob" + SpiderSyncLoufengJob = "spiderLouFengJob" + SpiderSyncfictionJob = "spiderFictionJob" + SpiderSyncAudiobookJob = "spiderAudiobookJob" + SpiderSyncLoufengAgentJob = "spiderSyncLoufengAgentJob" + RedisSyncShortVideoJob = "rdShortVideoJob" + + //sql折叠 + SqlCollasperVidPageView = "sqlCollasperVidPageView" + SqlCollasperUserTotalWork = "sqlCollasperUserTotalWorks" +) + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/s/stattagplaymod/tagPlay.go b/models/s/stattagplaymod/tagPlay.go new file mode 100644 index 0000000..210dec4 --- /dev/null +++ b/models/s/stattagplaymod/tagPlay.go @@ -0,0 +1,90 @@ +package stattagplaymod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.TagPlayStat + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "tagID", Value: 1}, {Key: "videoID", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "playCount", Value: 1}, {Key: "updatedAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func GetTagPlayCount(tagID ObjectID, videoID ObjectID) (TagPlay, error) { + tagPlay := TagPlay{} + if err := coll(nil).FindOne(&tagPlay, bson.M{"tagID": tagID, "videoID": videoID}); err != nil { + log.Error("stat tag play model GetTagPlayCount error", log.E(err), log.Any("tagID", tagID), log.Any("videoID", videoID)) + return tagPlay, err + } + return tagPlay, nil +} + +func ChangeStatTrans(trans *db.MongoTool, incDoc []IncDoc) error { + incDocLen := len(incDoc) + if incDocLen == 0 { + return nil + } + // create the slice of write models + writes := make([]mongo.WriteModel, incDocLen) + for i, tagPlay := range incDoc { + filter := bson.M{ + "tagID": tagPlay.TagID, + "videoID": tagPlay.VideoID, + } + update := bson.M{ + "$setOnInsert": bson.M{ + "tagID": tagPlay.TagID, + "videoID": tagPlay.VideoID, + }, + "$inc": bson.M{ + "playCount": tagPlay.PlayCount, + }, + "$set": bson.M{ + "updatedAt": time.Now(), + }, + } + writes[i] = mongo.NewUpdateOneModel(). + SetFilter(filter). + SetUpdate(update). + SetUpsert(true) + } + //bulkWrite 不是原子操作 不具备事务性 + opt := (&options.BulkWriteOptions{}).SetOrdered(false) //设为无序,触发并行写,提升写效率 + if _, err := coll(trans).Bulk(writes, opt); err != nil { + log.Warn("stat tag play model Bulk error", log.E(err)) + return err + } + return nil +} diff --git a/models/s/stattagplaymod/type.go b/models/s/stattagplaymod/type.go new file mode 100644 index 0000000..641edff --- /dev/null +++ b/models/s/stattagplaymod/type.go @@ -0,0 +1,30 @@ +package stattagplaymod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +type TagPlay struct { + ID ObjectID `bson:"_id,omitempty"` + TagID ObjectID `bson:"tagID"` + VideoID ObjectID `bson:"videoID"` + PlayCount int64 `bson:"playCount"` + UpdatedAt time.Time `bson:"updatedAt"` +} + +type IncDoc struct { + TagID ObjectID `bson:"tagID"` + VideoID ObjectID `bson:"videoID"` + PlayCount int64 `bson:"playCount"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/s/statusermod/list.go b/models/s/statusermod/list.go new file mode 100644 index 0000000..79d3d58 --- /dev/null +++ b/models/s/statusermod/list.go @@ -0,0 +1,31 @@ +package statusermod + +import ( + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Matcher = pageopt.Matcher + +// ItemMatch +type ItemMatch struct { + Item Itemtype +} + +func (t *ItemMatch) New() Matcher { + return pageopt.NewAssignMatch("item", t.Item) +} + +func List(sort bson.D, skip, limit int64, matchers ...Matcher) ([]UserStat, error) { + filter := pageopt.MergeM(matchers) + opt := &options.FindOptions{} + if len(sort) != 0 { + opt.SetSort(sort) + } + opt.SetSkip(skip) + opt.SetLimit(limit) + list := make([]UserStat, 0, limit) + return list, coll(nil).Find(&list, filter, opt) +} diff --git a/models/s/statusermod/type.go b/models/s/statusermod/type.go new file mode 100644 index 0000000..6e5f43a --- /dev/null +++ b/models/s/statusermod/type.go @@ -0,0 +1,47 @@ +package statusermod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ( + M = bson.M + + D = bson.D + + Itemtype string +) + +const ( + VidIncome Itemtype = "vidIncome" //视频收益 + UploadCount Itemtype = "uploadCount" //上传数量 +) + +type UserStat struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + UID uint64 `bson:"uid"` + Item Itemtype `bson:"item"` + Count int64 `bson:"count"` + RecordAt time.Time `bson:"recordAt"` //数据记录时间 + UpdatedAt time.Time `bson:"updatedAt"` //文档更新时间 + CreatedAt time.Time `bson:"createdAt"` //文档创建时间 +} + +type SetDoc struct { + UID uint64 `bson:"uid"` + Item Itemtype `bson:"item"` + Count int64 `bson:"count"` + RecordAt time.Time `bson:"recordAt"` //数据记录时间 +} + +type IncDoc SetDoc + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/s/statusermod/userstat.go b/models/s/statusermod/userstat.go new file mode 100644 index 0000000..b1c16ca --- /dev/null +++ b/models/s/statusermod/userstat.go @@ -0,0 +1,141 @@ +package statusermod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.UserStat + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "item", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "uid", Value: -1}}, + }, + { + Keys: bson.D{{Key: "item", Value: -1}}, + }, + { + Keys: bson.D{{Key: "count", Value: -1}}, + }, + { + Keys: bson.D{{Key: "recordAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func ChangeIncStatTrans(trans *db.MongoTool, incDocList []IncDoc) error { + // create the slice of write models + writes := make([]mongo.WriteModel, len(incDocList)) + for i, incDoc := range incDocList { + filter := M{ + "uid": incDoc.UID, + "item": incDoc.Item, + } + update := M{ + "$setOnInsert": M{ + "uid": incDoc.UID, + "item": incDoc.Item, + "createdAt": time.Now(), + }, + "$inc": M{ + "count": incDoc.Count, //设计为统计表keywordStat聚合word得到 + }, + "$set": M{ + "updatedAt": time.Now(), + "recordAt": incDoc.RecordAt, + }, + } + writes[i] = mongo.NewUpdateOneModel(). + SetFilter(filter). + SetUpdate(update). + SetUpsert(true) + log.Debug(fmt.Sprintf("table:%s [ filter:%+v update:%+v ]\n", table, filter, update)) + } + if len(writes) == 0 { + return nil + } + //bulkWrite 不是原子操作 不具备事务性 + opt := (&options.BulkWriteOptions{}).SetOrdered(false) //设为无序,触发并行写,提升写效率 + _, err := coll(trans).Bulk(writes, opt) + return err +} + +func ChangeSetStatTrans(trans *db.MongoTool, setDocList []SetDoc) error { + // create the slice of write models + writes := make([]mongo.WriteModel, len(setDocList)) + for i, setDoc := range setDocList { + filter := M{ + "uid": setDoc.UID, + "item": setDoc.Item, + } + update := M{ + "$setOnInsert": M{ + "uid": setDoc.UID, + "item": setDoc.Item, + "createdAt": time.Now(), + }, + "$set": M{ + "count": setDoc.Count, + "updatedAt": time.Now(), + "recordAt": setDoc.RecordAt, + }, + } + + writes[i] = mongo.NewUpdateOneModel(). + SetFilter(filter). + SetUpdate(update). + SetUpsert(true) + log.Debug(fmt.Sprintf("table:%s [ filter:%+v update:%+v ]\n", table, filter, update)) + } + if len(writes) == 0 { + return nil + } + //bulkWrite 不是原子操作 不具备事务性 + opt := (&options.BulkWriteOptions{}).SetOrdered(false) //设为无序,触发并行写,提升写效率 + _, err := coll(trans).Bulk(writes, opt) + return err +} + +func ListByVidIncome(limit int64) ([]UserStat, error) { + sort := bson.D{{Key: "count", Value: -1}} + itemMatch := ItemMatch{VidIncome} + return List(sort, 0, limit, itemMatch.New()) +} + +func ListByUploadCount(limit int64) ([]UserStat, error) { + sort := bson.D{{Key: "count", Value: -1}} + itemMatch := ItemMatch{UploadCount} + return List(sort, 0, limit, itemMatch.New()) +} diff --git a/models/s/statvidmod/list.go b/models/s/statvidmod/list.go new file mode 100644 index 0000000..20b6299 --- /dev/null +++ b/models/s/statvidmod/list.go @@ -0,0 +1,93 @@ +package statvidmod + +import ( + "time" + + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Matcher = pageopt.Matcher + +// SumDateMatch +type SumDateMatch struct { + SumDate *time.Time +} + +func (s *SumDateMatch) New() Matcher { + return pageopt.NewAssignMatch("sumDate", s.SumDate) +} + +// SumDateGTEAndLTMatch +type SumDateGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (s *SumDateGTEAndLTMatch) New() Matcher { + return pageopt.NewGTEAndLTMatch("sumDate", s.GTE, s.LT) +} + +// PublisherIDMatch +type PublisherIDMatch struct { + UID *uint64 +} + +func (p *PublisherIDMatch) New() Matcher { + return pageopt.NewAssignMatch("publisherID", p.UID) +} + +// PayCountGTMatch +type PayCountGTMatch struct { + GT *int64 +} + +func (p *PayCountGTMatch) New() Matcher { + return pageopt.NewGTMatch("payCount", p.GT) +} + +// PlayCountGTMatch +type PlayCountGTMatch struct { + GT *int64 +} + +func (p *PlayCountGTMatch) New() Matcher { + return pageopt.NewGTMatch("playCount", p.GT) +} + +// VidMatch +type VidMatch struct { + Vid *ObjectID +} + +func (p *VidMatch) New() Matcher { + return pageopt.NewAssignMatch("vid", p.Vid) +} + +// VidInMatch +type VidInMatch struct { + Vids []ObjectID +} + +func (p *VidInMatch) New() Matcher { + return pageopt.NewInMatch("vid", p.Vids) +} + +func List(sort bson.D, skip, limit int64, matchs ...Matcher) ([]Stat, error) { + opt := &options.FindOptions{} + if len(sort) != 0 { + opt.SetSort(sort) + } + opt.SetSkip(skip) + opt.SetLimit(limit) + filter := pageopt.MergeM(matchs) + list := make([]Stat, 0, limit) + return list, coll(nil).Find(&list, filter, opt) +} + +func Count(matchs ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchs) + return coll(nil).Count(filter) +} diff --git a/models/s/statvidmod/page.go b/models/s/statvidmod/page.go new file mode 100644 index 0000000..b4cb990 --- /dev/null +++ b/models/s/statvidmod/page.go @@ -0,0 +1,128 @@ +package statvidmod + +import ( + "fmt" + "time" + + "91porn-server/common/log" + "91porn-server/common/maths" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "91porn-server/web/vidhelp" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Video struct { + ID *primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title *string `json:"title" bson:"title,omitempty"` //视频标题 + PublisherID *uint64 `json:"publisherID" bson:"publisherID,omitempty"` //上传者ID + PublisherName *string `json:"publisherName"` //上传者姓名 + Day *float64 `json:"day"` //售卖天数 + Coins *int64 `json:"coins" bson:"coins,omitempty"` //定价 +} + +type VideoStatRecord struct { + SumDate *time.Time `json:"sumDate" bson:"sumDate,omitempty"` + Video Video `json:"video" bson:"video"` //视屏 + Income *int64 `json:"income" bson:"income,omitempty"` //当日收入 + Tax *float64 `json:"tax" bson:"tax,omitempty"` //税 + PayCount *int64 `json:"payCount" bson:"payCount,omitempty"` //购买数 + CreatedAt *time.Time `json:"createdAt" bson:"createdAt,omitempty"` //日志创建时间 +} + +type VideoStatExport struct { + SumDate time.Time `json:"sumDate" bson:"sumDate,omitempty"` + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title,omitempty"` //视频标题 + PublisherID uint64 `json:"publisherID" bson:"publisherID,omitempty"` //上传者ID + Coins int64 `json:"coins" bson:"coins,omitempty"` //定价 + Income int64 `json:"income" bson:"income,omitempty"` //当日收入 + Tax float64 `json:"tax" bson:"tax,omitempty"` //税 + PayCount int64 `json:"payCount" bson:"payCount,omitempty"` //购买数 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //日志创建时间 +} + +type VideoStatPage struct { + Total int64 `json:"total" bson:"total"` + List []VideoStatRecord `json:"list" bson:"list"` +} + +var payCountMin int64 = 0 + +// VideoStatPages +func VideoStatPages(skip, limit int64, + sumDateGTEAndLTMatch SumDateGTEAndLTMatch, + vidMatch VidMatch, + vidInMatch VidInMatch, +) (VideoStatPage, error) { + matList := []Matcher{ + (&PayCountGTMatch{&payCountMin}).New(), + sumDateGTEAndLTMatch.New(), + vidMatch.New(), + vidInMatch.New(), + } + sort := bson.D{{Key: "sumDate", Value: -1}} + statList, err := List(sort, skip, limit, matList...) + if err != nil { + return VideoStatPage{}, err + } + vidList := make([]ObjectID, len(statList)) + for i, v := range statList { + vidList[i] = v.Vid + } + videoMap, err := vidmod.VideoMap(vidList) + if err != nil { + return VideoStatPage{}, err + } + //用户id + statListLen := len(statList) + userIds := make([]uint64, 0, statListLen) + recordList := make([]VideoStatRecord, statListLen) + for i, stat := range statList { + _stat := stat + vid := _stat.Vid + videoInfo := Video{ + ID: &vid, + } + if video, ok := videoMap[vid]; ok { + videoInfo.Title = &video.Title + videoInfo.PublisherID = &video.PublisherID + videoInfo.Coins = &video.Coins + d := vidhelp.CalculateVideoSellDays(video.ReviewAt) + videoInfo.Day = &d + userIds = append(userIds, video.PublisherID) + } + tax := maths.ToFloat64_b2(_stat.Tax) + recordList[i] = VideoStatRecord{ + SumDate: &_stat.SumDate, + Video: videoInfo, + Income: &_stat.Income, + Tax: &tax, + PayCount: &_stat.PayCount, + CreatedAt: &_stat.CreatedAt, + } + } + //查询用户信息 + userMap, err := usermod.UserMap(userIds) + if err == nil { + for _, r := range recordList { + if r.Video.PublisherID == nil { + log.Warn(fmt.Sprintf("视频[%v]信息异常==>缺少上传者ID", r.Video.ID)) + continue + } + if u, ok := userMap[*r.Video.PublisherID]; ok { + r.Video.PublisherName = &u.Name + } + } + } + count, err := Count(matList...) + if err != nil { + return VideoStatPage{}, err + } + return VideoStatPage{ + Total: count, + List: recordList, + }, nil +} diff --git a/models/s/statvidmod/stat.go b/models/s/statvidmod/stat.go new file mode 100644 index 0000000..f7415ee --- /dev/null +++ b/models/s/statvidmod/stat.go @@ -0,0 +1,134 @@ +package statvidmod + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.VideoStat + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "vid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "sumDate", Value: -1}, {Key: "vid", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "playCount", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "recordAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "sumDate", Value: -1}, {Key: "playCount", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func ChangeStatTrans(trans *db.MongoTool, sumDate time.Time, recordTime time.Time, statDocMap VideoStatDocMap) error { + writes := make([]mongo.WriteModel, len(statDocMap)) + i := 0 + for vid, statDoc := range statDocMap { + filter := M{ + "sumDate": sumDate, + "vid": vid, + } + setM, err := common.ToBsonM(statDoc.VideoStatSetDoc) + if err != nil { + return err + } + setM["recordAt"] = recordTime + setM["updatedAt"] = time.Now() + incM, err := common.ToBsonM(statDoc.VideoStatIncDoc) + if err != nil { + return err + } + update := M{ + "$setOnInsert": M{ + "sumDate": sumDate, + "vid": vid, + "createdAt": time.Now(), + }, + } + if len(incM) != 0 { + update["$inc"] = incM + } + if len(setM) != 0 { + update["$set"] = setM + } + writes[i] = mongo.NewUpdateOneModel(). + SetFilter(filter). + SetUpdate(update). + SetUpsert(true) + i++ + log.Debug(fmt.Sprintf("sumDate:%s table:%s [ vid:%+v filter:%+v update:%+v ]\n", sumDate, table, vid, filter, update)) + } + if len(writes) == 0 { + return nil + } + //bulkWrite 不是原子操作 不具备事务性 + opt := (&options.BulkWriteOptions{}).SetOrdered(false) //设为无序,触发并行写,提升写效率 + _, err := coll(trans).Bulk(writes, opt) + return err +} + +// GetDailyPlayCountGroup +// @return vid List, vid->dailyPlayCount Map, error +func GetDailyPlayCountGroup(skip, limit int64) ([]ObjectID, map[ObjectID]int64, error) { + playCountMin := int64(0) + playCountGTMatch := PlayCountGTMatch{&playCountMin} + sort := D{{Key: "sumDate", Value: -1}, {Key: "playCount", Value: -1}} + list, err := List(sort, skip, limit, playCountGTMatch.New()) + if err != nil { + return nil, nil, err + } + listLen := len(list) + vidList := make([]ObjectID, listLen) + countMap := make(map[ObjectID]int64, listLen) + for i, v := range list { + vidList[i] = v.Vid + countMap[v.Vid] = v.PlayCount + } + return vidList, countMap, nil +} + +func SumByRecordAt(match RecordAtGTEAndLTMatch) ([]SumGroup, error) { + return Sum(match.New()) +} + +func DeleteBeforeCreatedAt(t *db.MongoTool, tm time.Time) error { + _, err := coll(t).DeleteMany(bson.M{"createdAt": bson.M{"$lt": tm}}) + return err +} diff --git a/models/s/statvidmod/sum.go b/models/s/statvidmod/sum.go new file mode 100644 index 0000000..698eca4 --- /dev/null +++ b/models/s/statvidmod/sum.go @@ -0,0 +1,44 @@ +package statvidmod + +import ( + "time" + + "91porn-server/common/pageopt" +) + +type SumGroup struct { + Vid ObjectID `bson:"_id"` + Income int64 `bson:"income"` //收入 + PayCount int64 `bson:"payCount"` //购买次数 + PlayCount int64 `bson:"playCount"` //播放次数 + PayPlayCount int64 `bson:"payPlayCount"` //付费播放次数 + Tax float64 `bson:"tax"` //[未完成]税 +} + +// PlayCountGTMatch +type RecordAtGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (r *RecordAtGTEAndLTMatch) New() Matcher { + return pageopt.NewGTEAndLTMatch("recordAt", r.GTE, r.LT) +} + +func Sum(matchs ...Matcher) ([]SumGroup, error) { + filter := pageopt.MergeM(matchs) + pipeline := []M{} + pipeline = append(pipeline, M{"$match": filter}) + pipeline = append(pipeline, M{ + "$group": M{ + "_id": "$vid", + "income": M{"$sum": "$income"}, + "payCount": M{"$sum": "$payCount"}, + "playCount": M{"$sum": "$playCount"}, + "payPlayCount": M{"$sum": "$payPlayCount"}, + "tax": M{"$sum": "$tax"}, + }, + }) + list := []SumGroup{} + return list, coll(nil).Aggregate(&list, pipeline) +} diff --git a/models/s/statvidmod/type.go b/models/s/statvidmod/type.go new file mode 100644 index 0000000..f78a1e6 --- /dev/null +++ b/models/s/statvidmod/type.go @@ -0,0 +1,58 @@ +package statvidmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ( + M = bson.M + + D = bson.D + + ObjectID = primitive.ObjectID + + Stat struct { + ID ObjectID `bson:"_id,omitempty"` + SumDate time.Time `bson:"sumDate"` + Vid ObjectID `bson:"vid"` + + Income int64 `bson:"income"` //收入 + PayCount int64 `bson:"payCount"` //购买次数 + PlayCount int64 `bson:"playCount"` //播放次数 + PayPlayCount int64 `bson:"payPlayCount"` //付费播放次数 + Tax float64 `bson:"tax"` //税 + + RecordAt time.Time `bson:"recordAt"` //数据记录时间 + CreatedAt time.Time `bson:"createdAt"` + UpdatedAt time.Time `bson:"updatedAt"` + } + + VideoStatIncDoc struct { + Income *int64 `bson:"income,omitempty"` //收入 + PayCount *int64 `bson:"payCount,omitempty"` //购买次数 + PlayCount *int64 `bson:"playCount,omitempty"` //播放次数 + PayPlayCount *int64 `bson:"payPlayCount,omitempty"` //付费播放次数 + Tax *float64 `bson:"tax,omitempty"` //税 + } + + VideoStatSetDoc struct { + //**** + } + + VideoStatDoc struct { + VideoStatIncDoc `bson:",inline"` + VideoStatSetDoc `bson:",inline"` + } + + VideoStatDocMap = map[ObjectID]*VideoStatDoc +) + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/s/statvidtotalmod/stat.go b/models/s/statvidtotalmod/stat.go new file mode 100644 index 0000000..44314bb --- /dev/null +++ b/models/s/statvidtotalmod/stat.go @@ -0,0 +1,117 @@ +package statvidtotalmod + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.VideoTotalStat + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "vid", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "playCount", Value: -1}}, + }, + { + Keys: bson.D{{Key: "payCount", Value: -1}}, + }, + { + Keys: bson.D{{Key: "payPlayCount", Value: -1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "recordAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func ChangeStatTrans(trans *db.MongoTool, recordAt time.Time, incDocMap IncDocMap) error { + writes := make([]mongo.WriteModel, len(incDocMap)) + i := 0 + for vid, doc := range incDocMap { + filter := M{ + "vid": vid, + } + incM, err := common.ToBsonM(doc) + if err != nil { + return err + } + update := M{ + "$setOnInsert": M{ + "vid": vid, + "createdAt": time.Now(), + }, + "$set": M{ + "updatedAt": time.Now(), + "recordAt": recordAt, + }, + } + if len(incM) != 0 { + update["$inc"] = incM + } + writes[i] = mongo.NewUpdateOneModel(). + SetFilter(filter). + SetUpdate(update). + SetUpsert(true) + i++ + log.Debug(fmt.Sprintf("table:%s [ vid:%+v filter:%+v update:%+v ]\n", table, vid, filter, update)) + } + if len(writes) == 0 { + return nil + } + //bulkWrite 不是原子操作 不具备事务性 + opt := (&options.BulkWriteOptions{}).SetOrdered(false) //设为无序,触发并行写,提升写效率 + _, err := coll(trans).Bulk(writes, opt) + return err +} + +func GetPayCountMap(vidList []ObjectID) (map[ObjectID]int64, error) { + if len(vidList) == 0 { + return make(map[ObjectID]int64), nil + } + filter := M{ + "vid": M{"$in": vidList}, + } + list := []struct { + Vid ObjectID `bson:"vid"` + PayCount int64 `bson:"payCount"` //购买次数 + }{} + if err := coll(nil).Find(&list, filter); err != nil { + return make(map[ObjectID]int64), nil + } + m := make(map[ObjectID]int64, len(list)) + for _, v := range list { + m[v.Vid] = v.PayCount + } + return m, nil +} diff --git a/models/s/statvidtotalmod/type.go b/models/s/statvidtotalmod/type.go new file mode 100644 index 0000000..9fa05c2 --- /dev/null +++ b/models/s/statvidtotalmod/type.go @@ -0,0 +1,48 @@ +package statvidtotalmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ( + M = bson.M + + D = bson.D + + ObjectID = primitive.ObjectID + + Stat struct { + ID ObjectID `bson:"_id,omitempty"` + Vid ObjectID `bson:"vid"` + + Income int64 `bson:"income"` //收入 + PayCount int64 `bson:"payCount"` //购买次数 + PlayCount int64 `bson:"playCount"` //播放次数 + PayPlayCount int64 `bson:"payPlayCount"` //付费播放次数 + Tax float64 `bson:"tax"` //[未完成]税 + + RecordAt time.Time `bson:"recordAt"` + CreatedAt time.Time `bson:"createdAt"` + UpdatedAt time.Time `bson:"updatedAt"` + } + + IncDoc struct { + Income int64 `bson:"income"` //收入 + PayCount int64 `bson:"payCount"` //购买次数 + PlayCount int64 `bson:"playCount"` //播放次数 + PayPlayCount int64 `bson:"payPlayCount"` //付费播放次数 + Tax float64 `bson:"tax"` //[未完成]税 + } + + IncDocMap = map[ObjectID]*IncDoc +) + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/s/useradverstatmod/aggregate.go b/models/s/useradverstatmod/aggregate.go new file mode 100644 index 0000000..b8e5a0e --- /dev/null +++ b/models/s/useradverstatmod/aggregate.go @@ -0,0 +1,63 @@ +package useradverstatmod + +import ( + "91porn-server/common/log" + "91porn-server/models/commod" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type DailyRetentionSummary struct { + ID DailyRetentionSummaryID `bson:"_id"` + Retained int64 `bson:"retained"` + AdClick int64 `bson:"adClick"` + TotalClick int64 `bson:"totalClick"` + PayCount int64 `bson:"payCount"` + PayTotal int64 `bson:"payTotal"` +} + +type DailyRetentionSummaryID struct { + RegDay time.Time `bson:"regDay"` + AdGroup commod.AdGroup `bson:"adGroup"` +} + +func AggregateDailyRetention(date, regDayStart, regDayEnd time.Time) (out []*DailyRetentionSummary, err error) { + pipeline := []bson.M{ + { + "$match": bson.M{ + "date": date, + "regDay": bson.M{ + "$gte": regDayStart, + "$lte": regDayEnd, + }, + }, + }, + { + "$group": bson.M{ + "_id": bson.M{ + "regDay": "$regDay", + "adGroup": "$adGroup", + }, + "retained": bson.M{"$sum": 1}, + "adClick": bson.M{"$sum": "$adClick"}, + "totalClick": bson.M{"$sum": "$totalClick"}, + "payCount": bson.M{"$sum": "$payCount"}, + "payTotal": bson.M{"$sum": "$payTotal"}, + }, + }, + } + + opts := options.Aggregate().SetAllowDiskUse(true) + if err = coll(nil).Aggregate(&out, pipeline, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AggregateDailyRetention", table, "Aggregate", err), + log.Any("date", date), + log.Any("regDayStart", regDayStart), + log.Any("regDayEnd", regDayEnd), + ) + return nil, err + } + return +} diff --git a/models/s/useradverstatmod/type.go b/models/s/useradverstatmod/type.go new file mode 100644 index 0000000..0f8f2b2 --- /dev/null +++ b/models/s/useradverstatmod/type.go @@ -0,0 +1,29 @@ +package useradverstatmod + +import ( + "91porn-server/common/db" + "91porn-server/models/commod" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +type UserAdStat struct { + Id primitive.ObjectID `json:"id" bson:"_id"` // 自增ID + UserId int64 `json:"userId" bson:"userId" index:"user_idx,user_date_idx:unique"` // 用户Id + Date time.Time `json:"date" bson:"date" index:"-user_idx,user_date_idx:unique"` // 日期(当日0点) + DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码(只统计有渠道码的) + SysType string `json:"sysType" bson:"sysType"` // 系统类型(目前只统计安卓) + AdGroup commod.AdGroup `json:"adGroup" bson:"adGroup" index:"ad_group"` // 广告组 + AdClick int64 `json:"adClick" bson:"adClick"` // 广告点击 + TotalClick int64 `json:"totalClick" bson:"totalClick"` // 广告+APP总点击 + AppClick int64 `json:"appClick" bson:"appClick"` // APP点击 + PayCount int64 `json:"payCount" bson:"payCount"` // 充值次数 + PayTotal int64 `json:"payTotal" bson:"payTotal"` // 充值总金额 + RegDay time.Time `json:"regDay" bson:"regDay" index:"-reg_day_idx"` // 注册日期(用户注册当天0点) + Version string `json:"version" bson:"version"` // 版本号 + CreatedAt time.Time `json:"createdAt" bson:"createdAt" index:"-created_at_idx"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 +} diff --git a/models/s/useradverstatmod/useradverstat.go b/models/s/useradverstatmod/useradverstat.go new file mode 100644 index 0000000..5b22d5c --- /dev/null +++ b/models/s/useradverstatmod/useradverstat.go @@ -0,0 +1,94 @@ +package useradverstatmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.UserAdverStat + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{ + {Key: "adGroup", Value: 1}, + }, + }, + { + Keys: bson.D{ + {Key: "date", Value: -1}, + }, + }, + { + Keys: bson.D{ + {Key: "regDay", Value: -1}, + }, + }, + { + Keys: bson.D{ + {Key: "date", Value: 1}, + {Key: "regDay", Value: 1}, + {Key: "adGroup", Value: 1}, + }, + }, + { + Keys: bson.D{ + {Key: "createdAt", Value: -1}, + }, + }, + { + Keys: bson.D{ + {Key: "userId", Value: 1}, + {Key: "date", Value: -1}, + }, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +// QueryAllList 分页查询文档 +func QueryAllList(filter primitive.M, opts ...*options.FindOptions) (out []*UserAdStat, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// UpsertOne +func UpsertOne(filter bson.M, updateData bson.M) (err error) { + if _, err = coll(nil).UpsertOne(filter, updateData); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncrCommentCountByID", table, "UpdateOne", err), + log.Any("filter", filter), + log.Any("updateData", updateData), + ) + return + } + return +} diff --git a/models/table.go b/models/table.go new file mode 100644 index 0000000..6e6781d --- /dev/null +++ b/models/table.go @@ -0,0 +1,274 @@ +package models + +const ( + VideoDb = "DataBase:Video" // 数据库对象名 主数据库 存放强业务相关的数据 + LogDb = "DataBase:Log" // 日志数据库 + LogReadDb = "Read:DataBase:Log" // 只读日志数据 前缀Read 不可缺少 否则默认选择为主库(primary) + StatDb = "DataBase:Stat" // 统计数据库 存放统计 渠道相关的数据 + + UserTable = "user" // 用户 + IDTable = "auto_id" // 自增id表 + Setting = "setting" // 用户设置 + Wallet = "wallet" // 钱包 + FundTransferLog = "fund_transfer_log" // AI女友V2上下分记录 + Activity = "activity" // 活动表 + ActivityPrize = "activity_prize" // 活动奖品表 + ActivityPrizePool = "activity_prize_pool" // 活动奖池表 + ActivityUserFree = "activity_user_free" // 活动用户免费次数记录表 + ActivityUserRecord = "activity_user_record" // 活动用户抽奖记录表 + ActivityModels = "activity_models" + ActivityGitStock = "activity_gift_stock" + ActivitySoldRecord = "activity_sold_record" + ActivityWinRecord = "activity_win_record" + ActivityJoinPersons = "activity_join_persons" + FaqTable = "user_faq" // 用户常见问题表 + FeedBack = "user_feedback" // 用户反馈意见表 + WordsFilter = "filter" // 敏感词表 + Tag = "tag" // 标签表 + UserTag = "user_tag" // 用户标签表 + VideoInfo = "video_info" // 视频信息表 + HevcTask = "hevc_task" // H.265 云转码任务记录 + SearchLog = "search_log" // 搜索日志 + Report = "report" // 举报 + ReportTypes = "report_types" // 举报种类 + Admin = "admin" // 管理员 + StatRecord = "stat_record" // 统计记录表 + VideoStat = "video_stat" // 视屏统计表 + VideoTotalStat = "video_total_stat" // 视屏总量统计表 + TagPlayStat = "tag_play_stat" // 标签播放次数统计表 + Source = "source" // 资源管理 + Tone = "tone" // 音色最热 + ToneRecom = "tone_recom" // 官方推荐 + UserAct = "user_act" // 用户行为记录表 + VisitLog = "visit_log" // 用户访问记录表 + Product = "product" // vip商品表 + ProductBenefit = "product_benefit" // vip商品权益 + ProductHistory = "product_history" // vip商品购买记录 + ProductPosition = "product_position" // 商品位置 + Invitation = "invitation" // 推广关系绑定 + ProxyIncomeLog = "proxy_income_log" // 推广收益记录 + InviteLog = "invite_log" // 邀请动作记录表 + WithdrawChannel = "withdraw_channel" // 提现渠道配置 + WithdrawTariff = "withdraw_tariff" // 提现税率 + WithdrawOrder = "withdraw_order" // 提现订单 + RechargeOrder = "recharge_order" // 充值订单 + RejectTemplate = "reject_template" // 审核拒绝模版 + Transaction = "transaction" // 收支金币流水 + PayChannel = "pay_channel" // 支付渠道 + Gold = "gold" // 充值金额配置 + GoldConf = "gold_config" // 金币配置表 + GameGold = "gameGold" // 游戏充值金额配置 + LoginLog = "login_log" // 设备登陆,手机登陆日志 + PlayLog = "play_log" // 播放日志 + Ads = "ads" // 广告配置表 + AdReview = "ad_review" // 广告审核 + Annou = "annou" // 公告 + Announce = "announce" // 会员中心跑马灯 + Follow = "user_follow" // 关注表 + HotCity = "hot_city" // 热门城市 + Location = "location" // 城市信息 + PayVideoLog = "pay_video_log" // 视频支付日志 + RewardVideoLog = "reward_video_log" // 视频打赏日志 + Authority = "authority" // 权限 + Role = "role" // 角色表 + PushModel = "video_push" // 官方推荐视频列表 + PullLog = "pull_log" // 视频同步记录 + WelfareLog = "welfare" // 福利添加记录 + Version = "version" // 系统版本 + KeywordStat = "key_word_stat" // 关键字统计 + KWRankStat = "kw_rank_stat" // 关键排行统计 + SectionClick = "section_click" // 专题点击统计表 + TransactionAct = "transaction_act" // 兑换账户 + OrderStat = "order_stat" // 订单统计 + Msg = "message" // 消息表 + SyncCdnLog = "sync_cnd_log" // 同步CDN记录表 + WebOperatorLog = "web_operator_log" // 后台操作日志 + UserRecoRecord = "user_reco_record" // 用户推荐记录 + IpBlock = "ip_block" // ip黑名单 + IpWhite = "ip_white" // ip白名单 + OperationLog = "operation_log" // 帖子、用户列表操作日志记录 + UserStat = "user_stat" // 用户数据统计 + NoticeFmt = "notice_fmt" // 通知样板 + NoticeRecd = "notice_recd" // 通知记录 + UserReco = "user_reco" // 用户推荐配置 + NewRegister = "user_register_log" // 新注册用户接口调用记录表 + ExchCode = "exch_code" // 兑换码 + ExchLog = "exch_log" // 兑换码兑换记录 + ChatRobot = "chat_robot" // 聊天机器人配置 + ContentLibrary = "content_library" // 聊天机器人内容库 + Fsid2UID = "fsid_uid" // 文件服发布者id映射uid + NewsHot = "news_hot" // 帖子热度统计 + RecmdTag = "recmd_tag" // 热词 + FreeVidConfig = "free_vid_config" // 免费观看视频配置列表 + LDYConfig = "ldy_config" // 落地也配置 + Questionnaire = "questionnaire" // 调查问卷 + LotteryLog = "lottery_log" // 活动抽奖号记录 + VerifyReport = "verify_report" // 验证报告 + Fiction = "fiction" // 小说 + AudioBook = "audio_book" // 有声小说 + AudioAnchor = "audio_anchor" // 有声小说作者 + ErrFeedback = "err_feedback" // 错误反馈 + PayAudioBook = "pay_audiobook_log" // 有声小说购买记录 + VipConfig = "vip_config" // 楼风配置 + VideoPopularityConfig = "vid_pop_config" // 视频热度配置 + Task = "task" // 任务 + TaskConfig = "task_config" // 成长任务 + Backpack = "backpack" // 用户背包 + Currency = "currency" // 货币信息 + SystemConfig = "system_config" // 系统配置 + VideoDiscount = "video_discount" // 视频折扣 + ModuleConf = "module_conf" // 模块配置 + Section = "section" // 模块下专题配置 + SectionVideo = "section_video" // 模块下视频 + TagConf = "tag_conf" // 标签配置 + VideoCoupon = "video_coupon" // 购买VIP赠送的观影券 + ModuleMarquee = "module_marquee" // 跑马灯 + VideoActivity = "video_activity" // 活动视频 + UserWatchRecord = "user_watch_record" // 用户观看记录 + ImageTop = "image_top" // 帖子置顶 + Active2023User = "active_2023_user" // 用户2023抽奖活动基本信息 + Active2023Lottery = "active_2023_lottery" // 用户2023抽奖活动抽奖信息 + Active2023Prize = "active_2023_prize" // 用户2023抽奖活动获得奖品详情 + Active2023FreeLotteryInc = "active_2023_free_lottery_inc" // 用户2023抽奖活动免费抽奖资格获取详情 + GoldExtra = "gold_extra" // 金币加赠券 + Act = "act" // 活动. 因为activity已被占用, 故而起名act + AdsClickLog = "ads_click_log" // 广告点击 + DailyTask = "daily_task" // 每日任务 + UserDailyTaskLog = "user_daily_task_log" // 用户每日任务完成记录 + OnceTask = "once_task" // 一次性任务 + AiUnDress = "ai_un_dress" // AI脱衣记录 + Like = "like" // 我的收藏 + IntegralConfig = "integral_config" // 积分配置 + IntegralExchange = "integral_exchange" // 积分兑换列表 + OfficialConfig = "official_config" // 官方配置 + Comment = "comment" // 评论表 + Collect = "collect" // 收藏表 + UserResource = "user_resource" // 用户资源配置 + AiChangeFace = "ai_change_face" // AI视频换脸 + AiChangeFaceVideo = "ai_change_face_video" // AI换脸视频模版 + AiChangeFaceImg = "ai_change_face_img" // AI图片换脸 + ChatSession = "chat_session" // 会话(配套消息使用) + ChatMessage = "chat_message" // 消息 + ImUser = "im_user" // IM用户映射 + AdvanceOrder = "advance_order" // 预售订单 + VideoGoldCoin = "video_gold_coin_ser" // 金币视频列表 + AdvanceConfig = "advance_config" // 预售配置 + CouponRecord = "coupon_record" // 优惠券记录 + SignRecord = "sign_record" // 打卡记录表 + VidTimeOnline = "video_time_online" // 视频定时上线表 + DiscountArea = "discount_area" // 折扣专区表 + DiscountAreaVideo = "discount_area_video" // 折扣专区视频表 + ExportTask = "export_task" // 导出任务 + QuickSearch = "quick_search" // 快捷搜索表 + SysConf = "sys_conf" // 系统通用配置 + BannerJump = "banner_jump" // 限时Banner活动 + PaymentGuide = "payment_guide" // 付费引导配置 + PaymentGuideImpression = "payment_guide_impression" // 付费引导曝光记录 + SceneBanner = "scene_banner" // 场景Banner配置 + VIPCardExperiment = "vip_card_experiment" // VIP卡片A/B实验 + VIPCardAnalyticsEvent = "vip_card_analytics_event" // VIP卡片A/B埋点事件 + Media = "media" // 动漫媒体 + MediaContent = "media_content" // 动漫媒体内容 + MediaTag = "media_tag" // 动漫标签 + MediaBookshelf = "media_bookshelf" // 动漫书架 + MediaTagDimension = "media_tag_dimension" // 动漫媒体纬度 + MediaBuyRecord = "media_buy_record" // 动漫购买记录表 + Jingang = "jingang" // 金刚区配置 + ImGroup = "im_group" // im群组 + ImGroupMember = "im_group_member" // im群组成员 + ImMessage = "im_message" // im消息 + NakedChat = "naked_chat" // 裸聊表 + NakedChatOrder = "naked_chat_order" // 裸聊订单表 + + AiTextToNovel = "ai_text_to_novel" // AI小说记录 + AiImageToVideo = "ai_image_to_video" // AI图生视频记录 + AiTextToImage = "ai_text_to_image" // AI绘图记录 + AiTemplateModule = "ai_template_module" // AI模版模块列表 + AiPlaza = "ai_plaza" // ai广场 + SignTaskConfig = "sign_task_config" // 签到任务 + Event = "event" // 事件表 + DailyRetention = "daily_retention" // 每日用户留存统计 + UserAdverStat = "user_adver_stat" // 用户每日广告统计 + CheckinPrize = "checkin_prize" // 签到奖品配置 + UserCheckin = "user_checkin" // 用户签到记录 + CheckinConfig = "checkin_config" // 签到系统配置 + SensitiveWord = "sensitive_word" // 内容审查敏感词 + ContentReviewTask = "content_review_task" // 内容检测任务 + ContentReviewIssue = "content_review_issue" // 内容检测命中问题记录 + + // 品牌官网相关表 + OfficialWebsiteBasicData = "official_website_basic_data" + OfficialWebsiteHero = "official_website_hero" + OfficialWebsiteAlbum = "official_website_album" + OfficialWebsiteVideo = "official_website_video" + OfficialWebsiteTag = "official_website_tag" + OfficialWebsiteRecruitForm = "official_website_recruit_form" + OfficialWebsiteJobList = "official_website_job" + OfficialWebsiteNews = "official_website_news" + OfficialWebsiteNewsDetail = "official_website_news_detail" + OfficialWebsitePartner = "official_website_partner" + OfficialWebsiteBusiness = "official_website_business" + OfficialWebsitePhotograph = "official_website_photograph" +) + +type Register struct { + Key string + Table []string +} + +var VideoDBTable = []string{UserTable, IDTable, Setting, Wallet, FundTransferLog, Activity, + FaqTable, WordsFilter, Tag, UserTag, VideoInfo,HevcTask, + Report, ReportTypes, Admin, Source, Tone, ToneRecom, UserAct, Product, Invitation, + ProxyIncomeLog, WithdrawChannel, WithdrawTariff, WithdrawOrder, RechargeOrder, + Transaction, PayChannel, Gold, GameGold, Ads, AdReview, Annou, Announce, Follow, HotCity, Location, PayVideoLog, + Authority, PushModel, Version, ProductHistory, TransactionAct, Msg, + IpBlock, IpWhite, OperationLog, NoticeFmt, NoticeRecd, UserReco, ExchCode, + ChatRobot, ContentLibrary, Fsid2UID, GoldConf, RejectTemplate, + RecmdTag, FreeVidConfig, Role, ActivityModels, ActivityGitStock, ActivitySoldRecord, + ActivityWinRecord, ActivityJoinPersons, LDYConfig, Questionnaire, ProductPosition, FeedBack, RewardVideoLog, VerifyReport, Fiction, AudioBook, AudioAnchor, ErrFeedback, + PayAudioBook, VideoPopularityConfig, VipConfig, Task, TaskConfig, Backpack, ActivityPrize, ActivityPrizePool, ActivityUserFree, ActivityUserRecord, Currency, SystemConfig, VideoDiscount, + ModuleConf, Section, SectionVideo, TagConf, VideoCoupon, ModuleMarquee, VideoActivity, UserWatchRecord, ProductBenefit, ImageTop, + Active2023User, Active2023Lottery, Active2023Prize, Active2023FreeLotteryInc, GoldExtra, Act, DailyTask, OnceTask, AiUnDress, Like, IntegralConfig, + OfficialConfig, Comment, Collect, UserResource, AiChangeFace, AiChangeFaceVideo, ChatSession, ChatMessage, ImUser, AiChangeFaceImg, + VideoGoldCoin, AdvanceOrder, AdvanceConfig, CouponRecord, SignRecord, VidTimeOnline, IntegralExchange, DiscountArea, DiscountAreaVideo, ExportTask, QuickSearch, SysConf, + BannerJump, PaymentGuide, PaymentGuideImpression, SceneBanner, VIPCardExperiment, VIPCardAnalyticsEvent, Media, MediaContent, MediaTag, MediaBookshelf, MediaTagDimension, MediaBuyRecord, Jingang, ImGroup, ImGroupMember, ImMessage, NakedChat, NakedChatOrder, + AiImageToVideo, AiTextToImage, AiTemplateModule, AiPlaza, SignTaskConfig, AiTextToNovel, Event, + CheckinPrize, UserCheckin, CheckinConfig, + SensitiveWord, + ContentReviewTask, ContentReviewIssue, + // official website 相关 + OfficialWebsiteBasicData, OfficialWebsiteHero, OfficialWebsiteAlbum, OfficialWebsiteVideo, OfficialWebsiteTag, OfficialWebsiteRecruitForm, OfficialWebsiteJobList, + OfficialWebsiteNews, OfficialWebsiteNewsDetail, OfficialWebsitePartner, OfficialWebsiteBusiness, OfficialWebsitePhotograph, +} + +var LogDBTable = []string{InviteLog, VisitLog, LoginLog, SearchLog, LoginLog, PlayLog, PullLog, + SyncCdnLog, WebOperatorLog, UserRecoRecord, NewRegister, ExchLog, + WelfareLog, LotteryLog, AdsClickLog, UserDailyTaskLog} + +var LogReadDBTable = []string{UserRecoRecord} + +var StatDbTable = []string{StatRecord, VideoStat, + VideoTotalStat, KeywordStat, KWRankStat, SectionClick, OrderStat, TagPlayStat, + UserStat, NewsHot, DailyRetention, UserAdverStat} + +var primary = Register{ + Key: VideoDb, + Table: VideoDBTable, +} + +var log = Register{ + Key: LogDb, + Table: LogDBTable, +} + +var readLog = Register{ + Key: LogReadDb, + Table: LogReadDBTable, +} + +var stat = Register{ + Key: StatDb, + Table: StatDbTable, +} + +var RegisterPool = []Register{primary, log, stat, readLog} diff --git a/models/v/active2023mod/free.go b/models/v/active2023mod/free.go new file mode 100644 index 0000000..2f3657d --- /dev/null +++ b/models/v/active2023mod/free.go @@ -0,0 +1,56 @@ +package active2023mod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +const active2023LotteryFreeInc = models.Active2023FreeLotteryInc // 用户免费次数新增表 + +func initFreeIndex() { + coll := active2023LotteryFreeIncColl(nil) + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", active2023PrizeTable, err)) + } +} + +func active2023LotteryFreeIncColl(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(active2023LotteryFreeInc) + } + return t.Coll(active2023LotteryFreeInc) +} + +// 添加免费次数 +func AddLotteryCountFree(t *db.MongoTool, uid int64, fi FreeLotteryInc) error { + f := bson.M{"uid": uid} + now := time.Now() + update := bson.M{"$inc": bson.M{"lotteryRemain": fi.Count}, "$set": bson.M{"updatedAt": now}} + result, err := active2023UserColl(t).UpdateOne(f, update) + if err != nil { + return err + } + if result.ModifiedCount != 1 { + if _, err := active2023UserColl(t).InsertOne(&UserActive2023{ + UID: uid, + LotteryRemain: fi.Count, + CreatedAt: now, + UpdatedAt: now, + }); err != nil { + return err + } + } + _, err = active2023LotteryFreeIncColl(t).InsertOne(fi) + return err +} diff --git a/models/v/active2023mod/lottery.go b/models/v/active2023mod/lottery.go new file mode 100644 index 0000000..4b0588c --- /dev/null +++ b/models/v/active2023mod/lottery.go @@ -0,0 +1,40 @@ +package active2023mod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +const active2023LotteryTable = models.Active2023Lottery // 用户抽奖详情表 + +func initLotteryIndex() { + coll := active2023LotteryColl(nil) + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", active2023LotteryTable, err)) + } +} + +func active2023LotteryColl(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(active2023LotteryTable) + } + return t.Coll(active2023LotteryTable) +} + +func InsertUserLottery(t *db.MongoTool, ul *UserLottery) error { + if ul == nil { + return nil + } + _, err := active2023LotteryColl(t).InsertOne(ul) + return err +} diff --git a/models/v/active2023mod/prize.go b/models/v/active2023mod/prize.go new file mode 100644 index 0000000..6be7e48 --- /dev/null +++ b/models/v/active2023mod/prize.go @@ -0,0 +1,52 @@ +package active2023mod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +const active2023PrizeTable = models.Active2023Prize // 用户中奖奖品详情表 + +func initPrizeIndex() { + coll := active2023PrizeColl(nil) + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", active2023PrizeTable, err)) + } +} + +func active2023PrizeColl(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(active2023PrizeTable) + } + return t.Coll(active2023PrizeTable) +} + +func InsertUserPrizes(t *db.MongoTool, ps *[]Prize) ([]primitive.ObjectID, error) { + if ps == nil || len(*ps) == 0 { + return nil, nil + } + result, err := active2023PrizeColl(t).InsertMany(ps) + if err != nil { + return nil, err + } + insertIDsLen := len(result.InsertedIDs) + if insertIDsLen == 0 { + return nil, nil + } + ids := make([]primitive.ObjectID, insertIDsLen) + for i := 0; i < insertIDsLen; i++ { + ids[i], _ = result.InsertedIDs[i].(primitive.ObjectID) + } + return ids, nil +} diff --git a/models/v/active2023mod/type.go b/models/v/active2023mod/type.go new file mode 100644 index 0000000..28218dd --- /dev/null +++ b/models/v/active2023mod/type.go @@ -0,0 +1,62 @@ +package active2023mod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + PrizeTypeGold = iota + 1 // 金币 + PrizeTypeGameGold // 游戏币 + PrizeTypeProduct // 现有产品. 目前仅支持会员卡和视频折扣卡 + PrizeTypeVIPDate // 会员天数 + PrizeTypeGoldCoinBonus // 金币加赠券 + PrizeTypeGoldWatch // 金币观影券 + PrizeTypeInKind // 实物奖励 +) + +// 用户抽奖信息 +type UserActive2023 struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + UID int64 `bson:"uid" json:"uid"` + LotteryRemain int64 `bson:"lotteryRemain" json:"lotteryRemain"` // 剩余免费抽奖次数 + LotteryFree int64 `bson:"lotteryFree" json:"lotteryFree"` // 已经免费抽奖次数 + LotteryRecharge int64 `bson:"lotteryRecharge" json:"lotteryRecharge"` // 已经付费抽奖次数 + CreatedAt time.Time `bson:"createdAt" json:"createdAt"` // 创建时间 + UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"` // 更新时间 +} + +// 用户抽奖详情 +type UserLottery struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + UID int64 `bson:"uid" json:"uid"` + Count int64 `bson:"count" json:"count"` // 抽奖次数 + UseGold int64 `bson:"useGold" json:"useGold"` // 抽奖消耗金币 + Prizes []primitive.ObjectID `bson:"prizes" json:"prizes"` // 获得奖品 + Desc string `bson:"desc" json:"desc"` // 说明 + CreatedAt time.Time `bson:"createdAt" json:"createdAt"` // 创建时间 + UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"` // 更新时间 +} + +// 中奖详情 +type Prize struct { + UID int64 `bson:"uid" json:"uid"` + Type int64 `bson:"type" json:"type"` // 奖品类型 + Name string `bson:"name" json:"name"` // 奖品名字 + Unit int64 `bson:"unit" json:"unit"` // 单个奖品包含奖品数量, 配合类型. 金币: 金币数量; 金币加购券: 加购券金币面值; 观影券: 观影券抵扣面值; 会员礼包: 会员延续天数 + ProductID *primitive.ObjectID `bson:"productID,omitempty" json:"productID,omitempty"` // 获得奖品的产品id + Amount int64 `bson:"amount" json:"amount"` // 奖品数量 + CreatedAt time.Time `bson:"createdAt" json:"createdAt"` // 创建时间 + UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"` // 更新时间 +} + +// 免费抽奖次数增加详情 +type FreeLotteryInc struct { + UID int64 `bson:"uid" json:"uid"` + Count int64 `bson:"count" json:"count"` + Desc string `bson:"desc" json:"desc"` // 说明 + OrderID *primitive.ObjectID `bson:"orderID,omitempty" json:"orderID"` // 对应的订单id + CreatedAt time.Time `bson:"createdAt" json:"createdAt"` // 创建时间 + UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"` // 更新时间 +} diff --git a/models/v/active2023mod/user.go b/models/v/active2023mod/user.go new file mode 100644 index 0000000..6ddd57a --- /dev/null +++ b/models/v/active2023mod/user.go @@ -0,0 +1,106 @@ +package active2023mod + +import ( + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const active2023UserTable = models.Active2023User // 用户抽奖信息表 + +func active2023UserColl(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(active2023UserTable) + } + return t.Coll(active2023UserTable) +} + +func Init() { + mdb = db.Init(active2023UserTable) + initIndex() +} + +// ActInitIndex 索引设置 +func initIndex() { + initUserIndex() + initLotteryIndex() + initPrizeIndex() + initFreeIndex() +} + +func initUserIndex() { + coll := active2023UserColl(nil) + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", active2023UserTable, err)) + } +} + +func GetActive2023UserInfoByID(t *db.MongoTool, uid int64) (*UserActive2023, error) { + var ua UserActive2023 + if err := active2023UserColl(t).FindOne(&ua, bson.M{"uid": uid}); err != nil { + return nil, err + } + if ua.UID == 0 { + now := time.Now() + ua = UserActive2023{ + UID: uid, + CreatedAt: now, + UpdatedAt: now, + } + if _, err := active2023UserColl(t).InsertOne(&ua); err != nil { + return nil, err + } + } + return &ua, nil +} + +// 使用免费次数 +func UseLotteryCountFree(t *db.MongoTool, uid int64, count int64) error { + f := bson.M{"uid": uid, "lotteryRemain": bson.M{"$gte": count}} + update := bson.M{"$inc": bson.M{"lotteryRemain": -int64(count), "lotteryFree": int64(count)}, "$set": bson.M{"updatedAt": time.Now()}} + result, err := active2023UserColl(t).UpdateOne(f, update) + if err != nil { + return err + } + if result.ModifiedCount != 1 { + return errors.New("免费抽奖次数不足") + } + return nil +} + +// 付费抽奖 +func UserLotteryCountPay(t *db.MongoTool, uid int64, count int64) error { + now := time.Now() + f := bson.M{"uid": uid} + update := bson.M{"$inc": bson.M{"lotteryRecharge": int64(count)}, "$set": bson.M{"updatedAt": time.Now()}} + result, err := active2023UserColl(t).UpdateOne(f, update) + if err != nil { + return err + } + if result.ModifiedCount != 1 { + if _, err := active2023UserColl(t).InsertOne(&UserActive2023{ + UID: uid, + LotteryRecharge: int64(count), + CreatedAt: now, + UpdatedAt: now, + }); err != nil { + return err + } + } + return nil +} diff --git a/models/v/activitymod/activity.go b/models/v/activitymod/activity.go new file mode 100644 index 0000000..200e0d8 --- /dev/null +++ b/models/v/activitymod/activity.go @@ -0,0 +1,138 @@ +package activitymod + +import ( + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.Activity + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// ActInitIndex 索引设置 +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "status", Value: 1}}, + }, + { + Keys: bson.D{{Key: "startTime", Value: -1}}, + }, + { + Keys: bson.D{{Key: "endTime", Value: -1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: -1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// 获取活动信息 +func Get(id primitive.ObjectID) (*Activity, error) { + var out *Activity + if err := coll(nil).FindOne(&out, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Get", models.Activity, "FindOne", err), + log.Any("id", id), + ) + return nil, err + } + return out, nil +} + +// 新增活动 +func Add(in AddCond) (interface{}, error) { + result, err := coll(nil).InsertOne(&Activity{ + Title: in.Title, + Desc: in.Desc, + Status: false, + StartTime: in.StartTime, + EndTime: in.EndTime, + UpdatedAt: time.Now(), + CreatedAt: time.Now(), + }) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Add", models.Activity, "InsertOne", err), + log.Any("in", in), + ) + return nil, err + } + return result, nil +} + +// 修改活动 +func Modify(t *db.MongoTool, filter, update primitive.M) error { + result, err := coll(nil).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Modify", models.Activity, "UpdateOne", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + if result.ModifiedCount == 0 { + err = errors.New("activity UpdateOne ModifiedCount err") + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Modify", models.Activity, "result.ModifiedCount == 0", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + return nil +} + +// 查询活动列表 +func QueryAll(filter primitive.M, opts ...*options.FindOptions) ([]*Activity, error) { + var out []*Activity = []*Activity{} + if err := coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAll", models.Activity, "Find", err), + log.Any("filter", filter), + ) + return nil, err + } + return out, nil +} + +// 查询总条数 +func Count(filter primitive.M) (int64, error) { + count, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Count", models.Activity, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } + return count, nil +} + +func QueryDropDownBox() ([]*Activity, error) { + var out []*Activity = []*Activity{} + if err := coll(nil).Find(&out, bson.M{"status": true}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryDropDownBox", models.Activity, "Find", err)) + return nil, err + } + return out, nil +} diff --git a/models/v/activitymod/prize_pool.go b/models/v/activitymod/prize_pool.go new file mode 100644 index 0000000..96b1321 --- /dev/null +++ b/models/v/activitymod/prize_pool.go @@ -0,0 +1,146 @@ +package activitymod + +import ( + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var prizePoolDB *db.MongoDB + +func collPrizePool(t *db.MongoTool) *db.MongoTool { + if t == nil { + return prizePoolDB.Coll(models.ActivityPrizePool) + } + return t.Coll(models.ActivityPrizePool) +} + +// initPrizePoolIndex 索引设置 +func initPrizePoolIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "activityId", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "createTime", Value: -1}}, + }, + } + _, err := collPrizePool(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func GetPrizePool(activityID primitive.ObjectID) (*PrizePool, error) { + var out *PrizePool + if err := collPrizePool(nil).FindOne(&out, bson.M{"activityId": activityID}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetPrizePool", models.ActivityPrizePool, "FindOne", err), + log.Any("activityID", activityID), + ) + return nil, err + } + return out, nil +} + +func UpdatePrizePoolInc(t *db.MongoTool, activityID primitive.ObjectID, reservoir, systemIncome, systemRake int64) error { + result, err := collPrizePool(t).UpdateOne(bson.M{"activityId": activityID}, bson.M{"$inc": bson.M{ + "reservoir": reservoir, + "systemIncome": systemIncome, + "systemRake": systemRake, + }}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdatePrizePoolInc", models.ActivityPrizePool, "UpdateOne", err), + log.Any("activityID", activityID), + log.Any("reservoir", reservoir), + log.Any("systemIncome", systemIncome), + log.Any("systemRake", systemRake), + ) + return err + } + if result.ModifiedCount == 0 { + err = errors.New("activity UpdateOne ModifiedCount err") + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdatePrizePoolInc", models.ActivityPrizePool, "result.ModifiedCount == 0", err), + log.Any("activityID", activityID), + log.Any("reservoir", reservoir), + log.Any("systemIncome", systemIncome), + log.Any("systemRake", systemRake), + ) + return err + } + return nil +} + +// 新增活动奖池 +func AddPrizePool(in AddPrizePoolCond) (interface{}, error) { + result, err := collPrizePool(nil).InsertOne(&PrizePool{ + ActivityID: in.ActivityID, + Model: in.Model, + EstimatedIncome: in.EstimatedIncome, + Price: in.Price, + FreeCount: in.FreeCount, + UpdateTime: time.Now(), + CreateTime: time.Now(), + }) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AddPrizePool", models.ActivityPrizePool, "InsertOne", err), + log.Any("in", in), + ) + return nil, err + } + return result, nil +} + +// 修改活动奖池 +func ModifyPrizePool(filter, update primitive.M) error { + result, err := collPrizePool(nil).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ModifyPrizePool", models.ActivityPrizePool, "UpdateOne", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + if result.ModifiedCount == 0 { + err = errors.New("activity UpdateOne ModifiedCount err") + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ModifyPrizePool", models.ActivityPrizePool, "result.ModifiedCount == 0", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + return nil +} + +// 查询活动奖池列表 +func QueryAllPrizePool(filter primitive.M, opts ...*options.FindOptions) ([]*PrizePool, error) { + var out []*PrizePool = []*PrizePool{} + if err := collPrizePool(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllPrizePool", models.ActivityPrizePool, "Find", err), + log.Any("filter", filter), + ) + return nil, err + } + return out, nil +} + +// 查询活动奖池总条数 +func CountPrizePool(filter primitive.M) (int64, error) { + count, err := collPrizePool(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountPrizePool", models.ActivityPrizePool, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } + return count, nil +} diff --git a/models/v/activitymod/prize_pool_type.go b/models/v/activitymod/prize_pool_type.go new file mode 100644 index 0000000..8a02177 --- /dev/null +++ b/models/v/activitymod/prize_pool_type.go @@ -0,0 +1,28 @@ +package activitymod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type PrizePoolModel int + +const ( + Game PrizePoolModel = iota + 1 // 棋牌游戏 + Free // 免费 +) + +type PrizePool struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档ID + ActivityID primitive.ObjectID `json:"activityId" bson:"activityId"` // 活动ID + Model PrizePoolModel `json:"model" bson:"model"` // 奖池模式 + Price int64 `json:"price" bson:"price"` // 单次抽奖金额 + FreeCount int32 `json:"freeCount" bson:"freeCount"` // 免费次数 + EstimatedIncome int64 `json:"estimatedIncome" bson:"estimatedIncome"` // 当日预计收益---杀放概率固定30% + Reservoir int64 `json:"reservoir" bson:"reservoir"` // 蓄水池---固定抽成5% + SystemIncome int64 `json:"systemIncome" bson:"systemIncome"` // 系统收益 + SystemRake int64 `json:"systemRake" bson:"systemRake"` // 系统抽成金额---固定抽成5% + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 修改时间 + CreateTime time.Time `json:"createTimt" bson:"createTimt"` // 创建时间 +} diff --git a/models/v/activitymod/type.go b/models/v/activitymod/type.go new file mode 100644 index 0000000..f5c3f14 --- /dev/null +++ b/models/v/activitymod/type.go @@ -0,0 +1,32 @@ +package activitymod + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Activity struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档ID + Title string `json:"title" bson:"title"` // 活动名称 + Desc string `json:"desc" bson:"desc"` // 活动注意事项 + Status bool `json:"status" bson:"status"` // 状态---启用/禁用 + StartTime time.Time `json:"startTime" bson:"startTime"` // 活动开始时间 + EndTime time.Time `json:"endTime" bson:"endTime"` // 活动结束时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 文档更新时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 文档创建时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() + prizePoolDB = db.Init(models.ActivityPrizePool) + initPrizePoolIndex() + userFreeDB = db.Init(models.ActivityUserFree) + initUserFreeIndex() + userRecordDB = db.Init(models.ActivityUserRecord) + initUserRecordIndex() +} diff --git a/models/v/activitymod/user_free.go b/models/v/activitymod/user_free.go new file mode 100644 index 0000000..371fa5f --- /dev/null +++ b/models/v/activitymod/user_free.go @@ -0,0 +1,117 @@ +package activitymod + +import ( + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var userFreeDB *db.MongoDB + +func collUserFree(t *db.MongoTool) *db.MongoTool { + if t == nil { + return userFreeDB.Coll(models.ActivityUserFree) + } + return t.Coll(models.ActivityUserFree) +} + +// initUserFreeIndex 索引设置 +func initUserFreeIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "activityId", Value: 1}, {Key: "uid", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "createTime", Value: -1}}, + }, + } + if _, err := collUserFree(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func GetUserFree(activityID primitive.ObjectID, uid uint64) (*UserFree, error) { + var out *UserFree + if err := collUserFree(nil).FindOne(&out, bson.M{"activityId": activityID, "uid": uid}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AddUserFree", models.ActivityUserFree, "InsertOne", err), + log.Any("activityID", activityID), + log.Any("uid", uid), + ) + return nil, err + } + return out, nil +} + +func AddUserFree(activityID primitive.ObjectID, uid uint64, freeCount int32) error { + if _, err := collUserFree(nil).InsertOne(&UserFree{ + ActivityID: activityID, + UID: uid, + FreeCount: freeCount, + UpdateTime: time.Now(), + CreateTime: time.Now(), + }); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AddUserFree", models.ActivityUserFree, "InsertOne", err), + log.Any("activityID", activityID), + log.Any("uid", uid), + log.Any("freeCount", freeCount), + ) + return err + } + return nil +} + +func UpdateOneUserFreeInc(t *db.MongoTool, activityID primitive.ObjectID, uid uint64, freeCount int32) error { + result, err := collUserFree(t).UpdateOne(bson.M{"activityId": activityID, "uid": uid}, + bson.M{"$inc": bson.M{"freeCount": freeCount}, "$set": bson.M{"updateTime": time.Now()}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateOneInc", models.ActivityUserFree, "UpdateOne", err), + log.Any("activityID", activityID), + log.Any("uid", uid), + log.Any("freeCount", freeCount), + ) + return err + } + if result.ModifiedCount == 0 { + err = errors.New("activity UpdateOne ModifiedCount err") + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateOneInc", models.ActivityUserFree, "result.ModifiedCount == 0", err), + log.Any("activityID", activityID), + log.Any("uid", uid), + log.Any("freeCount", freeCount), + ) + return err + } + return nil +} + +func UpdateOneSet(activityID primitive.ObjectID, uid uint64, freeCount int32) error { + result, err := collUserFree(nil).UpdateOne(bson.M{"activityId": activityID, "uid": uid}, + bson.M{"$set": bson.M{"freeCount": freeCount, "updateTime": time.Now()}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateOneSet", models.ActivityUserFree, "UpdateOne", err), + log.Any("activityID", activityID), + log.Any("uid", uid), + log.Any("freeCount", freeCount), + ) + return err + } + if result.ModifiedCount == 0 { + err = errors.New("activity UpdateOne ModifiedCount err") + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateOneSet", models.ActivityUserFree, "result.ModifiedCount == 0", err), + log.Any("activityID", activityID), + log.Any("uid", uid), + log.Any("freeCount", freeCount), + ) + return err + } + return nil +} diff --git a/models/v/activitymod/user_free_type.go b/models/v/activitymod/user_free_type.go new file mode 100644 index 0000000..842a1d2 --- /dev/null +++ b/models/v/activitymod/user_free_type.go @@ -0,0 +1,17 @@ +package activitymod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 用户免费次数记录 +type UserFree struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档ID + ActivityID primitive.ObjectID `json:"activityId" bson:"activityId"` // 活动ID + UID uint64 `json:"uid" bson:"uid"` // 用户ID + FreeCount int32 `json:"freeCount" bson:"freeCount"` // 剩余免费次数 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 修改时间 + CreateTime time.Time `json:"createTimt" bson:"createTimt"` // 创建时间 +} diff --git a/models/v/activitymod/user_record.go b/models/v/activitymod/user_record.go new file mode 100644 index 0000000..b865efe --- /dev/null +++ b/models/v/activitymod/user_record.go @@ -0,0 +1,93 @@ +package activitymod + +import ( + "errors" + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/v/prizemod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var userRecordDB *db.MongoDB + +func collUserRecord(t *db.MongoTool) *db.MongoTool { + if t == nil { + return userRecordDB.Coll(models.ActivityUserRecord) + } + return t.Coll(models.ActivityUserRecord) +} + +// initUserFreeIndex 索引设置 +func initUserRecordIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "activityId", Value: 1}, {Key: "uid", Value: 1}, {Key: "createTime", Value: -1}}, + }, + { + Keys: bson.D{{Key: "createTime", Value: -1}}, + }, + } + if _, err := collUserRecord(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", models.ActivityUserRecord, err)) + } +} + +func AddMany(records []UserRecord) error { + result, err := collUserRecord(nil).InsertMany(&records) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AddMany", models.ActivityUserRecord, "InsertMany", err), + log.Any("records", records), + ) + return err + } + if len(result.InsertedIDs) != len(records) { + err = errors.New("activity InsertMany InsertedIDs err") + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AddMany", models.ActivityUserRecord, "len(result.InsertedIDs) != len(records)", err), + log.Any("records", records), + ) + return err + } + return nil +} + +func QueryAllRecord(filter primitive.M, opts ...*options.FindOptions) ([]*UserRecord, error) { + var out []*UserRecord = []*UserRecord{} + if err := collUserRecord(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllRecord", models.ActivityUserRecord, "Find", err), + log.Any("filter", filter), + ) + return nil, err + } + return out, nil +} + +// 查询奖品总条数 +func CountRecord(filter primitive.M) (int64, error) { + count, err := collUserRecord(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountRecord", models.ActivityUserRecord, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } + return count, nil +} + +func GetMarquee(activityId primitive.ObjectID) ([]*UserRecord, error) { + filter := bson.M{"activityId": activityId, "level": prizemod.High} + var out []*UserRecord = []*UserRecord{} + if err := collUserRecord(nil).Find(&out, filter, options.Find().SetLimit(10).SetSort(bson.M{"createTime": -1})); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllRecord", models.ActivityUserRecord, "Find", err), + log.Any("filter", filter), + ) + return nil, err + } + return out, nil +} diff --git a/models/v/activitymod/user_record_type.go b/models/v/activitymod/user_record_type.go new file mode 100644 index 0000000..4ae682c --- /dev/null +++ b/models/v/activitymod/user_record_type.go @@ -0,0 +1,26 @@ +package activitymod + +import ( + "time" + + "91porn-server/models/v/prizemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 用户抽奖记录 +type UserRecord struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档ID + ActivityID primitive.ObjectID `json:"activityId" bson:"activityId"` // 活动ID + VIPCardID primitive.ObjectID `json:"vipCardId" bson:"vipCardId"` // 会员卡ID + UID uint64 `json:"uid" bson:"uid"` // 用户ID + UserName string `json:"userName" bson:"userName"` // 用户名 + Name string `json:"name" bson:"name"` // 奖品名称 + Type prizemod.PrizeType `json:"type" bson:"type"` // 奖品类型 + Count int32 `json:"count" bson:"count"` // 奖品数量 + Price int64 `json:"price" bson:"price"` // 奖品价值 + Value int64 `json:"value" bson:"value"` // 折扣率/活跃值 + Level prizemod.PrizeLevel `json:"level" bson:"level"` // 奖品等级 + DrawPrice int64 `json:"drawPrice" bson:"drawPrice"` // 单次抽奖价格(金币) + CreateTime time.Time `json:"createTime" bson:"createTime"` // 创建时间 +} diff --git a/models/v/activitymod/web.go b/models/v/activitymod/web.go new file mode 100644 index 0000000..5368c1e --- /dev/null +++ b/models/v/activitymod/web.go @@ -0,0 +1,171 @@ +package activitymod + +import ( + "time" + + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// ActReq 活动请求,公告跳转到活动页面 +type ActReq struct { + Token string `form:"token" json:"token"` + Type string `form:"type" json:"type"` +} + +// 新增活动 +type AddCond struct { + Title string `json:"title"` // 活动名称 + Desc string `json:"desc"` // 活动注意事项 + StartTime time.Time `json:"startTime"` // 活动开始时间 + EndTime time.Time `json:"endTime"` // 活动结束时间 +} + +// 修改活动 +type ModifyCond struct { + ID primitive.ObjectID `json:"id" binding:"required"` // 文档ID + Title *string `json:"title"` // 活动名称 + Desc *string `json:"desc"` // 活动注意事项 + Status *bool `json:"status"` // 状态---启用/禁用 + StartTime *time.Time `json:"startTime"` // 活动开始时间 + EndTime *time.Time `json:"endTime"` // 活动结束时间 +} + +func (m *ModifyCond) Cond() primitive.M { + return bson.M{"_id": m.ID} +} + +func (m *ModifyCond) Bson() primitive.M { + set := bson.M{"updatedAt": time.Now()} + if m.Title != nil { + set["title"] = m.Title + } + if m.Desc != nil { + set["desc"] = m.Desc + } + if m.Status != nil { + set["status"] = m.Status + } + if m.StartTime != nil { + set["startTime"] = m.StartTime + } + if m.EndTime != nil { + set["endTime"] = m.EndTime + } + return bson.M{"$set": set} +} + +// 查询活动列表 +type QueryAllCond struct { + Status *bool `form:"status"` // 状态---启用/禁用 + commod.Page +} + +func (q *QueryAllCond) Options() *options.FindOptions { + return options.Find().SetLimit(int64(q.PageSize)).SetSkip(int64((q.PageNumber - 1) * q.PageSize)).SetSort(bson.M{"createdAt": -1}) +} + +func (q *QueryAllCond) Query() primitive.M { + fliter := bson.M{} + if q.Status != nil { + fliter["status"] = q.Status + } + return fliter +} + +// 新增活动奖池 +type AddPrizePoolCond struct { + ActivityID primitive.ObjectID `json:"activityId" binding:"required"` // 活动ID + Model PrizePoolModel `json:"model"` // 奖池模式 + EstimatedIncome int64 `json:"estimatedIncome"` // 当日预计收益 + Price int64 `json:"price"` // 单次抽奖金额 + FreeCount int32 `json:"freeCount"` // 免费次数 +} + +// 修改活动奖池 +type ModifyPrizePoolCond struct { + ID primitive.ObjectID `json:"id" binding:"required"` // 文档ID + ActivityID *primitive.ObjectID `json:"activityId"` // 活动ID + Model *PrizePoolModel `json:"model"` // 奖池模式 + EstimatedIncome *int64 `json:"estimatedIncome"` // 当日预计收益 + Price *int64 `json:"price"` // 单次抽奖金额 + FreeCount *int32 `json:"freeCount"` // 免费次数 +} + +func (m *ModifyPrizePoolCond) Cond() primitive.M { + return bson.M{"_id": m.ID} +} + +func (m *ModifyPrizePoolCond) Bson() primitive.M { + set := bson.M{"updatedAt": time.Now()} + if m.ActivityID != nil { + set["activityId"] = m.ActivityID + } + if m.Model != nil { + set["model"] = m.Model + } + if m.EstimatedIncome != nil { + set["estimatedIncome"] = m.EstimatedIncome + } + if m.Price != nil { + set["price"] = m.Price + } + if m.FreeCount != nil { + set["freeCount"] = m.FreeCount + } + return bson.M{"$set": set} +} + +// 查询活动奖池列表 +type QueryAllPrizePoolCond struct { + Model *PrizePoolModel `form:"model"` // 奖池模式 + ActivityID *string `form:"activityId"` // 活动ID + commod.Page +} + +func (q *QueryAllPrizePoolCond) Options() *options.FindOptions { + return options.Find().SetLimit(int64(q.PageSize)).SetSkip(int64((q.PageNumber - 1) * q.PageSize)).SetSort(bson.M{"createdAt": -1}) +} + +func (q *QueryAllPrizePoolCond) Query() primitive.M { + fliter := bson.M{} + if q.Model != nil { + fliter["model"] = q.Model + } + if q.ActivityID != nil { + id, _ := primitive.ObjectIDFromHex(*q.ActivityID) + fliter["activityId"] = id + } + return fliter +} + +// 查询活动参与记录列表 +type QueryAllRecordCond struct { + ActivityID *string `form:"activityId"` // 活动ID + UID *string `form:"uid"` // 活动ID + commod.Page +} + +func (q *QueryAllRecordCond) Options() *options.FindOptions { + return options.Find().SetLimit(int64(q.PageSize)).SetSkip(int64((q.PageNumber - 1) * q.PageSize)).SetSort(bson.M{"createTime": -1}) +} + +func (q *QueryAllRecordCond) Query() primitive.M { + fliter := bson.M{} + if q.UID != nil { + fliter["uid"] = q.UID + } + if q.ActivityID != nil { + id, _ := primitive.ObjectIDFromHex(*q.ActivityID) + fliter["activityId"] = id + } + return fliter +} + +type OpenBotDrawCond struct { + ID primitive.ObjectID `json:"id" binding:"required"` // 活动ID + Status bool +} diff --git a/models/v/actmod/activity.go b/models/v/actmod/activity.go new file mode 100644 index 0000000..10a8d04 --- /dev/null +++ b/models/v/actmod/activity.go @@ -0,0 +1,55 @@ +package actmod + +import ( + "errors" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func GetActivitiesCount() (int64, error) { + return coll(nil).Count(bson.M{}) +} + +func GetActivities(skip, limit uint64) ([]Activity, error) { + opts := options.Find().SetSort(bson.D{{Key: "sort", Value: 1}, {Key: "createdAt", Value: -1}}) + if skip != 0 { + opts.SetSkip(int64(skip)) + } + if limit != 0 { + opts.SetLimit(int64(limit)) + } + var activities []Activity + if err := coll(nil).Find(&activities, bson.M{}, opts); err != nil { + return nil, err + } + return activities, nil +} + +func GetActivitiesValid(skip, limit uint64) ([]Activity, error) { + opts := options.Find().SetSort(bson.D{{Key: "sort", Value: 1}, {Key: "createdAt", Value: -1}}) + if skip != 0 { + opts.SetSkip(int64(skip)) + } + if limit != 0 { + opts.SetLimit(int64(limit)) + } + var activities []Activity + if err := coll(nil).Find(&activities, bson.M{"status": 1, "expiredIn": bson.M{"$gt": time.Now()}}, opts); err != nil { + return nil, err + } + return activities, nil +} + +func GetActivityByActivityID(ActivityID primitive.ObjectID) (*Activity, error) { + var act Activity + if err := coll(nil).FindOne(&act, bson.M{"_id": ActivityID}); err != nil { + return nil, err + } + if act.ID.IsZero() { + return nil, errors.New("active not found") + } + return &act, nil +} diff --git a/models/v/actmod/type.go b/models/v/actmod/type.go new file mode 100644 index 0000000..e1bda97 --- /dev/null +++ b/models/v/actmod/type.go @@ -0,0 +1,57 @@ +package actmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.Act + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "activityName", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type Activity struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // id + ActivityName string `json:"activityName" bson:"activityName"` // 活动名 + Desc string `json:"desc" bson:"desc"` // 活动说明 + Img string `json:"img" bson:"img"` // 活动图片 + Content string `json:"content" bson:"content"` // 富文本内容 + Link string `json:"link" bson:"link"` // 链接地址 + Sort int `json:"sort" bson:"sort"` // 活动排序 + Status int `json:"status" bson:"status"` // 状态 1 有效 0 无效 + ExpiredIn time.Time `json:"expiredIn" bson:"expiredIn"` // 过期时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 文档创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 文档更新时间 +} diff --git a/models/v/actmod/web.go b/models/v/actmod/web.go new file mode 100644 index 0000000..1597563 --- /dev/null +++ b/models/v/actmod/web.go @@ -0,0 +1,16 @@ +package actmod + +import ( + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func AddACtivity(act Activity) error { + _, err := coll(nil).InsertOne(act) + return err +} + +func EidtActivity(actID primitive.ObjectID, b bson.M) error { + _, err := coll(nil).UpdateOne(bson.M{"_id": actID}, bson.M{"$set": b}) + return err +} diff --git a/models/v/adminmod/admin.go b/models/v/adminmod/admin.go new file mode 100644 index 0000000..228c42b --- /dev/null +++ b/models/v/adminmod/admin.go @@ -0,0 +1,246 @@ +package adminmod + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/models" + "91porn-server/models/v/authoritymod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.Admin + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "name", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "secret", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "lastLoginAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func FindOneByName(name string) (doc Admin, err error) { + return findOne(AdminDoc{ + Name: &name, + }) +} + +func FindOneByID(id ObjectID) (doc Admin, err error) { + return findOne(AdminDoc{ + ID: &id, + }) +} + +func findOne(filter AdminDoc) (doc Admin, err error) { + filterM, err := common.ToBsonM(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "findOne", table, "ToBsonM", err)) + return + } + if err = coll(nil).FindOne(&doc, filterM); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "findOne", table, "FindOne", err)) + return + } + return +} + +func ExistsByID(id ObjectID) (existed bool, err error) { + doc, err := FindOneByID(id) + existed = !doc.ID.IsZero() + return +} + +func ExistsByName(name string) (existed bool, err error) { + doc, err := FindOneByName(name) + existed = !doc.ID.IsZero() + return +} + +// Insert 插入记录 +func Insert(tool *db.MongoTool, doc AdminDoc) (err error) { + now := time.Now() + doc.CreatedAt = &now + doc.UpdatedAt = &now + if doc.Role != nil { + authority, err := authoritymod.FindOne(authoritymod.AuthorityDoc{Role: doc.Role}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "FindOne", err)) + return err + } + if authority.ID.IsZero() { + return fmt.Errorf("coll:%s update Role is not exist", table) + } + } + if _, err = coll(tool).InsertOne(doc); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + return +} + +func Bind(name string) error { + hasBind := true + return update(AdminDoc{Name: &name}, AdminDoc{HasBind: &hasBind}) +} + +// UpdateTo UpdateTo +func UpdateByID(id ObjectID, updat AdminDoc) error { + return update(AdminDoc{ID: &id}, updat) +} + +// UpdateTo UpdateTo +func update(filter AdminDoc, update AdminDoc) error { + filterM, err := common.ToBsonM(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "update", table, "filter ToBsonM", err)) + return err + } + now := time.Now() + update.UpdatedAt = &now + if update.Role != nil { + authority, err := authoritymod.FindOne(authoritymod.AuthorityDoc{Role: update.Role}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "update", table, "FindOne", err)) + return err + } + if authority.ID.IsZero() { + return fmt.Errorf("coll:%s update Role is not exist", table) + } + } + updateM, err := common.ToBsonM(update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "update", table, "update ToBsonM", err)) + return err + } + if _, err := coll(nil).UpdateOne(filterM, bson.M{"$set": updateM}); err != nil { + log.Error(fmt.Sprintf( + "[METHOD-%s]==> Model %s %s fail error:%+v:", "update", table, "UpdateOne", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + return nil +} + +func UnsetPrivilege(id ObjectID) error { + filter := M{"_id": id} + update := M{"$unset": M{ + "role": 1, + }} + result, err := coll(nil).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UnsetPrivilege", table, "UpdateOne", err), log.Any("id", id)) + return err + } + if result.ModifiedCount+result.UpsertedCount != 1 { + return fmt.Errorf("coll:%s UpdateTo Count error, id:%+v", table, id) + } + return nil +} + +// Delete Delete +func Delete(doc AdminDoc) error { + filer, err := common.ToBsonM(doc) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Delete", table, "ToBsonM", err)) + return err + } + if _, err := coll(nil).DeleteOne(filer); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Delete", table, "ToBsonM", err), log.Any("filer", filer)) + return err + } + return nil +} + +// IsUsing +// @return true:角色被使用 否则flase +func IsUsing(role string) (bool, error) { + filter := M{"role": role} + cnt, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsUsing", table, "Count", err)) + return false, err + } + return cnt > 0, nil +} + +func List(skip, limit int64, matcher ...pageopt.Matcher) ([]Admin, error) { + filter := pageopt.MergeM(matcher) + opt := (&options.FindOptions{}). + SetSkip(skip). + SetLimit(limit) + adminList := make([]Admin, 0, limit) + if err := coll(nil).Find(&adminList, filter, opt); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "List", table, "Find", err)) + return nil, err + } + return adminList, nil +} + +func Count(matcher ...pageopt.Matcher) (int64, error) { + filter := pageopt.MergeM(matcher) + count, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Count", table, "Count", err)) + return 0, err + } + return count, nil +} + +// GetNickByAccount 通过账号获取名称 +func GetNickByAccount(accounts []string) (map[string]string, error) { + m := make(map[string]string) + datas := []Admin{} + cond := bson.M{"name": bson.M{"$in": accounts}} + if err := coll(nil).Find(&datas, cond); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetNickByAccount", table, "Find", err), log.Any("accounts", accounts)) + return m, err + } + for _, d := range datas { + m[d.Name] = d.Nickname + } + return m, nil +} + +// GetNameByNick 通过昵称过去账号名 +func GetNameByNick(nick string) (string, error) { + data := Admin{} + cond := bson.M{"nickname": nick} + if err := coll(nil).FindOne(&data, cond); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetNameByNick", table, "FindOne", err), log.Any("nick", nick)) + return data.Name, err + } + return data.Name, nil +} diff --git a/models/v/adminmod/type.go b/models/v/adminmod/type.go new file mode 100644 index 0000000..88eabde --- /dev/null +++ b/models/v/adminmod/type.go @@ -0,0 +1,64 @@ +package adminmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ( + // A A from Array + A = bson.A + + // D D from Doc + D = bson.D + + // M M from Map + M = bson.M + + ObjectID = primitive.ObjectID + + // Admin Admin + Admin struct { + ID ObjectID `bson:"_id,omitempty"` + Name string `bson:"name"` //账号 + Password string `bson:"password"` //密码 + Nickname string `bson:"nickname"` //昵称 + Email string `bson:"email"` //邮箱 + Role string `bson:"role"` //角色 + Secret string `bson:"secret"` //两步验证密钥 + HasBind bool `bson:"hasBind"` //已绑定 true,曾成功登录,反之false + HasLocked bool `bson:"hasLocked"` //已禁止登陆 + LockReason string `bson:"lockReason"` //禁止登陆的原因 + CreatedAt time.Time `bson:"createdAt"` + UpdatedAt time.Time `bson:"updatedAt"` + LastLoginAt time.Time `bson:"lastLoginAt"` //最后登陆时间 + BindAt time.Time `bson:"bindAt"` //绑定电话时间 + } + + // AdminDoc AdminDoc + AdminDoc struct { + ID *primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + PrivilegeID *primitive.ObjectID `json:"privilegeId,omitempty" bson:"privilegeID,omitempty"` //权限ID + Name *string `json:"name,omitempty" bson:"name,omitempty"` //账号 + Password *string `json:"password,omitempty" bson:"password,omitempty"` //密码 + Nickname *string `json:"nickname,omitempty" bson:"nickname,omitempty"` //昵称 + Email *string `json:"email,omitempty" bson:"email,omitempty"` //邮箱 + Role *string `json:"role,omitempty" bson:"role,omitempty"` //角色 + Secret *string `json:"secret,omitempty" bson:"secret,omitempty"` //两步验证密钥 + HasBind *bool `json:"hasBind,omitempty" bson:"hasBind,omitempty"` //已绑定 true,曾成功登录,反之false + HasLocked *bool `json:"hasLocked,omitempty" bson:"hasLocked,omitempty"` //已禁止登陆 + LockReason *string `json:"lockReason,omitempty" bson:"lockReason,omitempty"` //禁止登陆的原因 + CreatedAt *time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` + UpdatedAt *time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` + LastLoginAt *time.Time `json:"lastLoginAt,omitempty" bson:"lastLoginAt,omitempty"` + } +) + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/adreviewmod/adreview.go b/models/v/adreviewmod/adreview.go new file mode 100644 index 0000000..03d4f8c --- /dev/null +++ b/models/v/adreviewmod/adreview.go @@ -0,0 +1,169 @@ +package adreviewmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +const table = models.AdReview + +var mdb *db.MongoDB + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 初始化索引 +func initAdsIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "reviewStatus", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "districtCode", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "passAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "refussAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "optTypeAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// InsertOne 增加广告 +func InsertOne(t *db.MongoTool, doc InsertDoc) error { + if doc.CreatedAt.IsZero() { + doc.CreatedAt = time.Now() + } + if doc.ApplyAt.IsZero() { + doc.ApplyAt = time.Now() + } + if _, err := coll(t).InsertOne(doc); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertMany", err)) + return err + } + return nil +} + +var processong = Processong + +func updateOne(doc UpdateDoc, mats ...Matcher) error { + var d struct { + Doc UpdateDoc `bson:",inline"` + ReviewStatus *ReviewStatus `bson:"reviewStatus,omitempty"` //审核状态 + } + d.Doc = doc + filter := pageopt.MergeM(mats) + now := time.Now() + if doc.OptType != nil { + doc.ApplyAt = &now + } + if doc.Title != nil || doc.Cover != nil || doc.HrefType != nil || doc.Href != nil || doc.Position != nil || doc.SortCode != nil { + d.ReviewStatus = &processong + } + if doc.UpdatedAt.IsZero() { + doc.UpdatedAt = now + } + res, err := coll(nil).UpdateOne(filter, bson.M{"$set": d}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateAds", table, "UpdateOne", err), log.Any("doc", doc), log.Any("filter", filter)) + return err + } + if res.MatchedCount == 0 { + return ADReviewNotExistError{} + } + return nil +} + +func UpdateOneByID(id ObjectID, doc UpdateDoc) error { + return updateOne(doc, (&IDMatch{ID: &id}).New()) +} + +func DeleteOne(t *db.MongoTool, id ObjectID) error { + if _, err := coll(t).DeleteById(id); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteOne", table, "DeleteById", err)) + if err == mongo.ErrNoDocuments { + return ADReviewNotExistError{} + } + return err + } + return nil +} + +func updateReview(review Review, now time.Time, mats ...Matcher) error { + var doc struct { + Review `bson:",inline"` + UpdatedAt time.Time `bson:"updatedAt"` //更新时间 + } + doc.Review = review + if doc.ReviewStatus == Pass { + doc.PassAt = now + } + if doc.ReviewStatus == Refuse { + doc.RefussAt = now + } + doc.UpdatedAt = now + filter := pageopt.MergeM(mats) + res, err := coll(nil).UpdateOne(filter, bson.M{"$set": doc}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateAds", table, "UpdateOne", err), log.Any("doc", doc), log.Any("filter", filter)) + return err + } + if res.MatchedCount == 0 { + return ADReviewNotExistError{} + } + return nil +} + +func UpdateReviewByID(id ObjectID, curStatus ReviewStatus, targetStatus ReviewStatus, adID *ObjectID, reviewer string, receiptInfo string, now time.Time) error { + review := Review{ + Reviewer: reviewer, + ReviewStatus: targetStatus, + ReceiptInfo: receiptInfo, + } + if adID != nil { + review.ADID = *adID + } + mats := []Matcher{ + (&IDMatch{ID: &id}).New(), + (&ReviewStatusMatch{&curStatus}).New(), + } + return updateReview(review, now, mats...) +} + +func findOne(mats ...Matcher) (AdReview, error) { + filter := pageopt.MergeM(mats) + ads := AdReview{} + if err := coll(nil).FindOne(&ads, filter); err != nil { + return AdReview{}, err + } + if ads.ID.IsZero() { + return AdReview{}, ADReviewNotExistError{} + } + return ads, nil +} + +func FindOneByID(id ObjectID) (AdReview, error) { + return findOne((&IDMatch{ID: &id}).New()) +} diff --git a/models/v/adreviewmod/error.go b/models/v/adreviewmod/error.go new file mode 100644 index 0000000..8bab5c1 --- /dev/null +++ b/models/v/adreviewmod/error.go @@ -0,0 +1,9 @@ +package adreviewmod + +// ADReviewNotExistError +type ADReviewNotExistError struct { +} + +func (ADReviewNotExistError) Error() string { + return "review is not exist" +} diff --git a/models/v/adreviewmod/list.go b/models/v/adreviewmod/list.go new file mode 100644 index 0000000..66cef4a --- /dev/null +++ b/models/v/adreviewmod/list.go @@ -0,0 +1,69 @@ +package adreviewmod + +import ( + "91porn-server/common/log" + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type ( + AdsSlice []AdReview + + Matcher = pageopt.Matcher + + // ReviewStatusMatch + ReviewStatusMatch struct { + ReviewStatus *ReviewStatus + } + + // DistrictCodeMatch + DistrictCodeMatch struct { + DistrictCode *string + } + + IDMatch = pageopt.IDMatch + + Sort = bson.D +) + +func (a *ReviewStatusMatch) New() Matcher { + return pageopt.NewAssignMatch("reviewStatus", a.ReviewStatus) +} + +func (c *DistrictCodeMatch) New() Matcher { + return pageopt.NewAssignMatch("districtCode", c.DistrictCode) +} + +var Sort_createdAt_N1 = Sort{{Key: "createdAt", Value: -1}} + +func List(sort Sort, skip, limit *int64, matchers ...Matcher) (AdsSlice, error) { + filter := pageopt.MergeM(matchers) + opt := (&options.FindOptions{}) + if len(sort) != 0 { + opt.SetSort(sort) + } + if skip != nil { + opt.SetSkip(*skip) + } + if limit != nil { + opt.SetLimit(*limit) + } + list := AdsSlice{} + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.Error("adsmod List error", log.E(err)) + return nil, err + } + return list, nil +} + +func Count(matchers ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchers) + count, err := coll(nil).Count(filter) + if err != nil { + log.Error("adsmod List error", log.E(err)) + return 0, err + } + return count, nil +} diff --git a/models/v/adreviewmod/type.go b/models/v/adreviewmod/type.go new file mode 100644 index 0000000..1d1d557 --- /dev/null +++ b/models/v/adreviewmod/type.go @@ -0,0 +1,105 @@ +package adreviewmod + +import ( + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/ysurl" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + ADD OptType = iota //添加 + Modify //修改 + Delete //删除 +) + +const ( + Processong ReviewStatus = iota // 审核中 + Pass // 通过 + Refuse // 拒绝 +) + +type ( + ObjectID = primitive.ObjectID + + // 操作类型 + OptType int + + // RUL跳转类型 + URLJumpType = ysurl.URLJumpType + + // 审核状态 + ReviewStatus int + + // Operation 操作信息 + Operation struct { + OptType OptType `bson:"optType"` //操作类型 + ApplyAt time.Time `bson:"applyAt"` //申请时间 + } + + Review struct { + ADID ObjectID `bson:"adID,omitempty"` //广告id + PassAt time.Time `bson:"passAt,omitempty"` //通过时间 + RefussAt time.Time `bson:"refussAt,omitempty"` //拒绝时间 + Reviewer string `bson:"reviewer"` //审核人 + ReviewStatus ReviewStatus `bson:"reviewStatus"` //审核状态 + ReceiptInfo string `bson:"receiptInfo"` //回执信息 + } + + CoverProp = common.PictureProp + + // Ads 广告审核 + AdReview struct { + ID ObjectID `bson:"_id"` //id + Cover string `bson:"cover"` //广告封面图 + Title string `bson:"title"` //广告标题 + HrefType URLJumpType `bson:"hrefType"` //广告链接类型 + Href string `bson:"href"` //广告跳转地址 + Position int `bson:"position"` //广告位置 + SortCode int `bson:"sortCode"` //排序号 + Remark string `bson:"remark"` //备注 + DistrictCode string `bson:"districtCode"` //商区码 + UpdatedAt time.Time `bson:"updatedAt"` //更新时间 + CreatedAt time.Time `bson:"createdAt"` //创建时间 + Operation `bson:",inline"` //操作 + Review `bson:",inline"` //审核信息 + CoverProp `bson:",inline"` //图片属性 + } + + InsertDoc struct { + Cover string `bson:"cover"` //广告封面图 + Title string `bson:"title"` //广告标题 + HrefType URLJumpType `bson:"hrefType"` //广告链接类型 + Href string `bson:"href"` //广告跳转地址 + Position int `bson:"position"` //广告位置 + SortCode int `bson:"sortCode"` //排序号 + Remark string `bson:"remark"` //备注 + DistrictCode string `bson:"districtCode"` //商区码 + CreatedAt time.Time `bson:"createdAt"` //创建时间 + Operation `bson:",inline"` //操作 + Review `bson:",inline"` //审核信息 + CoverProp `bson:",inline"` //图片属性 + } + + UpdateDoc struct { + Title *string `bson:"title,omitempty"` //广告标题 + Cover *string `bson:"cover,omitempty"` //广告封面图 + HrefType *URLJumpType `bson:"hrefType,omitempty"` //广告链接类型 + Href *string `bson:"href,omitempty"` //广告跳转地址 + Position *int `bson:"position,omitempty"` //广告位置 + SortCode *int `bson:"sortCode,omitempty"` //排序号 + Remark *string `bson:"remark,omitempty"` //备注 + OptType *OptType `bson:"optType,omitempty"` //操作类型 + ApplyAt *time.Time `bson:"applyAt,omitempty"` //申请时间 + UpdatedAt time.Time `bson:"updatedAt"` //更新时间 + CoverProp `bson:",inline"` //图片属性 + } +) + +func Init() { + mdb = db.Init(table) + initAdsIndex() +} diff --git a/models/v/adsmod/ads.go b/models/v/adsmod/ads.go new file mode 100644 index 0000000..da98f8a --- /dev/null +++ b/models/v/adsmod/ads.go @@ -0,0 +1,141 @@ +package adsmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +const table = models.Ads + +var mdb *db.MongoDB + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 初始化索引 +func initAdsIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "active", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "sortCode", Value: 1}}, + }, + { + Keys: bson.D{{Key: "type", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// InsertOne 增加广告 +func InsertOne(t *db.MongoTool, doc InsertDoc) (ObjectID, error) { + if doc.CreatedAt.IsZero() { + doc.CreatedAt = time.Now() + } + res, err := coll(t).InsertOne(doc) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertMany", err)) + return ObjectID{}, err + } + return res.InsertedID.(ObjectID), nil +} + +// DeleteOne 删除广告 +func DeleteOne(t *db.MongoTool, id ObjectID) error { + if _, err := coll(t).DeleteOne(bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteOne", table, "DeleteOne", err), log.Any("ids", id)) + if err == mongo.ErrNoDocuments { + return ADSNotExistError{} + } + return err + } + return nil +} + +func updateOne(t *db.MongoTool, doc UpdateDoc, mats ...Matcher) error { + filter := pageopt.MergeM(mats) + now := time.Now() + if doc.UpdatedAt.IsZero() { + doc.UpdatedAt = now + } + res, err := coll(t).UpdateOne(filter, bson.M{"$set": doc}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateAds", table, "UpdateOne", err), log.Any("doc", doc), log.Any("filter", filter)) + return err + } + if res.MatchedCount == 0 { + return ADSNotExistError{} + } + return nil +} + +func UpdateOneByID(t *db.MongoTool, id ObjectID, doc UpdateDoc) error { + return updateOne(t, doc, (&IDMatch{ID: &id}).New()) +} + +func findOne(mats ...Matcher) (Ads, error) { + filter := pageopt.MergeM(mats) + ads := Ads{} + if err := coll(nil).FindOne(&ads, filter); err != nil { + return Ads{}, err + } + if ads.ID.IsZero() { + return Ads{}, ADSNotExistError{} + } + return ads, nil +} + +func FindOneByID(id ObjectID) (Ads, error) { + return findOne((&IDMatch{ID: &id}).New()) +} + +// IncAdsClick 增加广告一次点击 +func IncAdsClick(id primitive.ObjectID) error { + filter := bson.M{"_id": id} + update := bson.M{"$inc": bson.M{"click": 1}} + if _, err := coll(nil).UpdateOne(filter, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncAdsClick", table, "UpdateOne", err), log.Any("id", id)) + return err + } + return nil +} + +var ActiveIsTrue = true + +// ActiveList ActiveList +func ActiveList(sort Sort, skip, limit *int64, matchs ...Matcher) (AdsSlice, error) { + matchs = append(matchs, (&ActiveMatch{Active: &ActiveIsTrue}).New()) + return List(sort, skip, limit, matchs...) +} + +// ActiveStartAndEndList ActiveStartAndEndList +func ActiveStartAndEndList(sort Sort, skip, limit *int64, matchs ...Matcher) (AdsSlice, error) { + now := time.Now() + matchs = append(matchs, + (&StartLTEMatch{t: now}).New(), + (&EndGTMatch{t: now}).New(), + ) + return ActiveList(sort, skip, limit, matchs...) +} diff --git a/models/v/adsmod/error.go b/models/v/adsmod/error.go new file mode 100644 index 0000000..959f0e2 --- /dev/null +++ b/models/v/adsmod/error.go @@ -0,0 +1,9 @@ +package adsmod + +// ADSNotExistError +type ADSNotExistError struct { +} + +func (ADSNotExistError) Error() string { + return "ads is not exist" +} diff --git a/models/v/adsmod/list.go b/models/v/adsmod/list.go new file mode 100644 index 0000000..90b95e0 --- /dev/null +++ b/models/v/adsmod/list.go @@ -0,0 +1,183 @@ +package adsmod + +import ( + "time" + + "91porn-server/common/log" + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type AdsSlice []Ads + +func (r AdsSlice) Len() int { + return len(r) +} + +func (r AdsSlice) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + +func (r AdsSlice) Less(i, j int) bool { + return r[i].SortCode < r[j].SortCode +} + +func (a AdsSlice) ToIDs() []ObjectID { + ids := make([]ObjectID, 0, len(a)) + for _, v := range a { + ids = append(ids, v.ID) + } + return ids +} + +func (a AdsSlice) ToPositions() []AdPosition { + ps := make([]AdPosition, 0, len(a)) + for _, v := range a { + ps = append(ps, v.Position) + } + return ps +} + +func (a AdsSlice) SliceByPosition(position AdPosition) AdsSlice { + ads := make(AdsSlice, 0, len(a)) + for _, v := range a { + if v.Position == position { + ads = append(ads, v) + } + } + return ads +} + +type Matcher = pageopt.Matcher + +// AdsTypeMatch +type AdsTypeMatch struct { + AdsType *AdsType +} + +func (a *AdsTypeMatch) New() Matcher { + return pageopt.NewAssignMatch("adsType", a.AdsType) +} + +// AdPositionMatch +type AdPositionMatch struct { + Position *AdPosition +} + +func (a *AdPositionMatch) New() Matcher { + return pageopt.NewAssignMatch("position", a.Position) +} + +// AdPositionNinMatch +type AdPositionNinMatch struct { + PositionList []AdPosition +} + +func (a *AdPositionNinMatch) New() Matcher { + return pageopt.NewNinMatch("position", a.PositionList) +} + +// DistrictCodeMatch +type DistrictCodeMatch struct { + DistrictCode *string +} + +func (a *DistrictCodeMatch) New() Matcher { + return pageopt.NewAssignMatch("districtCode", a.DistrictCode) +} + +// ActiveMatch +type ActiveMatch struct { + Active *bool +} + +type StartLTEMatch struct { + t time.Time +} + +// IDMatch +type IDMatch = pageopt.IDMatch + +// IDInMatch +type IDInMatch = pageopt.IDInMatch + +type Sort = bson.D + +type EndGTMatch struct { + t time.Time +} + +func (a *ActiveMatch) New() Matcher { + return pageopt.NewAssignMatch("active", a.Active) +} + +func (s *StartLTEMatch) New() Matcher { + return pageopt.NewLTEMatch("start", s.t) +} + +func (e *EndGTMatch) New() Matcher { + return pageopt.NewGTMatch("end", e.t) +} + +var ( + Sort_createdAt_N1 = Sort{{Key: "createdAt", Value: -1}} + Sort_sortCode_N1 = Sort{{Key: "sortCode", Value: -1}} +) + +func List(sort Sort, skip, limit *int64, matchs ...Matcher) (AdsSlice, error) { + filter := pageopt.MergeM(matchs) + opt := (&options.FindOptions{}) + if len(sort) != 0 { + opt.SetSort(sort) + } + if skip != nil { + opt.SetSkip(*skip) + } + if limit != nil { + opt.SetLimit(*limit) + } + list := AdsSlice{} + err := coll(nil).Find(&list, filter, opt) + if err != nil { + log.Error("adsmod List error", log.E(err)) + return nil, err + } + return list, nil +} + +func Count(matchs ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchs) + count, err := coll(nil).Count(filter) + if err != nil { + log.Error("adsmod List error", log.E(err)) + return 0, err + } + return count, nil +} + +func SumClickByID(matchs ...Matcher) (map[ObjectID]int64, error) { + filter := pageopt.MergeM(matchs) + var pipeline []bson.M + pipeline = append(pipeline, bson.M{"$match": filter}) + pipeline = append(pipeline, bson.M{ + "$group": bson.M{ + "_id": "$_id", + "click": bson.M{"$sum": "$click"}, + }, + }) + var list []struct { + ID ObjectID `bson:"_id"` + Click int64 `bson:"click"` //点击次数 + } + err := coll(nil).Aggregate(&list, pipeline) + if err != nil { + return nil, err + } + m := make(map[ObjectID]int64, len(list)) + for _, v := range list { + m[v.ID] += v.Click + } + return m, err +} diff --git a/models/v/adsmod/type.go b/models/v/adsmod/type.go new file mode 100644 index 0000000..cb61802 --- /dev/null +++ b/models/v/adsmod/type.go @@ -0,0 +1,118 @@ +package adsmod + +import ( + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/ysurl" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + SysAds AdsType = iota //系统广告 + DiscAds //商区广告 +) + +const ( + GetAdvertiseList = "GetAdvertiseList" // 获取广告信息 + GetVersionList = "GetVersionList" // 获取版本信息 + GetAnnounceList = "GetAnnounceList" // 获取公告信息 + GetAnnounceByTypeList = "GetAnnounceByTypeList" // 根据类型获取公告信息 + GetMsgAnnounceList = "GetMsgAnnounceList" // 获取消息公告信息 +) + +const ( + StartupPage AdPosition = iota //启动页广告 + HomeRecoSmall //首页-推荐小广告 + HomePopupAnnou //首页-弹窗公告 + NotifyPage //消息-界面广告 + LiaoCarousel //撩吧-轮播广告 + WalletPage //钱包-轮播广告 + SearchPage //搜索-轮播广告 + AvCommentPage //AV解说-轮播广告 + SignPage //签到-轮播广告 +) + +type ( + ObjectID = primitive.ObjectID + + AdsType int + + URLJumpType = ysurl.URLJumpType + + // 广告位 + AdPosition int + + ReviewInfo struct { + ReviewID ObjectID `bson:"reviewID,omitempty"` //审核id + ReviewAt *time.Time `bson:"reviewAt,omitempty"` //审核时间 omitempty 不可删除 + Reviewer *string `bson:"reviewer,omitempty"` //审核人 omitempty 不可删除 + } + + CoverProp = common.PictureProp + + // Ads 广告模型 + Ads struct { + ID ObjectID `bson:"_id,omitempty"` //广告id + AdsType AdsType `bson:"adsType"` //广告类型 + Cover string `bson:"cover"` //广告封面图 + Title string `bson:"title"` //广告标题 + HrefType URLJumpType `bson:"type"` //广告链接类型 + Href string `bson:"href"` //广告跳转类型 + Position AdPosition `bson:"position"` //广告位置 + SortCode int `bson:"sortCode"` //排序号 + Active bool `bson:"active"` //激活状态 true:激活 + Click int `bson:"click"` //点击次数 + Remark string `bson:"remark"` //备注 + DistrictCode string `bson:"districtCode"` //商区码 + Start time.Time `bson:"start"` //开始时间 + End time.Time `bson:"end"` //结束时间 + CreatedAt time.Time `bson:"createdAt"` //创建时间 + UpdatedAt time.Time `bson:"updatedAt"` //更新时间 + ReviewInfo `bson:",inline"` //商区 + CoverProp `bson:",inline"` //封面属性 + } + + InsertDoc struct { + AdsType AdsType `bson:"adsType"` //广告类型 + Cover string `bson:"cover"` //广告封面图 + Title string `bson:"title"` //广告标题 + HrefType URLJumpType `bson:"type"` //广告链接类型 + Href string `bson:"href"` //广告跳转类型 + Position AdPosition `bson:"position"` //广告位置 + SortCode int `bson:"sortCode"` //排序号 + Active bool `bson:"active"` //激活状态 true:激活 + Click int `bson:"click"` //点击次数 + Remark string `bson:"remark"` //备注 + DistrictCode string `bson:"districtCode"` //商区码 + Start time.Time `bson:"start"` //开始时间 + End time.Time `bson:"end"` //结束时间 + CreatedAt time.Time `bson:"createdAt"` //创建时间 + ReviewInfo `bson:",inline"` + CoverProp `bson:",inline"` //封面属性 + } + + UpdateDoc struct { + Title *string `bson:"title,omitempty"` //广告标题 + Cover *string `bson:"cover,omitempty"` //广告封面图 + HrefType *URLJumpType `bson:"type,omitempty"` //广告链接类型 + Href *string `bson:"href,omitempty"` //广告跳转类型 + Position *AdPosition `bson:"position,omitempty"` //广告位置 + SortCode *int `bson:"sortCode,omitempty"` //排序号 + Active *bool `bson:"active,omitempty"` //激活状态 true:激活 + Click *int `bson:"click,omitempty"` //点击次数 + Remark *string `bson:"remark,omitempty"` //备注 + Start *time.Time `bson:"start,omitempty"` //开始时间 + End *time.Time `bson:"end,omitempty"` //结束时间 + UpdatedAt time.Time `bson:"updatedAt,omitempty"` //更新时间 + ReviewInfo `bson:",inline"` + CoverProp `bson:",inline"` //封面属性 + } +) + +func Init() { + mdb = db.Init(table) + initAdsIndex() +} diff --git a/models/v/advanceconfigmod/advanceconfig.go b/models/v/advanceconfigmod/advanceconfig.go new file mode 100644 index 0000000..d800e8a --- /dev/null +++ b/models/v/advanceconfigmod/advanceconfig.go @@ -0,0 +1,130 @@ +package advanceconfigmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.AdvanceConfig + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "sortCode", Value: -1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}}, + }, + { + Keys: bson.D{{Key: "type", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func InsertOne(mt *db.MongoTool, cfg *AdvanceConfig) error { + if _, err := coll(mt).InsertOne(cfg); err != nil { + log.Warn(fmt.Sprintf("[METHOD-InsertOne]==> Model %s InsertOne fail error:%+v:", table, err)) + return err + } + return nil +} + +// FindDataByID 根据ID查询 +func FindDataByID(id string) (AdvanceConfig, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-FindDataByID]==> Model %s ObjectIDFromHex fail error:%+v:", table, err), + log.Any("id", id), + ) + return AdvanceConfig{}, err + } + var aud AdvanceConfig + if err = coll(nil).FindOne(&aud, bson.M{"_id": objID}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-FindDataByID]==> Model %s FindOne fail error:%+v:", table, err), + log.Any("objID", objID), + ) + return aud, err + } + return aud, nil +} + +// FindDataByUID 根据UID查询 +func FindDataByUID(uid uint64) ([]*AdvanceConfig, error) { + var aud []*AdvanceConfig + if err := coll(nil).Find(&aud, bson.M{"uid": uid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-FindDataByUID]==> Model %s FindOne fail error:%+v:", table, err), + log.Any("uid", uid), + ) + return aud, err + } + return aud, nil +} + +// QueryAllDocument 分页查询文档 +func QueryAllDocument(filter primitive.M, opts ...*options.FindOptions) (out []*AdvanceConfig, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllDocument", table, "Find", err), + log.Any("filter", filter), + ) + return + } + return +} + +// CountDocument 查询文档条目数 +func CountDocument(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountDocument", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// IsExist 根据标签名获取标签 +func IsExist(filter primitive.M) (data *AdvanceConfig, err error) { + if err = coll(nil).FindOne(&data, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "isExist", table, "FindOne", err), + log.Any("filter", filter), + ) + return nil, err + } + return data, nil +} + +// Update 修改订单 +func Update(t *db.MongoTool, id primitive.ObjectID, set bson.M) error { + if _, err := coll(t).UpdateOne(bson.M{"_id": id}, set); err != nil { + log.Warn(fmt.Sprintf("[METHOD-Update]==> Model %s UpdateOne fail error:%+v:", table, err), + log.Any("id", id), + log.Any("set", set), + ) + return err + } + return nil +} diff --git a/models/v/advanceconfigmod/app.go b/models/v/advanceconfigmod/app.go new file mode 100644 index 0000000..e919e3e --- /dev/null +++ b/models/v/advanceconfigmod/app.go @@ -0,0 +1,51 @@ +package advanceconfigmod + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +type AppAdvanceRes struct { + List []*AppAdvanceConfig `json:"list" bson:"list"` // 预售配置列表 + CardList []*ProductCard `json:"cardList" bson:"cardList"` // 预售卡配置 + ActivityDetail ActivityDetail `json:"activityDetail" bson:"activityDetail"` // 活动详情 +} + +type AppAdvanceConfig struct { + Name string `json:"name" bson:"name"` // 名称 + Url string `json:"url" bson:"url"` // 配置链接 + Cover string `json:"cover" bson:"cover"` // 封面 + SourceURL string `form:"sourceURL" json:"sourceURL"` // 视频资源链接 + Type int `json:"type" bson:"type"` // 类型 1:宣传视频,2:游戏截图,3:盲盒展示,4:花絮视频,5:下载链接 +} + +type ActivityDetail struct { + StartTime time.Time `json:"startTime" bson:"startTime,omitempty"` // 游戏预售开始时间 + EndTime time.Time `json:"endTime" bson:"endTime,omitempty"` // 游戏预售结束时间 + ActivityTime time.Time `json:"activityTime" bson:"activityTime,omitempty"` // 游戏预售活动时间 + Status int `json:"status" bson:"status"` // 预售订单状态 +} + +type ProductCard struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"productID" ` // ID + Name string `bson:"productName" json:"productName" ` // 商品名字 + Desc string `bson:"desc" json:"desc"` // 内容描述 + Type int `bson:"type" json:"type"` // 会员卡类型 (1:初级,2:高级) + SendGame bool `bson:"sendGame" json:"sendGame"` // 是否赠送游戏 + BGImg string `bson:"bgImg" json:"bgImg"` // 背景图 + OriginalPrice int64 `bson:"originalPrice" json:"originalPrice"` // 原价 + DiscountedPrice int64 `bson:"discountedPrice" json:"discountedPrice"` // 现价 单位角(金币) + DiscountedPriceIos int64 `bson:"discountedPriceIos,omitempty" json:"discountedPriceIos,omitempty"` // ios现价 + DiscountedPriceAnd int64 `bson:"discountedPriceAnd,omitempty" json:"discountedPriceAnd,omitempty"` // 安卓现价 + Position string `bson:"position" json:"position"` // 位置 + NewPrivilege []PrivilegeInfo `bson:"newPrivilege" json:"newPrivilege"` // 新特权 + GameCode []string `bson:"gameCode" json:"gameCode"` // 游戏码 + IsPay bool `json:"isPay" bson:"isPay"` // 是否购买 +} + +// PrivilegeInfo 特权详情 +type PrivilegeInfo struct { + Image string `bson:"img" json:"img"` // 特权图片 + Name string `bson:"privilegeName" json:"privilegeName"` // 特权名称 + Desc string `bson:"privilegeDesc" json:"privilegeDesc"` // 特权描述 +} diff --git a/models/v/advanceconfigmod/type.go b/models/v/advanceconfigmod/type.go new file mode 100644 index 0000000..23cef78 --- /dev/null +++ b/models/v/advanceconfigmod/type.go @@ -0,0 +1,32 @@ +package advanceconfigmod + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +type AdvanceConfig struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + Name string `json:"name" bson:"name"` // 名称 + Url string `json:"url" bson:"url"` // 配置链接 + Cover string `json:"cover" bson:"cover"` // 封面 + SourceURL string `form:"sourceURL" json:"sourceURL"` // 视频资源链接 + Status int `json:"status" bson:"status"` // 配置状态 1:开启,2:关闭 + Type int `json:"type" bson:"type"` // 类型 1:宣传视频,2:游戏截图,3:盲盒展示,4:花絮视频,5:下载链接 + SortCode int `json:"sortCode" bson:"sortCode"` // 排序 + IsDelete bool `json:"isDelete" bson:"isDelete"` // 是否删除 + UpdatedAct string `json:"updatedAct" bson:"updatedAct"` // 操作用户账号 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 文档更新时间 +} + +type AdvanceType int + +const ( + _ = iota + PromotionalVideo // 1 宣传视频 + GameScreenshot // 2 游戏截图 + BlindBox // 3 盲盒展示 + SidelightsVideo // 4 花絮视频 + Download // 5 下载链接 +) diff --git a/models/v/advanceconfigmod/web.go b/models/v/advanceconfigmod/web.go new file mode 100644 index 0000000..72c5e5b --- /dev/null +++ b/models/v/advanceconfigmod/web.go @@ -0,0 +1,126 @@ +package advanceconfigmod + +import ( + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +type QueryAllCond struct { + Page int64 `form:"pageNumber" binding:"required"` // 查询页码 + Limit int64 `form:"pageSize" binding:"required"` // 页码大小 + Type *int `form:"type" json:"type" bson:"type,omitempty"` // 类型 + Status *int `form:"status" json:"status" bson:"status,omitempty"` // 配置状态 +} + +type QueryAllRes struct { + List []*AdvanceConfig `json:"list"` + Total int64 `json:"total"` +} + +func (receiver *QueryAllCond) Filter() bson.M { + var query = bson.M{} + if receiver.Type != nil { + query["type"] = receiver.Type + } + if receiver.Status != nil { + query["status"] = receiver.Status + } + query["isDelete"] = false + return query +} + +func (receiver QueryAllCond) Options() *options.FindOptions { + return options.Find().SetSkip((receiver.Page - 1) * receiver.Limit).SetLimit(receiver.Limit).SetSort(bson.M{"sortCode": -1}) +} + +type WebCreateReq struct { + Name string `json:"name" bson:"name"` // 名称 + Cover string `json:"cover" bson:"cover"` // 封面 + SourceURL string `form:"sourceURL" json:"sourceURL"` // 视频资源链接 + Status int `json:"status" bson:"status"` // 配置状态 1:开启,2:关闭 + Type int `json:"type" bson:"type"` // 类型 1:宣传视频,2:游戏截图,3:盲盒展示,4:花絮视频,5:下载链接 + SortCode int `json:"sortCode" bson:"sortCode"` // 排序 + Url string `json:"url" bson:"url"` // 配置链接 +} + +// Create 发布数据fi +func (p *WebCreateReq) Create(manager string) *AdvanceConfig { + now := time.Now() + data := AdvanceConfig{ + Name: p.Name, + Cover: p.Cover, + Url: p.Url, + Type: p.Type, + SourceURL: p.SourceURL, + SortCode: p.SortCode, + Status: p.Status, + IsDelete: false, + UpdatedAct: manager, + UpdateTime: now, + CreatedAt: now, + } + return &data +} + +type WebUpdateReq struct { + ID string `json:"id" bson:"id" binding:"required"` + Name *string `json:"name" bson:"name,omitempty"` // 名称 + Cover *string `json:"cover" bson:"cover,omitempty"` // 封面图 + Type *int `json:"type" bson:"type,omitempty"` // 类型 + SourceURL *string `form:"sourceURL" json:"sourceURL"` // 视频资源链接 + SortCode *int `json:"sortCode" bson:"sortCode,omitempty"` // 排序 + Status *int `json:"status" bson:"status,omitempty"` // 是否开启 + Url *string `json:"url" bson:"url,omitempty"` // 配置链接 +} + +func (p *WebUpdateReq) Filter() primitive.ObjectID { + id, _ := primitive.ObjectIDFromHex(p.ID) + return id +} + +func (p *WebUpdateReq) Update(updateAct string) bson.M { + var data = bson.M{} + if p.Name != nil { + data["name"] = *p.Name + } + if p.Cover != nil { + data["cover"] = *p.Cover + } + if p.SourceURL != nil { + data["sourceURL"] = *p.SourceURL + } + if p.Type != nil { + data["type"] = *p.Type + } + if p.SortCode != nil { + data["sortCode"] = *p.SortCode + } + if p.Status != nil { + data["status"] = *p.Status + } + if p.Url != nil { + data["url"] = *p.Url + } + data["updatedAct"] = updateAct + data["updateTime"] = time.Now() + return bson.M{"$set": data} +} + +type WebDeleteReq struct { + ID string `json:"id" bson:"id" binding:"required"` // ID +} + +func (p *WebDeleteReq) Filter() primitive.ObjectID { + id, _ := primitive.ObjectIDFromHex(p.ID) + return id +} + +func (p *WebDeleteReq) Del(updateAct string) bson.M { + var data = bson.M{} + data["isDelete"] = true + data["updatedAct"] = updateAct + data["updateTime"] = time.Now() + return bson.M{"$set": data} +} diff --git a/models/v/advanceordermod/advanceorder.go b/models/v/advanceordermod/advanceorder.go new file mode 100644 index 0000000..09401a3 --- /dev/null +++ b/models/v/advanceordermod/advanceorder.go @@ -0,0 +1,254 @@ +package advanceordermod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "errors" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.AdvanceOrder + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "productID", Value: 1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "productID", Value: 1}}, + }, + { + Keys: bson.D{{Key: "oid", Value: 1}}, + }, + //{ + // Keys: bson.D{{Key: "endTime", Value: 1}, {Key: "status", Value: 1}}, + //}, + { + Keys: bson.D{{Key: "status", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}, {Key: "endTime", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func InsertOne(mt *db.MongoTool, cfg *AdvanceOrder) error { + if _, err := coll(mt).InsertOne(cfg); err != nil { + log.Warn(fmt.Sprintf("[METHOD-InsertOne]==> Model %s InsertOne fail error:%+v:", table, err)) + return err + } + return nil +} + +// CreditOrder 创建预付订单 +func CreditOrder(t *db.MongoTool, cfg *AdvanceOrder) (data *AdvanceOrder, err error) { + filter := bson.M{"uid": cfg.UID, "productID": cfg.ProductID} + update := bson.M{"$set": bson.M{ + "uid": cfg.UID, + "advOid": cfg.AdvOid, + "productID": cfg.ProductID, + "status": cfg.Status, + "totalAmount": cfg.TotalAmount, + "advanceAmount": cfg.AdvanceAmount, + "balanceAmount": cfg.BalanceAmount, + "endTime": cfg.EndTime, + "prepaidPrivilege": cfg.PrepaidPrivilege, + "todayUse": cfg.TodayUse, + "createdAt": cfg.CreatedAt, + }} + var advanceOrder *AdvanceOrder + if err := coll(t).FindOneAndUpsert(&advanceOrder, filter, update); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditOrder", table, "FindOneAndUpsert", err)) + return nil, err + } + if advanceOrder.ID.IsZero() { + return nil, errors.New("insert is null") + } + return advanceOrder, nil +} + +// GetUserValidOrder 获取用户当前订单 +func GetUserValidOrder(uid uint64) (hasOrder bool, info *AdvanceOrder) { + n := time.Now() + var cond = bson.M{"uid": uid, "endTime": bson.M{"$gt": n}, "status": bson.M{"$in": []int{AdvanceSUCCESS, BalanceProcessing}}} + var opt = options.FindOne().SetSort(bson.D{{"endTime", -1}}) + if err := coll(nil).FindOne(&info, cond, opt); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetUserValidCount", table, "FindOne", err), + log.Any("uid", uid), + ) + return + } + if info == nil || info.ID.IsZero() { + return + } + + hasOrder = true + return +} + +// Debit 扣除每日使用次数 +func Debit(t *db.MongoTool, uid uint64, debitPlan DebitPlan) error { + // 先查找用户订单 + hasOrder, adOrder := GetUserValidOrder(uid) + if !hasOrder { + return errors.New("user advance order not found") + } + + // 检查次数是否够用 + if debitPlan.CoinVideoCount != nil { + if adOrder.PrepaidPrivilege.CoinVideoLimitPerDay-adOrder.TodayUse.CoinVideoCount < *debitPlan.CoinVideoCount { + return errors.New("free count not enough") + } + } + if debitPlan.LuckyDrawCount != nil { + if adOrder.PrepaidPrivilege.LuckyDrawLimitPerDay-adOrder.TodayUse.LuckyDrawCount < *debitPlan.LuckyDrawCount { + return errors.New("free count not enough") + } + } + if debitPlan.AiUndressCount != nil { + if adOrder.PrepaidPrivilege.AiUndressLimitPerDay-adOrder.TodayUse.AiUndressCount < *debitPlan.AiUndressCount { + return errors.New("free count not enough") + } + } + if debitPlan.DownloadCount != nil { + if adOrder.PrepaidPrivilege.DownloadLimitPerDay-adOrder.TodayUse.DownloadCount < *debitPlan.DownloadCount { + return errors.New("free count not enough") + } + } + + if _, err := coll(t).UpdateOne(bson.M{"_id": adOrder.ID}, bson.M{ + "$inc": debitPlan, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Debit", table, "UpdateOne", err), + log.Any("debitPlan", debitPlan), + log.Any("id", adOrder.ID), + ) + return err + } + + return nil +} + +// ResetToday 重置当天使用次数 +func ResetToday() error { + n := time.Now() + filter := bson.M{"endTime": bson.M{"$gt": n}, "status": bson.M{"$in": []int{AdvanceSUCCESS, BalanceProcessing}}} + if _, err := coll(nil).UpdateMany(filter, bson.M{ + "$set": bson.M{ + "todayUse.coinVideoCount": int64(0), + "todayUse.luckyDrawCount": int64(0), + "todayUse.aiUndressCount": int64(0), + "todayUse.downloadCount": int64(0), + }, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ResetToday", table, "UpdateMany", err)) + return err + } + + return nil +} + +// FindDataByID 根据ID查询 +func FindDataByID(id string) (AdvanceOrder, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-FindDataByID]==> Model %s ObjectIDFromHex fail error:%+v:", table, err), + log.Any("id", id), + ) + return AdvanceOrder{}, err + } + var aud AdvanceOrder + if err = coll(nil).FindOne(&aud, bson.M{"_id": objID}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-FindDataByID]==> Model %s FindOne fail error:%+v:", table, err), + log.Any("objID", objID), + ) + return aud, err + } + return aud, nil +} + +// FindDataByUID 根据UID查询 +func FindDataByUID(uid uint64) ([]*AdvanceOrder, error) { + var aud []*AdvanceOrder + if err := coll(nil).Find(&aud, bson.M{"uid": uid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-FindDataByUID]==> Model %s FindOne fail error:%+v:", table, err), + log.Any("uid", uid), + ) + return aud, err + } + return aud, nil +} + +// QueryAllDocument 分页查询文档 +func QueryAllDocument(filter primitive.M, opts ...*options.FindOptions) (out []*AdvanceOrder, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllDocument", table, "Find", err), + log.Any("filter", filter), + ) + return + } + return +} + +// CountDocument 查询文档条目数 +func CountDocument(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountDocument", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// IsExist 根据标签名获取标签 +func IsExist(filter primitive.M) (data *AdvanceOrder, err error) { + if err = coll(nil).FindOne(&data, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "isExist", table, "FindOne", err), + log.Any("filter", filter), + ) + return nil, err + } + return data, nil +} + +// Update 修改订单 +func Update(t *db.MongoTool, id primitive.ObjectID, set bson.M) error { + set["updatedAt"] = time.Now() + if _, err := coll(t).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-Update]==> Model %s UpdateOne fail error:%+v:", table, err), + log.Any("id", id), + log.Any("set", set), + ) + return err + } + return nil +} diff --git a/models/v/advanceordermod/app.go b/models/v/advanceordermod/app.go new file mode 100644 index 0000000..a4efa5f --- /dev/null +++ b/models/v/advanceordermod/app.go @@ -0,0 +1,56 @@ +package advanceordermod + +import "time" + +// AdvanceStatus 预售状态 +type AdvanceStatus struct { + ActivityStatus bool `json:"activityStatus" bson:"activityStatus"` // 预售活动是否开启 + ActivityPopUp bool `json:"activityPopUp" bson:"activityPopUp"` // 预售活动是否首页弹窗 + GameActivityPopUp bool `json:"gameActivityPopUp" bson:"gameActivityPopUp"` // 游戏预售是否首页弹窗 + GameActivityStatus bool `json:"gameActivityStatus" bson:"gameActivityStatus"` // 游戏预售是否开启 + BalancePayment bool `json:"balancePayment" bson:"balancePayment"` // 是否有尾款支付 + BalanceAmount int64 `bson:"balanceAmount" json:"balanceAmount"` // 尾款金额 + Status int `json:"status" bson:"status"` // 预售订单状态 + StartTime time.Time `json:"startTime" bson:"startTime,omitempty"` // 预售尾款开始时间 + EndTime time.Time `json:"endTime" bson:"endTime,omitempty"` // 预售尾款结束时间 + OId string `json:"oid" bson:"oid"` // 预付订单号 + PrivilegeLimit PrivilegeLimit `json:"privilegeLimit"` // 预付权益 +} + +type PrivilegeLimit struct { + HasLimit bool `json:"hasLimit"` // 是否有权益限制 + Limit struct { + CoinVideoLimitPerDay int64 `json:"coinVideoLimitPerDay"` // 每日金币视频免费次数 + LuckyDrawLimitPerDay int64 `json:"luckyDrawLimitPerDay"` // 每日抽奖次数限制 + AiUndressLimitPerDay int64 `json:"aiUndressLimitPerDay"` // 每日ai脱衣次数限制 + DownloadLimitPerDay int64 `json:"downloadLimitPerDay"` // 每日下载次数限制 + } `json:"limit"` // 每日限制 + Remain struct { + TodayCoinVideoCount int64 `json:"todayCoinVideoCount"` // 今日剩余金币视频观看次数 + TodayLuckyDrawCount int64 `json:"todayLuckyDrawCount"` // 今日剩余抽奖次数 + TodayAiUndressCount int64 `json:"todayAiUndressCount"` // 今日剩余ai脱衣次数 + TodayDownloadCount int64 `json:"todayDownloadCount"` // 今日剩余下载次数 + } `json:"remain"` // 当日可使用次数 +} + +type DebitPlan struct { + CoinVideoCount *int64 `bson:"todayUse.coinVideoCount,omitempty"` // 金币视频观看次数 + LuckyDrawCount *int64 `bson:"todayUse.luckyDrawCount,omitempty"` // 抽奖次数 + AiUndressCount *int64 `bson:"todayUse.aiUndressCount,omitempty"` // ai脱衣次数 + DownloadCount *int64 `bson:"todayUse.downloadCount,omitempty"` // 下载次数 +} + +func (d *DebitPlan) InvertCounts() { + if d.CoinVideoCount != nil { + *d.CoinVideoCount = -(*d.CoinVideoCount) + } + if d.LuckyDrawCount != nil { + *d.LuckyDrawCount = -(*d.LuckyDrawCount) + } + if d.AiUndressCount != nil { + *d.AiUndressCount = -(*d.AiUndressCount) + } + if d.DownloadCount != nil { + *d.DownloadCount = -(*d.DownloadCount) + } +} diff --git a/models/v/advanceordermod/type.go b/models/v/advanceordermod/type.go new file mode 100644 index 0000000..12fa2af --- /dev/null +++ b/models/v/advanceordermod/type.go @@ -0,0 +1,62 @@ +package advanceordermod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + DEFAULT = iota // 0 默认 + AdvanceProcessing // 1 预付中 + AdvanceSUCCESS // 2 预付成功 + BalanceProcessing // 3 尾款预付中 + BalanceSUCCESS // 4 尾款预付成功 +) + +type AdvanceOrder struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + UID uint64 `json:"uid" bson:"uid"` // 用户ID + AdvOid string `json:"advOid" bson:"advOid"` // 预付支付平台订单号 + BalOid string `json:"balOid" bson:"balOid"` // 尾款支付平台订单号 + ProductID primitive.ObjectID `json:"productID" bson:"productID"` // 预售产品ID + Status int `json:"status" bson:"status" ` // 预售订单状态 + TotalAmount int64 `bson:"totalAmount" json:"totalAmount" ` // 预付卡总金额 + AdvanceAmount int64 `bson:"advanceAmount" json:"advanceAmount" ` // 预付金额 + BalanceAmount int64 `bson:"balanceAmount" json:"balanceAmount"` // 尾款金额 + StartTime time.Time `json:"startTime" bson:"startTime,omitempty"` // 预售尾款开始时间 + EndTime time.Time `json:"endTime" bson:"endTime,omitempty"` // 预售尾款结束时间 + PrepaidPrivilege AdvanceCardPrepaidPrivilege `json:"prepaidPrivilege" bson:"prepaidPrivilege"` // 预付后的权益 + TodayUse DayUse `json:"todayUse" bson:"todayUse"` // 当天使用次数 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` // 刷新时间 +} + +type AdvanceCardPrepaidPrivilege struct { + CoinVideoLimitPerDay int64 `json:"coinVideoLimitPerDay" bson:"coinVideoLimitPerDay"` // 每日金币视频免费次数 + LuckyDrawLimitPerDay int64 `json:"luckyDrawLimitPerDay" bson:"luckyDrawLimitPerDay"` // 每日抽奖次数限制 + AiUndressLimitPerDay int64 `json:"aiUndressLimitPerDay" bson:"aiUndressLimitPerDay"` // 每日ai脱衣次数限制 + DownloadLimitPerDay int64 `json:"downloadLimitPerDay" bson:"downloadLimitPerDay"` // 每日下载次数限制 +} + +type DayUse struct { + CoinVideoCount int64 `json:"coinVideoCount" bson:"coinVideoCount"` // 金币视频观看次数 + LuckyDrawCount int64 `json:"luckyDrawCount" bson:"luckyDrawCount"` // 抽奖次数 + AiUndressCount int64 `json:"aiUndressCount" bson:"aiUndressCount"` // ai脱衣次数 + DownloadCount int64 `json:"downloadCount" bson:"downloadCount"` // 下载次数 +} + +type AdvanceOrderExport struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty" xlsx:"ID"` + UID uint64 `json:"uid" bson:"uid" xlsx:"用户ID"` // 用户ID + AdvOid string `json:"advOid" bson:"advOid" xlsx:"预付支付平台订单号"` // 预付支付平台订单号 + BalOid string `json:"balOid" bson:"balOid" xlsx:"尾款支付平台订单号"` // 尾款支付平台订单号 + ProductID primitive.ObjectID `json:"productID" bson:"productID" xlsx:"预售产品ID"` // 预售产品ID + Status string `json:"status" bson:"status" xlsx:"预售订单状态"` // 预售订单状态 + TotalAmount int64 `bson:"totalAmount" json:"totalAmount" xlsx:"预付卡总金额"` // 预付卡总金额 + AdvanceAmount int64 `bson:"advanceAmount" json:"advanceAmount" xlsx:"预付金额"` // 预付金额 + BalanceAmount int64 `bson:"balanceAmount" json:"balanceAmount" xlsx:"尾款金额"` // 尾款金额 + StartTime time.Time `json:"startTime" bson:"startTime,omitempty" xlsx:"预售尾款开始时间"` // 预售尾款开始时间 + EndTime time.Time `json:"endTime" bson:"endTime,omitempty" xlsx:"预售尾款结束时间"` // 预售尾款结束时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty" xlsx:"创建时间"` // 创建时间 +} diff --git a/models/v/advanceordermod/web.go b/models/v/advanceordermod/web.go new file mode 100644 index 0000000..cab7214 --- /dev/null +++ b/models/v/advanceordermod/web.go @@ -0,0 +1,49 @@ +package advanceordermod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type QueryAllCond struct { + Page int64 `form:"pageNumber" json:"pageNumber" binding:"required"` // 查询页码 + Limit int64 `form:"pageSize" json:"pageSize" binding:"required"` // 页码大小 + AdvOid *string `form:"advOid" json:"advOid" bson:"advOid"` // 预付订单号 + BalOid *string `form:"balOid" json:"balOid" bson:"balOid"` // 尾款订单号 + Uid *int `form:"uid" json:"uid" bson:"uid"` // 用户ID + Status *int `form:"status" json:"status" bson:"status"` // 预售订单状态 + TimeField string `form:"timeField" json:"timeField"` // 时间筛选字段 createdAt 创建时间 endTime 预售尾款结束时间 + Start *time.Time `form:"start" json:"start"` // 开始时间 + End *time.Time `form:"end" json:"end"` // 结束时间 +} + +type QueryAllRes struct { + List []*AdvanceOrder `json:"list"` + Total int64 `json:"total"` +} + +func (receiver *QueryAllCond) Filter() bson.M { + var query = bson.M{} + if receiver.Uid != nil { + query["uid"] = receiver.Uid + } + if receiver.Status != nil { + query["status"] = receiver.Status + } + if receiver.AdvOid != nil { + query["advOid"] = receiver.AdvOid + } + if receiver.BalOid != nil { + query["balOid"] = receiver.BalOid + } + if receiver.TimeField != "" && receiver.Start != nil && receiver.End != nil { + query[receiver.TimeField] = bson.M{"$gte": receiver.Start, "$lt": receiver.End} + } + return query +} + +func (receiver QueryAllCond) Options() *options.FindOptions { + return options.Find().SetSkip((receiver.Page - 1) * receiver.Limit).SetLimit(receiver.Limit).SetSort(bson.M{"createdAt": -1}) +} diff --git a/models/v/aiUnDressmod/aiUnDress.go b/models/v/aiUnDressmod/aiUnDress.go new file mode 100644 index 0000000..0c71ee6 --- /dev/null +++ b/models/v/aiUnDressmod/aiUnDress.go @@ -0,0 +1,198 @@ +package aiUnDressmod + +import ( + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.AiUnDress + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitARDIndex 设置账户充值流水表索引 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("AiUnDressIndex err+%v", err)) + } +} + +// Insert 插入一条订单 +func Insert(a *AiUnDress) error { + a.CreatedAt = time.Now() + a.UpdatedAt = a.CreatedAt + res, err := coll(nil).InsertOne(a) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-Insert]==> Model %s InsertOne fail error:%+v:", table, err)) + return err + } + a.ID, _ = res.InsertedID.(primitive.ObjectID) + return nil +} + +func InsertOne(mt *db.MongoTool, cfg AiUnDress) error { + if _, err := coll(mt).InsertOne(cfg); err != nil { + log.Warn(fmt.Sprintf("[METHOD-InsertOne]==> Model %s InsertOne fail error:%+v:", table, err)) + return err + } + return nil +} + +// FindOrders 查询脱衣订单 +func FindOrders(cond bson.M, opts *options.FindOptions) (int64, []*AiUnDress, error) { + data := make([]*AiUnDress, 0) + if err := coll(nil).Find(&data, cond, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-FindOrders]==> Model %s Find fail error:%+v:", table, err), + log.Any("cond", cond), + ) + return 0, data, err + } + total, err := coll(nil).Count(cond) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-FindOrders]==> Model %s Count fail error:%+v:", table, err), + log.Any("cond", cond), + ) + return total, data, err + } + return total, data, nil +} + +// Update 修改订单 +func Update(t *db.MongoTool, id primitive.ObjectID, set bson.M) error { + set["updatedAt"] = time.Now() + if _, err := coll(t).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-Update]==> Model %s UpdateOne fail error:%+v:", table, err), + log.Any("id", id), + log.Any("set", set), + ) + return err + } + return nil +} + +// TransitionStatus atomically changes an order from the expected status. The +// returned boolean is false when another worker or callback handled it first. +func TransitionStatus(t *db.MongoTool, id primitive.ObjectID, from, to int, set bson.M) (bool, error) { + if set == nil { + set = bson.M{} + } + set["status"] = to + set["updatedAt"] = time.Now() + result, err := coll(t).UpdateOne(bson.M{"_id": id, "status": from}, bson.M{"$set": set}) + if err != nil { + return false, err + } + return result.ModifiedCount == 1, nil +} + +// Edit 修改文档 +func Edit(t *db.MongoTool, filter, update primitive.M) error { + result, err := coll(t).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-Edit]==> Model %s UpdateOne fail error:%+v:", table, err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + if result.ModifiedCount == 0 { + log.Error(fmt.Sprintf("[METHOD-Edit]==> Model %s result.ModifiedCount fail error: ModifiedCount is zero", table), + log.Any("filter", filter), + log.Any("update", update), + ) + return errors.New("result is null") + } + return nil +} + +// FindDataByID 根据ID查询 +func FindDataByID(id string) (AiUnDress, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-FindDataByID]==> Model %s ObjectIDFromHex fail error:%+v:", table, err), + log.Any("id", id), + ) + return AiUnDress{}, err + } + var aud AiUnDress + if err = coll(nil).FindOne(&aud, bson.M{"_id": objID}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-FindDataByID]==> Model %s FindOne fail error:%+v:", table, err), + log.Any("objID", objID), + ) + return aud, err + } + return aud, nil +} + +// QueryAllDocument 分页查询文档 +func QueryAllDocument(filter primitive.M, opts ...*options.FindOptions) ([]*AiUnDress, error) { + var out []*AiUnDress + if err := coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-QueryAllDocument]==> Model %s Find fail error:%+v:", table, err), + log.Any("filter", filter), + ) + return nil, err + } + return out, nil +} + +// CountDocument 查询文档条目数 +func CountDocument(filter primitive.M) (int64, error) { + count, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-CountDocument]==> Model %s Count fail error:%+v:", table, err), + log.Any("filter", filter), + ) + return 0, err + } + return count, nil +} + +func FindByID(t *db.MongoTool, id primitive.ObjectID) (*AiUnDress, error) { + var acf AiUnDress + return &acf, coll(t).FindOne(&acf, bson.M{"_id": id}) +} + +func InsertMany(mt *db.MongoTool, cfg []AiUnDress) error { + if _, err := coll(mt).InsertMany(cfg); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertMany", table, "InsertMany", err)) + return err + } + return nil +} + +// 更新订单提交状态 +func SubmitStatus(id string, status int, remark string) error { + _id, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-SubmitStatus]==> Model %s ObjectIDFromHex fail error:%+v:", table, err), log.Any("id", id)) + return err + } + _, err = coll(nil).UpdateOne(bson.M{"_id": _id}, bson.M{"$set": bson.M{"status": status, "newPic": nil, "remark": remark}}) + return err +} diff --git a/models/v/aiUnDressmod/app.go b/models/v/aiUnDressmod/app.go new file mode 100644 index 0000000..f94caaf --- /dev/null +++ b/models/v/aiUnDressmod/app.go @@ -0,0 +1,165 @@ +package aiUnDressmod + +import ( + "time" + + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// AiUnDressAppRes AI记录 App返回给前端 +type AiUnDressAppRes struct { + UID uint64 `json:"uid" bson:"uid"` // 用户id + OriginPic string `json:"originPic" bson:"originPic"` // 脱衣原图 + OriginPics []string `json:"originPics" bson:"originPics"` // 脱衣原图多张 + NewPic []string `json:"newPic" bson:"newPic"` // 脱衣后新图 + Coin int64 `json:"coin" bson:"coin"` // 此次脱衣金币个数 + Status int `json:"status" bson:"status"` // 1、进行中 2、生成成功 3、生成失败 + Remark string `json:"remark" bson:"remark"` // 备注 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 刷新时间 +} + +type ListRequest struct { + Status *int `form:"status" json:"status" bson:"status"` // 1、进行中 2、生成成功 3、生成失败 + commod.Page +} + +func (receiver *ListRequest) Filter(uid uint64) primitive.M { + filter := bson.M{} + if receiver.Status != nil { + if *receiver.Status == Processing { + filter["status"] = bson.M{"$in": []int{Processing, SubmitOrder}} + } else { + filter["status"] = receiver.Status + } + } + filter["uid"] = uid + filter["isHide"] = bson.M{"$in": []any{nil, false}} + return filter +} + +func (receiver *ListRequest) Options() *options.FindOptions { + return options.Find().SetSkip(int64(receiver.Skip())).SetLimit(int64(receiver.Limit())).SetSort(bson.M{"updatedAt": -1}) +} + +type GenerateRequest struct { + OriginPic []string `json:"originPic" bson:"originPic"` // 脱衣原图 + Coin int64 `json:"coin" bson:"coin"` // 此次脱衣金币个数 + IsFreeTimes bool `json:"isFreeTimes" bson:"isFreeTimes"` // 是否使用免费次数 + ShareTitle string `json:"shareTitle"` // 分享标题 + ShareStatus int `json:"shareStatus" bson:"shareStatus"` // 是否分享 0-不分享 1-分享 +} + +//func (receiver *GenerateRequest) Generate(uid uint64) AiUnDress { +// now := time.Now() +// return AiUnDress{ +// OriginPic: receiver.OriginPic, +// UID: uid, +// Coin: receiver.Coin, +// Status: Processing, +// IsHide: false, +// UpdatedAt: now, +// CreatedAt: now, +// } +//} + +func (receiver *GenerateRequest) GenerateMany(orderId primitive.ObjectID, orderCreatedAt time.Time, uid uint64, privilegeCount, debitFreeCount, debitCoins, debitIncomeCoins, singeCoin int64, shareTitle string, shareStatus int) []AiUnDress { + var generateAiUnDress []AiUnDress + if len(receiver.OriginPic) > 0 { + p := receiver.OriginPic[0] + //for _, p := range receiver.OriginPic { + var ( + picture []string + newCoin int64 + newDebitAmountCoin int64 + newDebitIncomeCoin int64 + newDebitFreeCount int64 + isFreeTimes bool + ) + picture = append(picture, p) + + if debitFreeCount > 0 { + isFreeTimes = true + newDebitFreeCount = 1 + // 免费次数减一 + debitFreeCount-- + } else if privilegeCount > 0 { + isFreeTimes = true + newDebitFreeCount = 1 + privilegeCount-- + } else { + if debitCoins > 0 { + newDebitAmountCoin = min(singeCoin, debitCoins) + debitCoins -= newDebitAmountCoin + newCoin += newDebitAmountCoin + } else { + newDebitIncomeCoin = min(singeCoin, debitIncomeCoins) + debitIncomeCoins -= newDebitIncomeCoin + newCoin += debitIncomeCoins + } + if newCoin < singeCoin { + if debitCoins >= singeCoin-newCoin { + new2DebitAmountCoin := min(singeCoin-newCoin, debitCoins) + debitCoins -= new2DebitAmountCoin + newDebitAmountCoin += new2DebitAmountCoin + newCoin += new2DebitAmountCoin + } else { + new2DebitIncomeCoin := min(singeCoin-newCoin, debitIncomeCoins) + debitIncomeCoins -= new2DebitIncomeCoin + newDebitIncomeCoin += new2DebitIncomeCoin + newCoin += new2DebitIncomeCoin + } + } + } + + generateAiUnDress = append(generateAiUnDress, AiUnDress{ + ID: orderId, + OriginPics: picture, + Coin: newDebitAmountCoin + newDebitIncomeCoin, + DebitAmountCoin: newDebitAmountCoin, + DebitIncomeCoin: newDebitIncomeCoin, + IsFreeTimes: isFreeTimes, + Count: newDebitFreeCount, + UID: uid, + Status: Processing, + IsHide: false, + ShareTitle: shareTitle, + ShareStatus: shareStatus, + UpdatedAt: orderCreatedAt, + CreatedAt: orderCreatedAt, + }) + } + + //} + + return generateAiUnDress +} + +// 定义一个函数,用来返回两个整数中的较小值 +func min(a, b int64) int64 { + if a < b { + return a + } + return b +} + +type DelRequest struct { + ID primitive.ObjectID `json:"id" bson:"id"` // AI订单ID +} + +func (receiver *DelRequest) Filter() primitive.M { + filter := bson.M{} + filter["_id"] = receiver.ID + return filter +} + +func (receiver *DelRequest) Update() primitive.M { + update := bson.M{} + update["isHide"] = true + update["updatedAt"] = time.Now() + return bson.M{"$set": update} +} diff --git a/models/v/aiUnDressmod/type.go b/models/v/aiUnDressmod/type.go new file mode 100644 index 0000000..d74f6bf --- /dev/null +++ b/models/v/aiUnDressmod/type.go @@ -0,0 +1,64 @@ +package aiUnDressmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + Processing = 1 // 1、进行中 + SUCCESS = 2 // 2、成功 + FAILURE = 3 // 3、失败 + REFUND = 4 // 4、退款 + PartSuccess = 5 // 5、部分成功 + SubmitOrder = 6 // 6、已提交 + +) + +type Status int + +var status = map[Status]string{ + Processing: "processing", + FAILURE: "failure", + SUCCESS: "success", + REFUND: "refund", +} + +func (s Status) Desc() string { + if desc, ok := status[s]; ok { + return desc + } + return "" +} + +var mdb *db.MongoDB + +// AiUnDress AI脱衣 +type AiUnDress struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 脱衣记录id + UID uint64 `json:"uid" bson:"uid"` // 用户id + OriginPic string `json:"originPic" bson:"originPic"` // 脱衣原图 + OriginPics []string `json:"originPics" bson:"originPics"` // 脱衣原图多张 + NewPic []string `json:"newPic" bson:"newPic"` // 脱衣后新图 + Coin int64 `json:"coin" bson:"coin"` // 此次脱衣金币个数 + DebitAmountCoin int64 `json:"debitAmountCoin" bson:"debitAmountCoin"` // 此次脱衣扣除金币个数 + DebitIncomeCoin int64 `json:"debitIncomeCoin" bson:"debitIncomeCoin"` // 此次脱衣扣除收益金币个数 + Status int `json:"status" bson:"status"` // 1、进行中 2、生成成功 3、生成失败 4、退款 5、部分成功 6、已提交 + Count int64 `json:"count" bson:"count"` // 此次ai脱衣消耗次数 + IsFreeTimes bool `json:"isFreeTimes" bson:"isFreeTimes"` // 是否使用免费次数 + IsHide bool `json:"isHide" bson:"isHide"` // 是否被用户隐藏(用户订单列表是否不显示) + Remark string `json:"remark" bson:"remark"` // 备注 + UpdateAct string `json:"updateAct" bson:"updateAct"` // 修改账号 + ShareTitle string `json:"shareTitle" bson:"shareTitle"` // 分享标题 + ShareStatus int `json:"shareStatus" bson:"shareStatus"` // 是否分享 0-不分享 1-分享 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 刷新时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/aiUnDressmod/web.go b/models/v/aiUnDressmod/web.go new file mode 100644 index 0000000..84abe38 --- /dev/null +++ b/models/v/aiUnDressmod/web.go @@ -0,0 +1,105 @@ +package aiUnDressmod + +import ( + "time" + + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// RechargeUpdateReq 修改参数 +type RechargeUpdateReq struct { + StatusDesc *string `json:"statusDesc" bson:"statusDesc"` // 状态描述 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 +} + +type RchgQueryReq struct { + ID *string `form:"id" json:"_id,omitempty" bson:"_id"` // 流水id + UID *uint64 `form:"uid" json:"uid,omitempty" bson:"uid"` // 用户id + Status *int `form:"status" json:"status,omitempty" bson:"status"` // 1、进行中 2、生成成功 3、生成失败 4、已经退款 +} + +type WebListRequest struct { + Status *int `form:"status" json:"status" bson:"status"` // 1、进行中 2、生成成功 3、生成失败 4、已经退款 + UID *uint64 `form:"uid" json:"uid,omitempty" bson:"uid"` // 用户id + ID *string `form:"id" json:"_id,omitempty" bson:"_id"` // 流水id + commod.Page +} + +func (receiver *WebListRequest) Filter() primitive.M { + filter := bson.M{} + if receiver.Status != nil { + filter["status"] = receiver.Status + } + if receiver.UID != nil { + filter["uid"] = receiver.UID + } + if receiver.ID != nil { + id, _ := primitive.ObjectIDFromHex(*receiver.ID) + filter["_id"] = id + } + return filter +} + +func (receiver *WebListRequest) Options() *options.FindOptions { + return options.Find().SetSkip(int64(receiver.Skip())).SetLimit(int64(receiver.Limit() + 1)).SetSort(bson.M{"updatedAt": -1}) +} + +type EditCond struct { + ID string `json:"id" binding:"required"` // 文档 + NewPic *[]string `json:"newPic" bson:"newPic"` // 脱衣后新图 + Status *int `json:"status" bson:"status"` // 状态 1、进行中 2、生成成功 3、生成失败 + Remark *string `json:"remark" bson:"remark"` // 拒绝理由 +} + +type AutoCond struct { + ID string `json:"id" binding:"required"` // 文档 +} + +type AutoBatchCond struct { + IDs []string `json:"ids" from:"ids" binding:"required"` // 文档 + Pass bool `json:"pass" from:"pass"` // 是否通过 + Remark string `json:"remark" from:"remark"` // 拒绝理由 +} + +func (receiver *EditCond) Filter() bson.M { + objID, _ := primitive.ObjectIDFromHex(receiver.ID) + return bson.M{"_id": objID} +} + +func (receiver *EditCond) Update(updateAct string) bson.M { + var update = bson.M{} + if receiver.NewPic != nil { + update["newPic"] = receiver.NewPic + } + if receiver.Status != nil { + update["status"] = receiver.Status + } + if receiver.Remark != nil { + update["remark"] = receiver.Remark + } + update["updateAct"] = updateAct + update["updatedAt"] = time.Now() + return bson.M{"$set": update} +} + +type AiUndressOrderReq struct { + AppId int `json:"appId"` + FileUrl []string `json:"fileUrl"` //base64存的文件服地址 + UserId string `json:"userId"` //用户id + AppOrderNum string `json:"appOrderNum"` //app中生成的订单号 + NotifyUrl string `json:"notifyUrl"` //回调产品地址 +} + +type AiUndressOrderResp struct { + Code int `json:"code"` +} + +type CallBackReq struct { + ImgUrl []string `json:"imgUrl"` //脱衣后地址 + AppOrderNum string `json:"appOrderNum"` //app订单号 + Msg string `json:"msg"` //消息 +} diff --git a/models/v/aichangefaceimgmod/aichangefaceimg.go b/models/v/aichangefaceimgmod/aichangefaceimg.go new file mode 100644 index 0000000..aae765b --- /dev/null +++ b/models/v/aichangefaceimgmod/aichangefaceimg.go @@ -0,0 +1,177 @@ +package aichangefaceimgmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "errors" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +const table = models.AiChangeFaceImg + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitARDIndex 设置账户充值流水表索引 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("AiUnDressIndex err+%v", err)) + } +} + +// Insert 插入一条订单 +func Insert(a *AiChangeFaceImg) error { + a.CreatedAt = time.Now() + a.UpdatedAt = a.CreatedAt + res, err := coll(nil).InsertOne(a) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-Insert]==> Model %s InsertOne fail error:%+v:", table, err)) + return err + } + a.ID, _ = res.InsertedID.(primitive.ObjectID) + return nil +} + +func InsertOne(mt *db.MongoTool, cfg AiChangeFaceImg) error { + if _, err := coll(mt).InsertOne(cfg); err != nil { + log.Warn(fmt.Sprintf("[METHOD-InsertOne]==> Model %s InsertOne fail error:%+v:", table, err)) + return err + } + return nil +} + +// FindOrders 查询AI换脸订单 +func FindOrders(cond bson.M, opts *options.FindOptions) (int64, []*AiChangeFaceImg, error) { + data := make([]*AiChangeFaceImg, 0) + if err := coll(nil).Find(&data, cond, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-FindOrders]==> Model %s Find fail error:%+v:", table, err), + log.Any("cond", cond), + ) + return 0, data, err + } + total, err := coll(nil).Count(cond) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-FindOrders]==> Model %s Count fail error:%+v:", table, err), + log.Any("cond", cond), + ) + return total, data, err + } + return total, data, nil +} + +// Update 修改订单 +func Update(t *db.MongoTool, id primitive.ObjectID, set bson.M) error { + set["updatedAt"] = time.Now() + if _, err := coll(t).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-Update]==> Model %s UpdateOne fail error:%+v:", table, err), + log.Any("id", id), + log.Any("set", set), + ) + return err + } + return nil +} + +// TransitionStatus atomically changes an order from the expected status. The +// returned boolean is false when another worker or callback handled it first. +func TransitionStatus(t *db.MongoTool, id primitive.ObjectID, from, to int, set bson.M) (bool, error) { + if set == nil { + set = bson.M{} + } + set["status"] = to + set["updatedAt"] = time.Now() + result, err := coll(t).UpdateOne(bson.M{"_id": id, "status": from}, bson.M{"$set": set}) + if err != nil { + return false, err + } + return result.ModifiedCount == 1, nil +} + +// Edit 修改文档 +func Edit(t *db.MongoTool, filter, update primitive.M) error { + result, err := coll(t).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-Edit]==> Model %s UpdateOne fail error:%+v:", table, err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + if result.ModifiedCount == 0 { + log.Error(fmt.Sprintf("[METHOD-Edit]==> Model %s result.ModifiedCount fail error: ModifiedCount is zero", table), + log.Any("filter", filter), + log.Any("update", update), + ) + return errors.New("result is null") + } + return nil +} + +// FindDataByID 根据ID查询 +func FindDataByID(id string) (AiChangeFaceImg, error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-FindDataByID]==> Model %s ObjectIDFromHex fail error:%+v:", table, err), + log.Any("id", id), + ) + return AiChangeFaceImg{}, err + } + var aud AiChangeFaceImg + if err = coll(nil).FindOne(&aud, bson.M{"_id": objID}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-FindDataByID]==> Model %s FindOne fail error:%+v:", table, err), + log.Any("objID", objID), + ) + return aud, err + } + return aud, nil +} + +// QueryAllDocument 分页查询文档 +func QueryAllDocument(filter primitive.M, opts ...*options.FindOptions) ([]*AiChangeFaceImg, error) { + var out []*AiChangeFaceImg + if err := coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-QueryAllDocument]==> Model %s Find fail error:%+v:", table, err), + log.Any("filter", filter), + ) + return nil, err + } + return out, nil +} + +// CountDocument 查询文档条目数 +func CountDocument(filter primitive.M) (int64, error) { + count, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-CountDocument]==> Model %s Count fail error:%+v:", table, err), + log.Any("filter", filter), + ) + return 0, err + } + return count, nil +} + +func FindByID(t *db.MongoTool, id primitive.ObjectID) (*AiChangeFaceImg, error) { + var acf AiChangeFaceImg + return &acf, coll(t).FindOne(&acf, bson.M{"_id": id}) +} diff --git a/models/v/aichangefaceimgmod/app.go b/models/v/aichangefaceimgmod/app.go new file mode 100644 index 0000000..086202c --- /dev/null +++ b/models/v/aichangefaceimgmod/app.go @@ -0,0 +1,99 @@ +package aichangefaceimgmod + +import ( + "91porn-server/models/commod" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// AiChangFaceImgAppRes AI记录 App返回给前端 +type AiChangFaceImgAppRes struct { + UID uint64 `json:"uid" bson:"uid"` // 用户id + OriginPic string `json:"originPic" bson:"originPic"` // 脱衣原图 + NewPic []string `json:"newPic" bson:"newPic"` // 脱衣后新图 + Status int `json:"status" bson:"status"` // 1、进行中 2、生成成功 3、生成失败 + Remark string `json:"remark" bson:"remark"` // 备注 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 刷新时间 +} + +type ListRequest struct { + Status *int `form:"status" json:"status" bson:"status"` // 1、进行中 2、生成成功 3、生成失败 + commod.Page +} + +func (receiver *ListRequest) Filter(uid uint64) primitive.M { + filter := bson.M{} + if receiver.Status != nil { + filter["status"] = receiver.Status + if *receiver.Status == 1 { + filter["status"] = bson.M{"$in": []int{Processing, StatusSubmit}} + } + } + filter["uid"] = uid + filter["isHide"] = bson.M{"$in": []any{nil, false}} + return filter +} + +func (receiver *ListRequest) Options() *options.FindOptions { + return options.Find().SetSkip(int64(receiver.Skip())).SetLimit(int64(receiver.Limit())).SetSort(bson.M{"fakeLikeCount": -1}) +} + +type GenerateRequest struct { + OriginPic string `json:"originPic" bson:"originPic" binding:"required"` // 换脸原图 + MId string `json:"mId" bson:"mId" binding:"required"` // 模版ID + IsFreeTimes bool `json:"isFreeTimes" bson:"isFreeTimes"` // 是否使用免费次数 + Coin int64 `json:"coin" bson:"coin"` // 此次脱衣金币个数 + ShareTitle string `json:"shareTitle" bson:"shareTitle"` // 分享标题 + ShareStatus int `json:"shareStatus" bson:"shareStatus"` // 是否分享 0-不分享 1-分享 + +} + +func (receiver *GenerateRequest) Generate(orderId primitive.ObjectID, createdAt time.Time, uid uint64, modTitle, modPic string, privilegeCount, debitFreeCount, debitCoins, debitIncomeCoins int64) AiChangeFaceImg { + isFreeTimes := false + if debitFreeCount > 0 { + isFreeTimes = true + } + if privilegeCount > 0 { + isFreeTimes = true + } + return AiChangeFaceImg{ + ID: orderId, + Coin: debitIncomeCoins + debitCoins, + DebitAmountCoin: debitCoins, + DebitIncomeCoin: debitIncomeCoins, + IsFreeTimes: isFreeTimes, + Count: debitFreeCount + privilegeCount, + OriginPic: receiver.OriginPic, + MId: receiver.MId, + ModTitle: modTitle, + ModPic: modPic, + UID: uid, + Status: Processing, + IsHide: false, + ShareTitle: receiver.ShareTitle, + ShareStatus: receiver.ShareStatus, + UpdatedAt: createdAt, + CreatedAt: createdAt, + } +} + +type DelRequest struct { + ID primitive.ObjectID `json:"id" bson:"id"` // AI订单ID +} + +func (receiver *DelRequest) Filter() primitive.M { + filter := bson.M{} + filter["_id"] = receiver.ID + return filter +} + +func (receiver *DelRequest) Update() primitive.M { + update := bson.M{} + update["isHide"] = true + update["updatedAt"] = time.Now() + return bson.M{"$set": update} +} diff --git a/models/v/aichangefaceimgmod/type.go b/models/v/aichangefaceimgmod/type.go new file mode 100644 index 0000000..1fb9a5b --- /dev/null +++ b/models/v/aichangefaceimgmod/type.go @@ -0,0 +1,62 @@ +package aichangefaceimgmod + +import ( + "91porn-server/common/db" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +const ( + Processing = 1 // 1、进行中 + SUCCESS = 2 // 2、成功 + FAILURE = 3 // 3、失败 + REFUND = 4 // 4、退款 + StatusSubmit = 5 //5,提交 +) + +type Status int + +var status = map[Status]string{ + Processing: "processing", + FAILURE: "failure", + SUCCESS: "success", + REFUND: "refund", +} + +func (s Status) Desc() string { + if desc, ok := status[s]; ok { + return desc + } + return "" +} + +var mdb *db.MongoDB + +// AiChangeFaceImg AI图片换脸 +type AiChangeFaceImg struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 脱衣记录id + UID uint64 `json:"uid" bson:"uid"` // 用户id + OriginPic string `json:"originPic" bson:"originPic"` // AI换脸原图 + MId string `json:"mId" bson:"mId"` // AI换脸模版图ID + ModTitle string `json:"modTitle" bson:"modTitle"` // AI换脸模标题 + ModPic string `json:"modPic" bson:"modPic"` // AI换脸模版图 + NewPic []string `json:"newPic" bson:"newPic"` // AI换脸后新图 + Coin int64 `json:"coin" bson:"coin"` // 此次AI换脸金币个数 + DebitAmountCoin int64 `json:"debitAmountCoin" bson:"debitAmountCoin"` // 此次AI换脸扣除金币个数 + DebitIncomeCoin int64 `json:"debitIncomeCoin" bson:"debitIncomeCoin"` // 此次AI换脸扣除收益金币个数 + Status int `json:"status" bson:"status"` // 1、进行中 2、生成成功 3、生成失败 + Remark string `json:"remark" bson:"remark"` // 备注 + Count int64 `json:"count" bson:"count"` // 此次AI换脸AI换脸消耗次数 + IsFreeTimes bool `json:"isFreeTimes" bson:"isFreeTimes"` // 是否使用免费次数 + IsHide bool `json:"isHide" bson:"isHide"` // 是否被用户隐藏(用户订单列表是否不显示) + UpdateAct string `json:"updateAct" bson:"updateAct"` // 修改账号 + ShareTitle string `json:"shareTitle" bson:"shareTitle"` // 分享标题 + ShareStatus int `json:"shareStatus" bson:"shareStatus"` // 是否分享 0-不分享 1-分享 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 刷新时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/aichangefaceimgmod/web.go b/models/v/aichangefaceimgmod/web.go new file mode 100644 index 0000000..cc39f6f --- /dev/null +++ b/models/v/aichangefaceimgmod/web.go @@ -0,0 +1,79 @@ +package aichangefaceimgmod + +import ( + "91porn-server/models/commod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +// RechargeUpdateReq 修改参数 +type RechargeUpdateReq struct { + StatusDesc *string `json:"statusDesc" bson:"statusDesc"` // 状态描述 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 +} + +type RchgQueryReq struct { + ID *string `form:"id" json:"_id,omitempty" bson:"_id"` // 流水id + UID *uint64 `form:"uid" json:"uid,omitempty" bson:"uid"` // 用户id + Status *int `form:"status" json:"status,omitempty" bson:"status"` // 1、进行中 2、生成成功 3、生成失败 4、已经退款 +} + +type WebListRequest struct { + Status *int `form:"status" json:"status" bson:"status"` // 1、进行中 2、生成成功 3、生成失败 4、已经退款 + UID *uint64 `form:"uid" json:"uid,omitempty" bson:"uid"` // 用户id + ID *string `form:"id" json:"_id,omitempty" bson:"_id"` // 流水id + MId *string `form:"mId" json:"mId,omitempty" bson:"_id"` // AI模版id + commod.Page +} + +func (receiver *WebListRequest) Filter() primitive.M { + filter := bson.M{} + if receiver.Status != nil { + filter["status"] = receiver.Status + } + if receiver.UID != nil { + filter["uid"] = receiver.UID + } + if receiver.MId != nil { + filter["mId"] = receiver.MId + } + if receiver.ID != nil { + id, _ := primitive.ObjectIDFromHex(*receiver.ID) + filter["_id"] = id + } + return filter +} + +func (receiver *WebListRequest) Options() *options.FindOptions { + return options.Find().SetSkip(int64(receiver.Skip())).SetLimit(int64(receiver.Limit() + 1)).SetSort(bson.M{"updatedAt": -1}) +} + +type EditCond struct { + ID string `json:"id" binding:"required"` // 文档 + NewPic *[]string `json:"newPic" bson:"newPic"` // AI图片换脸后新图 + Status *int `json:"status" bson:"status"` // 状态 1、进行中 2、生成成功 3、生成失败 + Remark *string `json:"remark" bson:"remark"` // 拒绝理由 +} + +func (receiver *EditCond) Filter() bson.M { + objID, _ := primitive.ObjectIDFromHex(receiver.ID) + return bson.M{"_id": objID} +} + +func (receiver *EditCond) Update(updateAct string) bson.M { + var update = bson.M{} + if receiver.NewPic != nil { + update["newPic"] = receiver.NewPic + } + if receiver.Status != nil { + update["status"] = receiver.Status + } + if receiver.Remark != nil { + update["remark"] = receiver.Remark + } + update["updateAct"] = updateAct + update["updatedAt"] = time.Now() + return bson.M{"$set": update} +} diff --git a/models/v/aichangefacemod/aichangeface.go b/models/v/aichangefacemod/aichangeface.go new file mode 100644 index 0000000..037769a --- /dev/null +++ b/models/v/aichangefacemod/aichangeface.go @@ -0,0 +1,94 @@ +package aichangefacemod + +import ( + "91porn-server/common/aiService" + "91porn-server/common/db" + "91porn-server/models" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.AiChangeFace + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func AddAiChangeFace(t *db.MongoTool, orderId primitive.ObjectID, orderCreatedAt time.Time, uid uint64, pic []string, vidMod aiService.Template, coin, incomeCoin int64, discount []primitive.ObjectID, shareTitle string, shareStatus int) error { + _, err := coll(t).InsertOne(AiChangeFace{ + ID: orderId, + Uid: uid, + Pic: pic, + VidID: vidMod.ID, + ModTitle: vidMod.Title, + ModCover: vidMod.Cover, + ModVideo: vidMod.M3u8Url, + Coin: coin, + IncomeCoin: incomeCoin, + Discount: discount, + Status: StatusGenning, + IsHide: false, + Cover: "", + Url: "", + ShareTitle: shareTitle, + ShareStatus: shareStatus, + CreatedAt: orderCreatedAt, + UpdatedAt: orderCreatedAt, + }) + return err +} + +func ListByUid(uid uint64, status *AiChangeFaceStatus, skip, limit int) ([]AiChangeFace, error) { + var acfs []AiChangeFace + opts := options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}}) + if skip > 0 { + opts.SetSkip(int64(skip)) + } + if limit > 0 { + opts.SetLimit(int64(limit)) + } + cond := bson.M{"uid": uid, "isHide": bson.M{"$in": []any{nil, false}}} + if status != nil { + cond["status"] = *status + if *status == StatusGenning { + cond["status"] = bson.M{"$in": []int{0, 2}} + } + } + return acfs, coll(nil).Find(&acfs, cond, opts) +} + +func Hide(uid uint64, id primitive.ObjectID) error { + _, err := coll(nil).UpdateOne(bson.M{"uid": uid, "_id": id}, bson.M{"$set": bson.M{"isHide": true}}) + return err +} + +func FindByID(t *db.MongoTool, id primitive.ObjectID) (*AiChangeFace, error) { + var acf AiChangeFace + return &acf, coll(t).FindOne(&acf, bson.M{"_id": id}) +} diff --git a/models/v/aichangefacemod/app.go b/models/v/aichangefacemod/app.go new file mode 100644 index 0000000..4d534d6 --- /dev/null +++ b/models/v/aichangefacemod/app.go @@ -0,0 +1,11 @@ +package aichangefacemod + +import "go.mongodb.org/mongo-driver/bson/primitive" + +type GenerateRequest struct { + Pic []string `json:"picture"` // 换脸照片 + VidModID primitive.ObjectID `json:"vidModId"` // 视频模版id + Discount []primitive.ObjectID `json:"discount"` // 使用折扣券 + ShareTitle string `json:"shareTitle" bson:"shareTitle"` // 分享标题 + ShareStatus int `json:"shareStatus" bson:"shareStatus"` // 是否分享 0-不分享 1-分享 +} diff --git a/models/v/aichangefacemod/type.go b/models/v/aichangefacemod/type.go new file mode 100644 index 0000000..fecc906 --- /dev/null +++ b/models/v/aichangefacemod/type.go @@ -0,0 +1,39 @@ +package aichangefacemod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AiChangeFaceStatus int + +const ( + StatusGenning AiChangeFaceStatus = 0 // 未完成 + StatusComplete AiChangeFaceStatus = 1 // 已完成 + StatusRefund AiChangeFaceStatus = -1 // 已退款 + StatusSubmit AiChangeFaceStatus = 2 // 已提交 +) + +type AiChangeFace struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Uid uint64 `json:"uid" bson:"uid"` // 用户 + Pic []string `json:"picture" bson:"picture"` // 换脸照片 + VidID primitive.ObjectID `json:"vidId" bson:"vidId"` // 视频模版id + ModTitle string `json:"modTitle" bson:"modTitle"` //模版标题 + ModCover string `json:"modCover" bson:"modCover"` //模版封面 + ModVideo string `json:"modVideo" bson:"modVideo"` //模版视频 + Coin int64 `json:"coin" bson:"coin"` // 普通金币数 + IncomeCoin int64 `json:"incomeCoin" bson:"imcomeCoin"` // 可提现金币数 + Status AiChangeFaceStatus `json:"status" bson:"status"` // 0 未完成; 1 已完成; -1 已退款 + Discount []primitive.ObjectID `json:"discount" bson:"discount"` // 抵扣券id + IsHide bool `json:"-" bson:"isHide"` // 是否被用户隐藏(用户订单列表是否不显示) + Cover string `json:"cover" bson:"cover"` // 换脸后封面大图 + Url string `json:"url" bson:"url"` // 换脸后视频地址 + Remark string `json:"remark" bson:"remark"` // 拒绝理由 + ShareTitle string `json:"shareTitle" bson:"shareTitle"` // 分享标题 + ShareStatus int `json:"shareStatus" bson:"shareStatus"` // 是否分享 0-不分享 1-分享 + Operator string `json:"operator" bson:"operator"` // 操作员 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 刷新时间 +} diff --git a/models/v/aichangefacemod/web.go b/models/v/aichangefacemod/web.go new file mode 100644 index 0000000..ca6cfc6 --- /dev/null +++ b/models/v/aichangefacemod/web.go @@ -0,0 +1,115 @@ +package aichangefacemod + +import ( + "91porn-server/common/db" + "91porn-server/models/commod" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type AiChangeFaceWebResp struct { + ID primitive.ObjectID `json:"id"` + Uid uint64 `json:"uid"` // 用户 + Pic []string `json:"picture"` // 换脸照片 + ModId primitive.ObjectID `json:"modId"` // 视频模版id + ModUrl string `json:"modUrl"` // 视频模版播放地址url + ModTitle string `json:"modTitle"` // 模版标题 + Coin int64 `json:"coin"` // 普通金币数 + IncomeCoin int64 `json:"incomeCoin"` // 可提现金币数 + Status AiChangeFaceStatus `json:"status"` // 0 未完成; 1 已完成; -1 已退款 + Cover string `json:"cover"` // 换脸后封面大图 + Url string `json:"url"` // 换脸后视频地址 + Remark string `json:"remark"` // 拒绝理由 + Operator string `json:"operator"` // 操作员 + CreatedAt time.Time `json:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt"` // 刷新时间 +} + +type ListRequest struct { + ID *string `form:"id" json:"id"` + Uid *uint64 `form:"uid" json:"uid"` + Status *AiChangeFaceStatus `form:"status" json:"status"` + VidID *string `form:"vidId" json:"vidId"` + commod.Page +} + +type EditAiChangeFace struct { + ID primitive.ObjectID `json:"id" binding:"required"` // 被编辑的ai换脸id + Status *AiChangeFaceStatus `json:"status"` // 0 未完成; 1 已完成; -1 已退款 2 已经提交 + Cover *string `json:"cover"` // 换脸后封面大图 + Url *string `json:"url"` // 换脸后视频地址 + Remark *string `json:"remark"` // 拒绝理由 + Title *string `json:"title"` // 标题 +} + +func (receiver *EditAiChangeFace) Update(updateAct string) bson.M { + var update = bson.M{} + if receiver.Url != nil { + update["url"] = receiver.Url + } + if receiver.Cover != nil { + update["cover"] = receiver.Cover + } + if receiver.Status != nil { + update["status"] = receiver.Status + } + if receiver.Remark != nil { + update["remark"] = receiver.Remark + } + update["operator"] = updateAct + update["updatedAt"] = time.Now() + return bson.M{"$set": update} +} + +func WebList(id *primitive.ObjectID, uid *uint64, status *AiChangeFaceStatus, vidId *string, skip, limit int) ([]AiChangeFace, int64, error) { + cond := bson.M{} + if id != nil { + cond["_id"] = *id + } + if uid != nil { + cond["uid"] = *uid + } + if status != nil { + cond["status"] = *status + } + if vidId != nil { + vidObjId, _ := primitive.ObjectIDFromHex(*vidId) + cond["vidId"] = vidObjId + } + count, err := coll(nil).Count(cond) + if err != nil { + return nil, 0, err + } + var acfs []AiChangeFace + opts := options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}}) + if skip > 0 { + opts.SetSkip(int64(skip)) + } + if limit > 0 { + opts.SetLimit(int64(limit)) + } + return acfs, count, coll(nil).Find(&acfs, cond, opts) +} + +func Update(t *db.MongoTool, id primitive.ObjectID, set bson.M) error { + _, err := coll(t).UpdateOne(bson.M{"_id": id}, set) + return err +} + +// TransitionStatus atomically changes an order from the expected status. The +// returned boolean is false when another worker or callback handled it first. +func TransitionStatus(t *db.MongoTool, id primitive.ObjectID, from, to AiChangeFaceStatus, set bson.M) (bool, error) { + if set == nil { + set = bson.M{} + } + set["status"] = to + set["updatedAt"] = time.Now() + result, err := coll(t).UpdateOne(bson.M{"_id": id, "status": from}, bson.M{"$set": set}) + if err != nil { + return false, err + } + return result.ModifiedCount == 1, nil +} diff --git a/models/v/aichangefacevidmod/aichangefacevid.go b/models/v/aichangefacevidmod/aichangefacevid.go new file mode 100644 index 0000000..1c26752 --- /dev/null +++ b/models/v/aichangefacevidmod/aichangefacevid.go @@ -0,0 +1,119 @@ +package aichangefacevidmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "fmt" + "go.mongodb.org/mongo-driver/mongo" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.AiChangeFaceVideo + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitARDIndex 设置账户充值流水表索引 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "sort", Value: -1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "status", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("AiUnDressIndex err+%v", err)) + } +} + +func GetMods(cond bson.M, skip, limit int) ([]AiChangeFaceVidMod, error) { + var acfms []AiChangeFaceVidMod + opts := options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}}) + if skip > 0 { + opts.SetSkip(int64(skip)) + } + if limit > 0 { + opts.SetLimit(int64(limit)) + } + return acfms, coll(nil).Find(&acfms, cond, opts) +} + +func GetValidMods(skip, limit int) ([]AiChangeFaceVidMod, error) { + var acfms []AiChangeFaceVidMod + opts := options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}}) + if skip > 0 { + opts.SetSkip(int64(skip)) + } + if limit > 0 { + opts.SetLimit(int64(limit)) + } + return acfms, coll(nil).Find(&acfms, bson.M{"status": 1}, opts) +} + +func GetAllMods() ([]AiChangeFaceVidMod, error) { + var mod []AiChangeFaceVidMod + opts := options.Find().SetSort(bson.D{{Key: "sort", Value: -1}, {Key: "createdAt", Value: -1}}) + return mod, coll(nil).Find(&mod, bson.M{"status": 1}, opts) +} + +func GetModById(id primitive.ObjectID) (*AiChangeFaceVidMod, error) { + var acfvm AiChangeFaceVidMod + return &acfvm, coll(nil).FindOne(&acfvm, bson.M{"_id": id}) +} + +func GetModByIDs(ids []primitive.ObjectID) ([]AiChangeFaceVidMod, error) { + if len(ids) == 0 { + return nil, nil + } + var acfms []AiChangeFaceVidMod + return acfms, coll(nil).Find(&acfms, bson.M{"_id": bson.M{"$in": ids}}) +} + +// QueryAllDocument 分页查询文档 +func QueryAllDocument(filter primitive.M, opts ...*options.FindOptions) (out []*AiChangeFaceVidMod, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllDocument", table, "Find", err), + log.Any("filter", filter), + ) + return + } + return +} + +// CountDocument 查询文档条目数 +func CountDocument(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountDocument", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +func GetAllModsByCond(filter primitive.M) ([]AiChangeFaceVidMod, error) { + var mod []AiChangeFaceVidMod + opts := options.Find().SetSort(bson.D{{Key: "sort", Value: -1}, {Key: "createdAt", Value: -1}}) + return mod, coll(nil).Find(&mod, filter, opts) +} + +func UpdateManyByCond(filter primitive.M, set bson.M) error { + _, err := coll(nil).UpdateMany(filter, set) + return err +} diff --git a/models/v/aichangefacevidmod/app.go b/models/v/aichangefacevidmod/app.go new file mode 100644 index 0000000..ad3b441 --- /dev/null +++ b/models/v/aichangefacevidmod/app.go @@ -0,0 +1,60 @@ +package aichangefacevidmod + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AiChangeMod struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // AI模版ID + Title string `json:"title" bson:"title"` // 模版标题 + SourceURL string `json:"sourceURL" bson:"sourceURL"` // 视频资源地址 + Status int `json:"status" bson:"status"` // 状态,0 未激活 1 激活 + PlayTime uint `json:"playTime" bson:"playTime"` // 影片长度 + Cover string `json:"cover" bson:"cover"` // 封面大图 + Type string `json:"type" bson:"type"` // 类型 + ModuleType int `json:"moduleType" bson:"moduleType"` // 模块属性 + HotValue int `json:"hotValue" bson:"hotValue"` // 热门值 + HotMark string `json:"hotMark" bson:"hotMark"` // 热门标签 + Coin uint `json:"coin" bson:"coin"` // 价格(金币) + VipCoin uint `json:"vipCoin" bson:"vipCoin"` // vip价格(金币) +} + +type AiUndressMod struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // AI模版ID + Cover string `json:"cover" bson:"cover"` // AI模版图 +} + +type AiChangeFaceMod struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // AI模版ID + Title string `json:"title" bson:"title"` // 模版标题 + Cover string `json:"cover" bson:"cover"` // AI模版图 + HotValue int `json:"hotValue" bson:"hotValue"` // 热门值 + HotMark string `json:"hotMark" bson:"hotMark"` // 热门标签 + Coin uint `json:"coin" bson:"coin"` // 价格(金币) + VipCoin uint `json:"vipCoin" bson:"vipCoin"` // vip价格(金币) +} + +type AiImgToVideoMod struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // AI模版ID + Cover string `json:"cover" bson:"cover"` // AI模版图 + NewUrl string `json:"newUrl" bson:"newUrl"` // AI图生视频链接 + Title string `json:"title" bson:"title"` // 模版标题 + SceneType int `json:"sceneType" bson:"sceneType"` // 场景类型 +} + +type AiTextToImgMod struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // AI模版ID + Title string `json:"title" bson:"title"` // 模版标题 + Cover string `json:"cover" bson:"cover"` // AI模版图 + StyleType int `json:"styleType" bson:"styleType"` // AI绘图风格类型 + ModuleType int `json:"moduleType" bson:"moduleType"` // AI模块属性类型 +} + +type AppResponse struct { + AiUndressMod []AiUndressMod `json:"aiUndressMod" bson:"aiUndressMod"` // AI脱衣模版 + AiChangeFaceVideoMod []AiChangeMod `json:"aiChangeFaceVideoMod" bson:"aiChangeFaceVideoMod"` // AI视频换脸模型 + AiChangeFaceMod []AiChangeFaceMod `json:"aiChangeFaceMod" bson:"aiChangeFaceMod"` // Ai换脸模型 + AiImgToVideoMod []AiImgToVideoMod `json:"aiImgToVideoMod" bson:"aiImgToVideoMod"` // AI图生视频模型 + AiTextToImgMod []AiTextToImgMod `json:"aiTextToImgMod" bson:"aiTextToImgMod"` // AI绘图模型 + +} diff --git a/models/v/aichangefacevidmod/type.go b/models/v/aichangefacevidmod/type.go new file mode 100644 index 0000000..b77f631 --- /dev/null +++ b/models/v/aichangefacevidmod/type.go @@ -0,0 +1,44 @@ +package aichangefacevidmod + +import ( + "91porn-server/common/db" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + _ = iota + AIUndress // 1、AI脱衣模型配置 + AIVideoChangeFace // 2、AI视频换脸配置 + AIImgChangeFace // 3、AI图片换脸配置 + AIImgToVideo // 4、AI图生视频配置 + AITextToImage // 5、AI绘图配置 + +) + +type AiChangeFaceVidMod struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` // 视频标题 + SourceURL string `json:"sourceURL" bson:"sourceURL"` // 视频资源地址 + Status int `json:"status" bson:"status"` // 状态,0 未激活 1 激活 + PlayTime uint `json:"playTime" bson:"playTime"` // 影片长度 + Cover string `json:"cover" bson:"cover"` // 封面大图 + NewUrl string `json:"newUrl" bson:"newUrl"` // 生成后链接 + StyleType int `json:"styleType" bson:"styleType"` // 风格类型 + Type string `json:"type" bson:"type"` // 类型 + ModuleType int `json:"moduleType" bson:"moduleType"` // 模块属性 + HotValue int `json:"hotValue" bson:"hotValue"` // 热门值 + HotMark string `json:"hotMark" bson:"hotMark"` // 热门标签 + Coin uint `json:"coin" bson:"coin"` // 价格(金币) + VipCoin uint `json:"vipCoin" bson:"vipCoin"` // vip价格(金币) + SceneType int `json:"sceneType" bson:"sceneType"` // 场景类型 + Sort int `json:"sort" bson:"sort"` // 排序 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` // 刷新时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/aichangefacevidmod/web.go b/models/v/aichangefacevidmod/web.go new file mode 100644 index 0000000..aadb792 --- /dev/null +++ b/models/v/aichangefacevidmod/web.go @@ -0,0 +1,91 @@ +package aichangefacevidmod + +import ( + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/mongo/options" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRequest struct { + Status *int `form:"status" json:"status"` + ModuleType *int `form:"moduleType" json:"moduleType"` // 模块属性 + commod.Page +} + +type AddAiChangeFaceVidReq struct { + Title string `json:"title"` // 视频标题 + SourceURL string `json:"sourceURL"` // 视频资源地址 + Status int `json:"status"` // 状态,0 未激活 1 激活 + PlayTime uint `json:"playTime"` // 影片长度 + Cover string `json:"cover"` // 封面大图 + NewUrl string `json:"newUrl" bson:"newUrl"` // 生成后链接 + ModuleType int `json:"moduleType" bson:"moduleType"` // 模块属性 + HotValue int `json:"hotValue" bson:"hotValue"` // 热门值 + HotMark string `json:"hotMark" bson:"hotMark"` // 热门标签 + Type string `json:"type"` // 类型 + StyleType int `json:"styleType" bson:"styleType"` // 风格类型 + Coin uint `json:"coin"` // 模版价格 + Sort int `json:"sort" bson:"sort"` // 排序 + VipCoin uint `json:"vipCoin"` // 模版会员价格 + SceneType int `json:"sceneType" form:"sceneType"` // 场景类型 +} + +type EditAiChangeFaceVid struct { + ID primitive.ObjectID `json:"id" binding:"required"` + Title *string `json:"title"` // 视频标题 + SourceURL *string `json:"sourceURL"` // 视频资源地址 + Status *int `json:"status"` // 状态,0 未激活 1 激活 + PlayTime *uint `json:"playTime"` // 影片长度 + Cover *string `json:"cover"` // 封面大图 + ModuleType *int `json:"moduleType" bson:"moduleType"` // 模块属性 + StyleType *int `json:"styleType" bson:"styleType"` // 风格类型 + NewUrl *string `json:"newUrl" bson:"newUrl"` // 生成后链接 + HotValue *int `json:"hotValue" bson:"hotValue"` // 热门值 + HotMark *string `json:"hotMark" bson:"hotMark"` // 热门标签 + Type *string `json:"type"` // 类型 + Coin *uint `json:"coin"` // 模版价格 + Sort *int `json:"sort" bson:"sort"` // 排序 + VipCoin *uint `json:"vipCoin"` // 模版会员价格 + SceneType *int `json:"sceneType" form:"sceneType"` // 场景类型 +} + +func AddAiChangeFaceVid(afvm AiChangeFaceVidMod) error { + _, err := coll(nil).InsertOne(afvm) + return err +} + +func UpdateAiChangeFaceVid(id primitive.ObjectID, set bson.M) error { + _, err := coll(nil).UpdateOne(bson.M{"_id": id}, set) + return err +} + +type WebListRequest struct { + Status *int `form:"status" json:"status" bson:"status"` // 状态 + ModuleType *int `form:"moduleType" json:"moduleType" bson:"moduleType"` // 模块属性 + PageNumber int64 `form:"pageNumber" json:"pageNumber"` // 第几页 + PageSize int64 `form:"pageSize" json:"pageSize"` // 每页数量 +} + +func (receiver *WebListRequest) Filter() primitive.M { + filter := bson.M{} + if receiver.Status != nil { + filter["status"] = receiver.Status + } + if receiver.ModuleType != nil { + filter["moduleType"] = receiver.ModuleType + } + return filter +} +func (receiver *WebListRequest) Options() *options.FindOptions { + return options.Find().SetSkip((receiver.PageNumber - 1) * receiver.PageSize).SetLimit(receiver.PageSize + 1).SetSort(bson.D{{Key: "sort", Value: -1}, {Key: "createdAt", Value: -1}}) +} + +// 换脸订单回调请求 +type CallbackReq struct { + ImgUrl string `json:"imgUrl"` //脱衣后地址 + AppOrderNum string `json:"appOrderNum"` //app订单号 + Msg string `json:"msg"` //消息 +} diff --git a/models/v/aiimagetovideomod/aiimagetovideo.go b/models/v/aiimagetovideomod/aiimagetovideo.go new file mode 100755 index 0000000..79eaf85 --- /dev/null +++ b/models/v/aiimagetovideomod/aiimagetovideo.go @@ -0,0 +1,146 @@ +package aiimagetovideomod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "encoding/json" + "errors" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []AiImageToVideo, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// GetAll 查询全部文档 +func GetAll(filter primitive.M, sort bson.D) (out []*AiImageToVideo, err error) { + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAll", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (AiImageToVideo, error) { + v := AiImageToVideo{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d AiImageToVideo) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} + +func Hide(uid uint64, id primitive.ObjectID) error { + _, err := coll(nil).UpdateOne(bson.M{"uid": uid, "_id": id}, bson.M{"$set": bson.M{"isHide": true}}) + return err +} diff --git a/models/v/aiimagetovideomod/app.go b/models/v/aiimagetovideomod/app.go new file mode 100755 index 0000000..85ec640 --- /dev/null +++ b/models/v/aiimagetovideomod/app.go @@ -0,0 +1,23 @@ +package aiimagetovideomod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// AiImageToVideoInfo 移动端返回内容 +type AiImageToVideoInfo struct { + ID primitive.ObjectID `json:"id"` // 文档id + UID uint64 `json:"uid"` // 用户ID + ImgUrl string `json:"imgUrl"` // 未处理的图片地址 + NewImgUrl string `json:"newImgUrl"` // 处理后的图片地址 + Status int `json:"status"` // 状态 + Coin int64 `json:"coin"` // 图生视频金币个数 + IsFreeTimes bool `json:"isFreeTimes"` // 是否使用免费次数 + IsHide bool `json:"isHide"` // 是否被用户隐藏 + Remark string `json:"remark"` // 备注 + IsDelete bool `json:"isDelete"` // 是否删除 + CreatedAt time.Time `json:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt"` // 更新时间 +} diff --git a/models/v/aiimagetovideomod/type.go b/models/v/aiimagetovideomod/type.go new file mode 100755 index 0000000..1ba7dc7 --- /dev/null +++ b/models/v/aiimagetovideomod/type.go @@ -0,0 +1,111 @@ +package aiimagetovideomod + +import ( + "91porn-server/common/db" + "91porn-server/models" + "fmt" + + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.AiImageToVideo + +type AiImageToVideoStatus int + +const ( + StatusOrderSuccess AiImageToVideoStatus = iota + 1 // 1: 下单成功 + StatusSubmitted // 2: 已提交 + StatusGenerationSuccess // 3: 生成成功 + StatusGenerationFailed // 4: 生成失败 + StatusRefunded // 5: 退款 +) + +type SceneType int + +const ( + SceneTagLiftShirt SceneType = iota // 0:掀衣露乳 + SceneTagTearClothes // 1:男人撕衣 + SceneTagCumOnFace // 2:神屌射脸 + SceneOFace // 3:阿黑颜 + SceneTagKiss // 4:接吻 + SceneTagSuckBreasts // 5:吸奶 + SceneRevealed // 6:露逼 + SceneLargerChest // 7:胸部变大 +) + +// String 返回 SceneTag 对应的中文描述,方便打印与调试 +func (t SceneType) String() string { + switch t { + case SceneTagTearClothes: + return "男人撕衣" + case SceneTagLiftShirt: + return "掀衣露乳" + case SceneTagCumOnFace: + return "神屌射脸" + case SceneOFace: + return "阿黑颜" + case SceneTagKiss: + return "接吻" + case SceneTagSuckBreasts: + return "吸奶" + case SceneRevealed: + return "露逼" + case SceneLargerChest: + return "胸部变大" + default: + return "未知场景" + } +} + +type AiImageToVideo struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + UID uint64 `json:"uid" bson:"uid"` // 用户ID + ImgUrl string `json:"imgUrl" bson:"imgUrl"` // 未处理的图片地址 + NewImgUrl string `json:"newImgUrl" bson:"newImgUrl"` // 处理后的图片地址 + SceneType int `json:"sceneType" bson:"sceneType"` // 模版场景类型 + Status int `json:"status" bson:"status"` // 状态 1、下单成功 2、已提交 3、生成成功 4、生成失败 5、退款 + Coin int64 `json:"coin" bson:"coin"` // 图生视频金币个数 + DebitAmountCoin int64 `json:"debitAmountCoin" bson:"debitAmountCoin"` // 此次脱衣扣除金币个数 + DebitIncomeCoin int64 `json:"debitIncomeCoin" bson:"debitIncomeCoin"` // 此次脱衣扣除收益金币个数 + IsFreeTimes bool `json:"isFreeTimes" bson:"isFreeTimes"` // 是否使用免费次数 + IsHide bool `json:"isHide" bson:"isHide"` // 是否被用户隐藏 + Remark string `json:"remark" bson:"remark"` // 备注 + ShareTitle string `json:"shareTitle" bson:"shareTitle"` // 分享标题 + ShareStatus int `json:"shareStatus" bson:"shareStatus"` // 是否分享 0-不分享 1-分享 + IsDelete bool `json:"isDelete" bson:"isDelete"` // 是否删除 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "status", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} diff --git a/models/v/aiplazamod/aiplaza.go b/models/v/aiplazamod/aiplaza.go new file mode 100755 index 0000000..26d4862 --- /dev/null +++ b/models/v/aiplazamod/aiplaza.go @@ -0,0 +1,221 @@ +package aiplazamod + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []AiPlaza, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// GetAll 查询全部文档 +func GetAll(filter primitive.M, sort bson.D) (out []AiPlaza, err error) { + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAll", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (AiPlaza, error) { + v := AiPlaza{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d AiPlaza) (data primitive.ObjectID, err error) { + if d.Status == 1 { + d.ReviewAt = time.Now() + } + d.CreatedAt = time.Now() + d.UpdatedAt = time.Now() + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// IncCommentCount 评论统计次数加加 +func IncCommentCount(id primitive.ObjectID) error { + query := bson.M{"_id": id} + update := bson.M{"$inc": bson.M{"commentCount": 1}} + if _, err := coll(nil).UpdateOne(query, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncCommentCount", table, "UpdateOne", err), + log.Any("id", id), + ) + return err + } + return nil +} + +// IncCollectCount 收藏统计次数加加 +func IncCollectCount(id primitive.ObjectID) error { + query := bson.M{"_id": id} + update := bson.M{"$set": bson.M{"updatedAt": time.Now()}, "$inc": bson.M{"collectCount": 1}} + if _, err := coll(nil).UpdateOne(query, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "收藏统计次数加加", table, "UpdateOne", err), + log.Any("id", id), + ) + return err + } + return nil +} + +// IncLikeCount 点赞统计次数加加 +func IncLikeCount(id primitive.ObjectID) error { + query := bson.M{"_id": id} + update := bson.M{"$set": bson.M{"updatedAt": time.Now()}, "$inc": bson.M{"likeCount": 1}} + if _, err := coll(nil).UpdateOne(query, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncLikeCount", table, "UpdateOne", err), + log.Any("id", id), + ) + return err + } + return nil +} + +// DecLikeCount 点赞统计次数减减 +func DecLikeCount(ids []primitive.ObjectID) error { + var query = bson.M{} + if len(ids) == 1 { + query["_id"] = ids[0] + } else { + query["_id"] = bson.M{"$in": ids} + } + update := bson.M{"$set": bson.M{"updatedAt": time.Now()}, "$inc": bson.M{"likeCount": -1}} + if _, err := coll(nil).UpdateMany(query, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DecLikeCount", table, "UpdateOne", err), + log.Any("ids", ids), + ) + return err + } + return nil +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// UpdateByIds 根据ids更新数据 +func UpdateByIds(t *db.MongoTool, ids []primitive.ObjectID, data map[string]interface{}) (int64, error) { + if len(ids) == 0 { + return 0, nil + } + cond := bson.M{"_id": bson.M{"$in": ids}} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, ids ...primitive.ObjectID) error { + if len(ids) == 0 { + return nil + } + filter := bson.M{"_id": ids[0]} + if len(ids) > 1 { + filter["_id"] = bson.M{"$in": ids} + } + _, err := coll(t).DeleteMany(filter) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", ids)) + } + } + + return err +} diff --git a/models/v/aiplazamod/app.go b/models/v/aiplazamod/app.go new file mode 100755 index 0000000..49fdc5a --- /dev/null +++ b/models/v/aiplazamod/app.go @@ -0,0 +1,38 @@ +package aiplazamod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// AiPlazaInfo 移动端返回内容 +type AiPlazaInfo struct { + ID primitive.ObjectID `json:"id"` // 文档id + Type int `json:"type"` // 1-ai图片换脸 2-ai视频换脸 3-ai脱衣 4-ai图生视频 5-ai绘画 + Template string `json:"template" bson:"template"` // 关联模版id + Uid uint64 `json:"uid"` // + Name string `json:"name"` //姓名 + Gender string `json:"gender"` //性别 + Portrait string `json:"portrait"` //头像 + Title string `json:"title"` // 标题 + OriginalImage string `json:"originalImage" bson:"originalImage"` // 原图 + OriginalVideo string `json:"originalVideo" bson:"originalVideo"` // 原视频 + OriginalVideoCover string `json:"originalVideoCover" bson:"originalVideoCover"` // 原视频封面 + OriginContent string `json:"originContent" bson:"originContent"` // 输入的原文 + GenerateImage string `json:"generateImage" bson:"generateImage"` // 生成的图 + GenerateVideo string `json:"generateVideo" bson:"generateVideo"` // 生成的视频 + GenerateVideoCover string `json:"generateVideoCover" bson:"generateVideoCover"` // 生成的视频封面 + Status int `json:"status"` // 0-待审核 1-审核通过 2-拒绝 3-下架 + Reason string `json:"reason"` // 原因 + SortCode int `json:"sortCode"` // 置顶排序号,大于0就是置顶 + HasCollected bool `json:"hasCollected"` // 是否收藏 + HasLiked bool `json:"hasLiked"` // 是否点赞 + ViewCount int64 `json:"viewCount" bson:"viewCount"` // 浏览数 + LikeCount int64 `json:"likeCount" bson:"likeCount"` // 点赞数 + CollectCount int64 `json:"collectCount" bson:"collectCount"` // 收藏数 + CommentCount int64 `json:"commentCount" bson:"commentCount"` // 评论数 + ReviewAt time.Time `json:"reviewAt"` // 审核时间 + CreatedAt time.Time `json:"createdAt"` // 文档创建时间 + UpdatedAt time.Time `json:"updatedAt"` // 文档更新时间 +} diff --git a/models/v/aiplazamod/type.go b/models/v/aiplazamod/type.go new file mode 100755 index 0000000..425c349 --- /dev/null +++ b/models/v/aiplazamod/type.go @@ -0,0 +1,80 @@ +package aiplazamod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.AiPlaza + +const ( + DefaultStatus = 0 // 默认审核状态 +) + +type AiPlaza struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + Type int `json:"type" bson:"type"` // 1-ai图片换脸 2-ai视频换脸 3-ai脱衣 4-ai图生视频 5-ai绘画 + OrderId primitive.ObjectID `json:"orderId" bson:"orderId"` // 关联订单id + Template string `json:"template" bson:"template"` // 关联模版id + Uid uint64 `json:"uid" bson:"uid"` // + Title string `json:"title" bson:"title"` // 标题 + OriginalImage string `json:"originalImage" bson:"originalImage"` // 原图 + OriginalVideo string `json:"originalVideo" bson:"originalVideo"` // 原视频 + OriginalVideoCover string `json:"originalVideoCover" bson:"originalVideoCover"` // 原视频封面 + OriginContent string `json:"originContent" bson:"originContent"` // 输入的原文 + GenerateImage string `json:"generateImage" bson:"generateImage"` // 生成的图 + GenerateVideo string `json:"generateVideo" bson:"generateVideo"` // 生成的视频 + GenerateVideoCover string `json:"generateVideoCover" bson:"generateVideoCover"` // 生成的视频封面 + Status int `json:"status" bson:"status"` // 0-待审核 1-审核通过 2-拒绝 3-下架 + Reason string `json:"reason" bson:"reason"` // 原因 + SortCode int `json:"sortCode" bson:"sortCode"` // 置顶排序号,大于0就是置顶 + ReviewAt time.Time `json:"reviewAt" bson:"reviewAt"` // 审核时间 + ViewCount int64 `json:"viewCount" bson:"viewCount"` // 浏览数 + LikeCount int64 `json:"likeCount" bson:"likeCount"` // 点赞数 + CollectCount int64 `json:"collectCount" bson:"collectCount"` // 收藏数 + CommentCount int64 `json:"commentCount" bson:"commentCount"` // 评论数 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 文档创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 文档更新时间 +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 todo: 在models/Init/init.go中调用初始化 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "_id", Value: 1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "type", Value: 1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}, {Key: "sortCode", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} diff --git a/models/v/aitemplatemodulemod/aitemplatemodule.go b/models/v/aitemplatemodulemod/aitemplatemodule.go new file mode 100755 index 0000000..aa2013e --- /dev/null +++ b/models/v/aitemplatemodulemod/aitemplatemodule.go @@ -0,0 +1,142 @@ +package aitemplatemodulemod + +import ( + "encoding/json" + "errors" + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []AiTemplateModule, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// GetAll 查询全部文档 +func GetAll(filter primitive.M, sort bson.D) (out []AiTemplateModule, err error) { + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAll", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (AiTemplateModule, error) { + v := AiTemplateModule{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d AiTemplateModule) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} diff --git a/models/v/aitemplatemodulemod/app.go b/models/v/aitemplatemodulemod/app.go new file mode 100755 index 0000000..14bf60c --- /dev/null +++ b/models/v/aitemplatemodulemod/app.go @@ -0,0 +1,12 @@ +package aitemplatemodulemod + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// AiTemplateModuleInfo 移动端返回内容 +type AiTemplateModuleInfo struct { + ID primitive.ObjectID `json:"id"` // 文档id + Type int `json:"type"` // 模块类型 + Name string `json:"name"` // 模块名称 +} diff --git a/models/v/aitemplatemodulemod/type.go b/models/v/aitemplatemodulemod/type.go new file mode 100755 index 0000000..948fa45 --- /dev/null +++ b/models/v/aitemplatemodulemod/type.go @@ -0,0 +1,55 @@ +package aitemplatemodulemod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.AiTemplateModule + +type AiTemplateModule struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + Type int `json:"type" bson:"type"` // 模块类型 1:AI视频换脸,2:AI图片换脸 + Name string `json:"name" bson:"name"` // 模块名称 + Status int `json:"status" bson:"status"` // 状态 1:开启,2:关闭 + SortNum int `json:"sortNum" bson:"sortNum"` // 模块排序 + IsDelete bool `json:"isDelete" bson:"isDelete"` // 是否删除 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "type", Value: 1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} diff --git a/models/v/aitexttoimagemod/aitexttoimage.go b/models/v/aitexttoimagemod/aitexttoimage.go new file mode 100755 index 0000000..cee361e --- /dev/null +++ b/models/v/aitexttoimagemod/aitexttoimage.go @@ -0,0 +1,147 @@ +package aitexttoimagemod + +import ( + "encoding/json" + "errors" + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []AiTextToImage, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// GetAll 查询全部文档 +func GetAll(filter primitive.M, sort bson.D) (out []*AiTextToImage, err error) { + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAll", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (AiTextToImage, error) { + v := AiTextToImage{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d AiTextToImage) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} + +func Hide(uid uint64, id primitive.ObjectID) error { + _, err := coll(nil).UpdateOne(bson.M{"uid": uid, "_id": id}, bson.M{"$set": bson.M{"isHide": true}}) + return err +} diff --git a/models/v/aitexttoimagemod/app.go b/models/v/aitexttoimagemod/app.go new file mode 100755 index 0000000..5e31faa --- /dev/null +++ b/models/v/aitexttoimagemod/app.go @@ -0,0 +1,28 @@ +package aitexttoimagemod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// AiTextToImageInfo 移动端返回内容 +type AiTextToImageInfo struct { + ID primitive.ObjectID `json:"id"` // 文档id + UID uint64 `json:"uid"` // 用户ID + Text string `json:"text"` // 图片描述 + NewImgUrl string `json:"newImgUrl"` // 处理后的图片地址 + Status int `json:"status"` // 状态 + AspectRatio string `json:"aspectRatio"` // 长宽比 + StyleType int `json:"styleType"` // 风格类型 + StyleUrl string `json:"styleUrl"` // 绘画风格图片 + Coin int64 `json:"coin"` // AI绘图金币个数 + DebitAmountCoin int64 `json:"debitAmountCoin"` // 此次脱衣扣除金币个数 + DebitIncomeCoin int64 `json:"debitIncomeCoin"` // 此次脱衣扣除收益金币个数 + IsFreeTimes bool `json:"isFreeTimes"` // 是否使用免费次数 + IsHide bool `json:"isHide"` // 是否被用户隐藏 + Remark string `json:"remark"` // 备注 + IsDelete bool `json:"isDelete"` // 是否删除 + CreatedAt time.Time `json:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt"` // 更新时间 +} diff --git a/models/v/aitexttoimagemod/type.go b/models/v/aitexttoimagemod/type.go new file mode 100755 index 0000000..fba7385 --- /dev/null +++ b/models/v/aitexttoimagemod/type.go @@ -0,0 +1,78 @@ +package aitexttoimagemod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.AiTextToImage + +type AiTextToImageStatus int + +const ( + StatusOrderSuccess AiTextToImageStatus = iota + 1 // 1: 下单成功 + StatusSubmitted // 2: 已提交 + StatusGenerationSuccess // 3: 生成成功 + StatusGenerationFailed // 4: 生成失败 + StatusRefunded // 5: 退款 +) + +type AiTextToImage struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + UID uint64 `json:"uid" bson:"uid"` // 用户ID + Text string `json:"text" bson:"text"` // 图片描述 + NewImgUrl string `json:"newImgUrl" bson:"newImgUrl"` // 处理后的图片地址 + Status int `json:"status" bson:"status"` // 状态 + AspectRatio string `json:"aspectRatio" bson:"aspectRatio"` // 长宽比 + StyleType int `json:"styleType" bson:"styleType"` // 风格类型 + Coin int64 `json:"coin" bson:"coin"` // AI绘图金币个数 + DebitAmountCoin int64 `json:"debitAmountCoin" bson:"debitAmountCoin"` // 此次脱衣扣除金币个数 + DebitIncomeCoin int64 `json:"debitIncomeCoin" bson:"debitIncomeCoin"` // 此次脱衣扣除收益金币个数 + IsFreeTimes bool `json:"isFreeTimes" bson:"isFreeTimes"` // 是否使用免费次数 + IsHide bool `json:"isHide" bson:"isHide"` // 是否被用户隐藏 + Remark string `json:"remark" bson:"remark"` // 备注 + ShareTitle string `json:"shareTitle" bson:"shareTitle"` // 分享标题 + ShareStatus int `json:"shareStatus" bson:"shareStatus"` // 是否分享 0-不分享 1-分享 + IsDelete bool `json:"isDelete" bson:"isDelete"` // 是否删除 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "status", Value: 1}}, + }, + { + Keys: bson.D{{Key: "isDelete", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} diff --git a/models/v/aitexttonovelmod/aitexttonovel.go b/models/v/aitexttonovelmod/aitexttonovel.go new file mode 100755 index 0000000..998b6dc --- /dev/null +++ b/models/v/aitexttonovelmod/aitexttonovel.go @@ -0,0 +1,146 @@ +package aitexttonovelmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "encoding/json" + "errors" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []AiTextToNovel, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// GetAll 查询全部文档 +func GetAll(filter primitive.M, sort bson.D) (out []*AiTextToNovel, err error) { + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAll", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (AiTextToNovel, error) { + v := AiTextToNovel{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d AiTextToNovel) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} + +func Hide(uid uint64, id primitive.ObjectID) error { + _, err := coll(nil).UpdateOne(bson.M{"uid": uid, "_id": id}, bson.M{"$set": bson.M{"isHide": true}}) + return err +} diff --git a/models/v/aitexttonovelmod/type.go b/models/v/aitexttonovelmod/type.go new file mode 100755 index 0000000..ca36bcc --- /dev/null +++ b/models/v/aitexttonovelmod/type.go @@ -0,0 +1,77 @@ +package aitexttonovelmod + +import ( + "91porn-server/common/db" + "91porn-server/models" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.AiTextToNovel + +type AiTextToNovelStatus int + +const ( + StatusOrderSuccess AiTextToNovelStatus = iota + 1 // 1: 下单成功 + StatusSubmitted // 2: 已提交 + StatusGenerationSuccess // 3: 生成成功 + StatusGenerationFailed // 4: 生成失败 + StatusRefunded // 5: 退款 +) + +type AiTextToNovel struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + UID uint64 `json:"uid" bson:"uid"` // 用户ID + CharacterSetting string `json:"characterSetting" bson:"characterSetting"` // 小说人物设定/故事背景等 + LocationScene string `json:"locationScene" bson:"locationScene"` // 地点场景 + Description string `json:"description" bson:"description"` // 故事情节描述 + Details string `json:"details" bson:"details"` // 细节说明/其他要求 + ModelType int `json:"modelType" bson:"modelType"` // AI小说模型 1,2 + Content string `json:"content" bson:"content"` // AI生成小说正文 + Status int `json:"status" bson:"status"` // 状态 + Coin int64 `json:"coin" bson:"coin"` // AI小说金币个数 + DebitAmountCoin int64 `json:"debitAmountCoin" bson:"debitAmountCoin"` // 此次AI小说扣除金币个数 + DebitIncomeCoin int64 `json:"debitIncomeCoin" bson:"debitIncomeCoin"` // 此次AI小说扣除收益金币个数 + IsFreeTimes bool `json:"isFreeTimes" bson:"isFreeTimes"` // 是否使用免费次数 + IsHide bool `json:"isHide" bson:"isHide"` // 是否被用户隐藏 + Remark string `json:"remark" bson:"remark"` // 备注 + IsDelete bool `json:"isDelete" bson:"isDelete"` // 是否删除 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "status", Value: 1}}, + }, + { + Keys: bson.D{{Key: "isDelete", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} diff --git a/models/v/annoumod/annou.go b/models/v/annoumod/annou.go new file mode 100644 index 0000000..9d38d0f --- /dev/null +++ b/models/v/annoumod/annou.go @@ -0,0 +1,172 @@ +package annoumod + +import ( + "fmt" + "math" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.Annou + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 初始化索引 +func initAnnouIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "active", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "type", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// AddAnnou 增加公告 +func AddAnnou(annou []Annou) error { + if _, err := coll(nil).InsertMany(&annou); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AddAnnou", table, "InsertMany", err), log.Any("annou", annou)) + return err + } + return nil +} + +// DeleteAnnou 删除公告 +func DeleteAnnou(ids []primitive.ObjectID) (int64, error) { + if ids == nil { + ids = []primitive.ObjectID{} + } + cond := bson.M{"_id": bson.M{"$in": ids}} + result, err := coll(nil).DeleteMany(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteAnnou", table, "DeleteMany", err), log.Any("ids", ids)) + return 0, err + } + return result.DeletedCount, nil +} + +// UpdateAnnou 更新公告 +func UpdateAnnou(id primitive.ObjectID, update map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + result, err := coll(nil).UpdateMany(cond, bson.M{"$set": bson.M(update)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateAnnou", table, "UpdateMany", err), + log.Any("id", id), + log.Any("update", update), + ) + return 0, err + } + return result.ModifiedCount, nil +} + +// getTotalCnt 获取查询总数总数 +func getTotalCnt(cond bson.M) (int64, error) { + total, err := coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getTotalCnt", table, "Count", err), log.Any("cond", cond)) + return 0, err + } + return total, nil +} + +// GetSkipSize 计算跳转 +func getSkipSize(page, size uint64, cond bson.M) (uint64, uint64, int64, error) { + total, err := getTotalCnt(cond) + if err != nil { + return 0, 0, 0, err + } + totalpages := uint64(math.Ceil(float64(total) / float64(size))) + if page > totalpages { + page = totalpages + } + if page < 1 { + page = 1 + } + return (page - 1) * size, totalpages, total, nil +} + +// GetAnnou 查询公告 +func GetAnnou(page, size uint64, field string, desc int) ([]*Annou, uint64, int64, error) { + cond := bson.M{} + sort := bson.D{{Key: field, Value: desc}} + skip, totalPages, total, err := getSkipSize(page, size, cond) + if err != nil { + return nil, 0, 0, err + } + opts := options.FindOptions{} + opts.SetSort(sort).SetSkip(int64(skip)).SetLimit(int64(size)) + var back []*Annou + if err = coll(nil).Find(&back, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAnnou", table, "Find", err), + log.Any("page", page), + log.Any("size", size), + log.Any("field", field), + log.Any("desc", desc), + ) + return nil, totalPages, total, err + } + return back, totalPages, total, nil +} + +// GetAnnouByType 获取指定类型的公告 +func GetAnnouByType(types int) ([]*Annou, error) { + cond := bson.M{"active": true} + if types != 2 { + cond = bson.M{"type": types, "active": true} + } + sort := bson.D{{Key: "updatedAt", Value: -1}} + opts := options.FindOptions{} + opts.SetSort(sort) + var back []*Annou + if err := coll(nil).Find(&back, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAnnouByType", table, "Find", err), log.Any("types", types)) + return nil, err + } + return back, nil +} + +const ( + TotalType = 1 //系统公告 + VIPUP = 3 //vip升级公告 +) + +// GetAnnouForDomain 获取指定类型的公告 +func GetAnnouForDomain(types int) ([]AnnounInfo, error) { + cond := bson.M{"active": true} + if types != 2 { + cond = bson.M{"type": types, "active": true} + } + sort := bson.D{{Key: "updatedAt", Value: -1}} + opts := options.FindOptions{} + opts.SetSort(sort) + var back []AnnounInfo + if err := coll(nil).Find(&back, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAnnouForDomain", table, "Find", err), log.Any("types", types)) + return nil, err + } + return back, nil +} diff --git a/models/v/annoumod/announce.go b/models/v/annoumod/announce.go new file mode 100644 index 0000000..7e6f3a6 --- /dev/null +++ b/models/v/annoumod/announce.go @@ -0,0 +1,109 @@ +package annoumod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdbAnnounce *db.MongoDB + +const tableAnnounce = models.Announce + +func collAnnounce(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdbAnnounce.Coll(tableAnnounce) + } + return t.Coll(tableAnnounce) +} + +// InitIndex 初始化索引 +func initAnnounceIndex() { + indexModels := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "active", Value: 1}}, + Options: options.Index(), + }, + } + if _, err := collAnnounce(nil).CreateIndex(indexModels); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", tableAnnounce, err)) + } +} + +// AddAnnounce 增加会员中心跑马灯 +func AddAnnounce(announce []Announce) error { + if _, err := collAnnounce(nil).InsertMany(&announce); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AddAnnounce", tableAnnounce, "InsertMany", err), log.Any("announce", announce)) + return err + } + return nil +} + +// DeleteAnnounce 删除会员中心跑马灯 +func DeleteAnnounce(ids []primitive.ObjectID) (int64, error) { + if ids == nil { + ids = []primitive.ObjectID{} + } + cond := bson.M{"_id": bson.M{"$in": ids}} + result, err := collAnnounce(nil).DeleteMany(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteAnnounce", tableAnnounce, "DeleteMany", err), log.Any("ids", ids)) + return 0, err + } + return result.DeletedCount, nil +} + +// UpdateAnnounce 更新会员中心跑马灯 +func UpdateAnnounce(id primitive.ObjectID, update map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + result, err := collAnnounce(nil).UpdateMany(cond, bson.M{"$set": bson.M(update)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateAnnou", tableAnnounce, "UpdateMany", err), + log.Any("id", id), + log.Any("update", update), + ) + return 0, err + } + return result.ModifiedCount, nil +} + +// GetAnnounce 查询会员中心跑马灯 +func GetAnnounceApi(field string, desc int) ([]Announce, error) { + cond := bson.M{"active": true, "type": 0} + sort := bson.D{{Key: field, Value: desc}} + opts := options.FindOptions{} + opts.SetSort(sort) + var back []Announce + if err := collAnnounce(nil).Find(&back, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAnnounce", tableAnnounce, "Find", err), + log.Any("field", field), + log.Any("desc", desc), + ) + return nil, err + } + return back, nil +} + +// GetAnnounce 查询会员中心跑马灯 +func GetAnnounce(field string, desc int) ([]Announce, error) { + cond := bson.M{"type": 0} + sort := bson.D{{Key: field, Value: desc}} + opts := options.FindOptions{} + opts.SetSort(sort) + var back []Announce + if err := collAnnounce(nil).Find(&back, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAnnounce", tableAnnounce, "Find", err), + log.Any("field", field), + log.Any("desc", desc), + ) + return nil, err + } + return back, nil +} diff --git a/models/v/annoumod/app.go b/models/v/annoumod/app.go new file mode 100644 index 0000000..96e8a68 --- /dev/null +++ b/models/v/annoumod/app.go @@ -0,0 +1,24 @@ +package annoumod + +import "go.mongodb.org/mongo-driver/bson/primitive" + +// PopReq 公告列表 +type PopReq struct { + Type int `form:"type" json:"type"` //公告类型,0,2,所有 3会员升级公告 +} + +// AppRes 公告模型 +type AnnounInfo struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //公告id + Title string `json:"title" bson:"title"` //公告标题 + Content string `json:"content" bson:"content"` //公告内容 + Cover string `json:"cover" bson:"cover"` //公告封面图 + Href string `json:"href" bson:"href"` //公告跳转地址 + Type int `json:"type" bson:"type"` //公告类型 +} + +// MsgListReq 公告列表 +type MsgListReq struct { + PageNumber int `form:"pageNumber" json:"pageNumber"` + PageSize int `form:"pageSize" json:"pageSize"` +} diff --git a/models/v/annoumod/type.go b/models/v/annoumod/type.go new file mode 100644 index 0000000..5da8e2b --- /dev/null +++ b/models/v/annoumod/type.go @@ -0,0 +1,42 @@ +package annoumod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Annou 公告模型 +type Annou struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //公告id + Title string `json:"title" bson:"title"` //公告标题 + Content string `json:"content" bson:"content"` //公告内容 + Cover string `json:"cover" bson:"cover"` //公告封面图 + Type int `json:"type" bson:"type"` //公告类型,0,外部连接;1,直接安装app + Href string `json:"href" bson:"href"` //公告跳转地址 + Active bool `json:"active" bson:"active"` //激活状态 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //更新时间 +} + +func Init() { + mdb = db.Init(table) + mdbAnnounce = db.Init(tableAnnounce) + initAnnouIndex() + initAnnounceIndex() +} + +// Announce 会员中心跑马灯模型 +type Announce struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //公告id + Content string `json:"content" bson:"content"` //跑马灯内容 + Type int `json:"type" bson:"type"` //跑马灯类型,0,会员中心; + Url string `json:"url" bson:"url"` //跳转连接 + Active bool `json:"active" bson:"active"` //激活状态 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //更新时间 +} + +const UserCenter = 0 //会员中心 diff --git a/models/v/annoumod/web.go b/models/v/annoumod/web.go new file mode 100644 index 0000000..d8b7a38 --- /dev/null +++ b/models/v/annoumod/web.go @@ -0,0 +1,85 @@ +package annoumod + +import "91porn-server/models/commod" + +// ListReq 视频列表请求 +type ListReq struct { + Sort string `form:"sort" json:"sort"` + Desc int `form:"desc" json:"desc"` + commod.Page +} + +// EditInfo 更新的内容 +type EditInfo struct { + Title string `form:"title" json:"title"` //公告标题 + Cover string `form:"cover" json:"cover"` //公告封面图 + Content string `form:"content" json:"content"` //公告内容 + Type int `form:"type" json:"type"` //公告类型,0,外部连接;1,直接安装app + Href string `form:"href" json:"href"` //公告跳转地址 + Active bool `form:"active" json:"active"` //激活状态 +} + +// EditReq 更新公告 +type EditReq struct { + ID string `form:"id" json:"id"` + EditInfo +} + +// DeleteReq 删除公告 +type DeleteReq struct { + IDs []string `form:"ids" json:"ids"` +} + +// AddReq 增加一条公告 +type AddReq struct { + Title string `form:"title" json:"title"` //公告标题 + Content string `form:"content" json:"content"` //公告内容 + Cover string `form:"cover" json:"cover"` //公告封面图 + Type int `form:"type" json:"type"` //公告类型,0,外部连接;1,直接安装app 3,会员升级公告 + Href string `form:"href" json:"href"` //公告跳转地址 + Active bool `form:"active" json:"active"` //激活状态 +} + +// ListResp 列表应答 +type ListResp struct { + AInfos []*Annou `json:"aInfos"` + Total int64 `json:"total"` +} + +// OpeResp 操作应答 +type OpeResp struct { + Count int64 `json:"count"` +} + +// EditInfo 更新的内容 +type EditAnnounceInfo struct { + Content string `form:"content" json:"content"` //跑马灯内容 + Type int `form:"type" json:"type"` //跑马灯类型 + Url string `form:"url" json:"url"` //跳转链接 + Active bool `form:"active" json:"active"` //激活状态 +} + +// EditReq 更新公告 +type EditAnnounceReq struct { + ID string `form:"id" json:"id"` + EditAnnounceInfo +} + +// DeleteReq 删除公告 +type DeleteAnnounceReq struct { + IDs []string `form:"ids" json:"ids"` +} + +// AddReq 增加一条公告 +type AddAnnounceReq struct { + Content string `form:"content" json:"content,omitempty" binding:"required"` //跑马灯内容 + Url string `form:"url" json:"url,omitempty"` //跳转链接 + Active bool `form:"active" json:"active,omitempty"` //激活状态 +} + +// 3、走马灯通知 +// 接口:goldegg/announce +// AnnounceInfoReq 走马灯通告信息请求 +type AnnounceInfoReq struct { + commod.Page +} diff --git a/models/v/audioanchormod/audioanchormod.go b/models/v/audioanchormod/audioanchormod.go new file mode 100644 index 0000000..51d94d6 --- /dev/null +++ b/models/v/audioanchormod/audioanchormod.go @@ -0,0 +1,323 @@ +package audioanchormod + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "91porn-server/app/appg" + "91porn-server/app/service/searcher" + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.AudioAnchor + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func GetByID(id primitive.ObjectID) (loufeng AudioAnchor, err error) { + if err = coll(nil).FindOne(&loufeng, bson.M{"_id": id}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByID", table, "FindOne", err), log.Any("id", id.Hex())) + } + return +} + +// todo app +func GetInIds(ids []primitive.ObjectID) (res []AudioAnchor, err error) { + data := make([]AudioAnchor, 0) + f := bson.M{} + if ids != nil { + f["_id"] = bson.M{"$in": ids} + } + if err = coll(nil).Find(&data, f); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInIds", table, "Find", err), log.Any("cond", f)) + return + } + tempM := make(map[primitive.ObjectID]AudioAnchor) + for i := range data { + tempM[data[i].ID] = data[i] + } + res = make([]AudioAnchor, len(ids)) + for i := range ids { + res[i] = tempM[ids[i]] + } + return +} + +func GetRandomPush(size int) (data []AudioAnchor, err error) { + data = make([]AudioAnchor, 0) + f := bson.M{"name": bson.M{"$gt": ""}} + if err = coll(nil).Find(&data, f); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetRandomPush", table, "Aggregate", err), log.Any("filter", f)) + return + } + return +} + +func GetByName(name string) (data AudioAnchor, err error) { + f := bson.M{"name": name} + if err = coll(nil).FindOne(&data, f); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByFType", table, "Find", err), log.Any("cond", f)) + return + } + return +} + +func GetFTypes() ([]interface{}, error) { + data := make([]interface{}, 0) + redisKey := RedisSetKey + ":fType" + redisc := appg.Redis + if redisc == nil || !redisc.Exists(redisKey) { + data, err := coll(nil).Distinct("fType", bson.M{}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCitys", table, "Distinct", err)) + return nil, err + } + if redisc == nil { + return data, nil + } + jsonBytes, err := json.Marshal(data) + if err != nil { + return data, err + } + _ = redisc.Set(redisKey, string(jsonBytes), time.Minute*10) + return data, nil + } + str, err := redisc.Get(redisKey) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetFTypes", table, "Find", err)) + return nil, err + } + if str == nil { + return data, errors.New("redis key is null") + } + return data, json.Unmarshal([]byte(*str), &data) +} + +func GetSortWord(page commod.Page, sortWord string) ([]AudioAnchor, bool, error) { + data := make([]AudioAnchor, 0) + skip := int64(page.Skip()) + limit := int64(page.Limit() + 1) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: sortWord, Value: -1}}, + } + f := bson.M{"name": bson.M{"$gt": ""}} + if err := coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByFType", table, "Find", err), log.Any("cond", f)) + return data, false, err + } + hasNext := false + if uint64(len(data)) > page.PageSize { + hasNext = true + data = data[:page.PageSize] + } + return data, hasNext, nil +} + +func SearchName(name string, page commod.Page) (data []AudioAnchorAppRes, hasNext bool, err error) { + data = make([]AudioAnchorAppRes, 0) + skip := int64(page.Skip()) + limit := int64(page.Limit() + 1) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: 1}}, + } + f := bson.M{} + f["$or"] = bson.A{ + bson.M{"name": primitive.Regex{ + Pattern: name, + Options: "", + }}, + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "SearchName", table, "Find", err), log.Any("cond", f)) + return + } + if uint64(len(data)) > page.PageSize { + hasNext = true + data = data[:page.PageSize] + } + return +} + +func IncCount(id primitive.ObjectID, inc CountInc) error { + res, err := coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$inc": inc}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncCount", table, "UpdateOne", err), log.Any("id", id), log.Any("inc", inc)) + return err + } + if res.ModifiedCount <= 0 { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s none", "IncCount", table, "UpdateOne"), log.Any("id", id), log.Any("inc", inc), log.Any("res", res)) + return nil + } + return nil +} + +func GetNew(opt searcher.Opter) (data []AudioAnchorAppRes, hasNext bool, err error) { + data = make([]AudioAnchorAppRes, 0) + skip := opt.Skip() + limit := opt.Limit() + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + f := bson.M{"isActive": true} + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetNew", table, "Find", err), log.Any("cond", f)) + return + } + if int64(len(data)) > limit { + hasNext = true + data = data[:limit] + } + return +} + +// todo web +// StdFind 通用查询 +func StdFind(q QuerySelector, page commod.Page) (data []AudioAnchor, total int64, err error) { + data = make([]AudioAnchor, 0) + skip := int64(page.Skip()) + limit := int64(page.Limit()) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + f, err := common.ToBsonM(q) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "StdFind", table, "ToBsonM", err), log.Any("cond", f)) + return + } + if q.Title != nil { + f["title"] = primitive.Regex{ + Pattern: *q.Title, + Options: "", + } + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "StdFind", table, "Find", err), log.Any("cond", f)) + return + } + total, err = coll(nil).Count(f) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "StdFind", table, "Count", err), log.Any("cond", f)) + } + return +} + +// IsExisted 查询 +func IsExisted(id primitive.ObjectID) (ok bool, err error) { + ok, err = coll(nil).Exists(bson.M{"_id": id}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsExisted", table, "Exists", err), log.Any("id", id)) + return + } + return +} + +// InsertAudioAnchor 插入一条数据 +func InsertAudioAnchor(p *AudioAnchor) error { + p.CreatedAt = time.Now() + if _, err := coll(nil).InsertOne(p); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertAudioAnchor", table, "InsertOne", err)) + return err + } + return nil +} + +// InsertBulket 插入一条数据 +func InsertBulket(p []EditSelector) (err error) { + fmt.Println("------------", len(p)) + now := time.Now() + wm := make([]mongo.WriteModel, len(p)) + for i := range p { + filter := bson.M{"name": p[i].Name} + update := bson.M{"$inc": bson.M{"totalRaido": 1}, "$set": p[i], "$setOnInsert": bson.M{"createdAt": now}} + wm[i] = mongo.NewUpdateOneModel(). + SetFilter(filter). + SetUpdate(update). + SetUpsert(true) + } + ordered := false + opts := options.BulkWriteOptions{ + Ordered: &ordered, + } + if _, err = coll(nil).Bulk(wm, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertMany", table, "InsertMany", err)) + } + return +} + +// InsertAudioAnchor 插入一条数据 +func InsertMany(p []AudioAnchor) error { + if _, err := coll(nil).InsertMany(p); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertMany", table, "InsertMany", err)) + return err + } + return nil +} + +// UpdateAudioAnchor 修改AudioAnchor类型 +func UpdateAudioAnchor(set *EditSelector) error { + set.UpdatedAt = time.Now() + if _, err := coll(nil).UpdateOne(bson.M{"_id": set.ID}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateAudioAnchor", table, "UpdateOne", err), + log.Any("set", set), + ) + return err + } + return nil +} + +// RemoveAudioAnchor 删除AudioAnchor类型 +func RemoveAudioAnchor(id string) error { + OID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RemoveAudioAnchor", table, "ObjectIDFromHex", err), + log.Any("id", id), + ) + return err + } + if _, err = coll(nil).DeleteOne(bson.M{"_id": OID}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RemoveAudioAnchor", table, "DeleteOne", err), + log.Any("OID", OID), + ) + return err + } + return nil +} + +// todo skd +// 获取ES同步数据 +func GetListByUpdateTimeRange(start time.Time, end time.Time) (data []AudioAnchor, err error) { + var query = bson.M{ + "updatedAt": bson.M{"$gte": start, "$lt": end}, + } + if err = coll(nil).Find(&data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetListByUpdateTimeRange", table, "Find", err), + log.Any("start", start), + log.Any("end", end), + ) + return + } + return +} diff --git a/models/v/audioanchormod/es.go b/models/v/audioanchormod/es.go new file mode 100644 index 0000000..83bfa92 --- /dev/null +++ b/models/v/audioanchormod/es.go @@ -0,0 +1,105 @@ +package audioanchormod + +import ( + "fmt" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" +) + +var es *elastic.Client + +const ESTable = models.ESInfoLouFengTable + +func InitESIndex() { + es = elastic.Init() + var setting = elastic.M{ + "settings": elastic.M{ + "number_of_shards": elastic.NumberOfShards, + "number_of_replicas": elastic.NumberOfReplicas, + "analysis": elastic.M{ + "analyzer": elastic.M{ + "ik": elastic.M{ + "tokenizer": elastic.AnalyzerIkMaxWord, + }, + }, + }, + }, + "mappings": elastic.M{ + "properties": elastic.M{ + "age": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkMaxWord, + "search_analyzer": elastic.AnalyzerIkSmart, + }, + "price": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkMaxWord, + "search_analyzer": elastic.AnalyzerIkSmart, + }, + + "title": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkMaxWord, + "search_analyzer": elastic.AnalyzerIkSmart, + }, + "city": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkMaxWord, + "search_analyzer": elastic.AnalyzerIkSmart, + }, + "district": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkMaxWord, + "search_analyzer": elastic.AnalyzerIkSmart, + }, + "impression": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkMaxWord, + "search_analyzer": elastic.AnalyzerIkSmart, + }, + "contact": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkSmart, + "search_analyzer": elastic.AnalyzerIkSmart, + }, + "serviceItems": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkMaxWord, + "search_analyzer": elastic.AnalyzerIkSmart, + }, + }, + }, + } + if err := es.CreateIndices(ESTable, setting); err != nil { + panic(fmt.Sprintf("%s index indeices err ==>[%+v]", ESTable, err)) + } +} + +func Search(keywords string, from int64, size int64) (data []ESUserSource, err error) { + query := elastic.M{ + "query": elastic.M{ + "bool": elastic.M{ + "must": elastic.A{ + {"multi_match": elastic.M{ + "query": keywords, + "fields": []string{"city", "impression", "title", "serviceItems", "price"}}, + }, + {"term": elastic.M{"isActive": true}}, + }, + }, + }, + "from": from, + "size": size, + } + if err = es.Search(ESTable, &data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Search", table, "CommonSearch", err), + log.Any("keywords", keywords), + log.Any("from", from), + log.Any("size", size), + ) + return + } + return +} diff --git a/models/v/audioanchormod/type.go b/models/v/audioanchormod/type.go new file mode 100644 index 0000000..6d1abaa --- /dev/null +++ b/models/v/audioanchormod/type.go @@ -0,0 +1,69 @@ +package audioanchormod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +type AudioAnchor struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` //名字 + Avatar string `json:"avatar" bson:"avatar"` //头像 + TotalRaido int `json:"totalRaido" bson:"totalRaido"` //作品总量 + CountCollect int64 `json:"countCollect" bson:"countCollect"` //收藏数 + IsCollect bool `json:"isCollect" bson:"-"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 +} + +func Init() { + mdb = db.Init(table) +} + +type QuerySelector struct { + ID *primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + Title *string `json:"title,omitempty" bson:"title,omitempty"` //标题 + FType *string `json:"fType,omitempty" bson:"fType,omitempty"` //小说类型 + Summary *string `json:"summary,omitempty" bson:"summary,omitempty"` //简介 + ContentURL *string `json:"contentUrl,omitempty" bson:"contentUrl,omitempty"` //内容地址 + IsActive *bool `json:"isActive,omitempty" bson:"isActive,omitempty"` //是否激活 + Number *int `json:"number,omitempty" bson:"number,omitempty"` //编号 +} + +type EditSelector struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` //名字 + Avatar string `json:"avatar" bson:"avatar,omitempty"` //头像 + Number int `json:"number" bson:"number,omitempty"` //编号 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //更新时间 +} + +const RedisSetKey = "fiction" + +// todo app +type AudioAnchorAppRes struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title,omitempty"` //标题 + FType string `json:"fType" bson:"fType"` //小说类型 + Summary string `json:"summary" bson:"summary"` //简介 + ContentURL string `json:"contentUrl" bson:"contentUrl"` //内容地址 + IsActive bool `json:"isActive" bson:"isActive"` //是否激活 + CountPurchases int64 `json:"countPurchases" bson:"countPurchases"` //购买数 + CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` //浏览数 + CountCollect int64 `json:"countCollect" bson:"countCollect"` //收藏数 + IsCollect bool `json:"isCollect"` //是否收藏 +} + +type CountInc struct { + CountCollect *int `json:"countCollect" bson:"countCollect,omitempty"` //收藏数 +} + +type ESUserSource struct { + ID string `json:"_id"` + Source AudioAnchorAppRes `json:"_source"` +} diff --git a/models/v/audiobookmod/audiobook.go b/models/v/audiobookmod/audiobook.go new file mode 100644 index 0000000..9442254 --- /dev/null +++ b/models/v/audiobookmod/audiobook.go @@ -0,0 +1,631 @@ +package audiobookmod + +import ( + "encoding/json" + "errors" + "fmt" + "math/rand" + "time" + + "91porn-server/app/appg" + "91porn-server/app/service/searcher" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.AudioBook + +// InitIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "number", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "isActive", Value: 1}}, + }, + { + Keys: bson.D{{Key: "fType", Value: 1}}, + }, + { + Keys: bson.D{{Key: "anchor", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func GetByID(id primitive.ObjectID) (ab AudioBookAppRes, err error) { + redisKey := redisconst.DataCachKey(table, id.Hex()) + redisc := appg.Redis + if redisc == nil || !redisc.Exists(redisKey) { + if err = coll(nil).FindOne(&ab, bson.M{"_id": id}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByID", table, "FindOne", err), log.Any("id", id.Hex())) + return + } + if redisc == nil { + return + } + var jsonBytes []byte + jsonBytes, err = json.Marshal(&ab) + if err != nil { + return + } + err = redisc.Set(redisKey, string(jsonBytes), redisconst.DataCachExpire) + return + } + str, err := redisc.Get(redisKey) + if err != nil { + return + } + if str == nil { + err = errors.New("redis key is null") + return + } + if err = json.Unmarshal([]byte(*str), &ab); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByID", table, "FindOne", err), log.Any("id", id.Hex())) + return + } + return +} + +// todo app +func GetByIds(ids []primitive.ObjectID) (res []AudioBookAppRes, err error) { + data := make([]AudioBookAppRes, 0) + f := bson.M{} + if ids != nil { + f["_id"] = bson.M{"$in": ids} + } + if err = coll(nil).Find(&data, f); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByBusinessDateAndCityInIds", table, "Find", err), log.Any("cond", f)) + return + } + tempM := make(map[primitive.ObjectID]AudioBookAppRes) + for i := range data { + tempM[data[i].ID] = data[i] + } + res = make([]AudioBookAppRes, len(ids)) + for i := range ids { + res[i] = tempM[ids[i]] + } + return +} + +func GetByFType(fType string, page commod.Page) (data []AudioBookBase, hasNext bool, err error) { + data = make([]AudioBookBase, 0) + skip := int64(page.Skip()) + limit := int64(page.Limit() + 1) + sortWord := "createdAt" + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: sortWord, Value: -1}}, + } + f := bson.M{"isActive": true} + if fType != "" { + if fType == ListenPermissionBuyZone { + f["contentSet.listenPermission"] = ListenPermissionBuy + } else { + f["fType"] = fType + } + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByFType", table, "Find", err), log.Any("cond", f)) + return + } + if uint64(len(data)) > page.PageSize { + hasNext = true + data = data[:page.PageSize] + } + for i := range data { + for j := range data[i].ContentSet { + data[i].ContentSet[j].ListenPermission = 3 + data[i].ContentSet[j].Price = 0 + } + } + return +} + +func GetByAnchor(anchor string, page commod.Page) (data []AudioBookBase, hasNext bool, err error) { + data = make([]AudioBookBase, 0) + skip := int64(page.Skip()) + limit := int64(page.Limit() + 1) + sortWord := "createdAt" + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: sortWord, Value: -1}}, + } + f := bson.M{"isActive": true} + if anchor != "" { + f["anchor"] = anchor + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByFType", table, "Find", err), log.Any("cond", f)) + return + } + if uint64(len(data)) > page.PageSize { + hasNext = true + data = data[:page.PageSize] + } + return +} + +// 获取热门 +func GetHots(page commod.Page) (data []AudioBookBase, hasNext bool, err error) { + data = make([]AudioBookBase, 0) + skip := int64(page.Skip()) + limit := int64(page.Limit() + 1) + sortWord := "countPurchases" + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: sortWord, Value: -1}}, + } + f := bson.M{"isActive": true} + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByFType", table, "Find", err), log.Any("cond", f)) + return + } + if uint64(len(data)) > page.PageSize { + hasNext = true + data = data[:page.PageSize] + } + return +} + +func GetRandomPush(size int) (data []AudioBookBase, err error) { + data = make([]AudioBookBase, 0) + redisK := "tempSet:" + RedisSetKey + ":randomData" + redisc := appg.Redis + if redisc == nil || !redisc.Exists(redisK) { + f := bson.M{"isActive": true} + var total int64 + total, err = randomCount(f) + if err != nil { + return + } + var limit int64 = 100 + var skip int64 = 0 + if total-limit > limit { + skip = rand.Int63n(total - limit) + } + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetRandomPush", table, "Find", err), log.Any("filter", f)) + return + } + if len(data) < 1 { + return + } + if redisc == nil { + return + } + tempd := make([]string, len(data)) + for i := range data { + var strB []byte + strB, err = json.Marshal(data[i]) + if err != nil { + return + } + tempd[i] = string(strB) + } + if _, err = redisc.SAdd(redisK, tempd); err != nil { + return + } + _, _ = redisc.ExpireKey(redisK, time.Minute*10) + } + tempd, err := redisc.SRandMemberN(redisK, int64(size)) + if len(tempd) < 1 { + return data, errors.New("redis key is null") + } + data = make([]AudioBookBase, len(tempd)) + for i := range data { + _ = json.Unmarshal([]byte(tempd[i]), &data[i]) + } + return +} + +func GetRandomPush_PermissionBuy(size int) (data []AudioBookBase, err error) { + data = make([]AudioBookBase, 0) + redisK := "tempSet:" + RedisSetKey + ":randomData:permissionBuy" + redisc := appg.Redis + if redisc == nil || !redisc.Exists(redisK) { + f := bson.M{"isActive": true, "contentSet.listenPermission": ListenPermissionBuy} + var total int64 + total, err = randomCount(f) + if err != nil { + return + } + var limit int64 = 100 + var skip int64 = 0 + if total-limit > limit { + skip = rand.Int63n(total - limit) + } + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetRandomPush_PermissionBuy", table, "Find", err), log.Any("filter", f)) + return + } + if len(data) < 1 { + return + } + if redisc == nil { + return + } + tempd := make([]string, len(data)) + for i := range data { + var strB []byte + strB, err = json.Marshal(data[i]) + if err != nil { + return + } + tempd[i] = string(strB) + } + if _, err = redisc.SAdd(redisK, tempd); err != nil { + return + } + _, _ = redisc.ExpireKey(redisK, time.Minute*10) + } + tempd, err := redisc.SRandMemberN(redisK, int64(size)) + if len(tempd) < 1 { + return data, errors.New("redis key is null") + } + data = make([]AudioBookBase, len(tempd)) + for i := range data { + _ = json.Unmarshal([]byte(tempd[i]), &data[i]) + } + return +} + +func GetRandomPush_PermissionVIP(size int) (data []AudioBookBase, err error) { + data = make([]AudioBookBase, 0) + redisK := "tempSet:" + RedisSetKey + ":randomData:PermissionVIP" + redisc := appg.Redis + if redisc == nil || !redisc.Exists(redisK) { + f := bson.M{"isActive": true, "contentSet.listenPermission": ListenPermissionVIP} + var total int64 + total, err = randomCount(f) + if err != nil { + return + } + var limit int64 = 100 + var skip int64 = 0 + if total-limit > limit { + skip = rand.Int63n(total - limit) + } + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetRandomPush_PermissionVIP", table, "Find", err), log.Any("filter", f)) + return + } + if len(data) < 1 { + return + } + if redisc == nil { + return + } + tempd := make([]string, len(data)) + for i := range data { + var strB []byte + strB, err = json.Marshal(data[i]) + if err != nil { + return + } + tempd[i] = string(strB) + } + if _, err = redisc.SAdd(redisK, tempd); err != nil { + return + } + _, _ = redisc.ExpireKey(redisK, time.Minute*10) + } + tempd, err := redisc.SRandMemberN(redisK, int64(size)) + if len(tempd) < 1 { + return data, errors.New("redis key is null") + } + data = make([]AudioBookBase, len(tempd)) + for i := range data { + _ = json.Unmarshal([]byte(tempd[i]), &data[i]) + } + return +} + +func GetFTypes() (data []interface{}, err error) { + data = make([]interface{}, 0) + redisKey := RedisSetKey + ":fType" + redisc := appg.Redis + if redisc == nil || !redisc.Exists(redisKey) { + data, err = coll(nil).Distinct("fType", bson.M{}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCitys", table, "Distinct", err)) + return nil, err + } + if redisc == nil { + return data, err + } + jsonBytes, err := json.Marshal(data) + if err != nil { + return data, err + } + _ = redisc.Set(redisKey, string(jsonBytes), time.Minute*10) + return data, err + } + str, err := redisc.Get(redisKey) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetFTypes", table, "Find", err)) + return nil, err + } + if str == nil { + return data, errors.New("redis key is null") + } + err = json.Unmarshal([]byte(*str), &data) + return +} + +func SearchTitle(title string, page commod.Page) (data []AudioBookBase, hasNext bool, err error) { + data = make([]AudioBookBase, 0) + skip := int64(page.Skip()) + limit := int64(page.Limit() + 1) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: 1}}, + } + f := bson.M{"isActive": true} + f["$or"] = bson.A{ + bson.M{"title": primitive.Regex{ + Pattern: title, + Options: "", + }}, + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "SearchTitle", table, "Find", err), log.Any("cond", f)) + return + } + if uint64(len(data)) > page.PageSize { + hasNext = true + data = data[:page.PageSize] + } + return +} + +func IncCount(id primitive.ObjectID, inc CountInc) (err error) { + res, err := coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$inc": inc}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncCount", table, "UpdateOne", err), log.Any("id", id), log.Any("inc", inc)) + return + } + if res.ModifiedCount <= 0 { + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncCount", table, "UpdateOne", err), log.Any("id", id), log.Any("inc", inc), log.Any("res", res)) + return + } + } + return +} + +func GetNew(opt searcher.Opter) (data []AudioBookAppRes, hasNext bool, err error) { + data = make([]AudioBookAppRes, 0) + skip := opt.Skip() + limit := opt.Limit() + 1 + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + f := bson.M{"isActive": true} + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetNew", table, "Find", err), log.Any("cond", f)) + return + } + if int64(len(data)) > opt.Limit() { + hasNext = true + data = data[:opt.Limit()] + } + return +} + +func randomCount(f bson.M) (count int64, err error) { + count, err = coll(nil).Count(f) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCitys", table, "Distinct", err)) + return + } + return +} + +// todo web +// StdFind 通用查询 +func StdFind(q QuerySelector, page commod.Page) (data []AudioBook, total int64, err error) { + data = make([]AudioBook, 0) + skip := int64(page.Skip()) + limit := int64(page.Limit()) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + } + f, err := common.ToBsonM(q) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "StdFind", table, "ToBsonM", err), log.Any("cond", f)) + return + } + if q.Title != nil { + f["title"] = primitive.Regex{ + Pattern: *q.Title, + Options: "", + } + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "StdFind", table, "Find", err), log.Any("cond", f)) + return + } + total, err = coll(nil).Count(f) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "StdFind", table, "Count", err), log.Any("cond", f)) + } + return +} + +// IsExisted 查询 +func IsExisted(id primitive.ObjectID) (ok bool, err error) { + ok, err = coll(nil).Exists(bson.M{"_id": id}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsExisted", table, "Exists", err), log.Any("id", id)) + return + } + return +} + +// InsertAudioBook 插入一条数据 +func InsertAudioBook(p *AudioBook) (err error) { + p.CreatedAt = time.Now() + if _, err = coll(nil).InsertOne(p); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertAudioBook", table, "InsertOne", err)) + } + return +} + +// InsertBulket 插入一条数据 +func InsertBulket(p []AudioBookUpsertEdit) (err error) { + fmt.Println("------------", len(p)) + now := time.Now() + wm := make([]mongo.WriteModel, len(p)) + for k := range p { + filter := bson.M{"number": p[k].Number} + update := bson.M{"$set": p[k], "$setOnInsert": bson.M{"createdAt": now, "contentSet": p[k].ContentSet}} + model := mongo.NewUpdateOneModel(). + SetFilter(filter). + SetUpdate(update). + SetUpsert(true) + wm[k] = model + } + ordered := false + opts := options.BulkWriteOptions{ + Ordered: &ordered, + } + if _, err = coll(nil).Bulk(wm, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertMany", table, "InsertMany", err)) + } + return +} + +// InsertAudioBook 插入一条数据 +func InsertMany(p []AudioBook) (err error) { + if _, err = coll(nil).InsertMany(p); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertMany", table, "InsertMany", err)) + } + return +} + +// UpdateAudioBook 修改AudioBook类型 +func UpdateAudioBookBatch(sets []EditSelector) (err error) { + now := time.Now() + wm := make([]mongo.WriteModel, len(sets)) + for k := range sets { + sets[k].UpdatedAt = now + filter := bson.M{"_id": sets[k].ID} + update := bson.M{"$set": sets[k]} + + model := mongo.NewUpdateOneModel(). + SetFilter(filter). + SetUpdate(update). + SetUpsert(false) + wm[k] = model + } + ordered := false + opts := options.BulkWriteOptions{ + Ordered: &ordered, + } + if _, err = coll(nil).Bulk(wm, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertMany", table, "InsertMany", err)) + } + return +} + +// UpdateAudioBook 修改AudioBook类型 +func UpdateAudioBook(set EditSelector) error { + set.UpdatedAt = time.Now() + setM, err := common.ToBsonM(set) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ToBsonM", table, "UpdateOne", err), + log.Any("set", set)) + return err + } + if _, err = coll(nil).UpdateOne(bson.M{"_id": set.ID}, bson.M{"$set": setM}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateAudioBook", table, "UpdateOne", err), + log.Any("set", set), + ) + return err + } + return nil +} + +// RemoveAudioBook 删除AudioBook类型 +func RemoveAudioBook(id string) error { + OID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RemoveAudioBook", table, "ObjectIDFromHex", err), + log.Any("id", id), + ) + return err + } + if _, err = coll(nil).DeleteOne(bson.M{"_id": OID}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RemoveAudioBook", table, "DeleteOne", err), + log.Any("OID", OID), + ) + return err + } + return nil +} + +// todo skd +// 获取ES同步数据 +func GetListByUpdateTimeRange(start time.Time, end time.Time) (data []AudioBookBase, err error) { + var query = bson.M{ + "updatedAt": bson.M{"$gte": start, "$lt": end}, + } + if err = coll(nil).Find(&data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetListByUpdateTimeRange", table, "Find", err), + log.Any("start", start), + log.Any("end", end), + ) + return + } + return +} + +func GetListByCond(cond bson.M, opts *options.FindOptions) ([]AudioBook, error) { + var data []AudioBook + if err := coll(nil).Find(&data, cond, opts); err != nil { + return nil, err + } + return data, nil +} diff --git a/models/v/audiobookmod/es.go b/models/v/audiobookmod/es.go new file mode 100644 index 0000000..96f7ef9 --- /dev/null +++ b/models/v/audiobookmod/es.go @@ -0,0 +1,69 @@ +package audiobookmod + +import ( + "fmt" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" +) + +var es *elastic.Client + +const ESTable = models.ESInfoAudioBookTable + +func InitESIndex() { + es = elastic.Init() + var setting = elastic.M{ + "settings": elastic.M{ + "number_of_shards": elastic.NumberOfShards, + "number_of_replicas": elastic.NumberOfReplicas, + "analysis": elastic.M{ + "analyzer": elastic.M{ + "ik": elastic.M{ + "tokenizer": elastic.AnalyzerIkMaxWord, + }, + }, + }, + }, + "mappings": elastic.M{ + "properties": elastic.M{ + "title": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkMaxWord, + "search_analyzer": elastic.AnalyzerIkSmart, + }, + }, + }, + } + if err := es.CreateIndices(ESTable, setting); err != nil { + panic(fmt.Sprintf("%s index indeices err ==>[%+v]", ESTable, err)) + } +} + +func Search(keywords string, from int64, size int64) (data []ESAudioBookSource, err error) { + query := elastic.M{ + "query": elastic.M{ + "bool": elastic.M{ + "must": elastic.A{ + {"multi_match": elastic.M{ + "query": keywords, + "fields": []string{"title"}}, + }, + {"term": elastic.M{"isActive": true}}, + }, + }, + }, + "from": from, + "size": size, + } + if err = elastic.Init().Search(ESTable, &data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Search", table, "CommonSearch", err), + log.Any("keywords", keywords), + log.Any("from", from), + log.Any("size", size), + ) + return + } + return +} diff --git a/models/v/audiobookmod/type.go b/models/v/audiobookmod/type.go new file mode 100644 index 0000000..bb327f8 --- /dev/null +++ b/models/v/audiobookmod/type.go @@ -0,0 +1,145 @@ +package audiobookmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +const ( + ListenPermissionVIP = 0 + ListenPermissionBuy = 1 + + ListenPermissionBuyZone = "付费专区" +) + +type Content struct { + EpisodeNumber int `json:"episodeNumber" bson:"episodeNumber" ` //第几集 + ListenPermission int `json:"listenPermission" bson:"listenPermission"` //收听权限 0:会员 1:金币购买 3:试听 + Price int64 `json:"price" bson:"price"` //购买价格 + Name string `json:"name" bson:"name"` //章节名 + URL string `json:"contentUrl" bson:"contentUrl"` //资源地址 + MediaSize int64 `json:"mediaSize" bson:"mediaSize"` //资源大小 + IsBrought bool `json:"isBrought" bson:"-"` //是否购买 +} + +type AudioBook struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title,omitempty"` //标题 + Anchor string `json:"anchor" bson:"anchor"` //作者 + Cover string `json:"cover" bson:"cover"` //封面 + ContentSet []Content `json:"contentSet" bson:"contentSet"` //内容集 + TotalEpisode int `json:"totalEpisode" bson:"totalEpisode"` //总集数 + TrialSet []Content `json:"trialSet" bson:"trialSet"` //试听集 + FType string `json:"fType" bson:"fType"` //小说类型 + Summary string `json:"summary" bson:"summary"` //简介 + IsActive bool `json:"isActive" bson:"isActive"` //是否激活 + CountPurchases int64 `json:"countPurchases" bson:"countPurchases"` //购买数 + CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` //浏览数 + CountCollect int64 `json:"countCollect" bson:"countCollect"` //收藏数 + Number int64 `json:"number" bson:"number,omitempty"` //编号 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type QuerySelector struct { + ID *primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + Title *string `json:"title,omitempty" bson:"title,omitempty"` //标题 + Anchor *string `json:"anchor,omitempty" bson:"anchor,omitempty"` //作者 + FType *string `json:"fType,omitempty" bson:"fType,omitempty"` //小说类型 + IsActive *bool `json:"isActive,omitempty" bson:"isActive,omitempty"` //是否激活 + Number *int `json:"number,omitempty" bson:"number,omitempty"` //编号 +} + +type EditSelector struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title,omitempty"` //标题 + Anchor string `json:"anchor" bson:"anchor"` //作者 + Cover string `json:"cover" bson:"cover"` //封面 + ContentSet []Content `json:"contentSet" bson:"contentSet"` //内容集 + TotalEpisode int `json:"totalEpisode" bson:"totalEpisode"` //总集数 + TrialSet []Content `json:"trialSet" bson:"trialSet"` //试听集 + FType string `json:"fType" bson:"fType"` //小说类型 + Summary string `json:"summary" bson:"summary"` //简介 + IsActive bool `json:"isActive" bson:"isActive"` //是否激活 + CountPurchases int64 `json:"countPurchases" bson:"countPurchases"` //购买数 + CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` //浏览数 + CountCollect int64 `json:"countCollect" bson:"countCollect"` //收藏数 + Number int `json:"number" bson:"number,omitempty"` //编号 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 +} + +const RedisSetKey = "audiobook" + +// todo app +type AudioBookAppRes struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title,omitempty"` //标题 + Anchor string `json:"-" bson:"anchor"` //作者 + Cover string `json:"cover" bson:"cover"` //封面 + ContentSet []Content `json:"contentSet" bson:"contentSet"` //内容集 + TrialSet []Content `json:"trialSet" bson:"trialSet"` //试听集 + FType string `json:"fType" bson:"fType"` //小说类型 + Summary string `json:"summary" bson:"summary"` //简介 + IsActive bool `json:"isActive" bson:"isActive"` //是否激活 + TotalEpisode int `json:"totalEpisode" bson:"totalEpisode"` //总集数 + CountPurchases int64 `json:"countPurchases" bson:"countPurchases"` //购买数 + CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` //浏览数 + OriginalPrice int64 `json:"originalPrice" bson:"-"` //原价 + DiscountPrice int64 `json:"discountPrice" bson:"-"` //优惠价(用户实际需要支付的价格) +} + +type AudioBookBase struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title,omitempty"` //标题 + Anchor string `json:"anchor" bson:"anchor"` //作者 + Cover string `json:"cover" bson:"cover"` //封面 + TotalEpisode int `json:"totalEpisode" bson:"totalEpisode"` //总集数 + TrialSet []Content `json:"trialSet" bson:"trialSet"` //试听集 + FType string `json:"fType" bson:"fType"` //小说类型 + Summary string `json:"summary" bson:"summary"` //简介 + CountPurchases int64 `json:"countPurchases" bson:"countPurchases"` //购买数 + CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` //浏览数 + ContentSet []struct { + EpisodeNumber int `json:"episodeNumber" bson:"episodeNumber" ` //第几集 + ListenPermission int `json:"listenPermission" bson:"listenPermission"` //收听权限 0:会员 1:金币购买 3:试听 + Price int64 `json:"price" bson:"price"` //购买价格 + Name string `json:"name" bson:"name"` //章节名 + MediaSize int64 `json:"mediaSize" bson:"mediaSize"` //资源大小 + } `json:"contentSet" bson:"contentSet"` +} + +type AudioBookUpsertEdit struct { + Title string `json:"title" bson:"title,omitempty"` //标题 + Anchor string `json:"anchor" bson:"anchor"` //作者 + Cover string `json:"cover" bson:"cover"` //封面 + ContentSet []Content `json:"contentSet" bson:"-"` //内容集 + TotalEpisode int `json:"totalEpisode" bson:"totalEpisode"` //总集数 + TrialSet []Content `json:"trialSet" bson:"trialSet"` //试听集 + FType string `json:"fType" bson:"fType"` //小说类型 + Summary string `json:"summary" bson:"summary"` //简介 + IsActive bool `json:"isActive" bson:"isActive"` //是否激活 + Number int64 `json:"number" bson:"number,omitempty"` //编号 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 +} + +type CountInc struct { + CountPurchases *int `json:"countPurchases" bson:"countPurchases,omitempty"` //购买数 + CountBrowse *int `json:"countBrowse" bson:"countBrowse,omitempty"` //浏览数 + CountCollect *int `json:"countCollect" bson:"countCollect,omitempty"` //收藏数 +} + +type ESAudioBookSource struct { + ID string `json:"_id"` + Source AudioBookBase `json:"_source"` +} diff --git a/models/v/authoritymod/authority.go b/models/v/authoritymod/authority.go new file mode 100644 index 0000000..8ddaf12 --- /dev/null +++ b/models/v/authoritymod/authority.go @@ -0,0 +1,131 @@ +package authoritymod + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.Authority + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "role", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// Insert 插入记录 +func Insert(doc AuthorityDoc) error { + now := time.Now() + doc.CreatedAt = &now + doc.UpdatedAt = &now + if _, err := coll(nil).InsertOne(doc); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return err + } + return nil +} + +// UpdateTo +func UpdateTo(id ObjectID, doc AuthorityDoc) error { + now := time.Now() + doc.UpdatedAt = &now + update, err := common.ToBsonM(doc) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateTo", table, "ToBsonM", err), log.Any("id", id)) + return err + } + result, err := coll(nil).UpdateOne(M{"_id": id}, M{"$set": update}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateTo", table, "UpdateOne", err), log.Any("id", id), log.Any("update", update)) + return err + } + if result.ModifiedCount+result.UpsertedCount != 1 { + return fmt.Errorf("coll:%s UpdateTo Count error, id:%+v update:%+v", table, id, update) + } + return nil +} + +// DeleteMany DeleteMany +func DeleteMany(idArray []ObjectID) error { + if _, err := coll(nil).DeleteMany(M{"_id": M{"$in": idArray}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteMany", table, "DeleteMany", err)) + return err + } + return nil +} + +func FindOne(doc AuthorityDoc) (authority Authority, err error) { + filter, err := common.ToBsonM(doc) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOne", table, "ToBsonM", err)) + return + } + if err = coll(nil).FindOne(&authority, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOne", table, "FindOne", err)) + } + return +} + +func ExistsByID(id ObjectID) (existed bool, err error) { + filter := M{ + "_id": id, + } + count, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ExistsByID", table, "Count", err)) + return false, err + } + return count > 0, nil +} + +// FineOneByRole 通过权限id获取权限json +func FineOneByRole(role string) (Authority, error) { + var auth Authority + if err := coll(nil).FindOne(&auth, bson.M{"role": role}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FineOneByRole", table, "FindOne", err), log.Any("role", role)) + return Authority{}, err + } + return auth, nil +} + +// IDMapByRoles role->id Map +func IDMapByRoles(roleArray []string) (map[string]ObjectID, error) { + filter := M{ + "role": M{"$in": roleArray}, + } + authArray := make([]Authority, 0, len(roleArray)) + if err := coll(nil).Find(&authArray, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IDMapByRoles", table, "Find", err), log.Any("roleArray", roleArray)) + return nil, err + } + idMap := make(map[string]ObjectID, len(authArray)) + for _, auth := range authArray { + idMap[auth.Role] = auth.ID + } + return idMap, nil +} diff --git a/models/v/authoritymod/page.go b/models/v/authoritymod/page.go new file mode 100644 index 0000000..8d4fa74 --- /dev/null +++ b/models/v/authoritymod/page.go @@ -0,0 +1,40 @@ +package authoritymod + +import ( + "fmt" + + "91porn-server/common/log" +) + +// AuthorPage AuthorPage +type AuthorPage struct { + Total int `json:"total" bson:"total"` + List []AuthorityDoc `json:"list" bson:"list"` +} + +// AuthorPages AuthorPages +func AuthorPages(skip int64, limit int64) (page AuthorPage, err error) { + pipeline := []M{ + M{ + "$facet": M{ + "total": A{M{"$count": "total"}}, + "list": A{ + M{"$sort": M{"role": 1}}, + M{"$skip": skip}, + M{"$limit": limit}, + }, + }, + }, + M{ + "$project": M{ + "total": M{"$arrayElemAt": A{"$total.total", 0}}, + "list": 1, + }, + }, + } + if err = coll(nil).AggregateDecode(&page, pipeline); err != nil { + log.ZapLog.Error(fmt.Sprintf("coll:%s Page error:%+v:", table, err)) + return + } + return +} diff --git a/models/v/authoritymod/type.go b/models/v/authoritymod/type.go new file mode 100644 index 0000000..97797ce --- /dev/null +++ b/models/v/authoritymod/type.go @@ -0,0 +1,48 @@ +package authoritymod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ( + // M M from Map + M = bson.M + + // D D from Doc + D = bson.D + + // A A from Array + A = bson.A + + // ObjectID ObjectID + ObjectID = primitive.ObjectID + + // 用于记录权限配置 + Authority struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Role string `json:"role" bson:"role"` //角色,映射到权限模型 + AuthJson string `json:"authJson" bson:"authJson"` //"路由权限数据,json格式" + ActJson string `json:"actJson" bson:"actJson"` //"功能权限数据,json格式" + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + } + + AuthorityDoc struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Role *string `json:"role" bson:"role,omitempty"` //角色,映射到权限模型 + AuthJson *string `json:"authJson" bson:"authJson,omitempty" ` //"路由权限数据,json格式" + ActJson *string `json:"actJson" bson:"actJson,omitempty"` //"功能权限数据,json格式" + CreatedAt *time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` + UpdatedAt *time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` + } +) + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/backpackmod/app.go b/models/v/backpackmod/app.go new file mode 100644 index 0000000..5e8fd6d --- /dev/null +++ b/models/v/backpackmod/app.go @@ -0,0 +1,9 @@ +package backpackmod + +import "91porn-server/models/commod" + +type QueryAllCond struct { + UID uint64 + Status *GoodsStatus `form:"status" json:"status" binding:"required,min=1"` // 物品状态 + commod.Page +} diff --git a/models/v/backpackmod/backpack.go b/models/v/backpackmod/backpack.go new file mode 100644 index 0000000..b9572f4 --- /dev/null +++ b/models/v/backpackmod/backpack.go @@ -0,0 +1,221 @@ +package backpackmod + +import ( + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.Backpack + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 索引设置 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{ + {Key: "uid", Value: 1}, + {Key: "status", Value: 1}, + {Key: "createTimt", Value: -1}, + }, + }, + { + Keys: bson.D{ + {Key: "uid", Value: 1}, + {Key: "status", Value: 1}, + {Key: "useTime", Value: -1}, + }, + }, + { + Keys: bson.D{ + {Key: "uid", Value: 1}, + {Key: "expiredTime", Value: -1}, + }, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("backpack model set index err ==>[%+v]", err)) + } +} + +// 获取个人拥有可用优惠卷列表 +func GetCouponListByUID(uid uint64, goodsType int) ([]*Backpack, error) { + var out []*Backpack = make([]*Backpack, 0) + if err := coll(nil).Find(&out, bson.M{ + "uid": uid, + "goodsType": goodsType, + "status": Unused, + "expiredTime": bson.M{"$gt": time.Now()}, + }); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCouponListByUID", models.Backpack, "Find", err), + log.Any("uid", uid), + log.Any("goodsType", goodsType), + ) + return nil, err + } + return out, nil +} + +// 添加物品到背包 +func AddGoods(t *db.MongoTool, uid uint64, b ...Backpack) error { + for _, item := range b { + item.GoodsName = item.GoodsType.GetName() + item.GoodsDesc = item.GoodsType.GetDesc() + } + if _, err := coll(t).InsertMany(&b); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AddGoods", models.Backpack, "InsertMany", err), + log.Any("uid", uid), + ) + return err + } + return nil +} + +// AddGoodsMany 添加物品到背包 +func AddGoodsMany(t *db.MongoTool, uid uint64, b []Backpack) error { + if _, err := coll(t).InsertMany(&b); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AddGoodsMany", models.Backpack, "InsertMany", err), + log.Any("uid", uid), + ) + return err + } + return nil +} + +// 使用物品 +func UseGoods(t *db.MongoTool, id primitive.ObjectID) error { + result, err := coll(t).UpdateOne(bson.M{"_id": id}, bson.M{"$set": bson.M{"status": Used, "useTime": time.Now()}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UseGoods", models.Backpack, "UpdateOne", err), + log.Any("id", id), + ) + return err + } + if result.ModifiedCount == 0 { + err = errors.New("backpack UpdateOne ModifiedCount err") + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UseGoods", models.Backpack, "result.ModifiedCount == 0", err), + log.Any("id", id), + ) + return err + } + return nil +} + +// 查询背包列表 +func GetCouponList(uid uint64, types, status int, limit, page int64) ([]*Backpack, error) { + var out []*Backpack = make([]*Backpack, 0) + cond := bson.M{"uid": uid} + sort := bson.M{} + switch GoodsStatus(status) { + case Used: + sort["useTime"] = -1 + cond["status"] = status + case Unused: + cond["status"] = status + cond["expiredTime"] = bson.M{"$gt": time.Now()} + sort["createTime"] = 1 + case Expired: + cond["expiredTime"] = bson.M{"$lte": time.Now()} + sort["expiredTime"] = -1 + } + if types > 0 { + cond["goodsType"] = types + } + opts := options.Find().SetLimit(limit).SetSkip((page - 1) * limit).SetSort(sort) + if err := coll(nil).Find(&out, cond, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetGoodsList", models.Backpack, "Find", err), + log.Any("uid", uid), + ) + return nil, err + } + return out, nil +} + +// 获取物品信息 +func GetGoodsDetail(id primitive.ObjectID) (*Backpack, error) { + var out *Backpack + if err := coll(nil).FindOne(&out, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetGoodsDetail", models.Backpack, "FindOne", err), + log.Any("id", id), + ) + return nil, err + } + return out, nil +} + +// 根据id获取券信息 +// 如果未找到, 返回nil +func GetByID(t *db.MongoTool, id primitive.ObjectID) (*Backpack, error) { + var bp Backpack + if err := coll(t).FindOne(&bp, bson.M{"_id": id}); err != nil { + return nil, err + } + if bp.ID != id { + return nil, nil + } + return &bp, nil +} + +// UseManyGoods 使用物品(多件) +func UseManyGoods(t *db.MongoTool, ids []primitive.ObjectID) (int64, error) { + filter := bson.M{"_id": bson.M{"$in": ids}, "status": Unused} + result, err := coll(t).UpdateMany(filter, bson.M{"$set": bson.M{"status": Used, "useTime": time.Now()}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UseManyGoods", models.Backpack, "UpdateOne", err), + log.Any("id", ids), + ) + return 0, err + } + return result.ModifiedCount, nil +} + +// UnuseManyGoods 退换物品(多件) +func UnuseManyGoods(t *db.MongoTool, ids []primitive.ObjectID) (int64, error) { + result, err := coll(t).UpdateMany(bson.M{"_id": bson.M{"$in": ids}, "status": Used}, bson.M{"$set": bson.M{"status": Unused}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UseManyGoods", models.Backpack, "UpdateOne", err), + log.Any("id", ids), + ) + return 0, err + } + return result.ModifiedCount, nil +} + +// QueryAllList 分页查询文档 +func QueryAllList(filter primitive.M, opts ...*options.FindOptions) (out []*Backpack, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} diff --git a/models/v/backpackmod/type.go b/models/v/backpackmod/type.go new file mode 100644 index 0000000..6fd719e --- /dev/null +++ b/models/v/backpackmod/type.go @@ -0,0 +1,76 @@ +package backpackmod + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +var itemNames = map[GoodsType]string{ + LouFengUnLock: "楼风解锁折扣卷", + VIPDiscount: "会员折扣卷", + AiChangeFaceDiscount: "AI换脸折扣券", + GoldBonusCoupon: "金币加赠券", + ExchangeCoupon: "兑换码观影券", + SIGNCoupon: "签到赠观影券", +} + +var itemDesc = map[GoodsType]string{ + LouFengUnLock: "楼风解锁专享折扣", + VIPDiscount: "购买会员专享折扣", + AiChangeFaceDiscount: "AI换脸专享折扣", + GoldBonusCoupon: "充值加送金币", + ExchangeCoupon: "兑换码观影券", + SIGNCoupon: "签到赠观影券", +} + +type GoodsType int + +const ( + LouFengUnLock GoodsType = 1 // 1、楼风解锁折扣卷 + VIPDiscount GoodsType = 2 // 2、会员折扣卷 + AiChangeFaceDiscount GoodsType = 3 // 3、AI换脸折扣券 + GameCode GoodsType = 4 // 4、游戏码 + GoldBonusCoupon GoodsType = 5 // 5、金币加赠券(用于购买金币时额外赠送额度) + ExchangeCoupon GoodsType = 6 // 6、兑换赠观影券 + SIGNCoupon GoodsType = 7 // 7、签到赠观影券 +) + +func (t GoodsType) GetName() string { + return itemNames[t] +} +func (t GoodsType) GetDesc() string { + return itemDesc[t] +} + +type GoodsStatus int + +const ( + Used GoodsStatus = iota + 1 // 已使用 + Unused // 未使用 + Expired // 过期 +) + +type Backpack struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档ID + UID uint64 `json:"uid" bson:"uid"` // 用户ID + GoodsName string `json:"goodsName" bson:"goodsName"` // 物品名称 + GoodsType GoodsType `json:"goodsType" bson:"goodsType"` // 物品类型 + GoodsValue int64 `json:"goodsValue" bson:"goodsValue"` // 物品价值 + GoodsOrigin string `json:"goodsOrigin" bson:"goodsOrigin"` // 物品来源 + GoodsDesc string `json:"goodsDesc" bson:"goodsDesc"` // 物品描述 + Status GoodsStatus `json:"status" bson:"status"` // 物品状态 + ExpiredTime time.Time `json:"expiredTime" bson:"expiredTime"` // 物品过期时间 + UseTime time.Time `json:"useTime" bson:"useTime"` // 物品使用时间 + CreateTime time.Time `json:"createTime" bson:"createTime"` // 创建时间 +} + +func Init() { + mdb = db.Init(models.Backpack) + initIndex() +} diff --git a/models/v/backpackmod/web.go b/models/v/backpackmod/web.go new file mode 100644 index 0000000..c3e997e --- /dev/null +++ b/models/v/backpackmod/web.go @@ -0,0 +1,41 @@ +package backpackmod + +import ( + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type QueryAllWebCond struct { + Page int64 `form:"page" binding:"required"` + Limit int64 `form:"limit" binding:"required"` + Uid *int `form:"uid"` // 用户ID + Status *int `form:"status"` // 使用状态 1、已使用 2、未使用 3、过期 + GoodsType *int `form:"goodsType"` // 类型 + GoodsDesc *string `form:"goodsDesc"` // 描述 +} + +type QueryAllRes struct { + List []*Backpack `json:"list"` + Total int64 `json:"total"` +} + +func (receiver *QueryAllWebCond) Filter() bson.M { + var query = bson.M{} + if receiver.Uid != nil { + query["uid"] = receiver.Uid + } + if receiver.GoodsType != nil { + query["goodsType"] = receiver.GoodsType + } + if receiver.GoodsDesc != nil { + query["goodsDesc"] = receiver.GoodsDesc + } + if receiver.Status != nil { + query["status"] = receiver.Status + } + return query +} + +func (receiver QueryAllWebCond) Options() *options.FindOptions { + return options.Find().SetSkip((receiver.Page - 1) * receiver.Limit).SetLimit(receiver.Limit).SetSort(bson.D{{"createTime", -1}}) +} diff --git a/models/v/bannerjumpmod/app.go b/models/v/bannerjumpmod/app.go new file mode 100755 index 0000000..08026a0 --- /dev/null +++ b/models/v/bannerjumpmod/app.go @@ -0,0 +1,19 @@ +package bannerjumpmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// BannerJumpInfo 移动端返回内容 +type BannerJumpInfo struct { + ID primitive.ObjectID `json:"id"` // 浮窗ID + Position Position `json:"position"` // 位置 1-视频播放器下方 2-收益页 3-首页浮窗 + Banner string `json:"banner"` // BANNER图片 + Title string `json:"title"` // 标题 + Url string `json:"url"` // 跳转地址 + StartAt time.Time `json:"startAt"` // 开始时间;CountdownType=1 时为红包雨场次开始时间,否则为 banner 自身开始时间 + EndAt time.Time `json:"endAt"` // 结束时间;CountdownType=1 时为红包雨场次结束时间,否则为 banner 自身结束时间 + CountdownType int `json:"countdownType"` // 倒计时类型 0-无 1-红包雨倒计时 +} diff --git a/models/v/bannerjumpmod/bannerjump.go b/models/v/bannerjumpmod/bannerjump.go new file mode 100755 index 0000000..8b1071c --- /dev/null +++ b/models/v/bannerjumpmod/bannerjump.go @@ -0,0 +1,130 @@ +package bannerjumpmod + +import ( + "encoding/json" + "errors" + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []BannerJump, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// GetAll 查询全部文档 +func GetAll(filter primitive.M, sort bson.D) (out []*BannerJump, err error) { + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAll", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (BannerJump, error) { + v := BannerJump{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d BannerJump) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} diff --git a/models/v/bannerjumpmod/type.go b/models/v/bannerjumpmod/type.go new file mode 100755 index 0000000..0c14e72 --- /dev/null +++ b/models/v/bannerjumpmod/type.go @@ -0,0 +1,62 @@ +package bannerjumpmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.BannerJump + +type Position = int + +const ( + VideoPlayer Position = 1 // 视频播放器下翻跳转banner + UsersProxy Position = 2 // 全民代理页面页面banner +) + +type BannerJump struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + Position Position `json:"position" bson:"position"` // 位置 1-视频播放器下方 2-收益页 3-首页浮窗 + Banner string `json:"banner" bson:"banner"` // BANNER图片 + Title string `json:"title" bson:"title"` // 标题 + Url string `json:"url" bson:"url"` // 跳转地址 + StartAt time.Time `json:"startAt" bson:"startAt"` // 开始时间 + EndAt time.Time `json:"endAt" bson:"endAt"` // 结束时间 + Weight int `json:"weight" bson:"weight"` // 权重 + Enable bool `json:"enable" bson:"enable"` // 是否启用 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "startAt", Value: 1}, {Key: "endAt", Value: 1}, {Key: "enable", Value: 1}, {Key: "weight", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} diff --git a/models/v/chatrobotmod/chatrobot.go b/models/v/chatrobotmod/chatrobot.go new file mode 100644 index 0000000..f1c40a3 --- /dev/null +++ b/models/v/chatrobotmod/chatrobot.go @@ -0,0 +1,98 @@ +package chatrobotmod + +import ( + "fmt" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.ChatRobot + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initCommentIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "type", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "isActive", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func Insert(doc ChatRobot) error { + if _, err := coll(nil).InsertOne(doc); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return err + } + return nil +} + +func ConfExists(rType EnumRobotType) (isExists bool, err error) { + if isExists, err = coll(nil).Exists(bson.M{"type": rType}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ConfExists", table, "Exists", err), log.Any("rType", rType)) + return + } + return +} + +func GetRobotConfByType(rType EnumRobotType) (data ChatRobot, err error) { + if err = coll(nil).FindOne(&data, bson.M{"type": rType}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetRobotConfByType", table, "FindOne", err), log.Any("rType", rType)) + return + } + return +} + +func GetRobotConf(rType *EnumRobotType, skip int64, limit int64) (total int64, data []ChatRobot, err error) { + var query = bson.M{} + if rType != nil { + query["type"] = *rType + } + var opts = options.Find() + opts.SetSkip(skip).SetLimit(limit) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetRobotConf", table, "Find", err), log.Any("rType", rType)) + return + } + if total, err = coll(nil).Count(query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetRobotConf", table, "Count", err), log.Any("rType", rType)) + return + } + return +} + +func UpdateCmtRobotConf(rType EnumRobotType, doc CmtUpdateDoc) (err error) { + var update, _ = common.ToBsonM(doc) + if _, err = coll(nil).UpdateOne(bson.M{"type": rType}, bson.M{"$set": update}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateCmtRobotConf", table, "UpdateOne", err), + log.Any("rType", rType), + log.Any("update", update), + ) + return + } + return +} diff --git a/models/v/chatrobotmod/type.go b/models/v/chatrobotmod/type.go new file mode 100644 index 0000000..6c082f3 --- /dev/null +++ b/models/v/chatrobotmod/type.go @@ -0,0 +1,63 @@ +package chatrobotmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ( + ObjectID = primitive.ObjectID + EnumRobotType = string // 机器人类型枚举 +) + +const ( + Comment EnumRobotType = "COMMENT" // 评论机器人 + Film EnumRobotType = "FILM" // 影院机器人 +) + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type ChatRobot struct { + ID ObjectID `bson:"_id,omitempty"` + Type EnumRobotType `bson:"type"` // 机器人类型 + IsActive bool `bson:"isActive"` // 是否开启 + CreatedAt time.Time `bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `bson:"updatedAt"` // 更新时间 + CommentRobot `bson:",inline"` // 评论机器人配置 +} + +// 评论机器人配置 +type CommentRobot struct { + From string `bson:"from,omitempty"` // 每天的起始时间 HH:MM:SS + To string `bson:"to,omitempty"` // 每天的结束时间 HH:MM:SS + TotalLimit int `bson:"totalLimit,omitempty"` // 机器人每天总评论数 + PureLimit int `bson:"pureLimit,omitempty"` // 单个机器人每天评论数 + Frequency int `bson:"frequency"` // 发表评论频率 单位:秒/条 + MaxCommentNum int `bson:"maxCommentNum"` // 视频评论最小数 + MaxFakeLikes int `bson:"maxFakeLikes"` // 视频点赞数最大值 + MinFakeLikes int `bson:"minFakeLikes"` // 视频点赞数最小值 + MaxFakePlayCount int `bson:"maxFakePlayCount"` // 视频播放量最大值 + MinFakePlayCount int `bson:"minFakePlayCount"` // 视频播放量最小值 +} + +// 评论机器人配置更新 +type CmtUpdateDoc struct { + From *string `bson:"from,omitempty"` + To *string `bson:"to,omitempty"` + TotalLimit *int `bson:"totalLimit,omitempty"` + PureLimit *int `bson:"pureLimit,omitempty"` + IsActive *bool `bson:"isActive,omitempty"` + Frequency *int `bson:"frequency"` + UpdatedAt time.Time `bson:"updatedAt"` + MaxCommentNum *int `bson:"maxCommentNum"` + MaxFakeLikes *int `bson:"maxFakeLikes"` // 视频点赞数最大值 + MinFakeLikes *int `bson:"minFakeLikes"` // 视频点赞数最小值 + MaxFakePlayCount *int `bson:"maxFakePlayCount"` // 视频播放量最大值 + MinFakePlayCount *int `bson:"minFakePlayCount"` // 视频播放量最小值 +} diff --git a/models/v/chatrobotmod/web.go b/models/v/chatrobotmod/web.go new file mode 100644 index 0000000..31afa6d --- /dev/null +++ b/models/v/chatrobotmod/web.go @@ -0,0 +1,74 @@ +package chatrobotmod + +import ( + "time" + + "91porn-server/models/commod" +) + +type ReqConfList struct { + Type *string `form:"type" json:"type"` + commod.Page +} + +type RespList struct { + ID ObjectID `json:"id"` + Type EnumRobotType `json:"type"` // 机器人类型 + IsActive bool `json:"isActive"` // 是否开启 + CreatedAt time.Time `json:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt"` // 更新时间 + CommentRobotResp `json:",inline"` +} + +type CommentRobotResp struct { + From string `json:"from"` // 每天的起始时间 HH:MM:SS + To string `json:"to"` // 每天的结束时间 HH:MM:SS + TotalLimit int `json:"totalLimit"` // 机器人每天总评论数 + PureLimit int `json:"pureLimit"` // 单个机器人每天评论数 + Frequency int `json:"frequency"` // 发表评论频率 单位:秒/条 + MaxCommentNum int `json:"maxCommentNum"` // 参与评论视频的最大评论数 + MaxFakeLikes int `json:"maxFakeLikes"` // 视频点赞数最大值 + MinFakeLikes int `json:"minFakeLikes"` // 视频点赞数最小值 + MaxFakePlayCount int `json:"maxFakePlayCount"` // 视频播放量最大值 + MinFakePlayCount int `json:"minFakePlayCount"` // 视频播放量最小值 +} + +type ReqUpdate struct { + Type EnumRobotType `json:"type" binding:"required"` + ReqUpdateCmt // 评论机器人配置更新请求参数 +} + +// 评论机器人配置更新请求参数 +type ReqUpdateCmt struct { + From *string `json:"from"` + To *string `json:"to"` + TotalLimit *int `json:"totalLimit"` + PureLimit *int `json:"pureLimit"` + Frequency *int `json:"frequency"` + IsActive *bool `json:"isActive"` + MaxCommentNum *int `json:"maxCommentNum"` + MaxFakeLikes *int `json:"maxFakeLikes"` // 视频点赞数最大值 + MinFakeLikes *int `json:"minFakeLikes"` // 视频点赞数最小值 + MaxFakePlayCount *int `json:"maxFakePlayCount"` // 视频播放量最大值 + MinFakePlayCount *int `json:"minFakePlayCount"` // 视频播放量最小值 +} + +// 添加配置 +type ReqAdd struct { + Type EnumRobotType `json:"type" binding:"required"` + ReqAddCmt +} + +type ReqAddCmt struct { + From string `json:"from"` + To string `json:"to"` + TotalLimit int `json:"totalLimit"` + PureLimit int `json:"pureLimit"` + Frequency int `json:"frequency"` + IsActive bool `json:"isActive"` + MaxCommentNum int `json:"maxCommentNum"` + MaxFakeLikes int `json:"maxFakeLikes"` // 视频点赞数最大值 + MinFakeLikes int `json:"minFakeLikes"` // 视频点赞数最小值 + MaxFakePlayCount int `json:"maxFakePlayCount"` // 视频播放量最大值 + MinFakePlayCount int `json:"minFakePlayCount"` // 视频播放量最小值 +} diff --git a/models/v/checkinconfigmod/checkinconfig.go b/models/v/checkinconfigmod/checkinconfig.go new file mode 100644 index 0000000..e3e04a4 --- /dev/null +++ b/models/v/checkinconfigmod/checkinconfig.go @@ -0,0 +1,83 @@ +package checkinconfigmod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +const table = models.CheckinConfig + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "enable", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +// FindOne 查找签到配置 +func FindOne(filter primitive.M) (*CheckinConfig, error) { + var config CheckinConfig + if err := coll(nil).FindOne(&config, filter); err != nil { + log.Error(fmt.Sprintf("[checkinconfigmod:FindOne] failed: %+v", err)) + return nil, err + } + if config.ID.IsZero() { + return nil, nil + } + return &config, nil +} + +// UpdateOne 更新签到配置 +func UpdateOne(filter primitive.M, update primitive.M) error { + result, err := coll(nil).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[checkinconfigmod:UpdateOne] failed: %+v", err)) + return err + } + if result.MatchedCount == 0 { + return mongo.ErrNoDocuments + } + return nil +} + +// InsertOne 插入签到配置 +func InsertOne(config *CheckinConfig) error { + if _, err := coll(nil).InsertOne(config); err != nil { + log.Error(fmt.Sprintf("[checkinconfigmod:InsertOne] failed: %+v", err)) + return err + } + return nil +} + +// Count 统计数量 +func Count(filter primitive.M) (int64, error) { + count, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[checkinconfigmod:Count] failed: %+v", err)) + return 0, err + } + return count, nil +} diff --git a/models/v/checkinconfigmod/type.go b/models/v/checkinconfigmod/type.go new file mode 100644 index 0000000..78f063e --- /dev/null +++ b/models/v/checkinconfigmod/type.go @@ -0,0 +1,33 @@ +package checkinconfigmod + +import ( + "91porn-server/common/db" + "91porn-server/models/v/prizemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const CheckinConfigId = "checkin_config_1" + +type CheckinConfig struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Enable bool `json:"enable" bson:"enable"` // 是否启用 + Description string `json:"description" bson:"description"` // 规则说明 + BackgroundImage string `json:"backgroundImage" bson:"backgroundImage"` // 背景图片 + RewardBgVideos []RewardBgVideo `json:"rewardBgVideos" bson:"rewardBgVideos"` // 奖励播放媒体地址 + IntegerExchangeList []GiftItem `json:"integerExchangeList" bson:"integerExchangeList"` // 积分可兑换豪礼列表 +} + +type GiftItem struct { + Name string `json:"name" bson:"name"` + Icon string `json:"icon" bson:"icon"` +} + +type RewardBgVideo struct { + PrizeType prizemod.PrizeType `json:"prizeType" bson:"prizeType"` // 奖品类型 + BgMediaUrl string `json:"bgMediaUrl" bson:"bgMediaUrl"` // 奖励背景动画 + FsResourceStatus string `json:"fsResourceStatus" bson:"fsResourceStatus"` // 视频文件切片状态 + FsResourceId string `json:"fsResourceId" bson:"fsResourceId"` // 文件服id +} + +var mdb *db.MongoDB diff --git a/models/v/checkinprizemod/checkinprize.go b/models/v/checkinprizemod/checkinprize.go new file mode 100644 index 0000000..322e44c --- /dev/null +++ b/models/v/checkinprizemod/checkinprize.go @@ -0,0 +1,116 @@ +package checkinprizemod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.CheckinPrize + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "status", Value: 1}, {Key: "checkinType", Value: 1}, {Key: "checkinDays", Value: 1}}, + }, + { + Keys: bson.D{{Key: "checkinDays", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +// InsertOne 插入一个签到奖品 +func InsertOne(data *CheckinPrize) error { + if _, err := coll(nil).InsertOne(data); err != nil { + log.Error(fmt.Sprintf("[checkinprizemod:InsertOne] failed: %+v", err)) + return err + } + return nil +} + +// FindMany 查找一批签到奖品 +func FindMany(filter primitive.M, opts ...*options.FindOptions) (CheckinPrizeList, error) { + data := make(CheckinPrizeList, 0) + if err := coll(nil).Find(&data, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[checkinprizemod:FindMany] failed: %+v", err)) + return nil, err + } + return data, nil +} + +// FindManyWithBind 用自定义结构查找 +func FindManyWithBind(bind interface{}, filter primitive.M, opts ...*options.FindOptions) error { + if err := coll(nil).Find(bind, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[checkinprizemod:FindManyWithBind] failed: %+v", err)) + return err + } + return nil +} + +// FindOne 查找一个签到奖品 +func FindOne(filter primitive.M) (*CheckinPrize, error) { + var data CheckinPrize + if err := coll(nil).FindOne(&data, filter); err != nil { + log.Error(fmt.Sprintf("[checkinprizemod:FindOne] failed: %+v", err)) + return nil, err + } + if data.ID.IsZero() { + return nil, nil + } + return &data, nil +} + +// Count 统计数量 +func Count(filter primitive.M) (int64, error) { + count, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[checkinprizemod:Count] failed: %+v", err)) + return 0, err + } + return count, nil +} + +// UpdateOne 更新一个签到奖品 +func UpdateOne(filter primitive.M, update primitive.M) error { + result, err := coll(nil).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[checkinprizemod:UpdateOne] failed: %+v", err)) + return err + } + if result.MatchedCount == 0 { + return mongo.ErrNoDocuments + } + return nil +} + +// DeleteOne 删除一个签到奖品 +func DeleteOne(filter primitive.M) (int64, error) { + result, err := coll(nil).DeleteOne(filter) + if err != nil { + log.Error(fmt.Sprintf("[checkinprizemod:DeleteOne] failed: %+v", err)) + return 0, err + } + return result.DeletedCount, nil +} diff --git a/models/v/checkinprizemod/type.go b/models/v/checkinprizemod/type.go new file mode 100644 index 0000000..cbdb2a6 --- /dev/null +++ b/models/v/checkinprizemod/type.go @@ -0,0 +1,47 @@ +package checkinprizemod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type CheckinType int64 + +const ( + CheckinTypeNone CheckinType = 0 // 占位符,无意义 + CheckinTypeContinuously CheckinType = 1 // 连续签到 + CheckinTypeCumulative CheckinType = 2 // 累计签到 +) + +type CheckinPrize struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + Title string `json:"title" bson:"title"` // 标题 + Image string `json:"image" bson:"image"` // 图片 + CheckinDays int64 `json:"checkinDays" bson:"checkinDays"` // 签到天数 + CheckinType CheckinType `json:"checkinType" bson:"checkinType"` // 签到类型 + PrizeId primitive.ObjectID `json:"prizeId" bson:"prizeId"` // 奖品ID(关联activity_prize) + PrizeName string `json:"prizeName" bson:"prizeName"` // 奖品名称 + Status bool `json:"status" bson:"status"` // 状态 + BigPrize bool `json:"bigPrize" bson:"bigPrize"` // 是否VIP奖励 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +func (s *CheckinPrize) Desc() string { + msg := "" + if s.CheckinType == CheckinTypeContinuously { + msg += "连续" + } else if s.CheckinType == CheckinTypeCumulative { + msg += "累计" + } + msg += fmt.Sprintf("签到%d天", s.CheckinDays) + return msg +} + +type CheckinPrizeList []*CheckinPrize + +var mdb *db.MongoDB diff --git a/models/v/cmtmod/agent.go b/models/v/cmtmod/agent.go new file mode 100644 index 0000000..e53cc70 --- /dev/null +++ b/models/v/cmtmod/agent.go @@ -0,0 +1,149 @@ +package cmtmod + +type ComicsTitlePullReq struct { + PageNum int64 `form:"pageNum" json:"pageNum"` + PageSize int64 `form:"pageSize" json:"pageSize"` + TagId string `form:"tagId" json:"tagId"` +} + +type ComicsTitlePullResp struct { + List []ComicsTitleInfo `json:"list"` +} + +type ComicsTitleInfo struct { + Id string `json:"id"` + Title string `json:"title"` +} + +type ComicsTitlePushReq struct { + TagId string `json:"tagId"` + List []ComicsTitlePushInfo `json:"list"` +} + +type ComicsTitlePushInfo struct { + Id string `json:"id"` + Title string `json:"title"` +} + +type ComicsTitlePushRes struct{} + +type PullReq struct { + AppId string `form:"appId" json:"appId" binding:"required"` //应用标识,与平台配置一致 + Sign string `form:"sign" json:"sign" binding:"required"` //签名 + Limit int `form:"limit" json:"limit" binding:"omitempty"` //单次拉取数量,默认 20,最大 100 + Status string `form:"status" json:"status" binding:"omitempty"` //评论状态,固定传 "pending" +} + +type PullResp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data []PullComment `json:"data"` +} + +type PullComment struct { + Id string `json:"id"` //评论唯一 ID + UserId string `json:"user_id"` //发布者用户 ID + Nickname string `json:"nickname"` //发布者昵称 + Content string `json:"content"` //评论正文 + CreatedAt string `json:"created_at"` //发布时间,ISO 8601 格式 + Ip string `json:"ip"` //发布者 IP + Context Context `json:"context"` //评论正文 +} + +type Context struct { + ContentType int `json:"content_type"` //帖子类型 1=帖子 2=视频 3=漫画 + ContentId string `json:"content_id"` //所属内容业务 ID + ContentTitle string `json:"content_title"` //所属内容标题 +} + +type ApproveReq struct { + Reviews []Review `json:"reviews"` +} + +type AppIdJson struct { + AppId string `form:"appId" json:"appId"` +} + +type Review struct { + CommentId string `form:"comment_id" json:"comment_id" binding:"omitempty"` //评论 ID,与拉取接口返回的 id 一致 + Action string `form:"action" json:"action" binding:"omitempty" ` //审核动作:approve 通过,reject 拒绝 + Category string `form:"category" json:"category" binding:"omitempty"` //拒绝原因简述 + Reason string `form:"reason" json:"reason" binding:"omitempty"` //违规类别:abuse/politics/ad/spam/competitor/foreign/link/complaint/minor +} + +type RepliesReq struct { + Replies []ReplyReq `json:"replies"` +} + +type ReplyReq struct { + CommentId string `form:"comment_id" json:"comment_id" binding:"omitempty"` //被回复评论的id + Content string `form:"content" json:"content" binding:"omitempty"` // 被回复评论的内容 +} + +type ReplyResp struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +type SignBody struct { + AppID string `json:"appId" binding:"required"` + Sign string `json:"sign" binding:"required"` +} + +type VideoCommentSubmitReq struct { + AppID string `json:"appId" binding:"required"` + Sign string `json:"sign" binding:"required"` + Videos []VideoCommentItem `json:"videos" binding:"required"` +} + +type VideoCommentItem struct { + VideoID string `json:"videoId" binding:"required"` + Title string `json:"title" binding:"required"` + Tags []string `json:"tags,omitempty"` + Description string `json:"description,omitempty"` + Subtitle string `json:"subtitle,omitempty"` + CommentCount int `json:"commentCount" binding:"required"` +} + +type VideoCommentSubmitResp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data VideoCommentSubmitRespData `json:"data"` +} + +type VideoCommentSubmitRespData struct { + Results []VideoCommentSubmitResult `json:"results"` +} + +type VideoCommentSubmitResult struct { + ID string `json:"id"` + VideoID string `json:"videoId"` +} + +type VideoCommentResultReq struct { + AppID string `json:"appId" binding:"required"` + Sign string `json:"sign" binding:"required"` + IDs []string `json:"ids" binding:"required"` +} + +type VideoCommentResultResp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data VideoCommentResultRespData `json:"data"` +} + +type VideoCommentResultRespData struct { + Results []VideoCommentResultItem `json:"results"` +} + +type VideoCommentResultItem struct { + ID string `json:"id"` + VideoID string `json:"videoId"` + Comments []GeneratedVideoComment `json:"comments"` + ErrorMsg string `json:"errorMsg,omitempty"` +} + +type GeneratedVideoComment struct { + Type string `json:"type"` + Text string `json:"text"` +} diff --git a/models/v/cmtmod/app.go b/models/v/cmtmod/app.go new file mode 100644 index 0000000..4c41cb7 --- /dev/null +++ b/models/v/cmtmod/app.go @@ -0,0 +1,173 @@ +package cmtmod + +import ( + "91porn-server/models/v/usermod" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +// ParentRespList 列表响应信息(父评论) +type ParentRespList struct { + //全局唯一评论ID + ID string `form:"id" json:"id"` + //评论对象的ID + ObjID string `form:"objID" json:"objID"` + //发表评论的 用户ID + UserID uint64 `form:"userID" json:"userID"` + //发表评论的 用户昵称 + UserName string `form:"userName" json:"userName"` + //头像 URL + UserPortrait string `form:"userPortrait" json:"userPortrait" ` + //评论内容 + Content string `form:"content" json:"content"` + Image string `form:"image" json:"image"` + LinkStr string `form:"linkStr" json:"linkStr" bson:"linkStr"` //跳转配置 + LinkType int `form:"linkType" json:"linkType"` //1:内链,2:外链 + SearchKeyword string `form:"searchKeyword" json:"searchKeyword"` //搜索关键字 + //此评论点赞次数 + LikeCount int `form:"likeCount" json:"likeCount"` + //此评论的评论数 + CommCount int `form:"commCount" json:"commCount"` + //此评论的评论数,冗余 + CommCountBurden int `form:"commCountBurden" json:"commCountBurden"` + //是否是作者 + IsAuthor bool `form:"isAuthor" json:"isAuthor" ` + //是否点过赞 + IsLike bool `form:"isLike" json:"isLike"` + //评论者是否已关注 + IsFollow bool `form:"isFollow" json:"isFollow"` + //是否已删除 + IsDelete bool `form:"isDelete" json:"isDelete"` + IsGodComment bool `form:"isGodComment" json:"isGodComment"` //是否是神评论 + //性别 + Gender string `form:"gender" json:"gender"` + //此评论的状态 + Status int `form:"status" json:"status"` + //评论地点 + City string `form:"city" json:"city"` + //评论时间 + CreatedAt time.Time `form:"createdAt" json:"createdAt"` + //年龄 + Age int `json:"age"` + // 引用类型 vid视频 col用户播单 sec官方播单 cover图集 + QuoteType string `json:"quoteType"` + // 引用id + QuoteID ObjectID `json:"quoteID"` + // 引用资源图片 + QuoteImg string `json:"quoteImg"` + // 引用资源标题 + QuoteTitle string `json:"quoteTitle"` + //用户vip等级 + Level int `json:"level"` + VipLevel int `json:"vipLevel"` + Awards []int `json:"awards"` //用户奖章 + AwardsExpire []usermod.AwardsExpireResp `json:"awardsExpire" bson:"awardsExpire"` //奖章是否有效 + SuperUser bool `json:"superUser" bson:"superUser"` //是否大V + VipExpireDate time.Time `json:"vipExpireDate" bson:"vipExpireDate"` + Info []ChildRespList +} + +// ChildRespList 列表响应信息(子评论) +type ChildRespList struct { + ID string `form:"id" json:"id"` //全局唯一评论ID + ObjID string `form:"objID" json:"objID"` //评论对象的ID + CID string `form:"cid" json:"cid"` //评论的id + UserID uint64 `form:"userID" json:"userID"` //发表评论的 用户ID + UserName string `form:"userName" json:"userName"` //发表评论的 用户昵称 + UserPortrait string `form:"userPortrait" json:"userPortrait"` //头像 URL + ToUserID uint64 `form:"toUserID" json:"toUserID"` //对某用户回复评论 用户ID + ToUserName string `form:"toUserName" json:"toUserName"` //对某用户回复评论 用户昵称 + Content string `form:"content" json:"content"` //评论内容 + Image string `form:"image" json:"image"` + LinkStr string `form:"linkStr" json:"linkStr" bson:"linkStr"` //跳转配置 + LikeCount int `form:"likeCount" json:"likeCount"` //此评论点赞次数 + IsLike bool `form:"isLike" json:"isLike"` //是否点过赞 + IsAuthor bool `form:"isAuthor" json:"isAuthor"` //是否是作者 + IsFollow bool `form:"isFollow" json:"isFollow"` //评论者是否已关注 + IsDelete bool `form:"isDelete" json:"isDelete"` //是否已删除 + Gender string `form:"gender" json:"gender"` //性别 + Status int `form:"status" json:"status"` //此评论的状态 + City string `form:"city" json:"city"` //评论地点 + QuoteType string `json:"quoteType"` // 引用类型 vid视频 col用户播单 sec官方播单 + QuoteID ObjectID `json:"quoteID"` // 引用id + QuoteImg string `json:"quoteImg"` // 引用资源图片 + QuoteTitle string `json:"quoteTitle"` // 引用资源标题 + CreatedAt time.Time `form:"createdAt" json:"createdAt"` //评论时间 + Age int `json:"age"` //年龄 + Level int `json:"level"` //用户评论层级 1:一级评论 2:二级评论 + VipLevel int `json:"vipLevel"` //用户vip等级 + Awards []int `json:"awards"` //用户奖章 + AwardsExpire []usermod.AwardsExpireResp `json:"awardsExpire" bson:"awardsExpire"` //奖章是否有效 + SuperUser bool `json:"superUser" bson:"superUser"` //是否大V + VipExpireDate time.Time `json:"vipExpireDate" bson:"vipExpireDate"` +} + +type PublishReqInfo struct { + ObjID ObjectID `form:"objID" json:"objID"` // 评论对象的ID + ObjType string `form:"objType" json:"objType"` // 评论对象类型 video:视频(默认) cartoon:动漫 AiPlaza:ai广场 + CID string `form:"cid" json:"cid"` // 此评论是对某条评论的评论或回复,如果为空,则为对该视频的评论 + RID string `form:"rid" json:"rid"` // 被回复评论的id + ToUserID uint64 `form:"toUserID" json:"toUserID"` // 对某用户回复评论 用户ID + Content string `form:"content" json:"content"` // 评论内容 + Level int `form:"level" json:"level"` // 评论层级 1:一级评论 2:二级评论 + Image string `json:"image" form:"image"` // 图片 + QuoteType string `form:"quoteType" json:"quoteType"` // 引用类型 vid视频 col用户播单 sec官方播单 cover图集 + QuoteID ObjectID `form:"quoteID" json:"quoteID"` // 引用id + QuoteImg string `form:"quoteImg" json:"quoteImg"` // 引用资源图片 + QuoteTitle string `form:"quoteTitle" json:"quoteTitle"` // 引用资源标题 +} + +// CmtRespList 列表响应信息 +type CmtRespList struct { + //全局唯一评论ID + ID ObjectID `form:"id" json:"id" bson:"_id"` + //评论对象的ID + ObjID string `form:"objID" json:"objID"` + CID string `form:"cid json:"cid"` //二级评论时,为被评论的评论id + //发表评论的 用户ID + UserID uint64 `form:"userID" json:"userID"` + //发表评论的 用户昵称 + UserName string `form:"userName" json:"userName"` + //头像 URL + UserPortrait string `form:"userPortrait" json:"userPortrait" ` + //是否认证 + SuperUser int `form:"superUser" json:"superUser"` + //评论内容 + Content string `form:"content" json:"content"` + //此评论点赞次数 + LikeCount int `form:"likeCount" json:"likeCount"` + //是否点过赞 + IsLike bool `form:"isLike" json:"isLike"` + VipExpireDate time.Time `json:"vipExpireDate" bson:"vipExpireDate"` + //评论时间 + CreatedAt time.Time `form:"createdAt" json:"createdAt"` + //用户vip等级 + Level int `json:"level"` + VipLevel int `json:"vipLevel" bson:"vipLevel"` + Info []CmtRespList +} + +// MyCmtResp 列表响应信息(父评论) +type MyCmtResp struct { + List []MyCmt `json:"list"` + HasNext bool `json:"hasNext"` +} + +type MyCmt struct { + // 全局唯一评论ID + ID primitive.ObjectID `bson:"_id" json:"id"` + // 评论对象的ID 帖子id + ObjID primitive.ObjectID `bson:"objID" json:"objID"` + // 评论内容 + Content string `bson:"content" json:"content"` + // 此评论点赞次数 + LikeCount int `bson:"likeCount" json:"likeCount"` + // 是否是神评论 + IsGodComment bool `bson:"isGodComment" json:"isGodComment"` + // 帖子标题 + VidTitle string `json:"vidTitle"` + // 帖子封面 + VidCover string `json:"vidCover"` + //评论时间 + CreatedAt time.Time `bson:"createdAt" json:"createdAt"` +} diff --git a/models/v/cmtmod/comment.go b/models/v/cmtmod/comment.go new file mode 100644 index 0000000..224c34e --- /dev/null +++ b/models/v/cmtmod/comment.go @@ -0,0 +1,1053 @@ +package cmtmod + +import ( + "91porn-server/common/constant" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/commod" + "encoding/json" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.Comment + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initCommentIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{"objID", 1}, {"cid", 1}, {"sortCnt", -1}, {"gid", -1}, {"createdAt", 1}}, + }, + { + Keys: bson.D{{"objID", 1}, {"level", 1}, {"userID", 1}, {"createdAt", -1}}, + }, + { + Keys: bson.D{{"objID", 1}, {"level", 1}, {"likeCount", -1}, {"createdAt", -1}}, + }, + { + Keys: bson.D{{"objID", 1}, {"level", 1}, {"commentCount", -1}, {"createdAt", -1}}, + }, + { + Keys: bson.D{{"objID", 1}, {"cid", 1}, {"gid", -1}, {"createdAt", 1}}, + }, + { // SKD 统计热度统计查询 + Keys: bson.D{{"objType", 1}, {"status", 1}, {"isAdvertiser", 1}, {"createdAt", -1}}, + }, + { + Keys: bson.D{{"createdAt", 1}, {"toUserID", 1}}, + }, + { + Keys: bson.D{{"publisherID", 1}, {"createdAt", 1}}, + }, + { + Keys: bson.D{{"userID", 1}, {"createdAt", 1}}, + }, + { + Keys: bson.D{{"content", 1}}, + }, + { + Keys: bson.D{{"isGodComment", 1}}, + }, + // 后台筛选使用 + { + Keys: bson.D{{"status", 1}}, + }, + { + Keys: bson.D{{"isAdvertiser", 1}}, + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// InsertManyBulket 插入多条 +func InsertManyBulket(docs []*Comment) (insertCount int64, err error) { + opts := options.BulkWriteOptions{} + opts.SetOrdered(false) + models := make([]mongo.WriteModel, 0) + for _, v := range docs { + models = append(models, mongo.NewInsertOneModel().SetDocument(v)) + } + res, err := coll(nil).Bulk(models, &opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertManyBulket", table, "Bulk", err)) + return + } + insertCount = res.InsertedCount + return +} + +// CmtInsertOne 插入一条评论 +func CmtInsertOne(c *Comment) (id ObjectID, err error) { + res, err := coll(nil).InsertOne(c) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtInsertOne", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(res.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtInsertOne", table, "Marshal", err)) + return + } + err = id.UnmarshalJSON(byteID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtInsertOne", table, "UnmarshalJSON", err)) + return + } + return +} + +// CmtInsertMany 插入多条评论 +func CmtInsertMany(c []Comment) (count int, err error) { + res, err := coll(nil).InsertMany(c) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtInsertMany", table, "InsertMany", err)) + return + } + count = len(res.InsertedIDs) + return +} + +// CmtFindOneByID 获取评论信息 +func CmtFindOneByID(id ObjectID) (data Comment, err error) { + err = coll(nil).FindOne(&data, bson.M{"_id": id}, options.FindOne()) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtFindOneByID", table, "FindOne", err), log.Any("id", id)) + return + } + return +} + +// CmtFindOneByIds 获取评论信息 +func CmtFindOneByIds(ids []ObjectID) (data []Comment, err error) { + err = coll(nil).Find(&data, bson.M{"_id": bson.M{"$in": ids}}, options.Find()) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtFindOneByIds", table, "Find", err), log.Any("ids", ids)) + return + } + return +} + +// CmtFindCommentsAndCount 后台根据条件获取评论 +func CmtFindCommentsAndCount(vid string, uid uint64, content string, isRobot, isAdvertiser *bool, status *int, page commod.Page) (data []Comment, total int64, err error) { + vidObjID := ObjectID{} + query := bson.M{"isDelete": false} + if vid != "" { + if vidObjID, err = primitive.ObjectIDFromHex(vid); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtFindCommentsAndCount", table, "ObjectIDFromHex", err), + log.Any("vid", vid), + log.Any("uid", uid), + log.Any("content", content), + ) + return nil, 0, err + } + query["objID"] = vidObjID + } + if content != "" { + //query["content"] = primitive.Regex{Pattern: content, Options: "i"} + query["content"] = content // 完全匹配内容 + } + if isRobot != nil && *isRobot == true { + query["userID"] = bson.M{"$lte": constant.RobotUIDLimit} + } else if isRobot != nil && *isRobot == false { + query["userID"] = bson.M{"$gt": constant.RobotUIDLimit} + } + if uid != 0 { + query["userID"] = uid + } + if status != nil { + query["status"] = status + } + if isAdvertiser != nil { + query["isAdvertiser"] = isAdvertiser + } + + stdQuery := commod.StdQuery{ + Page: &commod.PageBy{Num: page.PageNumber, Size: page.PageSize}, + Order: &[]commod.OrderBy{{Key: "createdAt", Desc: true}}, + } + err = coll(nil).Find(&data, query, commod.ConvertToListQuery(stdQuery)) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtFindCommentsAndCount", table, "Find", err), + log.Any("vid", vid), + log.Any("uid", uid), + log.Any("content", content), + ) + return + } + total, err = coll(nil).Count(query) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtFindCommentsAndCount", table, "Count", err), + log.Any("vid", vid), + log.Any("uid", uid), + log.Any("content", content), + ) + return + } + return +} + +// GetVideosFirstComment 获取视频第一条评论 +func GetVideosFirstComment(videoIDs []ObjectID) (map[ObjectID]Comment, error) { + m := make(map[ObjectID]Comment) + var infos []Comment + opts := options.Find() + opts.SetSort(bson.D{{Key: "isGodComment", Value: -1}, {Key: "likeCount", Value: -1}, {Key: "createdAt", Value: -1}}) + err := coll(nil).Find(&infos, bson.M{"objID": bson.M{"$in": videoIDs}, "level": 1, "isDelete": false}, opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideosFirstComment", table, "Find", err), log.Any("videoIDs", videoIDs)) + return m, err + } + for _, i := range infos { + m[i.ObjID] = i + } + return m, nil +} + +// CmtFindCountByObjID 根据对象id获取评论数 +func CmtFindCountByObjID(objID ObjectID, curTime time.Time, level *int) (count int64, err error) { + query := bson.M{"objID": objID, "isDelete": false, "createdAt": bson.M{"$lte": curTime}} + if level != nil { + query = bson.M{"objID": objID, "level": *level, "isDelete": false, "createdAt": bson.M{"$lte": curTime}} + } + count, err = coll(nil).Count(query) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtFindCountByObjID", table, "Count", err), + log.Any("objID", objID), + log.Any("curTime", curTime), + ) + return + } + return +} + +// CmtFindFirstLevelComments 获取一级评论列表 +func CmtFindFirstLevelComments(objID ObjectID, curTime time.Time, stdQuery commod.StdQuery) (total int64, err error, data []Comment) { + *stdQuery.Order = append(*stdQuery.Order, commod.OrderBy{Key: "createdAt", Desc: true}) + query := bson.M{"objID": objID, "level": 1, "isDelete": false, "createdAt": bson.M{"$lte": curTime}} + if err = coll(nil).Find(&data, query, commod.ConvertToListQuery(stdQuery)); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtFindFirstLevelComments", table, "Find", err), + log.Any("objID", objID), + log.Any("curTime", curTime), + ) + return + } + if total, err = coll(nil).Count(query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtFindFirstLevelComments", table, "Count", err), + log.Any("objID", objID), + log.Any("curTime", curTime), + ) + return + } + return +} + +// CmtFindSecondLevelComments 获取二级评论列表 +func CmtFindSecondLevelComments(objID ObjectID, cid ObjectID, fstID ObjectID, curTime time.Time, stdQuery commod.StdQuery) (data []Comment, err error) { + *stdQuery.Order = append(*stdQuery.Order, commod.OrderBy{Key: "createdAt", Desc: true}) + var query = bson.M{"_id": bson.M{"$ne": fstID}, "objID": objID, "cid": cid, "isDelete": false, "createdAt": bson.M{"$lte": curTime}} + if err = coll(nil).Find(&data, query, commod.ConvertToListQuery(stdQuery)); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtFindSecondLevelComments", table, "Find", err), + log.Any("objID", objID), + log.Any("cid", cid), + log.Any("fstID", fstID), + log.Any("curTime", curTime), + ) + return + } + return +} + +// CmtIncreaseLikeCountByID 更新评论点赞数 +func CmtIncreaseLikeCountByID(value int, ids ...ObjectID) (err error) { + if len(ids) == 0 { + return nil + } + + var query = bson.M{} + if len(ids) == 1 { + query["_id"] = ids[0] + } else { + query["_id"] = bson.M{"$in": ids} + } + + if _, err = coll(nil).UpdateOne(query, bson.M{"$inc": bson.M{"likeCount": value}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtIncreaseLikeCountByID", table, "UpdateOne", err), + log.Any("ids", ids), + log.Any("value", value), + ) + return + } + return +} + +// CmtUpdateCommentStatusByID 修改评论状态 +func CmtUpdateCommentStatusByID(id ObjectID, status int) (err error) { + if _, err = coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": bson.M{"status": status}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtUpdateCommentStatusByID", table, "UpdateOne", err), + log.Any("id", id), + log.Any("status", status), + ) + return + } + return +} + +// PassCommentStatusByIds 修改评论 +func PassCommentStatusByIds(ids []ObjectID, status int) (err error) { + if _, err = coll(nil).UpdateMany(bson.M{"_id": bson.M{"$in": ids}}, bson.M{"$set": bson.M{"status": status}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "PassCommentStatusByIds", table, "UpdateMany", err), + log.Any("ids", ids), + log.Any("status", status), + ) + return + } + return +} + +// CmtUpdateCommentStatusByUID 修改评论状态 +func CmtUpdateCommentStatusByUID(uid uint64) (err error) { + if _, err = coll(nil).UpdateMany(bson.M{"userID": uid}, bson.M{"$set": bson.M{"isDelete": true}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtUpdateCommentStatusByUID", table, "UpdateMany", err), log.Any("uid", uid)) + return + } + return +} + +// CmtUpdateCommentStatusByIDS 修改评论状态 +func CmtUpdateCommentStatusByIDS(ids []ObjectID) (err error) { + if _, err = coll(nil).UpdateMany(bson.M{"_id": bson.M{"$in": ids}}, bson.M{"$set": bson.M{"isDelete": true}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtUpdateCommentStatusByIDS", table, "UpdateMany", err), log.Any("ids", ids)) + return + } + return +} + +// CmtIncreaseCommentCountByID 增加一级评论的评论数 +func CmtIncreaseCommentCountByID(id ObjectID) (err error) { + query := bson.M{"_id": id} + update := bson.M{"$inc": bson.M{"commentCount": 1}} + if _, err = coll(nil).UpdateOne(query, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtIncreaseCommentCountByID", table, "UpdateOne", err), log.Any("id", id)) + return + } + return +} + +// CmtIncreaseCommentCountByID 减少一级评论的评论数 +func CmtDecreaseCommentCountByID(id ...ObjectID) (err error) { + if len(id) == 0 { + return nil + } + query := bson.M{"_id": id[0], "commentCount": bson.M{"$gte": 1}} + if len(id) > 1 { + query["_id"] = bson.M{"$in": id} + } + update := bson.M{"$inc": bson.M{"commentCount": -1}} + if _, err = coll(nil).UpdateOne(query, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtDecreaseCommentCountByID", table, "UpdateOne", err), log.Any("id", id)) + return + } + return +} + +// CmtFIndFirstCommentOfComment 获取一级评论的第一条评论 +func CmtFIndFirstCommentOfComment(objID ObjectID, CIDs []ObjectID, curTime time.Time) (data []Comment, err error) { + var cmtInfos []FstCmt + pipeline := []bson.M{ + {"$match": bson.M{"objID": objID, "cid": bson.M{"$in": CIDs}, "level": 2, "isDelete": false, "createdAt": bson.M{"$lte": curTime}}}, + {"$sort": bson.D{{Key: "createdAt", Value: -1}}}, + {"$group": bson.M{"_id": "$cid", "data": bson.M{"$first": "$$ROOT"}}}, + } + var opts = options.Aggregate() + opts.SetHint(bson.D{{"objID", 1}, {"cid", 1}, {"sortCnt", -1}, {"gid", -1}, {"createdAt", 1}}) + if err = coll(nil).Aggregate(&cmtInfos, pipeline, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtFIndFirstCommentOfComment", table, "Aggregate", err), + log.Any("objID", objID), + log.Any("CIDs", CIDs), + log.Any("curTime", curTime), + ) + return + } + if cmtInfos == nil { + data = []Comment{} + return + } + for _, value := range cmtInfos { + data = append(data, value.Data) + } + return +} + +func GetCommentMap(cidList []ObjectID) (map[ObjectID]Comment, error) { + if len(cidList) == 0 { + return make(map[ObjectID]Comment), nil + } + filter := bson.M{ + "_id": bson.M{"$in": cidList}, + } + cmtlist := make([]Comment, 0, len(cidList)) + if err := coll(nil).Find(&cmtlist, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCommentMap", table, "Find", err), log.Any("cidList", cidList)) + return nil, err + } + m := make(map[ObjectID]Comment, len(cmtlist)) + for _, cmt := range cmtlist { + m[cmt.ID] = cmt + } + return m, nil +} + +func GetCommentsByUID(uid uint64) (data []Comment, err error) { + var query = bson.M{"userID": uid, "isDelete": false} + if err = coll(nil).Find(&data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCommentsByUID", table, "Find", err), log.Any("uid", uid)) + return + } + return +} +func GetCommentsByContent(content string) (data []Comment, err error) { + var query = bson.M{"content": content, "isDelete": false} + if err = coll(nil).Find(&data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCommentsByContent", table, "Find", err), log.Any("content", content)) + return + } + return +} + +func GetCommentsByIDS(ids []ObjectID) (data []Comment, err error) { + var query = bson.M{"_id": bson.M{"$in": ids}, "isDelete": false} + if err = coll(nil).Find(&data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCommentsByIDS", table, "Find", err), log.Any("ids", ids)) + return + } + return +} + +// 根据评论id获取子评论 +func GetCommentsByCID(objID ObjectID, cid ObjectID) (data []Comment, err error) { + var query = bson.M{"objID": objID, "cid": cid, "isDelete": false} + if err = coll(nil).Find(&data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCommentsByCID", table, "Find", err), + log.Any("objID", objID), + log.Any("cid", cid), + ) + return + } + return +} + +// 设置分组id +func SetGid(id ObjectID) (err error) { + if _, err = coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": bson.M{"gid": id}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "SetGid", table, "UpdateOne", err), log.Any("id", id)) + return + } + return +} + +// 增加回复的排序值 +func IncSortKey(objId ObjectID, gid ObjectID) { + var query = bson.M{"objID": objId, "gid": gid} + var update = bson.M{"$inc": bson.M{"sortCnt": 1}} + if _, err := coll(nil).UpdateMany(query, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncSortKey", table, "UpdateMany", err), + log.Any("objId", objId), + log.Any("gid", gid), + ) + return + } +} + +// GetAuthorParentCmt 获取作者的所有评论 +func GetAuthorParentCmt(isAdvertiser bool, objId ObjectID, author uint64, curTime time.Time) (data []Comment, err error) { + var query = bson.M{"objID": objId, "userID": author, "cid": nil, "level": 1, "status": 1, "isDelete": false, "createdAt": bson.M{"$lte": curTime}} + var opts = options.Find() + if !isAdvertiser { + query["isAdvertiser"] = bson.M{"$in": []any{nil, false}} + } + opts.SetSort(bson.D{{"isGodComment", -1}, {"createdAt", -1}}) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAuthorParentCmt", table, "Find", err), + log.Any("objId", objId), + log.Any("author", author), + log.Any("curTime", curTime), + ) + return + } + return +} + +// GetIsGodCommentParentCmt 获取作者的所有神评论 +func GetIsGodCommentParentCmt(isAdvertiser bool, objId ObjectID, curTime time.Time) (data []Comment, err error) { + var query = bson.M{"objID": objId, "isAuthor": false, "isGodComment": true, "level": 1, "status": 1, "isDelete": false, "createdAt": bson.M{"$lte": curTime}} + if !isAdvertiser { + query["isAdvertiser"] = bson.M{"$in": []any{nil, false}} + } + var opts = options.Find() + opts.SetSort(bson.D{{"isGodComment", -1}, {"createdAt", -1}}) + + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAuthorParentCmt", table, "Find", err), + log.Any("objId", objId), + log.Any("curTime", curTime), + ) + return + } + return +} + +// GetHotParentCmtList 获取热门的父评论及其条数 +func GetHotParentCmtList(uid uint64, isAdvertiser bool, noIds []primitive.ObjectID, objId ObjectID, skip int64, limit int64, curTime time.Time) (data []Comment, err error) { + var query = bson.M{ + "objID": objId, + "level": 1, + "status": 1, + "$or": []bson.M{ + {"status": 1}, + {"userID": uid}, + }, + "isGodComment": false, + "isDelete": false, + "likeCount": bson.M{"$gt": 0}, + "createdAt": bson.M{"$lte": curTime}, + } + if !isAdvertiser { + query["isAdvertiser"] = bson.M{"$in": []any{nil, false}} + } + if len(noIds) > 0 { + query["_id"] = bson.M{"$nin": noIds} + } + var opts = options.Find() + opts.SetSort(bson.D{{"likeCount", -1}, {"createdAt", -1}}).SetSkip(skip).SetLimit(limit) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetHotParentCmtList", table, "Find", err), + log.Any("objId", objId), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("curTime", curTime), + ) + return + } + return +} + +// GetHotReplyParentCmtList 获取热门的父评论及其条数(回复数) +func GetHotReplyParentCmtList(uid uint64, isAdvertiser bool, objId ObjectID, skip int64, limit int64, curTime time.Time) (data []Comment, err error) { + var query = bson.M{ + "objID": objId, + "level": 1, + "status": 1, + "$or": []bson.M{ + {"status": 1}, + {"userID": uid}, + }, + "isGodComment": false, "isDelete": false, "likeCount": bson.M{"$gt": 0}, "createdAt": bson.M{"$lte": curTime}} + if !isAdvertiser { + query["isAdvertiser"] = bson.M{"$in": []any{nil, false}} + } + var opts = options.Find() + opts.SetSort(bson.D{{"commentCount", -1}, {"createdAt", -1}}).SetSkip(skip).SetLimit(limit) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetHotReplyParentCmtList", table, "Find", err), + log.Any("objId", objId), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("curTime", curTime), + ) + return + } + return +} + +// GetParentCmtList 获取普通父评论 +func GetParentCmtList(isAdvertiser bool, objId ObjectID, ids []ObjectID, skip int64, limit int64, curTime time.Time) (data []Comment, err error) { + var query = bson.M{ + "_id": bson.M{"$nin": ids}, + "objID": objId, + "level": 1, + "status": 1, + "isGodComment": false, + "isDelete": false, + "createdAt": bson.M{"$lte": curTime}, + } + if !isAdvertiser { + query["isAdvertiser"] = bson.M{"$in": []any{nil, false}} + } + var opts = options.Find() + opts.SetSort(bson.D{{"createdAt", -1}}).SetSkip(skip).SetLimit(limit) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetParentCmtList", table, "Find", err), + log.Any("objId", objId), + log.Any("ids", ids), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("curTime", curTime), + ) + return + } + return +} + +// GetFstAuthorCmtOfParentCmt 获取父评论的首条作者的评论 +func GetFstAuthorCmtOfParentCmt(isAdvertiser bool, objID ObjectID, CIDs []ObjectID, author uint64, curTime time.Time) (data []Comment, err error) { + var cmtInfos []FstCmt + var query = bson.M{"objID": objID, "cid": bson.M{"$in": CIDs}, "level": 2, "status": 1, "userID": author, "rid": nil, "isDelete": false, "createdAt": bson.M{"$lte": curTime}} + if !isAdvertiser { + query["isAdvertiser"] = bson.M{"$in": []any{nil, false}} + } + pipeline := []bson.M{ + {"$match": query}, + {"$sort": bson.D{{"isGodComment", -1}, {Key: "createdAt", Value: -1}}}, + {"$group": bson.M{"_id": "$cid", "data": bson.M{"$first": "$$ROOT"}}}, + } + if err = coll(nil).Aggregate(&cmtInfos, pipeline); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetFstAuthorCmtOfParentCmt", table, "Aggregate", err), + log.Any("objID", objID), + log.Any("CIDs", CIDs), + log.Any("author", author), + log.Any("curTime", curTime), + ) + return + } + if cmtInfos == nil { + data = []Comment{} + return + } + for _, value := range cmtInfos { + data = append(data, value.Data) + } + return +} + +// 获取父评论的首条作者的评论 +func GetFstCmtOfParentCmt(isAdvertiser bool, objID ObjectID, CIDs []ObjectID, curTime time.Time) (data []Comment, err error) { + var cmtInfos []FstCmt + var query = bson.M{"objID": objID, "cid": bson.M{"$in": CIDs}, "level": 2, "status": 1, "rid": nil, "isDelete": false, "createdAt": bson.M{"$lte": curTime}} + if !isAdvertiser { + query["isAdvertiser"] = bson.M{"$in": []any{nil, false}} + } + pipeline := []bson.M{ + {"$match": query}, + {"$sort": bson.D{{"isGodComment", -1}, {Key: "sortCnt", Value: -1}, {Key: "createdAt", Value: -1}}}, + {"$group": bson.M{"_id": "$cid", "data": bson.M{"$first": "$$ROOT"}}}, + } + if err = coll(nil).Aggregate(&cmtInfos, pipeline); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetFstCmtOfParentCmt", table, "Aggregate", err), + log.Any("objID", objID), + log.Any("CIDs", CIDs), + log.Any("curTime", curTime), + ) + return + } + if cmtInfos == nil { + data = []Comment{} + return + } + for _, value := range cmtInfos { + data = append(data, value.Data) + } + return +} + +// 获取作者的子评论及其回复和数量 +func GetAuthorChildCmtList(objID ObjectID, cid ObjectID, fstId ObjectID, skip int64, limit int64, curTime time.Time) (total int64, data []Comment, err error) { + var query = bson.M{"_id": bson.M{"$ne": fstId}, "objID": objID, "cid": cid, "authorMark": true, "status": 1, "isDelete": false, "createdAt": bson.M{"$lte": curTime}} + var opts = options.Find() + opts.SetSort(bson.D{{"isGodComment", -1}, {"gid", -1}, {"createdAt", 1}}).SetSkip(skip).SetLimit(limit) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAuthorChildCmtList", table, "Find", err), + log.Any("objID", objID), + log.Any("cid", cid), + log.Any("fstId", fstId), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("curTime", curTime), + ) + return + } + if total, err = coll(nil).Count(query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAuthorChildCmtList", table, "Count", err), + log.Any("objID", objID), + log.Any("cid", cid), + log.Any("fstId", fstId), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("curTime", curTime), + ) + return + } + return +} + +// 获取热门的子评论及其回复 +func GetHotChildCmtList(objID ObjectID, cid ObjectID, fstId ObjectID, skip int64, limit int64, curTime time.Time) (total int64, data []Comment, err error) { + var query = bson.M{"_id": bson.M{"$ne": fstId}, "objID": objID, "cid": cid, "authorMark": false, "level": 2, "status": 1, "isDelete": false, "createdAt": bson.M{"$lte": curTime}} + var opts = options.Find() + opts.SetSort(bson.D{{"isGodComment", -1}, {"sortCnt", -1}, {"gid", -1}, {"createdAt", 1}}).SetSkip(skip).SetLimit(limit) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetHotChildCmtList", table, "Find", err), + log.Any("objID", objID), + log.Any("cid", cid), + log.Any("fstId", fstId), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("curTime", curTime), + ) + return + } + return +} + +// 获取普通的评论和回复 +func GetChildCmtList(objID ObjectID, cid ObjectID, fstId ObjectID, skip int64, limit int64, curTime time.Time) (data []Comment, err error) { + var query = bson.M{"_id": bson.M{"$ne": fstId}, "objID": objID, "cid": cid, "idDelete": false, "level": 2, "status": 1, "createdAt": bson.M{"$lte": curTime}} + var opts = options.Find() + opts.SetSort(bson.D{{"isGodComment", -1}, {"gid", -1}, {"createdAt", 1}}).SetSkip(skip).SetLimit(limit) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetChildCmtList", table, "Find", err), + log.Any("objID", objID), + log.Any("cid", cid), + log.Any("fstId", fstId), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("curTime", curTime), + ) + return + } + return +} + +func ListByPublisherID(skip, limit int64, publisherID uint64) ([]Comment, error) { + sort := bson.D{{"createdAt", -1}} + publisherIDMatch := PublisherIDMatch{&publisherID} + return List(sort, skip, limit, publisherIDMatch.New()) +} + +// DelCommentsByUID 删除评论通过uid +func DelCommentsByUID(uid uint64) (err error) { + if _, err = coll(nil).DeleteMany(bson.M{"userID": uid}); err != nil { + log.Error("models comment DelCommentsByUID error", log.E(err), log.Any("uid", uid)) + return + } + return +} + +// DelCommentsByUIDs 批量删除评论通过uid +func DelCommentsByUIDs(uids []uint64) (err error) { + if uids == nil { + uids = []uint64{} + } + if _, err = coll(nil).DeleteMany(bson.M{"userID": bson.M{"$in": uids}}); err != nil { + log.Error("models comment DelCommentsByUIDs error", log.E(err), log.Any("uids", uids)) + return + } + return +} + +// 删除评论通过_id +func DelCommentsByIDS(ids []ObjectID) (err error) { + if _, err = coll(nil).DeleteMany(bson.M{"_id": bson.M{"$in": ids}}); err != nil { + log.Error("models comment DelCommentsByIDS error", log.E(err), log.Any("ids", ids)) + return + } + return +} + +// 删除评论通过评论内容精确匹配 +func DelCommentsByContent(content string) (err error) { + if _, err = coll(nil).DeleteMany(bson.M{"content": content}); err != nil { + log.Error("models comment DelCommentsByContent error", log.E(err), log.Any("content", content)) + return + } + return +} + +// 获取评论 +func GetCmtList(objID, adoptionCmtId ObjectID, skip int64, limit int64) (data []CmtRespList, hasNext bool, err error) { + hasNext = false + var query = bson.M{"objID": objID, "isDelete": false, "level": 1} + if !adoptionCmtId.IsZero() { + query["_id"] = bson.M{"$ne": adoptionCmtId} + } + opts := options.Find().SetSkip(limit * (skip - 1)).SetLimit(limit + 1).SetSort(bson.D{{"createdAt", -1}}) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCmtList", table, "Find", err), + log.Any("objID", objID), + log.Any("skip", skip), + log.Any("limit", limit), + ) + return + } + if int64(len(data)) > limit { + hasNext = true + data = data[:limit] + } + return +} + +// 根据objIds获取子评论 +func GetCmtListByIds(objIDs []ObjectID) (data []CmtRespList, err error) { + var query = bson.M{"cid": bson.M{"$in": objIDs}, "isDelete": false, "level": 2} + opts := options.Find().SetSort(bson.D{{"createdAt", -1}}) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCmtListByIds", table, "Find", err), + log.Any("objID", objIDs), + ) + return + } + return +} + +// CmtFindByID 根据_id获取评论 +func CmtFindByID(id ObjectID) (data CmtRespList, err error) { + err = coll(nil).FindOne(&data, bson.M{"_id": id}, options.FindOne()) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtFindByID", table, "FindOne", err), log.Any("id", id)) + return + } + return +} + +// Update 修改评论状态 +func Update(filter, update bson.M) (err error) { + if _, err = coll(nil).UpdateOne(filter, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return + } + return +} + +// UpdateMany 修改评论状态 +func UpdateMany(filter, update bson.M) (err error) { + if _, err = coll(nil).UpsertMany(filter, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return + } + return +} + +// GetCmtListByCond 根据条件获取评论 +func GetCmtListByCond(filter bson.M) (data []CmtRespList, err error) { + opts := options.Find().SetSort(bson.D{{"createdAt", -1}}) + if err = coll(nil).Find(&data, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCmtListByCond", table, "Find", err), + log.Any("filter", filter), + ) + return + } + return +} + +// FindList 根据条件获取评论 +func FindList(filter bson.M, opt ...*options.FindOptions) (data []*Comment, err error) { + if err = coll(nil).Find(&data, filter, opt...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCmtListByCond", table, "Find", err), + log.Any("filter", filter), + ) + return + } + return +} + +// Count +func GetCount(m bson.M) (int64, error) { + count, err := coll(nil).Count(m) + if err != nil { + return 0, err + } + return count, nil +} + +// GetMyCmtList 获取我的评论 +func GetMyCmtList(userID uint64, skip int64, limit int64) (data []MyCmt, hasNext bool, err error) { + var query = bson.M{"userID": userID, "isDelete": false, "level": 1} + opts := options.Find().SetSkip(skip).SetSort(bson.D{{"createdAt", -1}}) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetMyCmtList", table, "Find", err), + log.Any("userID", userID), + log.Any("skip", skip), + log.Any("limit", limit), + ) + return + } + if int64(len(data)) > limit { + hasNext = true + data = data[:limit] + } + return +} +func GetList(filter bson.M, findOptions *options.FindOptions) (data []Comment, err error) { + if err = coll(nil).Find(&data, filter, findOptions); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Find", err), + log.Any("filter", filter), + log.Any("findOptions", findOptions), + ) + return + } + return +} + +// CmtFindCountByCond 根据过滤条件获取评论数 +func CmtFindCountByCond(cond bson.M) (count int64, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CmtFindCountByCond", table, "Count", err), + log.Any("cond", cond), + ) + return + } + return +} + +type ObjIdsData struct { + Id primitive.ObjectID `bson:"_id"` +} + +type ObjCommentCountData struct { + Id primitive.ObjectID `bson:"_id"` + Count int64 `bson:"count"` +} + +// CmtFindParentCountByObjIDs 批量获取对象的一级评论数量(审核通过、未删除、非广告) +func CmtFindParentCountByObjIDs(cmtType CmtType, objIDs []primitive.ObjectID) (map[primitive.ObjectID]int64, error) { + res := make(map[primitive.ObjectID]int64, len(objIDs)) + if len(objIDs) == 0 { + return res, nil + } + data := make([]ObjCommentCountData, 0, len(objIDs)) + pipeline := []bson.M{ + { + "$match": bson.M{ + "type": cmtType, + "objID": bson.M{"$in": objIDs}, + "level": 1, + "status": 1, + "isDelete": false, + "isAdvertiser": bson.M{"$in": []any{nil, false}}, + }, + }, + { + "$group": bson.M{ + "_id": "$objID", + "count": bson.M{"$sum": 1}, + }, + }, + } + if err := coll(nil).Aggregate(&data, pipeline); err != nil { + log.Error("Comment CmtFindParentCountByObjIDs fail", log.E(err), log.Any("cmtType", cmtType), log.Any("objIDs", objIDs)) + return nil, err + } + for _, item := range data { + res[item.Id] = item.Count + } + return res, nil +} + +// GetObjIdsBetweenTime 按照一段时间内被评论的对象id +func GetObjIdsBetweenTime(objType string, start, end time.Time, skip, limit int) (objIds []primitive.ObjectID, hasNext bool, err error) { + list := []ObjIdsData{} + pipeline := []bson.M{ + bson.M{ + "$match": bson.M{ + "objType": objType, + "status": 1, + "isAdvertiser": false, + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + }, + }, + bson.M{ + "$group": bson.M{ + "_id": "$objID", + "createdAt": bson.M{"$first": "$createdAt"}, // 获取第一个发布的时间 + }, + }, + bson.M{ + "$sort": bson.D{{"createdAt", 1}}, // 按照第一个发布时间从晚到早 + }, + bson.M{ + "$skip": skip, + }, + bson.M{ + "$limit": limit + 1, + }, + } + err = coll(nil).Aggregate(&list, pipeline) + if err != nil { + log.Error("Comment GetObjIdsBetweenTime fail", log.E(err)) + return + } + if len(list) > limit { + hasNext = true + list = list[:limit] + } + for _, v := range list { + objIds = append(objIds, v.Id) + } + return +} + +type CommentCountGroupByDay struct { + Count int64 `json:"count" bson:"count"` + Time string `json:"_id" bson:"_id"` +} + +// GetCountGroupByDay 按天分组,获取一段时间内某个对象的评论数量 +func GetCountGroupByDay(objId primitive.ObjectID, start, end time.Time) (res []CommentCountGroupByDay, err error) { + pipeline := []bson.M{ + bson.M{ + "$match": bson.M{ + "objID": objId, + "status": 1, + "isAdvertiser": false, + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + }, + }, + bson.M{ + "$group": bson.M{ + "_id": bson.M{ + "$dateToString": bson.M{ + "format": "%Y-%m-%d", + "date": "$createdAt", + }, + }, + "count": bson.M{"$sum": 1}, + }, + }, + } + err = coll(nil).Aggregate(&res, pipeline) + if err != nil { + log.Error("Comment GetCountGroupByDay fail", log.E(err)) + return + } + return +} diff --git a/models/v/cmtmod/list.go b/models/v/cmtmod/list.go new file mode 100644 index 0000000..772e451 --- /dev/null +++ b/models/v/cmtmod/list.go @@ -0,0 +1,63 @@ +package cmtmod + +import ( + "91porn-server/common/pageopt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +type Matcher = pageopt.Matcher + +// ToUserIDMatch +type ToUserIDMatch struct { + ToUserID *uint64 +} + +func (t *ToUserIDMatch) New() Matcher { + return pageopt.NewAssignMatch("toUserID", t.ToUserID) +} + +// PublisherIDMatch +type PublisherIDMatch struct { + PublisherID *uint64 +} + +func (t *PublisherIDMatch) New() Matcher { + return pageopt.NewAssignMatch("publisherID", t.PublisherID) +} + +type CreatedAtGTEMatch struct { + GTE *time.Time +} + +func (c *CreatedAtGTEMatch) New() Matcher { + return pageopt.NewGTEMatch("createdAt", c.GTE) +} + +// List +func List(sort bson.D, skip, limit int64, matchers ...Matcher) ([]Comment, error) { + filter := pageopt.MergeM(matchers) + opt := &options.FindOptions{} + if len(sort) != 0 { + opt.SetSort(sort) + } + opt.SetSkip(skip) + opt.SetLimit(limit) + list := make([]Comment, 0, limit) + err := coll(nil).Find(&list, filter, opt) + if err != nil { + return nil, err + } + return list, nil +} + +// Count +func Count(matchers ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchers) + count, err := coll(nil).Count(filter) + if err != nil { + return 0, err + } + return count, nil +} diff --git a/models/v/cmtmod/type.go b/models/v/cmtmod/type.go new file mode 100644 index 0000000..f202a70 --- /dev/null +++ b/models/v/cmtmod/type.go @@ -0,0 +1,82 @@ +package cmtmod + +import ( + "91porn-server/common/db" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type CmtType string + +const ( + CmtTypeVideo CmtType = "VIDEO" + CmtTypeComment CmtType = "CMET" + CmtTypeReply CmtType = "REPLY" + CmtAt CmtType = "AT" + CmtNoVideo CmtType = "NOVIDEO" +) + +type OType = string + +const ( + OTypeVideo OType = "video" //视频 + OTypeCartoon OType = "cartoon" //漫画 + OTypeDrama OType = "drama" //短剧 + OTypeAiPlaza OType = "AiPlaza" //ai广场 +) + +// Comment 评论 +type Comment struct { + ID ObjectID `bson:"_id,omitempty"` // 全局唯一ID + ObjID ObjectID `bson:"objID"` // 评论对象的ID + ObjType OType `bson:"objType"` // 评论对象类型 video:视频 cartoon:动漫 + Title string `bson:"title"` // 如果是视频/动漫 填充对应标题 + CID ObjectID `bson:"cid,omitempty"` // 评论id + RID ObjectID `bson:"rid,omitempty"` // 被回复的评论id + GID ObjectID `bson:"gid,omitempty"` // 二级评论的主评论id,用于分组聚集评论的回复 + Type CmtType `bson:"type"` // 评论类型 video:对视频发表评论 comment:对评论发表评论 reply:对子评论的回复 noVideo:非视频帖子评论 + PublisherID uint64 `bson:"publisherID"` // 字段Type指定资源的发布者 + AuthorMark bool `bson:"authorMark"` // 如果是做的评论,其跟随回复全部置为true,便于获取作者的评论及其回复 + SortCnt int64 `bson:"sortCnt,omitempty"` // 热门排序标记,同组的值一致,用于排序热门评论,存主评论的点赞数,点赞时更新数据 + UserID uint64 `bson:"userID"` // 发表评论的 用户ID + ToUserID uint64 `bson:"toUserID,omitempty"` // 对某用户回复评论 用户ID + ToUserName string `form:"toUserName" json:"toUserName"` // 对某用户回复评论 用户昵称 + LikeCount int `bson:"likeCount"` // 点赞数 + CommentCount int `bson:"commentCount"` // 评论数 + IsGodComment bool `bson:"isGodComment"` // 是否是神评论 + GodCommentAward bool `bson:"godCommentAward,omitempty"` // 是否已经发放过神评奖励 + IsAuthor bool `bson:"isAuthor"` // 是否是作者 + IsRobot bool `bson:"isRobot"` // 是否是机器人 + QuoteType string `json:"quoteType" bson:"quoteType"` // 引用类型 vid视频 col用户播单 sec官方播单 cover图集 + QuoteID primitive.ObjectID `json:"quoteID" bson:"quoteID"` // 引用id + QuoteImg string `json:"quoteImg" bson:"quoteImg"` // 引用资源图片 + QuoteTitle string `json:"quoteTitle" bson:"quoteTitle"` // 引用资源标题 + Content string `bson:"content"` // 评论内容 + OrgContent string `bson:"orgContent"` // 评论原文 + Image string `bson:"image"` // 评论图片 + Level int `bson:"level"` // 评论层级 1:一级评论 2:二级评论 + IPAddr string `bson:"ipAddr"` // 发表评论的ip地址 + Status int `bson:"status"` // 评论状态 1:通过审核 2:未审核 3:被自动过滤 4:审核未通过 + Reason string `bson:"reason"` // 评论未通过原因 + Desc string `bson:"desc" json:"desc"` // 备注 + City string `bson:"city"` // 发表评论的城市 + LinkStr string `json:"linkStr" bson:"linkStr"` // 跳转配置 + IsAdvertiser bool `json:"isAdvertiser" bson:"isAdvertiser"` // 是否是打广告用户 + IsDelete bool `bson:"isDelete"` // true:已删除 false:未删除 + CreatedAt time.Time `bson:"createdAt,omitempty"` // 评论发表时间 + DeletedAt time.Time `bson:"deletedAt"` // 评论发表时间 +} + +// FstCmt 获取一级评论的第一条评论 +type FstCmt struct { + ID ObjectID `bson:"_id"` + Data Comment +} diff --git a/models/v/cmtmod/web.go b/models/v/cmtmod/web.go new file mode 100644 index 0000000..390b003 --- /dev/null +++ b/models/v/cmtmod/web.go @@ -0,0 +1,82 @@ +package cmtmod + +import ( + "91porn-server/models/commod" + "go.mongodb.org/mongo-driver/bson" + "time" +) + +type WebCmtRespList struct { + ID ObjectID `json:"id"` // 全局唯一ID + ObjID ObjectID `json:"objID"` // 评论对象的ID + UserID uint64 `json:"userID"` // 发表评论的 用户ID + LikeCount int `json:"likeCount"` // 点赞数 + Content string `json:"content"` // 评论内容 + Level int `json:"level"` // 评论层级 1:一级评论 2:二级评论 + Status int `json:"status"` // 评论状态 1:通过审核 2:未审核 3:被自动过滤 + IsDelete bool `json:"isDelete"` // true:已删除 false:未删除 + IsBaned bool `json:"isBaned"` // true:已禁言 false:未禁言 + IsGodComment bool `json:"isGodComment"` // 是否是神评论 + IsRobot bool `bson:"isRobot"` // 是否是机器人 + Portrait string `json:"portrait"` // 用户头像 + Name string `json:"name"` // 用户昵称 + LinkStr string `json:"linkStr"` // 跳转配置 + CreatedAt time.Time `json:"createdAt"` // 评论发表时间 + HasLocked bool `json:"hasLocked"` // 用户是否封禁 + IsAdvertiser bool `json:"isAdvertiser"` // 是否是打广告用户 + Image string `json:"image"` // 评论图片 +} + +type WebCmtListReqInfo struct { + VidID string `form:"vidID" json:"vidID" binding:"omitempty"` // 视频id + UID uint64 `form:"uid" json:"uid" binding:"omitempty"` // 用户id + Content string `form:"content" json:"content" binding:"omitempty"` // 搜索内容 + IsRobot *bool `form:"isRobot" json:"isRobot"` // 是否机器人 + IsAdvertiser *bool `form:"isAdvertiser" json:"isAdvertiser"` // 是否是打广告用户 + Status *int `form:"status" bson:"status"` // 评论状态 1:通过审核 2:未审核 3:被自动过滤 + Page commod.Page +} + +type WebCmtDeleteReqInfo struct { + IDS []ObjectID `form:"ids" json:"ids" binding:"omitempty"` + UID uint64 `form:"uid" json:"uid" binding:"omitempty"` + Content string `form:"content" json:"content" binding:"omitempty"` +} + +type WebCmtReqInfo struct { + ID ObjectID `form:"id" json:"id"` +} + +type BatchPassReqInfo struct { + IDS []ObjectID `form:"ids" json:"ids"` // 评论IDS + Status int `form:"status" json:"status" binding:"required"` // 状态 +} + +type EditCond struct { + ID ObjectID `form:"id" json:"id"` // 评论ID + IsGodComment *bool `form:"isGodComment" json:"isGodComment"` // 是否神评论 + LinkStr *string `form:"linkStr" json:"linkStr"` // 评论跳转链接 + GodCommentAward bool `json:"-" form:"-"` // 是否需要给神评奖励 + LikeCount *int `json:"likeCount" form:"likeCount"` // 点赞数 +} + +func (receiver *EditCond) Filter() bson.M { + return bson.M{"_id": receiver.ID} +} + +func (receiver *EditCond) Update() bson.M { + var update = bson.M{} + if receiver.IsGodComment != nil { + update["isGodComment"] = receiver.IsGodComment + } + if receiver.GodCommentAward == true { + update["godCommentAward"] = true + } + if receiver.LinkStr != nil { + update["linkStr"] = receiver.LinkStr + } + if receiver.LikeCount != nil { + update["likeCount"] = receiver.LikeCount + } + return bson.M{"$set": update} +} diff --git a/models/v/collectmod/app.go b/models/v/collectmod/app.go new file mode 100644 index 0000000..255b5e6 --- /dev/null +++ b/models/v/collectmod/app.go @@ -0,0 +1,38 @@ +package collectmod + +import ( + "91porn-server/models/v/locmod" + "91porn-server/models/v/vidmod" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// CInfo 查询返回的收藏结构 +type CInfo struct { + Video []primitive.ObjectID `bson:"video"` + Tags []primitive.ObjectID `bson:"tags"` + Place []primitive.ObjectID `bson:"place"` + VideoTC int64 `bson:"videoTC"` + TagsTC int64 `bson:"tagsTC"` + PlaceTC int64 `bson:"placeTC"` +} + +// ICollectResp 我的收藏回复 +type ICollectResp struct { + VInfos []*vidmod.VideoInfo + TInfos []vidmod.TagInfo + LInfos []locmod.Location + VideoTC int64 `bson:"videoTC"` + TagsTC int64 `bson:"tagsTC"` + PlaceTC int64 `bson:"placeTC"` +} + +type DoCollectReqInfo struct { + ObjID primitive.ObjectID `form:"objID" json:"objID" binding:"required"` // 收藏的对象的ID + Type string `form:"type" json:"type" binding:"required"` // 收藏类型 SP:长视频 SHORT:短视频 COVER:图文帖子 PIC:图集帖子 SEED_LINK:种子/黄油帖子 + IsCollect bool `form:"isCollect" json:"isCollect"` // 收藏or取消收藏 +} + +type DoBatchCancelCollectReq struct { + ObjIDs []primitive.ObjectID `form:"objIDs" json:"objIDs" binding:"required"` // 取消收藏的ID的合集数组 + Type string `form:"type" json:"type" binding:"required"` // 收藏类型 SP:长视频 SHORT:短视频 COVER:图文帖子 PIC:图集帖子 SEED_LINK:种子/黄油帖子 +} diff --git a/models/v/collectmod/collect.go b/models/v/collectmod/collect.go new file mode 100644 index 0000000..0be640b --- /dev/null +++ b/models/v/collectmod/collect.go @@ -0,0 +1,387 @@ +package collectmod + +import ( + "91porn-server/common/constant" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/commod" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +var mdb *db.MongoDB +var maxCollectTagsSize = 30 + +const table = models.Collect + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initCollectIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{"uid", 1}, {"type", 1}}, + }, + { + Keys: bson.D{{"uid", 1}, {"type", 1}, {"objID", 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{"uniq", 1}}, + Options: options.Index().SetUnique(true), + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } + return +} + +// CollInsertOne 插入收藏消息记录 +func CollInsertOne(c *Collect) (err error) { + c.Uniq = Unique(c.UID, c.Type, c.ObjID) + c.CreatedAt = time.Now() + if _, err = coll(nil).InsertOne(c); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CollInsertOne", table, "InsertOne", err)) + return + } + return +} + +// CollInsertMany 插入收藏消息记录 +func CollInsertMany(c *[]Collect) (err error) { + if _, err = coll(nil).InsertMany(c); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CollInsertMany", table, "InsertMany", err)) + return + } + return +} + +// CollDeleteOne 取消收藏 +func CollDeleteOne(uid uint64, cType string, objID primitive.ObjectID) (err error) { + var query = bson.M{"uid": uid, "type": cType, "objID": objID} + if _, err = coll(nil).DeleteOne(query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CollDeleteOne", table, "DeleteOne", err), + log.Any("uid", uid), + log.Any("cType", cType), + log.Any("objID", objID), + ) + return + } + return +} + +// CollDeleteMany 取消收藏 +func CollDeleteMany(ids []string) (err error) { + var query = bson.M{"uniq": bson.M{"$in": ids}} + if _, err = coll(nil).DeleteMany(query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CollDeleteMany", table, "DeleteMany", err), + log.Any("ids", ids), + ) + return + } + return +} + +// CollFindOneByUidAndObjID 获取单条收藏信息 用于验证是否已经收藏 +func CollFindOneByUidAndObjID(uid uint64, objID primitive.ObjectID) (data Collect, err error) { + var query = bson.M{"uid": uid, "objID": objID} + if err = coll(nil).FindOne(&data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CollFindOneByUidAndObjID", table, "FindOne", err), + log.Any("uid", uid), + log.Any("objID", objID), + ) + return + } + return +} + +// CollFindOneByUidAndObjIds 获取多条收藏信息 用于验证是否已经收藏 +func CollFindOneByUidAndObjIds(uid uint64, objIDS []primitive.ObjectID) (data []Collect, err error) { + var query = bson.M{"uid": uid, "objID": bson.M{"$in": objIDS}} + if err = coll(nil).Find(&data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CollFindOneByUidAndObjIds", table, "Find", err), + log.Any("uid", uid), + log.Any("objIDS", objIDS), + ) + return + } + return +} + +// CollFindCollects 获取收藏列表 +func CollFindCollects(uid uint64, cType string, stdQuery commod.StdQuery) (data []Collect, err error) { + *stdQuery.Order = append(*stdQuery.Order, commod.OrderBy{Key: "createdAt", Desc: true}) + var query = bson.M{"uid": uid, "type": cType} + if err = coll(nil).Find(&data, query, commod.ConvertToListQuery(stdQuery)); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CollFindOneByUidAndObjID", table, "FindOne", err), + log.Any("uid", uid), + log.Any("objID", cType), + ) + return + } + return +} + +// CollFindCollectsByUIDAndObjIDs 获取包含指定对象收藏列表 +func CollFindCollectsByUIDAndObjIDs(uid uint64, cType string, objIDs []primitive.ObjectID) (data []Collect, err error) { + var query = bson.M{"uid": uid, "type": cType, "objID": bson.M{"$in": objIDs}} + if err = coll(nil).Find(&data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CollFindOneByUidAndObjID", table, "FindOne", err), + log.Any("uid", uid), + log.Any("objID", cType), + ) + return + } + return +} + +// CollFindCollectsByUIDAndObjId 获取包含指定对象收藏列表 +func CollFindCollectsByUIDAndObjId(uid uint64, objId primitive.ObjectID) (data Collect, err error) { + var query = bson.M{"uid": uid, "objID": objId} + if err = coll(nil).FindOne(&data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CollFindCollectsByUIDAndObjId", table, "FindOne", err), + log.Any("uid", uid), + ) + return + } + return +} +func QuickList(uid uint64, cType string, skip, limit int64) ([]Collect, int64, error) { + filter := bson.M{"uid": uid, "type": cType} + const maxLimit = 450 //iphone 11 一屏显示3项 1页3屏 1页9项 50页450项 + opt := (&options.FindOptions{}).SetLimit(maxLimit) + var totalList []Collect + err := coll(nil).Find(&totalList, filter, opt) //取总共的数据 最多1000条 + if err != nil { + return nil, 0, err + } + total := int64(len(totalList)) + if total == 0 { + return []Collect{}, 0, nil + } + + //内存中分页 skip + limit ∈ [0, total) + if skip+limit > total { + if skip > total { + return []Collect{}, 0, nil + } + limit = total - skip + } + + list := make([]Collect, 0, limit) + for i := skip; i < skip+limit; i++ { + list = append(list, totalList[i]) + } + return list, total, err +} + +// CollFindCountByUID 获取收藏数量 +func CollFindCountByUID(uid uint64) (data int64, err error) { + data, err = coll(nil).Count(bson.M{"uid": uid}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CollFindCountByUID", table, "Count", err), log.Any("uid", uid)) + return + } + return +} + +// CollFindCountByUidAndType 更具uid/type获取标签数量 +func CollFindCountByUidAndType(uid uint64, cType string) (data int64, err error) { + var query = bson.M{"uid": uid, "type": cType, "videoCount": bson.M{"$gt": 0}} + if data, err = coll(nil).Count(query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CollFindCountByUidAndType", table, "Count", err), + log.Any("uid", uid), + log.Any("cType", cType), + ) + return + } + return +} + +// CollFindCollectByTypeAndUID 我的收藏 +func CollFindCollectByTypeAndUID(uid uint64, cType string) (cIDs []primitive.ObjectID, total int64, err error) { + var opt options.FindOptions + opt.SetSort(bson.M{"_id": -1}).SetLimit(4) + query := bson.M{"uid": uid, "type": cType} + var collects []*Collect + if err = coll(nil).Find(&collects, query, &opt); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CollFindCollectByTypeAndUID", table, "Find", err), + log.Any("uid", uid), + log.Any("cType", cType), + ) + return + } + for _, v := range collects { + cIDs = append(cIDs, v.ObjID) + } + total, err = coll(nil).Count(query) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CollFindCollectByTypeAndUID", table, "Count", err), + log.Any("uid", uid), + log.Any("cType", cType), + ) + return + } + return +} + +// CollIsExisted 是否收藏 +func CollIsExisted(uid uint64, cType string, objID primitive.ObjectID) (isExists bool, err error) { + cnt, err := coll(nil).Count(bson.M{"uid": uid, "type": cType, "objID": objID}) + isExists = false + if err != nil || cnt == 0 { + return + } + isExists = true + return +} + +// 从给定的uniq列表中获取收藏状态映射:uniq->Statue +// uniq通过uniq()获取 +func CltStatueMap(uniqList []string) (map[string]bool, error) { + if len(uniqList) == 0 { + return make(map[string]bool), nil + } + + filter := bson.M{ + "uniq": bson.M{"$in": uniqList}, + } + existCltList := make([]Collect, 0, len(uniqList)) + err := coll(nil).Find(&existCltList, filter) + if err != nil { + return nil, err + } + m := make(map[string]bool, len(existCltList)) + //初始化 + for _, uniq := range uniqList { + m[uniq] = false + } + //已经收藏的 + for _, v := range existCltList { + m[v.Uniq] = true + } + return m, nil +} + +// IsCollectVideos 是否收藏视频 +func IsCollectVideos(uid uint64, videoIDs []primitive.ObjectID) (map[primitive.ObjectID]bool, error) { + m := make(map[primitive.ObjectID]bool) + videoIDs = uniqueVideoIDs(videoIDs) + if len(videoIDs) == 0 { + return m, nil + } + + var infos []Collect + query := collectVideoStatusFilter(uid, videoIDs) + opts := options.Find().SetProjection(bson.M{"_id": 0, "objID": 1}) + if err := coll(nil).Find(&infos, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsCollectVideos", table, "Find", err), + log.Any("uid", uid), + log.Any("videoIDs", videoIDs), + ) + return m, err + } + for _, i := range infos { + m[i.ObjID] = true + } + return m, nil +} + +func collectVideoStatusFilter(uid uint64, videoIDs []primitive.ObjectID) bson.M { + return bson.M{ + "uid": uid, + "type": bson.M{"$in": []string{ + constant.CollectTypeSP, + constant.CollectTypeShort, + constant.CollectTypeCover, + constant.CollectTypePIC, + constant.CollectTypeSEED_LINK, + constant.CollectTypeAiPlaza, + }}, + "objID": bson.M{"$in": videoIDs}, + } +} + +func uniqueVideoIDs(ids []primitive.ObjectID) []primitive.ObjectID { + if len(ids) < 2 { + return ids + } + seen := make(map[primitive.ObjectID]struct{}, len(ids)) + unique := make([]primitive.ObjectID, 0, len(ids)) + for _, id := range ids { + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + unique = append(unique, id) + } + return unique +} + +// GetLocsCollectCnt2Map 获取位置的收藏数 +func GetLocsCollectCnt2Map(locIDs []primitive.ObjectID) (map[primitive.ObjectID]int, error) { + if locIDs == nil { + locIDs = []primitive.ObjectID{} + } + m := make(map[primitive.ObjectID]int) + var data []CityCount + p := []bson.M{ + {"$match": bson.M{"type": constant.CollectTypeLocation, "objID": bson.M{"$in": locIDs}}}, + {"$group": bson.M{"_id": "$objID", "count": bson.M{"$sum": 1}}}, + } + if err := coll(nil).Aggregate(&data, p); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetLocsCollectCnt2Map", table, "Aggregate", err), log.Any("locIDs", locIDs)) + return nil, err + } + for _, d := range data { + m[d.ID] = d.Count + } + return m, nil +} + +// MyCollectTags 获取收藏标签 +func MyCollectTags(uid uint64, top int) ([]primitive.ObjectID, error) { + var ids []primitive.ObjectID + stdQuery := commod.StdQuery{Page: &commod.PageBy{Num: 1, Size: uint64(top)}, Order: &[]commod.OrderBy{}} + data, err := CollFindCollects(uid, constant.CollectTypeTag, stdQuery) + if err != nil { + return ids, nil + } + for _, d := range data { + ids = append(ids, d.ObjID) + } + return ids, nil +} + +// 根据标签id获取收藏列表 +func GetCollectTagList(uid uint64, tagsID []primitive.ObjectID) (data []Collect, err error) { + if tagsID == nil || uid == 0 { + return + } + var query = bson.M{ + "uid": uid, + "type": constant.CollectTypeTag, + "objID": bson.M{"$in": tagsID}, + } + err = coll(nil).Find(&data, query) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCollectTagList", table, "Find", err), + log.Any("uid", uid), + log.Any("tagsID", tagsID), + ) + return + } + return +} diff --git a/models/v/collectmod/collect_video_status_test.go b/models/v/collectmod/collect_video_status_test.go new file mode 100644 index 0000000..0893933 --- /dev/null +++ b/models/v/collectmod/collect_video_status_test.go @@ -0,0 +1,68 @@ +package collectmod + +import ( + "reflect" + "testing" + + "91porn-server/common/constant" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestCollectVideoStatusFilterUsesCompoundIndexFields(t *testing.T) { + id1 := primitive.NewObjectID() + id2 := primitive.NewObjectID() + ids := uniqueVideoIDs([]primitive.ObjectID{id1, id2, id1}) + + filter := collectVideoStatusFilter(123, ids) + if got := filter["uid"]; got != uint64(123) { + t.Fatalf("uid = %#v, want 123", got) + } + + typeMatch, ok := filter["type"].(bson.M) + if !ok { + t.Fatalf("type filter = %T, want bson.M", filter["type"]) + } + gotTypes, ok := typeMatch["$in"].([]string) + if !ok { + t.Fatalf("type.$in = %T, want []string", typeMatch["$in"]) + } + wantTypes := []string{ + constant.CollectTypeSP, + constant.CollectTypeShort, + constant.CollectTypeCover, + constant.CollectTypePIC, + constant.CollectTypeSEED_LINK, + constant.CollectTypeAiPlaza, + } + if !reflect.DeepEqual(gotTypes, wantTypes) { + t.Fatalf("type.$in = %#v, want %#v", gotTypes, wantTypes) + } + + objMatch, ok := filter["objID"].(bson.M) + if !ok { + t.Fatalf("objID filter = %T, want bson.M", filter["objID"]) + } + gotIDs, ok := objMatch["$in"].([]primitive.ObjectID) + if !ok { + t.Fatalf("objID.$in = %T, want []primitive.ObjectID", objMatch["$in"]) + } + if wantIDs := []primitive.ObjectID{id1, id2}; !reflect.DeepEqual(gotIDs, wantIDs) { + t.Fatalf("objID.$in = %#v, want %#v", gotIDs, wantIDs) + } +} + +func TestIsCollectVideosEmptyIDsReturnsBeforeDatabaseAccess(t *testing.T) { + originalDB := mdb + mdb = nil + t.Cleanup(func() { mdb = originalDB }) + + got, err := IsCollectVideos(123, nil) + if err != nil { + t.Fatalf("IsCollectVideos() error = %v", err) + } + if len(got) != 0 { + t.Fatalf("IsCollectVideos() = %#v, want empty map", got) + } +} diff --git a/models/v/collectmod/type.go b/models/v/collectmod/type.go new file mode 100644 index 0000000..b23bad0 --- /dev/null +++ b/models/v/collectmod/type.go @@ -0,0 +1,40 @@ +package collectmod + +import ( + "91porn-server/common/db" + "go.mongodb.org/mongo-driver/bson/primitive" + "strconv" + "strings" + "time" +) + +func Init() { + mdb = db.Init(table) + initIndex() +} + +// Collect 用户收藏信息 +type Collect struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + ObjID primitive.ObjectID `bson:"objID"` //收藏的对象id + UID uint64 `bson:"uid"` //用户id + Type string `bson:"type"` //收藏类型 video:视频 tag:专题 location:地点 avcomment:AV解说 + Uniq string `bson:"uniq"` //uid.objID.type + CreatedAt time.Time `bson:"createdAt"` +} + +// CityCount 城市收藏量 +type CityCount struct { + ID primitive.ObjectID `bson:"_id"` + Count int `bson:"count"` +} + +func Unique(uid uint64, typ string, objID primitive.ObjectID) string { + list := []string{ + strconv.FormatInt(int64(uid), 10), + objID.Hex(), + typ, + } + s := strings.Join(list, ".") + return s +} diff --git a/models/v/contentlibmod/contentlib.go b/models/v/contentlibmod/contentlib.go new file mode 100644 index 0000000..7514f25 --- /dev/null +++ b/models/v/contentlibmod/contentlib.go @@ -0,0 +1,80 @@ +package contentlibmod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.ContentLibrary + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "type", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func Insert(doc ContentLibrary) (err error) { + if _, err = coll(nil).InsertOne(doc); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + return +} + +func DeleteByIDS(ids []ObjectID) (err error) { + if _, err = coll(nil).DeleteMany(bson.M{"_id": bson.M{"$in": ids}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByIDS", table, "DeleteMany", err), log.Any("ids", ids)) + return + } + return +} + +func GetContentList(robotType *string, skip int64, limit int64) (total int64, data []ContentLibrary, err error) { + var query = bson.M{} + if robotType != nil { + query["robotType"] = *robotType + } + var opts = options.Find() + opts.SetSort(bson.D{{Key: "createdAt", Value: -1}}).SetSkip(skip).SetLimit(limit) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetContentList", table, "Find", err), log.Any("robotType", robotType)) + return + } + if total, err = coll(nil).Count(query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetContentList", table, "Count", err), log.Any("robotType", robotType)) + return + } + return +} + +func GetContentListByIDS(ids []ObjectID) (data []ContentLibrary, err error) { + if err = coll(nil).Find(&data, bson.M{"_id": bson.M{"$in": ids}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetContentListByIDS", table, "Find", err), log.Any("ids", ids)) + return + } + return +} diff --git a/models/v/contentlibmod/type.go b/models/v/contentlibmod/type.go new file mode 100644 index 0000000..3b250f9 --- /dev/null +++ b/models/v/contentlibmod/type.go @@ -0,0 +1,24 @@ +package contentlibmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +func Init() { + mdb = db.Init(table) + initIndex() +} + +// 机器人聊天的内容库 +type ContentLibrary struct { + ID ObjectID `bson:"_id,omitempty"` + RobotType string `bson:"robotType"` // 机器人类型 + Content string `bson:"content"` // 聊天内容 + CreatedAt time.Time `bson:"createdAt"` // 创建时间 +} diff --git a/models/v/contentlibmod/web.go b/models/v/contentlibmod/web.go new file mode 100644 index 0000000..83ef23a --- /dev/null +++ b/models/v/contentlibmod/web.go @@ -0,0 +1,31 @@ +package contentlibmod + +import ( + "time" + + "91porn-server/models/commod" +) + +type RespList struct { + ID ObjectID `json:"id"` + RobotType string `json:"robotType"` // 机器人类型 + Content string `json:"content"` // 聊天内容 + CreatedAt time.Time `json:"createdAt"` // 创建时间 +} + +// 内容库列表请求信息 +type ReqContentList struct { + RobotType *string `form:"robotType" json:"robotType"` + commod.Page +} + +// 内容库增加内容请求参数 +type ReqContentAdd struct { + RobotType string `json:"robotType" binding:"required"` + Content string `json:"content" binding:"required"` +} + +// 内容库删除请求参数 +type ReqContentDel struct { + IDS []ObjectID `json:"ids" binding:"required"` +} diff --git a/models/v/contentreviewmod/crud.go b/models/v/contentreviewmod/crud.go new file mode 100644 index 0000000..2d5ed11 --- /dev/null +++ b/models/v/contentreviewmod/crud.go @@ -0,0 +1,377 @@ +package contentreviewmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var ( + mdbTask *db.MongoDB + mdbIssue *db.MongoDB +) + +const ( + taskTable = models.ContentReviewTask + issueTable = models.ContentReviewIssue +) + +func collTask(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdbTask.Coll(taskTable) + } + return t.Coll(taskTable) +} + +func collIssue(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdbIssue.Coll(issueTable) + } + return t.Coll(issueTable) +} + +func Init() { + mdbTask = db.Init(taskTable) + mdbIssue = db.Init(issueTable) + initIndex() +} + +func initIndex() { + taskIdx := []mongo.IndexModel{ + {Keys: bson.D{{Key: "status", Value: 1}}}, + {Keys: bson.D{{Key: "createdAt", Value: -1}}}, + } + if _, err := collTask(nil).CreateIndex(taskIdx); err != nil { + panic(fmt.Sprintf("%s set index err: %+v", taskTable, err)) + } + issueIdx := []mongo.IndexModel{ + {Keys: bson.D{{Key: "taskId", Value: 1}, {Key: "createdAt", Value: 1}}}, + {Keys: bson.D{{Key: "targetId", Value: 1}}}, + } + if _, err := collIssue(nil).CreateIndex(issueIdx); err != nil { + panic(fmt.Sprintf("%s set index err: %+v", issueTable, err)) + } +} + +// CreateTask 新建任务 +func CreateTask(t *ReviewTask) error { + now := time.Now() + t.CreatedAt = now + t.Status = TaskStatusPending + if _, err := collTask(nil).InsertOne(t); err != nil { + log.Error(fmt.Sprintf("[contentreviewmod] CreateTask fail: %+v", err)) + return err + } + return nil +} + +// GetTaskByID 查询任务 +func GetTaskByID(id primitive.ObjectID) (*ReviewTask, error) { + var t ReviewTask + if err := collTask(nil).FindOneByID(&t, id); err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + return nil, err + } + return &t, nil +} + +// HeartbeatTimeout 心跳超时阈值:超过该时间未更新心跳的 running 任务视为僵尸任务(进程崩溃后留下的) +const HeartbeatTimeout = 3 * time.Minute + +// PickNextTask 取最早的一个待处理任务:pending OR 心跳超时的 running +// skd 单进程下足够;调用方需保证进程内串行执行 +func PickNextTask() (*ReviewTask, error) { + threshold := time.Now().Add(-HeartbeatTimeout) + cond := bson.M{ + "$or": []bson.M{ + {"status": TaskStatusPending}, + {"status": TaskStatusRunning, "heartbeatAt": bson.M{"$lt": threshold}}, + }, + } + var t ReviewTask + opts := options.FindOne().SetSort(bson.D{{Key: "createdAt", Value: 1}}) + if err := collTask(nil).FindOne(&t, cond, opts); err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + return nil, err + } + return &t, nil +} + +// AcquireTask 标记任务进入运行态并刷心跳 +// 首次执行(pending)时设置 startedAt;续跑(running 心跳超时)保留原 startedAt +func AcquireTask(id primitive.ObjectID, isResume bool) error { + now := time.Now() + set := bson.M{ + "status": TaskStatusRunning, + "heartbeatAt": now, + } + if !isResume { + set["startedAt"] = now + } + _, err := collTask(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}) + return err +} + +// UpdateProgress 更新进度(批处理过程中调用),同时刷心跳和游标 +func UpdateProgress(id, lastTargetID primitive.ObjectID, checked, issues int64, total int64) error { + set := bson.M{ + "checkedCount": checked, + "issueCount": issues, + "heartbeatAt": time.Now(), + } + if !lastTargetID.IsZero() { + set["lastTargetId"] = lastTargetID + } + if total >= 0 { + set["totalCount"] = total + } + _, err := collTask(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}) + return err +} + +// MarkFinished 标记完成 +func MarkFinished(id primitive.ObjectID, checked, issues int64) error { + _, err := collTask(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": bson.M{ + "status": TaskStatusFinished, + "checkedCount": checked, + "issueCount": issues, + "finishedAt": time.Now(), + }}) + return err +} + +// MarkFailed 标记失败 +func MarkFailed(id primitive.ObjectID, errMsg string) error { + _, err := collTask(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": bson.M{ + "status": TaskStatusFailed, + "errMsg": errMsg, + "finishedAt": time.Now(), + }}) + return err +} + +// MarkTgSent 标记 TG 发送成功 +func MarkTgSent(id primitive.ObjectID) error { + _, err := collTask(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": bson.M{"tgSent": true}}) + return err +} + +// ListTasks 任务分页 +func ListTasks(page, size int64, taskType string, status *int) ([]*ReviewTask, int64, error) { + cond := bson.M{} + if taskType != "" { + cond["taskType"] = taskType + } + if status != nil { + cond["status"] = *status + } + total, err := collTask(nil).Count(cond) + if err != nil { + return nil, 0, err + } + if page < 1 { + page = 1 + } + if size < 1 { + size = 20 + } + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "createdAt", Value: -1}}). + SetSkip((page - 1) * size). + SetLimit(size) + var list []*ReviewTask + if err = collTask(nil).Find(&list, cond, &opts); err != nil { + return nil, total, err + } + return list, total, nil +} + +// InsertIssues 批量写入命中记录 +// 注意:底层 db.InsertMany 通过反射访问元素字段,要求 slice 元素必须是 struct 值 +// 而非指针,否则 FieldByName 会 panic;这里把 []*ReviewIssue 拷贝为 []ReviewIssue +func InsertIssues(list []*ReviewIssue) error { + if len(list) == 0 { + return nil + } + now := time.Now() + flat := make([]ReviewIssue, len(list)) + for i, p := range list { + p.CreatedAt = now + flat[i] = *p + } + if _, err := collIssue(nil).InsertMany(&flat); err != nil { + log.Error(fmt.Sprintf("[contentreviewmod] InsertIssues fail: %+v", err)) + return err + } + return nil +} + +// incTaskResolved 原子累加任务的已解决命中计数 +func incTaskResolved(taskID primitive.ObjectID, delta int64) error { + _, err := collTask(nil).UpdateOne(bson.M{"_id": taskID}, bson.M{"$inc": bson.M{"resolvedCount": delta}}) + return err +} + +// CountUnresolvedIssues 实时统计某任务下未解决(resolveStatus != 已解决)的记录数。 +// 列表页已改用 IssueCount-ResolvedCount 冗余差值,本函数保留用于计数重算/对账修复。 +func CountUnresolvedIssues(taskID primitive.ObjectID) (int64, error) { + cond := bson.M{ + "taskId": taskID, + "resolveStatus": bson.M{"$ne": ResolveStatusResolved}, + } + return collIssue(nil).Count(cond) +} + +// GetIssueByID 按 ID 查命中记录 +func GetIssueByID(id primitive.ObjectID) (*ReviewIssue, error) { + var i ReviewIssue + if err := collIssue(nil).FindOneByID(&i, id); err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + return nil, err + } + return &i, nil +} + +// GetIssuesByIDs 批量按 ID 拉取命中记录;缺失/重复 ID 不会出现在返回中 +func GetIssuesByIDs(ids []primitive.ObjectID) ([]*ReviewIssue, error) { + if len(ids) == 0 { + return nil, nil + } + var list []*ReviewIssue + if err := collIssue(nil).Find(&list, bson.M{"_id": bson.M{"$in": ids}}); err != nil { + return nil, err + } + return list, nil +} + +// BatchOffShelfResolveIssues 批量把命中切到已解决(resolveAction=offshelf), +// 调用方应先成功执行批量下架动作。 +// 仅 resolveStatus ∈ {Pending, Rejected} 的会被改动;perTaskResolved 是按 TaskID +// 聚合的本批已解决条数,本函数原子地为每个任务 $inc resolvedCount。 +func BatchOffShelfResolveIssues(ids []primitive.ObjectID, perTaskResolved map[primitive.ObjectID]int64, operator string) error { + if len(ids) == 0 { + return nil + } + now := time.Now() + filter := bson.M{ + "_id": bson.M{"$in": ids}, + "resolveStatus": bson.M{"$in": []int{ + ResolveStatusPending, + ResolveStatusRejected, + }}, + } + update := bson.M{"$set": bson.M{ + "resolveStatus": ResolveStatusResolved, + "resolveAction": ResolveActionOffShelf, + "submittedAt": now, + "submittedBy": operator, + "resolvedAt": now, + }} + if _, err := collIssue(nil).UpdateMany(filter, update); err != nil { + return err + } + for taskID, n := range perTaskResolved { + if n == 0 { + continue + } + if err := incTaskResolved(taskID, n); err != nil { + log.Warn(fmt.Sprintf("[contentreviewmod] inc resolvedCount fail: taskId=%s delta=%d err=%+v", taskID.Hex(), n, err)) + } + } + return nil +} + +// ResolveIssue 管理员提交修改并直接标记已解决 +// 允许从未解决(0)或已搁置(1)切到已解决(2);调用方应先成功写回数据源 +// 状态确实发生转换(ModifiedCount>0)时,原子累加所属任务的已解决计数 +func ResolveIssue(id, taskID primitive.ObjectID, submittedTitle, submittedContent, submittedRichText, operator string) (bool, error) { + now := time.Now() + filter := bson.M{ + "_id": id, + "resolveStatus": bson.M{"$in": []int{ + ResolveStatusPending, + ResolveStatusRejected, + }}, + } + update := bson.M{"$set": bson.M{ + "resolveStatus": ResolveStatusResolved, + "resolveAction": ResolveActionText, + "submittedTitle": submittedTitle, + "submittedContent": submittedContent, + "submittedRichText": submittedRichText, + "submittedAt": now, + "submittedBy": operator, + "resolvedAt": now, + }} + res, err := collIssue(nil).UpdateOne(filter, update) + if err != nil { + return false, err + } + if res.ModifiedCount > 0 { + if err := incTaskResolved(taskID, 1); err != nil { + log.Warn(fmt.Sprintf("[contentreviewmod] inc resolvedCount fail: taskId=%s err=%+v", taskID.Hex(), err)) + } + } + return res.ModifiedCount > 0, nil +} + +// RejectIssue 搁置:记录原因并把状态切到 已搁置(1) +// 只在当前状态为 未解决(0)时允许;reason 必填便于追溯 +func RejectIssue(id primitive.ObjectID, operator, reason string) (bool, error) { + filter := bson.M{ + "_id": id, + "resolveStatus": ResolveStatusPending, + } + update := bson.M{"$set": bson.M{ + "resolveStatus": ResolveStatusRejected, + "reviewedAt": time.Now(), + "reviewedBy": operator, + "rejectReason": reason, + }} + res, err := collIssue(nil).UpdateOne(filter, update) + if err != nil { + return false, err + } + return res.ModifiedCount > 0, nil +} + +// ListIssues 命中记录分页;resolveStatus 传 nil 表示全部 +func ListIssues(taskID primitive.ObjectID, page, size int64, resolveStatus *int) ([]*ReviewIssue, int64, error) { + cond := bson.M{"taskId": taskID} + if resolveStatus != nil { + cond["resolveStatus"] = *resolveStatus + } + total, err := collIssue(nil).Count(cond) + if err != nil { + return nil, 0, err + } + if page < 1 { + page = 1 + } + if size < 1 { + size = 20 + } + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "createdAt", Value: 1}}). + SetSkip((page - 1) * size). + SetLimit(size) + var list []*ReviewIssue + if err = collIssue(nil).Find(&list, cond, &opts); err != nil { + return nil, total, err + } + return list, total, nil +} diff --git a/models/v/contentreviewmod/type.go b/models/v/contentreviewmod/type.go new file mode 100644 index 0000000..f376ff8 --- /dev/null +++ b/models/v/contentreviewmod/type.go @@ -0,0 +1,121 @@ +package contentreviewmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// TaskType 任务类型常量 +const ( + TaskTypeVideo = "VIDEO" // 视频帖子(扫描 VideoModel 表) + TaskTypeACG = "ACG" // ACG 漫画/小说(扫描 Media + MediaContent 表) +) + +// TaskStatus 任务状态机 +// +// 0 待执行 → 1 执行中 → 2 已完成 +// ↓ +// 3 失败(panic 或异常) +// +// 心跳超时的 1 执行中 任务,下次 cron tick 会被自动接管续跑 +const ( + TaskStatusPending = 0 // 待执行:刚创建,等待 cron 调度 + TaskStatusRunning = 1 // 执行中:cron 已挑出并正在扫描 + TaskStatusFinished = 2 // 已完成:扫描完所有目标 + TaskStatusFailed = 3 // 失败:执行中遇到不可恢复错误 +) + +// IssueTargetType 命中记录关联的目标对象类型(字符串枚举) +type IssueTargetType string + +const ( + IssueTargetVideo IssueTargetType = "VIDEO" // 命中视频帖子(VideoModel) + IssueTargetACGMedia IssueTargetType = "ACG_MEDIA" // 命中 ACG 的 Media 自身字段 + IssueTargetACGContent IssueTargetType = "ACG_CONTENT" // 命中 ACG 的 MediaContent 章节 +) + +// ResolveStatus 命中记录的解决状态机 +// +// 0 未解决 ─── Resolve(提交修改+写回数据源) ─────────→ 2 已解决 +// │ ↑ +// └── Reject(搁置,记录原因) ──→ 1 已搁置 ── Resolve ────┘ +// +// 未解决 / 已搁置 都视为"待解决"(unresolvedCount 累计) +const ( + ResolveStatusPending = 0 // 未解决:初始状态,等待管理员处理 + ResolveStatusRejected = 1 // 已搁置:管理员审阅后决定暂不修改数据源,已记录搁置原因;仍可后续 Resolve + ResolveStatusResolved = 2 // 已解决:管理员提交修改并写回数据源,闭环 +) + +// SensitiveWordSnap 任务发起时刻对敏感词库的快照 +// 防止敏感词库后续被修改影响历史任务的可追溯性 +type SensitiveWordSnap struct { + Word string `json:"word" bson:"word"` // 词条 + Category string `json:"category" bson:"category"` // 一级分类 +} + +// ReviewTask 内容审查任务记录 +// 由管理员发起,skd 服务的定时调度器异步扫描;同一时刻只跑一个任务 +type ReviewTask struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 任务 ID + TaskType string `json:"taskType" bson:"taskType"` // 任务类型:VIDEO / ACG + SnapshotWords []SensitiveWordSnap `json:"snapshotWords" bson:"snapshotWords"` // 发起时刻的敏感词快照 + Status int `json:"status" bson:"status"` // 任务状态 见 TaskStatusXxx + TotalCount int64 `json:"totalCount" bson:"totalCount"` // 待扫描总数(启动时初始化) + CheckedCount int64 `json:"checkedCount" bson:"checkedCount"` // 已扫描数量 + IssueCount int64 `json:"issueCount" bson:"issueCount"` // 累计命中记录数 + // ResolvedCount 已解决命中数的冗余计数:每次 Resolve 成功 $inc +1。 + // 待解决数 = IssueCount - ResolvedCount,列表页读时算差值,避免对每个任务实时 count。 + ResolvedCount int64 `json:"resolvedCount" bson:"resolvedCount"` + LastTargetID primitive.ObjectID `json:"lastTargetId" bson:"lastTargetId"` // 已扫描的最后一条目标 _id,断点续跑游标 + HeartbeatAt time.Time `json:"heartbeatAt" bson:"heartbeatAt"` // 心跳:每批结束刷新,超时视为僵尸任务可被接管 + TgSent bool `json:"tgSent" bson:"tgSent"` // 完成通知 TG 是否发送成功 + Operator string `json:"operator" bson:"operator"` // 发起人账号 + ErrMsg string `json:"errMsg" bson:"errMsg"` // 失败原因(status=3 时) + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 任务创建时间 + StartedAt time.Time `json:"startedAt" bson:"startedAt"` // 首次启动时间(续跑不重置) + FinishedAt time.Time `json:"finishedAt" bson:"finishedAt"` // 完成时间 +} + +// ReviewIssue 命中问题记录(一条 issue 代表一个目标对象的全部字段命中信息) +// +// VIDEO: Title/Content/RichText 字段对应 VideoModel.Title/Content/RichText +// ACG_MEDIA: Title=Media.Title, Content=Media.Summary, RichText 不使用 +// ACG_CONTENT: Title=MediaContent.Name, Content=MediaContent.Text, MediaID 关联回 Media._id +// +// Title/Content/Tags/RichText 字段存放经过 ... 高亮标记后的文本 +type ReviewIssue struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 记录 ID + TaskID primitive.ObjectID `json:"taskId" bson:"taskId"` // 所属任务 ID + TargetType IssueTargetType `json:"targetType" bson:"targetType"` // 目标对象类型 见 IssueTargetXxx + TargetID primitive.ObjectID `json:"targetId" bson:"targetId"` // 目标对象 _id + MediaID primitive.ObjectID `json:"mediaId,omitempty" bson:"mediaId,omitempty"` // ACG_CONTENT 时关联的 Media._id + + Title string `json:"title" bson:"title"` // 高亮后的标题(VIDEO/ACG_MEDIA.Title 或 ACG_CONTENT.Name) + Content string `json:"content" bson:"content"` // 高亮后的内容(VIDEO.Content / ACG_MEDIA.Summary / ACG_CONTENT.Text) + Tags string `json:"tags" bson:"tags"` // 高亮后的标签("标签1、标签2") + RichText string `json:"richText" bson:"richText"` // 高亮后的富文本(仅 VIDEO 有) + Description string `json:"description" bson:"description"` // 问题描述,形如"标题命中 词1、词2\n标签命中 词3" + HitWords []string `json:"hitWords" bson:"hitWords"` // 去重后的全部命中词 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 记录创建时间 + + // —— 解决/审核流程 —— + ResolveStatus int `json:"resolveStatus" bson:"resolveStatus"` // 解决状态 见 ResolveStatusXxx + SubmittedTitle string `json:"submittedTitle" bson:"submittedTitle"` // 管理员提交修改后的标题(待审核内容) + SubmittedContent string `json:"submittedContent" bson:"submittedContent"` // 管理员提交修改后的内容 + SubmittedRichText string `json:"submittedRichText" bson:"submittedRichText"` // 管理员提交修改后的富文本(仅 VIDEO) + SubmittedAt time.Time `json:"submittedAt" bson:"submittedAt"` // 最近一次提交时间 + SubmittedBy string `json:"submittedBy" bson:"submittedBy"` // 最近一次提交人账号 + RejectReason string `json:"rejectReason" bson:"rejectReason"` // 审核打回原因(resolveStatus 退回 0 时保留) + ReviewedAt time.Time `json:"reviewedAt" bson:"reviewedAt"` // 最近一次审核时间 + ReviewedBy string `json:"reviewedBy" bson:"reviewedBy"` // 最近一次审核人账号 + ResolvedAt time.Time `json:"resolvedAt" bson:"resolvedAt"` // 最终解决时间(resolveStatus=2 时) + ResolveAction string `json:"resolveAction" bson:"resolveAction"` // 解决方式:空/"text"-文本修改 "offshelf"-直接下架 +} + +// 解决方式取值 +const ( + ResolveActionText = "text" // 提交文本修改 + ResolveActionOffShelf = "offshelf" // 批量下架处置 +) diff --git a/models/v/coupon_record_mod/app.go b/models/v/coupon_record_mod/app.go new file mode 100644 index 0000000..6035efb --- /dev/null +++ b/models/v/coupon_record_mod/app.go @@ -0,0 +1,73 @@ +package coupon_record_mod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// AppListReq 优惠券列表请求 +type AppListReq struct { + Type int `form:"type" json:"type" ` // 优惠券使用类型 0表示返回全部 1表示只返回有效(未使用且未过期)2 返回已过期 + PageNumber int64 `form:"pageNumber" json:"pageNumber"` // 当前页码 + PageSize int64 `form:"pageSize" json:"pageSize"` // 每页数量 +} + +type QueryAllRes struct { + List []*AppCouponRecordRes `json:"list"` + Total int64 `json:"total"` +} + +func (receiver *AppListReq) Filter(uid uint64) bson.M { + var query = bson.M{} + if receiver.Type == 1 { + query["expireTime"] = bson.M{"$gte": time.Now()} + query["used"] = false + } + if receiver.Type == 2 { + query["expireTime"] = bson.M{"$lt": time.Now()} + } + query["uid"] = uid + query["isDelete"] = false + return query +} + +func (receiver AppListReq) Options() *options.FindOptions { + return options.Find().SetSkip((receiver.PageNumber - 1) * receiver.PageSize).SetLimit(receiver.PageSize).SetSort(bson.M{"createTime": -1}) +} + +type AppGainUserInfoReq struct { + UID uint64 `json:"uid"` // 用户ID + UserName string `json:"user_name"` // 用户名称 + AppID int32 `json:"app_id"` // APPId + TotalRecharge int64 `json:"total_recharge"` // 用户累计充值金额 + Balance int64 `json:"balance"` // 用户余额 + Token string `json:"token"` // Token +} + +// AppCouponRecordRes 用户优惠券记录响应 +type AppCouponRecordRes struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档ID + CID string `json:"cId" bson:"cId"` // 优惠券唯一ID + Name string `json:"name" bson:"name"` // 奖品名称 + Type PrizeType `json:"type" bson:"type"` // 奖品类型 + Count int32 `json:"count" bson:"count"` // 优惠券数量 + Price int64 `json:"price" bson:"price"` // 奖品价值 + Value int64 `json:"value" bson:"value"` // 折扣率/活跃值 + Used bool `json:"used" bson:"used"` // 优惠券是否使用 + ExpireTime time.Time `json:"expireTime" bson:"expireTime"` // 过期时间 + CreateTime time.Time `json:"createTime" bson:"createTime"` // 创建时间 +} + +type AppGainUserInfoRes struct { + UID uint64 `json:"uid"` // 用户ID + LotteryTimes int64 `json:"lottery_times"` // 用户生剩余次数 + Data string `json:"data"` // 数据 +} + +type EditSelector struct { + Used bool `json:"used" bson:"used"` // 优惠券是否使用 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 修改时间 +} diff --git a/models/v/coupon_record_mod/coupon_record.go b/models/v/coupon_record_mod/coupon_record.go new file mode 100644 index 0000000..dde83a6 --- /dev/null +++ b/models/v/coupon_record_mod/coupon_record.go @@ -0,0 +1,236 @@ +package coupon_record_mod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "context" + "errors" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.CouponRecord + +// InitIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{"uid", 1}}, + }, + { + Keys: bson.D{{"cId", 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{"createdAt", 1}}, + }, + { + Keys: bson.D{{"expireTime", 1}}, + }, + } + _, err := coll.CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func InsertOne(cfg CouponRecord) error { + cfg.CreateTime = time.Now() + cfg.UpdateTime = time.Now() + if _, err := coll(nil).InsertOne(cfg); err != nil { + return err + } + return nil +} + +func InsertManyCouponRecords(t *db.MongoTool, trans []CouponRecord) (err error) { + ops := options.InsertMany().SetOrdered(false) + _, err = coll(t).InsertMany(trans, ops) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertManyCouponRecords", table, "InsertMany", err)) + } + return +} + +func FindOneById(id primitive.ObjectID) (CouponRecord, error) { + cfg := CouponRecord{} + err := coll(nil).FindOne(&cfg, bson.M{"_id": id}) + if err != nil { + return cfg, err + } + return cfg, err +} + +func FindOneByCId(cId string) (CouponRecord, error) { + cfg := CouponRecord{} + err := coll(nil).FindOne(&cfg, bson.M{"cId": cId}) + if err != nil { + return cfg, err + } + return cfg, err +} + +func IsExist(uid uint64, videoId primitive.ObjectID) (bool, error) { + if count, err := coll(nil).Count(bson.M{"uid": uid, "videoId": videoId}); err != nil { + return false, err + } else if count == 0 { + return false, nil + } + return true, nil +} + +// SubmitDocument 提交文档 +func SubmitDocument(document CouponRecord) error { + result, err := coll(nil).InsertOne(&document) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "SubmitDocument", table, "InsertOne", err), + log.Any("document", document), + ) + return err + } + if result.InsertedID.(primitive.ObjectID).IsZero() { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "SubmitDocument", table, "result.InsertedID", "InsertedID is null"), + log.Any("document", document), + ) + return errors.New("result is null") + } + return nil +} + +// Remove 删除 +func Remove(filter primitive.M) (err error) { + _, err = coll(nil).DeleteOne(filter) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Remove", table, "DeleteOne", err), + log.Any("filter", filter), + ) + return + } + return +} + +// Edit 修改文档 +func Edit(filter, update primitive.M) error { + result, err := coll(nil).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Edit", table, "UpdateOne", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + if result.ModifiedCount == 0 { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Edit", table, "result.ModifiedCount", "ModifiedCount is zero"), + log.Any("filter", filter), + log.Any("update", update), + ) + return errors.New("result is null") + } + return nil +} + +// QueryAllList 分页查询文档 +func QueryAllList(filter primitive.M, opts ...*options.FindOptions) (out []*AppCouponRecordRes, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// QueryAllWebList 分页查询文档 +func QueryAllWebList(filter primitive.M, opts ...*options.FindOptions) (out []*CouponRecord, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllWebList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllWebCount 查询文档条目数 +func QueryAllWebCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllWebCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// QueryCouponRecord 查询用户优惠券 +func QueryCouponRecord(uid uint64, cId string) (cr *CouponRecord, err error) { + cr = &CouponRecord{} + err = coll(nil).FindOne(cr, bson.M{"uid": uid, "cId": cId}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryCouponRecord", table, "FindOne", err), + log.Any("uid", uid), + log.Any("cId", cId), + ) + } + return +} + +// QueryCouponRecordById 查询用户优惠券 +func QueryCouponRecordById(cId string) (cr *CouponRecord, err error) { + cr = &CouponRecord{} + err = coll(nil).FindOne(cr, bson.M{"cId": cId}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryCouponRecordById", table, "FindOne", err), + log.Any("cId", cId), + ) + } + return +} + +// UpdateOneById 修改状态 +func UpdateOneById(ctx context.Context, t *db.MongoTool, cId string, update primitive.M) ( + err error) { + _, err = coll(t).UpdateOne(bson.M{"cId": cId, "used": false}, update) + if err != nil { + log.WarnX(ctx, fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("cId", cId), + log.Any("update", update), + ) + } + return +} diff --git a/models/v/coupon_record_mod/type.go b/models/v/coupon_record_mod/type.go new file mode 100644 index 0000000..a5120c7 --- /dev/null +++ b/models/v/coupon_record_mod/type.go @@ -0,0 +1,39 @@ +package coupon_record_mod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type PrizeType int + +// 奖品类型 1-金币 2-游戏币 3-会员卡 4-vip 5-金币加赠券 6-金币观影券 7-实物 +const ( + PrizeTypeGold PrizeType = iota + 1 // 1、金币 + PrizeTypeGameGold // 2、棋牌游戏币 + PrizeTypeVIPDiscount // 3、会员折扣卡 + PrizeTypeVIPCard // 4、会员卡 + PrizeTypeGoldWatch // 5、金币观影券 + PrizeTypeGoldCoinBonus // 6、金币加赠券 + PrizeTypeInKind // 7、实物奖励 +) + +// CouponRecord 用户优惠券记录 +type CouponRecord struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档ID + CID string `json:"cId" bson:"cId"` // 优惠券记录唯一ID + PID primitive.ObjectID `json:"pId" bson:"pId"` // 优惠券ID + UID uint64 `json:"uid" bson:"uid"` // 用户ID + UserName string `json:"userName" bson:"userName"` // 用户名 + Name string `json:"name" bson:"name"` // 奖品名称 + Type PrizeType `json:"type" bson:"type"` // 奖品类型 + Count int32 `json:"count" bson:"count"` // 优惠券数量 + Price int64 `json:"price" bson:"price"` // 奖品价值 + Value int64 `json:"value" bson:"value"` // 折扣率/活跃值 + Used bool `json:"used" bson:"used"` // 优惠券是否使用 + IsDelete bool `json:"isDelete" bson:"isDelete"` // 优惠券是否删除 + ExpireTime time.Time `json:"expireTime" bson:"expireTime"` // 过期时间 + CreateTime time.Time `json:"createTime" bson:"createTime"` // 创建时间 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 修改时间 +} diff --git a/models/v/coupon_record_mod/web.go b/models/v/coupon_record_mod/web.go new file mode 100644 index 0000000..bff46e3 --- /dev/null +++ b/models/v/coupon_record_mod/web.go @@ -0,0 +1,34 @@ +package coupon_record_mod + +import ( + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// WebListReq 优惠券列表请求 +type WebListReq struct { + Used *bool `form:"used" json:"used" ` // 优惠券使用类型 0表示返回全部 1表示只返回有效(未使用且未过期) + Uid *uint64 `form:"uid" json:"uid" ` // 用户ID + PageNumber int64 `form:"pageNumber" json:"pageNumber"` // 当前页码 + PageSize int64 `form:"pageSize" json:"pageSize"` // 每页数量 +} + +type QueryAllWebRes struct { + List []*CouponRecord `json:"list"` + Total int64 `json:"total"` +} + +func (receiver *WebListReq) Filter() bson.M { + var query = bson.M{} + if receiver.Used != nil { + query["used"] = receiver.Used + } + if receiver.Uid != nil { + query["uid"] = receiver.Uid + } + return query +} + +func (receiver WebListReq) Options() *options.FindOptions { + return options.Find().SetSkip((receiver.PageNumber - 1) * receiver.PageSize).SetLimit(receiver.PageSize).SetSort(bson.M{"createTime": -1}) +} diff --git a/models/v/currencymod/base.go b/models/v/currencymod/base.go new file mode 100644 index 0000000..080f2e3 --- /dev/null +++ b/models/v/currencymod/base.go @@ -0,0 +1,15 @@ +package currencymod + +import ( + "91porn-server/common/db" + "91porn-server/models" +) + +const currencyTable = models.Currency + +var currencyDB *db.MongoDB + +func Init() { + currencyDB = db.Init(currencyTable) + currencyIndex() +} diff --git a/models/v/currencymod/currency.go b/models/v/currencymod/currency.go new file mode 100644 index 0000000..89f407a --- /dev/null +++ b/models/v/currencymod/currency.go @@ -0,0 +1,112 @@ +package currencymod + +import ( + "encoding/json" + "errors" + "fmt" + + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func currencyColl(t *db.MongoTool) *db.MongoTool { + if t == nil { + return currencyDB.Coll(currencyTable) + } + return t.Coll(currencyTable) +} + +func currencyIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "type", Value: 1}, {Key: "isActive", Value: 1}, {Key: "coins", Value: -1}}, + }, + } + if _, err := currencyColl(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("currency model set index err ==>[%+v]", err)) + } +} + +// Get 获取单个货币配置 +func Get(id primitive.ObjectID) (Currency, error) { + var item Currency + return item, currencyColl(nil).FindOne(&item, bson.M{"_id": id}) +} + +// List 获取货币配置列表 +func List(t commod.CurrencyType) ([]Currency, error) { + var items []Currency + return items, currencyColl(nil).Find(&items, bson.M{"isActive": true, "type": t}, options.Find().SetSort(bson.M{"coins": 1})) +} + +// Add 新增 +func Add(document interface{}) error { + _, err := currencyColl(nil).InsertOne(&document) + return err +} + +// Edit 编辑 +func Edit(filter primitive.M, update primitive.M) error { + result, err := currencyColl(nil).UpdateOne(filter, update) + if err != nil { + log.Warn("currency edit err", log.Any("filter", filter), log.Any("update", update), log.E(err)) + return err + } + if result.ModifiedCount == 0 { + log.Warn("currency edit fail", log.Any("filter", filter), log.Any("update", update), log.E(err)) + return errors.New("ModifiedCount is nil") + } + return nil +} + +// QueryAll 查询 +func QueryAll(filter primitive.M, opts ...*options.FindOptions) ([]*Currency, error) { + var out []*Currency + return out, currencyColl(nil).Find(&out, filter, opts...) +} + +// QueryCount 查询条目 +func QueryCount(filter primitive.M) (int64, error) { + return currencyColl(nil).Count(filter) +} + +// GetCurrencyListByRedis 条件获取金币配置表,缓存拿 +func GetCurrencyListByRedis(t commod.CurrencyType) ([]*Currency, error) { + var back []*Currency + redisKey := redisconst.DataCachKey(currencyTable, "trueList") + var redisc *redis.Client + if redisc == nil || !redisc.Exists(redisKey) { + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "coins", Value: 1}}) + if err := currencyColl(nil).Find(&back, bson.M{"isActive": true, "type": t}, &opts); err != nil { + log.ZapLog.Warn("currencymod GetCurrencyListByRedis Find fail", log.E(err)) + return nil, err + } + if redisc == nil { + return back, nil + } + jsonBytes, err := json.Marshal(back) + if err != nil { + return back, err + } + _ = redisc.Set(redisKey, string(jsonBytes), redisconst.EmailCaptchaExpire) + return back, err + } + str, err := redisc.Get(redisKey) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCurrencyListByRedis", currencyTable, "Find", err)) + return nil, err + } + if str == nil { + return back, errors.New("redis key is null") + } + return back, json.Unmarshal([]byte(*str), &back) +} diff --git a/models/v/currencymod/type.go b/models/v/currencymod/type.go new file mode 100644 index 0000000..b29d122 --- /dev/null +++ b/models/v/currencymod/type.go @@ -0,0 +1,80 @@ +package currencymod + +import ( + "time" + + "91porn-server/models/commod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Currency 货币 +type Currency struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` //货币名称 + Type commod.CurrencyType `json:"type" bson:"type"` //货币类型 + Coins int64 `json:"coins" bson:"coins"` //购买货币数 + Price int64 `json:"price" bson:"price"` //价格 单位:分 + CouponDesc string `json:"couponDesc" bson:"couponDesc"` //优惠描述 + GiveVipDays int `json:"giveVipDays" bson:"giveVipDays"` //赠送vip天数 + LouFengUnlockTimes int `json:"louFengUnlockTimes" bson:"louFengUnlockTimes"` //赠送楼凤解锁次数 + GiveFruitCoin int64 `json:"giveFruitCoin" bson:"giveFruitCoin"` //赠送果币 + GiveGold int64 `json:"giveGold" bson:"giveGold"` //赠送金币 + FirstGiveVipDays int `json:"firstGiveVipDays" bson:"firstGiveVipDays"` //首次赠送vip天数 + FirstLouFengUnlockTimes int `json:"firstLouFengUnlockTimes" bson:"firsLouFengUnlockTimes"` //首次赠送楼凤解锁次数 + FirstGiveFruitCoin int64 `json:"firstGiveFruitCoin" bson:"firstGiveFruitCoin"` //首次赠送果币 + FirstGiveGold int64 `json:"firstGiveGold" bson:"firstGiveGold"` //首次赠送金币 + NotFirst bool `json:"notFirst" bson:"notFirst"` //非首充依然赠送 + IsActive bool `json:"isActive" bson:"isActive"` //是否激活 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //刷新时间 +} + +// CurrencyApp 货币 +type CurrencyApp struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + Name string `json:"name" bson:"name"` // 货币名称 + Type commod.CurrencyType `json:"type" bson:"type"` // 货币类型 + Coins int64 `json:"coins" bson:"coins"` // 购买货币数 + Price int64 `json:"price" bson:"price"` // 价格 单位:分 + CouponDesc string `json:"couponDesc" bson:"couponDesc"` // 优惠描述 +} + +// YuanToFen 货币转化 +func (c *Currency) YuanToFen() int64 { + return decimal.NewFromInt(c.Price).Mul(decimal.NewFromInt(100)).IntPart() +} + +// HandlerGiveVip 赠送会员天数处理 +func (c *Currency) HandlerGiveVip(level int, expireTime time.Time, isFirstPay bool) (int, time.Time) { + addDay := c.GiveVipDays + if isFirstPay { + addDay += c.FirstGiveVipDays + } + // 默认赠送等级为1 + var giveLevel = 1 + // VIP未过期 + if expireTime.After(time.Now()) { + expireTime = expireTime.AddDate(0, 0, addDay) + if level > giveLevel { + return level, expireTime + } + return giveLevel, expireTime + } + // VIP已过期 + return giveLevel, time.Now().AddDate(0, 0, addDay) +} + +func ListByIdsMap(ids []primitive.ObjectID) (map[primitive.ObjectID]*Currency, error) { + var items []*Currency + if err := currencyColl(nil).Find(&items, bson.M{"_id": bson.M{"$in": ids}}); err != nil { + return nil, err + } + var m = make(map[primitive.ObjectID]*Currency, 0) + for _, v := range items { + m[v.ID] = v + } + return m, nil +} diff --git a/models/v/dailytaskmod/dailytask.go b/models/v/dailytaskmod/dailytask.go new file mode 100644 index 0000000..badc247 --- /dev/null +++ b/models/v/dailytaskmod/dailytask.go @@ -0,0 +1,128 @@ +package dailytaskmod + +import ( + "errors" + "fmt" + "go.mongodb.org/mongo-driver/mongo/options" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.DailyTask + +// InitIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "type", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type DailyTaskTypeEnum int64 + +const ( + DailyTaskTypeAdsClick DailyTaskTypeEnum = 0 // 每日广告点击 + DailyTaskTypeUserInvite DailyTaskTypeEnum = 1 // 每日邀请 + DailyUserLogin DailyTaskTypeEnum = 2 // 每日登录 + DailyComment DailyTaskTypeEnum = 3 // 每日评论 + DailyPublish DailyTaskTypeEnum = 4 // 每日发布 + CommentSetGod DailyTaskTypeEnum = 5 // 评论被设置成神评 + DailyDoCollect DailyTaskTypeEnum = 6 // 每日收藏 + DailyUnlockByGold DailyTaskTypeEnum = 7 // 每日解锁(video/acg) +) + +type DailyTask struct { + ID primitive.ObjectID `json:"_id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` // 任务标题 + Img string `json:"img" bson:"img"` // 任务图片 + Type DailyTaskTypeEnum `json:"type" bson:"type"` // 0 每日广告点击; 1 每日邀请 + Desc string `json:"desc" bson:"desc"` // 任务说明 + Detail []TaskDetail `json:"detail" bson:"detail"` // 任务详情 + Status bool `json:"status" bson:"status"` // 启用/禁用 + Link string `json:"link" bson:"link"` // 跳转链接 + SortNum int `json:"sortNum" bson:"sortNum"` // 排序 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +type TaskDetail struct { + Prizes []primitive.ObjectID `json:"prizes" bson:"prizes"` // 奖品列表 + FinishCondition uint64 `json:"finishCondition" bson:"finishCondition"` // 达成条件 +} + +func GetUserDailyTasks(t *db.MongoTool) ([]DailyTask, error) { + var ds []DailyTask + op := options.Find().SetSort(bson.D{{"sortNum", 1}}) + return ds, coll(t).Find(&ds, bson.M{"status": true}, op) +} +func GetTasks(filter bson.M, opt *options.FindOptions) ([]DailyTask, bool, error) { + var ds []DailyTask + if opt == nil { + opt = options.Find() + } + if opt.Limit == nil || *opt.Limit == 0 { + opt.SetLimit(101) + } + + err := coll(nil).Find(&ds, filter, opt) + if err != nil { + return nil, false, err + } + next := len(ds) == int(*opt.Limit) + if next { + ds = ds[:*opt.Limit-1] + } + return ds, true, nil +} + +func GetUserDailyTaskByType(t *db.MongoTool, taskType DailyTaskTypeEnum) (*DailyTask, error) { + var dt DailyTask + if err := coll(t).FindOne(&dt, bson.M{"type": taskType, "status": true}); err != nil { + return nil, err + } + if dt.ID.IsZero() { + return nil, errors.New("not found") + } + if !dt.Status { + return nil, errors.New("task not active") + } + return &dt, nil +} + +func GetUserDailyTaskByID(t *db.MongoTool, id primitive.ObjectID) (*DailyTask, error) { + var dt DailyTask + if err := coll(t).FindOne(&dt, bson.M{"_id": id}); err != nil { + return nil, err + } + if dt.ID.IsZero() { + return nil, errors.New("not found") + } + if !dt.Status { + return nil, errors.New("task not active") + } + return &dt, nil +} diff --git a/models/v/dailytaskmod/web.go b/models/v/dailytaskmod/web.go new file mode 100644 index 0000000..839cd2d --- /dev/null +++ b/models/v/dailytaskmod/web.go @@ -0,0 +1,41 @@ +package dailytaskmod + +import ( + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AddDailyTaskReq struct { + Title *string `json:"title"` // 任务标题 + Img *string `json:"img"` // 任务图片 + Type *DailyTaskTypeEnum `json:"type"` // 0 每日广告点击; 1 每日邀请 + Desc *string `json:"desc"` // 任务说明 + Detail []TaskDetail `json:"detail"` // 任务详情 + Status *bool `json:"status"` // 启用/禁用 + Link *string `json:"link"` // 跳转链接 + SortNum *int `json:"sortNum"` // 排序 +} + +func AddDailyTask(t *db.MongoTool, dt DailyTask) error { + _, err := coll(t).InsertOne(dt) + return err +} + +type EditDailyTaskReq struct { + ID primitive.ObjectID `json:"_id" binding:"required"` // 被编辑的任务id + Title *string `json:"title" bson:"title"` // 任务标题 + Img *string `json:"img" bson:"img"` // 任务图片 + Type *DailyTaskTypeEnum `json:"type" bson:"type"` // 0 每日广告点击; 1 每日邀请 + Desc *string `json:"desc" bson:"desc"` // 任务说明 + Detail []TaskDetail `json:"detail" bson:"detail"` // 任务详情 + Status *bool `json:"status" bson:"status"` // 启用/禁用 + Link *string `json:"link" bson:"link"` // 跳转链接 + SortNum *int `json:"sortNum" bson:"sortNum"` // 排序 +} + +func EditDailyTask(t *db.MongoTool, id primitive.ObjectID, set bson.M) error { + _, err := coll(t).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}) + return err +} diff --git a/models/v/discount_area_mod/app.go b/models/v/discount_area_mod/app.go new file mode 100644 index 0000000..7d46322 --- /dev/null +++ b/models/v/discount_area_mod/app.go @@ -0,0 +1 @@ +package discount_area_mod diff --git a/models/v/discount_area_mod/dIscountArea.go b/models/v/discount_area_mod/dIscountArea.go new file mode 100644 index 0000000..ca0a49c --- /dev/null +++ b/models/v/discount_area_mod/dIscountArea.go @@ -0,0 +1,123 @@ +package discount_area_mod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const ( + table = models.DiscountArea +) + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func GetNoStatusDiscountAreaByIds(ids []primitive.ObjectID) (list []DiscountArea, err error) { + cond := bson.M{} + cond["_id"] = bson.M{"$in": ids} + if err = coll(nil).Find(&list, cond); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetNoStatusDiscountAreaByIds", table, "Find", err), + log.Any("cond", cond), + ) + return + } + return +} + +// GetDiscountAreaById 通过id获取上架的折扣区 +func GetDiscountAreaById(id primitive.ObjectID) (data DiscountArea, err error) { + cond := bson.M{} + cond["status"] = 1 + cond["_id"] = id + if err = coll(nil).FindOne(&data, cond); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetDiscountAreaById", table, "FindOne", err), + log.Any("cond", cond), + ) + return data, err + } + return +} + +// GetDiscountAreaByIds 通过ids获取上架的折扣区 +func GetDiscountAreaByIds(ids []primitive.ObjectID) (list []DiscountArea, err error) { + opts := options.FindOptions{} + sort := bson.D{{Key: "sortCode", Value: -1}} + opts.SetSort(sort) + cond := bson.M{} + cond["status"] = 1 + if len(ids) > 0 { + cond["_id"] = bson.M{"$in": ids} + } + if err = coll(nil).Find(&list, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetDiscountAreaByIds", table, "Find", err), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return nil, err + } + return +} + +// GetAllDiscountArea 获取所有上架的折扣区 +func GetAllDiscountArea() (list []DiscountArea, err error) { + opts := options.FindOptions{} + sort := bson.D{{Key: "sortCode", Value: -1}} + opts.SetSort(sort) + cond := bson.M{} + cond["status"] = 1 + if err = coll(nil).Find(&list, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAllDiscountArea", table, "Find", err), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return nil, err + } + return +} + +// GetAllNoDeleteDiscountArea 获取没有被删除的专区 +func GetAllNoDeleteDiscountArea() (list []DiscountArea, err error) { + opts := options.FindOptions{} + sort := bson.D{{Key: "sortCode", Value: -1}} + opts.SetSort(sort) + cond := bson.M{} + cond["status"] = bson.M{"$in": []int{0, 1}} + if err = coll(nil).Find(&list, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAllNoDeleteDiscountArea", table, "Find", err), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return nil, err + } + return +} + +// AddDiscountArea 新增折扣专区 +func AddDiscountArea(model *DiscountArea) (err error) { + if _, err = coll(nil).InsertOne(model); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AddDiscountArea", table, "InsertOne", err)) + return err + } + return +} + +// UpdateDiscountArea 修改折扣专区 +func UpdateDiscountArea(id primitive.ObjectID, update bson.M) (err error) { + filter := bson.M{"_id": id} + if _, err = coll(nil).UpdateOne(filter, bson.M{"$set": update}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateDiscountArea", table, "UpdateOne", err)) + return err + } + return +} diff --git a/models/v/discount_area_mod/type.go b/models/v/discount_area_mod/type.go new file mode 100644 index 0000000..8cbab60 --- /dev/null +++ b/models/v/discount_area_mod/type.go @@ -0,0 +1,24 @@ +package discount_area_mod + +import ( + "91porn-server/common/db" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +// DiscountArea 帖子打折专区 +type DiscountArea struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` // 专区名 + Desc string `json:"desc" bson:"desc"` //专区描述 + Discount int `json:"discount" bson:"discount"` // 折扣,1折 10,1.5折 15 + Status int `json:"status" bson:"status"` // 0-未开启 1-开启 2-删除 + SortCode int `json:"sortCode" bson:"sortCode"` //排序字段 + RecommendNum int `json:"recommendNum" bson:"recommendNum"` // 推荐展示个数 + UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"` // 更新时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 +} + +func Init() { + mdb = db.Init(table) +} diff --git a/models/v/discount_area_mod/web.go b/models/v/discount_area_mod/web.go new file mode 100644 index 0000000..bf16173 --- /dev/null +++ b/models/v/discount_area_mod/web.go @@ -0,0 +1,75 @@ +package discount_area_mod + +import ( + "91porn-server/models/commod" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +type WebDiscountAreaListReq struct { +} + +type WebDiscountAreaListResp struct { + List []DiscountArea `json:"list"` +} + +type WebDiscountAreaAddReq struct { + Title string `json:"title" form:"title" binding:"required"` // 专区名 + Desc string `json:"desc" form:"desc"` //专区描述 + Discount uint `json:"discount" form:"discount" binding:"min=0,max=100"` // 折扣,1折 10,1.5折 15 + Status uint `json:"status" form:"status,omitempty"` // 0-未开启 1-开启 + SortCode uint `json:"sortCode" form:"sortCode"` //排序字段 + RecommendNum uint `json:"recommendNum" form:"recommendNum"` // 推荐展示个数 +} + +type WebDiscountAreaUpdateReq struct { + Id string `json:"id" form:"id" binding:"required"` // 专区名 + WebDiscountAreaUpdateData +} + +type WebDiscountAreaUpdateData struct { + Title *string `json:"title" bson:"title,omitempty" form:"title"` // 专区名 + Desc *string `json:"desc" bson:"desc,omitempty" form:"desc"` //专区描述 + Discount *uint `json:"discount" bson:"discount,omitempty" form:"discount,omitempty"` // 折扣,1折 10,1.5折 15 + Status *uint `json:"status" bson:"status,omitempty" form:"status,omitempty"` // 0-未开启 1-开启 + SortCode *uint `json:"sortCode" bson:"sortCode,omitempty" form:"sortCode"` //排序字段 + RecommendNum *uint `json:"recommendNum" bson:"recommendNum,omitempty" form:"recommendNum"` // 推荐展示个数 +} + +type WebDiscountAreaDeleteReq struct { + ID string `json:"id" form:"id" binding:"required"` +} + +type WebDiscountAreaAddVidReq struct { + DiscountAreaId primitive.ObjectID `json:"discountAreaId" form:"discountAreaId" binding:"required"` + VIds []primitive.ObjectID `json:"vids" form:"vids" binding:"required"` +} + +type WebDiscountAreaDeleteVidReq struct { + Ids []primitive.ObjectID `json:"ids" form:"ids" binding:"required"` +} + +type WebDiscountAreaVidsReq struct { + commod.Page + DiscountAreaId string `json:"discountAreaId" form:"discountAreaId"` // 折扣专区id + Vid string `json:"vid" form:"vid"` // 视频id +} + +type WebDiscountAreaVidsResp struct { + List []WebDiscountAreaVideo `json:"list"` + Total int64 `json:"total"` +} + +type WebDiscountAreaVideo struct { + Id primitive.ObjectID `json:"id"` // 记录id + VideoID primitive.ObjectID `json:"videoID"` // 视频ID + Title string `json:"title"` // 标题 + PublisherID uint64 `json:"publisherID"` // 发布用户ID + Cover string `json:"cover"` // 视频封面图 + DiscountAreaTitle string `json:"discountAreaTitle"` // 折扣专区标题 + DiscountAreaId primitive.ObjectID `json:"discountAreaId"` // 折扣专区id + OriginalPrice int64 `json:"originalPrice"` // 原价 + DiscountedPrice int64 `json:"discountedPrice"` // 折扣区价格 + UpdatedAt time.Time `json:"updatedAt"` // 更新时间 + CreatedAt time.Time `json:"createdAt"` // 创建时间 +} diff --git a/models/v/discount_area_video_mod/discount_area_video.go b/models/v/discount_area_video_mod/discount_area_video.go new file mode 100644 index 0000000..b2b7800 --- /dev/null +++ b/models/v/discount_area_video_mod/discount_area_video.go @@ -0,0 +1,115 @@ +package discount_area_video_mod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.DiscountAreaVideo + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "discountAreaId", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + // 根据视频筛选 + Keys: bson.D{{Key: "videoID", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// BatchAdd 折扣专区批量添加视频 +func BatchAdd(list []DiscountAreaVideo) (err error) { + if len(list) == 0 { + return + } + if _, err = coll(nil).InsertMany(list); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "BatchAdd", table, "InsertMany", err)) + return err + } + return +} + +// GetListByCond 条件获取专区视频列表 +func GetListByCond(filter primitive.M, opts ...*options.FindOptions) (list []DiscountAreaVideo, total int64, err error) { + // 获取数量 + total, err = coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-GetListByCond]==> Model %s Count fail error:%+v:", table, err), + log.Any("filter", filter), + ) + return + } + if err = coll(nil).Find(&list, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-GetListByCond]==> Model %s Find fail error:%+v:", table, err), + log.Any("filter", filter), + ) + return + } + return +} + +func GetDiscountAreaVideoByVids(vids []primitive.ObjectID) (list []DiscountAreaVideo, err error) { + filter := bson.M{ + "videoID": bson.M{"$in": vids}, + } + if err = coll(nil).Find(&list, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-GetDiscountAreaVideo]==> Model %s Find fail error:%+v:", table, err), + log.Any("filter", filter), + ) + return + } + return +} + +func GetDiscountAreaVideoByIds(ids []primitive.ObjectID) (list []DiscountAreaVideo, err error) { + if len(ids) == 0 { + return + } + filter := bson.M{ + "_id": bson.M{"$in": ids}, + } + if err = coll(nil).Find(&list, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-GetDiscountAreaVideoByIds]==> Model %s Find fail error:%+v:", table, err), + log.Any("filter", filter), + ) + return + } + return +} + +func DeleteDiscountAreaVideoByIds(ids []primitive.ObjectID) (err error) { + if len(ids) == 0 { + return + } + filter := bson.M{ + "_id": bson.M{"$in": ids}, + } + if _, err = coll(nil).DeleteMany(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-DeleteDiscountAreaVideoByIds]==> Model %s DeleteMany fail error:%+v:", table, err), + log.Any("filter", filter), + ) + return + } + return +} diff --git a/models/v/discount_area_video_mod/type.go b/models/v/discount_area_video_mod/type.go new file mode 100644 index 0000000..40e66d7 --- /dev/null +++ b/models/v/discount_area_video_mod/type.go @@ -0,0 +1,21 @@ +package discount_area_video_mod + +import ( + "91porn-server/common/db" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +// DiscountAreaVideo 折扣区视频 +type DiscountAreaVideo struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + DiscountAreaId primitive.ObjectID `json:"discountAreaId" bson:"discountAreaId" binding:"required"` // 折扣区id + VideoID primitive.ObjectID `json:"videoID" bson:"videoID" binding:"required"` // 视频id + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` // 刷新时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/errfedbkmod/errorfeedback.go b/models/v/errfedbkmod/errorfeedback.go new file mode 100644 index 0000000..1d0d246 --- /dev/null +++ b/models/v/errfedbkmod/errorfeedback.go @@ -0,0 +1,147 @@ +package errfedbkmod + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.ErrFeedback + +// InitIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// todo app +func Insert(f ErrFeedback) (err error) { + now := time.Now() + f.UpdatedAt = now + f.CreatedAt = now + if _, err = coll(nil).InsertOne(f); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err), log.Any("LouFengErrFeedback", f)) + return + } + return +} + +func FindOneByProductId(pid primitive.ObjectID) (*ErrFeedback, error) { + filter := bson.M{"productID": pid, "processingStatus": ProcessingStatusDefault} + var f ErrFeedback + if err := coll(nil).FindOne(&f, filter); err != nil { + return nil, err + } + if f.ID.IsZero() { + return nil, nil + } + return &f, nil +} + +// todo web +// StdFind 通用查询 +func StdFind(q QuerySelector, page commod.Page) (data []ErrFeedback, total int64, err error) { + data = make([]ErrFeedback, 0) + skip := int64(page.Skip()) + limit := int64(page.Limit()) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + f, err := common.ToBsonM(q) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "StdFind", table, "ToBsonM", err), log.Any("cond", f)) + return + } + createdTime := bson.M{} + if q.StartTime != nil { + createdTime["$gte"] = q.StartTime + } + if q.EndTime != nil { + createdTime["$lt"] = q.EndTime + } + if q.TimeString != nil { + f[*q.TimeString] = createdTime + } else { + if len(createdTime) > 0 { + f["createdAt"] = createdTime + } + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "StdFind", table, "Find", err), log.Any("cond", f)) + return + } + total, err = coll(nil).Count(f) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "StdFind", table, "Count", err), log.Any("cond", f)) + return + } + return +} + +// Update 修改LouFeng类型 +func Update(set *EditSelector) (err error) { + set.UpdatedAt = time.Now() + if _, err = coll(nil).UpdateOne(bson.M{"_id": set.ID}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("set", set), + ) + return + } + return +} + +// Update 修改LouFeng类型 +func FindUpdate(set *EditSelector) (fb ErrFeedback, err error) { + set.UpdatedAt = time.Now() + if err = coll(nil).FindOneAndUpdate(&fb, bson.M{"_id": set.ID}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("set", set), + ) + return + } + return +} + +// Remove 删除LouFeng类型 +func Remove(id string) (err error) { + OID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Remove", table, "ObjectIDFromHex", err), + log.Any("id", id), + ) + return + } + if _, err = coll(nil).DeleteOne(bson.M{"_id": OID}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Remove", table, "DeleteOne", err), + log.Any("OID", OID), + ) + return + } + return +} diff --git a/models/v/errfedbkmod/type.go b/models/v/errfedbkmod/type.go new file mode 100644 index 0000000..33b01cc --- /dev/null +++ b/models/v/errfedbkmod/type.go @@ -0,0 +1,64 @@ +package errfedbkmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +const ( + ProcessingStatusDefault = 0 //等待处理 + ProcessingStatusComplete = 1 //处理完成 + ProcessingStatusIgnore = 2 //忽略 + + ErrFeedbackTypeInvalidContact = 1 //无效联系方式 + ErrFeedbackTypeFalseInformation = 2 //骗子 + ErrFeedbackTypeRepeat = 3 //重复信息 + ErrFeedbackTypeGarbled = 4 //乱码 + + ObjectTypeLoufeng = "loufeng" //楼凤 + ObjectTypeNengMo = "nengmo" //嫩模 + ObjectTypeFiction = "fiction" //小说 +) + +type ErrFeedback struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + UID uint64 `json:"uid" bson:"uid"` //用户id + ErrFeedbackType int `json:"errFeedbackType" bson:"errFeedbackType"` //错误举报类型 + ObjectType string `json:"objectType" bson:"objectType"` //举报对象类型 + ObjectID string `json:"objID" bson:"objID"` //对象id + Content string `json:"content" bson:"content,omitempty"` //内容 + Imgs []string `json:"imgs" bson:"imgs"` //图片 + ProcessingStatus int `json:"processingStatus" bson:"processingStatus"` //处理状态 + ProcessedReceipt string `json:"processedReceipt" bson:"processedReceipt"` //处理回执 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type QuerySelector struct { + ID *primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + UID *uint64 `json:"uid,omitempty" bson:"uid,omitempty"` + ObjectType *string `json:"objectType,omitempty" bson:"objectType,omitempty"` //举报对象类型 + ObjectID *string `json:"objID,omitempty" bson:"objID,omitempty"` //楼凤id + ErrFeedbackType *int `json:"errFeedbackType,omitempty" bson:"errFeedbackType,omitempty"` //错误举报类型 + ProcessingStatus *int `json:"processingStatus,omitempty" bson:"processingStatus,omitempty"` //处理状态 + StartTime *time.Time ` json:"startTime,omitempty" bson:"-"` // 开始时间 + EndTime *time.Time ` json:"endTime,omitempty" bson:"-"` // 结束时间 + TimeString *string `json:"timeString,omitempty" bson:"-"` //时间 +} + +type EditSelector struct { + ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + ProcessingStatus *int `json:"processingStatus,omitempty" bson:"processingStatus,omitempty"` //处理状态 + ProcessedReceipt *string `json:"processedReceipt,omitempty" bson:"processedReceipt,omitempty"` //处理回执 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 +} diff --git a/models/v/eventmod/event.go b/models/v/eventmod/event.go new file mode 100644 index 0000000..278a3a9 --- /dev/null +++ b/models/v/eventmod/event.go @@ -0,0 +1,86 @@ +package eventmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.Event + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initCollectIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{"eventId", 1}}, + }, + { + Keys: bson.D{{"createdAt", 1}}, + }, + { + Keys: bson.D{{"status", 1}, {"retryCount", 1}}, + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } + return +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d *Event) (err error) { + _, err = coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + return +} + +func Exists(eventID string) (bool, error) { + var event Event + if err := coll(nil).FindOne(&event, bson.M{"eventId": eventID}); err != nil { + return false, err + } + return !event.ID.IsZero(), nil +} + +// GetList 获取列表 +func GetList(cond bson.M, limit int64) (res []*Event, err error) { + opts := options.Find() + opts = opts.SetSort(bson.D{{Key: "_id", Value: 1}}).SetLimit(limit) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + return +} + +// UpdateMany 更新数据 +func UpdateMany(cond primitive.M, data bson.M) error { + _, err := coll(nil).UpdateMany(cond, data) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return err + } + + return nil +} diff --git a/models/v/eventmod/type.go b/models/v/eventmod/type.go new file mode 100644 index 0000000..a8b1a62 --- /dev/null +++ b/models/v/eventmod/type.go @@ -0,0 +1,32 @@ +package eventmod + +import ( + "91porn-server/common/db" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type EventStatus int + +const ( + Default EventStatus = 0 // 默认状态,等待上报 + Success EventStatus = 1 // 成功 + Failure EventStatus = 2 // 失败 +) + +type Event struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + EventId string `json:"eventId" bson:"eventId"` + EventType string `json:"eventType" bson:"eventType"` + Data string `json:"data" bson:"data"` + Status EventStatus `json:"status" bson:"status"` // + RetryCount int `json:"retryCount" bson:"retryCount"` // 重试次数 + CreatedAt time.Time `json:"createdAt"bson:"createdAt"` // 创建事件 + UpdatedAt time.Time `bson:"updatedAt"` // 修改时间 +} diff --git a/models/v/exchcodemod/exchcode.go b/models/v/exchcodemod/exchcode.go new file mode 100644 index 0000000..e75e3b0 --- /dev/null +++ b/models/v/exchcodemod/exchcode.go @@ -0,0 +1,174 @@ +package exchcodemod + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.ExchCode + +// initIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "code", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "batchNum", Value: 1}, {Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "app", Value: 1}, {Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "channel", Value: 1}, {Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "authority", Value: 1}, {Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}, {Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func BulkWrite(docs []ExchCode) error { + writeModels := make([]mongo.WriteModel, len(docs)) + for i, doc := range docs { + writeModels[i] = mongo.NewInsertOneModel().SetDocument(doc) + } + if _, err := coll(nil).Bulk(writeModels); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "BulkWrite", table, "Bulk", err)) + return err + } + return nil +} + +func Insert(e ExchCode) error { + if _, err := coll(nil).InsertOne(e); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return err + } + return nil +} + +// 获取兑换码 +func GetExchangeCodeByCode(code string) (data *ExchCode, err error) { + if err = coll(nil).FindOne(&data, bson.M{"code": code}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetExchangeCodeByCode", table, "FindOne", err), + log.Any("code", code), + ) + return + } + return +} + +// 获取兑换码 +func GetExchangeCodeByID(id ObjectID) (data *ExchCode, err error) { + if err = coll(nil).FindOne(&data, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetExchangeCodeByCode", table, "FindOne", err), + log.Any("id", id), + ) + return + } + return +} + +// 兑换 +func Exchange(code string) (err error) { + if _, err = coll(nil).UpdateOne(bson.M{"code": code}, bson.M{"$inc": bson.M{"usableNum": -1}, "$set": bson.M{"updatedAt": time.Now()}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Exchange", table, "UpdateOne", err), + log.Any("code", code), + ) + return + } + return +} + +// 获取推广码列表 +func GetCodeList(filterParams FilterDoc, status *ExchangeStatus, page commod.Page, isSuperAdmin bool) (total int64, data []ExchCode, err error) { + filter, _ := common.ToBsonM(filterParams) + if !isSuperAdmin { + filter["authority"] = bson.M{"$ne": AuthorityGold} + } + if status != nil { + switch *status { + case StatusExpired: + filter["invalidAt"] = bson.M{"$lt": time.Now()} + case StatusUnused: + filter["invalidAt"] = bson.M{"$gte": time.Now()} + filter["status"] = *status + case StatusUsed: + filter["status"] = *status + } + } + var skip = int64((page.PageNumber - 1) * page.PageSize) + var limit = int64(page.PageSize) + var opts = options.Find() + opts.SetSort(bson.D{{Key: "createdAt", Value: -1}}).SetSkip(skip).SetLimit(limit) + if err = coll(nil).Find(&data, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCodeList", table, "Find", err), + log.Any("queryParams", filterParams), + log.Any("page", page), + ) + return + } + if total, err = coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCodeList", table, "Count", err), + log.Any("queryParams", filterParams), + log.Any("page", page), + ) + return + } + return +} + +func UpdateExchangeCode(id ObjectID, doc UpdateDoc) (err error) { + update, _ := common.ToBsonM(doc) + if _, err = coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": update}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateExchangeCode", table, "UpdateOne", err), + log.Any("id", id), + log.Any("doc", doc), + ) + return + } + return +} + +// 获取最近的批次号 +func GetLatestBatchNumber(app AppType) (data ExchCode, err error) { + var opts = options.FindOne() + opts.SetSort(bson.D{{Key: "createdAt", Value: -1}}) + if err = coll(nil).FindOne(&data, bson.M{"app": app}, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetLatestBatchNumber", table, "Find", err), + log.Any("app", app), + ) + return + } + return +} diff --git a/models/v/exchcodemod/type.go b/models/v/exchcodemod/type.go new file mode 100644 index 0000000..6487652 --- /dev/null +++ b/models/v/exchcodemod/type.go @@ -0,0 +1,80 @@ +package exchcodemod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + AppTypeYSVideo AppType = "ysAppVideo" // 音色短视频 + AppTypePFVideo AppType = "pfAppVideo" // 泡芙视频 + AppTypeLTVpn AppType = "ltAppVpn" // 雷霆加速器 + + AuthorityShortVideoVip AuthorityType = "shortVideoVip" // 短视频vip + AuthorityFilmVip AuthorityType = "filmVip" // 影视vip(影院) + AuthoritySuper AuthorityType = "superVip" // 超级vip(可以兑换短视频,也可兑换影视) + AuthorityGold AuthorityType = "gold" // 兑换金币 + Authority3dPermanentVIP AuthorityType = "3dPermanentVip" // 兑换3天vip + AuthorityVideoCoupon AuthorityType = "videoCoupon" // 观影券 + + StatusUnused ExchangeStatus = "unused" // 未使用 + StatusUsed ExchangeStatus = "used" // 已使用 + StatusExpired ExchangeStatus = "expired" // 已过期 + + SystemOperator = "SystemOperator" +) + +type ( + ObjectID = primitive.ObjectID + AppType = string // 兑换码所属的app + AuthorityType = string // 兑换权限 + ExchangeStatus = string // 状态 + + ExchCode struct { + ID ObjectID `json:"id" bson:"_id,omitempty"` + BatchNum string `json:"batchNum" bson:"batchNum"` //批次号 + App AppType `json:"app" bson:"app"` //app + Channel string `json:"channel" bson:"channel"` //渠道号 + Code string `json:"code" bson:"code"` //兑换码 + Authority AuthorityType `json:"authority" bson:"authority"` //兑换权限 + Status ExchangeStatus `json:"status" bson:"status"` //状态 unused: 未使用,used: 已兑换,expired:已过期 + Reward int `json:"reward" bson:"reward"` //奖励值 + RewardCount int `json:"rewardCount" bson:"rewardCount"` //奖励量 + UsableNum int `json:"usableNum" bson:"usableNum"` //可用次数 + Operator string `json:"operator" bson:"operator"` //操作者 + Remark string `json:"remark" bson:"remark"` //备注 + EffectiveAt time.Time `json:"effectiveAt" bson:"effectiveAt"` //生效时间 + InvalidAt time.Time `json:"invalidAt" bson:"invalidAt"` //失效时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //更新时间 + } + + // 列表过滤条件 + FilterDoc struct { + BatchNum *string `bson:"batchNum,omitempty"` + Channel *string `bson:"channel,omitempty"` + Code *string `bson:"code,omitempty"` + App *AppType `bson:"app,omitempty"` + Authority *AuthorityType `bson:"authority,omitempty"` + } + + // 更新条件 + UpdateDoc struct { + Status *ExchangeStatus `bson:"status,omitempty"` + Authority *AuthorityType `bson:"authority,omitempty"` + Channel *string `bson:"channel,omitempty"` + Reward *int `bson:"reward,omitempty"` + EffectiveAt *time.Time `bson:"effectiveAt,omitempty"` + InvalidAt *time.Time `bson:"invalidAt,omitempty"` + Remark *string `bson:"remark,omitempty"` + UsableNum *int `bson:"usableNum,omitempty"` //可用次数 + } +) + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/exchcodemod/web.go b/models/v/exchcodemod/web.go new file mode 100644 index 0000000..e79acca --- /dev/null +++ b/models/v/exchcodemod/web.go @@ -0,0 +1,63 @@ +package exchcodemod + +import ( + "time" + + "91porn-server/models/commod" +) + +type ListReqParam struct { + BatchNum *string `form:"batchNum" json:"batchNum"` + Channel *string `form:"channel" json:"channel"` + Code *string `form:"code" json:"code"` + App *AppType `form:"app" json:"app"` + Authority *AuthorityType `form:"authority" json:"authority"` + Status *ExchangeStatus `form:"status" json:"status"` + Page commod.Page + Role *string `form:"role" json:"role"` +} + +type AddReqParam struct { + App AppType `form:"app" json:"app" binding:"required"` + Channels []string `form:"channels" json:"channels"` + Authority AuthorityType `form:"authority" json:"authority" binding:"required"` + Reward int `form:"reward" json:"reward" binding:"required"` + RewardCount int `form:"rewardCount" json:"rewardCount"` + Count int64 `form:"count" json:"count" binding:"required"` + EffectiveAt time.Time `form:"effectiveAt" json:"effectiveAt" binding:"required"` + InvalidAt time.Time `form:"invalidAt" json:"invalidAt" binding:"required"` + Remark string `form:"remark" json:"remark"` + UsableNum int `form:"usableNum,omitempty" bson:"usableNum,omitempty" binding:"required"` //可用次数 +} + +type UpdateReqParam struct { + ID ObjectID `form:"id" json:"id" binding:"required"` + Status *ExchangeStatus `form:"status" json:"status"` + Channel *string `form:"channel" json:"channel"` + Authority *AuthorityType `form:"authority" json:"authority"` + Reward *int `form:"reward" json:"reward"` + RewardCount *int `form:"rewardCount" json:"rewardCount"` + EffectiveAt *time.Time `form:"effectiveAt" json:"effectiveAt"` + InvalidAt *time.Time `form:"invalidAt" json:"invalidAt"` + Remark *string `form:"remark" json:"remark"` + UsableNum *int `form:"usableNum,omitempty" bson:"usableNum,omitempty"` //可用次数 +} + +type RespList struct { + ID ObjectID `json:"id"` + BatchNum string `json:"batchNum" ` //批次号 + App AppType `json:"app" ` //app ysAppVideo--音色短视频 pfAppVideo--泡芙视频 ltAppVpn--雷霆加速器 + Channel string `json:"channel" ` //渠道号 + Code string `json:"code" ` //兑换码 + Authority AuthorityType `json:"authority" ` //兑换权限 shortVideoVip--短视频vip filmVip--影视vip(影院) superVip--超级vip(可以兑换短视频,也可兑换影视) gold--兑换金币 + Status ExchangeStatus `json:"status" ` //状态 unused: 未使用,used: 已兑换,expired:已过期 + Reward int `json:"reward" ` //奖励值 + RewardCount int `json:"rewardCount" ` //奖励量 + UsableNum int `json:"usableNum" bson:"usableNum"` //可用次数 + Operator string `json:"operator" ` //操作者 + Remark string `json:"remark" ` //备注 + EffectiveAt time.Time `json:"effectiveAt" ` //生效时间 + InvalidAt time.Time `json:"invalidAt" ` //失效时间 + CreatedAt time.Time `json:"createdAt" ` //创建时间 + UpdatedAt time.Time `json:"updatedAt"` //更新时间 +} diff --git a/models/v/export_task_mod/exportTask.go b/models/v/export_task_mod/exportTask.go new file mode 100644 index 0000000..455ba94 --- /dev/null +++ b/models/v/export_task_mod/exportTask.go @@ -0,0 +1,57 @@ +package export_task_mod + +import ( + "91porn-server/common/db" + "91porn-server/models" + "encoding/json" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +var mdb *db.MongoDB + +const table = models.ExportTask + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// CreateTask 新增一个任务 +func CreateTask(taskType int, admin string, param interface{}) (err error) { + b, _ := json.Marshal(param) + _, err = coll(nil).InsertOne(&ExportTask{ + Type: taskType, + Param: string(b), + Admin: admin, + UpdatedAt: time.Now(), + CreatedAt: time.Now(), + }) + if err != nil { + return + } + return +} + +// GetTaskList 拉取需要处理的任务数据 +func GetTaskList() (list []*ExportTask, err error) { + var opts = options.Find() + opts.SetLimit(10) + opts.SetSort(bson.D{{"createdAt", 1}}) + err = coll(nil).Find(&list, bson.M{ + "status": 0, + }, opts) + return list, err +} + +func UpdateTask(id primitive.ObjectID, updateData bson.M) (err error) { + _, err = coll(nil).UpdateOne( + bson.M{"_id": id}, + bson.M{"$set": updateData}, + ) + return +} diff --git a/models/v/export_task_mod/type.go b/models/v/export_task_mod/type.go new file mode 100644 index 0000000..df2bc23 --- /dev/null +++ b/models/v/export_task_mod/type.go @@ -0,0 +1,33 @@ +package export_task_mod + +import ( + "91porn-server/common/db" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 导出任务枚举 +const ( + ExportUserTask = 1 // 导出用户列表 + ExportVidTask = 2 // 导出帖子列表 + ExportAdvanceOrderTask = 3 // 导出预售订单列表 + ExportProductHistoryTask = 4 // 会员卡购买记录表 +) + +type ExportTask struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Type int `json:"type" bson:"type"` + Param string `json:"param" bson:"param"` + Reason string `json:"reason"` // 失败原因 + Status int `json:"status" bson:"status"` // 任务状态 0-排队处理中 1-处理完成 2-处理失败 + FileUrl string `json:"fileUrl"` + Admin string `json:"admin" bson:"admin"` + Total int64 `json:"total" bson:"total"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //更新时间 +} + +func Init() { + mdb = db.Init(table) +} diff --git a/models/v/faqmod/faq.go b/models/v/faqmod/faq.go new file mode 100644 index 0000000..253bcb8 --- /dev/null +++ b/models/v/faqmod/faq.go @@ -0,0 +1,144 @@ +package faqmod + +import ( + "fmt" + "strings" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// CateRes 列表 +type CateRes struct { + Category string `json:"category" bson:"_id"` +} + +var mdb *db.MongoDB + +const table = models.FaqTable + +func faqColl(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initFaqIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "category", Value: 1}}, + }, + { + Keys: bson.D{{Key: "query", Value: 1}}, + Options: options.Index().SetUnique(true).SetPartialFilterExpression(bson.M{"query": bson.M{"$exists": true}}), + }, + { + Keys: bson.D{{Key: "answer", Value: 1}}, + }, + } + if _, err := faqColl(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// FaqInsert 新增常见问题 +func FaqInsert(f Faq) (code stderr.Code, err error) { + f.CreatedAt = time.Now() + f.UpdatedAt = time.Now() + if _, err = faqColl(nil).InsertOne(f); err != nil { + if strings.Contains(err.Error(), "E11000 duplicate key") { + code = stderr.ErrDbInputExist + } else { + code = stderr.ErrDbInsertError + } + log.ZapLog.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FaqInsert", "Faq", "InsertOne", err)) + return + } + return +} + +// FaqUpdate 修改常见问题 +func FaqUpdate(id primitive.ObjectID, doc FaqUpdateDoc) (err error) { + doc.UpdatedAt = time.Now() + update, _ := common.ToBsonM(doc) + if _, err = faqColl(nil).UpdateOne(bson.M{"_id": id}, bson.M{ + "$set": update, + }); err != nil { + log.ZapLog.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FaqUpdate", "Faq", "UpdateOne", err)) + return + } + return +} + +// FaqDelete 批量删除常见问题 +func FaqDelete(ids []primitive.ObjectID) (err error) { + if _, err = faqColl(nil).DeleteMany(bson.M{"_id": bson.M{"$in": ids}}); err != nil { + log.ZapLog.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FaqDelete", "Faq", "DeleteMany", err)) + return + } + return +} + +// FaqCateList 常见问题列表 +func FaqCateList() (data []CateRes, err error) { + query := []bson.M{bson.M{"$group": bson.M{"_id": "$category"}}} + if err = faqColl(nil).Aggregate(&data, query); err != nil { + log.ZapLog.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FaqAggregate", "Faq", "Aggregate", err)) + return + } + return +} + +func faqCount(filter bson.M) (cnt int64, err error) { + cnt, err = faqColl(nil).Count(filter) + if err != nil { + log.ZapLog.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FqaListForSearch", "Faq", "Find", err)) + return + } + return +} + +// FqaListForSearch 根据搜索条件 获取问题列表 +func FaqListForSearch(category string, keywords string, s commod.StdQuery) (data []Faq, total int64, err error) { + query := bson.M{} + q1 := bson.M{"query": bson.M{"$regex": keywords, "$options": "i"}} + q2 := bson.M{"answer": bson.M{"$regex": keywords, "$options": "i"}} + q3 := bson.M{"category": bson.M{"$regex": keywords, "$options": "i"}} + if category != "" && keywords != "" { + query = bson.M{"$and": []bson.M{bson.M{"category": category}, bson.M{"$or": []bson.M{q1, q2}}}} + } else if keywords != "" { + query = bson.M{"$or": []bson.M{q1, q2, q3}} + } else if category != "" { + query = bson.M{"category": category} + } + total, _ = faqCount(query) + s.Order = &[]commod.OrderBy{{Key: "sortCode", Desc: true}, {Key: "updatedAt", Desc: true}} + if err = faqColl(nil).Find(&data, query, commod.ConvertToListQuery(s)); err != nil { + log.ZapLog.Error(fmt.Sprintf("FaqListForSearch FqaListForSearch Faq Find err: %v", err)) + return + } + return +} + +// FaqList 根据类别条件 获取问题列表 +func FaqList(cate string, status bool, s commod.StdQuery) (data []Faq, total int64, err error) { + query := bson.M{"category": cate, "status": status} + total, _ = faqCount(query) + s.Order = &[]commod.OrderBy{{Key: "sortCode", Desc: true}, {Key: "updatedAt", Desc: true}} + if err = faqColl(nil).Find(&data, query, commod.ConvertToListQuery(s)); err != nil { + log.ZapLog.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FqaListForSearch", "Faq", "Find", err)) + return + } + return +} diff --git a/models/v/faqmod/type.go b/models/v/faqmod/type.go new file mode 100644 index 0000000..5cb909e --- /dev/null +++ b/models/v/faqmod/type.go @@ -0,0 +1,42 @@ +package faqmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Faq Faq 常见问题 +type Faq struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Category string `json:"category" bson:"category" binding:"required"` + Query string `json:"query" bson:"query" binding:"required"` + Answer string `json:"answer" bson:"answer" binding:"required"` + Status bool `json:"status" bson:"status"` //状态 是否开启 true 是 false 否 + SortCode int64 `json:"sortCode" bson:"sortCode"` //排序号 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +// FaqListRes 列表 +type FaqListRes struct { + Total int64 `json:"total" bson:"total"` + List []Faq `json:"list" bson:"list"` +} + +// FaqUpdateDoc Faq 常见问题 +type FaqUpdateDoc struct { + Category *string `json:"category" bson:"category,omitempty"` + Query *string `json:"query" bson:"query,omitempty"` + Answer *string `json:"answer" bson:"answer,omitempty"` + Status *bool `json:"status" bson:"status,omitempty"` //状态 ,是否开启 true 是 false 否 + SortCode *int64 `json:"sortCode" bson:"sortCode,omitempty"` //排序号 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` +} + +func Init() { + mdb = db.Init(table) + initFaqIndex() +} diff --git a/models/v/feedbackmod/feedback.go b/models/v/feedbackmod/feedback.go new file mode 100644 index 0000000..c4a9b63 --- /dev/null +++ b/models/v/feedbackmod/feedback.go @@ -0,0 +1,127 @@ +package feedbackmod + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.FeedBack + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// FeedBackInsert 新增反馈意见 +func FeedBackInsert(f FeedBack) (code stderr.Code, err error) { + f.CreatedAt = time.Now() + f.UpdatedAt = time.Now() + if _, err = coll(nil).InsertOne(f); err != nil { + code = stderr.ErrDbInsertError + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FeedBackInsert", table, "InsertOne", err)) + return + } + return stderr.Success, nil +} + +// FeedBackUpdate 修改反馈意见 +func FeedBackUpdate(id primitive.ObjectID, doc FeedBackUpdateDoc) (err error) { + doc.UpdatedAt = time.Now() + if _, err = coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{ + "$set": doc, + }); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FeedBackUpdate", table, "UpdateOne", err)) + return + } + return +} + +// FeedBackDelete 批量删除反馈意见 +func FeedBackDelete(ids []primitive.ObjectID) (err error) { + if _, err = coll(nil).DeleteMany(bson.M{"_id": bson.M{"$in": ids}}); err != nil { + log.ZapLog.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FeedBackDelete", table, "DeleteMany", err)) + return + } + return +} + +func GetFeedBackList(size, page uint64, uid uint64) (data []FeedBack, hasNext bool, err error) { + var feedBack []FeedBack + std := commod.StdQuery{ + Page: &commod.PageBy{ + CheckNext: true, + Num: page, + Size: size, + }, + Order: &[]commod.OrderBy{{Key: "_id", Desc: true}}, + } + if err = coll(nil).Find(&feedBack, bson.M{"uid": uid}, commod.ConvertToListQuery(std)); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "List", table, "Find", err)) + return + } + + if uint64(len(feedBack)) > size { + hasNext = true + feedBack = feedBack[:size] + } + + return feedBack, hasNext, nil +} + +// FeedBackList 根据类别条件 获反馈列表 +func FeedBackList(param ListParam) (data WebResp, err error) { + var feedBack []FeedBack + std := commod.StdQuery{ + Page: &commod.PageBy{ + CheckNext: false, + Num: param.PageNumber, + Size: param.PageSize, + }, + Order: &[]commod.OrderBy{{Key: "updatedAt", Desc: true}}, + } + query, _ := common.ToBsonM(param.ListFilter) + if param.Content != "" { + query["content"] = bson.M{"$regex": param.Content, "$options": "i"} + } + total, err := coll(nil).Count(query) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "List", table, "Count", err)) + return + } + if err = coll(nil).Find(&feedBack, query, commod.ConvertToListQuery(std)); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "List", table, "Find", err)) + return + } + data.List = feedBack + data.Total = total + return data, nil +} diff --git a/models/v/feedbackmod/type.go b/models/v/feedbackmod/type.go new file mode 100644 index 0000000..519747c --- /dev/null +++ b/models/v/feedbackmod/type.go @@ -0,0 +1,43 @@ +package feedbackmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 反馈意见 +type FeedBack struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + UID uint64 `json:"uid" bson:"uid" binding:"required"` + Version string `json:"version" bson:"version"` + DevType string `json:"devType" bson:"devType"` + Content ContentInfo `json:"content" bson:"content" binding:"required"` + Replay string `json:"replay" bson:"replay"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +// ContentInfo 反馈内容组成部分 +type ContentInfo struct { + CateInfo string `json:"cateInfo" bson:"cateInfo"` // 问题分类 + Desc string `json:"desc" bson:"desc"` // 描述 + Region string `json:"region" bson:"region"` // 地区 + DevInfo string `json:"devInfo" bson:"devInfo"` // 设备信息 + ISP string `json:"ISP" bson:"ISP"` // 运营商 + Contact string `json:"contact"` // 联系方式 + Images []string `json:"images"` // 图片信息 +} + +// FeedBackUpdateDoc 反馈意见编辑 +type FeedBackUpdateDoc struct { + Replay *string `json:"replay" bson:"replay,omitempty"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/feedbackmod/web.go b/models/v/feedbackmod/web.go new file mode 100644 index 0000000..7d2fc79 --- /dev/null +++ b/models/v/feedbackmod/web.go @@ -0,0 +1,18 @@ +package feedbackmod + +import "91porn-server/models/commod" + +type ListParam struct { + ListFilter + commod.Page +} + +type ListFilter struct { + UID uint64 `form:"uid,omitempty" json:"uid,omitempty" bson:"uid,omitempty"` + Content string `form:"content,omitempty" json:"content,omitempty" bson:"content,omitempty"` +} + +type WebResp struct { + Total int64 `json:"total"` + List interface{} `json:"list"` +} diff --git a/models/v/fictionmod/es.go b/models/v/fictionmod/es.go new file mode 100644 index 0000000..bd99557 --- /dev/null +++ b/models/v/fictionmod/es.go @@ -0,0 +1,69 @@ +package fictionmod + +import ( + "fmt" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" +) + +var es *elastic.Client + +const ESTable = models.ESInfoFictionTable + +func InitESIndex() { + es = elastic.Init() + var setting = elastic.M{ + "settings": elastic.M{ + "number_of_shards": elastic.NumberOfShards, + "number_of_replicas": elastic.NumberOfReplicas, + "analysis": elastic.M{ + "analyzer": elastic.M{ + "ik": elastic.M{ + "tokenizer": elastic.AnalyzerIkMaxWord, + }, + }, + }, + }, + "mappings": elastic.M{ + "properties": elastic.M{ + "title": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkMaxWord, + "search_analyzer": elastic.AnalyzerIkSmart, + }, + }, + }, + } + if err := es.CreateIndices(ESTable, setting); err != nil { + panic(fmt.Sprintf("%s index indeices err ==>[%+v]", ESTable, err)) + } +} + +func Search(keywords string, from int64, size int64) (data []ESUserSource, err error) { + query := elastic.M{ + "query": elastic.M{ + "bool": elastic.M{ + "must": elastic.A{ + {"multi_match": elastic.M{ + "query": keywords, + "fields": []string{"title"}}, + }, + {"term": elastic.M{"isActive": true}}, + }, + }, + }, + "from": from, + "size": size, + } + if err = elastic.Init().Search(ESTable, &data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Search", table, "CommonSearch", err), + log.Any("keywords", keywords), + log.Any("from", from), + log.Any("size", size), + ) + return + } + return +} diff --git a/models/v/fictionmod/fiction.go b/models/v/fictionmod/fiction.go new file mode 100644 index 0000000..fb82b87 --- /dev/null +++ b/models/v/fictionmod/fiction.go @@ -0,0 +1,478 @@ +package fictionmod + +import ( + "encoding/json" + "errors" + "fmt" + "math/rand" + "strconv" + "time" + + "91porn-server/app/appg" + "91porn-server/app/service/searcher" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.Fiction + +// InitIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "number", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "isActive", Value: 1}}, + }, + { + Keys: bson.D{{Key: "fType", Value: 1}}, + }, + { + Keys: bson.D{{Key: "title", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func GetByID(id primitive.ObjectID) (f FictionAppRes, err error) { + redisKey := redisconst.DataCachKey(table, id.Hex()) + redisc := appg.Redis + if redisc == nil || !redisc.Exists(redisKey) { + if err = coll(nil).FindOne(&f, bson.M{"_id": id}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByID", table, "FindOne", err), log.Any("id", id.Hex())) + return + } + if redisc == nil { + return + } + var jsonBytes []byte + jsonBytes, err = json.Marshal(&f) + if err != nil { + return + } + err = redisc.Set(redisKey, string(jsonBytes), redisconst.DataCachExpire) + return + } + str, err := redisc.Get(redisKey) + if err != nil { + return + } + if str == nil { + err = errors.New("redis key is null") + return + } + err = json.Unmarshal([]byte(*str), &f) + return +} + +// todo app +func GetInIds(ids []primitive.ObjectID) (res []FictionAppRes, err error) { + data := make([]FictionAppRes, 0) + f := bson.M{} + if ids != nil { + f["_id"] = bson.M{"$in": ids} + } + if err = coll(nil).Find(&data, f); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInIds", table, "Find", err), log.Any("cond", f)) + return + } + tempM := make(map[primitive.ObjectID]FictionAppRes) + for i := range data { + tempM[data[i].ID] = data[i] + } + res = make([]FictionAppRes, len(ids)) + for i := range ids { + res[i] = tempM[ids[i]] + } + return +} + +func GetByFType(fType string, page commod.Page) (data []FictionBase, hasNext bool, err error) { + data = make([]FictionBase, 0) + skip := int64(page.Skip()) + limit := int64(page.Limit() + 1) + sortWord := "createdAt" + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: sortWord, Value: -1}}, + } + f := bson.M{"isActive": true} + if fType != "" { + f["fType"] = fType + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByFType", table, "Find", err), log.Any("cond", f)) + return + } + if uint64(len(data)) > page.PageSize { + hasNext = true + data = data[:page.PageSize] + } + return +} + +func GetFTypes() (data []interface{}, err error) { + data = make([]interface{}, 0) + redisKey := RedisSetKey + ":fType" + redisc := appg.Redis + if redisc == nil || !redisc.Exists(redisKey) { + data, err = coll(nil).Distinct("fType", bson.M{}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCitys", table, "Distinct", err)) + return nil, err + } + if redisc == nil { + return data, err + } + jsonBytes, err := json.Marshal(data) + if err != nil { + return data, err + } + _ = redisc.Set(redisKey, string(jsonBytes), time.Minute*10) + return data, err + } + str, err := redisc.Get(redisKey) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetFTypes", table, "Find", err)) + return nil, err + } + if str == nil { + return data, errors.New("redis key is null") + } + err = json.Unmarshal([]byte(*str), &data) + return +} + +func GetHots(page commod.Page) (data []FictionBase, hasNext bool, err error) { + data = make([]FictionBase, 0) + skip := int64(page.Skip()) + limit := int64(page.Limit() + 1) + sortWord := "countCollect" + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: sortWord, Value: -1}}, + } + f := bson.M{"isActive": true} + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByFType", table, "Find", err), log.Any("cond", f)) + return + } + if uint64(len(data)) > page.PageSize { + hasNext = true + data = data[:page.PageSize] + } + return +} + +func SearchTitle(title string, page commod.Page) (data []FictionAppRes, hasNext bool, err error) { + data = make([]FictionAppRes, 0) + skip := int64(page.Skip()) + limit := int64(page.Limit() + 1) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: 1}}, + } + f := bson.M{"isActive": true} + f["$or"] = bson.A{ + bson.M{"title": primitive.Regex{ + Pattern: title, + Options: "", + }}, + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "SearchTitle", table, "Find", err), log.Any("cond", f)) + return + } + if uint64(len(data)) > page.PageSize { + hasNext = true + data = data[:page.PageSize] + } + return +} + +func IncCount(id primitive.ObjectID, inc CountInc) (err error) { + res, err := coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$inc": inc}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncCount", table, "UpdateOne", err), log.Any("id", id), log.Any("inc", inc)) + return + } + if res.ModifiedCount <= 0 { + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncCount", table, "UpdateOne", err), log.Any("id", id), log.Any("inc", inc), log.Any("res", res)) + return + } + } + return +} + +func GetNew(opt searcher.Opter) (data []FictionBase, hasNext bool, err error) { + data = make([]FictionBase, 0) + skip := opt.Skip() + limit := opt.Limit() + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + f := bson.M{"isActive": true} + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetNew", table, "Find", err), log.Any("cond", f)) + return + } + if int64(len(data)) > limit { + hasNext = true + data = data[:limit] + } + return +} + +// todo web +// StdFind 通用查询 +func StdFind(q QuerySelector, page commod.Page) (data []Fiction, total int64, err error) { + data = make([]Fiction, 0) + skip := int64(page.Skip()) + limit := int64(page.Limit()) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + } + sortD := bson.D{} + for i := range page.Sort { + sortD = append(sortD, bson.E{Key: page.Sort[i].SortKey, Value: page.Sort[i].SortVal}) + } + sortD = append(sortD, bson.E{Key: "createdAt", Value: -1}) + opts.SetSort(sortD) + f, err := common.ToBsonM(q) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "StdFind", table, "ToBsonM", err), log.Any("cond", f)) + return + } + if q.Title != nil { + f["title"] = primitive.Regex{ + Pattern: *q.Title, + Options: "", + } + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "StdFind", table, "Find", err), log.Any("cond", f)) + return + } + total, err = coll(nil).Count(f) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "StdFind", table, "Count", err), log.Any("cond", f)) + } + return +} + +// IsExisted 查询 +func IsExisted(id primitive.ObjectID) (ok bool, err error) { + ok, err = coll(nil).Exists(bson.M{"_id": id}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsExisted", table, "Exists", err), log.Any("id", id)) + return + } + return +} + +// InsertFiction 插入一条数据 +func InsertFiction(p *Fiction) (err error) { + p.CreatedAt = time.Now() + _, err = coll(nil).InsertOne(p) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertFiction", table, "InsertOne", err)) + } + return +} + +// InsertBulket 插入一条数据 +func InsertBulket(p []FictionUpsert) (err error) { + fmt.Println("------------", len(p)) + now := time.Now() + wm := make([]mongo.WriteModel, len(p)) + for i := range p { + filter := bson.M{"number": p[i].Number} + update := bson.M{"$set": p[i], "$setOnInsert": bson.M{"createdAt": now}} + wm[i] = mongo.NewUpdateOneModel(). + SetFilter(filter). + SetUpdate(update). + SetUpsert(true) + } + ordered := false + opts := options.BulkWriteOptions{ + Ordered: &ordered, + } + if _, err = coll(nil).Bulk(wm, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertMany", table, "InsertMany", err)) + } + return +} + +// InsertFiction 插入一条数据 +func InsertMany(p []Fiction) (err error) { + if _, err = coll(nil).InsertMany(p); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertMany", table, "InsertMany", err)) + } + return +} + +// UpdateFiction 修改Fiction类型 +func UpdateFiction(set *EditSelector) (err error) { + set.UpdatedAt = time.Now() + if _, err = coll(nil).UpdateOne(bson.M{"_id": set.ID}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateFiction", table, "UpdateOne", err), + log.Any("set", set), + ) + } + return +} + +// RemoveFiction 删除Fiction类型 +func RemoveFiction(id string) error { + OID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RemoveFiction", table, "ObjectIDFromHex", err), + log.Any("id", id), + ) + return err + } + if _, err = coll(nil).DeleteOne(bson.M{"_id": OID}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RemoveFiction", table, "DeleteOne", err), + log.Any("OID", OID), + ) + return err + } + return nil +} + +// todo skd +// 获取ES同步数据 +func GetListByUpdateTimeRange(start time.Time, end time.Time) (data []Fiction, err error) { + var query = bson.M{ + "updatedAt": bson.M{"$gte": start, "$lt": end}, + } + if err = coll(nil).Find(&data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetListByUpdateTimeRange", table, "Find", err), + log.Any("start", start), + log.Any("end", end), + ) + return + } + return +} + +func GetRandomPush(size int) (data []FictionBase, err error) { + data = make([]FictionBase, 0) + redisK := "tempSet:" + RedisSetKey + ":randomData" + redisc := appg.Redis + if redisc == nil || !redisc.Exists(redisK) { + f := bson.M{"isActive": true} + var total int64 + total, err = randomCount(f) + if err != nil { + return + } + var limit int64 = 100 + var skip int64 = 0 + if total-limit > limit { + skip = rand.Int63n(total - limit) + } + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetRandomPush", table, "Find", err), log.Any("filter", f)) + return + } + if len(data) < 1 { + return + } + if redisc == nil { + return + } + tempd := make([]string, len(data)) + for i := range data { + var strB []byte + strB, err = json.Marshal(data[i]) + if err != nil { + return + } + tempd[i] = string(strB) + } + if _, err = redisc.SAdd(redisK, tempd); err != nil { + return + } + _, _ = redisc.ExpireKey(redisK, time.Minute*10) + } + tempd, err := redisc.SRandMemberN(redisK, int64(size)) + if len(tempd) < 1 { + return data, errors.New("redis key is null") + } + data = make([]FictionBase, len(tempd)) + for i := range data { + _ = json.Unmarshal([]byte(tempd[i]), &data[i]) + } + return +} + +func randomCount(f bson.M) (count int64, err error) { + redisK := "temp:" + RedisSetKey + ":randomCount" + redisc := appg.Redis + if redisc == nil || !redisc.Exists(redisK) { + count, err = coll(nil).Count(f) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCitys", table, "Distinct", err)) + return + } + if redisc == nil { + return + } + var jsonBytes []byte + jsonBytes, err = json.Marshal(count) + if err != nil { + return + } + _ = redisc.Set(redisK, string(jsonBytes), time.Minute*10) + } + ptrV, err := redisc.Get(redisK) + if err != nil { + return + } + if ptrV != nil { + count, err = strconv.ParseInt(*ptrV, 10, 64) + return + } + return +} + +func GetListByCond(cond bson.M, opts *options.FindOptions) ([]Fiction, error) { + var data []Fiction + if err := coll(nil).Find(&data, cond, opts); err != nil { + return nil, err + } + return data, nil +} diff --git a/models/v/fictionmod/type.go b/models/v/fictionmod/type.go new file mode 100644 index 0000000..3feebb4 --- /dev/null +++ b/models/v/fictionmod/type.go @@ -0,0 +1,97 @@ +package fictionmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +type Fiction struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title,omitempty"` //标题 + FType string `json:"fType" bson:"fType"` //小说类型 + Summary string `json:"summary" bson:"summary"` //简介 + ContentURL string `json:"contentUrl" bson:"contentUrl"` //内容地址 + IsActive bool `json:"isActive" bson:"isActive"` //是否激活 + CountPurchases int64 `json:"countPurchases" bson:"countPurchases"` //购买数 + CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` //浏览数 + Number int64 `json:"number" bson:"number,omitempty"` //编号 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type QuerySelector struct { + ID *primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + Title *string `json:"title,omitempty" bson:"title,omitempty"` //标题 + FType *string `json:"fType,omitempty" bson:"fType,omitempty"` //小说类型 + ContentURL *string `json:"contentUrl,omitempty" bson:"contentUrl,omitempty"` //内容地址 + IsActive *bool `json:"isActive,omitempty" bson:"isActive,omitempty"` //是否激活 + Number *int `json:"number,omitempty" bson:"number,omitempty"` //编号 +} + +type EditSelector struct { + ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + Title *string `json:"title,omitempty" bson:"title,omitempty"` //标题 + FType *string `json:"fType,omitempty" bson:"fType,omitempty"` //小说类型 + Summary *string `json:"summary,omitempty" bson:"summary,omitempty"` //简介 + ContentURL *string `json:"contentUrl,omitempty" bson:"contentUrl,omitempty"` //内容地址 + IsActive *bool `json:"isActive,omitempty" bson:"isActive,omitempty"` //是否激活 + CountBrowse *int64 `json:"countBrowse,omitempty" bson:"countBrowse,omitempty"` //浏览数 + Number *int `json:"number,omitempty" bson:"number,omitempty"` //编号 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //更新时间 +} + +const RedisSetKey = "fiction" + +// todo app +type FictionAppRes struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title,omitempty"` //标题 + FType string `json:"fType" bson:"fType"` //小说类型 + Summary string `json:"summary" bson:"summary"` //简介 + ContentURL string `json:"contentUrl" bson:"contentUrl"` //内容地址 + IsActive bool `json:"isActive" bson:"isActive"` //是否激活 + CountPurchases int64 `json:"countPurchases" bson:"countPurchases"` //购买数 + CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` //浏览数 +} + +type FictionBase struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title,omitempty"` //标题 + FType string `json:"fType" bson:"fType"` //小说类型 + Summary string `json:"summary" bson:"summary"` //简介 + CountPurchases int64 `json:"countPurchases" bson:"countPurchases"` //购买数 + CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` //浏览数 +} + +type CountInc struct { + CountPurchases *int `json:"countPurchases" bson:"countPurchases,omitempty"` //购买数 + CountBrowse *int `json:"countBrowse" bson:"countBrowse,omitempty"` //浏览数 + CountCollect *int `json:"countCollect" bson:"countCollect,omitempty"` //收藏数 +} + +type ESUserSource struct { + ID string `json:"_id"` + Source FictionBase `json:"_source"` +} + +type FictionUpsert struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title,omitempty"` //标题 + FType string `json:"fType" bson:"fType"` //小说类型 + Summary string `json:"summary" bson:"summary"` //简介 + ContentURL string `json:"contentUrl" bson:"contentUrl"` //内容地址 + IsActive bool `json:"isActive" bson:"isActive"` //是否激活 + Number int64 `json:"number" bson:"number,omitempty"` //编号 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 +} diff --git a/models/v/filtermod/filter.go b/models/v/filtermod/filter.go new file mode 100644 index 0000000..5a5e882 --- /dev/null +++ b/models/v/filtermod/filter.go @@ -0,0 +1,107 @@ +package filtermod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.WordsFilter + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initCmtWordFilterIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "word", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func InsertOne(doc WordsFilter) (err error) { + if _, err = coll(nil).InsertOne(doc); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertOne", err), log.Any("doc", doc)) + } + return +} + +func InsertMany(docs []WordsFilter) (err error) { + if _, err = coll(nil).InsertMany(&docs); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertMany", table, "InsertMany", err)) + } + return +} + +// 加载所有的过滤词 +func GetAllFilterWords() (data []WordsFilter, err error) { + if err = coll(nil).Find(&data, bson.M{}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAllFilterWords", table, "Find", err)) + return + } + return +} + +// 获取过滤词 +func GetFilterWordList(content *string, page commod.Page) (total int64, data []WordsFilter, err error) { + var query = bson.M{} + if content != nil { + query["word"] = bson.M{"$regex": fmt.Sprintf("^%s", *content)} + } + var opts = options.Find() + opts.SetSort(bson.D{{Key: "createdAt", Value: -1}}).SetSkip(int64((page.PageNumber - 1) * page.PageSize)).SetLimit(int64(page.PageSize)) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetFilterWordList", table, "Find", err), log.Any("content", content)) + } + total, err = coll(nil).Count(query) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetFilterWordList", table, "Count", err), log.Any("content", content)) + } + return +} + +// 获取最新的过滤词 +func GetLatestFilterWord() (data WordsFilter, err error) { + var opts = options.FindOne() + opts.SetSort(bson.D{{Key: "createdAt", Value: -1}}) + if err = coll(nil).FindOne(&data, bson.M{}, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetLatestFilterWord", table, "FindOne", err)) + } + return +} + +// 根据时间获取过滤词 +func GetFilterWordsByDate(newest time.Time) (data []WordsFilter, err error) { + if err = coll(nil).Find(&data, bson.M{"createdAt": bson.M{"$gt": newest}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetFilterWordsByDate", table, "Find", err), log.Any("newest", newest)) + } + return +} + +// 删除过滤词 +func DeleteFilterWords(ids []primitive.ObjectID) (err error) { + if _, err = coll(nil).DeleteMany(bson.M{"_id": bson.M{"$in": ids}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteFilterWords", table, "DeleteMany", err), log.Any("ids", ids)) + } + return +} diff --git a/models/v/filtermod/type.go b/models/v/filtermod/type.go new file mode 100644 index 0000000..e097202 --- /dev/null +++ b/models/v/filtermod/type.go @@ -0,0 +1,21 @@ +package filtermod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func Init() { + mdb = db.Init(table) + initIndex() +} + +// WordsFilter 文本过滤器 +type WordsFilter struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + Word string `bson:"word"` // 过滤词 + CreatedAt time.Time `bson:"createdAt"` // 创建时间 +} diff --git a/models/v/filtermod/web.go b/models/v/filtermod/web.go new file mode 100644 index 0000000..90f32a3 --- /dev/null +++ b/models/v/filtermod/web.go @@ -0,0 +1,28 @@ +package filtermod + +import ( + "time" + + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WordListResp struct { + ID primitive.ObjectID `json:"id"` + Word string `json:"word"` + CreatedAt time.Time `json:"createdAt"` +} + +type WordListReq struct { + Content *string `form:"content" json:"content" binding:"omitempty"` + Page commod.Page +} + +type WordAddReq struct { + Words []string `json:"words"` +} + +type WordDeleteReq struct { + IDS []primitive.ObjectID `json:"ids"` +} diff --git a/models/v/followmod/app.go b/models/v/followmod/app.go new file mode 100644 index 0000000..f2f1769 --- /dev/null +++ b/models/v/followmod/app.go @@ -0,0 +1,67 @@ +package followmod + +import ( + "91porn-server/models/commod" + "91porn-server/models/v/vidmod" + "time" +) + +// BaseInfo 返回的关注或者粉丝信息 +type BaseInfo struct { //用户id + UID uint64 `json:"uid"` //名字 + Name string `json:"name"` //性别 + Gender string `json:"gender"` //头像 + Portrait string `json:"portrait"` //已禁止登陆 + HasLocked bool `json:"hasLocked"` //已禁言 + HasBanned bool `json:"hasBanned"` //会员等级 + VipLevel int `json:"vipLevel"` //是否vip + IsVip bool `json:"isVip"` //是否逆向关注 + Via string `json:"via"` //粉丝来源 + Fans int64 `json:"fans"` //粉丝数 + Summary *string `json:"summary"` //简介 + SuperUser bool `json:"superUser"` //是否大v + Awards []int `json:"awards"` //用户奖章 + TotalWorks int64 `json:"totalWorks"` //总作品数 + CreatedAt time.Time `json:"createdAt"` //创建时间 + // 其他数据 + HasFollow bool `json:"hasFollow"` // 是否关注 +} + +// ListReq 用户关注、粉丝列表 +type ListReq struct { + PageNumber int `form:"pageNumber" json:"pageNumber"` + PageSize int `form:"pageSize" json:"pageSize"` + UID uint64 `form:"uid" json:"uid"` + IsShort bool `form:"isShort" json:"isShort"` +} + +// UserFollowReq 用户关注动作 +type UserFollowReq struct { + FollowUID uint64 `form:"followUID" json:"followUID"` + IsFollow bool `form:"isFollow" json:"isFollow"` + IsShort bool `form:"isShort" json:"isShort"` +} + +// ListResp 列表返回 +type ListResp struct { + //数据列表 + List []*BaseInfo `json:"list"` + //是否还有下一页 + HasNext bool `json:"hasNext"` +} + +// DynamicsResp 关注用户的动态信息 +type DynamicsResp struct { + //视频列表 + List []*vidmod.VideoInfo `json:"list"` + VInfos []*vidmod.VideoInfo `json:"vInfos"` + HasNext bool `json:"hasNext"` + //总页数 + TotalPages int `json:"totalPages"` +} + +// AppDynamicsListReq 用户关注、粉丝列表 +type AppDynamicsListReq struct { + commod.Page + NewsType string `form:"newsType" json:"newsType"` // SHORT:短视频 SP:长视频 +} diff --git a/models/v/followmod/follow.go b/models/v/followmod/follow.go new file mode 100644 index 0000000..dddf34c --- /dev/null +++ b/models/v/followmod/follow.go @@ -0,0 +1,332 @@ +package followmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +var mdb *db.MongoDB + +const table = models.Follow + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{"followUID", -1}}, + }, + { + Keys: bson.D{{"uid", 1}, {"followUID", 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{"uniq", 1}}, + }, + { + Keys: bson.D{{"createdAt", -1}}, + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } + return +} + +// AddFollow 添加关注 +func AddFollow(uid uint64, followUID uint64, isShort bool) (int64, error) { + set := bson.M{"uniq": Unique(uid, followUID), "createdAt": time.Now()} + if isShort { + set["newsType"] = "SHORT" + } + res, err := coll(nil).UpsertOne( + bson.M{"uid": uid, "followUID": followUID}, + bson.M{"$set": set}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AddFollow", table, "UpsertOne", err), + log.Any("uid", uid), log.Any("followUID", followUID), + ) + } + if res != nil { + return res.UpsertedCount, err + } + return 0, err +} + +// CloseFollow 取消关注 +func CloseFollow(uid uint64, followUID uint64) (int64, error) { + res, err := coll(nil).DeleteOne( + bson.M{"uid": uid, "followUID": followUID}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CloseFollow", table, "DeleteOne", err), + log.Any("uid", uid), log.Any("followUID", followUID), + ) + } + if res != nil { + return res.DeletedCount, err + } + return 0, err +} + +// IsFollow 是否关注 +func IsFollow(uid uint64, followUID uint64) (bool, error) { + f := FollowModel{} + err := coll(nil).FindOne(&f, bson.M{"uid": uid, "followUID": followUID}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsFollow", table, "Count", err), log.Any("uid", uid), log.Any("followUID", followUID)) + return false, err + } + if f.FollowUID == 0 { + return false, err + } + return true, nil +} + +// IsFollowUsers 是否关注这些用户 +func IsFollowUsers(uid uint64, followUIDs []uint64) (map[uint64]bool, error) { + if followUIDs == nil { + followUIDs = []uint64{} + } + m := make(map[uint64]bool) + if uid == 0 || len(followUIDs) == 0 { + return m, nil + } + var infos []FollowModel + err := coll(nil).Find(&infos, bson.M{"uid": uid, "followUID": bson.M{"$in": followUIDs}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsFollowUsers", table, "Find", err), + log.Any("uid", uid), log.Any("followUIDs", followUIDs), + ) + return m, err + } + for _, i := range infos { + m[i.FollowUID] = true + } + return m, nil +} + +// IsFollowedByUsers 是否被这些用户关注 +func IsFollowedByUsers(followUID uint64, uids []uint64) (map[uint64]bool, error) { + if uids == nil { + uids = []uint64{} + } + m := make(map[uint64]bool) + var infos []FollowModel + err := coll(nil).Find(&infos, bson.M{"followUID": followUID, "uid": bson.M{"$in": uids}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsFollowedByUsers", table, "Find", err), + log.Any("followUID", followUID), log.Any("uids", uids), + ) + return m, err + } + for _, i := range infos { + m[i.UID] = true + } + return m, nil +} + +// FollowStatueMap 从给定的hash列表中获取like状态映射:hash->Statue +// hash通过Vector.hash()获取 +func FollowStatueMap(uniqList []string) (map[string]bool, error) { + filter := bson.M{ + "uniq": bson.M{"$in": uniqList}, + } + existLikeList := make([]FollowModel, 0, len(uniqList)) + err := coll(nil).Find(&existLikeList, filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FollowStatueMap", table, "Find", err), + log.Any("uniqList", uniqList), + ) + return nil, err + } + m := make(map[string]bool, len(existLikeList)) + //初始化 + for _, uniq := range uniqList { + m[uniq] = false + } + //已经关注的 + for _, v := range existLikeList { + uniq := v.Uniq + m[uniq] = true + } + return m, nil +} + +// GetFollowCount 获取关注总数 +func GetFollowCount(uid uint64) (int64, error) { + total, err := coll(nil).Count(bson.M{"uid": uid}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsFollowEachOther", table, "Count", err), + log.Any("uid", uid), + ) + return 0, err + } + return total, nil +} + +// GetFansCount 获取粉丝总数 +func GetFansCount(uid uint64) (int64, error) { + total, err := coll(nil).Count(bson.M{"followUID": uid}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetFansCount", table, "Count", err), + log.Any("uid", uid), + ) + return 0, err + } + return total, nil +} + +// GetFollowList 获取关注列表 +func GetFollowList(uid uint64, page int, size int, isShort bool) ([]uint64, map[uint64]FollowModel, bool, error) { + hasNext := false + m := make(map[uint64]FollowModel) + cond := bson.M{"uid": uid} + sort := bson.D{{Key: "createdAt", Value: -1}} + opts := options.FindOptions{} + opts.SetSort(sort).SetSkip(int64((page - 1) * size)).SetLimit(int64(size + 1)) + + if isShort { + cond["newsType"] = "SHORT" + } + + var data []*FollowModel + err := coll(nil).Find(&data, cond, &opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetFollowList", table, "Find", err), + log.Any("uid", uid), log.Any("page", page), log.Any("size", size), + ) + return nil, m, false, err + } + if len(data) > size { + hasNext = true + data = data[:size] + } + var uids []uint64 + for _, d := range data { + if d == nil { + continue + } + uids = append(uids, d.FollowUID) + m[d.FollowUID] = *d + } + return uids, m, hasNext, nil +} + +// GetFansList 获取粉丝列表 +func GetFansList(uid uint64, page int, size int) ([]uint64, map[uint64]FollowModel, bool, error) { + hasNext := false + m := make(map[uint64]FollowModel) + cond := bson.M{"followUID": uid} + sort := bson.D{{Key: "createdAt", Value: -1}} + opts := options.FindOptions{} + opts.SetSort(sort).SetSkip(int64((page - 1) * size)).SetLimit(int64(size + 1)) + var data []*FollowModel + err := coll(nil).Find(&data, cond, &opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetFansList", table, "Find", err), + log.Any("uid", uid), log.Any("page", page), log.Any("size", size), + ) + return nil, m, hasNext, err + } + if len(data) > size { + hasNext = true + data = data[:size] + } + var uids []uint64 + for _, d := range data { + if d == nil { + continue + } + uids = append(uids, d.UID) + m[d.UID] = *d + } + return uids, m, hasNext, nil +} + +// GetAllFansUid 获取所有粉丝用户id +func GetAllFansUid(uid uint64) ([]uint64, error) { + var data []*FollowModel + cond := bson.M{"followUID": uid} + sort := bson.D{{Key: "createdAt", Value: -1}} + opts := options.FindOptions{} + opts.SetSort(sort) + err := coll(nil).Find(&data, cond, &opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAllFansUid", table, "Find", err), + log.Any("uid", uid), + ) + return nil, err + } + var uids []uint64 + for _, d := range data { + uids = append(uids, d.UID) + } + return uids, nil +} + +// GetTotalFollowList 获取所有关注用户 +func GetTotalFollowList(uid uint64) ([]uint64, error) { + var data []*FollowModel + cond := bson.M{"uid": uid} + sort := bson.D{{Key: "createdAt", Value: -1}} + opts := options.FindOptions{} + opts.SetSort(sort).SetLimit(5) + err := coll(nil).Find(&data, cond, &opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetTotalFollowList", table, "Find", err), + log.Any("uid", uid), + ) + return nil, err + } + var uids []uint64 + for _, d := range data { + uids = append(uids, d.FollowUID) + } + return uids, nil +} + +// GetTotalFollowListLimit 获取所有关注用户 +func GetTotalFollowListLimit(uid uint64, limit int64) ([]uint64, error) { + var data []*FollowModel + cond := bson.M{"uid": uid} + sort := bson.D{{Key: "createdAt", Value: -1}} + opts := options.FindOptions{} + opts.SetSort(sort).SetLimit(limit) + err := coll(nil).Find(&data, cond, &opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetTotalFollowListLimit", table, "Find", err), + log.Any("uid", uid), + ) + return nil, err + } + var uids []uint64 + for _, d := range data { + uids = append(uids, d.FollowUID) + } + return uids, nil +} + +// WeekFollowLeaderboard 周关注榜单 +func WeekFollowLeaderboard(bind interface{}, filter bson.M, limit int) error { + opt := options.Aggregate().SetAllowDiskUse(true) + pip := []bson.M{ + {"$match": filter}, // 过滤条件 由外部决定 + {"$group": bson.M{"_id": "$followUID", "count": bson.M{"$sum": 1}}}, // 统计用户被关注 + {"$sort": bson.M{"count": -1}}, // 按照作品数排序 + {"$limit": limit}, // 限制返回条数 + } + return coll(nil).Aggregate(bind, pip, opt) +} diff --git a/models/v/followmod/list.go b/models/v/followmod/list.go new file mode 100644 index 0000000..276b8ad --- /dev/null +++ b/models/v/followmod/list.go @@ -0,0 +1,55 @@ +package followmod + +import ( + "91porn-server/common/pageopt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +type Matcher = pageopt.Matcher + +// CreatedAtGTEMatch +type CreatedAtGTEMatch struct { + GTE *time.Time +} + +func (c *CreatedAtGTEMatch) New() Matcher { + return pageopt.NewGTEMatch("createdAt", c.GTE) +} + +// FollowUIDMatch +type FollowUIDMatch struct { + FollowUID *uint64 +} + +func (f *FollowUIDMatch) New() Matcher { + return pageopt.NewAssignMatch("followUID", f.FollowUID) +} + +// List +func List(sort bson.D, skip, limit int64, matchers ...Matcher) ([]FollowModel, error) { + filter := pageopt.MergeM(matchers) + opt := &options.FindOptions{} + if len(sort) != 0 { + opt.SetSort(sort) + } + opt.SetSkip(skip) + opt.SetLimit(limit) + list := make([]FollowModel, 0, limit) + err := coll(nil).Find(&list, filter, opt) + if err != nil { + return nil, err + } + return list, nil +} + +// Count +func Count(matchers ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchers) + count, err := coll(nil).Count(filter) + if err != nil { + return 0, err + } + return count, nil +} diff --git a/models/v/followmod/type.go b/models/v/followmod/type.go new file mode 100644 index 0000000..f9cf4d6 --- /dev/null +++ b/models/v/followmod/type.go @@ -0,0 +1,46 @@ +package followmod + +import ( + "91porn-server/common/db" + "strconv" + "strings" + "time" +) + +const ( + //最大关注人数 + MaxFollowUsers = 500 + //单日关注上限 + ToDayFollowLimit = 50 +) + +// FollowModel 用户关注列表 +type FollowModel struct { + UID uint64 `json:"uid" bson:"uid"` //关注者 + FollowUID uint64 `json:"followUID" bson:"followUID"` //被关注的uid + Uniq string `bson:"uniq"` //uid.followUID + Via string `json:"via"` //粉丝来源 + NewsType string `json:"newsType" bson:"newsType"` //用户的类型 SHORT:短视频用户 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +// FollowCnt 关注人数统计 +type FollowCnt struct { + ID uint64 `bson:"_id"` + Count int `bson:"count"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +// Unique uid.followUID +func Unique(uid uint64, followUID uint64) string { + list := []string{ + strconv.FormatInt(int64(uid), 10), + strconv.FormatInt(int64(followUID), 10), + } + s := strings.Join(list, ".") + return s +} diff --git a/models/v/freeVidmod/freeVidConfig.go b/models/v/freeVidmod/freeVidConfig.go new file mode 100644 index 0000000..39f3f91 --- /dev/null +++ b/models/v/freeVidmod/freeVidConfig.go @@ -0,0 +1,184 @@ +package freeVidmod + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/models" + "91porn-server/models/commod" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.FreeVidConfig + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "publisherID", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func redisFreeVidKey(uid uint64) string { + return fmt.Sprintf("freeVidConfigList:%d", uid) +} + +// List 条件获取金币配置表 +func List(param ListParam) (data WebResp, err error) { + var freeVidConfig []FreeVidConfig + std := commod.StdQuery{ + Page: &commod.PageBy{ + CheckNext: false, + Num: param.PageNumber, + Size: param.PageSize, + }, + Order: &[]commod.OrderBy{{Key: "updatedAt", Desc: true}}, + } + opts := commod.ConvertToListQuery(std) + query, _ := common.ToBsonM(param.ListFilter) + total, err := coll(nil).Count(query) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "List", table, "Count", err)) + return + } + if err = coll(nil).Find(&freeVidConfig, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "List", table, "Find", err)) + return + } + data.List = freeVidConfig + data.Total = total + return data, nil +} + +// 根据uid查找用户免费观看的发布者列表 +func FindFreeVidByUid(uid uint64) (*FreeVidConfig, error) { + var back *FreeVidConfig + redisKey := redisFreeVidKey(uid) + redisc := getRedis() + if redisc == nil || !redisc.Exists(redisKey) { + if err := coll(nil).FindOne(&back, bson.M{"uid": uid}); err != nil { + log.Warn("recharge FindFreeVidByUid FindOne fail", log.E(err)) + return nil, err + } + if redisc == nil { + return back, nil + } + jsonBytes, err := json.Marshal(back) + if err != nil { + return back, err + } + _ = redisc.Set(redisKey, string(jsonBytes), 10*time.Minute) + return back, err + } + str, err := redisc.Get(redisKey) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindFreeVidByUid", table, "Find", err)) + return nil, err + } + if str == nil { + return back, errors.New("redis key is null") + } + return back, json.Unmarshal([]byte(*str), &back) +} + +func GetFreeVideoMapByUID(uid uint64) map[uint64]bool { + m := make(map[uint64]bool) + free, err := FindFreeVidByUid(uid) + if err != nil || free == nil { + return m + } + now := time.Now() + if free.StartTime.After(now) || free.EndTime.Before(now) { + return m + } + for _, v := range free.PublisherID { + m[v] = true + } + return m +} + +// InsertOne 插入一条数据 +func InsertOne(inc *Incr) error { + redisCachDel(redisFreeVidKey(inc.Uid)) + inc.CreatedAt = time.Now() + inc.UpdatedAt = time.Now() + if _, err := coll(nil).InsertOne(inc); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertOne", err), log.Any("inc", inc)) + return err + } + return nil +} + +// Delete 删除数据 +func Delete(uids []uint64) (int64, error) { + for _, uid := range uids { + redisCachDel(redisFreeVidKey(uid)) + } + result, err := coll(nil).DeleteMany(bson.M{"uid": bson.M{"$in": uids}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Delete", table, "Delete", err)) + return 0, err + } + return result.DeletedCount, err +} + +// Update 免费观看配置更新 +func Update(uid uint64, set EditDoc) (int64, error) { + defer redisCachDel(redisFreeVidKey(uid)) + updateAt := time.Now() + set.UpdatedAt = updateAt + result, err := coll(nil).UpdateOne(bson.M{"uid": uid}, bson.M{"$set": set}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("uid", uid), + log.Any("set", set), + ) + return 0, err + } + return result.ModifiedCount, nil +} + +func redisCachDel(m string) { + _, _ = getRedis().Del(m) +} + +func getRedis() *redis.Client { + if appg.Redis != nil { + return appg.Redis + } + if webg.Redis != nil { + return webg.Redis + } + return nil +} diff --git a/models/v/freeVidmod/type.go b/models/v/freeVidmod/type.go new file mode 100644 index 0000000..5e54241 --- /dev/null +++ b/models/v/freeVidmod/type.go @@ -0,0 +1,45 @@ +package freeVidmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Status int64 + +// FreeVidConfig 免费观看视频 +type FreeVidConfig struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Uid uint64 `json:"uid" bson:"uid"` + PublisherID []uint64 `json:"publisherID" bson:"publisherID"` + StartTime time.Time `json:"startTime" bson:"startTime,omitempty"` + EndTime time.Time `json:"endTime" bson:"endTime,omitempty"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +// Incr 插入结构体 +type Incr struct { + Uid uint64 `form:"uid" json:"uid" bson:"uid" binding:"required"` + PublisherID []uint64 `form:"publisherID" json:"publisherID" bson:"publisherID" binding:"required"` + StartTime time.Time `form:"startTime" json:"startTime" bson:"startTime,omitempty"` + EndTime time.Time `form:"endTime" json:"endTime" bson:"endTime,omitempty"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +// EditDoc 编辑结构体 +type EditDoc struct { + PublisherID []uint64 `json:"publisherID,omitempty" bson:"publisherID,omitempty"` + StartTime *time.Time `json:"startTime,omitempty" bson:"startTime,omitempty"` + EndTime *time.Time `json:"endTime,omitempty" bson:"endTime,omitempty"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/freeVidmod/web.go b/models/v/freeVidmod/web.go new file mode 100644 index 0000000..ca5b2b0 --- /dev/null +++ b/models/v/freeVidmod/web.go @@ -0,0 +1,17 @@ +package freeVidmod + +import "91porn-server/models/commod" + +type ListParam struct { + ListFilter + commod.Page +} + +type ListFilter struct { + UID uint64 `form:"uid" json:"uid,omitempty" bson:"uid,omitempty"` //用户ID +} + +type WebResp struct { + Total int64 `json:"total"` + List interface{} `json:"list"` +} diff --git a/models/v/fsuidmod/fsuid.go b/models/v/fsuidmod/fsuid.go new file mode 100644 index 0000000..5c0a92d --- /dev/null +++ b/models/v/fsuidmod/fsuid.go @@ -0,0 +1,70 @@ +package fsuidmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.Fsid2UID + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "fsid", Value: 1}, {Key: "uid", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// InsertFsid2UID 插入一条映射关系 +func InsertFsid2UID(fsid string, uid uint64) error { + record := Fs2UID{ + Fsid: fsid, + UID: uid, + CreatedAt: time.Now(), + } + if _, err := coll(nil).InsertOne(&record); err != nil { + if mongo.IsDuplicateKeyError(err) { + return nil + } + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertFsid2UID", table, "InsertOne", err), + log.Any("uid", uid), log.Any("fsid", fsid)) + return err + } + return nil +} + +// FindFsid2UID 查找条映射关系 +func FindFsid2UID(fsid string) (uint64, error) { + var record Fs2UID + if err := coll(nil).FindOne(&record, bson.M{"fsid": fsid}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertFsid2UID", table, "InsertOne", err), + log.Any("fsid", fsid)) + return 0, err + } + return record.UID, nil +} diff --git a/models/v/fsuidmod/type.go b/models/v/fsuidmod/type.go new file mode 100644 index 0000000..1bb2109 --- /dev/null +++ b/models/v/fsuidmod/type.go @@ -0,0 +1,19 @@ +package fsuidmod + +import ( + "time" + + "91porn-server/common/db" +) + +// Fs2UID 文件服发布者id映射uid +type Fs2UID struct { + Fsid string `bson:"fsid"` + UID uint64 `bson:"uid"` + CreatedAt time.Time `bson:"createdAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/fundtransferlogmod/crud.go b/models/v/fundtransferlogmod/crud.go new file mode 100644 index 0000000..f1d4c5e --- /dev/null +++ b/models/v/fundtransferlogmod/crud.go @@ -0,0 +1,74 @@ +package fundtransferlogmod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + indexes := []mongo.IndexModel{ + { + Keys: bson.D{ + {Key: "uid", Value: 1}, + {Key: "category", Value: 1}, + {Key: "createdAt", Value: -1}, + }, + }, + } + if _, err := coll(nil).CreateIndex(indexes); err != nil { + panic(fmt.Sprintf("%s model set index err ==>%+v", table, err)) + } +} + +func Insert(t *db.MongoTool, record *FundTransferLog) error { + if _, err := coll(t).InsertOne(record); err != nil { + log.Warn("insert fund transfer log failed", log.Any("uid", record.UID), log.E(err)) + return err + } + return nil +} + +func FindLatestByUIDAndCategory(uid uint64, category Category) (*FundTransferLog, error) { + record := &FundTransferLog{} + filter := bson.M{"uid": uid, "category": category} + opts := options.FindOne().SetSort(bson.D{{Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}) + if err := coll(nil).FindOne(record, filter, opts); err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + return nil, err + } + if record.ID.IsZero() { + return nil, nil + } + return record, nil +} + +func FindLatestOutByUIDAndCategory(uid uint64, category Category) (*FundTransferLog, error) { + record := &FundTransferLog{} + filter := bson.M{"uid": uid, "category": category, "fundType": FundTypeOut} + opts := options.FindOne().SetSort(bson.D{{Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}) + if err := coll(nil).FindOne(record, filter, opts); err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + return nil, err + } + if record.ID.IsZero() { + return nil, nil + } + return record, nil +} diff --git a/models/v/fundtransferlogmod/type.go b/models/v/fundtransferlogmod/type.go new file mode 100644 index 0000000..e3a6c4c --- /dev/null +++ b/models/v/fundtransferlogmod/type.go @@ -0,0 +1,44 @@ +package fundtransferlogmod + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const table = models.FundTransferLog + +var mdb *db.MongoDB + +type FundType int + +const ( + FundTypeIn FundType = 1 // 上分 + FundTypeOut FundType = 2 // 下分 +) + +type Category int + +const ( + CategoryAiGirlfriend Category = iota +) + +type FundTransferLog struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + UID uint64 `json:"uid" bson:"uid"` + Category Category `json:"category" bson:"category"` + FundType FundType `json:"fundType" bson:"fundType"` + Amount int64 `json:"amount" bson:"amount"` // 金币 + Balance int64 `json:"balance" bson:"balance"` // 操作后主钱包金币余额 + Remainder float64 `json:"remainder" bson:"remainder,omitempty"` // 不足1金币的人民币余数 + Desc string `json:"desc" bson:"desc"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/goldcfgmod/goldcfg.go b/models/v/goldcfgmod/goldcfg.go new file mode 100644 index 0000000..bdb795c --- /dev/null +++ b/models/v/goldcfgmod/goldcfg.go @@ -0,0 +1,176 @@ +package goldcfgmod + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/models" + "91porn-server/models/commod" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.GoldConf + +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "amount", Value: 1}, {Key: "type", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "type", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +var GoldConfigList = "goldConfigList" + +// InsertOne 插入一条数据 +func InsertOne(g *Incr) error { + defer redisCachDel(GoldConfigList) + g.CreatedAt = time.Now() + g.UpdatedAt = time.Now() + if _, err := coll(nil).InsertOne(g); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertOne", err), log.Any("g", g)) + return err + } + return nil +} + +// Delete 删除数据 +func Delete(ids []primitive.ObjectID) (int64, error) { + defer redisCachDel(GoldConfigList) + result, err := coll(nil).DeleteMany(bson.M{"_id": bson.M{"$in": ids}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Delete", table, "Delete", err)) + return 0, err + } + return result.DeletedCount, nil +} + +// Update 金币配置更新 +func Update(id primitive.ObjectID, set EditDoc) (int64, error) { + defer redisCachDel(GoldConfigList) + updateAt := time.Now() + set.UpdatedAt = &updateAt + result, err := coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("id", id), + log.Any("set", set), + ) + return 0, err + } + return result.ModifiedCount, nil +} + +// GetGoldConfigList 条件获取金币配置表 +func GetGoldConfigList() ([]*DiscountConfig, error) { + var back []*DiscountConfig + redisKey := redisconst.DataCachKey(table, GoldConfigList) + redisc := getRedis() + if redisc == nil || !redisc.Exists(redisKey) { + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "amount", Value: 1}}) + if err := coll(nil).Find(&back, bson.M{}, &opts); err != nil { + log.Warn("recharge GetGoldListByActiveTrue Find fail", log.E(err)) + return nil, err + } + if redisc == nil { + return back, nil + } + jsonBytes, err := json.Marshal(back) + if err != nil { + return back, err + } + _ = redisc.Set(redisKey, string(jsonBytes), redisconst.DataCachExpire) + return back, err + } + str, err := redisc.Get(redisKey) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetGoldConfigList", table, "Find", err)) + return nil, err + } + if str == nil { + return back, errors.New("redis key is null") + } + return back, json.Unmarshal([]byte(*str), &back) +} + +// List 条件获取金币配置表 +func List(param ListParam) (data DisConfRes, err error) { + var discountConfig []DiscountConfig + std := commod.StdQuery{ + Page: &commod.PageBy{ + CheckNext: false, + Num: param.PageNumber, + Size: param.PageSize, + }, + Order: &[]commod.OrderBy{{Key: "updatedAt", Desc: true}}, + } + opts := commod.ConvertToListQuery(std) + query, _ := common.ToBsonM(param.ListFilter) + total, err := coll(nil).Count(query) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "List", table, "Count", err)) + return + } + if err = coll(nil).Find(&discountConfig, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "List", table, "Find", err)) + return + } + data.List = discountConfig + data.Total = total + return data, nil +} + +func redisCachDel(m string) { + redisKey := redisconst.DataCachKey(table, m) + _, _ = getRedis().Del(redisKey) +} + +func getRedis() *redis.Client { + if appg.Redis != nil { + return appg.Redis + } + if webg.Redis != nil { + return webg.Redis + } + return nil +} + +// 根据类型和基础金币额查找对应的优惠信息 +func GetGoldCgfByAmountAndType(ctx context.Context, amount int64, cage string) (discountConfig *DiscountConfig, err error) { + if err = coll(nil).FindOne(&discountConfig, bson.M{"amount": amount, "type": cage}); err != nil { + log.ErrorX(ctx, fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOne", table, "FindOne", err), + log.Any("amount", amount), log.Any("cage", cage)) + } + return +} diff --git a/models/v/goldcfgmod/type.go b/models/v/goldcfgmod/type.go new file mode 100644 index 0000000..809b79a --- /dev/null +++ b/models/v/goldcfgmod/type.go @@ -0,0 +1,46 @@ +package goldcfgmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 充值优惠配置模型 +type DiscountConfig struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + Amount int64 `json:"amount" bson:"amount"` //基础货币额度 + IncrAmount int64 `json:"incrAmount" bson:"incrAmount"` //增加的优惠额度 + IncTax float64 `json:"incTax" bson:"incTax"` //按比率增加额外优惠额 0-1之间 ,如果 incrAmount 与 incrTax 同时存在 以 incrAmount 为准 + Type string `json:"type" bson:"type"` //充值方式 + TypeName string `json:"typeName" bson:"typeName"` //类型名称:支付宝,微信,代充 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //刷新时间 +} + +type EditDoc struct { + Amount *int64 `json:"amount,omitempty" bson:"amount,omitempty"` //基础货币额度 + IncrAmount *int64 `json:"incrAmount,omitempty" bson:"incrAmount,omitempty"` //增加的优惠额度 + IncTax *float64 `json:"incTax,omitempty" bson:"incTax,omitempty"` //按比率增加额外优惠额 0-1之间 ,如果 incrAmount 与 incrTax 同时存在 以 incrAmount 为准 + Type *string `json:"type,omitempty" bson:"type,omitempty"` //充值方式 + TypeName *string `json:"typeName,omitempty" bson:"typeName,omitempty"` //类型名称:支付宝,微信,代充 + UpdatedAt *time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` //刷新时间 +} + +// 插入结构体 +type Incr struct { + Amount int64 `form:"amount" json:"amount" bson:"amount" binding:"required"` + IncrAmount int64 `form:"incrAmount" json:"incrAmount" bson:"incrAmount" binding:"required_without=IncTax"` + IncTax float64 `form:"incTax" json:"incTax" bson:"incTax" binding:"required_without=IncrAmount"` + Type string `form:"type" json:"type" bson:"type" binding:"required"` + TypeName string `form:"typeName" json:"typeName" bson:"typeName" binding:"required"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //刷新时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/goldcfgmod/web.go b/models/v/goldcfgmod/web.go new file mode 100644 index 0000000..6447599 --- /dev/null +++ b/models/v/goldcfgmod/web.go @@ -0,0 +1,20 @@ +package goldcfgmod + +import "91porn-server/models/commod" + +// web 返回模型 +type DisConfRes struct { + Total int64 `json:"total" bson:"total"` + List []DiscountConfig `json:"list" bson:"list"` +} + +type ListParam struct { + ListFilter + commod.Page +} + +type ListFilter struct { + Type string `json:"type,omitempty" bson:"type,omitempty"` //充值方式 + Amount int64 `json:"amount,omitempty" bson:"amount,omitempty"` //基础货币额度 + TypeName string `json:"typeName,omitempty" bson:"typeName,omitempty"` //类型名称:支付宝,微信,代充 +} diff --git a/models/v/goldextramod/goldextramod.go b/models/v/goldextramod/goldextramod.go new file mode 100644 index 0000000..b370f2b --- /dev/null +++ b/models/v/goldextramod/goldextramod.go @@ -0,0 +1,118 @@ +package goldextramod + +import ( + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.GoldExtra // 金币加赠券表 + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +// ActInitIndex 索引设置 +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func InsertGoldExtra(t *db.MongoTool, ge *GoldExtra) error { + _, err := coll(t).InsertOne(ge) + return err +} + +func InsertGoldExtras(t *db.MongoTool, ges *[]GoldExtra) error { + _, err := coll(t).InsertMany(ges) + return err +} + +// 返回所有用户金币加购券 +func GetUserGoldExtra(t *db.MongoTool, uid uint64, skip, limit uint64) ([]GoldExtra, error) { + var ges []GoldExtra + opt := &options.FindOptions{ + Sort: bson.D{{Key: "used", Value: 1}, {Key: "expired", Value: 1}}, // 未使用的排在前列 + } + opt.SetSkip(int64(skip)) + opt.SetLimit(int64(limit)) + if err := coll(t).Find(&ges, bson.M{"uid": uid}, opt); err != nil { + return nil, err + } + return ges, nil +} + +// 返回所有可用的金币加购券 +func GetUserGoldExtraValid(t *db.MongoTool, uid uint64, skip, limit uint64) ([]GoldExtra, error) { + var ges []GoldExtra + opt := &options.FindOptions{ + Sort: bson.D{{Key: "expired", Value: 1}}, + } + opt.SetSkip(int64(skip)) + opt.SetLimit(int64(limit)) + return ges, coll(t).Find(&ges, bson.M{"uid": uid, "used": false, "expired": bson.M{"$gt": time.Now()}}, opt) +} + +// 返回所有已过期金币加购券 +func GetUserGoldExtraExpired(t *db.MongoTool, uid uint64, skip, limit uint64) ([]GoldExtra, error) { + var ges []GoldExtra + opt := &options.FindOptions{ + Sort: bson.D{{Key: "expired", Value: -1}}, + } + opt.SetSkip(int64(skip)) + opt.SetLimit(int64(limit)) + return ges, coll(t).Find(&ges, bson.M{"uid": uid, "expired": bson.M{"$lte": time.Now()}}, opt) +} + +// 返回所有已使用金币加购券 +func GetUserGoldExtraUsed(t *db.MongoTool, uid uint64, skip, limit uint64) ([]GoldExtra, error) { + var ges []GoldExtra + opt := &options.FindOptions{ + // Sort: bson.D{{Key: "expired", Value: -1}}, + } + opt.SetSkip(int64(skip)) + opt.SetLimit(int64(limit)) + return ges, coll(t).Find(&ges, bson.M{"uid": uid, "used": true}, opt) +} + +func GetUserGoldExtraByID(t *db.MongoTool, id primitive.ObjectID) (*GoldExtra, error) { + var ge *GoldExtra + return ge, coll(t).FindOne(&ge, bson.M{"_id": id}) +} + +func UseGoldExtra(t *db.MongoTool, uid uint64, id primitive.ObjectID) error { + now := time.Now() + result, err := coll(t).UpdateOne(bson.M{"_id": id, "uid": uid, "used": false, "expired": bson.M{"$gte": now}}, bson.M{"$set": bson.M{"used": true, "updatedAt": now}}) + if err != nil { + return err + } + if result.ModifiedCount != 1 { + return errors.New("无效的金币加购券") + } + return nil +} diff --git a/models/v/goldextramod/type.go b/models/v/goldextramod/type.go new file mode 100644 index 0000000..5ffe756 --- /dev/null +++ b/models/v/goldextramod/type.go @@ -0,0 +1,20 @@ +package goldextramod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type GoldExtra struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + Uid uint64 `bson:"uid" json:"uid"` + Name string `bson:"name" json:"name"` // "金币加购券" + Amount uint64 `bson:"amount" json:"amount"` // 面值 + Used bool `bson:"used" json:"used"` // 是否已使用 + Expired time.Time `bson:"expired" json:"expired"` // 过期时间 + Desc string `bson:"desc" json:"desc"` // 文字说明 + Source string `bson:"source" json:"source"` // 来源(发放途径) + CreatedAt time.Time `bson:"createdAt" json:"createdAt"` // 创建时间 + UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"` // 更新时间 +} diff --git a/models/v/hevctaskmod/hevctask.go b/models/v/hevctaskmod/hevctask.go new file mode 100644 index 0000000..705dece --- /dev/null +++ b/models/v/hevctaskmod/hevctask.go @@ -0,0 +1,116 @@ +package hevctaskmod + +import ( + "fmt" + "strings" + "time" + "unicode/utf8" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const ( + table = models.HevcTask + maxErrorMsgLen = 2048 +) + +var mdb *db.MongoDB + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + indexes := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "videoId", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "status", Value: 1}, {Key: "updatedAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(indexes); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// Upsert 按视频 ID 写入或更新一条转码任务。 +func Upsert(videoID primitive.ObjectID, videoName, submitURL, fileID, h265URL string, status H265TaskStatus) error { + return UpsertWithError(videoID, videoName, submitURL, fileID, h265URL, status, "") +} + +// UpsertWithError 更新转码任务并记录最近一次错误。 +func UpsertWithError(videoID primitive.ObjectID, videoName, submitURL, fileID, h265URL string, status H265TaskStatus, errorMsg string) error { + now := time.Now() + set := bson.M{ + "videoName": videoName, + "submitUrl": submitURL, + "status": status, + "updatedAt": now, + } + unset := bson.M{} + if fileID != "" { + set["fileId"] = fileID + } + if h265URL != "" { + set["h265Url"] = h265URL + } + errorMsg = truncateErrorMsg(errorMsg) + if errorMsg != "" { + set["errorMsg"] = errorMsg + } else { + unset["errorMsg"] = "" + } + update := bson.M{ + "$set": set, + "$setOnInsert": bson.M{"videoId": videoID, "createdAt": now}, + } + if len(unset) > 0 { + update["$unset"] = unset + } + if _, err := coll(nil).UpsertOne(bson.M{"videoId": videoID}, update); err != nil { + log.Warn("H265 task upsert failed", log.Any("videoId", videoID), log.E(err)) + return err + } + return nil +} + +func truncateErrorMsg(errorMsg string) string { + errorMsg = strings.TrimSpace(errorMsg) + if len(errorMsg) <= maxErrorMsgLen { + return errorMsg + } + cut := maxErrorMsgLen + for cut > 0 && !utf8.ValidString(errorMsg[:cut]) { + cut-- + } + return errorMsg[:cut] +} + +// UpdateStatus 只更新已有任务的状态。 +func UpdateStatus(videoID primitive.ObjectID, status H265TaskStatus) error { + if _, err := coll(nil).UpdateOne(bson.M{"videoId": videoID}, bson.M{"$set": bson.M{ + "status": status, + "updatedAt": time.Now(), + }}); err != nil { + log.Warn("H265 task status update failed", log.Any("videoId", videoID), log.E(err)) + return err + } + return nil +} diff --git a/models/v/hevctaskmod/type.go b/models/v/hevctaskmod/type.go new file mode 100644 index 0000000..ce728d7 --- /dev/null +++ b/models/v/hevctaskmod/type.go @@ -0,0 +1,33 @@ +package hevctaskmod + +import ( + "time" + + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// H265TaskStatus 与 video_info.h265Status 使用相同的状态值。 +type H265TaskStatus = vidmod.H265Status + +const ( + H265TaskStatusFailed H265TaskStatus = vidmod.H265StatusFailed + H265TaskStatusQueued H265TaskStatus = vidmod.H265StatusQueued + H265TaskStatusProcessing H265TaskStatus = vidmod.H265StatusPending + H265TaskStatusSuccess H265TaskStatus = vidmod.H265StatusSuccess +) + +// H265Task 记录视频最近一次 H.265 云转码处理状态。 +type H265Task struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + VideoID primitive.ObjectID `json:"videoId" bson:"videoId"` + VideoName string `json:"videoName" bson:"videoName"` + SubmitURL string `json:"submitUrl" bson:"submitUrl"` + FileID string `json:"fileId" bson:"fileId,omitempty"` + H265Url string `json:"h265Url" bson:"h265Url,omitempty"` + ErrorMsg string `json:"errorMsg" bson:"errorMsg,omitempty"` + Status H265TaskStatus `json:"status" bson:"status"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} diff --git a/models/v/idmod/id.go b/models/v/idmod/id.go new file mode 100644 index 0000000..b08fe2f --- /dev/null +++ b/models/v/idmod/id.go @@ -0,0 +1,110 @@ +package idmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type IDType string + +const ( + IDTypeUID IDType = "uid" + IDTypeMsgID IDType = "msgID" + IDTypeMerchantID IDType = "merchID" + IDTypeQuestionnaireId IDType = "questionnaireId" + IDTypeLouFengNumber IDType = "loufengNumber" + + table = models.IDTable +) + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "type", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// InitNextUID 初始化UID +func initNextID() { + initNextAutoID(IDTypeUID) + initNextAutoID(IDTypeMsgID) + initNextAutoID(IDTypeMerchantID) + initNextAutoID(IDTypeQuestionnaireId) +} + +// GetNextUID 下一个用户的ID +func GetNextUID() (uint64, error) { + return getNextAutoID(IDTypeUID) +} + +// GetNextUID 下一个消息的ID +func GetNextMsgID() (uint64, error) { + return getNextAutoID(IDTypeMsgID) +} + +// GetNextUID 下一个商人的ID +func GetNextMerchantID() (uint64, error) { + return getNextAutoID(IDTypeMerchantID) +} + +// GetNextUID 下一个调查问卷的ID +func GetQuestionnaireId() (uint64, error) { + return getNextAutoID(IDTypeQuestionnaireId) +} + +func initNextAutoID(idType IDType) { + now := time.Now() + if _, err := coll(nil).UpsertOne( + bson.M{"type": idType}, + bson.M{"$setOnInsert": bson.M{"type": idType, "createdAt": now, "updatedAt": now}}); err != nil { + panic(fmt.Sprintf("init auto id err:%+v", err)) + } +} + +func getNextAutoID(idType IDType) (uint64, error) { + u := AutoID{} + if err := coll(nil).FindOneAndUpdate( + &u, + bson.M{"type": idType}, + bson.M{"$inc": bson.M{"autoId": 1}, "$set": bson.M{"updatedAt": time.Now()}}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getNextAutoID", table, "FindOneAndUpdate", err), log.Any("idType", idType)) + return 0, err + } + return u.AID, nil +} + +func GetLouFengNumber(businessDate string) (int, error) { + now := time.Now() + n := AutoNumber{} + if err := coll(nil).FindOneAndUpsert( + &n, + bson.M{"type": IDTypeLouFengNumber}, + bson.M{"$inc": bson.M{"numberMap." + businessDate: 1}, "$set": bson.M{"updatedAt": now}, + "$setOnInsert": bson.M{"type": IDTypeLouFengNumber, "createdAt": now}}, + ); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getLouFengNumber", table, "FindOneAndUpsert", err), log.Any("businessDate", businessDate)) + return 0, err + } + return n.NumberMap[businessDate], nil +} diff --git a/models/v/idmod/type.go b/models/v/idmod/type.go new file mode 100644 index 0000000..6849fbf --- /dev/null +++ b/models/v/idmod/type.go @@ -0,0 +1,34 @@ +package idmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// AutoID 自增ID +type AutoID struct { + ID primitive.ObjectID `bson:"_id"` + Type string `bson:"type"` + AID uint64 `bson:"autoId"` //id 号 + CreatedAt time.Time `bson:"createdAt"` + UpdatedAt time.Time `bson:"updatedAt"` +} + +type AutoNumber struct { + ID primitive.ObjectID `bson:"_id"` + Type string `bson:"type"` + NumberMap map[string]int `bson:"numberMap"` //id 号 + CreatedAt time.Time `bson:"createdAt"` + UpdatedAt time.Time `bson:"updatedAt"` +} + +var mdb *db.MongoDB + +func Init() { + mdb = db.Init(table) + initIndex() + initNextID() +} diff --git a/models/v/imagetopmod/imagetop.go b/models/v/imagetopmod/imagetop.go new file mode 100644 index 0000000..4a22c73 --- /dev/null +++ b/models/v/imagetopmod/imagetop.go @@ -0,0 +1,97 @@ +package imagetopmod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.ImageTop + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "imageId", Value: 1}}, + }, + { + Keys: bson.D{{Key: "recommendEndTime", Value: 1}}, + }, + { + Keys: bson.D{{Key: "newEndTime", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// ThumbsUp 插入幸福广场置顶 +func InsertOne(l *ImageTopModel) (err error) { + if _, err = coll(nil).InsertOne(l); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertOne", err)) + return + } + return +} + +// UpdateImageTop 更新帖子置顶信息 +func UpdateImageTop(id primitive.ObjectID, update bson.M) (res *mongo.UpdateResult, err error) { + return coll(nil).UpdateOne(bson.M{"imageId": id}, bson.M{"$set": update}) +} + +// GetImageTopList 查询置顶帖子列表 +func GetImageTopList(filter primitive.M, opts ...*options.FindOptions) (out []ImageTopModel, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetImageTopList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + } + return +} + +// GetImageTopCount 查询置顶帖子数量 +func GetImageTopCount(filter primitive.M) (count int64, err error) { + if count, err = coll(nil).Count(filter); err != nil { + log.Error("imageTop location GetImageTopCount Count err", log.E(err)) + return + } + return +} + +// GetImageTopById 根据id查询置顶帖子列表 +func GetImageTopById(id primitive.ObjectID) (imgTopInfo ImageTopModel, err error) { + if err = coll(nil).FindOne(&imgTopInfo, bson.M{"imageId": id}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetImageTopById", table, "FindOne", err), + log.Any("find", id), + ) + } + return +} + +// DeleteOne 删除幸福广场置顶 +func DeleteOne(objID primitive.ObjectID) (err error) { + if _, err = coll(nil).DeleteOne(bson.M{"imageId": objID}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteOne", table, "DeleteOne", err), + log.Any("imageId", objID), + ) + return + } + return +} diff --git a/models/v/imagetopmod/type.go b/models/v/imagetopmod/type.go new file mode 100644 index 0000000..59f7c48 --- /dev/null +++ b/models/v/imagetopmod/type.go @@ -0,0 +1,107 @@ +package imagetopmod + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models/commod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func Init() { + mdb = db.Init(table) + initIndex() +} + +// 帖子置顶 +type ImageTopModel struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + ImageId primitive.ObjectID `json:"imageId" bson:"imageId"` //帖子Id + NewSort int32 `json:"newSort" bson:"newSort"` //最新排序值 + RecommendSort int32 `json:"recommendSort" bson:"recommendSort"` //推荐排序值 + NewEndTime *time.Time `json:"newEndTime" bson:"newEndTime"` //最新置顶结束时间 + RecommendEndTime *time.Time `json:"recommendEndTime" bson:"recommendEndTime"` //推荐置顶结束时间 //推荐置顶结束时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 刷新时间 +} + +// 查询帖子列表请求 +type ImageTopListReq struct { + TopType uint `form:"topType" json:"topType"` //1-推荐 2-最新 + commod.Page +} + +type ImageTopListRes struct { + Total int64 `json:"total"` + List []ImageTopResp `json:"list"` +} + +// 查询帖子列表返回 +type ImageTopResp struct { + ID primitive.ObjectID `json:"id" xlsx:"帖子ID"` // 帖子ID + NewsType string `json:"newsType" xlsx:"-"` // 帖子类型, VID,视频帖子,COVER + PubliserID uint64 `json:"-" xlsx:"用户ID"` // 用户ID + Title string `json:"title" xlsx:"视频标题"` // 帖子标题 + SourceID string `json:"sourceID" xlsx:"-"` // 帖子源站ID + SourceURL string `json:"sourceURL" xlsx:"-"` // 帖子源站URL + FileName string `json:"fileName" xlsx:"-"` // 文件名 + PlayTime uint `json:"playTime" xlsx:"总时长"` // 视频播放时长 + Status int `json:"status" xlsx:"-"` // 状态,0 未审核 1通过 2审核失败 3通过并认为免费 默认为0 + Cover string `json:"cover" xlsx:"-"` // 视频封面 + CoverThumb string `json:"coverThumb" xlsx:"-"` // 视频封面缩略图 + SeriesCover []string `json:"seriesCover" xlsx:"-"` // 封面套图 + IsUser bool `json:"isUser" xlsx:"-"` // TBD + PlayCount int `json:"playCount" xlsx:"播放量(真)"` // 视频播放量(真) + LikeCount int `json:"likeCount" xlsx:"点赞量(真)"` // 视频点赞量(真) + CommentCount int `json:"commentCount" xlsx:"评论数(真)"` // 视频评论量(真) + ShareCount int `json:"shareCount" xlsx:"-"` // 视频分享量(真) + FakeLikeCount int `json:"fakeLikeCount" xlsx:"-"` // 点赞假数据 + FakeCommentCount int `json:"fakeCommentCount" xlsx:"-"` // 评论假数据 + FakeShareCount int `json:"fakeShareCount" xlsx:"-"` // 分享假数据 + FakePlayCount int `json:"fakePlayCount" xlsx:"-"` // 播发假数据 + Coins int64 `json:"coins" xlsx:"价格"` // 视频定价金币数 + Resolution string `json:"resolution" xlsx:"-"` // 视频分辨率 + FreeTime int `json:"freeTime" xlsx:"免费时长"` // 视频免费观看时长 + Chosen bool `json:"chosen" xlsx:"-"` // 是否精选 + FreeArea bool `json:"freeArea" xlsx:"-"` // 是否免费专区 + Size int `json:"size" xlsx:"-"` // 文件大小 byte + CreatedAt time.Time `json:"createdAt" xlsx:"上传时间"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" xlsx:"更新时间"` // 更新时间 + Reason string `json:"reason" xlsx:"-"` // 审核失败的理由 + IsTopping bool `json:"isTopping" xlsx:"-"` // TBD + IsRecommend bool `json:"isRecommend" xlsx:"-"` // 力荐 + IsChoosen bool `json:"isChoosen" xlsx:"是否精选"` // 置精 + IsMadou bool `json:"isMadou" xlsx:"-"` // 是否麻豆上传 + RecoWeight int `json:"recoWeight" xlsx:"-"` // 推荐权重 -1,不可推荐 + ReviewAccount string `json:"reviewAccount" xlsx:"-"` // 审核人 + LinkUrl string `json:"linkUrl" xlsx:"-"` // TBD + SortCode int `json:"sortCode" xlsx:"-"` // 排序号 目前只有广告帖子有用 + Rewarded decimal.Decimal `json:"rewarded" xlsx:"-"` // 打赏 + FakeRewarded decimal.Decimal `json:"fakeRewarded" xlsx:"-"` // (假)获得打赏 + LiaoBaTop bool `json:"liaoBaTop" xlsx:"-"` // 是否撩吧置顶 + LiaoBaTopSort int `json:"liaoBaTopSort" xlsx:"-"` // 撩吧置顶排序码 + WorksSort int `json:"worksSort" xlsx:"-"` // TBD + PageViewCount int64 `json:"pageViewCount" xlsx:"展现量"` // 视频页面展示次数 + HitRate float64 `json:"hitRate" xlsx:"点击率"` // 视频点击率 = 页面展示次数 / 视频播放量 + TagsExport string `json:"-" xlsx:"标签"` // 标签,仅用于导出 + ActivityID primitive.ObjectID `json:"activityId" xlsx:"-"` // 参赛视频活动ID + NewSort int32 `json:"newSort" xlsx:"-"` //最新排序值 + RecommendSort int32 `json:"recommendSort" xlsx:"-"` //推荐排序值 + NewEndTime time.Time `json:"newEndTime" xlsx:"-"` //最新置顶结束时间 + RecommendEndTime time.Time `json:"recommendEndTime" xlsx:"-"` //推荐置顶结束时间 +} + +// EditReq 更新贴子置顶列表 +type EditReq struct { + ID primitive.ObjectID `form:"id" json:"id" binding:"required"` //置顶帖子id + NewSort int32 `form:"newSort" json:"newSort" binding:"omitempty"` //最新排序值 + RecommendSort int32 `form:"recommendSort" json:"recommendSort" binding:"omitempty"` //推荐排序值 + NewEndTime time.Time `form:"newEndTime" json:"newEndTime" binding:"omitempty"` //最新过期时间 + RecommendEndTime time.Time `form:"recommendEndTime" json:"recommendEndTime" binding:"omitempty"` //推荐过期时间 +} + +// DeleteReq 删除置顶贴子列表请求 +type DeleteReq struct { + Id primitive.ObjectID `form:"id" json:"id" binding:"required"` +} diff --git a/models/v/imgroupmembermod/app.go b/models/v/imgroupmembermod/app.go new file mode 100755 index 0000000..9263c1a --- /dev/null +++ b/models/v/imgroupmembermod/app.go @@ -0,0 +1,15 @@ +package imgroupmembermod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ImGroupMemberInfo 移动端返回内容 +type ImGroupMemberInfo struct { + ID primitive.ObjectID `json:"id"` // 文档id + Uid uint64 `json:"uid"` // uid + Price int64 `json:"price"` // 进群花费价格 + CreatedAt time.Time `json:"createdAt"` // 创建时间 +} diff --git a/models/v/imgroupmembermod/imgroupmember.go b/models/v/imgroupmembermod/imgroupmember.go new file mode 100755 index 0000000..0e75760 --- /dev/null +++ b/models/v/imgroupmembermod/imgroupmember.go @@ -0,0 +1,154 @@ +package imgroupmembermod + +import ( + "encoding/json" + "errors" + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []ImGroupMember, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// GetAll 查询全部文档 +func GetAll(filter primitive.M, sort bson.D) (out []*ImGroupMember, err error) { + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAll", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// GetInfoByCond 通过条件获取详细信息 +func GetInfoByCond(cond bson.M) (ImGroupMember, error) { + v := ImGroupMember{} + if err := coll(nil).FindOne(&v, cond, options.FindOne().SetSort(bson.D{{Key: "_id", Value: -1}})); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfoByCond", table, "FindOne", err), + log.Any("cond", cond), + ) + return v, err + } + return v, nil +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (ImGroupMember, error) { + v := ImGroupMember{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d ImGroupMember) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} diff --git a/models/v/imgroupmembermod/type.go b/models/v/imgroupmembermod/type.go new file mode 100755 index 0000000..fc55c7a --- /dev/null +++ b/models/v/imgroupmembermod/type.go @@ -0,0 +1,53 @@ +package imgroupmembermod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.ImGroupMember + +type ImGroupMember struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + GroupId int64 `json:"groupId" bson:"groupId"` // 群组id + Uid uint64 `json:"uid" bson:"uid"` // uid + Price int64 `json:"price" bson:"price"` // 进群花费价格 + Status int `json:"status" bson:"status"` // 0-禁言 1-可用 2-已移除 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "_id", Value: 1}}, + }, + { + Keys: bson.D{{Key: "groupId", Value: 1}, {Key: "uid", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} diff --git a/models/v/imgroupmod/app.go b/models/v/imgroupmod/app.go new file mode 100755 index 0000000..6dcef58 --- /dev/null +++ b/models/v/imgroupmod/app.go @@ -0,0 +1,21 @@ +package imgroupmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ImGroupInfo 移动端返回内容 +type ImGroupInfo struct { + ID primitive.ObjectID `json:"id"` // + GroupId int64 `json:"groupId"` // 群id + Name string `json:"name"` // 群组名 + Cover string `json:"cover"` // 封面 + Summary string `json:"summary"` // 简介 + MemberNum int64 `json:"memberNum"` // 成员数量(真实) + FakeMemberNum int64 `json:"fakeMemberNum"` // 成员数量(假的) + Price int64 `json:"price"` // 加入群聊价格 0-免费 + CreatedAt time.Time `json:"createdAt"` // 文档创建时间 + UpdatedAt time.Time `json:"updatedAt"` // 文档更新时间 +} diff --git a/models/v/imgroupmod/imgroup.go b/models/v/imgroupmod/imgroup.go new file mode 100755 index 0000000..4bf419a --- /dev/null +++ b/models/v/imgroupmod/imgroup.go @@ -0,0 +1,149 @@ +package imgroupmod + +import ( + "encoding/json" + "errors" + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []ImGroup, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// GetAll 查询全部文档 +func GetAll(filter primitive.M, sort bson.D) (out []*ImGroup, err error) { + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAll", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (ImGroup, error) { + v := ImGroup{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d ImGroup) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// IncMemberNum 增加成员数量(真实) +func IncMemberNum(t *db.MongoTool, id primitive.ObjectID, num int) error { + filter := bson.M{"_id": id} + _, err := coll(nil).UpdateOne(filter, bson.M{"$inc": bson.M{"memberNum": num}}) + return err +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} diff --git a/models/v/imgroupmod/type.go b/models/v/imgroupmod/type.go new file mode 100755 index 0000000..e57ae52 --- /dev/null +++ b/models/v/imgroupmod/type.go @@ -0,0 +1,58 @@ +package imgroupmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.ImGroup + +type ImGroup struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // + GroupId int64 `json:"groupId" bson:"groupId"` // 群id + Name string `json:"name" bson:"name"` // 群组名 + Cover string `json:"cover" bson:"cover"` // 封面 + Summary string `json:"summary" bson:"summary"` // 简介 + MemberNum int64 `json:"memberNum" bson:"memberNum"` // 成员数量(真实) + FakeMemberNum int64 `json:"fakeMemberNum" bson:"fakeMemberNum"` // 成员数量(假的) + Price int64 `json:"price" bson:"price"` // 加入群聊价格 0-免费 + Status int `json:"status" bson:"status"` // 0-不可用 1-可用 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 文档创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 文档更新时间 +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "_id", Value: 1}}, + }, + { + Keys: bson.D{{Key: "groupId", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} diff --git a/models/v/immessagemod/app.go b/models/v/immessagemod/app.go new file mode 100755 index 0000000..9111701 --- /dev/null +++ b/models/v/immessagemod/app.go @@ -0,0 +1,20 @@ +package immessagemod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ImMessageInfo 移动端返回内容 +type ImMessageInfo struct { + ID primitive.ObjectID `json:"id"` // 文档id + GroupId int64 `json:"groupId"` // 群组id + Uid uint64 `json:"uid"` // 用户id + Name string `json:"name"` //姓名 + Gender string `json:"gender"` //性别 + Portrait string `json:"portrait"` //头像 + Content string `json:"content"` // 消息内容 + Image string `json:"image"` // 图片 + CreatedAt time.Time `json:"createdAt"` // 创建时间 +} diff --git a/models/v/immessagemod/immessage.go b/models/v/immessagemod/immessage.go new file mode 100755 index 0000000..aa6191c --- /dev/null +++ b/models/v/immessagemod/immessage.go @@ -0,0 +1,142 @@ +package immessagemod + +import ( + "encoding/json" + "errors" + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []ImMessage, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// GetAll 查询全部文档 +func GetAll(filter primitive.M, sort bson.D) (out []*ImMessage, err error) { + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAll", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (ImMessage, error) { + v := ImMessage{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d ImMessage) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} diff --git a/models/v/immessagemod/type.go b/models/v/immessagemod/type.go new file mode 100755 index 0000000..1a1e5f0 --- /dev/null +++ b/models/v/immessagemod/type.go @@ -0,0 +1,53 @@ +package immessagemod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.ImMessage + +type ImMessage struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + GroupId int64 `json:"groupId" bson:"groupId"` // 群组id + Uid uint64 `json:"uid" bson:"uid"` // 用户id + Content string `json:"content" bson:"content"` // 消息内容 + Image string `json:"image" bson:"image"` // 图片 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "_id", Value: 1}}, + }, + { + Keys: bson.D{{Key: "groupId", Value: 1}, {Key: "uid", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} diff --git a/models/v/immod/app.go b/models/v/immod/app.go new file mode 100644 index 0000000..d46c71f --- /dev/null +++ b/models/v/immod/app.go @@ -0,0 +1,13 @@ +package immod + +import "time" + +// AppRes app端消息响应 +type MsgAppRes struct { + MsgID uint64 `json:"msgID" bson:"msgID"` //通知ID + Title string `json:"title" bson:"title"` //标题 + Content string `json:"content" bson:"content"` //内容 + Operate Action `json:"operate" bson:"operate"` //操作类型 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} diff --git a/models/v/immod/im.go b/models/v/immod/im.go new file mode 100644 index 0000000..7b009e6 --- /dev/null +++ b/models/v/immod/im.go @@ -0,0 +1,152 @@ +package immod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.Msg + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// ActInitIndex 索引设置 +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "msgID", Value: -1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "toUserId", Value: 1}}, + }, + { + Keys: bson.D{{Key: "content", Value: 1}}, + }, + { + Keys: bson.D{{Key: "operate", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// 保存消息 +func InsertOne(msg Msg) (err error) { + if _, err = coll(nil).InsertOne(msg); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertOne", err)) + return + } + return +} + +// MsgList 消息列表 +func MsgList(uid uint64, stdQuery commod.StdQuery) (msg []MsgAppRes, err error) { + query := bson.M{"$or": bson.A{bson.M{"operate": bson.M{"$in": []Action{System}}}, bson.M{"toUserId": uid}}} + opts := commod.ConvertToListQuery(stdQuery).SetSort(bson.D{{Key: "updatedAt", Value: -1}}) + if err = coll(nil).Find(&msg, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "MsgList", table, "Find", err), log.Any("uid", uid)) + return + } + return +} + +// 根据uid获取个人消息 +func FindMsgAndCountByUID(uid uint64, stdQuery commod.StdQuery) (data []Msg, total int64, err error) { + *stdQuery.Order = append(*stdQuery.Order, commod.OrderBy{Key: "createdAt", Desc: true}) + query := bson.M{"toUserId": uid, "operate": Private} + if err = coll(nil).Find(&data, query, commod.ConvertToListQuery(stdQuery)); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindMsgAndCountByUID", table, "Find", err), log.Any("uid", uid)) + return + } + total, err = coll(nil).Count(query) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindMsgAndCountByUID", table, "Count", err), log.Any("uid", uid)) + return + } + return +} + +// 根据消息行为获取消息 +func FindMsgAndCountByAction(action Action, stdQuery commod.StdQuery) (data []Msg, total int64, err error) { + *stdQuery.Order = append(*stdQuery.Order, commod.OrderBy{Key: "createdAt", Desc: true}) + query := bson.M{"operate": action} + if err = coll(nil).Find(&data, query, commod.ConvertToListQuery(stdQuery)); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindMsgAndCountByAction", table, "Find", err), log.Any("action", action)) + return + } + total, err = coll(nil).Count(query) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindMsgAndCountByAction", table, "Count", err), log.Any("action", action)) + return + } + return +} + +// MsgWebList 消息列表 +func MsgWebList(uid, msgID, operater *uint64, title *string, start, end *time.Time, stdQuery commod.StdQuery) (res WebMsgRes, err error) { + query := bson.M{} + if uid != nil { + query["uid"] = uid + } + if msgID != nil { + query["msgID"] = msgID + } + if operater != nil { + query["operater"] = operater + } + if title != nil { + query["title"] = title + } + if start != nil { + query["created"] = bson.M{"$gte": start} + } + if end != nil { + query["created"] = bson.M{"$lt": end} + } + opts := commod.ConvertToListQuery(stdQuery).SetSort(bson.D{{Key: "updatedAt", Value: -1}}) + total, err := coll(nil).Count(query) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "MsgWebList", table, "Count", err)) + return + } + m := []Msg{} + if err = coll(nil).Find(&m, query, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "MsgWebList", table, "find", err)) + return + } + res.List = m + res.Total = total + return +} + +func DeleteOneByMsgID(msgID uint64) (err error) { + if _, err = coll(nil).DeleteOne(bson.M{"msgID": msgID}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteOneByMsgID", table, "DeleteOne", err), log.Any("msgID", msgID)) + return + } + return +} diff --git a/models/v/immod/type.go b/models/v/immod/type.go new file mode 100644 index 0000000..e2c1d6d --- /dev/null +++ b/models/v/immod/type.go @@ -0,0 +1,94 @@ +package immod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// MsgType 消息类型 +type MsgType int + +const ( + Single MsgType = (1001 + iota) //单条消息 + Batch //批量消息 + Group //群聊消息 +) + +// Action 消息行为 +type Action string + +const ( + BannedUser Action = "BANNED_USER" //封禁用户 + Placard Action = "PLACARD" //公告 + System Action = "SYSTEM" //系统消息 + Private Action = "PRIVATE" //个人消息 +) + +// Message 消息结构体 +type Message struct { + FromUID int64 `json:"fromUID"` //发送用户ID + GroupID int64 `json:"chatID"` //群聊ID + MsgType MsgType `json:"msgType"` //消息类型 + Data string `json:"data"` //消息体 +} + +// SingleMsg 单条消息结构体 +type SingleMsg struct { + ToUserID int64 `json:"toUserId" binding:"required"` //接收用户ID + Content string `json:"content" binding:"required"` //内容 + Operate Action `json:"operate" binding:"required"` //操作类型 +} + +// BatchMsg 批量消息结构体 +type BatchMsg struct { + ToUserIDs []int64 `json:"toUserId"` //接收用户ID数组 + Content string `json:"content" binding:"required"` //内容 + Operate Action `json:"operate" binding:"required"` //操作类型 +} + +// GroupMsg 群聊消息 +type GroupMsg struct { + GroupIDs []int64 `json:"toGroupIds" binding:"required"` //接收用户ID数组 + Content string `json:"content" binding:"required"` //内容 + Operate Action `json:"operate" binding:"required"` //操作类型 +} + +// ImSign 客服聊天签名结构体 +type ImSign struct { + ID string `json:"id" binding:"required"` + AppID string `json:"appId"` + PlatName string `json:"platName"` + UserName string `json:"userName"` + IsVip bool `json:"isVip"` + Avatar string `json:"avatar"` +} + +// 发送消息 +type Msg struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + ToUserID []uint64 `json:"toUserId" bson:"toUserId"` //接收用户ID数组 + MsgID uint64 `json:"msgID" bson:"msgID"` //通知ID + TaskName string `json:"taskName" bson:"taskName"` //任务名称 + Title string `json:"title" bson:"title"` //标题 + Content string `json:"content" bson:"content"` //内容 + Operate Action `json:"operate" bson:"operate"` //操作类型 + Operator string `json:"operator" bson:"operator"` //操作人员 + TaskExecute time.Time `json:"taskExecute" bson:"taskExecute"` //任务开始执行的时间 + DurationAct string `json:"durationAct" bson:"durationAct"` //单位 天/周 + Interval uint64 `json:"interval"` //时间间隔 + Count uint64 `json:"count" bson:"count"` //执行几次 + NextTaskExecute time.Time `json:"nextTaskExecute" bson:"nextTaskExecute"` //下次执行任务的时间 + SpecifyTaskExecutes []time.Time `json:"specifyTaskExecutes" bson:"specifyTaskExecutes"` //指定执行的时间数组 + Status uint64 `json:"status" bson:"status"` //消息状态 + Remark string `json:"remark" bson:"remark"` //备注 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/immod/web.go b/models/v/immod/web.go new file mode 100644 index 0000000..e632c1c --- /dev/null +++ b/models/v/immod/web.go @@ -0,0 +1,7 @@ +package immod + +// WebMsgRes web响应 +type WebMsgRes struct { + Total int64 `json:"total" bson:"total"` + List []Msg `json:"list" bson:"list"` +} diff --git a/models/v/imusermod/im_user.go b/models/v/imusermod/im_user.go new file mode 100644 index 0000000..e426b6d --- /dev/null +++ b/models/v/imusermod/im_user.go @@ -0,0 +1,147 @@ +package imusermod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.ImUser + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "imUserId", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "thirdPartyId", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func FindByUID(uid uint64) (data IMUser, err error) { + err = coll(nil).FindOne(&data, bson.M{"uid": uid}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindByUID", table, "FindOne", err), log.Any("uid", uid)) + } + return +} + +func FindByUIDQuiet(uid uint64) (data IMUser, err error) { + err = coll(nil).FindOne(&data, bson.M{"uid": uid}) + return +} + +func FindByIMUserIDQuiet(imUserID int64) (data IMUser, err error) { + err = coll(nil).FindOne(&data, bson.M{"imUserId": imUserID}) + return +} + +func FindByIMUserIDsQuiet(imUserIDs []int64) (data []IMUser, err error) { + if len(imUserIDs) == 0 { + return []IMUser{}, nil + } + err = coll(nil).Find(&data, bson.M{"imUserId": bson.M{"$in": imUserIDs}}) + return +} + +// FindByUIDsQuiet 批量按 uid 查 imUser 映射。 +func FindByUIDsQuiet(uids []uint64) (data []IMUser, err error) { + if len(uids) == 0 { + return []IMUser{}, nil + } + err = coll(nil).Find(&data, bson.M{"uid": bson.M{"$in": uids}}) + return +} + +// IMUserIDByUID 解析单个 uid → imUserId,不存在返回 0。 +func IMUserIDByUID(uid uint64) int64 { + m, _ := FindByUIDQuiet(uid) + return m.IMUserID +} + +// IMUserIDMapByUIDs 批量解析 uid → imUserId(仅 imUserId>0 入表)。 +func IMUserIDMapByUIDs(uids []uint64) (map[uint64]int64, error) { + list, err := FindByUIDsQuiet(uids) + if err != nil { + return nil, err + } + out := make(map[uint64]int64, len(list)) + for _, m := range list { + if m.IMUserID > 0 { + out[m.UID] = m.IMUserID + } + } + return out, nil +} + +// UIDByIMUserID 解析单个 imUserId → uid,不存在返回 0。 +func UIDByIMUserID(imUserID int64) uint64 { + m, _ := FindByIMUserIDQuiet(imUserID) + return m.UID +} + +// ListAfterUID 按 uid 升序游标分页:返回 uid > lastUID 的前 limit 条。 +// lastUID 传 0 表示从头开始;分页恒定 O(limit),不走 skip。 +// 仅返回 uid 与 imUserId 字段,配合现有 uid_1 索引足够,无需新建覆盖索引。 +func ListAfterUID(lastUID uint64, limit int64) ([]IMUser, error) { + filter := bson.M{} + if lastUID > 0 { + filter["uid"] = bson.M{"$gt": lastUID} + } + opts := options.Find(). + SetSort(bson.D{{Key: "uid", Value: 1}}). + SetLimit(limit). + SetProjection(bson.M{"uid": 1, "imUserId": 1}) + var out []IMUser + if err := coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ListAfterUID", table, "Find", err), log.Any("lastUID", lastUID), log.Any("limit", limit)) + return nil, err + } + return out, nil +} + +func UpsertByUID(uid uint64, imUserID int64, thirdPartyID string) error { + now := time.Now() + update := bson.M{ + "$set": bson.M{ + "imUserId": imUserID, + "thirdPartyId": thirdPartyID, + "updatedAt": now, + }, + "$setOnInsert": bson.M{ + "uid": uid, + "createdAt": now, + }, + } + var data IMUser + err := coll(nil).FindOneAndUpsert(&data, bson.M{"uid": uid}, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpsertByUID", table, "UpdateOne", err), log.Any("uid", uid), log.Any("imUserID", imUserID)) + } + return err +} diff --git a/models/v/imusermod/type.go b/models/v/imusermod/type.go new file mode 100644 index 0000000..0902d9d --- /dev/null +++ b/models/v/imusermod/type.go @@ -0,0 +1,23 @@ +package imusermod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type IMUser struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + UID uint64 `json:"uid" bson:"uid"` + IMUserID int64 `json:"imUserId" bson:"imUserId"` + ThirdPartyID string `json:"thirdPartyId" bson:"thirdPartyId"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} diff --git a/models/v/integralconfigmod/app.go b/models/v/integralconfigmod/app.go new file mode 100644 index 0000000..187e9b0 --- /dev/null +++ b/models/v/integralconfigmod/app.go @@ -0,0 +1,33 @@ +package integralconfigmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type APPIntegralConfig struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 积分配置的唯一id + Name string `json:"name" bson:"name"` // 积分名称 + Duration uint64 `json:"duration" bson:"duration"` // 积分持续天数 + Type int `json:"type" bson:"type"` // 类型 + Desc string `json:"desc" bson:"desc"` // 描述 + Img string `json:"img" bson:"img"` // 图片LOGO + Price int64 `json:"price" bson:"price"` // 积分价格 +} + +type ExchangeIntegralReq struct { + ID string `json:"id" bson:"id" binding:"required"` // 积分配置的唯一id + Name string `json:"name" bson:"name"` // 兑换用户姓名 + Tel string `json:"tel" bson:"tel"` // 兑换用户电话 + Address string `json:"address" bson:"address"` // 兑换用户地址 +} + +type AppIntegralRecord struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 记录id + Integral int64 `json:"integral" bson:"integral"` // 积分数量 + Type int `json:"type" bson:"type"` // 兑换记录类型 + Desc string `json:"desc" bson:"desc"` // 描述 + Name string `json:"name" bson:"name"` // 积分名称 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 兑换时间 +} diff --git a/models/v/integralconfigmod/integralconfig.go b/models/v/integralconfigmod/integralconfig.go new file mode 100644 index 0000000..c4ae4d2 --- /dev/null +++ b/models/v/integralconfigmod/integralconfig.go @@ -0,0 +1,168 @@ +package integralconfigmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "errors" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.IntegralConfig + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{"createdAt", -1}}, + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("user model set index err ==>[%+v]", err)) + } +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func InsertOne(mt *db.MongoTool, cfg IntegralConfig) error { + if _, err := coll(mt).InsertOne(cfg); err != nil { + return err + } + return nil +} + +func FindOneById(id primitive.ObjectID) (IntegralConfig, error) { + cfg := IntegralConfig{} + err := coll(nil).FindOne(&cfg, bson.M{"_id": id}) + if err != nil { + return cfg, err + } + return cfg, err +} + +func FindOneByVId(vid primitive.ObjectID) (IntegralConfig, error) { + cfg := IntegralConfig{} + err := coll(nil).FindOne(&cfg, bson.M{"vidId": vid}) + if err != nil { + return cfg, err + } + return cfg, err +} + +// GetByVidIds 根据vid获取列表 +func GetByVidIds(vidIds []primitive.ObjectID) ([]IntegralConfig, error) { + vidTimeOnlineList := make([]IntegralConfig, 0) + err := coll(nil).Find(&vidTimeOnlineList, bson.M{"vidId": bson.M{"$in": vidIds}}) + if err != nil { + return nil, err + } + return vidTimeOnlineList, nil +} + +// DeleteByIds 删除 +func DeleteByIds(ids []primitive.ObjectID) error { + _, err := coll(nil).DeleteMany(bson.M{"_id": bson.M{"$in": ids}}) + if err != nil { + return err + } + return nil +} + +// DeleteByVids 根据帖子id删除数据 +func DeleteByVids(vids []primitive.ObjectID) error { + _, err := coll(nil).DeleteMany(bson.M{"vidId": bson.M{"$in": vids}}) + if err != nil { + return err + } + return nil +} + +// QueryAllList 分页查询文档 +func QueryAllList(filter primitive.M, opts ...*options.FindOptions) (out []*IntegralConfig, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// Edit 修改文档 +func Edit(filter, update primitive.M) error { + result, err := coll(nil).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Edit", table, "UpdateOne", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + if result.ModifiedCount == 0 { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Edit", table, "result.ModifiedCount", "ModifiedCount is zero"), + log.Any("filter", filter), + log.Any("update", update), + ) + return errors.New("result is null") + } + return nil +} + +// GetVideoList 获取作品列表 +func GetVideoList(skip, limit int64, cond bson.M, opts ...*options.FindOptions) (back []*APPIntegralConfig, total int64, hasNext bool, err error) { + // 获取总条数 + total, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond)) + return + } + if total == 0 { + return + } + err = coll(nil).Find(&back, cond, opts...) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + ) + return + } + if len(back) > int(limit) { + hasNext = true + back = back[:limit] + } + return +} diff --git a/models/v/integralconfigmod/type.go b/models/v/integralconfigmod/type.go new file mode 100644 index 0000000..37b5d13 --- /dev/null +++ b/models/v/integralconfigmod/type.go @@ -0,0 +1,39 @@ +package integralconfigmod + +import ( + "91porn-server/common/db" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var ( + mdb *db.MongoDB +) + +const ( + _ = iota + VIPDate // 1 VIP兑换 + AICount // 2 AI黑科技卷 + GoldCoinBonus // 3 金币加赠券 + GoldWatch // 4 金币观影券 + InKind // 5 实物奖励 + FreeCount // 6 幸运抽奖次数 +) + +// IntegralConfig 积分配置 +type IntegralConfig struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 积分配资的唯一id + Name string `json:"name" bson:"name"` // 积分名称 + Duration uint64 `json:"duration" bson:"duration"` // 积分持续天数 + Type int `json:"type" bson:"type"` // 类型 + Desc string `json:"desc" bson:"desc"` // 描述 + Img string `json:"img" bson:"img"` // 图片LOGO + Price int64 `json:"price" bson:"price"` // 积分价格 + EquivalentPrice int64 `json:"equivalentPrice" bson:"equivalentPrice"` // 积分价值 + SortCode int64 `json:"sortCode" bson:"sortCode"` // 排序 + Status bool `json:"status" bson:"status"` // 积分状态 + UpdateAct string `json:"updateAct" bson:"updateAct"` // 操作账号 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 更新时间 + CreateTime time.Time `json:"createTime" bson:"createTime"` // 创建时间 +} diff --git a/models/v/integralconfigmod/web.go b/models/v/integralconfigmod/web.go new file mode 100644 index 0000000..3336278 --- /dev/null +++ b/models/v/integralconfigmod/web.go @@ -0,0 +1,118 @@ +package integralconfigmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type AddCond struct { + Name string `json:"name" bson:"name"` // 积分名称 + Duration uint64 `json:"duration" bson:"duration"` // 积分持续天数 + Desc string `json:"desc" bson:"desc"` // 描述 + Img string `json:"img" bson:"img"` // 图片LOGO + Price int64 `json:"price" bson:"price"` // 积分价格 + EquivalentPrice int64 `json:"equivalentPrice" bson:"equivalentPrice"` // 积分价值 + Type int `json:"type" bson:"type"` // 类型 + Status bool `json:"status" bson:"status"` // 积分状态 + SortCode int64 `json:"sortCode" bson:"sortCode"` // 排序 +} + +func (receiver *AddCond) Generate(addAct string) IntegralConfig { + now := time.Now() + return IntegralConfig{ + Name: receiver.Name, + Duration: receiver.Duration, + Desc: receiver.Desc, + Price: receiver.Price, + EquivalentPrice: receiver.EquivalentPrice, + Status: receiver.Status, + Type: receiver.Type, + SortCode: receiver.SortCode, + Img: receiver.Img, + UpdateAct: addAct, + UpdateTime: now, + CreateTime: now, + } +} + +type EditCond struct { + ID primitive.ObjectID `json:"id" binding:"required"` // 文档 + Name *string `json:"name" bson:"name"` // 积分名称 + Duration *uint64 `json:"duration" bson:"duration"` // 积分持续天数 + Desc *string `json:"desc" bson:"desc"` // 描述 + Img *string `json:"img" bson:"img"` // 图片LOGO + Price *int64 `json:"price" bson:"price"` // 积分价格 + EquivalentPrice *int64 `json:"equivalentPrice" bson:"equivalentPrice"` // 积分价值 + Type *int `json:"type" bson:"type"` // 类型 + Status *bool `json:"status" bson:"status"` // 积分状态 + SortCode *int64 `json:"sortCode" bson:"sortCode"` // 排序 +} + +func (receiver *EditCond) Filter() bson.M { + return bson.M{"_id": receiver.ID} +} + +func (receiver *EditCond) Update(updateAct string) bson.M { + var update = bson.M{} + if receiver.Name != nil { + update["name"] = receiver.Name + } + if receiver.Duration != nil { + update["duration"] = receiver.Duration + } + if receiver.Desc != nil { + update["desc"] = receiver.Desc + } + if receiver.Price != nil { + update["price"] = receiver.Price + } + if receiver.EquivalentPrice != nil { + update["equivalentPrice"] = receiver.EquivalentPrice + } + if receiver.Status != nil { + update["status"] = receiver.Status + } + if receiver.SortCode != nil { + update["sortCode"] = receiver.SortCode + } + if receiver.Img != nil { + update["img"] = receiver.Img + } + if receiver.Type != nil { + update["type"] = receiver.Type + } + update["updateAct"] = updateAct + update["updateTime"] = time.Now() + update["reviewAt"] = time.Now() + return bson.M{"$set": update} +} + +type QueryAllCond struct { + Page int64 `form:"page" binding:"required"` // 当前页 + Limit int64 `form:"limit" binding:"required"` // 页码 + ID *string `form:"id"` // ID + Status *bool `form:"status"` // 审核状态 0未审核 1 通过 2 拒绝 +} + +type QueryAllRes struct { + List []*IntegralConfig `json:"list"` + Total int64 `json:"total"` +} + +func (receiver *QueryAllCond) Filter() bson.M { + var query = bson.M{} + if receiver.Status != nil { + query["status"] = receiver.Status + } + if receiver.ID != nil { + query["_id"] = receiver.ID + } + return query +} + +func (receiver QueryAllCond) Options() *options.FindOptions { + return options.Find().SetSkip((receiver.Page - 1) * receiver.Limit).SetLimit(receiver.Limit).SetSort(bson.M{"updateTime": -1}) +} diff --git a/models/v/integralexcangemod/app.go b/models/v/integralexcangemod/app.go new file mode 100644 index 0000000..976e6db --- /dev/null +++ b/models/v/integralexcangemod/app.go @@ -0,0 +1 @@ +package integralexcangemod diff --git a/models/v/integralexcangemod/integralexchange.go b/models/v/integralexcangemod/integralexchange.go new file mode 100644 index 0000000..fb0fff1 --- /dev/null +++ b/models/v/integralexcangemod/integralexchange.go @@ -0,0 +1,101 @@ +package integralexcangemod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "errors" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.IntegralExchange + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{"createdAt", -1}}, + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("user model set index err ==>[%+v]", err)) + } +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func InsertOne(mt *db.MongoTool, cfg IntegralExchange) error { + if _, err := coll(mt).InsertOne(cfg); err != nil { + return err + } + return nil +} + +func FindOneById(id primitive.ObjectID) (IntegralExchange, error) { + cfg := IntegralExchange{} + err := coll(nil).FindOne(&cfg, bson.M{"_id": id}) + if err != nil { + return cfg, err + } + return cfg, err +} + +// QueryAllList 分页查询文档 +func QueryAllList(filter primitive.M, opts ...*options.FindOptions) (out []*IntegralExchange, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// Edit 修改文档 +func Edit(filter, update primitive.M) error { + result, err := coll(nil).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Edit", table, "UpdateOne", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + if result.ModifiedCount == 0 { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Edit", table, "result.ModifiedCount", "ModifiedCount is zero"), + log.Any("filter", filter), + log.Any("update", update), + ) + return errors.New("result is null") + } + return nil +} diff --git a/models/v/integralexcangemod/type.go b/models/v/integralexcangemod/type.go new file mode 100644 index 0000000..77b4f9f --- /dev/null +++ b/models/v/integralexcangemod/type.go @@ -0,0 +1,39 @@ +package integralexcangemod + +import ( + "91porn-server/common/db" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var ( + mdb *db.MongoDB +) +var ExchangeType int + +const ( + _ = iota + PENDING // 1 待发货 + PROCCESSING // 2 发货中 + PAID // 3 已发货 + REFUSE // 4 拒绝发货 +) + +// IntegralExchange 积分兑换列表 +type IntegralExchange struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 兑换实物的唯一id + EID primitive.ObjectID `json:"eid" bson:"eid"` // 兑换配置id + UID uint64 `json:"uid" bson:"uid"` // 用户id + Portrait string `json:"portrait" bson:"portrait"` // 用户头像 + Name string `json:"name" bson:"name"` // 兑换用户姓名 + Tel string `json:"tel" bson:"tel"` // 兑换用户电话 + Address string `json:"address" bson:"address"` // 兑换用户地址 + Remark string `json:"remark" bson:"remark"` // 描述 + InKindName string `json:"inKindName" bson:"inKindName"` // 兑换实物名称 + Img string `json:"img" bson:"img"` // 兑换实物图片 + Status int `json:"status" bson:"status"` // 兑换实物状态 + UpdateAct string `json:"updateAct" bson:"updateAct"` // 操作账号 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 更新时间 + CreateTime time.Time `json:"createTime" bson:"createTime"` // 创建时间 +} diff --git a/models/v/integralexcangemod/web.go b/models/v/integralexcangemod/web.go new file mode 100644 index 0000000..effdaa9 --- /dev/null +++ b/models/v/integralexcangemod/web.go @@ -0,0 +1,77 @@ +package integralexcangemod + +import ( + "91porn-server/models/commod" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// RechargeUpdateReq 修改参数 +type RechargeUpdateReq struct { + StatusDesc *string `json:"statusDesc" bson:"statusDesc"` // 状态描述 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 +} + +type RchgQueryReq struct { + ID *string `form:"id" json:"_id,omitempty" bson:"_id"` // 流水id + UID *uint64 `form:"uid" json:"uid,omitempty" bson:"uid"` // 用户id + Status *int `form:"status" json:"status,omitempty" bson:"status"` // 1、进行中 2、生成成功 3、生成失败 4、已经退款 +} + +type WebListRequest struct { + Status *int `form:"status" json:"status" bson:"status"` // 1、待发货 2、发货中 3、已发货 4、拒绝发货 + UID *uint64 `form:"uid" json:"uid,omitempty" bson:"uid"` // 用户id + ID *string `form:"id" json:"_id,omitempty" bson:"_id"` // 流水id + commod.Page +} + +type QueryAllRes struct { + List []*IntegralExchange `json:"list"` + Total int64 `json:"total"` +} + +func (receiver *WebListRequest) Filter() primitive.M { + filter := bson.M{} + if receiver.Status != nil { + filter["status"] = receiver.Status + } + if receiver.UID != nil { + filter["uid"] = receiver.UID + } + if receiver.ID != nil { + id, _ := primitive.ObjectIDFromHex(*receiver.ID) + filter["_id"] = id + } + return filter +} + +func (receiver *WebListRequest) Options() *options.FindOptions { + return options.Find().SetSkip(int64(receiver.Skip())).SetLimit(int64(receiver.Limit() + 1)).SetSort(bson.M{"createTime": -1}) +} + +type EditCond struct { + ID string `json:"id" binding:"required"` // 文档 + Status *int `json:"status" bson:"status"` // 状态 2、发货中 3、已发货 4、拒绝发货 + Remark *string `json:"remark" bson:"remark"` // 拒绝理由 +} + +func (receiver *EditCond) Filter() bson.M { + objID, _ := primitive.ObjectIDFromHex(receiver.ID) + return bson.M{"_id": objID} +} + +func (receiver *EditCond) Update(updateAct string) bson.M { + var update = bson.M{} + if receiver.Status != nil { + update["status"] = receiver.Status + } + if receiver.Remark != nil { + update["remark"] = receiver.Remark + } + update["updateAct"] = updateAct + update["updateTime"] = time.Now() + return bson.M{"$set": update} +} diff --git a/models/v/ipblockmod/ipblock.go b/models/v/ipblockmod/ipblock.go new file mode 100644 index 0000000..ad19ed4 --- /dev/null +++ b/models/v/ipblockmod/ipblock.go @@ -0,0 +1,109 @@ +package ipblockmod + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.IpBlock + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "type", Value: 1}}, + Options: options.Index(), + }, + { + Keys: bson.D{{Key: "ip", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// 添加 +func Insert(l *IPBlock) (err error) { + l.UpdatedAt = time.Now() + l.CreatedAt = time.Now() + if _, err = coll(nil).InsertOne(l); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + return +} + +// 删除 +func DeleteOne(objID primitive.ObjectID) (err error) { + if _, err = coll(nil).DeleteOne(bson.M{"_id": objID}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteOne", table, "DeleteOne", err)) + return + } + return +} + +func DeleteMany(objID []primitive.ObjectID) (err error) { + if _, err = coll(nil).DeleteMany(bson.M{"_id": bson.M{"$in": objID}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteMany", table, "DeleteMany", err), log.Any("objID", objID)) + return + } + return +} + +// Update 修改 +func Update(id primitive.ObjectID, set IPBlockEdit) (err error) { + set.UpdatedAt = time.Now() + update, _ := common.ToBsonM(&set) + if _, err = coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": update}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("id", id), + log.Any("set", set), + ) + } + return +} + +// FindOneByType 根据Type查询ip信息 +func FindOneByType(blockType string) (data IPBlock, err error) { + if err = coll(nil).FindOne(&data, bson.M{"type": blockType}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneByType", table, "FindOne", err), log.Any("blockType", blockType)) + return + } + return +} + +func AggregateForIPArray(blockType string) (webAggre WebAggregateResp, err error) { + if err = coll(nil).AggregateDecode(&webAggre, []bson.M{ + { + "$match": bson.M{"type": blockType}, + }, + { + "$group": bson.M{"_id": bson.M{"blockType": "$type"}, "ips": bson.M{"$addToSet": "$ip"}}, + }}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AggregateForIPArray", table, "FindOne", err), log.Any("blockType", blockType)) + return + } + return +} diff --git a/models/v/ipblockmod/type.go b/models/v/ipblockmod/type.go new file mode 100644 index 0000000..4cc2b91 --- /dev/null +++ b/models/v/ipblockmod/type.go @@ -0,0 +1,29 @@ +package ipblockmod + +import ( + "time" + + "91porn-server/common/constant" + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// BlockAct 封禁行为 +type BlockAct string + +const BannedComment BlockAct = constant.BlockComment //评论 限制评论 + +type IPBlock struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + IP string `json:"ip" bson:"ip" binding:"required"` //ip + Type BlockAct `json:"type" bson:"type" binding:"required"` //block 行为 + Remark string `json:"remark" bson:"remark"` //备注 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/ipblockmod/web.go b/models/v/ipblockmod/web.go new file mode 100644 index 0000000..1f1bb21 --- /dev/null +++ b/models/v/ipblockmod/web.go @@ -0,0 +1,69 @@ +package ipblockmod + +import ( + "time" + + "91porn-server/common/log" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" +) + +type IPBlockEdit struct { + IP *string `form:"ip" json:"ip,omitempty" bson:"ip,omitempty"` //ip + Type *BlockAct `form:"type" json:"type,omitempty" bson:"type,omitempty" binding:"required"` //block 行为 + Remark string `form:"remark" json:"remark"` //备注 + UpdatedAt time.Time `form:"updatedAt" json:"updatedAt" bson:"updatedAt,omitempty"` +} + +type ListParam struct { + Type string `form:"type" json:"type"` + commod.Page +} + +type WebResp struct { + Total int64 `json:"total"` + List interface{} `json:"list"` +} + +type WebAggregateResp struct { + ID struct { + BlockType string `json:"blockType"` + } `json:"_id"` + IPS []string `json:"ips"` +} + +func List(param ListParam) (webResp WebResp, err error) { + stdqer := commod.StdQuery{Page: &commod.PageBy{Size: param.PageSize, Num: param.PageNumber}, + Order: &[]commod.OrderBy{{Key: "updatedAt", Desc: true}}} + filter := bson.M{} + if param.Type != "" { + filter["type"] = param.Type + } + ipblocks, err := find(filter, stdqer) + if err != nil { + log.Error("web ipblock list error", log.E(err), log.Any("param", param)) + return + } + total := count(filter) + webResp.List = ipblocks + webResp.Total = total + return +} + +func find(filter bson.M, stdQuery commod.StdQuery) (data []IPBlock, err error) { + if err = coll(nil).Find(&data, filter, commod.ConvertToListQuery(stdQuery)); err != nil { + log.Error("models ipblock Find error", log.E(err)) + return + } + return +} + +func count(query bson.M) (total int64) { + total, err := coll(nil).Count(query) + if err != nil { + log.Error("models ipblock Count error", log.E(err)) + return + } + return +} diff --git a/models/v/ipwhitemod/ipwhite.go b/models/v/ipwhitemod/ipwhite.go new file mode 100644 index 0000000..b945146 --- /dev/null +++ b/models/v/ipwhitemod/ipwhite.go @@ -0,0 +1,106 @@ +package ipwhitemod + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.IpWhite + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "ip", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// 添加 +func Insert(l *IPWhite) (err error) { + l.UpdatedAt = time.Now() + l.CreatedAt = time.Now() + if _, err = coll(nil).InsertOne(l); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + return +} + +// 删除 +func DeleteOne(objID primitive.ObjectID) (err error) { + if _, err = coll(nil).DeleteOne(bson.M{"_id": objID}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteOne", table, "DeleteOne", err)) + return + } + return +} + +func DeleteMany(objID []primitive.ObjectID) (err error) { + if _, err = coll(nil).DeleteMany(bson.M{"_id": bson.M{"$in": objID}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteMany", table, "DeleteMany", err), log.Any("objID", objID)) + return + } + return +} + +// Update 修改 +func Update(id primitive.ObjectID, set IPWhiteEdit) (err error) { + set.UpdatedAt = time.Now() + update, _ := common.ToBsonM(&set) + if _, err = coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": update}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("id", id), + log.Any("set", set), + ) + } + return +} + +// FindOneByType 根据Type查询ip信息 +func FindOneByIp(ip string) (data IPWhite, err error) { + if err = coll(nil).FindOne(&data, bson.M{"ip": ip}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneByType", table, "FindOne", err), log.Any("blockType", ip)) + return + } + return +} + +func AggregateForIPArray(blockType string) (webAggre WebAggregateResp, err error) { + if err = coll(nil).AggregateDecode(&webAggre, []bson.M{ + { + "$match": bson.M{"type": blockType}, + }, + { + "$group": bson.M{"_id": bson.M{"blockType": "$type"}, "ips": bson.M{"$addToSet": "$ip"}}, + }}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AggregateForIPArray", table, "FindOne", err), log.Any("blockType", blockType)) + return + } + return +} diff --git a/models/v/ipwhitemod/type.go b/models/v/ipwhitemod/type.go new file mode 100644 index 0000000..77b50a1 --- /dev/null +++ b/models/v/ipwhitemod/type.go @@ -0,0 +1,23 @@ +package ipwhitemod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type IPWhite struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + IP string `json:"ip" bson:"ip" binding:"required"` //ip + Remark string `json:"remark" bson:"remark"` //备注 + CreateUser string `json:"createUser" bson:"createUser"` //创建人 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/ipwhitemod/web.go b/models/v/ipwhitemod/web.go new file mode 100644 index 0000000..c54775b --- /dev/null +++ b/models/v/ipwhitemod/web.go @@ -0,0 +1,76 @@ +package ipwhitemod + +import ( + "time" + + "91porn-server/common/log" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" +) + +type IPWhiteEdit struct { + IP *string `form:"ip" json:"ip,omitempty" bson:"ip,omitempty"` //ip + Remark string `form:"remark" json:"remark"` //备注 + UpdatedAt time.Time `form:"updatedAt" json:"updatedAt" bson:"updatedAt,omitempty"` +} + +type ListParam struct { + IP string `form:"ip" json:"ip,omitempty"` + commod.Page +} + +type WebResp struct { + Total int64 `json:"total"` + List interface{} `json:"list"` +} + +type WebAggregateResp struct { + ID struct { + BlockType string `json:"blockType"` + } `json:"_id"` + IPS []string `json:"ips"` +} + +func List(param ListParam) (webResp WebResp, err error) { + stdqer := commod.StdQuery{Page: &commod.PageBy{Size: param.PageSize, Num: param.PageNumber}, + Order: &[]commod.OrderBy{{Key: "updatedAt", Desc: true}}} + filter := bson.M{} + if param.IP != "" { + filter["ip"] = param.IP + } + ipwhites, err := find(filter, stdqer) + total := count(filter) + if err != nil { + log.Error("web ipwhite list error", log.E(err), log.Any("param", param)) + return + } + webResp.List = ipwhites + webResp.Total = total + return +} + +func find(filter bson.M, stdQuery commod.StdQuery) (data []IPWhite, err error) { + if err = coll(nil).Find(&data, filter, commod.ConvertToListQuery(stdQuery)); err != nil { + log.Error("models ipwhite Find error", log.E(err)) + return + } + return +} + +func FindMany(filter bson.M) (data []IPWhite, err error) { + if err = coll(nil).Find(&data, filter); err != nil { + log.Error("models ipwhite Find error", log.E(err)) + return + } + return +} + +func count(query bson.M) (total int64) { + total, err := coll(nil).Count(query) + if err != nil { + log.Error("models ipwhite Count error", log.E(err)) + return + } + return +} diff --git a/models/v/jingangmod/app.go b/models/v/jingangmod/app.go new file mode 100644 index 0000000..ffb00b2 --- /dev/null +++ b/models/v/jingangmod/app.go @@ -0,0 +1,15 @@ +package jingangmod + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type JGModel struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + Name string `json:"name" bson:"name"` // 名称 + Img string `json:"img" bson:"img"` // 图片地址 + Desc string `json:"desc" bson:"desc"` // 说明 + LinkType int64 `json:"link_type" bson:"link_type"` // 链接类型 1 内部链接; 2 外部连接 + LinkUrl string `json:"link_url" bson:"link_url"` // 链接地址 + Mid primitive.ObjectID `json:"mid" bson:"mid"` // 所属模块 +} diff --git a/models/v/jingangmod/jingang.go b/models/v/jingangmod/jingang.go new file mode 100644 index 0000000..9d760da --- /dev/null +++ b/models/v/jingangmod/jingang.go @@ -0,0 +1,124 @@ +package jingangmod + +import ( + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/models" + "91porn-server/web/webg" + "errors" + "fmt" + "github.com/vmihailenco/msgpack/v5" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.Jingang + +var mdb *db.MongoDB + +// InitIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "status", Value: 1}}, + }, + { + Keys: bson.D{{Key: "sort", Value: -1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func GetJGListValid(t *db.MongoTool) ([]*JGModel, error) { + var jms []*JGModel + result, err := appg.Redis.Get(redisconst.JanGangQuCacheKey) + if err != nil { + log.Error(fmt.Sprintf("JanGangQuCacheKey redis获取数据异常:%v", err)) + return jms, err + } + if result == nil { + opts := options.Find().SetSort(bson.D{{Key: "sort", Value: 1}}) + err := coll(t).Find(&jms, bson.M{"status": 1}, opts) + if err == nil && len(jms) > 0 { + if bytes, mErr := msgpack.Marshal(jms); mErr == nil { + common.Go(func() { + appg.Redis.Set(redisconst.JanGangQuCacheKey, bytes, redisconst.JanGangQuCacheExpire) + }) + } + } + + return jms, err + } + + if err = msgpack.Unmarshal([]byte(*result), &jms); err != nil { + log.Error(fmt.Sprintf("JanGangQuCacheKey msgpack Unmarshal err:%v", err)) + return jms, err + } + return jms, err +} + +func AddJingang(t *db.MongoTool, j JingangModel) (*primitive.ObjectID, error) { + ir, err := coll(t).InsertOne(j) + if err != nil { + return nil, err + } + oid, ok := ir.InsertedID.(primitive.ObjectID) + if !ok { + return nil, errors.New("unexpected inserted objectid") + } + return &oid, nil +} + +func EditJingang(t *db.MongoTool, id primitive.ObjectID, update bson.M) error { + _, err := coll(t).UpdateOne(bson.M{"_id": id}, bson.M{"$set": update}) + return err +} + +func DeleteJingang(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteById(id) + return err +} + +func GetAllJingangList(t *db.MongoTool, skip, limit uint64) ([]JingangModel, error) { + var jms []JingangModel + opts := options.Find().SetSort(bson.D{{Key: "sort", Value: 1}}) + if skip > 0 { + opts.SetSkip(int64(skip)) + } + if limit > 0 { + opts.SetLimit(int64(limit)) + } + return jms, coll(t).Find(&jms, bson.M{}, opts) +} + +func GetAllJingangCount(t *db.MongoTool) (int64, error) { + return coll(t).Count(bson.M{}) +} + +func getRedis() *redis.Client { + if appg.Redis != nil { + return appg.Redis + } + return webg.Redis +} diff --git a/models/v/jingangmod/type.go b/models/v/jingangmod/type.go new file mode 100644 index 0000000..faa9952 --- /dev/null +++ b/models/v/jingangmod/type.go @@ -0,0 +1,21 @@ +package jingangmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type JingangModel struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + Mid primitive.ObjectID `json:"mid" bson:"mid"` // 所属模块 + Name string `json:"name" bson:"name"` // 名称 + Img string `json:"img" bson:"img"` // 图片地址 + Desc string `json:"desc" bson:"desc"` // 说明 + LinkType int64 `json:"link_type" bson:"link_type"` // 1 内部链接; 2 外部连接 + LinkUrl string `json:"link_url" bson:"link_url"` // 链接地址 + Sort uint64 `json:"sort" bson:"sort"` // 排序 + Status uint64 `json:"status" bson:"status"` // 1 正常; 2 禁用 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` // 刷新时间 +} diff --git a/models/v/ldycfgmod/ldy.go b/models/v/ldycfgmod/ldy.go new file mode 100644 index 0000000..9f19be3 --- /dev/null +++ b/models/v/ldycfgmod/ldy.go @@ -0,0 +1,40 @@ +package ldycfgmod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +const table = models.LDYConfig + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// FindOne 数据库应只有一条记录 +func FindOne() (cfg LdyCfg) { + _ = coll(nil).FindOne(&cfg, bson.M{}) + return +} + +func UpsertOneByID(id primitive.ObjectID, doc LdyCfgUpdateDoc) error { + if _, err := coll(nil).UpsertOne(bson.M{"_id": id}, bson.M{"$set": doc}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpsertOneByID", table, "UpsertOne", err), + log.Any("id", id), + log.Any("doc", doc), + ) + return err + } + return nil +} diff --git a/models/v/ldycfgmod/type.go b/models/v/ldycfgmod/type.go new file mode 100644 index 0000000..acb0a23 --- /dev/null +++ b/models/v/ldycfgmod/type.go @@ -0,0 +1,23 @@ +package ldycfgmod + +import ( + "91porn-server/common/db" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type LdyCfg struct { + ID primitive.ObjectID `form:"id" json:"id" bson:"_id,omitempty"` + Templet string `form:"templet" json:"templet" bson:"templet"` //落地页模版 + Buttons []commod.LdyButton `form:"buttons" json:"buttons" bson:"buttons"` //按钮列表 +} + +type LdyCfgUpdateDoc struct { + Templet string `form:"templet" json:"templet" bson:"templet"` //落地页模版 + Buttons []commod.LdyButton `form:"buttons" json:"buttons" bson:"buttons"` //按钮列表 +} + +func Init() { + mdb = db.Init(table) +} diff --git a/models/v/likemod/app.go b/models/v/likemod/app.go new file mode 100644 index 0000000..df05e7e --- /dev/null +++ b/models/v/likemod/app.go @@ -0,0 +1,19 @@ +package likemod + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ReqInfo struct { + Type string `form:"type" json:"type" binding:"required"` // 点赞类型: SP:长视频 SHORT:短视频 COVER:图文帖子 PIC:图集帖子 SEED_LINK:种子/黄油帖子 TAG:标签 COMMENT:评论 video:动漫 image:漫画 text:小说 + ObjID primitive.ObjectID `form:"objID" json:"objID" binding:"required"` // 对象id 视频/评论/ACG + TagID primitive.ObjectID `form:"tagID" json:"tagID" binding:"omitempty"` // 标签id,在通过标签获取的视频,并对视频点赞时需要传递该标签id +} + +type DesLikeReq struct { + Type string `form:"type" json:"type" binding:"required"` //点赞类型: SP:长视频 SHORT:短视频 COVER:图文帖子 PIC:图集帖子 SEED_LINK:种子/黄油帖子 TAG:标签 COMMENT:评论 video:动漫 image:漫画 text:小说 + ObjIDs []primitive.ObjectID `form:"objIDs" json:"objIDs" binding:"required"` //对象id数组 +} +type LikeOIDRes struct { + ObjID primitive.ObjectID `bson:"objID"` //点赞对象的ID 如果Type==video 则ObjID为 视频ID, 如果Type==comment ObjID 为评论ID +} diff --git a/models/v/likemod/like.go b/models/v/likemod/like.go new file mode 100644 index 0000000..22de1f8 --- /dev/null +++ b/models/v/likemod/like.go @@ -0,0 +1,304 @@ +package likemod + +import ( + "fmt" + "time" + + "91porn-server/common/constant" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson/primitive" + + "go.mongodb.org/mongo-driver/bson" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.Like + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{"userID", 1}, {"type", 1}, {"objID", 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{"type", 1}}, + }, + { + Keys: bson.D{{"objID", 1}}, + }, + { + Keys: bson.D{{"likedUserID", 1}, {"createdAt", -1}}, + }, + { + Keys: bson.D{{"createdAt", 1}}, + }, + { + Keys: bson.D{{"createdAt", -1}}, + }, + { + Keys: bson.D{{"uniq", 1}}, + Options: options.Index().SetUnique(true), + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// ThumbsUp 点赞 +func InsertOne(l *Like) (err error) { + l.Uniq = Unique(l.UserID, l.Type, l.ObjID) + l.CreatedAt = time.Now() + _, err = coll(nil).InsertOne(l) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertOne", err)) + return + } + return +} + +// InsertMany 批量点赞 +func InsertManyBulk(docs []*Like) (insertCount int64, err error) { + opts := options.BulkWriteOptions{} + opts.SetOrdered(false) + models := make([]mongo.WriteModel, 0) + for _, v := range docs { + models = append(models, mongo.NewInsertOneModel().SetDocument(v)) + } + res, err := coll(nil).Bulk(models, &opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertManyBulk", table, "Bulk", err)) + return + } + insertCount = res.InsertedCount + return +} + +// ThumbsDown 取消点赞 +func DeleteOne(uid uint64, lType string, objID primitive.ObjectID) (err error) { + _, err = coll(nil).DeleteOne(bson.M{"userID": uid, "type": lType, "objID": objID}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteOne", table, "DeleteOne", err), + log.Any("uid", uid), + log.Any("lType", lType), + log.Any("objID", objID), + ) + return + } + return +} + +// ThumbsDownBatch 批量取消点赞 +func DeleteMany(uid uint64, lType string, objID []primitive.ObjectID) (err error) { + _, err = coll(nil).DeleteMany(bson.M{"userID": uid, "type": lType, "objID": bson.M{"$in": objID}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteMany", table, "DeleteOne", err), + log.Any("uid", uid), + log.Any("lType", lType), + log.Any("objID", objID), + ) + return + } + return +} + +// FindLiked 获取单条点赞记录 验证是否已经点过赞 +func FindOneByUIDAndObjID(uid uint64, objID primitive.ObjectID) (data Like, err error) { + err = coll(nil).FindOne(&data, bson.M{"userID": uid, "objID": objID}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneByUIDAndObjID", table, "FindOne", err), + log.Any("uid", uid), + log.Any("objID", objID), + ) + return + } + return +} + +// FindLiked 根据对象id获取点赞信息 +func FindLikesByObjIDS(lType string, uid uint64, CIDs []primitive.ObjectID) (data []Like, err error) { + query := bson.M{"userID": uid, "type": lType, "objID": bson.M{"$in": CIDs}} + err = coll(nil).Find(&data, query) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindLikesByObjIDS", table, "Find", err), + log.Any("uid", uid), + log.Any("lType", lType), + log.Any("CIDs", CIDs), + ) + return + } + return +} + +// FindLiked 根据对象id获取点赞信息 +func FindLikesByTypeAndUID(lType string, uid uint64, pageNumber int, pageSize int) (data []LikeOIDRes, hasNext bool, err error) { + query := bson.M{"userID": uid, "type": lType} + skip := int64(pageSize * (pageNumber - 1)) + limit := int64(pageSize + 1) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{"createdAt", -1}}, + Projection: bson.M{"_id": 0, "objID": 1}, + } + err = coll(nil).Find(&data, query, &opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindLikesByTypeAndUID", table, "Find", err), + log.Any("uid", uid), + log.Any("lType", lType), + ) + return + } + if len(data) > pageSize { + hasNext = true + data = data[:pageSize] + } + return +} + +// 从给定的uniq列表中获取like状态映射:uniq->Statue +// uniq通过uniq()获取 +func LikeStatueMap(uniqList []string) (map[string]bool, error) { + filter := bson.M{ + "uniq": bson.M{"$in": uniqList}, + } + existLikeList := make([]Like, 0, len(uniqList)) + err := coll(nil).Find(&existLikeList, filter) + if err != nil { + return nil, err + } + m := make(map[string]bool, len(existLikeList)) + //初始化 + for _, uniq := range uniqList { + m[uniq] = false + } + //已经点赞的 + for _, v := range existLikeList { + uniq := v.Uniq + m[uniq] = true + } + return m, nil +} + +// LLCount 喜欢总数 +func FindLikeIDSByTypeAndUID(lType string, uid uint64) (like []*SimpleRes, err error) { + opts := options.Find() + opts.SetProjection(bson.M{"objID": 1}) + err = coll(nil).Find(&like, bson.M{"userID": uid, "type": lType}, opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindLikeIDSByTypeAndUID", table, "Find", err), + log.Any("uid", uid), + log.Any("lType", lType), + ) + } + return +} + +// IsLikeVideos 是否点赞批量查询 +func IsLikeVideos(uid uint64, videoIDs []primitive.ObjectID) (map[primitive.ObjectID]bool, error) { + m := make(map[primitive.ObjectID]bool) + videoIDs = uniqueVideoIDs(videoIDs) + if len(videoIDs) == 0 { + return m, nil + } + + var infos []Like + cond := likeVideoStatusFilter(uid, videoIDs) + opts := options.Find().SetProjection(bson.M{"_id": 0, "objID": 1}) + err := coll(nil).Find(&infos, cond, opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsLikeVideos", table, "Find", err), + log.Any("uid", uid), + log.Any("videoIDs", videoIDs), + ) + return m, err + } + for _, i := range infos { + m[i.ObjID] = true + } + return m, nil +} + +func likeVideoStatusFilter(uid uint64, videoIDs []primitive.ObjectID) bson.M { + return bson.M{ + "userID": uid, + "type": bson.M{"$in": []string{ + constant.LikeTypeSP, + constant.LikeTypeShort, + constant.LikeTypeCover, + constant.LikeTypePic, + constant.LikeTypeSEED_LINK, + constant.LikeTypeAiPlaza, + }}, + "objID": bson.M{"$in": videoIDs}, + } +} + +func uniqueVideoIDs(ids []primitive.ObjectID) []primitive.ObjectID { + if len(ids) < 2 { + return ids + } + seen := make(map[primitive.ObjectID]struct{}, len(ids)) + unique := make([]primitive.ObjectID, 0, len(ids)) + for _, id := range ids { + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + unique = append(unique, id) + } + return unique +} + +func ListByLikedUserID(uid uint64, skip, limit int64) ([]Like, error) { + likedUserID := LikedUserIDMatch{&uid} + sort := bson.D{{"createdAt", -1}} + return List(sort, skip, limit, likedUserID.New()) +} + +func GetImageLikeList(uid uint64, skip, limit int64) (out []*Like, err error) { + filter := bson.M{"userID": uid, "type": "image"} + if err = coll(nil).Find(&out, filter, options.Find().SetSkip(skip).SetLimit(limit).SetSort(bson.M{"createdAt": -1})); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetImageLikeList", table, "Find", err), + log.Any("uid", uid), + log.Any("skip", skip), + log.Any("limit", limit), + ) + return nil, err + } + return +} + +// IsLikeByType 是否点赞批量查询 +func IsLikeByType(uid uint64, typ []string, IDS []primitive.ObjectID) (map[primitive.ObjectID]bool, error) { + m := make(map[primitive.ObjectID]bool) + var infos []Like + cond := bson.M{"userID": uid, "type": bson.M{"$in": typ}, "objID": bson.M{"$in": IDS}} + err := coll(nil).Find(&infos, cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsLikeByType", table, "Find", err), + log.Any("uid", uid), + log.Any("IDS", IDS), + ) + return m, err + } + for _, i := range infos { + m[i.ObjID] = true + } + return m, nil +} diff --git a/models/v/likemod/like_video_status_test.go b/models/v/likemod/like_video_status_test.go new file mode 100644 index 0000000..5ae31d2 --- /dev/null +++ b/models/v/likemod/like_video_status_test.go @@ -0,0 +1,68 @@ +package likemod + +import ( + "reflect" + "testing" + + "91porn-server/common/constant" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestLikeVideoStatusFilterUsesCompoundIndexFields(t *testing.T) { + id1 := primitive.NewObjectID() + id2 := primitive.NewObjectID() + ids := uniqueVideoIDs([]primitive.ObjectID{id1, id2, id1}) + + filter := likeVideoStatusFilter(123, ids) + if got := filter["userID"]; got != uint64(123) { + t.Fatalf("userID = %#v, want 123", got) + } + + typeMatch, ok := filter["type"].(bson.M) + if !ok { + t.Fatalf("type filter = %T, want bson.M", filter["type"]) + } + gotTypes, ok := typeMatch["$in"].([]string) + if !ok { + t.Fatalf("type.$in = %T, want []string", typeMatch["$in"]) + } + wantTypes := []string{ + constant.LikeTypeSP, + constant.LikeTypeShort, + constant.LikeTypeCover, + constant.LikeTypePic, + constant.LikeTypeSEED_LINK, + constant.LikeTypeAiPlaza, + } + if !reflect.DeepEqual(gotTypes, wantTypes) { + t.Fatalf("type.$in = %#v, want %#v", gotTypes, wantTypes) + } + + objMatch, ok := filter["objID"].(bson.M) + if !ok { + t.Fatalf("objID filter = %T, want bson.M", filter["objID"]) + } + gotIDs, ok := objMatch["$in"].([]primitive.ObjectID) + if !ok { + t.Fatalf("objID.$in = %T, want []primitive.ObjectID", objMatch["$in"]) + } + if wantIDs := []primitive.ObjectID{id1, id2}; !reflect.DeepEqual(gotIDs, wantIDs) { + t.Fatalf("objID.$in = %#v, want %#v", gotIDs, wantIDs) + } +} + +func TestIsLikeVideosEmptyIDsReturnsBeforeDatabaseAccess(t *testing.T) { + originalDB := mdb + mdb = nil + t.Cleanup(func() { mdb = originalDB }) + + got, err := IsLikeVideos(123, nil) + if err != nil { + t.Fatalf("IsLikeVideos() error = %v", err) + } + if len(got) != 0 { + t.Fatalf("IsLikeVideos() = %#v, want empty map", got) + } +} diff --git a/models/v/likemod/list.go b/models/v/likemod/list.go new file mode 100644 index 0000000..7c80876 --- /dev/null +++ b/models/v/likemod/list.go @@ -0,0 +1,64 @@ +package likemod + +import ( + "91porn-server/common/pageopt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +type Matcher = pageopt.Matcher + +// CreatedAtGTEMatch +type CreatedAtGTEMatch struct { + GTE *time.Time +} + +func (s *CreatedAtGTEMatch) New() Matcher { + return pageopt.NewGTEMatch("createdAt", s.GTE) +} + +// LikedUserIDMatch +type LikedUserIDMatch struct { + LikedUserID *uint64 +} + +func (t *LikedUserIDMatch) New() Matcher { + return pageopt.NewAssignMatch("likedUserID", t.LikedUserID) +} + +// UserIDMatch +type UserIDMatch struct { + UserID *uint64 +} + +func (t *UserIDMatch) New() Matcher { + return pageopt.NewAssignMatch("userID", t.UserID) +} + +// List +func List(sort bson.D, skip, limit int64, matchers ...Matcher) ([]Like, error) { + filter := pageopt.MergeM(matchers) + opt := &options.FindOptions{} + if len(sort) != 0 { + opt.SetSort(sort) + } + opt.SetSkip(skip) + opt.SetLimit(limit) + list := make([]Like, 0, limit) + err := coll(nil).Find(&list, filter, opt) + if err != nil { + return nil, err + } + return list, nil +} + +// Count +func Count(matchers ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchers) + count, err := coll(nil).Count(filter) + if err != nil { + return 0, err + } + return count, nil +} diff --git a/models/v/likemod/type.go b/models/v/likemod/type.go new file mode 100644 index 0000000..9626ba8 --- /dev/null +++ b/models/v/likemod/type.go @@ -0,0 +1,42 @@ +package likemod + +import ( + "91porn-server/common/constant" + "91porn-server/common/db" + "strconv" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func Init() { + mdb = db.Init(table) + initIndex() +} + +// Like 点赞 +type Like struct { + ID primitive.ObjectID `bson:"_id,omitempty"` //点赞信息的唯一id + Type constant.LikeType `bson:"type"` //点赞对象类型 video:对视频点赞 comment:对评论点赞 image:对图片帖子点赞 + ObjID primitive.ObjectID `bson:"objID"` //点赞对象的ID 如果Type==video/image 则ObjID为 视频/图片ID, 如果Type==comment ObjID 为评论ID + TagID primitive.ObjectID `bson:"tagID,omitempty"` //视频标签列表 + UserID uint64 `bson:"userID"` //点赞用户ID + LikedUserID uint64 `bson:"likedUserID"` //被赞用户ID + Uniq string `bson:"uniq"` //uid.objID.type + CreatedAt time.Time `bson:"createdAt"` //点赞时间 +} + +type SimpleRes struct { + ObjID primitive.ObjectID `bson:"objID,omitempty"` //点赞信息的唯一id +} + +func Unique(uid uint64, typ string, objID primitive.ObjectID) string { + list := []string{ + strconv.FormatInt(int64(uid), 10), + objID.Hex(), + typ, + } + s := strings.Join(list, ".") + return s +} diff --git a/models/v/locmod/app.go b/models/v/locmod/app.go new file mode 100644 index 0000000..707f642 --- /dev/null +++ b/models/v/locmod/app.go @@ -0,0 +1,16 @@ +package locmod + +// CityResp 热门城市 +type CityResp struct { + ID string `json:"id" bson:"_id"` + City string `json:"city" bson:"city"` + Province string `json:"province" bson:"province"` + Hot int `json:"hot" bson:"hot"` + Visit int `json:"visit" bson:"visit"` +} + +// LInfoResp 位置响应信息 +type LInfoResp struct { + Location + PlayCount int `json:"visit,omitempty" bson:"visit,omitempty"` //访问人数 +} diff --git a/models/v/locmod/hotCity.go b/models/v/locmod/hotCity.go new file mode 100644 index 0000000..c859f5d --- /dev/null +++ b/models/v/locmod/hotCity.go @@ -0,0 +1,148 @@ +package locmod + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// M M from Map +type M = bson.M + +// ObjectID ObjectID +type ObjectID = primitive.ObjectID + +var mdb *db.MongoDB + +const table = models.HotCity + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndexHot 设置index +func initIndexHot() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "city", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// GetHotCity 获取热门城市 +func GetHotCity() ([]HotCity, error) { + var back []HotCity + if err := coll(nil).Find(&back, bson.M{"enable": true}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetHotCity", table, "Find", err)) + return nil, err + } + return back, nil +} + +// Insert 插入记录 +func Insert(doc HotCityDoc) error { + now := time.Now() + doc.CreatedAt = &now + doc.UpdatedAt = &now + if _, err := coll(nil).InsertOne(doc); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return err + } + return nil +} + +// UpdateTo UpdateTo +func UpdateTo(id ObjectID, doc HotCityDoc) error { + now := time.Now() + doc.UpdatedAt = &now + update, err := common.ToBsonM(doc) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateTo", table, "ToBsonM", err), + log.Any("id", id), + log.Any("doc", doc), + ) + return err + } + result, err := coll(nil).UpdateOne(M{"_id": id}, bson.M{"$set": update}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateTo", table, "UpdateOne", err), + log.Any("id", id), + log.Any("doc", doc), + ) + return err + } + if result.ModifiedCount+result.UpsertedCount != 1 { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateTo", table, "UpdateOne", err), + log.Any("id", id), + log.Any("doc", doc), + log.Any("mCount", result.ModifiedCount), + log.Any("uCount", result.UpsertedCount), + ) + return fmt.Errorf("wrong update count") + } + return nil +} + +// DeleteMany DeleteMany +func DeleteMany(idArray []ObjectID) error { + if idArray == nil { + idArray = []ObjectID{} + } + if _, err := coll(nil).DeleteMany(M{"_id": M{"$in": idArray}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteMany", table, "DeleteMany", err), log.Any("idArray", idArray)) + return err + } + return nil +} + +// HotCityPages 热门城市 +func HotCityPages(sort bson.D, skip int64, limit int64, matchs ...pageopt.Matcher) (HotCityPage, error) { + pipeline := []bson.M{ + {"$match": pageopt.MergeM(matchs)}, + { + "$facet": bson.M{ + "total": bson.A{bson.M{"$count": "total"}}, + "list": bson.A{ + bson.M{"$sort": sort}, + bson.M{"$skip": skip}, + bson.M{"$limit": limit}, + }, + }, + }, + { + "$project": bson.M{ + "total": bson.M{"$arrayElemAt": bson.A{"$total.total", 0}}, + "list": 1, + }, + }, + } + var page HotCityPage + if err := coll(nil).AggregateDecode(&page, pipeline); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "HotCityPages", table, "AggregateDecode", err), + log.Any("sort", sort), + log.Any("matchs", matchs), + ) + return page, err + } + return page, nil +} diff --git a/models/v/locmod/location.go b/models/v/locmod/location.go new file mode 100644 index 0000000..583a3d2 --- /dev/null +++ b/models/v/locmod/location.go @@ -0,0 +1,210 @@ +package locmod + +import ( + "fmt" + "math" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const tableLoc = models.Location + +// initIndex 初始化索引 +func initIndexLoc() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "city", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "visit", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := collLoc(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", tableLoc, err)) + } +} + +func collLoc(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(tableLoc) + } + return t.Coll(tableLoc) +} + +// GetVisit 获取城市的访问人数 +func GetVisit(city string) (int, error) { + var l Location + if err := collLoc(nil).FindOne(&l, bson.M{"city": city}); err != nil { + log.Error("GetVisit error", log.Any("city", city), log.E(err)) + return 0, err + } + return l.Visit, nil +} + +// IncVisit 增加城市的访问人数 +func IncVisit(id primitive.ObjectID) error { + if _, err := collLoc(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$inc": bson.M{"visit": 1, "fakeVisit": 1}}); err != nil { + log.Error("IncVisit error", log.Any("id", id), log.E(err)) + return err + } + return nil +} + +// GetLocationInfoByID 获取城市信息 +func GetLocationInfoByID(id primitive.ObjectID) (Location, error) { + var l Location + if err := collLoc(nil).FindOne(&l, bson.M{"_id": id}); err != nil { + log.Error("GetLocationInfoByID error", log.Any("id", id), log.E(err)) + return l, err + } + return l, nil +} + +// GetLocationInfoByIDs 获取城市信息 +func GetLocationInfoByIDs(ids []primitive.ObjectID) (map[primitive.ObjectID]Location, error) { + if ids == nil { + ids = []primitive.ObjectID{} + } + m := make(map[primitive.ObjectID]Location, len(ids)) + infos := make([]Location, 0, len(m)) + if err := collLoc(nil).Find(&infos, bson.M{"_id": bson.M{"$in": ids}}); err != nil { + log.Error("GetLocationInfoByIDs error", log.Any("ids", ids), log.E(err)) + return m, err + } + for _, i := range infos { + m[i.ID] = i + } + return m, nil +} + +// GetLocationIDByCity 获取城市id +func GetLocationIDByCity(city string) (primitive.ObjectID, error) { + var l Location + if err := collLoc(nil).FindOne(&l, bson.M{"city": city}); err != nil { + log.Error("GetLocationIDByCity error", log.Any("city", city), log.E(err)) + return l.ID, err + } + return l.ID, nil +} + +// InsertLocationInfo 插入一条未存在的城市信息 +func InsertLocationInfo(l Location) error { + if l.City == "" { + return nil + } + now := time.Now() + m := make(map[string]interface{}) + m2 := make(map[string]interface{}) + m["createdAt"] = now + m["updatedAt"] = now + m["country"] = l.Country + m["countryCode"] = l.CountryCode + m["province"] = l.Province + m["provinceCode"] = l.ProvinceCode + m["city"] = l.City + m["cityCode"] = l.CityCode + m2["address"] = l.Address + m2["longitude"] = l.Longitude + m2["latitude"] = l.Latitude + if _, err := collLoc(nil).UpsertOne(bson.M(m), bson.M{"$set": bson.M(m2)}); err != nil { + if !mongo.IsDuplicateKeyError(err) { + log.Error("InsertLocationInfo error", log.Any("l", l), log.E(err)) + return err + } + } + return nil +} + +// FindLoactionsByIDs 通过id获取位置信息数组 +func FindLoactionsByIDs(ids []primitive.ObjectID) ([]Location, error) { + if ids == nil { + ids = []primitive.ObjectID{} + } + var lInfos []Location + if err := collLoc(nil).Find(&lInfos, bson.M{"_id": bson.M{"$in": ids}}); err != nil { + log.Error("FindLoactionsByIDs error", log.Any("ids", ids), log.E(err)) + return nil, err + } + return lInfos, nil +} + +// UpdateToLocation UpdateToLocation +func UpdateToLocation(id ObjectID, doc LocationDoc) error { + now := time.Now() + doc.UpdatedAt = &now + update, err := common.ToBsonM(doc) + if err != nil { + log.Error("UpdateToLocation ToBsonM error", log.Any("id", id), log.Any("doc", doc), log.E(err)) + return fmt.Errorf("UpdateToLocation ToBsonM error") + } + result, err := collLoc(nil).UpdateOne(M{"_id": id}, bson.M{"$set": update}) + if err != nil { + log.Error("UpdateToLocation error", log.Any("id", id), log.Any("doc", doc), log.E(err)) + return fmt.Errorf("UpdateToLocation error") + } + if result.ModifiedCount+result.UpsertedCount != 1 { + log.Error("UpdateToLocation count error", log.Any("id", id), log.Any("doc", doc), log.Any("mCount", + result.ModifiedCount), log.Any("uCount", result.UpsertedCount), log.E(err)) + return fmt.Errorf("wrong update count") + } + return nil +} + +// getLocCnt 获取条件查询总数 +func getLocCnt(cond bson.M) (int64, error) { + total, err := collLoc(nil).Count(cond) + if err != nil { + log.Error("getTotalCnt error", log.Any("cond", cond), log.E(err)) + return 0, err + } + return total, nil +} + +// getSkipSize 计算跳转 +func getSkipSize(page, size uint64, cond bson.M) (uint64, uint64, int64, error) { + total, err := getLocCnt(cond) + if err != nil { + return 0, 0, 0, err + } + totalpages := uint64(math.Ceil(float64(total) / float64(size))) + if page > totalpages { + page = totalpages + } + if page < 1 { + page = 1 + } + return (page - 1) * size, totalpages, total, nil +} + +// GetLocationList 获取城市列表 +func GetLocationList(city string, page, size uint64) ([]*Location, int64, error) { + cond := bson.M{} + if len(city) != 0 { + cond = bson.M{"city": city} + } + skip, _, total, err := getSkipSize(page, size, cond) + if err != nil { + return nil, 0, err + } + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "createdAt", Value: -1}}).SetSkip(int64(skip)).SetLimit(int64(size)) + var back []*Location + if err = collLoc(nil).Find(&back, cond, &opts); err != nil { + log.Error("GetLocationList error", log.Any("page", page), log.Any("size", size), log.Any("city", city), log.E(err)) + return nil, total, err + } + return back, total, nil +} diff --git a/models/v/locmod/type.go b/models/v/locmod/type.go new file mode 100644 index 0000000..620c116 --- /dev/null +++ b/models/v/locmod/type.go @@ -0,0 +1,74 @@ +package locmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Location 位置 +type Location struct { + ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + Longitude string `json:"longitude,omitempty" bson:"longitude,omitempty"` //经度 + Latitude string `json:"latitude,omitempty" bson:"latitude,omitempty"` //纬度 + Country string `json:"country,omitempty" bson:"country,omitempty"` //国家 + CountryCode string `json:"countryCode,omitempty" bson:"countryCode,omitempty"` //国家编码 + Province string `json:"province,omitempty" bson:"province,omitempty"` //省份 + ProvinceCode string `json:"provinceCode,omitempty" bson:"provinceCode,omitempty"` //省份编码 + City string `json:"city,omitempty" bson:"city"` //城市 + CityCode string `json:"cityCode,omitempty" bson:"cityCode,omitempty"` //城市编码 + Address string `json:"address,omitempty" bson:"address,omitempty"` //地址 + Cover string `json:"cover,omitempty" bson:"cover,omitempty"` //封面 + Visit int `json:"visit,omitempty" bson:"visit,omitempty"` //访问人数 + FakeVisit int `json:"fakeVisit,omitempty" bson:"fakeVisit,omitempty"` //假访问数 + SortKey int `json:"sortKey,omitempty" bson:"sortKey,omitempty"` //web列表排序字段 + CreatedAt time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` //刷新时间 +} + +type LocationDoc struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Longitude *string `json:"longitude,omitempty" bson:"longitude,omitempty"` //经度 + Latitude *string `json:"latitude,omitempty" bson:"latitude,omitempty"` //纬度 + Country *string `json:"country,omitempty" bson:"country,omitempty"` //国家 + CountryCode *string `json:"countryCode,omitempty" bson:"countryCode,omitempty"` //国家编码 + Province *string `json:"province,omitempty" bson:"province,omitempty"` //省份 + ProvinceCode *string `json:"provinceCode,omitempty" bson:"provinceCode,omitempty"` //省份编码 + City *string `json:"city,omitempty" bson:"city,omitempty" binding:"required"` //城市 + CityCode *string `json:"cityCode,omitempty" bson:"cityCode,omitempty"` //城市编码 + Address *string `json:"address,omitempty" bson:"address,omitempty"` //地址 + Cover *string `json:"cover,omitempty" bson:"cover,omitempty"` //封面 + Visit *int `json:"visit,omitempty" bson:"visit,omitempty"` //访问人数 + FakeVisit *int `json:"fakeVisit,omitempty" bson:"fakeVisit,omitempty"` //假访问数 + SortKey *int `json:"sortKey,omitempty" bson:"sortKey,omitempty"` //web列表排序字段 + CreatedAt *time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt *time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` //刷新时间 +} + +type HotCity struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + City string `json:"city,omitempty" bson:"city,omitempty"` //城市 + Province string `json:"province,omitempty" bson:"province,omitempty"` //省份 + SortKey int `json:"sortKey,omitempty" bson:"sortKey,omitempty"` //排序 + Enable bool `json:"enable,omitempty" bson:"enable,omitempty"` //使能 + CreatedAt time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` //刷新时间 +} + +type HotCityDoc struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + City *string `json:"city,omitempty" bson:"city,omitempty"` //城市 + Province *string `json:"province,omitempty" bson:"province,omitempty"` //省份 + SortKey *int `json:"sortKey,omitempty" bson:"sortKey,omitempty"` //排序 + Enable *bool `json:"enable,omitempty" bson:"enable,omitempty"` //使能 + CreatedAt *time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt *time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` //刷新时间 +} + +func Init() { + mdb = db.Init(table) + initIndexLoc() + initIndexHot() +} diff --git a/models/v/locmod/web.go b/models/v/locmod/web.go new file mode 100644 index 0000000..4ffcd81 --- /dev/null +++ b/models/v/locmod/web.go @@ -0,0 +1,38 @@ +package locmod + +import ( + "time" + + "91porn-server/models/commod" +) + +// CityPageReq 城市列表 +type CityPageReq struct { + City string `form:"city" json:"city" bson:"city,omitempty"` + commod.Page +} + +// LocInfo 位置信息 +type LocInfo struct { + ID string `json:"id"` + City string `json:"city"` //城市 + Cover string `json:"cover"` //封面 + Visit int `json:"visit"` //访问人数 + FakeVisit int `json:"fakeVisit"` //假访问数 + SortKey int `json:"sortKey"` //web列表排序字段 + CreatedAt time.Time `json:"createdAt"` //创建时间 + VideoCount int `json:"videoCount"` //播放量 + PlayCount int `json:"playCount"` //播放总数 +} + +// CityPageResp 城市列表应答 +type CityPageResp struct { + List []LocInfo `json:"list"` + Total int `json:"total"` +} + +// HotCityPage HotCityPage +type HotCityPage struct { + Total int `json:"total" bson:"total"` + List []HotCityDoc `json:"list" bson:"list"` +} diff --git a/models/v/marqueemod/method.go b/models/v/marqueemod/method.go new file mode 100644 index 0000000..bbf4f40 --- /dev/null +++ b/models/v/marqueemod/method.go @@ -0,0 +1,20 @@ +package marqueemod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson" +) + +func Set(content string) error { + if _, err := coll(nil).DeleteMany(bson.M{}); err != nil { + return err + } + _, err := coll(nil).InsertOne(Marquee{Content: content, CreatedAt: time.Now()}) + return err +} + +func Get() (Marquee, error) { + var m Marquee + return m, coll(nil).FindOne(&m, bson.M{}) +} diff --git a/models/v/marqueemod/type.go b/models/v/marqueemod/type.go new file mode 100644 index 0000000..4f636b4 --- /dev/null +++ b/models/v/marqueemod/type.go @@ -0,0 +1,31 @@ +package marqueemod + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const table = models.ModuleMarquee + +var mdb *db.MongoDB + +type Marquee struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Content string `json:"content" bson:"content"` // 跑马灯内容 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +func Init() { + mdb = db.Init(table) +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} diff --git a/models/v/media_buy_record_mod/mediaBuyRecord.go b/models/v/media_buy_record_mod/mediaBuyRecord.go new file mode 100644 index 0000000..606c780 --- /dev/null +++ b/models/v/media_buy_record_mod/mediaBuyRecord.go @@ -0,0 +1,288 @@ +package media_buy_record_mod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +const table = models.MediaBuyRecord + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "mediaId", Value: 1}, {Key: "contentId", Value: 1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func Create(t *db.MongoTool, doc *MediaBuyRecord) (err error) { + if _, err := coll(t).InsertOne(doc); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Create", table, "InsertOne", err), + log.Any("doc", doc), + ) + return err + } + return nil +} + +// IsWholeBuy 判断是否整本购买 +func IsWholeBuy(uid uint64, mediaId primitive.ObjectID) (bool, error) { + model := MediaBuyRecord{} + if err := coll(nil).FindOne(&model, bson.M{ + "mediaId": mediaId, + "contentId": primitive.NilObjectID, + "type": 1, + "uid": uid, + }); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsWholeBuy", table, "FindOne", err), + log.Any("uid", uid), + log.Any("mediaId", mediaId), + ) + return false, err + } + return !model.ID.IsZero(), nil +} + +// IsBuy 判断子集是否购买 +func IsBuy(uid uint64, mediaId, contentId primitive.ObjectID) (bool, error) { + return IsBuyWithTool(nil, uid, mediaId, contentId) +} + +// IsBuyWithTool 支持在购买事务内再次校验单集,避免锁边界内重复扣款。 +func IsBuyWithTool(t *db.MongoTool, uid uint64, mediaId, contentId primitive.ObjectID) (bool, error) { + model := MediaBuyRecord{} + if err := coll(t).FindOne(&model, bson.M{ + "mediaId": mediaId, + "contentId": bson.M{"$in": []primitive.ObjectID{primitive.NilObjectID, contentId}}, + "uid": uid, + }); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsBuy", table, "FindOne", err), + log.Any("uid", uid), + log.Any("mediaId", mediaId), + log.Any("contentId", contentId), + ) + return false, err + } + return !model.ID.IsZero(), nil +} + +// IsWholeBuyBatch 批量判断是否整本购买 +func IsWholeBuyBatch(uid uint64, mediaIds []primitive.ObjectID) (res map[primitive.ObjectID]bool, err error) { + res = make(map[primitive.ObjectID]bool) + list := []*MediaBuyRecord{} + if err := coll(nil).Find(&list, bson.M{ + "mediaId": bson.M{"$in": mediaIds}, + "contentId": primitive.NilObjectID, + "type": 1, + "uid": uid, + }); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsWholeBuy", table, "FindOne", err), + log.Any("uid", uid), + log.Any("mediaIds", mediaIds), + ) + return res, err + } + + for _, v := range list { + res[v.MediaId] = true + } + return res, nil +} + +// IsBuyBatch 批量查询子集是否已经购买 +func IsBuyBatch(uid uint64, mediaId primitive.ObjectID, contentIds []primitive.ObjectID) (res map[primitive.ObjectID]bool, isWholeBuy bool, err error) { + res = make(map[primitive.ObjectID]bool) + list := []*MediaBuyRecord{} + contentIds = append(contentIds, primitive.NilObjectID) + if err := coll(nil).Find(&list, bson.M{ + "mediaId": mediaId, + "contentId": bson.M{"$in": contentIds}, + "uid": uid, + }); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsBuyBatch", table, "Find", err), + log.Any("uid", uid), + log.Any("mediaId", mediaId), + log.Any("contentIds", contentIds), + ) + return res, isWholeBuy, err + } + + for _, v := range list { + if v.ContentId.IsZero() { + isWholeBuy = true + continue + } + res[v.ContentId] = true + } + + return res, isWholeBuy, nil +} + +// IsContentBuyBatch 批量查询多个短剧下的单集购买状态。 +func IsContentBuyBatch(uid uint64, contentIds []primitive.ObjectID) (map[primitive.ObjectID]bool, error) { + result := make(map[primitive.ObjectID]bool, len(contentIds)) + if len(contentIds) == 0 { + return result, nil + } + list := make([]*MediaBuyRecord, 0) + err := coll(nil).Find(&list, bson.M{ + "uid": uid, "type": 0, "contentId": bson.M{"$in": contentIds}, + }) + if err != nil { + return nil, err + } + for _, record := range list { + if record != nil { + result[record.ContentId] = true + } + } + return result, nil +} + +// GetBuyContentCount 获取用户购买的子集总数 +func GetBuyContentCount(uid uint64, mediaId primitive.ObjectID) (total int64, err error) { + total, err = coll(nil).Count(bson.M{ + "mediaId": mediaId, + "uid": uid, + "type": 0, + }) + return +} + +// GetBuyContentCountOnTime 判断是否在某个时间点间购买的子集总数 +func GetBuyContentCountOnTime(uid uint64, mediaId primitive.ObjectID, start, end time.Time) (total int64, err error) { + total, err = coll(nil).Count(bson.M{ + "mediaId": mediaId, + "uid": uid, + "type": 0, + "createdAt": bson.M{ + "$gt": start, + "$lt": end, + }, + }) + return +} + +type MyBuyMedia struct { + Id primitive.ObjectID `json:"id" bson:"_id"` + MediaID primitive.ObjectID `json:"mediaId" bson:"mediaId"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +// GetMyBuyMediaIDs 按最后购买时间返回去重后的短剧ID。 +func GetMyBuyMediaIDs(uid uint64, mediaType string, skip, limit uint64) (list []MyBuyMedia, total int64, hasNext bool, err error) { + match := bson.M{"uid": uid, "mediaType": mediaType} + base := []bson.M{ + {"$match": match}, + {"$group": bson.M{ + "_id": "$mediaId", "mediaId": bson.M{"$first": "$mediaId"}, + "createdAt": bson.M{"$max": "$createdAt"}, + }}, + } + countResult := []struct { + Total int64 `bson:"total"` + }{} + countPipeline := append(append([]bson.M{}, base...), bson.M{"$count": "total"}) + if err = coll(nil).Aggregate(&countResult, countPipeline); err != nil { + return + } + if len(countResult) > 0 { + total = countResult[0].Total + } + pipeline := append(append([]bson.M{}, base...), + bson.M{"$sort": bson.D{{Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}}, + bson.M{"$skip": skip}, bson.M{"$limit": limit + 1}, + ) + err = coll(nil).Aggregate(&list, pipeline) + if len(list) > int(limit) { + hasNext = true + list = list[:limit] + } + return +} + +// GetMyBuy 获取我购买的动漫 +// 子集不能单独购买,只有父级mediaid,使用简单的列表查询,不使用聚合 +func GetMyBuy(uid uint64, mediaType string, skip, limit uint64) (list []*MediaBuyRecord, hasNext bool, err error) { + opts := options.Find().SetSkip(int64(skip)).SetLimit(int64(limit + 1)).SetSort(bson.D{{Key: "createAt", Value: -1}}) + err = coll(nil).Find(&list, bson.M{"uid": uid, "mediaType": mediaType}, opts) + if err != nil { + return nil, false, err + } + if len(list) > int(limit) { + hasNext = true + list = list[:limit] + } + return +} + +// GetMyBuyWithSubContent 获取我购买的动漫 (不包含子集) +// 适用于子集可以单独购买的情况 +func GetMyBuyWithSubContent(uid uint64, mediaType string, skip, limit uint64) (list []*MediaBuyRecord, hasNext bool, err error) { + pipeline := []bson.M{ + { + "$match": bson.M{ + "uid": uid, + "mediaType": mediaType, + }, + }, + { + "$group": bson.M{ + "_id": "$mediaId", + "createAt": bson.M{"$first": "$createAt"}, // 只保留排序字段 + }, + }, + { + "$sort": bson.M{ + "createdAt": -1, + }, + }, + { + "$skip": skip, + }, + { + "$limit": limit + 1, + }, + } + err = coll(nil).Aggregate( + &list, + pipeline, + ) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetMyBuy", table, "Aggregate", err), + log.Any("uid", uid), + ) + return + } + if len(list) > int(limit) { + hasNext = true + list = list[:limit] + } + return +} diff --git a/models/v/media_buy_record_mod/type.go b/models/v/media_buy_record_mod/type.go new file mode 100644 index 0000000..bf8bb63 --- /dev/null +++ b/models/v/media_buy_record_mod/type.go @@ -0,0 +1,23 @@ +package media_buy_record_mod + +import ( + "91porn-server/common/db" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +var mdb *db.MongoDB + +// MediaBuyRecord acg动漫购买记录表 +type MediaBuyRecord struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + MediaId primitive.ObjectID `json:"mediaId" bson:"mediaId"` + MediaType string `json:"mediaType" bson:"mediaType"` + Uid uint64 `json:"uid" bson:"uid"` + Type int `json:"type" bson:"type"` // 0-章节购买 1-整本购买 + ContentId primitive.ObjectID `json:"contentId" bson:"contentId"` + Coins int64 `json:"coins" bson:"coins"` // 定价 + PayMoney int64 `json:"payMoney" bson:"payMoney"` // 金币 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 文档创建时间 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 文档更新时间 +} diff --git a/models/v/mediabookshelfmod/app.go b/models/v/mediabookshelfmod/app.go new file mode 100755 index 0000000..e8530ca --- /dev/null +++ b/models/v/mediabookshelfmod/app.go @@ -0,0 +1,27 @@ +package mediabookshelfmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// MediaBookshelfInfo 移动端返回内容 +type MediaBookshelfInfo struct { + ID primitive.ObjectID `json:"id" bson:"id"` // 文档id + Name string `json:"name" bson:"name"` // 名称 + VerticalCover string `json:"verticalCover" bson:"verticalCover"` // 竖版封面 + TotalEpisode int `json:"totalEpisode" bson:"totalEpisode"` // 总集数 + UpdateStatus int `json:"updateStatus" bson:"updateStatus"` // 更新状态 + Permission int `json:"permission" bson:"permission"` // 收听权限 0:会员 1:金币购买 2:免费 + Price int64 `json:"price" bson:"price" ` //价格 + CurrentEpisode int `json:"currentEpisode"` // 当前更新到的集数 + MediaType string `json:"mediaType" bson:"mediaType"` // 类型 "video":动漫,"image":漫画,"text":小说 + MediaSubType int `json:"mediaSubType"` // 子类型 ,暂时只有小说使用 0-默认文本小说 1-有声小说 + CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` // 浏览数 + UID uint64 `json:"uid" bson:"uid"` // 用户ID + ReadAt time.Time `json:"readAt" bson:"readAt"` // 阅读时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 更新时间 + IsNew bool `json:"isNew" bson:"isNew"` // 是否最新 +} diff --git a/models/v/mediabookshelfmod/mediabookshelf.go b/models/v/mediabookshelfmod/mediabookshelf.go new file mode 100755 index 0000000..fd8cbde --- /dev/null +++ b/models/v/mediabookshelfmod/mediabookshelf.go @@ -0,0 +1,293 @@ +package mediabookshelfmod + +import ( + "91porn-server/models" + "encoding/json" + "errors" + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.MediaBookshelf + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "mid", Value: 1}, {Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "shelfType", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []MediaBookshelf, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// GetDramaMediaIDs 返回有效短剧收藏,并支持按收藏时间或短剧最近更新时间排序。 +func GetDramaMediaIDs(uid uint64, skip, limit int64, sortByMediaUpdate bool) ( + ids []primitive.ObjectID, + total int64, + hasNext bool, + err error, +) { + sortStage := bson.D{{Key: "readAt", Value: -1}, {Key: "_id", Value: -1}} + if sortByMediaUpdate { + sortStage = bson.D{ + {Key: "media.latestPublishedAt", Value: -1}, + {Key: "media.updateTime", Value: -1}, + {Key: "_id", Value: -1}, + } + } + type row struct { + MID primitive.ObjectID `bson:"mid"` + } + type count struct { + Total int64 `bson:"total"` + } + result := make([]struct { + Rows []row `bson:"rows"` + Count []count `bson:"count"` + }, 0, 1) + pipeline := []bson.M{ + {"$match": bson.M{"uid": uid, "shelfType": "drama"}}, + {"$lookup": bson.M{ + "from": models.Media, "localField": "mid", "foreignField": "_id", "as": "media", + }}, + {"$unwind": "$media"}, + {"$match": bson.M{ + "media.mediaType": "drama", "media.status": 1, "media.isDelete": false, + }}, + {"$sort": sortStage}, + {"$facet": bson.M{ + "rows": []bson.M{ + {"$skip": skip}, {"$limit": limit + 1}, {"$project": bson.M{"mid": 1}}, + }, + "count": []bson.M{{"$count": "total"}}, + }}, + } + if err = coll(nil).Aggregate(&result, pipeline); err != nil || len(result) == 0 { + return + } + if len(result[0].Count) > 0 { + total = result[0].Count[0].Total + } + rows := result[0].Rows + if len(rows) > int(limit) { + hasNext = true + rows = rows[:limit] + } + ids = make([]primitive.ObjectID, 0, len(rows)) + for _, item := range rows { + ids = append(ids, item.MID) + } + return +} + +// GetAll 查询全部文档 +func GetAll(filter primitive.M, sort bson.D) (out []*MediaBookshelf, err error) { + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAll", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (MediaBookshelf, error) { + v := MediaBookshelf{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// GetInfoByCond 通过id获取详细信息 +func GetInfoByCond(filter bson.M) (MediaBookshelf, error) { + v := MediaBookshelf{} + if err := coll(nil).FindOne(&v, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("filter", filter), + ) + return v, err + } + + return v, nil +} + +// HasAddBookshelf 用户是否添加书架 +func HasAddBookshelf(uid uint64, mIds []primitive.ObjectID) (map[primitive.ObjectID]bool, error) { + if mIds == nil { + mIds = []primitive.ObjectID{} + } + m := make(map[primitive.ObjectID]bool) + var infos []MediaBookshelf + query := bson.M{"uid": uid, "mid": bson.M{"$in": mIds}} + if err := coll(nil).Find(&infos, query); err != nil { + log.Error("IsAddBookshelf error", log.Any("uid", uid), log.Any("mIds", mIds), log.E(err)) + return m, err + } + for _, i := range infos { + m[i.MID] = true + } + return m, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d MediaBookshelf) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + return err +} + +// DeleteByIDS 删除数据 +func DeleteByIDS(t *db.MongoTool, uid uint64, ids []primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"mid": bson.M{"$in": ids}, "uid": uid}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByIDS", table, "DeleteMany", err), log.Any("ids", ids)) + } + } + return err +} + +// DeleteByCond 删除数据 +func DeleteByCond(t *db.MongoTool, cond bson.M) error { + _, err := coll(t).DeleteOne(cond) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteOne", err), log.Any("cond", cond)) + } + } + return err +} + +// GetCount 获取书架总数 +func GetCount(uid uint64) (total int64, err error) { + total, err = coll(nil).Count(bson.M{"uid": uid}) + return +} diff --git a/models/v/mediabookshelfmod/type.go b/models/v/mediabookshelfmod/type.go new file mode 100755 index 0000000..3339bdd --- /dev/null +++ b/models/v/mediabookshelfmod/type.go @@ -0,0 +1,29 @@ +package mediabookshelfmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +const ( + ShelfCartoon = "video" + ShelfComics = "image" + ShelfFiction = "text" +) + +type MediaBookshelf struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + Name string `json:"name" bson:"name"` // 名称 + ShelfType string `json:"shelfType" bson:"shelfType"` // 书架类型 "video":动漫,"image":漫画,"text":小说 + MID primitive.ObjectID `json:"mid" bson:"mid"` // 媒体ID + UID uint64 `json:"uid" bson:"uid"` // 用户ID + IsDelete bool `json:"isDelete" bson:"isDelete"` // 是否删除 + ReadAt time.Time `json:"readAt" bson:"readAt"` // 阅读时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 更新时间 +} diff --git a/models/v/mediacontentmod/mediacontent.go b/models/v/mediacontentmod/mediacontent.go new file mode 100755 index 0000000..359f8f8 --- /dev/null +++ b/models/v/mediacontentmod/mediacontent.go @@ -0,0 +1,392 @@ +package mediacontentmod + +import ( + "91porn-server/models" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.MediaContent + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "mediaId", Value: 1}}, + }, + { + Keys: bson.D{{Key: "hashId", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []MediaContent, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +func QueryContentsPrice(mediaId primitive.ObjectID) (price int64, err error) { + type contentsPrice struct { + Total int64 `bson:"total"` + } + res := []contentsPrice{} + pipeline := []bson.M{ + bson.M{"$match": bson.M{ + "mediaId": mediaId, + "isActive": true, + "isDelete": false, + }, + }, + bson.M{"$group": bson.M{"_id": "$mediaId", "total": bson.M{"$sum": "$price"}}}, + } + err = coll(nil).Aggregate(&res, pipeline) + if err != nil { + return + } + if len(res) > 0 { + price = res[0].Total + } + return +} + +// QueryAllList 分页查询文档 +func QueryAllList(filter primitive.M, opts ...*options.FindOptions) (out []*MediaContent, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +var MediaContentNotFound = errors.New("media not found") + +// GetInfoByHashId 通过hashId获取详细信息 +func GetInfoByHashId(mediaId primitive.ObjectID, hashId string) (MediaContent, error) { + v := MediaContent{} + if err := coll(nil).FindOne(&v, bson.M{"mediaId": mediaId, "hashId": hashId, "isDelete": false}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfoByHashId", table, "FindOne", err), + log.Any("hashId", hashId), + ) + return v, err + } + if v.ID.IsZero() { + return v, MediaContentNotFound + } + + return v, nil +} + +// GetInfoByMediaIdAndName 通过mediaId与name获取详细信息 +func GetInfoByMediaIdAndName(mediaId primitive.ObjectID, name string) (MediaContent, error) { + v := MediaContent{} + if err := coll(nil).FindOne(&v, bson.M{"mediaId": mediaId, "name": strings.TrimSpace(name), "isDelete": false}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfoByMediaIdAndName", table, "FindOne", err), + log.Any("mediaId", mediaId), + log.Any("title", name), + ) + return v, err + } + if v.ID.IsZero() { + return v, MediaContentNotFound + } + + return v, nil +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID, isActive ...bool) (MediaContent, error) { + cond := bson.M{"_id": id} + if len(isActive) > 0 { + cond["isActive"] = isActive[0] + } + v := MediaContent{} + if err := coll(nil).FindOne(&v, cond); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + return v, nil +} + +func GetInfoByCond(cond bson.M) (MediaContent, error) { + v := MediaContent{} + if err := coll(nil).FindOne(&v, cond); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("cond", cond), + ) + return v, err + } + + return v, nil +} + +// IncCountPurchases 购买次数增加 +func IncCountPurchases(id primitive.ObjectID, inc int) error { + return IncCountPurchasesWithTool(nil, id, inc) +} + +func IncCountPurchasesWithTool(t *db.MongoTool, id primitive.ObjectID, inc int) error { + query := bson.M{"_id": id} + update := bson.M{"$set": bson.M{"updateTime": time.Now()}, "$inc": bson.M{"countPurchases": inc}} + _, err := coll(t).UpdateOne(query, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncCountPurchases", table, "UpdateOne", err), + log.Any("id", id), + ) + return err + } + return nil +} + +// IncCollectCount 收藏次数增加 +func IncCollectCount(id primitive.ObjectID, inc int) error { + query := bson.M{"_id": id} + update := bson.M{"$set": bson.M{"updateTime": time.Now()}, "$inc": bson.M{"countCollect": inc}} + _, err := coll(nil).UpdateOne(query, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncLikeCount", table, "UpdateOne", err), + log.Any("id", id), + ) + return err + } + return nil +} + +// IncreaseCountLikeByID 更新媒体点赞数 +func IncreaseCountLikeByID(id primitive.ObjectID, value int) (err error) { + if _, err = coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$inc": bson.M{"countLike": value}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncreaseLikeCountByID", table, "UpdateOne", err), + log.Any("id", id), + log.Any("value", value), + ) + return + } + return +} + +// IncreaseCountDislikeByID 更新媒体点踩数 +func IncreaseCountDislikeByID(id primitive.ObjectID, value int) (err error) { + if _, err = coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$inc": bson.M{"countDisLike": value}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncreaseLikeCountByID", table, "UpdateOne", err), + log.Any("id", id), + log.Any("value", value), + ) + return + } + return +} + +// IncrCommentCountByID 更新媒体评论数 +func IncrCommentCountByID(id primitive.ObjectID, value int) (err error) { + if _, err = coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$inc": bson.M{"countComment": value}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncrCommentCountByID", table, "UpdateOne", err), + log.Any("id", id), + log.Any("value", value), + ) + return + } + return +} + +// IncBrowseCount 更新媒体评论数 +func IncBrowseCount(id primitive.ObjectID, value int) (err error) { + if _, err = coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$inc": bson.M{"countBrowse": value}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncBrowseCount", table, "UpdateOne", err), + log.Any("id", id), + log.Any("value", value), + ) + return + } + return +} + +func UpdateOneByID(id primitive.ObjectID, set bson.M) (*mongo.UpdateResult, error) { + return coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}) +} + +func InsetBatch(t *db.MongoTool, list []MediaContent) (err error) { + if len(list) == 0 { + return + } + if _, err := coll(t).InsertMany(list); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsetBatch", table, "InsertMany", err)) + return err + } + return +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d MediaContent) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// UpdateByIDS 根据ids更新数据 +func UpdateByIDS(t *db.MongoTool, ids []primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": bson.M{"$in": ids}} + return update(t, cond, data) +} + +// UpdateByCond 根据cond更新数据 +func UpdateByCond(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).UpdateOne(bson.M{"_id": id}, bson.M{"$set": bson.M{"isDelete": true}}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} + +// DeleteByMediaId 删除数据 +func DeleteByMediaId(t *db.MongoTool, mediaId primitive.ObjectID) error { + _, err := coll(t).UpdateMany(bson.M{"mediaId": mediaId}, bson.M{"$set": bson.M{"isDelete": true}}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByMediaId", table, "DeleteMany", err), log.Any("mediaId", mediaId)) + } + } + + return err +} + +// UpdateForReview 内容审查通过后回写文本字段 +// title 对应 MediaContent.Name,content 对应 MediaContent.Text +func UpdateForReview(id primitive.ObjectID, title, content string) error { + set := bson.M{} + if title != "" { + set["name"] = title + } + if content != "" { + set["text"] = content + } + if len(set) == 0 { + return nil + } + set["updateTime"] = time.Now() + _, err := coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}) + return err +} + +// OffShelfManyForReview 内容审查命中后批量下架章节(isActive -> false) +// 仅当前 isActive=true 的会被改动 +func OffShelfManyForReview(ids []primitive.ObjectID) error { + if len(ids) == 0 { + return nil + } + _, err := coll(nil).UpdateMany( + bson.M{"_id": bson.M{"$in": ids}, "isActive": true}, + bson.M{"$set": bson.M{"isActive": false, "updateTime": time.Now()}}, + ) + return err +} diff --git a/models/v/mediacontentmod/type.go b/models/v/mediacontentmod/type.go new file mode 100755 index 0000000..5ed40fb --- /dev/null +++ b/models/v/mediacontentmod/type.go @@ -0,0 +1,51 @@ +package mediacontentmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +type MediaContent struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + MediaID primitive.ObjectID `json:"mediaId" bson:"mediaId"` // 媒体资源ID + MediaType string `json:"mediaType" bson:"mediaType"` // 媒体类型 "video":视频,"image":图片,"text":小说 + EpisodeNumber int `json:"episodeNumber" bson:"episodeNumber"` // 第几集 + ListenPermission int `json:"listenPermission" bson:"listenPermission"` // 收听权限 0:会员 1:金币购买 2:免费 + Cover string `json:"cover" bson:"cover"` // 封面 + Price int64 `json:"price" bson:"price"` // 购买价格 + Name string `json:"name" bson:"name"` // 章节名 + Text string `json:"text" bson:"text"` // 内容(小说专用) + Md5 string `json:"md5" bson:"md5"` // 媒体md5 + AudioUrl string `json:"audioUrl" bson:"audioUrl"` // 有声小说地址 + Author string `json:"author" bson:"author"` // 作者 + VideoUrl string `json:"videoUrl" bson:"videoUrl"` // 视频地址 + H265Url string `json:"h265Url" bson:"h265Url"` // H.265 视频地址 + UrlSet []string `json:"urlSet" bson:"urlSet"` // 地址集(多个资源地址用 例如漫画) + Height int `json:"height" bson:"height"` // 高 + Weight int `json:"weight" bson:"weight"` // 宽 + MediaSize int64 `json:"mediaSize" bson:"mediaSize"` // 资源大小 + PlayTime uint `json:"playTime" bson:"playTime"` // 影片长度 + PreviewEnabled bool `json:"previewEnabled" bson:"previewEnabled"` // 是否允许试看 + PreviewStart int `json:"previewStart" bson:"previewStart"` // 试看起始时间(秒) + PreviewSeconds int `json:"previewSeconds" bson:"previewSeconds"` // 试看时长(秒) + Status int `json:"status" bson:"status"` // 状态 0:转码中 1:转码成功 2:转码失败, 视频资源使用 + IsActive bool `json:"isActive" bson:"isActive"` // 是否激活 + CountComment int64 `json:"countComment" bson:"countComment"` // 评论数 + CountPurchases int64 `json:"countPurchases" bson:"countPurchases"` // 购买数 + CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` // 浏览数 + FakeCountBrowse int64 `json:"fakeCountBrowse" bson:"fakeCountBrowse"` // 假浏览数,初始化数据展示使用 + CountCollect int64 `json:"countCollect" bson:"countCollect"` // 收藏数 + CountLike int64 `json:"countLike" bson:"countLike"` // 喜欢数 + CountDisLike int64 `json:"countDisLike" bson:"countDisLike"` // 不喜欢数 + Ratio float64 `json:"ratio" bson:"ratio"` // 宽高比 + IsDelete bool `json:"isDelete" bson:"isDelete"` // 是否删除 + UpdatedAct string `json:"updatedAct" bson:"updatedAct"` // 操作用户账号 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 文档创建时间 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 文档更新时间 + HashId string `json:"hashId" bson:"hashId,omitempty"` // 在媒资库中的唯一标识 +} diff --git a/models/v/mediamod/app.go b/models/v/mediamod/app.go new file mode 100644 index 0000000..f9e5dfa --- /dev/null +++ b/models/v/mediamod/app.go @@ -0,0 +1,213 @@ +package mediamod + +import ( + "91porn-server/models/commod" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +type AppMediaBase struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + SortCode int `json:"sortCode" bson:"sortCode"` // 排序 + Status int `json:"status" bson:"status"` // 状态 + Title string `json:"title" bson:"title"` // 标题 + HorizontalCover string `json:"horizontalCover" bson:"horizontalCover"` // 横版封面 + VerticalCover string `json:"verticalCover" bson:"verticalCover"` // 竖版封面 + Tags []primitive.ObjectID `json:"tags" bson:"tags"` // 标签 + TagDetails []TagDetail `json:"tagDetails" bson:"tagDetails"` // 标签对象 + Summary string `json:"summary" bson:"summary"` // 简介 + TotalEpisode int `json:"totalEpisode" bson:"totalEpisode"` // 总集数 + CurrentEpisode int `json:"currentEpisode" bson:"currentEpisode"` // 当前更新的集数 + FreeEpisode int `json:"freeEpisode" bson:"freeEpisode"` // 免费集数 + UpdateStatus int `json:"updateStatus" bson:"updateStatus"` // 更新状态 + MediaType string `json:"mediaType" bson:"mediaType"` // 媒体类型 "video":动漫,"image":漫画 "text":小说 + MediaSubType MediaSubType `json:"mediaSubType" bson:"mediaSubType"` // 子类型 ,暂时只有小说使用 0-默认文本小说 1-有声小说 + DefaultContent DefaultContent `json:"defaultContent" bson:"defaultContent"` // 默认展示内容,暂时只有漫画需要 + Kind int `json:"kind" bson:"kind"` // 种类(1、哩番,2、3D,3、同人动画,4、单行本,5、连载漫画,6、韩漫 ) + Style int `json:"style" bson:"style"` // 展示背景颜色 0-随机 1~5对应ui五种颜色 + PermissionIconHide bool `json:"permissionIconHide" bson:"permissionIconHide"` // 售卖类型标识隐藏 + Permission int `json:"permission" bson:"permission"` // 收听权限 0:会员 1:金币购买 2:免费 + Price int64 `json:"price" bson:"price"` // 价格(整部购买的类型 小说 cos 写真) + ContentsPrice int64 `json:"contentsPrice" bson:"contentsPrice"` // 所有子集加起来的价格 + Direction int64 `json:"direction" bson:"direction"` // 排版方向 0横 1竖(冗余字段) + //SellType int `json:"sellType" bson:"sellType"` // 商品售卖类型(0单集售卖 1整部售卖) + FreeTime int `json:"freeTime" bson:"freeTime"` // 免费试看时长 单位 s + MID string `json:"mId" bson:"mId"` // 模块ID + ModuleName string `json:"moduleName" bson:"moduleName"` // 模块名称 + SectionName string `json:"sectionName" bson:"sectionName"` // 专题名称 + SID primitive.ObjectID `json:"sId" bson:"sId"` // 专题ID + SectionSort int `json:"sectionSort" bson:"sectionSort"` // 专题排序 + CountComment int64 `json:"countComment" bson:"countComment"` // 评论数 + CountPurchases int64 `json:"countPurchases" bson:"countPurchases"` // 购买数 + HasFollow bool `json:"hasFollow" bson:"hasFollow"` // 是否关注 + CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` // 浏览数 + CountCollect int64 `json:"countCollect" bson:"countCollect"` // 收藏数 + CountLike int64 `json:"countLike" bson:"countLike"` // 喜欢数 + CountDisLike int64 `json:"countDisLike" bson:"countDisLike"` // 不喜欢数 + CountView int64 `json:"countView" bson:"countView"` // 展现数 + CountShare int64 `json:"countShare" bson:"countShare"` // 分享数 + MediaStatus MediaStatus `json:"mediaStatus" bson:"mediaStatus"` // 媒体状态 + Number int64 `json:"number" bson:"number"` // 编号 + Author string `json:"author"` // 作者 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 文档创建时间 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 文档更新时间 + LatestPublishedAt time.Time `json:"latestPublishedAt" bson:"latestPublishedAt"` // 最新上架或子集更新时间 + LsjId string `json:"lsjId" bson:"lsjId"` // 老司机ID + HasDramaCard bool `json:"hasDramaCard" bson:"hasDramaCard"` // 是否持有有效短剧权益卡 + +} + +// MediaStatus 媒体状态 +type MediaStatus struct { + HasPaid bool `json:"hasPaid" bson:"hasPaid"` // 已支付(如果是父集,表示整本购买) + HasCollected bool `json:"hasCollected" bson:"hasCollected"` // 已收藏 + HasLiked bool `json:"hasLiked" bson:"hasLiked"` // 已点赞 + +} + +type MediaContent struct { + ID primitive.ObjectID `json:"id"` // 文档id + MediaID primitive.ObjectID `json:"mediaId"` // 媒体资源ID + EpisodeNumber int `json:"episodeNumber"` // 第几集 + ListenPermission int `json:"listenPermission"` // 收听权限 0:会员 1:金币购买 2:免费 + Price int64 `json:"price"` // 购买价格 + Name string `json:"name"` // 章节名 + Text string `json:"text"` // 内容(小说专用) + Md5 string `json:"md5"` // 媒体md5 + VideoUrl string `json:"videoUrl"` // 视频地址 + AudioUrl string `json:"audioUrl" bson:"audioUrl"` // 有声小说地址 + UrlSet []string `json:"urlSet"` // 地址集(多个资源地址用 例如漫画) + Height int `json:"height"` // 高 + Weight int `json:"weight"` // 宽 + CountComment int64 `json:"countComment" bson:"countComment"` // 评论数 + CountPurchases int64 `json:"countPurchases" bson:"countPurchases"` // 购买数 + CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` // 浏览数 + CountCollect int64 `json:"countCollect" bson:"countCollect"` // 收藏数 + CountLike int64 `json:"countLike" bson:"countLike"` // 喜欢数 + CountDisLike int64 `json:"countDisLike" bson:"countDisLike"` // 不喜欢数 + MediaSize int64 `json:"mediaSize"` // 资源大小 + PlayTime uint `json:"playTime"` // 影片长度 + Ratio float64 `json:"ratio"` // 宽高比 + MediaStatus MediaStatus `json:"mediaStatus" bson:"mediaStatus"` // 媒体状态 + CreatedAt time.Time `json:"createdAt"` // 文档创建时间 + UpdateTime time.Time `json:"updateTime"` // 文档更新时间 +} + +type AppLibrary struct { + Key string `json:"key" bson:"key"` // 健值 + Name string `json:"name" bson:"name"` // 健名称 + Kind []KindInfo `json:"kind" bson:"kind"` // 种类 (1、哩番,2、3D,3、同人动画,4、单行本,5、连载漫画,6、韩漫 ) +} + +type KindInfo struct { + Key string `json:"key" bson:"key"` // 健值 + Name string `json:"name" bson:"name"` // 健名称 + Cover string `json:"cover" bson:"cover"` // 封面 + Dimension []DimensionInfo `json:"dimension" bson:"dimension"` // 纬度 +} + +type DimensionInfo struct { + Key string `json:"key" bson:"key"` // 健值 + Name string `json:"name" bson:"name"` // 健名称 + Tag []TagInfo `json:"tag" bson:"tag"` // 标签 +} + +type TagInfo struct { + ID string `json:"id" bson:"id"` // 标签ID + Name string `json:"name" bson:"name"` // 标签名称 +} + +type AppLibraryReq struct { + MediaType string `json:"mediaType" bson:"mediaType"` // 媒体类型 + TagIds []string `json:"tagIds" bson:"tagIds"` // 标签ID + SortType int `json:"sortType" bson:"sortType"` // 排序 1、精选,2、最新,3、最热,4、最多收藏 + commod.Page +} + +type AppElasticSearchLibraryResponse struct { + List []*AppMediaBase `json:"list"` // list + HasNext bool `json:"hasNext"` // hasNext +} + +func (p AppLibraryReq) Filter(ids []primitive.ObjectID) bson.M { + filter := bson.M{} + filter["_id"] = bson.M{"$in": ids} + return filter +} + +func AppLibrarySortField(sortType int) string { + switch sortType { + case 1: + return "choiceSort" + case 3: + return "countLike" + case 4: + return "countCollect" + default: + return "updateTime" + } +} + +func (p AppLibraryReq) Options() bson.D { + if p.MediaType == MediaTypeDrama && p.SortType == 1 { + return bson.D{ + {Key: "sortCode", Value: -1}, + {Key: "createdAt", Value: -1}, + {Key: "_id", Value: -1}, + } + } + return bson.D{{Key: AppLibrarySortField(p.SortType), Value: -1}} +} + +type AppSearchReq struct { + KeyWord string `form:"keyword" json:"keyword"` // 搜索关键词 + TagName string `form:"tagName" json:"tagName"` // 搜索标签名 + Kind int `form:"kind" json:"kind"` // 种类 1、动漫 2、漫画 3、小说 4、短剧 + commod.Page + SortType int `json:"sortType" form:"sortType"` // 1-最新上架 2-最多观看 +} + +type AppElasticSearchResponse struct { + List []*AppMediaBase `json:"list"` // list + TagID string `json:"tagID"` // 帖子搜索时附带的tagID + TagMediaList []*AppMediaBase `json:"tagMediaList"` // 搜索时tag相关的帖子 + HasNext bool `json:"hasNext"` // hasNext +} + +func (p AppSearchReq) Options() bson.D { + if p.Kind == 4 { + if p.SortType == 2 { + return bson.D{{Key: "countBrowse", Value: -1}, {Key: "latestPublishedAt", Value: -1}, {Key: "_id", Value: -1}} + } + return bson.D{{Key: "latestPublishedAt", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}} + } + return bson.D{{Key: "createdAt", Value: -1}} +} + +type AppLibraryV2Req struct { + MediaType string `json:"mediaType" form:"mediaType"` // 动漫类型 video-动漫 image-漫画 text-小说 +} + +type AppLibraryV2Resp struct { + MediaType []AppLibraryItem `json:"mediaType"` + SortType []AppLibraryItem `json:"sortType"` // 1-最多播放 2-收藏最多 3-最新上架 + TagName []AppLibraryItem `json:"tagName"` // 标签名 + PayType []AppLibraryItem `json:"payType"` // 付费类型 +} + +type AppLibraryItem struct { + Name string `json:"name"` + Value string `json:"value"` +} + +type AppLibrarySearchV2Req struct { + MediaType string `json:"mediaType" form:"mediaType"` // 媒体类型 + SortType string `json:"sortType" form:"sortType"` // 排序类型 + TagName string `json:"tagName" form:"tagName"` // 标签名 + PayType string `json:"payType" form:"payType"` // 付费类型 + commod.Page +} diff --git a/models/v/mediamod/es.go b/models/v/mediamod/es.go new file mode 100644 index 0000000..a413706 --- /dev/null +++ b/models/v/mediamod/es.go @@ -0,0 +1,154 @@ +package mediamod + +import ( + "fmt" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" +) + +var es *elastic.Client + +const ESTable = models.ESMediaTable + +func InitESIndex() { + es = elastic.Init() + var setting = elastic.M{ + "settings": elastic.M{ + "number_of_shards": elastic.NumberOfShards, + "number_of_replicas": elastic.NumberOfReplicas, + "analysis": elastic.M{ + "analyzer": elastic.M{ + "ik": elastic.M{ + "tokenizer": elastic.AnalyzerIkSmart, + }, + }, + }, + }, + "mappings": elastic.M{ + "properties": elastic.M{ + "title": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkSmart, + "search_analyzer": elastic.AnalyzerIkSmart, + }, + "tagsName": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkSmart, + "search_analyzer": elastic.AnalyzerIkSmart, + }, + }, + }, + } + if err := es.CreateIndices(ESTable, setting); err != nil { + panic(fmt.Sprintf("%s index indeices err ==>[%+v]", ESTable, err)) + } +} + +// Search 根据关键字搜索 +func Search(keywords string, from int64, size int64) (data []ESMediaSource, err error) { + es = elastic.Init() + query := elastic.M{ + "query": elastic.M{ + "bool": elastic.M{ + "must": elastic.A{ + {"multi_match": elastic.M{ + "query": keywords, + "fields": []string{"title"}}, + }, + {"terms": elastic.M{"status": []int{1}}}, + {"range": elastic.M{"playTime": elastic.M{"lt": 300}}}, + }, + }, + }, + "sort": elastic.A{{"hot": elastic.M{"order": "desc"}}}, + "from": from, + "size": size, + } + if err = es.Search(ESTable, &data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Search", table, "CommonSearch", err), + log.Any("keywords", keywords), + log.Any("from", from), + log.Any("size", size)) + return + } + return +} + +func SearchWithTotal(keywords string, from int64, size int64) (data ESVideoSourceWithTotal, err error) { + es = elastic.Init() + query := elastic.M{ + "query": elastic.M{ + "bool": elastic.M{ + "must": elastic.A{ + {"multi_match": elastic.M{ + "query": keywords, + "fields": []string{"title"}}, + }, + {"terms": elastic.M{"status": []int{1, 3}}}, + }, + }, + }, + "sort": elastic.A{{"hot": elastic.M{"order": "desc"}}}, + "from": from, + "size": size, + } + if err = es.SearchWithTotal(ESTable, &data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Search", table, "CommonSearch", err), + log.Any("keywords", keywords), + log.Any("from", from), + log.Any("size", size)) + return + } + return +} + +// DirectSearch 用户根据关键字搜索 +func DirectSearch(keywords string, vidType string, from int64, size int64) (data []ESMediaSource, err error) { + es = elastic.Init() + query := elastic.M{ + "query": elastic.M{ + "bool": elastic.M{ + "must": elastic.A{ + {"multi_match": elastic.M{ + "query": keywords, + "fields": []string{"title"}}, + }, + {"term": elastic.M{"mediaType.keyword": vidType}}, + {"term": elastic.M{"isDelete": false}}, + {"term": elastic.M{"status": 1}}, + }, + }, + }, + // "sort": elastic.A{{"hot": elastic.M{"order": "desc"}}}, + "from": from, + "size": size, + } + if err = es.Search(ESTable, &data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Search", table, "CommonSearch", err), + log.Any("keywords", keywords), + log.Any("from", from), + log.Any("size", size)) + return + } + return +} + +func DeleteByCond(filter elastic.M) (err error) { + es = elastic.Init() + if err = es.BulkDelete(ESTable, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Search", table, "CommonSearch", err), log.Any("filter", filter)) + return + } + return +} + +func SearchByCondWithTotal(filter elastic.M) (data ESVideoSourceWithTotal, err error) { + es = elastic.Init() + if err = es.SearchWithTotal(ESTable, &data, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Search", table, "CommonSearch", err), log.Any("filter", filter)) + return + } + return +} diff --git a/models/v/mediamod/media.go b/models/v/mediamod/media.go new file mode 100755 index 0000000..f5272be --- /dev/null +++ b/models/v/mediamod/media.go @@ -0,0 +1,608 @@ +package mediamod + +import ( + "91porn-server/models" + "encoding/json" + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.Media + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "lsjId", Value: 1}}, + }, + { + Keys: bson.D{{Key: "sId", Value: 1}, {Key: "sortCode", Value: -1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "sId", Value: 1}, {Key: "sectionSort", Value: -1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "tags", Value: 1}}, + }, { + Keys: bson.D{{Key: "tags", Value: 1}, {"isDelete", 1}, {"status", 1}, {"hot", -1}, {"createdAt", -1}}, + }, { + Keys: bson.D{{Key: "tags", Value: 1}, {"isDelete", 1}, {"status", 1}, {"countCollect", -1}, {"createdAt", -1}}, + }, { + Keys: bson.D{{Key: "tags", Value: 1}, {"isDelete", 1}, {"status", 1}, {"countBrowse", -1}, {"createdAt", -1}}, + }, { + Keys: bson.D{{Key: "tags", Value: 1}, {"isDelete", 1}, {"status", 1}, {"createdAt", -1}}, + }, + { + Keys: bson.D{{Key: "kind", Value: 1}}, + }, + { + Keys: bson.D{{Key: "isActive", Value: 1}}, + }, + { + Keys: bson.D{{Key: "specialSubjectID", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "mediaContentId", Value: 1}}, + }, + { + Keys: bson.D{{Key: "title", Value: 1}}, + }, { // 年度排行需要 + Keys: bson.D{{Key: "mediaType", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {"countBrowse", -1}, {Key: "createdAt", Value: -1}}, + }, { + Keys: bson.D{{Key: "sId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "createdAt", Value: -1}}, + }, { + Keys: bson.D{{Key: "sId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "countLike", Value: -1}, {Key: "createdAt", Value: -1}}, + }, { + Keys: bson.D{{Key: "sId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "countCollect", Value: -1}, {Key: "createdAt", Value: -1}}, + }, { + Keys: bson.D{{Key: "sId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "countBrowse", Value: -1}, {Key: "createdAt", Value: -1}}, + }, { + Keys: bson.D{{Key: "sId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "hot", Value: -1}, {Key: "createdAt", Value: -1}}, + }, + // 首页优化 + { + Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "createdAt", Value: -1}}, + }, { + Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "countLike", Value: -1}, {Key: "createdAt", Value: -1}}, + }, { + Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "countBrowse", Value: -1}, {Key: "createdAt", Value: -1}}, + }, { + Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "countCollect", Value: -1}, {Key: "createdAt", Value: -1}}, + }, { + Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "hot", Value: -1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "mId", Value: 1}, {Key: "sortCode", Value: -1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{ + {Key: "mId", Value: 1}, + {Key: "status", Value: 1}, + {Key: "isDelete", Value: 1}, + {Key: "latestPublishedAt", Value: -1}, + {Key: "contentUpdateTime", Value: -1}, + {Key: "createdAt", Value: -1}, + {Key: "_id", Value: -1}, + }, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []*Media, hasNext bool, err error) { + opts := options.Find() + if len(sort) > 0 { + opts.SetSort(sort) + } + opts = opts.SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// QueryAllList 分页查询文档 +func QueryAllList(filter primitive.M, opts ...*options.FindOptions) (out []*Media, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// GetLatestIDForESSync 固定本轮同步的 ID 上界,仅读取已有主键索引。 +func GetLatestIDForESSync() (primitive.ObjectID, error) { + var data []*Media + opts := options.Find().SetSort(bson.D{{Key: "_id", Value: -1}}). + SetProjection(bson.M{"_id": 1}).SetHint("_id_").SetLimit(1).SetMaxTime(30 * time.Second) + if err := coll(nil).Find(&data, bson.M{}, opts); err != nil { + return primitive.NilObjectID, err + } + if len(data) == 0 { + return primitive.NilObjectID, nil + } + return data[0].ID, nil +} + +// GetListForESSync 不限制更新时间上界,防止浏览等持续更新使记录逃出同步窗口。 +// 使用稳定的 ID 游标,避免修改 updateTime 导致 skip 分页漏数据。 +func GetListForESSync(since time.Time, after, maxID primitive.ObjectID, size int) (data []*Media, err error) { + query := bson.M{ + "updateTime": bson.M{"$gte": since}, + "_id": bson.M{"$gt": after, "$lte": maxID}, + } + opts := options.Find().SetSort(bson.D{{Key: "_id", Value: 1}}). + SetHint("_id_").SetLimit(int64(size)).SetMaxTime(30 * time.Second) + err = coll(nil).Find(&data, query, opts) + return +} + +var MediaNotFound = errors.New("media not found") + +// GetInfoByMediaCenterId 通过mediaCenterId获取详细信息 +func GetInfoByMediaCenterId(mediaCenterId uint) (Media, error) { + v := Media{} + if err := coll(nil).FindOne(&v, bson.M{"mediaCenterId": mediaCenterId, "isDelete": false}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfoByMediaCenterId", table, "FindOne", err), + log.Any("mediaCenterId", mediaCenterId), + ) + return v, err + } + + if v.ID.IsZero() { + return v, MediaNotFound + } + + return v, nil +} + +// GetInfoByTitle 通过标题获取详细信息 +func GetInfoByTitle(title string) (Media, error) { + v := Media{} + if err := coll(nil).FindOne(&v, bson.M{"title": title, "isDelete": false}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfoByTitle", table, "FindOne", err), + log.Any("title", title), + ) + return v, err + } + + if v.ID.IsZero() { + return v, MediaNotFound + } + + return v, nil +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (Media, error) { + v := Media{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +func GetListByIds(ids []primitive.ObjectID) (list []Media, res map[primitive.ObjectID]Media, err error) { + if err := coll(nil).Find(&list, bson.M{"_id": bson.M{"$in": ids}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetListByIds", table, "Find", err), + log.Any("ids", ids), + ) + return nil, nil, err + } + res = make(map[primitive.ObjectID]Media) + for _, v := range list { + res[v.ID] = v + } + return list, res, nil +} + +// IncreaseCountLikeByIDs 更新媒体点赞数 +func IncreaseCountLikeByIDs(value int, ids ...primitive.ObjectID) (err error) { + var query = bson.M{} + if len(ids) == 1 { + query["_id"] = ids[0] + } else { + query["_id"] = bson.M{"$in": ids} + } + if _, err = coll(nil).UpdateOne(query, bson.M{"$inc": bson.M{"countLike": value}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncreaseLikeCountByID", table, "UpdateOne", err), + log.Any("id", ids), + log.Any("value", value), + ) + return + } + return +} + +// IncreaseCountShareByID 累计短剧真实分享次数,供推荐互动分计算。 +func IncreaseCountShareByID(id primitive.ObjectID, value int) error { + _, err := coll(nil).UpdateOne(bson.M{"_id": id, "mediaType": MediaTypeDrama}, bson.M{"$inc": bson.M{"countShare": value}}) + return err +} + +// IncrCommentCountByID 更新媒体评论数 +func IncrCommentCountByID(id primitive.ObjectID, value int) (err error) { + if _, err = coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$inc": bson.M{"countComment": value}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncrCommentCountByID", table, "UpdateOne", err), + log.Any("id", id), + log.Any("value", value), + ) + return + } + return +} + +// QueryMediaByID 查询文档 +func QueryMediaByID(filter primitive.M) (out *Media, err error) { + if err = coll(nil).FindOne(&out, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err), + log.Any("filter", filter), + ) + return nil, err + } + return +} + +// QueryMediaByCond 查询文档 +func QueryMediaByCond(filter primitive.M, opts *options.FindOptions) (out []*Media, err error) { + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err), + log.Any("filter", filter), + ) + return nil, err + } + return +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d Media) (data primitive.ObjectID, err error) { + normalizeLatestPublishedAt(&d) + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +func InsertMany(c []Media) error { + for i := range c { + normalizeLatestPublishedAt(&c[i]) + } + if _, err := coll(nil).InsertMany(c); err != nil { + log.Warn(fmt.Sprintf("METHOD-%s==> Model %s %s fail error:%+v:", "InsertMany", table, "InsertMany", err)) + return err + } + return nil +} + +func normalizeLatestPublishedAt(media *Media) { + if media == nil || media.Status != 1 || !media.LatestPublishedAt.IsZero() { + return + } + switch { + case !media.ContentUpdateTime.IsZero(): + media.LatestPublishedAt = media.ContentUpdateTime + case !media.UpdateTime.IsZero(): + media.LatestPublishedAt = media.UpdateTime + case !media.CreatedAt.IsZero(): + media.LatestPublishedAt = media.CreatedAt + default: + media.LatestPublishedAt = time.Now() + } +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// UpdateByIDS 根据ids更新数据 +func UpdateByIDS(t *db.MongoTool, ids []primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": bson.M{"$in": ids}} + return update(t, cond, data) +} + +// SectionVideosBySectionID 根据专题获取视频列表 +func SectionVideosBySectionID(sectionID primitive.ObjectID, opts *options.FindOptions) ([]*Media, error) { + var out []*Media + return out, coll(nil).Find(&out, bson.M{"sId": sectionID, "isDelete": false, "status": 1}, opts) +} + +type SectionWorkCount struct { + SectionID primitive.ObjectID `bson:"_id"` + Count int64 `bson:"count"` +} + +// CountActiveDramaBySectionIDs groups all visible short dramas for one bounded topic page. +func CountActiveDramaBySectionIDs(sectionIDs []primitive.ObjectID) (map[primitive.ObjectID]int64, error) { + counts := make(map[primitive.ObjectID]int64, len(sectionIDs)) + if len(sectionIDs) == 0 { + return counts, nil + } + var rows []SectionWorkCount + err := coll(nil).Aggregate(&rows, []bson.M{ + {"$match": bson.M{"sId": bson.M{"$in": sectionIDs}, "mediaType": MediaTypeDrama, "status": 1, "isDelete": false}}, + {"$group": bson.M{"_id": "$sId", "count": bson.M{"$sum": 1}}}, + }) + if err != nil { + return nil, err + } + for _, row := range rows { + counts[row.SectionID] = row.Count + } + return counts, nil +} + +// ListActiveDramaByInteractionScore computes the full-history ranking in MongoDB +// and only returns the requested page. The formula is: +// likes + collections*2 + comments*3 + shares*5. +func ListActiveDramaByInteractionScore(skip, limit int64) ([]*Media, error) { + if skip < 0 { + skip = 0 + } + if limit <= 0 || limit > 101 { + limit = 21 + } + var list []*Media + pipeline := []bson.M{ + {"$match": bson.M{"mediaType": MediaTypeDrama, "status": 1, "isDelete": false}}, + {"$addFields": bson.M{"_dramaInteractionScore": bson.M{"$add": []any{ + bson.M{"$ifNull": []any{"$countLike", 0}}, + bson.M{"$multiply": []any{bson.M{"$ifNull": []any{"$countCollect", 0}}, 2}}, + bson.M{"$multiply": []any{bson.M{"$ifNull": []any{"$countComment", 0}}, 3}}, + bson.M{"$multiply": []any{bson.M{"$ifNull": []any{"$countShare", 0}}, 5}}, + }}}}, + {"$sort": bson.D{{Key: "_dramaInteractionScore", Value: -1}, {Key: "latestPublishedAt", Value: -1}, {Key: "_id", Value: -1}}}, + {"$skip": skip}, + {"$limit": limit}, + {"$project": bson.M{"_dramaInteractionScore": 0}}, + } + err := coll(nil).Aggregate(&list, pipeline, options.Aggregate().SetAllowDiskUse(true)) + return list, err +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).UpdateOne(bson.M{"_id": id}, bson.M{"$set": bson.M{"isDelete": true}}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} + +// IncCollectCount 收藏次数增加 +func IncCollectCount(id ObjectID, inc int) error { + query := bson.M{"_id": id} + update := bson.M{"$set": bson.M{"updateTime": time.Now()}, "$inc": bson.M{"countCollect": inc}} + _, err := coll(nil).UpdateOne(query, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncCollectCount", table, "UpdateOne", err), + log.Any("id", id), + ) + return err + } + return nil +} + +// IncContentPurchasesCount 子集购买次数增加 +func IncContentPurchasesCount(id ObjectID, inc int) error { + query := bson.M{"_id": id} + update := bson.M{"$set": bson.M{"updateTime": time.Now()}, "$inc": bson.M{"countContentPurchases": inc}} + _, err := coll(nil).UpdateOne(query, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncPurchasesCount", table, "UpdateOne", err), + log.Any("id", id), + ) + return err + } + return nil +} + +// IncPurchasesCount 购买次数增加 +func IncPurchasesCount(id ObjectID, inc int) error { + return IncPurchasesCountWithTool(nil, id, inc) +} + +func IncPurchasesCountWithTool(t *db.MongoTool, id ObjectID, inc int) error { + query := bson.M{"_id": id} + update := bson.M{"$set": bson.M{"updateTime": time.Now()}, "$inc": bson.M{"countPurchases": inc}} + _, err := coll(t).UpdateOne(query, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncPurchasesCount", table, "UpdateOne", err), + log.Any("id", id), + ) + return err + } + return nil +} + +// IncBrowseCount 浏览次数增加 +func IncBrowseCount(id ObjectID, inc int) error { + query := bson.M{"_id": id} + update := bson.M{"$set": bson.M{"updateTime": time.Now()}, "$inc": bson.M{"countBrowse": inc}} + _, err := coll(nil).UpdateOne(query, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncBrowseCount", table, "UpdateOne", err), + log.Any("id", id), + ) + return err + } + return nil +} + +func Bulk(models []mongo.WriteModel) error { + _, err := coll(nil).Bulk(models, options.BulkWrite().SetOrdered(false)) + return err +} + +func GetListByLsjIds(lsjIds []string) ([]Media, error) { + filter := bson.M{ + "lsjId": bson.M{"$in": lsjIds}, + } + var ms []Media + return ms, coll(nil).Find(&ms, filter) +} + +func GetListByLsjId(lsjId string) (Media, error) { + filter := bson.M{ + "lsjId": lsjId, + } + var ms Media + return ms, coll(nil).FindOne(&ms, filter) +} + +// UpdateMany 根据ids更新数据 +func UpdateMany(t *db.MongoTool, filter bson.M, data bson.M) (int64, error) { + result, err := coll(t).UpdateMany(filter, data) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", filter), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// FindForReviewBatch 按 _id 游标分页扫描上架 Media,用于内容审查任务 +// 仅扫描 status=1(上架) +func FindForReviewBatch(lastID primitive.ObjectID, limit int64) ([]*Media, error) { + cond := bson.M{"status": 1} + if !lastID.IsZero() { + cond["_id"] = bson.M{"$gt": lastID} + } + opts := options.Find(). + SetSort(bson.D{{Key: "_id", Value: 1}}). + SetLimit(limit) + var list []*Media + if err := coll(nil).Find(&list, cond, opts); err != nil { + return nil, err + } + return list, nil +} + +// CountForReview 上架 Media 总数;条件与 FindForReviewBatch 保持一致 +func CountForReview() (int64, error) { + return coll(nil).Count(bson.M{"status": 1}) +} + +// UpdateForReview 内容审查通过后回写文本字段 +// title 对应 Media.Title,content 对应 Media.Summary +func UpdateForReview(id primitive.ObjectID, title, content string) error { + set := bson.M{} + if title != "" { + set["title"] = title + } + if content != "" { + set["summary"] = content + } + if len(set) == 0 { + return nil + } + set["updateTime"] = time.Now() + _, err := coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}) + return err +} + +// OffShelfManyForReview 内容审查命中后批量下架 Media(status -> 0) +// 仅当前 status=1 的会被改动 +func OffShelfManyForReview(ids []primitive.ObjectID) error { + if len(ids) == 0 { + return nil + } + _, err := coll(nil).UpdateMany( + bson.M{"_id": bson.M{"$in": ids}, "status": 1}, + bson.M{"$set": bson.M{"status": 0, "updateTime": time.Now()}}, + ) + return err +} diff --git a/models/v/mediamod/media_latest_published_test.go b/models/v/mediamod/media_latest_published_test.go new file mode 100644 index 0000000..ccef350 --- /dev/null +++ b/models/v/mediamod/media_latest_published_test.go @@ -0,0 +1,25 @@ +package mediamod + +import ( + "testing" + "time" +) + +func TestNormalizeLatestPublishedAt(t *testing.T) { + createdAt := time.Date(2026, 7, 20, 1, 0, 0, 0, time.UTC) + updateTime := createdAt.Add(time.Hour) + contentUpdateTime := updateTime.Add(time.Hour) + media := &Media{Status: 1, CreatedAt: createdAt, UpdateTime: updateTime, ContentUpdateTime: contentUpdateTime} + normalizeLatestPublishedAt(media) + if !media.LatestPublishedAt.Equal(contentUpdateTime) { + t.Fatalf("latestPublishedAt = %v, want %v", media.LatestPublishedAt, contentUpdateTime) + } +} + +func TestNormalizeLatestPublishedAtDoesNotPublishOffShelfMedia(t *testing.T) { + media := &Media{Status: 0, CreatedAt: time.Now()} + normalizeLatestPublishedAt(media) + if !media.LatestPublishedAt.IsZero() { + t.Fatalf("off-shelf media latestPublishedAt = %v, want zero", media.LatestPublishedAt) + } +} diff --git a/models/v/mediamod/type.go b/models/v/mediamod/type.go new file mode 100755 index 0000000..a7a06ba --- /dev/null +++ b/models/v/mediamod/type.go @@ -0,0 +1,130 @@ +package mediamod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +type MediaSubType int + +type Media struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + LsjId string `json:"lsjId" bson:"lsjId"` // 老司机ID + SortCode int `json:"sortCode" bson:"sortCode"` // 排序 + Status int `json:"status" bson:"status"` // 状态 0 下架 1 上架 + Title string `json:"title" bson:"title"` // 标题 + HorizontalCover string `json:"horizontalCover" bson:"horizontalCover"` // 横版封面 + VerticalCover string `json:"verticalCover" bson:"verticalCover"` // 竖版封面 + Tags []primitive.ObjectID `json:"tags" bson:"tags"` // 标签 + TagDetails []TagDetail `json:"tagDetails" bson:"tagDetails"` // 标签对象 + Summary string `json:"summary" bson:"summary"` // 简介 + TotalEpisode int `json:"totalEpisode" bson:"totalEpisode"` // 总集数 + CurrentEpisode int `json:"currentEpisode" bson:"currentEpisode"` // 当前更新的集数 + FreeEpisode int `json:"freeEpisode" bson:"freeEpisode"` // 免费集数 + UpdateStatus int `json:"updateStatus" bson:"updateStatus"` // 更新状态 1-连载中 2-已完结 + MediaType string `json:"mediaType" bson:"mediaType"` // 媒体类型 "video":视频,"image":图片,"text":小说 + MediaSubType MediaSubType `json:"mediaSubType" bson:"mediaSubType"` // 子类型 ,暂时只有小说使用 0-默认文本小说 1-有声小说 + Kind int `json:"kind" bson:"kind"` // 种类(1、哩番,2、3D,3、同人动画,4、单行本,5、连载漫画,6、韩漫 7、小说 ) + PermissionIconHide bool `json:"permissionIconHide" bson:"permissionIconHide"` // 售卖类型标识隐藏 + Permission int `json:"permission" bson:"permission"` // 收听权限 0:会员 1:金币购买 2:免费 + Style int `json:"style" bson:"style"` // 对应五种风格背景颜色,0默认前端随机 + Price int64 `json:"price" bson:"price"` // 价格(整部购买的类型 小说 cos 写真) + ContentsPrice int64 `json:"contentsPrice" bson:"contentsPrice"` // 所有子集加起来的价格 + Direction int64 `json:"direction" bson:"direction"` // 排版方向 0横 1竖(冗余字段) + FreeTime int `json:"freeTime" bson:"freeTime"` // 免费试看时长 单位 s + DefaultContent DefaultContent `json:"defaultContent" bson:"defaultContent"` // 默认展示内容,暂时只有漫画需要,展示第一章前五张图片 + MID primitive.ObjectID `json:"mId" bson:"mId"` // 模块ID + ModuleName string `json:"moduleName" bson:"moduleName"` // 模块名称 + SectionName string `json:"sectionName" bson:"sectionName"` // 专题名称 + SID primitive.ObjectID `json:"sId" bson:"sId"` // 专题ID + SectionSort int `json:"sectionSort" bson:"sectionSort"` // 专题排序 + Choice primitive.ObjectID `json:"choice" bson:"choice"` // 精选 + ChoiceSort int `json:"choiceSort" bson:"choiceSort"` // 精选专题排序 + CountComment int64 `json:"countComment" bson:"countComment"` // 评论数 + CountPurchases int64 `json:"countPurchases" bson:"countPurchases"` // 购买数 + CountContentPurchases int64 `json:"countContentPurchases" bson:"countContentPurchases"` // 子集累计购买数 + CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` // 浏览数 + CountCollect int64 `json:"countCollect" bson:"countCollect"` // 收藏数 + CountLike int64 `json:"countLike" bson:"countLike"` // 喜欢数 + CountDisLike int64 `json:"countDisLike" bson:"countDisLike"` // 不喜欢数 + HasFollow bool `json:"hasFollow" bson:"hasFollow"` // 是否关注 + CountView int64 `json:"countView" bson:"countView"` // 展现数 + CountShare int64 `json:"countShare" bson:"countShare"` // 分享数,短剧推荐互动分使用 + Hot float64 `json:"hot" bson:"hot"` // 热度值 + Number int64 `json:"number" bson:"number"` // 编号 + Anchor string `json:"anchor" bson:"anchor"` // 作者 + CountPurchasesRate float32 `json:"countPurchasesRate" bson:"countPurchasesRate"` // 成交率 + IsDelete bool `json:"isDelete" bson:"isDelete"` // 是否删除 + UpdatedAct string `json:"updatedAct" bson:"updatedAct"` // 操作用户账号 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 文档创建时间 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 文档更新时间 + ContentUpdateTime time.Time `json:"contentUpdateTime" bson:"contentUpdateTime"` // 子集最新更新时间 用于连载排行榜 + LatestPublishedAt time.Time `json:"latestPublishedAt" bson:"latestPublishedAt"` // 最新上架或子集更新时间,用于“最新”列表排序 + MediaCenterId uint `json:"mediaCenterId" bson:"mediaCenterId,omitempty"` // 在媒资库中的唯一标识 + SyncTime int64 `json:"syncTime" bson:"syncTime,omitempty"` // 下一次需要同步的时间点位 + + //SellType int `json:"sellType" bson:"sellType"` // 商品售卖类型(0单集售卖 1整部售卖) +} + +type DefaultContent struct { + Id primitive.ObjectID `json:"id" bson:"id"` + Name string `json:"name" bson:"name"` + Cover string `json:"cover" bson:"cover"` + UrlSet []string `json:"urlSet" bson:"urlSet"` // 地址集(多个资源地址用 例如漫画) +} + +type ESMedia struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + Title string `json:"title" bson:"title"` // 标题 + HorizontalCover string `json:"horizontalCover" bson:"horizontalCover"` // 横版封面 + VerticalCover string `json:"verticalCover" bson:"verticalCover"` // 竖版封面 + Tags []primitive.ObjectID `json:"tags" bson:"tags"` // 标签 + Summary string `json:"summary" bson:"summary"` // 简介 + TotalEpisode int `json:"totalEpisode" bson:"totalEpisode"` // 总集数 + UpdateStatus int `json:"updateStatus" bson:"updateStatus"` // 更新状态 + MediaType string `json:"mediaType" bson:"mediaType"` // 媒体类型 "video":视频,"image":图片 + Kind int `json:"kind" bson:"kind"` // 种类(1、哩番,2、3D,3、同人动画,4、单行本,5、连载漫画,6、韩漫 7.有声小说 8.剧场) + PermissionIconHide bool `json:"permissionIconHide" bson:"permissionIconHide"` // 售卖类型标识隐藏 + Permission int `json:"permission" bson:"permission"` // 收听权限 0:会员 1:金币购买 2:免费 + Price int64 `json:"price" bson:"price"` // 价格(整部购买的类型 小说 cos 写真) + Direction int64 `json:"direction" bson:"direction"` // 排版方向 0横 1竖(冗余字段) + FreeTime int `json:"freeTime" bson:"freeTime"` // 免费试看时长 单位 s + MID primitive.ObjectID `json:"mId" bson:"mId"` // 模块ID + ModuleName string `json:"moduleName" bson:"moduleName"` // 模块名称 + SectionName string `json:"sectionName" bson:"sectionName"` // 专题名称 + SID primitive.ObjectID `json:"sId" bson:"sId"` // 专题ID + SectionSort int `json:"sectionSort" bson:"sectionSort"` // 专题排序 + CountLike int64 `json:"countLike" bson:"countLike"` // 喜欢数 + CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` // 浏览数 + CountCollect int64 `json:"countCollect" bson:"countCollect"` // 收藏数 + Hot float64 `json:"hot" bson:"hot"` // 热度值 + Choice primitive.ObjectID `json:"choice" bson:"choice"` // 精选 + ChoiceSort int `json:"choiceSort" bson:"choiceSort"` // 精选专题排序 + Number int64 `json:"number" bson:"number"` // 编号 + SortCode int `json:"sortCode" bson:"sortCode"` // 排序 + Status int `json:"status" bson:"status"` // 状态 + IsDelete bool `json:"isDelete" bson:"isDelete"` // 是否删除 + UpdatedAct string `json:"updatedAct" bson:"updatedAct"` // 操作用户账号 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 文档创建时间 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 文档更新时间 +} + +type ESMediaSource struct { + ID primitive.ObjectID `json:"_id"` + Source ESMedia `json:"_source"` +} + +type ESVideoSourceWithTotal struct { + Hits []ESMediaSource `json:"hits"` + Total Total `json:"total"` +} + +type Total struct { + Relation string `json:"relation"` + Value int `json:"value"` +} diff --git a/models/v/mediamod/update_marker.go b/models/v/mediamod/update_marker.go new file mode 100644 index 0000000..e2fc062 --- /dev/null +++ b/models/v/mediamod/update_marker.go @@ -0,0 +1,48 @@ +package mediamod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type moduleLatestContentAt struct { + ModuleID primitive.ObjectID `bson:"_id"` + LatestCreatedAt time.Time `bson:"latestCreatedAt"` + LatestContentUpdated time.Time `bson:"latestContentUpdated"` +} + +// LatestContentAtByModules 返回动漫、漫画等媒体亚模块最近一次新增或子集更新时间。 +func LatestContentAtByModules(moduleIDs []primitive.ObjectID) (map[string]time.Time, error) { + result := make(map[string]time.Time, len(moduleIDs)) + if len(moduleIDs) == 0 { + return result, nil + } + pipeline := []bson.M{ + {"$match": bson.M{ + "status": 1, + "isDelete": false, + "mId": bson.M{"$in": moduleIDs}, + }}, + {"$group": bson.M{ + "_id": "$mId", + "latestCreatedAt": bson.M{"$max": "$createdAt"}, + "latestContentUpdated": bson.M{"$max": "$contentUpdateTime"}, + }}, + } + var rows []moduleLatestContentAt + if err := coll(nil).Aggregate(&rows, pipeline); err != nil { + return nil, err + } + for _, row := range rows { + latest := row.LatestCreatedAt + if row.LatestContentUpdated.After(latest) { + latest = row.LatestContentUpdated + } + if !row.ModuleID.IsZero() && !latest.IsZero() { + result[row.ModuleID.Hex()] = latest + } + } + return result, nil +} diff --git a/models/v/mediamod/web.go b/models/v/mediamod/web.go new file mode 100644 index 0000000..90bbbf9 --- /dev/null +++ b/models/v/mediamod/web.go @@ -0,0 +1,200 @@ +package mediamod + +import ( + "91porn-server/common/constant" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type MediaType int +type KindType int + +const ( + _ = iota + RiFan // 1、哩番 + ThreeD // 2、3D + FanAnimation // 3、同人动画 + Offprint // 4、单行本 + ComicStrip // 5、连载漫画 + KoreanCartoon // 6、韩漫 + AudioNovel // 7、有声小说 + Theater // 8、剧场 +) +const ( + _ = iota + Animation // 1、动漫 + Cartoon // 2、漫画 + Novel // 3、小说 +) + +const ( + Default = iota // 0 默认 + Update // 1、更新中 + Finish // 2、已完结 +) + +const ( + SellTypeUnit = 0 // 单集购买 + SellTypeSet = 1 // 整部购买 + + MediaTypeVideo = constant.Cartoon // 视频 + MediaTypeImage = constant.Comics // 图片 + MediaTypeText = constant.Text // 音频小说 + MediaTypeDrama = constant.Drama // 短剧 +) + +type SendBaseInfo2AWsReq struct { + SourceID string `form:"sourceID" json:"sourceID"` + Md5 string `form:"md5" json:"md5"` + Title string `form:"title" json:"title"` + PlayTime uint `form:"playTime" json:"playTime"` + MediaSize int `form:"mediaSize" json:"mediaSize"` + Filename string `form:"filename" json:"filename"` + Tags []primitive.ObjectID `form:"tags" json:"tags" binding:"required"` +} + +type TagDetail struct { + Id primitive.ObjectID `json:"id"` // 标签id + Name string `json:"name"` // 标签名 +} + +type WebExportMedia struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty" xlsx:"ID"` // 文档id + Title string `json:"title" bson:"title" xlsx:"标题"` // 标题 + Kind string `json:"kind" bson:"kind" xlsx:"种类"` // 种类(1、哩番,2、3D,3、同人动画,4、单行本,5、连载漫画,6、韩漫 7、小说 ) + Direction string `json:"direction" bson:"direction" xlsx:"排版方向"` // 排版方向 0横 1竖(冗余字段) + TotalEpisode int `json:"totalEpisode" bson:"totalEpisode" xlsx:"总集数"` // 总集数 + UpdateStatus string `json:"updateStatus" bson:"updateStatus" xlsx:"更新状态"` // 更新状态 + Status string `json:"status" bson:"status" xlsx:"状态"` // 状态 0 下架 1 上架 + ModuleName string `json:"moduleName" bson:"moduleName" xlsx:"模块名称"` // 模块名称 + SectionName string `json:"sectionName" bson:"sectionName" xlsx:"专题名称"` // 专题名称 + Tags string `json:"tags" bson:"tags" xlsx:"标签"` // 标签 + MediaType string `json:"mediaType" bson:"mediaType" xlsx:"类型"` // 媒体类型 "video":视频,"image":图片,"text":小说 + FreeTime int `json:"freeTime" bson:"freeTime" xlsx:"免费时长"` // 免费试看时长 单位 s + Price int64 `json:"price" bson:"price" xlsx:"价格"` // 价格(整部购买的类型 小说 cos 写真) + CountBrowse int64 `json:"countBrowse" bson:"countBrowse" xlsx:"浏览数"` // 浏览数 + CountCollect int64 `json:"countCollect" bson:"countCollect" xlsx:"收藏数"` // 收藏数 + CountComment int64 `json:"countComment" bson:"countComment" xlsx:"评论数"` // 评论数 + CountLike int64 `json:"countLike" bson:"countLike" xlsx:"喜欢数"` // 喜欢数 + CountPurchases int64 `json:"countPurchases" bson:"countPurchases" xlsx:"购买数"` // 购买数 + CountView int64 `json:"countView" bson:"countView" xlsx:"展现数"` // 展现数 + Permission string `json:"permission" bson:"permission" xlsx:"观看权限"` // 收听权限 0:会员 1:金币购买 2:免费 + PermissionIconHide bool `json:"permissionIconHide" bson:"permissionIconHide" xlsx:"售卖类型标识隐藏"` // 售卖类型标识隐藏 + Summary string `json:"summary" bson:"summary" xlsx:"简介"` // 简介 + UpdatedAct string `json:"updatedAct" bson:"updatedAct" xlsx:"操作人"` // 操作用户账号 + CreatedAt string `json:"createdAt" bson:"createdAt" xlsx:"创建时间"` // 文档创建时间 + UpdateTime string `json:"updateTime" bson:"updateTime" xlsx:"更新时间"` // 文档更新时间 + +} + +func MediaTypeName(mediaType string) string { + var name string + switch mediaType { + case "video": + name = "动漫" + case "image": + name = "漫画" + case "text": + name = "小说" + case "drama": + name = "短剧" + } + return name +} + +func PermissionName(status int) string { + var name string + // 观看权限 0:会员 1:金币购买 2:免费 + switch status { + case 0: + name = "会员" + case 1: + name = "金币购买" + case 2: + name = "免费" + default: + name = "未知" + } + return name +} + +func SellTypeName(status int) string { + var name string + // 商品售卖类型(0单集售卖 1整部售卖) + switch status { + case 0: + name = "单集售卖" + case 1: + name = "整部售卖" + default: + name = "未知" + } + return name +} + +func StatusName(status int) string { + var name string + // 状态 1 上架 2 下架 + switch status { + case 1: + name = "上架" + case 2: + name = "下架" + default: + name = "未知" + } + return name +} + +func UpdateStatusName(updateStatus int) string { + var name string + // 排版方向 0横 1竖(冗余字段) + switch updateStatus { + case 0, 1: + name = "连载中" + case 2: + name = "已完结" + default: + name = "未知" + } + return name +} + +func DirectionName(direction int64) string { + var name string + // 排版方向 0横 1竖(冗余字段) + switch direction { + case 0: + name = "横版" + case 1: + name = "竖版" + default: + name = "未知" + } + return name +} + +func KindName(kind int) string { + var name string + // 1、哩番,2、3D,3、同人动画,4、单行本,5、连载漫画,6、韩漫 7、小说 + switch kind { + case 1: + name = "哩番" + case 2: + name = "3D" + case 3: + name = "同人动画" + case 4: + name = "单行本" + case 5: + name = "连载漫画" + case 6: + name = "韩漫" + case 7: + name = "小说" + case 8: + name = "剧场" + default: + name = "未知" + } + return name +} diff --git a/models/v/mediatagdimensionmod/mediatagdimension.go b/models/v/mediatagdimensionmod/mediatagdimension.go new file mode 100755 index 0000000..2f58fe6 --- /dev/null +++ b/models/v/mediatagdimensionmod/mediatagdimension.go @@ -0,0 +1,167 @@ +package mediatagdimensionmod + +import ( + "91porn-server/models" + "encoding/json" + "errors" + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.MediaTagDimension + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "sortCode", Value: 1}}, + }, + { + Keys: bson.D{{Key: "name", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []MediaTagDimension, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// QueryAllList 分页查询文档 +func QueryAllList(filter primitive.M, opts ...*options.FindOptions) (out []*MediaTagDimension, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (MediaTagDimension, error) { + v := MediaTagDimension{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d MediaTagDimension) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} diff --git a/models/v/mediatagdimensionmod/type.go b/models/v/mediatagdimensionmod/type.go new file mode 100755 index 0000000..cbdba31 --- /dev/null +++ b/models/v/mediatagdimensionmod/type.go @@ -0,0 +1,30 @@ +package mediatagdimensionmod + +import ( + "time" + + "91porn-server/common/db" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +type MediaTagDimension struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + SortCode int `json:"sortCode" bson:"sortCode"` // 排序 + Status int `json:"status" bson:"status"` // 是否开启 1:开启,2:关闭 + Type int `json:"type" bson:"type"` // 类型 1:动漫,2:漫画 + Name string `json:"name" bson:"name"` // 名称 + IsDelete bool `json:"isDelete" bson:"isDelete"` // 是否删除 + UpdatedAct string `json:"updatedAct" bson:"updatedAct"` // 操作用户账号 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 文档创建时间 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 文档更新时间 +} + +type MediaTagDimensionType int + +const ( + _ = iota + Animation // 1、动漫 + Cartoon // 2、漫画 +) diff --git a/models/v/mediatagdimensionmod/web.go b/models/v/mediatagdimensionmod/web.go new file mode 100644 index 0000000..e876704 --- /dev/null +++ b/models/v/mediatagdimensionmod/web.go @@ -0,0 +1,11 @@ +package mediatagdimensionmod + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebMediaTagDimension struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + Type int `json:"type" bson:"type"` // 类型 1:动漫,2:漫画 + Name string `json:"name" bson:"name"` // 名称 +} diff --git a/models/v/mediatagmod/mediatag.go b/models/v/mediatagmod/mediatag.go new file mode 100755 index 0000000..77feec2 --- /dev/null +++ b/models/v/mediatagmod/mediatag.go @@ -0,0 +1,312 @@ +package mediatagmod + +import ( + "91porn-server/models" + "encoding/json" + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.MediaTag + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{"kind", 1}}, + }, + { + Keys: bson.D{{"dimension", 1}}, + }, + { + Keys: bson.D{{"sort", 1}}, + }, + { + Keys: bson.D{{"isDiscovery", 1}}, + }, + { + Keys: bson.D{{Key: "name", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []MediaTag, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (MediaTag, error) { + v := MediaTag{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d MediaTag) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// Insert 插入记录 +func InsertMany(t *db.MongoTool, d []MediaTag) (err error) { + _, err = coll(t).InsertMany(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + } + return +} + +// FindTagsByIDs 根据ids获取标签信息 +func FindTagsByIDs(ids []primitive.ObjectID) (data []*MediaTag, err error) { + err = coll(nil).Find(&data, bson.M{"_id": bson.M{"$in": ids}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindTagsByIDs", table, "FindOne", err), + log.Any("ids", ids), + ) + return + } + return +} + +// FindFilterToMap 条件查询配置列表 +func FindFilterToMap(filter bson.M) (map[primitive.ObjectID]*MediaTag, error) { + var out []*MediaTag + filter["active"] = true + if err := coll(nil).Find(&out, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindFilterToMap", models.Activity, "Find", err), + log.Any("filter", filter), + ) + return nil, err + } + m := make(map[primitive.ObjectID]*MediaTag, len(out)) + for i := 0; i < len(out); i++ { + m[out[i].ID] = out[i] + } + return m, nil +} + +// QueryAllList 分页查询文档 +func QueryAllList(filter primitive.M, opts ...*options.FindOptions) (out []*MediaTag, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// FindOneTagByName 根据name获取标签信息 +func FindOneTagByName(name string) (data MediaTag, err error) { + err = coll(nil).FindOne(&data, bson.M{"name": name}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneTagByName", table, "FindOne", err), + log.Any("name", name), + ) + return + } + return +} + +// FindOneTagByFilter 根据过滤的条件获取标签信息 +func FindOneTagByFilter(filter bson.M) (data MediaTag, err error) { + err = coll(nil).FindOne(&data, filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneTagByFilter", table, "FindOne", err), + log.Any("filter", filter), + ) + return + } + return +} + +// FindTagIdsByName 根据name获取相关的标签id +func FindTagIdsByName(name string) (ids []primitive.ObjectID, err error) { + list := []MediaTag{} + err = coll(nil).Find(&list, bson.M{"name": name}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindTagIdsByName", table, "Find", err), + log.Any("name", name), + ) + return + } + for _, v := range list { + ids = append(ids, v.ID) + } + return +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} + +// 这里使用type, 兼容老版本 +func FindOneTagByNames(name []string, typ int) (data map[string]primitive.ObjectID, err error) { + data = make(map[string]primitive.ObjectID) + var datas []MediaTag + filter := bson.M{"name": bson.M{"$in": name}} + if typ > 0 { + filter["type"] = typ + } + err = coll(nil).Find(&datas, filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneTagByNames", table, "FindOne", err), + log.Any("name", name), + ) + return + } + if datas != nil && len(datas) > 0 { + for _, tag := range datas { + data[tag.Name] = tag.ID + } + } + return +} + +// FindOneTagOrInset 根据name获取标签信息若不存在则创建 +func FindOneTagOrInset(name string) (data MediaTag, err error) { + err = coll(nil).FindOne(&data, bson.M{"name": name}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneTagByName", table, "FindOne", err), + log.Any("name", name), + ) + return + } + if data.ID.IsZero() { + data = MediaTag{ + //Kind: 0, + Type: 1, + Dimension: "", + Name: name, + Active: true, + IsDiscovery: false, + Library: false, + Sort: 0, + IsDelete: false, + UpdatedAct: "", + CreatedAt: time.Now(), + UpdateTime: time.Now(), + } + + _, err = coll(nil).InsertOne(&data) + if err != nil { + return data, err + } + } + return +} diff --git a/models/v/mediatagmod/type.go b/models/v/mediatagmod/type.go new file mode 100755 index 0000000..478abda --- /dev/null +++ b/models/v/mediatagmod/type.go @@ -0,0 +1,27 @@ +package mediatagmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +type MediaTag struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + Kind int `json:"kind" bson:"kind"` // 种类 1、哩番 2、3D 3、同人动画 4、单行本 5、连载漫画 6、韩漫 + Type int `json:"type" bson:"type"` // 类型 (1、动漫 2、漫画 3-文本小说 4-有声小说 5-剧场) + Dimension string `json:"dimension" bson:"dimension"` // 维度 + Name string `json:"name" bson:"name"` // 标签名称 + Active bool `json:"active" bson:"active"` // 是都激活 + IsDiscovery bool `json:"isDiscovery" bson:"isDiscovery"` // 是否推荐发现 + Library bool `json:"library" bson:"library"` // 片库开关 + Sort int `json:"sort" bson:"sort"` // 排序码 + IsDelete bool `json:"isDelete" bson:"isDelete"` // 是否删除 + UpdatedAct string `json:"updatedAct" bson:"updatedAct"` // 操作用户账号 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 文档创建时间 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 文档更新时间 +} diff --git a/models/v/messagemod/app.go b/models/v/messagemod/app.go new file mode 100644 index 0000000..953e690 --- /dev/null +++ b/models/v/messagemod/app.go @@ -0,0 +1,100 @@ +package messagemod + +import ( + "91porn-server/common/constant" + "91porn-server/models/commod" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 查询列表 +type QueryCond struct { + commod.Page + MsgType int `form:"msgType" json:"msgType" binding:"required"` //1.点赞消息 2.评论消息 +} + +// 查询会话消息列表 +type QueryMsgCond struct { + SessionId string `form:"sessionId" json:"sessionId" binding:"required"` + commod.Page +} + +// AddMsgReqInfo 新增私信实体 +type AddMsgReqInfo struct { + TakeUid uint64 `form:"takeUid" json:"takeUid" binding:"required"` // 接受用户uid + Content string `form:"content" json:"content"` // 消息内容 + ImgUrl []string `form:"imgUrl" json:"imgUrl"` // 图片链接 +} + +// ChargePrivateLetterReq 私信扣费请求 +type ChargePrivateLetterReq struct { + TakeUid uint64 `form:"takeUid" json:"takeUid" binding:"required"` // 接受用户uid +} + +// ChargePrivateLetterResp 私信扣费响应 +type ChargePrivateLetterResp struct { + Price int64 `json:"price"` // 本次应扣金币数 + DeductAmount int64 `json:"deductAmount"` // 本次扣除金币余额 + DeductIncome int64 `json:"deductIncome"` // 本次扣除收益余额 + Amount int64 `json:"amount"` // 扣费后金币余额 + Income int64 `json:"income"` // 扣费后收益余额 + Balance int64 `json:"balance"` // 扣费后总余额 +} + +// 已读实体 +type ReadMsgReqInfo struct { + MsgIds []string `form:"msgIds" json:"msgIds" binding:"required"` //阅读消息id +} + +// MessageApp 消息app返回实体 +type MessageApp struct { + ID primitive.ObjectID `json:"id" bson:"_id"` // id + SendUid uint64 `json:"sendUid" bson:"sendUid"` // 发送者uid + SendName string `json:"sendName" bson:"sendName"` // 发送者姓名 + SendAvatar string `json:"sendAvatar" bson:"sendAvatar"` // 发送者头像 + TakeUid uint64 `json:"takeUid" bson:"takeUid"` // 接收者uid + TakeName string `json:"takeName" bson:"takeName"` // 接收者姓名 + TakeAvatar string `json:"takeAvatar" bson:"takeAvatar"` // 接收者头像 + PeerImUserID int64 `json:"peerImUserId" bson:"peerImUserId"` // 对端 IM 平台用户 ID + Content string `json:"content" bson:"content"` // 消息内容 + ContentType int `json:"contentType" bson:"contentType"` // 消息内容类型 + ImgUrl []string `json:"imgUrl" bson:"imgUrl"` // 消息图片内容链接 + IsRead bool `json:"isRead" bson:"isRead"` // 消息已读or未读 + SessionId string `json:"sessionId" bson:"sessionId"` // 会话id,私聊信息才有,便于分页查询 + LinkUrl string `json:"linkUrl" bson:"linkUrl"` // 附带链接地址 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + ObjId primitive.ObjectID `json:"objId" bson:"objId"` // 动态消息使用,eg:被转发、点赞、评论帖子id + ObjName string `json:"objName" bson:"objName"` // 动态消息使用,eg:被转发、点赞、评论帖子名称 +} + +// MsgDynamicsApp 消息动态app返回实体 +type MsgDynamicsApp struct { + ID primitive.ObjectID `json:"id" bson:"_id"` // id + SendUid uint64 `json:"sendUid" bson:"sendUid"` // 发送者uid + SendName string `json:"sendName" bson:"sendName"` // 发送者姓名 + SendAvatar string `json:"sendAvatar" bson:"sendAvatar"` // 发送者头像 + SendGender string `json:"sendGender" bson:"sendGender"` // 发送者性别 + SuperUser int `json:"SuperUser" bson:"SuperUser"` // 发送者是否大V + Awards []int `json:"awards" bson:"awards"` // 发送者奖章 + MsgType MsgType `json:"msgType" bson:"msgType"` // 消息类型 + Content string `json:"content" bson:"content"` // 消息内容 + IsRead bool `json:"isRead" bson:"isRead"` // 消息已读or未读 + ObjId primitive.ObjectID `json:"objId" bson:"objId"` // 动态消息使用,eg:被转发、点赞、评论帖子id + ObjName string `json:"objName" bson:"objName"` // 动态消息使用,eg:被转发、点赞、评论帖子名称 + ObjCover string `json:"objCover" bson:"objCover"` // 动态消息使用,eg:被转发、点赞、评论帖子封面图 + ObjType constant.NewsType `json:"objType" bson:"objType"` // 动态消息使用,eg:被转发、点赞、评论帖子类型 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + VipExpireDate time.Time `json:"vipExpireDate,omitempty" bson:"vipExpireDate"` + VipLevel int `json:"vipLevel"` + UserGroup []UserInfo `json:"userGroup" bson:"userGroup"` // 相同动态消息的用户集合,保留前十 + UserCount int `json:"userCount" bson:"userCount"` // 相同动态消息的用户数 + LikeCount int `json:"likeCount"` // 点赞数 + ImgUrl string `json:"imgUrl"` // 帖子封面 +} + +// NoReadMsgDynamicsCountApp 未读消息数 +type NoReadMsgDynamicsCountApp struct { + DtCount int `json:"dtCount"` // 动态未读数 + MsgCount int `json:"msgCount"` // 消息未读数 +} diff --git a/models/v/messagemod/message.go b/models/v/messagemod/message.go new file mode 100644 index 0000000..f10e570 --- /dev/null +++ b/models/v/messagemod/message.go @@ -0,0 +1,262 @@ +package messagemod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "encoding/json" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.ChatMessage + +const NoRedDynamicNumRedisKey = "user-no-red-dynamic-num" + +// Coll 获取表名 +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{"sendUid", 1}}, + }, + { + Keys: bson.D{{"takeUid", 1}}, + }, + { + Keys: bson.D{{"msgType", 1}}, + }, + { + Keys: bson.D{{"sessionId", 1}}, + }, + { + Keys: bson.D{{"createdAt", -1}}, + }, + { + Keys: bson.D{{"msgType", 1}, {"sessionId", 1}, {"takeUid", 1}, {"isRead", 1}}, + }, + { + Keys: bson.D{{"sessionId", 1}, {"takeUid", 1}, {"isRead", 1}}, + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } + return +} + +// InsertOne 新增 +func InsertOne(mt *db.MongoTool, t *Message) (data primitive.ObjectID, err error) { + result, err := coll(mt).InsertOne(&t) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "Marshal", err)) + return + } + err = data.UnmarshalJSON(byteID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "UnmarshalJSON", err)) + return + } + return +} + +// 查询总条数 +func Count(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Count", models.Activity, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// DeleteMessages 删除 +func DeleteMessages(ids []primitive.ObjectID) (int64, error) { + if ids == nil { + ids = []primitive.ObjectID{} + } + cond := bson.M{"_id": bson.M{"$in": ids}} + result, err := coll(nil).DeleteMany(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteMessages", table, "DeleteMany", err), log.Any("ids", ids)) + return 0, err + } + return result.DeletedCount, nil +} + +// DeleteMsgBySessionId 根据会话消息删除会话聊天消息 +func DeleteMsgBySessionId(sessionId string) (int64, error) { + cond := bson.M{"sessionId": sessionId, "msgType": PrivateLetterMsg} + result, err := coll(nil).DeleteMany(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteMsgBySessionId", table, "DeleteMany", err), log.Any("sessionId", sessionId)) + return 0, err + } + return result.DeletedCount, nil +} + +// FindOneByFilter 根据条件查询单个信息 +func FindOneByFilter(cond bson.M) (data Message, err error) { + opts := options.FindOne().SetSort(bson.D{{"createdAt", -1}}) + err = coll(nil).FindOne(&data, cond, opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneByFilter", table, "FindOne", err), + log.Any("cond", cond), + ) + return + } + return +} + +// Query 查询列表,app使用 +func Query(cond bson.M, pageSize, pageNumber int) (data []MessageApp, hasNext bool, err error) { + data = make([]MessageApp, 0) + skip := pageSize * (pageNumber - 1) + sort := bson.D{{"createdAt", -1}} + opts := options.Find().SetSkip(int64(skip)).SetLimit(int64(pageSize) + 1).SetSort(sort) + err = coll(nil).Find(&data, cond, opts) + if err != nil { + log.Error("Query", log.Any("cond", cond), log.Any("skip", skip), log.Any("limit", pageSize), log.E(err)) + return + } + if len(data) > pageSize { + hasNext = true + data = data[:pageSize] + } + return +} + +// 查询动态列表,app使用 +func QueryDynamics(cond bson.M, pageSize, pageNumber int) (data []MsgDynamicsApp, hasNext bool, err error) { + data = make([]MsgDynamicsApp, 0) + skip := pageSize * (pageNumber - 1) + sort := bson.D{{"createdAt", -1}} + opts := options.Find().SetSkip(int64(skip)).SetLimit(int64(pageSize) + 1).SetSort(sort) + err = coll(nil).Find(&data, cond, opts) + if err != nil { + log.Error("QueryDynamics", log.Any("cond", cond), log.Any("skip", skip), log.Any("limit", pageSize), log.E(err)) + return + } + if len(data) > pageSize { + hasNext = true + data = data[:pageSize] + } + return +} + +// FindByFilter 条件查询次数,app使用 +func FindByFilter(cond bson.M) (data []*MsgDynamicsApp, err error) { + err = coll(nil).Find(&data, cond) + if err != nil { + log.Error("FindByFilter", log.Any("cond", cond), log.E(err)) + return + } + return +} + +// UpdIsRead 更新消息已读状态 +func UpdIsRead(t *db.MongoTool, uid uint64, ids []primitive.ObjectID) (err error) { + filter := bson.M{"_id": bson.M{"$in": ids}, "isRead": false, "takeUid": uid} + set := bson.M{"$set": bson.M{"isRead": true, "updatedAt": time.Now()}} + _, err = coll(t).UpdateMany(filter, set) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdIsRead", table, "UpdateMany", err), + log.Any("ids", ids), + ) + return err + } + return +} + +// UpdIsReadBySessionIdAndUid 根据sessionId更新消息已读状态 +func UpdIsReadBySessionIdAndUid(t *db.MongoTool, uid uint64, sessionId string) (err error) { + msgTypes := [...]string{string(PrivateLetterMsg), string(OfficialPrivateLetterMsg)} + filter := bson.M{"msgType": bson.M{"$in": msgTypes}, "sessionId": sessionId, "takeUid": uid, "isRead": false} + set := bson.M{"$set": bson.M{"isRead": true}} + _, err = coll(t).UpdateMany(filter, set) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdIsReadBySessionIdAndUid", table, "UpdateMany", err), + log.Any("uid", uid), log.Any("sessionId", sessionId)) + return err + } + return +} + +// UpdDynamicsIsReadByUid 根据uid更新动态消息已读状态 +func UpdDynamicsIsReadByUid(t *db.MongoTool, uid uint64) (err error) { + filter := bson.M{"msgType": bson.M{"$in": GetDynamics()}, "takeUid": uid, "isRead": false} + set := bson.M{"$set": bson.M{"isRead": true}} + _, err = coll(t).UpdateMany(filter, set) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdDynamicsIsReadByUid", table, "UpdateMany", err), log.Any("uid", uid)) + return err + } + return +} + +// QueryAllDocument 分页查询文档 +func QueryAllDocument(filter primitive.M, opts ...*options.FindOptions) ([]*Message, error) { + var out []*Message + if err := coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-QueryAllDocument]==> Model %s Find fail error:%+v:", table, err), + log.Any("filter", filter), + ) + return nil, err + } + return out, nil +} + +// CountDocument 查询文档条目数 +func CountDocument(filter primitive.M) (int64, error) { + count, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-CountDocument]==> Model %s Count fail error:%+v:", table, err), + log.Any("filter", filter), + ) + return 0, err + } + return count, nil +} + +// RemoveDocument 删除用户私聊信息 +func RemoveDocument(filter primitive.M) error { + _, err := coll(nil).DeleteOne(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RemoveDocument", table, "DeleteOne", err), + log.Any("filter", filter), + ) + } + return err +} + +// UpdOneByFilter 更新 +func UpdOneByFilter(t *db.MongoTool, cond bson.M, set bson.M) (err error) { + _, err = coll(t).UpdateOne(cond, set) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdOneByFilter", table, "UpdateOne", err), log.Any("cond", cond), log.Any("set", cond)) + return err + } + return +} diff --git a/models/v/messagemod/type.go b/models/v/messagemod/type.go new file mode 100644 index 0000000..f32b9fb --- /dev/null +++ b/models/v/messagemod/type.go @@ -0,0 +1,95 @@ +package messagemod + +import ( + "91porn-server/common/constant" + "91porn-server/common/db" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Init 初始化标签model索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +// 类型 +type MsgType string +type ContentType int + +const ( + PrivateLetterMsg MsgType = "private_letter" // 私信消息 + OfficialPrivateLetterMsg MsgType = "official_private_letter" // 官方私信消息 + LikeMsg MsgType = "like_msg" // 点赞消息,属于动态 + FollowMsg MsgType = "follow_msg" // 关注消息,属于动态 + ForwardMsg MsgType = "forward_msg" // 转发消息,属于动态 + CommentMsg MsgType = "comment_msg" // 评论消息,属于动态 + LikeCommentMsg MsgType = "like_comment_msg" // 点赞评论消息,属于动态 + ReplyCommentMsg MsgType = "reply_comment_msg" // 回复评论消息,属于动态 + DesireMsg MsgType = "desire_msg" // 心愿工单消息消息,属于动态 + +) +const ( + _ ContentType = iota + PromotionNotice // 1 推广通知 + WithdrawalNotice // 2 提现通知 + AuditNotice // 3 审核通知 + BigVNotice // 4 大V通知 +) + +func GetDynamics() []MsgType { + var dynamics = []MsgType{LikeMsg, FollowMsg, ForwardMsg, CommentMsg, DesireMsg, OfficialPrivateLetterMsg, LikeCommentMsg, ReplyCommentMsg} + return dynamics +} + +func GetDynamicsList() []MsgType { + var dynamics = []MsgType{LikeMsg, FollowMsg, ForwardMsg, CommentMsg, DesireMsg, LikeCommentMsg, ReplyCommentMsg} + return dynamics +} + +//动态类型数组 + +// Message 消息 +type Message struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // id + SendUid uint64 `json:"sendUid" bson:"sendUid"` // 发送者uid + TakeUid uint64 `json:"takeUid" bson:"takeUid"` // 接收者uid + MsgType MsgType `json:"msgType" bson:"msgType"` // 消息类型 + ContentType int `json:"contentType" bson:"contentType"` // 消息内容类型 + Content string `json:"content" bson:"content"` // 消息内容 + ImgUrl []string `json:"imgUrl" bson:"imgUrl"` // 消息图片内容链接 + SessionId string `json:"sessionId" bson:"sessionId"` // 会话id,私聊信息才有,便于分页查询 + IsRead bool `json:"isRead" bson:"isRead"` // 消息已读or未读 + ObjId primitive.ObjectID `json:"objId" bson:"objId"` // 动态消息使用,eg:被转发、点赞、评论帖子id + ObjName string `json:"objName" bson:"objName"` // 动态消息使用,eg:被转发、点赞、评论帖子名称 + ObjCover string `json:"objCover" bson:"objCover"` // 动态消息使用,eg:被转发、点赞、评论帖子封面图 + ObjType constant.NewsType `json:"objType" bson:"objType"` // 动态消息使用,eg:被转发、点赞、评论帖子类型 + UserGroup []UserInfo `json:"userGroup" bson:"userGroup"` // 相同动态消息的用户集合,保留前十 + UserCount int `json:"userCount" bson:"userCount"` // 相同动态消息的用户数 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 +} + +type UserInfo struct { + Uid uint64 `json:"uid" bson:"uid"` // 用户id + Name string `json:"name" bson:"name"` // 用户名 + Avatar string `json:"avatar" bson:"avatar"` // 用户头像 +} + +// PublicAddMsg 添加消息公共实体 +type PublicAddMsg struct { + SendUid uint64 `json:"sendUid" bson:"sendUid"` // 发送者uid + TakeUid uint64 `json:"takeUid" bson:"takeUid"` // 接收者uid + SessionId string `json:"sessionId" bson:"sessionId"` // 会话id,私聊信息才有,便于分页查询 + Content string `json:"content" bson:"content"` // 消息内容 + ImgUrl []string `json:"imgUrl" bson:"imgUrl"` // 消息图片内容链接 + ContentType int `json:"contentType" bson:"contentType"` // 消息内容类型 + MsgType MsgType `json:"msgType" bson:"msgType"` // 消息类型 + ObjId primitive.ObjectID `json:"objId" bson:"objId"` // 动态消息使用,eg:被转发、点赞、评论帖子id + ObjName string `json:"objName" bson:"objName"` // 动态消息使用,eg:被转发、点赞、评论帖子名称 + ObjCover string `json:"objCover" bson:"objCover"` // 动态消息使用,eg:被转发、点赞、评论帖子封面图 + ObjType constant.NewsType `json:"objType" bson:"objType"` // 动态消息使用,eg:被转发、点赞、评论帖子类型 + UserGroup []UserInfo `json:"userGroup" bson:"userGroup"` // 相同动态消息的用户集合,保留前十 + UserCount int `json:"userCount" bson:"userCount"` // 相同动态消息的用户数 +} diff --git a/models/v/messagemod/web.go b/models/v/messagemod/web.go new file mode 100644 index 0000000..833f496 --- /dev/null +++ b/models/v/messagemod/web.go @@ -0,0 +1,63 @@ +package messagemod + +import ( + "91porn-server/models/commod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type WebListRequest struct { + SendUid *uint64 `json:"sendUid" bson:"sendUid"` // 发送者uid + TakeUid *uint64 `json:"takeUid" bson:"takeUid"` // 接收者uid + IsRead *bool `json:"isRead" bson:"isRead"` // 消息已读or未读 + commod.Page +} + +func (receiver *WebListRequest) Filter() primitive.M { + filter := bson.M{} + if receiver.SendUid != nil { + filter["sendUid"] = receiver.SendUid + } + if receiver.TakeUid != nil { + filter["takeUid"] = receiver.TakeUid + } + if receiver.IsRead != nil { + filter["isRead"] = receiver.IsRead + } + return filter +} + +func (receiver *WebListRequest) Options() *options.FindOptions { + return options.Find().SetSkip(int64(receiver.Skip())).SetLimit(int64(receiver.Limit() + 1)).SetSort(bson.M{"updatedAt": -1}) +} + +// WebDelReq 用户私聊删除 +type WebDelReq struct { + ID *string `form:"id" json:"id"` +} + +func (receiver *WebDelReq) Filter() primitive.M { + filter := bson.M{} + if receiver.ID != nil { + id, _ := primitive.ObjectIDFromHex(*receiver.ID) + filter["_id"] = id + } + return filter +} + +// WebSendReq 发送私信 +type WebSendReq struct { + Sender uint64 `form:"sender" json:"sender" binding:"required"` + Receiver uint64 `form:"receiver" json:"receiver" binding:"required"` + Content string `form:"content" json:"content" binding:"required"` +} + +type SendMsg struct { + Sender uint64 `form:"sender" json:"sender" binding:"required"` + Receiver uint64 `form:"receiver" json:"receiver" binding:"required"` + Content string `form:"content" json:"content" binding:"required"` + MsgType MsgType + ObjId primitive.ObjectID `json:"objId" bson:"objId"` // 动态消息使用,eg:被转发、点赞、评论帖子id + ObjName string `json:"objName" bson:"objName"` // 动态消息使用,eg:被转发、点赞、评论帖子名称 +} diff --git a/models/v/moduleconfmod/app.go b/models/v/moduleconfmod/app.go new file mode 100644 index 0000000..f27a53a --- /dev/null +++ b/models/v/moduleconfmod/app.go @@ -0,0 +1,39 @@ +package moduleconfmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AppModuleConf struct { + HomePage []APPModuleConf `json:"homePage" bson:"homePage"` // 首页 + Community []APPModuleConf `json:"community" bson:"community"` // 社区 + //PrivateCircle []APPModuleConf `json:"privateCircle" bson:"privateCircle"` // 私密圈 + DeepWeb []APPModuleConf `json:"deepWeb" bson:"deepWeb"` // 暗网 + Novel []APPModuleConf `json:"novel" bson:"novel"` // 小说 + Pics []APPModuleConf `json:"pics" bson:"pics"` // 图集 + NakedChat []APPModuleConf `json:"nakedChat" bson:"nakedChat"` // 裸聊模块 + ShortPage []APPModuleConf `json:"shortPage" bson:"shortPage"` // 短视频模块 + AiPlaza []APPModuleConf `json:"aiPlaza" bson:"aiPlaza"` // AI广场(弃用2026-1) + DramaPage []APPModuleConf `json:"dramaPage" bson:"dramaPage"` // 短剧频道 +} + +type APPModuleConf struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 模块ID + ModuleName string `json:"moduleName" bson:"moduleName" binding:"required"` // 模块名称 + Cover string `json:"cover" bson:"cover"` // 封面 + Type int `json:"type" bson:"type"` // 模块类型 + ShowType int `json:"showType" bson:"showType"` // 模块展示类型 + ShowJG bool `json:"showJG" bson:"showJG"` // 是否展示金刚区 + HaiJiaoStyle HaiJiaoStyle `json:"haiJiaoStyle"` // 跟海角样式关联的展示样式 + AiPlazaStyle AiPlazaStyle `json:"aiPlazaStyle" bson:"aiPlazaStyle"` // ai广场样式(弃用2026-1) + DefaultTagId string `json:"defaultTagId" bson:"defaultTagId"` // 社区默认展示标签 + PureVersion bool `json:"pureVersion" bson:"pureVersion"` // 纯净模式无广告 + OnlineAt *time.Time `json:"onlineAt,omitempty" bson:"onlineAt,omitempty"` + OfflineAt *time.Time `json:"offlineAt,omitempty" bson:"offlineAt,omitempty"` + ExcludeLatest bool `json:"excludeLatest" bson:"excludeLatest"` + ExcludeRecommend bool `json:"excludeRecommend" bson:"excludeRecommend"` + ExcludeSearch bool `json:"excludeSearch" bson:"excludeSearch"` + SearchOnlyWhenInactive bool `json:"searchOnlyWhenInactive" bson:"searchOnlyWhenInactive"` +} diff --git a/models/v/moduleconfmod/moduleconf.go b/models/v/moduleconfmod/moduleconf.go new file mode 100644 index 0000000..05b46c4 --- /dev/null +++ b/models/v/moduleconfmod/moduleconf.go @@ -0,0 +1,325 @@ +package moduleconfmod + +import ( + "91porn-server/common/log" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.ModuleConf + +var moduleMetadataCache = newModuleSnapshotCache(loadModuleMetadata, time.Now, moduleSnapshotTTL, moduleSnapshotRetryDelay) + +func loadModuleMetadata() (modules []ModuleConf, err error) { + err = coll(nil).Find(&modules, bson.M{}) + return +} + +// InitIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "moduleName", Value: 1}, {Key: "subModuleName", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InsertOne 插入一条数据 +func InsertOne(p *ModuleConf) error { + p.HaiJiaoStyle.NormalizeRefreshConfig() + if err := p.HaiJiaoStyle.ValidateRefreshConfig(); err != nil { + return err + } + if err := p.ValidateSchedule(); err != nil { + return err + } + p.CreatedAt = time.Now() + _, err := coll(nil).InsertOne(p) + if err == nil { + moduleMetadataCache.invalidate() + } + return err +} + +// UpdateOne 更新一条数据 +func UpdateOne(set EditSelector) error { + current, err := getByIDFromDB(set.ID) + if err != nil { + return err + } + if set.HaiJiaoStyle != nil { + set.HaiJiaoStyle.NormalizeRefreshConfig() + if err = set.HaiJiaoStyle.ValidateRefreshConfig(); err != nil { + return err + } + } + if set.ClearOnlineAt && set.OnlineAt != nil { + return fmt.Errorf("onlineAt and clearOnlineAt cannot be set together") + } + if set.ClearOfflineAt && set.OfflineAt != nil { + return fmt.Errorf("offlineAt and clearOfflineAt cannot be set together") + } + if set.OnlineAt != nil { + current.OnlineAt = set.OnlineAt + } + if set.OfflineAt != nil { + current.OfflineAt = set.OfflineAt + } + if set.ClearOnlineAt { + current.OnlineAt = nil + } + if set.ClearOfflineAt { + current.OfflineAt = nil + } + if err = current.ValidateSchedule(); err != nil { + return err + } + set.UpdatedAt = time.Now() + update := bson.M{"$set": set} + unset := bson.M{} + if set.ClearOnlineAt { + unset["onlineAt"] = "" + } + if set.ClearOfflineAt { + unset["offlineAt"] = "" + } + if len(unset) > 0 { + update["$unset"] = unset + } + _, err = coll(nil).UpdateOne(bson.M{"_id": set.ID}, update) + if err == nil { + moduleMetadataCache.invalidate() + } + return err +} + +func (p ModuleConf) ValidateSchedule() error { + if p.OnlineAt != nil && p.OfflineAt != nil && !p.OfflineAt.After(*p.OnlineAt) { + return fmt.Errorf("offlineAt must be later than onlineAt") + } + return nil +} + +func (p ModuleConf) IsActiveAt(now time.Time) bool { + if p.Status != 1 || p.DeletedAt != nil { + return false + } + if p.OnlineAt != nil && p.OnlineAt.After(now) { + return false + } + return p.OfflineAt == nil || p.OfflineAt.After(now) +} + +// DeleteOne 删除一条数据 +func DeleteOne(id primitive.ObjectID) (err error) { + now := time.Now() + status := uint8(0) + set := EditSelector{ID: id, DeletedAt: &now, Status: &status} + return UpdateOne(set) +} + +// GetByID 根据id获取一条记录 +func GetByID(id primitive.ObjectID) (conf ModuleConf, err error) { + snapshot, err := moduleMetadataCache.get() + if err != nil { + return ModuleConf{}, err + } + conf, ok := snapshot.findByID(id) + if !ok { + return ModuleConf{}, mongo.ErrNoDocuments + } + return conf, nil +} + +func getByIDFromDB(id primitive.ObjectID) (conf ModuleConf, err error) { + err = coll(nil).FindOne(&conf, bson.M{"_id": id}) + return +} + +// GetModuleConf 获取一个模块下的所有配置 +func GetModuleConf(moduleName string) (ret []ModuleConf, err error) { + snapshot, err := moduleMetadataCache.get() + if err != nil { + return nil, err + } + return snapshot.findByModuleName(moduleName), nil +} + +// GetModuleConfByType 根据类型获取配置 +func GetModuleConfByType(moduleType int) (ret []ModuleConf, err error) { + snapshot, err := moduleMetadataCache.get() + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetModuleConfByType", table, "snapshot", err)) + return nil, err + } + return snapshot.findByType(moduleType), nil +} + +// GetAllModule 获取所有开启(status=1)的模块配置 +func GetAllModule() (ret []ModuleConf, err error) { + snapshot, err := moduleMetadataCache.get() + if err != nil { + return nil, err + } + return snapshot.enabled(), nil +} + +// GetAllActiveModule 获取当前处于有效展示时间内的亚模块。 +func GetAllActiveModule(now time.Time) (ret []ModuleConf, err error) { + snapshot, err := moduleMetadataCache.get() + if err != nil { + return nil, err + } + return snapshot.activeAt(now), nil +} + +// ExcludedVideoModuleIDs 返回指定场景需要从聚合列表排除的亚模块 ID。 +func ExcludedVideoModuleIDs(now time.Time, recommend bool) ([]string, error) { + snapshot, err := moduleMetadataCache.get() + if err != nil { + return nil, err + } + return snapshot.excludedVideoModuleIDs(now, recommend), nil +} + +// ExcludedSearchModuleIDs 返回配置为不进入搜索列表的亚模块 ID。 +func ExcludedSearchModuleIDs() ([]string, error) { + snapshot, err := moduleMetadataCache.get() + if err != nil { + return nil, err + } + return snapshot.excludedSearchModuleIDs(), nil +} + +// BlockedOutsideSearchModuleIDs 返回当前仅允许通过搜索入口访问的亚模块 ID 集合。 +func BlockedOutsideSearchModuleIDs(moduleIDs []string, now time.Time) (map[string]struct{}, error) { + objectIDs := make([]primitive.ObjectID, 0, len(moduleIDs)) + seen := make(map[primitive.ObjectID]struct{}, len(moduleIDs)) + for _, moduleID := range moduleIDs { + id, parseErr := primitive.ObjectIDFromHex(moduleID) + if parseErr != nil || id.IsZero() { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + objectIDs = append(objectIDs, id) + } + if len(objectIDs) == 0 { + return map[string]struct{}{}, nil + } + snapshot, err := moduleMetadataCache.get() + if err != nil { + return nil, err + } + return snapshot.blockedOutsideSearchModuleIDs(objectIDs, now), nil +} + +// CanBrowseModule 判断亚模块是否能从搜索以外的普通入口访问。 +func CanBrowseModule(moduleID primitive.ObjectID, now time.Time) (bool, error) { + module, err := GetByID(moduleID) + if err != nil { + return false, err + } + return module.IsActiveAt(now), nil +} + +// Search 根据搜索条件检索 +func Search(q QuerySelector, page commod.Page) (resp ListResp, err error) { + skip := int64(page.Skip()) + limit := int64(page.Limit() + 1) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "sortNum", Value: 1}}, + } + filter := bson.M{} + if err != nil { + return + } + if q.ModuleName != nil { + filter["moduleName"] = primitive.Regex{ + Pattern: *q.ModuleName, + } + } + if q.SubModuleName != nil { + filter["subModuleName"] = primitive.Regex{ + Pattern: *q.SubModuleName, + } + } + if q.Type != nil { + filter["type"] = *q.Type + } + if q.ShowType != nil { + filter["showType"] = q.ShowType + } + data := []ModuleConf{} + if err = coll(nil).Find(&data, filter, &opts); err != nil { + return + } + total := int64(0) + if total, err = coll(nil).Count(filter); err != nil { + return + } + hasNext := false + if uint64(len(data)) > page.Limit() { + hasNext = true + data = data[:page.Limit()] + } + resp.List = data + resp.Total = total + resp.HasNext = hasNext + return +} + +// FindByIDs 根据section id批量获取section详情 +func FindByIDs(ids []primitive.ObjectID) (list []ModuleConf, err error) { + if len(ids) == 0 { + return nil, nil + } + snapshot, err := moduleMetadataCache.get() + if err != nil { + return nil, err + } + return snapshot.findByIDs(ids), nil +} + +func FindOneById(id primitive.ObjectID) (ModuleConf, error) { + return GetByID(id) +} + +// QueryAllList 分页查询文档 +func QueryAllList(filter primitive.M, opts ...*options.FindOptions) (out []*ModuleConf, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} diff --git a/models/v/moduleconfmod/moduleconf_test.go b/models/v/moduleconfmod/moduleconf_test.go new file mode 100644 index 0000000..9551c2d --- /dev/null +++ b/models/v/moduleconfmod/moduleconf_test.go @@ -0,0 +1,101 @@ +package moduleconfmod + +import ( + "encoding/json" + "testing" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestModuleConfValidateSchedule(t *testing.T) { + start := time.Date(2026, 7, 27, 8, 0, 0, 0, time.UTC) + after := start.Add(time.Hour) + before := start.Add(-time.Second) + + tests := []struct { + name string + module ModuleConf + wantErr bool + }{ + {name: "no limits", module: ModuleConf{}}, + {name: "only online", module: ModuleConf{OnlineAt: &start}}, + {name: "only offline", module: ModuleConf{OfflineAt: &after}}, + {name: "valid window", module: ModuleConf{OnlineAt: &start, OfflineAt: &after}}, + {name: "equal boundary", module: ModuleConf{OnlineAt: &start, OfflineAt: &start}, wantErr: true}, + {name: "reversed window", module: ModuleConf{OnlineAt: &start, OfflineAt: &before}, wantErr: true}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.module.ValidateSchedule() + if (err != nil) != test.wantErr { + t.Fatalf("ValidateSchedule() error = %v, wantErr %v", err, test.wantErr) + } + }) + } +} + +func TestModuleConfIsActiveAt(t *testing.T) { + now := time.Date(2026, 7, 27, 8, 0, 0, 0, time.UTC) + past := now.Add(-time.Hour) + future := now.Add(time.Hour) + + tests := []struct { + name string + module ModuleConf + want bool + }{ + {name: "enabled without window", module: ModuleConf{Status: 1}, want: true}, + {name: "disabled", module: ModuleConf{Status: 0}, want: false}, + {name: "not online yet", module: ModuleConf{Status: 1, OnlineAt: &future}, want: false}, + {name: "online boundary is inclusive", module: ModuleConf{Status: 1, OnlineAt: &now}, want: true}, + {name: "inside window", module: ModuleConf{Status: 1, OnlineAt: &past, OfflineAt: &future}, want: true}, + {name: "offline boundary is exclusive", module: ModuleConf{Status: 1, OfflineAt: &now}, want: false}, + {name: "already offline", module: ModuleConf{Status: 1, OfflineAt: &past}, want: false}, + {name: "deleted", module: ModuleConf{Status: 1, DeletedAt: &past}, want: false}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := test.module.IsActiveAt(now); got != test.want { + t.Fatalf("IsActiveAt() = %v, want %v", got, test.want) + } + }) + } +} + +func TestModuleConfJSONIncludesExcludeSearch(t *testing.T) { + data, err := json.Marshal(ModuleConf{}) + if err != nil { + t.Fatal(err) + } + var decoded map[string]interface{} + if err = json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + value, exists := decoded["excludeSearch"] + if !exists || value != false { + t.Fatalf("excludeSearch = %#v, exists = %v; want false and present", value, exists) + } +} + +func TestEditSelectorBSONIncludesFalseExcludeSearch(t *testing.T) { + value := false + data, err := bson.Marshal(EditSelector{ + ID: primitive.NewObjectID(), + ExcludeSearch: &value, + }) + if err != nil { + t.Fatal(err) + } + var decoded bson.M + if err = bson.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + got, exists := decoded["excludeSearch"] + if !exists || got != false { + t.Fatalf("excludeSearch = %#v, exists = %v; want false and present", got, exists) + } +} diff --git a/models/v/moduleconfmod/refresh_test.go b/models/v/moduleconfmod/refresh_test.go new file mode 100644 index 0000000..ac6d415 --- /dev/null +++ b/models/v/moduleconfmod/refresh_test.go @@ -0,0 +1,37 @@ +package moduleconfmod + +import ( + "testing" + + "91porn-server/models/commod" +) + +func TestNormalizeLegacyHotRefreshConfig(t *testing.T) { + item := SortItem{Val: commod.MostHot} + item.NormalizeRefreshConfig() + if item.RefreshMode != RefreshModeRandomTopN || item.RandomCandidateN != DefaultRandomCandidateN { + t.Fatalf("unexpected normalized item: %+v", item) + } + if err := item.ValidateRefreshConfig(); err != nil { + t.Fatal(err) + } +} + +func TestNormalizeDefaultRefreshConfig(t *testing.T) { + item := SortItem{Val: commod.New} + item.NormalizeRefreshConfig() + if item.RefreshMode != RefreshModeDefault || item.RandomCandidateN != 0 { + t.Fatalf("unexpected normalized item: %+v", item) + } +} + +func TestEnsureSortRulesCreatesRefreshableLegacyDefaults(t *testing.T) { + style := HaiJiaoStyle{} + style.EnsureSortRules() + if len(style.SortRules) != 4 { + t.Fatalf("got %d default sort rules", len(style.SortRules)) + } + if style.SortRules[1].RefreshMode != RefreshModeRandomTopN { + t.Fatalf("hot default should support random refresh: %+v", style.SortRules[1]) + } +} diff --git a/models/v/moduleconfmod/snapshot_cache.go b/models/v/moduleconfmod/snapshot_cache.go new file mode 100644 index 0000000..e7c921e --- /dev/null +++ b/models/v/moduleconfmod/snapshot_cache.go @@ -0,0 +1,334 @@ +package moduleconfmod + +import ( + "errors" + "fmt" + "sort" + "sync" + "time" + + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson/primitive" + "golang.org/x/sync/singleflight" +) + +const ( + moduleSnapshotTTL = 15 * time.Second + moduleSnapshotRetryDelay = time.Second +) + +var errModuleSnapshotInvalidated = errors.New("module configuration snapshot invalidated while loading") + +type moduleSnapshotLoader func() ([]ModuleConf, error) + +// moduleSnapshot is immutable after construction. Every accessor returns +// copies so callers cannot mutate data shared by concurrent requests. +type moduleSnapshot struct { + modules []ModuleConf + byID map[primitive.ObjectID]int +} + +func newModuleSnapshot(modules []ModuleConf) *moduleSnapshot { + copied := cloneModuleConfs(modules) + byID := make(map[primitive.ObjectID]int, len(copied)) + for i := range copied { + byID[copied[i].ID] = i + } + return &moduleSnapshot{ + modules: copied, + byID: byID, + } +} + +func (s *moduleSnapshot) all() []ModuleConf { + if s == nil { + return nil + } + return cloneModuleConfs(s.modules) +} + +func (s *moduleSnapshot) findByID(id primitive.ObjectID) (ModuleConf, bool) { + if s == nil { + return ModuleConf{}, false + } + index, ok := s.byID[id] + if !ok { + return ModuleConf{}, false + } + return cloneModuleConf(s.modules[index]), true +} + +func (s *moduleSnapshot) findByIDs(ids []primitive.ObjectID) []ModuleConf { + if s == nil || len(ids) == 0 { + return nil + } + wanted := make(map[primitive.ObjectID]struct{}, len(ids)) + for _, id := range ids { + wanted[id] = struct{}{} + } + modules := make([]ModuleConf, 0, len(wanted)) + for i := range s.modules { + if _, ok := wanted[s.modules[i].ID]; ok { + modules = append(modules, cloneModuleConf(s.modules[i])) + } + } + return modules +} + +func (s *moduleSnapshot) findByModuleName(moduleName string) []ModuleConf { + modules := make([]ModuleConf, 0) + for i := range s.modules { + if s.modules[i].ModuleName == moduleName { + modules = append(modules, cloneModuleConf(s.modules[i])) + } + } + return modules +} + +func (s *moduleSnapshot) findByType(moduleType int) []ModuleConf { + modules := make([]ModuleConf, 0) + for i := range s.modules { + if s.modules[i].Type == moduleType { + modules = append(modules, cloneModuleConf(s.modules[i])) + } + } + return modules +} + +func (s *moduleSnapshot) enabled() []ModuleConf { + modules := make([]ModuleConf, 0) + for i := range s.modules { + if s.modules[i].Status == 1 && s.modules[i].DeletedAt == nil { + modules = append(modules, cloneModuleConf(s.modules[i])) + } + } + sortModuleConfs(modules) + return modules +} + +func (s *moduleSnapshot) activeAt(now time.Time) []ModuleConf { + modules := make([]ModuleConf, 0) + for i := range s.modules { + if s.modules[i].IsActiveAt(now) { + modules = append(modules, cloneModuleConf(s.modules[i])) + } + } + sortModuleConfs(modules) + return modules +} + +func (s *moduleSnapshot) excludedVideoModuleIDs(now time.Time, recommend bool) []string { + ids := make([]string, 0) + for i := range s.modules { + module := s.modules[i] + excludedByScene := module.ExcludeLatest + if recommend { + excludedByScene = module.ExcludeRecommend + } + if excludedByScene || (module.SearchOnlyWhenInactive && !module.IsActiveAt(now)) { + ids = append(ids, module.ID.Hex()) + } + } + return ids +} + +func (s *moduleSnapshot) excludedSearchModuleIDs() []string { + ids := make([]string, 0) + for i := range s.modules { + if s.modules[i].ExcludeSearch { + ids = append(ids, s.modules[i].ID.Hex()) + } + } + return ids +} + +func (s *moduleSnapshot) blockedOutsideSearchModuleIDs(moduleIDs []primitive.ObjectID, now time.Time) map[string]struct{} { + blocked := make(map[string]struct{}) + seen := make(map[primitive.ObjectID]struct{}, len(moduleIDs)) + for _, moduleID := range moduleIDs { + if moduleID.IsZero() { + continue + } + if _, ok := seen[moduleID]; ok { + continue + } + seen[moduleID] = struct{}{} + index, ok := s.byID[moduleID] + if !ok { + continue + } + module := s.modules[index] + if module.SearchOnlyWhenInactive && !module.IsActiveAt(now) { + blocked[module.ID.Hex()] = struct{}{} + } + } + return blocked +} + +type moduleSnapshotCache struct { + mu sync.RWMutex + loadGroup singleflight.Group + loader moduleSnapshotLoader + now func() time.Time + ttl time.Duration + retryDelay time.Duration + + snapshot *moduleSnapshot + refreshAt time.Time + generation uint64 +} + +func newModuleSnapshotCache( + loader moduleSnapshotLoader, + now func() time.Time, + ttl time.Duration, + retryDelay time.Duration, +) *moduleSnapshotCache { + if now == nil { + now = time.Now + } + if ttl <= 0 { + ttl = moduleSnapshotTTL + } + if retryDelay <= 0 { + retryDelay = moduleSnapshotRetryDelay + } + return &moduleSnapshotCache{ + loader: loader, + now: now, + ttl: ttl, + retryDelay: retryDelay, + } +} + +func (c *moduleSnapshotCache) get() (*moduleSnapshot, error) { + for { + now := c.now() + c.mu.RLock() + if c.snapshot != nil && now.Before(c.refreshAt) { + snapshot := c.snapshot + c.mu.RUnlock() + return snapshot, nil + } + generation := c.generation + c.mu.RUnlock() + + key := fmt.Sprintf("module-snapshot-%d", generation) + value, err, _ := c.loadGroup.Do(key, func() (interface{}, error) { + return c.load(generation) + }) + if errors.Is(err, errModuleSnapshotInvalidated) { + continue + } + if err != nil { + return nil, err + } + // A write may invalidate the cache after load() installs the snapshot + // but before singleflight returns it to this reader. + c.mu.RLock() + currentGeneration := c.generation + c.mu.RUnlock() + if currentGeneration != generation { + continue + } + return value.(*moduleSnapshot), nil + } +} + +func (c *moduleSnapshotCache) load(generation uint64) (*moduleSnapshot, error) { + now := c.now() + c.mu.RLock() + if generation != c.generation { + c.mu.RUnlock() + return nil, errModuleSnapshotInvalidated + } + if c.snapshot != nil && now.Before(c.refreshAt) { + snapshot := c.snapshot + c.mu.RUnlock() + return snapshot, nil + } + c.mu.RUnlock() + + modules, err := c.loader() + loadedAt := c.now() + + c.mu.Lock() + if generation != c.generation { + c.mu.Unlock() + return nil, errModuleSnapshotInvalidated + } + if err != nil { + if c.snapshot == nil { + c.mu.Unlock() + return nil, err + } + // Avoid retrying Mongo on every request while still keeping retries + // frequent enough for a transient outage to recover quickly. + c.refreshAt = loadedAt.Add(c.retryDelay) + snapshot := c.snapshot + c.mu.Unlock() + log.Warn("module configuration snapshot refresh failed; using last-known-good data", log.E(err)) + return snapshot, nil + } + + c.snapshot = newModuleSnapshot(modules) + c.refreshAt = loadedAt.Add(c.ttl) + snapshot := c.snapshot + c.mu.Unlock() + return snapshot, nil +} + +// invalidate forces the next reader to reload. The previous snapshot remains +// available as last-known-good if the refresh fails. +func (c *moduleSnapshotCache) invalidate() { + c.mu.Lock() + c.generation++ + c.refreshAt = time.Time{} + c.mu.Unlock() +} + +// reset is used when the package is rebound to a new Mongo client. +func (c *moduleSnapshotCache) reset() { + c.mu.Lock() + c.generation++ + c.snapshot = nil + c.refreshAt = time.Time{} + c.mu.Unlock() +} + +func cloneModuleConfs(modules []ModuleConf) []ModuleConf { + if modules == nil { + return nil + } + copied := make([]ModuleConf, len(modules)) + for i := range modules { + copied[i] = cloneModuleConf(modules[i]) + } + return copied +} + +func cloneModuleConf(module ModuleConf) ModuleConf { + copied := module + copied.OnlineAt = cloneTime(module.OnlineAt) + copied.OfflineAt = cloneTime(module.OfflineAt) + copied.DeletedAt = cloneTime(module.DeletedAt) + if module.HaiJiaoStyle.SortRules != nil { + copied.HaiJiaoStyle.SortRules = append([]SortItem(nil), module.HaiJiaoStyle.SortRules...) + } + return copied +} + +func cloneTime(value *time.Time) *time.Time { + if value == nil { + return nil + } + copied := *value + return &copied +} + +func sortModuleConfs(modules []ModuleConf) { + sort.SliceStable(modules, func(i, j int) bool { + return modules[i].SortNum < modules[j].SortNum + }) +} diff --git a/models/v/moduleconfmod/snapshot_cache_test.go b/models/v/moduleconfmod/snapshot_cache_test.go new file mode 100644 index 0000000..89ecec5 --- /dev/null +++ b/models/v/moduleconfmod/snapshot_cache_test.go @@ -0,0 +1,322 @@ +package moduleconfmod + +import ( + "errors" + "sync" + "sync/atomic" + "testing" + "time" + + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +type moduleCacheTestClock struct { + mu sync.Mutex + now time.Time +} + +func (c *moduleCacheTestClock) Now() time.Time { + c.mu.Lock() + defer c.mu.Unlock() + return c.now +} + +func (c *moduleCacheTestClock) Advance(duration time.Duration) { + c.mu.Lock() + c.now = c.now.Add(duration) + c.mu.Unlock() +} + +func TestModuleSnapshotCacheConcurrentReadersShareOneLoad(t *testing.T) { + clock := &moduleCacheTestClock{now: time.Date(2026, 7, 31, 8, 0, 0, 0, time.UTC)} + moduleID := primitive.NewObjectID() + var calls atomic.Int32 + var startedOnce sync.Once + started := make(chan struct{}) + release := make(chan struct{}) + cache := newModuleSnapshotCache(func() ([]ModuleConf, error) { + calls.Add(1) + startedOnce.Do(func() { close(started) }) + <-release + return []ModuleConf{{ID: moduleID, SubModuleName: "cached"}}, nil + }, clock.Now, time.Minute, time.Second) + + const readers = 32 + errs := make(chan error, readers) + var wg sync.WaitGroup + wg.Add(readers) + for i := 0; i < readers; i++ { + go func() { + defer wg.Done() + snapshot, err := cache.get() + if err != nil { + errs <- err + return + } + module, ok := snapshot.findByID(moduleID) + if !ok || module.SubModuleName != "cached" { + errs <- errors.New("reader received an unexpected snapshot") + } + }() + } + + <-started + close(release) + wg.Wait() + close(errs) + for err := range errs { + t.Error(err) + } + if got := calls.Load(); got != 1 { + t.Fatalf("loader calls = %d, want 1", got) + } +} + +func TestModuleSnapshotCacheInitialFailureReturnsError(t *testing.T) { + wantErr := errors.New("mongo unavailable") + cache := newModuleSnapshotCache(func() ([]ModuleConf, error) { + return nil, wantErr + }, time.Now, time.Minute, time.Second) + + if _, err := cache.get(); !errors.Is(err, wantErr) { + t.Fatalf("get() error = %v, want %v", err, wantErr) + } +} + +func TestModuleSnapshotCacheUsesLastKnownGoodAndRetries(t *testing.T) { + clock := &moduleCacheTestClock{now: time.Date(2026, 7, 31, 8, 0, 0, 0, time.UTC)} + moduleID := primitive.NewObjectID() + var calls atomic.Int32 + cache := newModuleSnapshotCache(func() ([]ModuleConf, error) { + switch calls.Add(1) { + case 1: + return []ModuleConf{{ID: moduleID, SubModuleName: "v1"}}, nil + case 2: + return nil, errors.New("temporary mongo failure") + default: + return []ModuleConf{{ID: moduleID, SubModuleName: "v2"}}, nil + } + }, clock.Now, 10*time.Second, time.Second) + + assertCachedModuleName(t, cache, moduleID, "v1") + clock.Advance(11 * time.Second) + assertCachedModuleName(t, cache, moduleID, "v1") + assertCachedModuleName(t, cache, moduleID, "v1") + if got := calls.Load(); got != 2 { + t.Fatalf("loader calls during retry delay = %d, want 2", got) + } + + clock.Advance(2 * time.Second) + assertCachedModuleName(t, cache, moduleID, "v2") + if got := calls.Load(); got != 3 { + t.Fatalf("loader calls after retry delay = %d, want 3", got) + } +} + +func TestModuleSnapshotCacheInvalidateForcesReload(t *testing.T) { + moduleID := primitive.NewObjectID() + var calls atomic.Int32 + cache := newModuleSnapshotCache(func() ([]ModuleConf, error) { + version := calls.Add(1) + return []ModuleConf{{ + ID: moduleID, + SubModuleName: "v" + string(rune('0'+version)), + }}, nil + }, time.Now, time.Hour, time.Second) + + assertCachedModuleName(t, cache, moduleID, "v1") + cache.invalidate() + assertCachedModuleName(t, cache, moduleID, "v2") + if got := calls.Load(); got != 2 { + t.Fatalf("loader calls = %d, want 2", got) + } +} + +func TestModuleSnapshotCacheInvalidateDuringLoadDiscardsStaleResult(t *testing.T) { + moduleID := primitive.NewObjectID() + var calls atomic.Int32 + firstStarted := make(chan struct{}) + releaseFirst := make(chan struct{}) + cache := newModuleSnapshotCache(func() ([]ModuleConf, error) { + call := calls.Add(1) + if call == 1 { + close(firstStarted) + <-releaseFirst + return []ModuleConf{{ID: moduleID, SubModuleName: "stale"}}, nil + } + return []ModuleConf{{ID: moduleID, SubModuleName: "fresh"}}, nil + }, time.Now, time.Hour, time.Second) + + result := make(chan string, 1) + errs := make(chan error, 1) + go func() { + snapshot, err := cache.get() + if err != nil { + errs <- err + return + } + module, _ := snapshot.findByID(moduleID) + result <- module.SubModuleName + }() + + <-firstStarted + cache.invalidate() + close(releaseFirst) + + select { + case err := <-errs: + t.Fatal(err) + case got := <-result: + if got != "fresh" { + t.Fatalf("module name = %q, want fresh", got) + } + case <-time.After(3 * time.Second): + t.Fatal("cache reload timed out") + } + if got := calls.Load(); got != 2 { + t.Fatalf("loader calls = %d, want 2", got) + } +} + +func TestModuleSnapshotScheduleBoundariesDoNotRequireReload(t *testing.T) { + base := time.Date(2026, 7, 31, 8, 0, 0, 0, time.UTC) + onlineAt := base.Add(time.Hour) + offlineAt := base.Add(2 * time.Hour) + moduleID := primitive.NewObjectID() + var calls atomic.Int32 + cache := newModuleSnapshotCache(func() ([]ModuleConf, error) { + calls.Add(1) + return []ModuleConf{{ + ID: moduleID, + Status: 1, + OnlineAt: &onlineAt, + OfflineAt: &offlineAt, + SearchOnlyWhenInactive: true, + }}, nil + }, func() time.Time { return base }, time.Hour, time.Second) + + snapshot, err := cache.get() + if err != nil { + t.Fatal(err) + } + assertStringSetContains(t, snapshot.excludedVideoModuleIDs(onlineAt.Add(-time.Nanosecond), false), moduleID.Hex(), true) + assertStringSetContains(t, snapshot.excludedVideoModuleIDs(onlineAt, false), moduleID.Hex(), false) + assertStringSetContains(t, snapshot.excludedVideoModuleIDs(offlineAt, false), moduleID.Hex(), true) + if _, ok := snapshot.blockedOutsideSearchModuleIDs([]primitive.ObjectID{moduleID}, onlineAt.Add(-time.Nanosecond))[moduleID.Hex()]; !ok { + t.Fatal("module should be blocked before onlineAt") + } + if _, ok := snapshot.blockedOutsideSearchModuleIDs([]primitive.ObjectID{moduleID}, onlineAt)[moduleID.Hex()]; ok { + t.Fatal("module should be browsable at onlineAt") + } + if _, ok := snapshot.blockedOutsideSearchModuleIDs([]primitive.ObjectID{moduleID}, offlineAt)[moduleID.Hex()]; !ok { + t.Fatal("module should be blocked at offlineAt") + } + + if got := len(snapshot.activeAt(onlineAt.Add(-time.Nanosecond))); got != 0 { + t.Fatalf("active before onlineAt = %d, want 0", got) + } + if got := len(snapshot.activeAt(onlineAt)); got != 1 { + t.Fatalf("active at onlineAt = %d, want 1", got) + } + if got := len(snapshot.activeAt(offlineAt)); got != 0 { + t.Fatalf("active at offlineAt = %d, want 0", got) + } + if got := calls.Load(); got != 1 { + t.Fatalf("loader calls = %d, want 1", got) + } +} + +func TestModuleSnapshotExcludedSearchModuleIDs(t *testing.T) { + excludedID := primitive.NewObjectID() + includedID := primitive.NewObjectID() + snapshot := newModuleSnapshot([]ModuleConf{ + {ID: excludedID, ExcludeSearch: true}, + {ID: includedID, ExcludeSearch: false}, + }) + + ids := snapshot.excludedSearchModuleIDs() + assertStringSetContains(t, ids, excludedID.Hex(), true) + assertStringSetContains(t, ids, includedID.Hex(), false) +} + +func TestModuleSnapshotAccessorsReturnDeepCopies(t *testing.T) { + moduleID := primitive.NewObjectID() + onlineAt := time.Date(2026, 7, 31, 8, 0, 0, 0, time.UTC) + snapshot := newModuleSnapshot([]ModuleConf{{ + ID: moduleID, + SubModuleName: "original", + OnlineAt: &onlineAt, + HaiJiaoStyle: HaiJiaoStyle{ + SortRules: []SortItem{{Val: commod.MostHot, Name: "original-rule"}}, + }, + }}) + + first := snapshot.all() + first[0].SubModuleName = "changed" + *first[0].OnlineAt = first[0].OnlineAt.Add(time.Hour) + first[0].HaiJiaoStyle.SortRules[0].Name = "changed-rule" + + second := snapshot.all() + if second[0].SubModuleName != "original" { + t.Fatalf("shared scalar was mutated: %+v", second[0]) + } + if !second[0].OnlineAt.Equal(onlineAt) { + t.Fatalf("shared schedule was mutated: %v", second[0].OnlineAt) + } + if second[0].HaiJiaoStyle.SortRules[0].Name != "original-rule" { + t.Fatalf("shared sort rules were mutated: %+v", second[0].HaiJiaoStyle.SortRules) + } +} + +func TestGetByIDPreservesNoDocumentsContract(t *testing.T) { + original := moduleMetadataCache + moduleMetadataCache = newModuleSnapshotCache( + func() ([]ModuleConf, error) { return []ModuleConf{}, nil }, + time.Now, + time.Hour, + time.Second, + ) + t.Cleanup(func() { moduleMetadataCache = original }) + + if _, err := GetByID(primitive.NewObjectID()); !errors.Is(err, mongo.ErrNoDocuments) { + t.Fatalf("GetByID() error = %v, want mongo.ErrNoDocuments", err) + } +} + +func assertCachedModuleName( + t *testing.T, + cache *moduleSnapshotCache, + moduleID primitive.ObjectID, + want string, +) { + t.Helper() + snapshot, err := cache.get() + if err != nil { + t.Fatal(err) + } + module, ok := snapshot.findByID(moduleID) + if !ok { + t.Fatalf("module %s not found", moduleID.Hex()) + } + if module.SubModuleName != want { + t.Fatalf("module name = %q, want %q", module.SubModuleName, want) + } +} + +func assertStringSetContains(t *testing.T, values []string, target string, want bool) { + t.Helper() + for _, value := range values { + if value == target { + if !want { + t.Fatalf("%q unexpectedly found in %v", target, values) + } + return + } + } + if want { + t.Fatalf("%q not found in %v", target, values) + } +} diff --git a/models/v/moduleconfmod/type.go b/models/v/moduleconfmod/type.go new file mode 100644 index 0000000..70150ec --- /dev/null +++ b/models/v/moduleconfmod/type.go @@ -0,0 +1,186 @@ +package moduleconfmod + +import ( + "fmt" + "strings" + "time" + + "91porn-server/common/db" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ShowType 取值 +const ( + HJShowType = 1 // 模块普通海角系样式 + AllSectionShowType = 2 // 全专题组合样式 + HengSlideShowType = 3 // (17岁)单排专题横滑动展示 + ActressShowType = 4 // 女优网黄展示样式 +) + +// ModuleConf 模块配置 +type ModuleConf struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + ModuleName string `json:"moduleName" bson:"moduleName" binding:"required"` // 版块名称,如“撩吧” + SubModuleName string `json:"subModuleName" bson:"subModuleName" binding:"required"` // 版块下亚模块名称,如“撩吧”下的“原创” + Status uint8 `json:"status" bson:"status"` // 状态,0-关闭,1-展示 + SectionLimit int `json:"sectionLimit" bson:"sectionLimit" binding:"required"` // 专题数量限制,0-不限制 + ShowType int `json:"showType" bson:"showType"` // 模块排版类型 + ShowJG bool `json:"showJG" bson:"showJG"` // 是否展示金刚区 + Cover string `json:"cover" bson:"cover"` // 封面 + AiPlazaStyle AiPlazaStyle `json:"aiPlazaStyle" bson:"aiPlazaStyle"` // ai广场样式 + HaiJiaoStyle HaiJiaoStyle `json:"haiJiaoStyle" bson:"haiJiaoStyle"` // 海角样式详情 + DefaultTagId primitive.ObjectID `json:"defaultTagId" bson:"defaultTagId"` // 社区默认展示标签 + SortNum int `json:"sortNum" bson:"sortNum"` // 模块排序 + Type int `json:"type" bson:"type"` // 模块类型 + PureVersion bool `json:"pureVersion" bson:"pureVersion"` // 是否纯净版无广告 + OnlineAt *time.Time `json:"onlineAt" bson:"onlineAt,omitempty"` // 定时上架时间,为空表示不限制 + OfflineAt *time.Time `json:"offlineAt" bson:"offlineAt,omitempty"` // 定时下架时间,为空表示不限制 + ExcludeLatest bool `json:"excludeLatest" bson:"excludeLatest"` // 亚模块内容不进入最新列表 + ExcludeRecommend bool `json:"excludeRecommend" bson:"excludeRecommend"` // 亚模块内容不进入推荐列表 + ExcludeSearch bool `json:"excludeSearch" bson:"excludeSearch"` // 亚模块内容不进入搜索列表 + SearchOnlyWhenInactive bool `json:"searchOnlyWhenInactive" bson:"searchOnlyWhenInactive"` // 失效后仅允许搜索入口访问 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` // 刷新时间 + DeletedAt *time.Time `json:"deletedAt" bson:"deletedAt,omitempty"` // 删除时间,不能去除omitempty +} + +type AiPlazaStyle struct { + Type int `json:"type" bson:"type"` // 1-ai图片换脸 2-ai视频换脸 3-ai脱衣 4-ai图生视频 5-ai绘画 + Select bool `json:"select" bson:"select"` // 默认选中 +} + +type HaiJiaoStyle struct { + SectionStyle int `json:"sectionStyle" bson:"sectionStyle"` // 专题展示样式 0-不展示 1-十六岁专题 2-女优专题 3-网黄专题 4-普通专题 + SortStyle int `json:"sortShow" bson:"sectionShow"` // 排序规则展示样式 展示样式 0-不展示 1-展示 + DefaultShow int `json:"defaultShow" bson:"defaultShow"` // 0-一排两个 1-一排一个 + ShowChosenVideo int `json:"showChosenVideo" bson:"showChosenVideo"` // 0-不展示精选视频 1-展示精选视频 + SortRules []SortItem `json:"sortRules" bson:"sortRules"` // 排序规则 +} + +type SortItem struct { + Val commod.SortType `json:"val" bson:"val"` // 排序规则 // 1、最新上架,2、热门推荐,3、最多观看 7-最多收藏 9、最新热评 + Top bool `json:"top" bson:"top"` // 是否置顶排序 在原有的排序规则上增加一个置顶排序,置顶条件优先 + Name string `json:"name" bson:"name"` + RefreshMode string `json:"refreshMode" bson:"refreshMode"` // 刷新方式 DEFAULT / RANDOM_TOP_N + RandomCandidateN int `json:"randomCandidateN" bson:"randomCandidateN"` // 随机候选池大小,最大30 +} + +const ( + RefreshModeDefault = "DEFAULT" + RefreshModeRandomTopN = "RANDOM_TOP_N" + DefaultRandomCandidateN = 30 + MaxRandomCandidateN = 30 +) + +// NormalizeRefreshConfig 补齐历史排序配置的刷新语义。 +// 历史数据没有 refreshMode 时,仅按稳定排序值 MostHot 做一次兼容, +// App 后续只消费 refreshMode,不依赖后台可编辑标题。 +func (s *SortItem) NormalizeRefreshConfig() { + s.RefreshMode = strings.ToUpper(strings.TrimSpace(s.RefreshMode)) + if s.RefreshMode == "" { + s.RefreshMode = RefreshModeDefault + if s.Val == commod.MostHot { + s.RefreshMode = RefreshModeRandomTopN + } + } + if s.RefreshMode == RefreshModeRandomTopN { + if s.RandomCandidateN <= 0 || s.RandomCandidateN > MaxRandomCandidateN { + s.RandomCandidateN = DefaultRandomCandidateN + } + return + } + s.RandomCandidateN = 0 +} + +func (s SortItem) ValidateRefreshConfig() error { + switch s.RefreshMode { + case RefreshModeDefault: + if s.RandomCandidateN != 0 { + return fmt.Errorf("randomCandidateN must be 0 when refreshMode is DEFAULT") + } + case RefreshModeRandomTopN: + if s.RandomCandidateN < 1 || s.RandomCandidateN > MaxRandomCandidateN { + return fmt.Errorf("randomCandidateN must be between 1 and %d", MaxRandomCandidateN) + } + default: + return fmt.Errorf("unsupported refreshMode: %s", s.RefreshMode) + } + return nil +} + +func (h *HaiJiaoStyle) NormalizeRefreshConfig() { + for i := range h.SortRules { + h.SortRules[i].NormalizeRefreshConfig() + } +} + +// EnsureSortRules 保持与历史 App 接口一致:后台未配置排序项时返回默认排序项。 +func (h *HaiJiaoStyle) EnsureSortRules() { + if len(h.SortRules) == 0 { + h.SortRules = []SortItem{ + {Val: commod.New}, + {Val: commod.MostHot}, + {Val: commod.MostWatch}, + {Val: commod.MostCollect}, + } + } + h.NormalizeRefreshConfig() +} + +func (h HaiJiaoStyle) ValidateRefreshConfig() error { + for _, rule := range h.SortRules { + if err := rule.ValidateRefreshConfig(); err != nil { + return err + } + } + return nil +} + +func Init() { + mdb = db.Init(table) + moduleMetadataCache.reset() + initIndex() +} + +type EditSelector struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty" binding:"required"` + ModuleName *string `json:"moduleName" bson:"moduleName,omitempty"` + SubModuleName *string `json:"subModuleName" bson:"subModuleName,omitempty"` + Status *uint8 `json:"status" bson:"status,omitempty"` + SectionLimit *int `json:"sectionLimit" bson:"sectionLimit,omitempty"` + SortNum *int `json:"sortNum" bson:"sortNum,omitempty"` + Type *int `json:"type" bson:"type"` // 模块类型 + AiPlazaStyle *AiPlazaStyle `json:"aiPlazaStyle" bson:"aiPlazaStyle"` // ai广场样式 + HaiJiaoStyle *HaiJiaoStyle `json:"haiJiaoStyle" bson:"haiJiaoStyle"` // 海角样式详情 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` + DeletedAt *time.Time `json:"deletedAt" bson:"deletedAt,omitempty"` + ShowType *int `json:"showType" bson:"showType,omitempty"` // 模块排版类型 + DefaultTagId *primitive.ObjectID `json:"defaultTagId" bson:"defaultTagId"` // 社区默认展示标签 + PureVersion *bool `json:"pureVersion" bson:"pureVersion,omitempty"` // 是否纯净版无广告 + OnlineAt *time.Time `json:"onlineAt" bson:"onlineAt,omitempty"` + OfflineAt *time.Time `json:"offlineAt" bson:"offlineAt,omitempty"` + ClearOnlineAt bool `json:"clearOnlineAt" bson:"-"` + ClearOfflineAt bool `json:"clearOfflineAt" bson:"-"` + ExcludeLatest *bool `json:"excludeLatest" bson:"excludeLatest,omitempty"` + ExcludeRecommend *bool `json:"excludeRecommend" bson:"excludeRecommend,omitempty"` + ExcludeSearch *bool `json:"excludeSearch" bson:"excludeSearch,omitempty"` + SearchOnlyWhenInactive *bool `json:"searchOnlyWhenInactive" bson:"searchOnlyWhenInactive,omitempty"` +} + +type QuerySelector struct { + ID *primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + ModuleName *string `json:"moduleName,omitempty" bson:"moduleName,omitempty"` + SubModuleName *string `json:"subModuleName,omitempty" bson:"subModuleName,omitempty"` + Status *uint8 `json:"status,omitempty" bson:"status,omitempty"` + Type *int `json:"type" bson:"type"` // 模块类型 + ShowType *int `json:"showType" bson:"showType"` // 模块排版类型 + +} + +type ListResp struct { + Total int64 `json:"total"` // 总数 + HasNext bool `json:"hasNext"` // 是否还有下一页 + List []ModuleConf `json:"list"` // 列表 +} diff --git a/models/v/moduleconfmod/web.go b/models/v/moduleconfmod/web.go new file mode 100644 index 0000000..dc824c3 --- /dev/null +++ b/models/v/moduleconfmod/web.go @@ -0,0 +1,18 @@ +package moduleconfmod + +type ModuleType int + +const ( + HomePage = iota + 1 // 1、首页-视频 + Community // 2、社区 + DeepWeb // 3、暗网 + Cartoon // 4、动画 + Comics // 5、漫画 + Novel // 6、小说 + Game // 7、黄游(弃用) + Pics // 8、图集 + ShortPage // 9、短视频 + PrivateCircle // 10、私密圈(弃用) + AiPlaza // 11、AI广场 + Drama // 12、短剧 +) diff --git a/models/v/modulesectionmod/section.go b/models/v/modulesectionmod/section.go new file mode 100644 index 0000000..6b40267 --- /dev/null +++ b/models/v/modulesectionmod/section.go @@ -0,0 +1,309 @@ +package modulesectionmod + +import ( + "91porn-server/common/log" + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/stderr" + "91porn-server/models" + "91porn-server/models/commod" + "91porn-server/models/v/moduleconfmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.Section + +var mdb *db.MongoDB + +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "sectionName", Value: 1}, {Key: "subModuleID", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{ + {Key: "subModuleID", Value: 1}, {Key: "status", Value: 1}, {Key: "sort", Value: -1}, {Key: "createdAt", Value: -1}, + }, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func InsertOne(section Section) error { + section.CreatedAt = time.Now() + section.UpdatedAt = section.CreatedAt + ok, err := checkLimit(section.SubModuleID) + if err != nil { + return err + } + if !ok { + return stderr.ModuleConfLimitExceed + } + _, err = coll(nil).InsertOne(section) + return err +} + +func UpdateOne(set EditSelector) error { + subModule, err := moduleconfmod.GetByID(*set.SubModuleID) + if err != nil { + return err + } + total, err := CountBySubModuleID(*set.SubModuleID) + if err != nil { + return err + } + if (set.Status != nil && *set.Status == 1) && total >= int64(subModule.SectionLimit) { + return stderr.ModuleConfLimitExceed + } + cond := bson.M{"updatedAt": time.Now()} + if set.Status != nil { + cond["status"] = set.Status + } + if set.Tags != nil && len(*set.Tags) > 0 { + cond["tags"] = set.Tags + } else { + cond["tags"] = []string{} + } + if set.TagIds != nil && len(set.TagIds) > 0 { + cond["tagIds"] = set.TagIds + } else { + cond["tagIds"] = []primitive.ObjectID{} + } + if set.SectionName != nil { + cond["sectionName"] = set.SectionName + } + if set.SubModuleID != nil { + cond["subModuleID"] = set.SubModuleID + } + if set.ShowType != nil { + cond["showType"] = set.ShowType + } + if set.Sort != nil { + cond["sort"] = set.Sort + } + if set.SectionCover != nil { + cond["sectionCover"] = set.SectionCover + } + if set.Hot != nil { + cond["hot"] = set.Hot + } + _, err = coll(nil).UpdateOne(bson.M{"_id": set.ID}, bson.M{"$set": cond}) + return err +} + +// DeleteOne 删除一条数据,仅作删除标记-deletedAt +func DeleteOne(id primitive.ObjectID) error { + now := time.Now() + status := uint8(0) + set := EditSelector{ID: id, DeletedAt: &now, Status: &status} + return UpdateOne(set) +} + +func DeleteById(id primitive.ObjectID) (err error) { + _, err = coll(nil).DeleteOne(bson.M{"_id": id}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteById", table, "DeleteOne", err), + log.Any("id", id), + ) + return + } + return +} + +// GetBySectionID 根据专题ID获取专题详情 +func GetBySectionByID(sectionID primitive.ObjectID) (section Section, err error) { + err = coll(nil).FindOne(§ion, bson.M{"_id": sectionID}) + return +} + +// GetBySubModuleID 通过亚模块id获取专题列表 +func GetBySubModuleID(subModuleID primitive.ObjectID, page commod.Page) (section []Section, hasNext bool, err error) { + opts := options.FindOptions{} + opts.SetSkip(int64(page.Skip())).SetLimit(int64(page.Limit()) + 1).SetSort(bson.D{{"sort", 1}, {"_id", -1}}) + if err = coll(nil).Find(§ion, bson.M{"subModuleID": subModuleID, "status": 1}, &opts); err != nil { + return + } + if uint64(len(section)) > page.Limit() { + hasNext = true + section = section[:page.Limit()] + } + return +} + +func GetAllBySubModuleID(subModuleID primitive.ObjectID) (section []Section, err error) { + section = make([]Section, 0) + op := options.Find().SetSort(bson.D{{"sort", 1}}) + err = coll(nil).Find(§ion, bson.M{"subModuleID": subModuleID, "status": 1}, op) + return +} + +func HomePageGetBySubModuleID(subModuleID primitive.ObjectID) (section []Section, err error) { + section = make([]Section, 0) + op := options.Find().SetSort(bson.D{{"sort", 1}}).SetLimit(50) + err = coll(nil).Find(§ion, bson.M{"subModuleID": subModuleID, "status": 1}, op) + return +} + +func CountBySubModuleID(subModuleID primitive.ObjectID) (int64, error) { + return coll(nil).Count(bson.M{"subModuleID": subModuleID, "status": 1}) +} + +func AllSections() (list []Section, err error) { + err = coll(nil).Find(&list, bson.M{"status": 1}) + return +} + +// AllSectionsModule 获取所有激活的专题,以及对应的模块信息 +func AllSectionsModule() (list []SectionModule, err error) { + pipeline := []bson.M{ + {"$match": bson.M{"status": 1}}, + {"$lookup": bson.M{ + "from": models.ModuleConf, + "localField": "subModuleID", + "foreignField": "_id", + "as": "sectionMoudle", + }}, + } + err = coll(nil).Aggregate(&list, pipeline) + return +} + +func GetByIDs(ids []primitive.ObjectID) (list []SectionModule, err error) { + pipeline := []bson.M{ + {"$match": bson.M{"_id": bson.M{"$in": ids}}}, + {"$lookup": bson.M{ + "from": models.ModuleConf, + "localField": "subModuleID", + "foreignField": "_id", + "as": "sectionMoudle", + }}, + } + err = coll(nil).Aggregate(&list, pipeline) + return +} + +// TODO: 并发不安全 +func checkLimit(subModuleID primitive.ObjectID) (ok bool, err error) { + subModule, err := moduleconfmod.GetByID(subModuleID) + if err != nil { + return + } + total, err := CountBySubModuleID(subModuleID) + if err != nil { + return + } + return subModule.SectionLimit == 0 || int(total) < subModule.SectionLimit, nil +} + +// ListBySubModule returns topic metadata under one module. The hard limit keeps +// the merged system/custom topic response bounded even if historical data is malformed. +func ListBySubModule(subModuleID primitive.ObjectID, limit int64) ([]Section, error) { + if limit <= 0 || limit > 1000 { + limit = 1000 + } + var list []Section + err := coll(nil).Find(&list, bson.M{ + "subModuleID": subModuleID, + "deletedAt": nil, + }, options.Find().SetSort(bson.D{{Key: "sort", Value: -1}, {Key: "updatedAt", Value: -1}}).SetLimit(limit)) + return list, err +} + +// UpdateTopicFields updates a section only when it belongs to the expected module. +func UpdateTopicFields(t *db.MongoTool, id, subModuleID primitive.ObjectID, fields bson.M) (int64, error) { + fields["updatedAt"] = time.Now() + result, err := coll(t).UpdateOne(bson.M{ + "_id": id, "subModuleID": subModuleID, "deletedAt": nil, + }, bson.M{"$set": fields}) + if err != nil { + return 0, err + } + return result.MatchedCount, nil +} + +func DeleteTopic(t *db.MongoTool, id, subModuleID primitive.ObjectID) (int64, error) { + result, err := coll(t).DeleteOne(bson.M{"_id": id, "subModuleID": subModuleID}) + if err != nil { + return 0, err + } + return result.DeletedCount, nil +} + +// Search 根据条件检索section +func Search(q QuerySelector, p commod.Page) (list []Section, hasNext bool, total int64, err error) { + skip := int64(p.Skip()) + limit := int64(p.Limit() + 1) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: append(p.GetSort(), bson.E{Key: "createdAt", Value: -1}), + } + filter, err := common.ToBsonM(q) + if err != nil { + return + } + if q.SectionName != nil { + filter["sectionName"] = primitive.Regex{ + Pattern: *q.SectionName, + } + } + if err = coll(nil).Find(&list, filter, &opts); err != nil { + return + } + if total, err = coll(nil).Count(filter); err != nil { + return + } + if uint64(len(list)) > p.Limit() { + hasNext = true + list = list[:p.Limit()] + } + return +} + +// GetBySectionByIDs 根据专题IDS获取专题详情 +func GetBySectionByIDs(sectionIds []primitive.ObjectID) (section []Section, err error) { + err = coll(nil).Find(§ion, bson.M{"_id": bson.M{"$in": sectionIds}}) + return +} + +// GetBySectionBySids 根据专题IDS获取专题详情 +func GetBySectionBySids(sIds []primitive.ObjectID) (section []Section, err error) { + err = coll(nil).Find(§ion, bson.M{"subModuleID": bson.M{"$in": sIds}, "status": 1}) + return +} + +// GetBySectionID 根据专题ID获取专题详情 +func GetBySectionID(sectionID primitive.ObjectID) (section Section, err error) { + err = coll(nil).FindOne(§ion, bson.M{"_id": sectionID}) + return +} + +// QueryAllList 分页查询文档 +func QueryAllList(filter primitive.M, opts ...*options.FindOptions) (out []*Section, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} diff --git a/models/v/modulesectionmod/type.go b/models/v/modulesectionmod/type.go new file mode 100644 index 0000000..b3dddf6 --- /dev/null +++ b/models/v/modulesectionmod/type.go @@ -0,0 +1,144 @@ +package modulesectionmod + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models/v/moduleconfmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + OneLargeAndFourSmall = 101 // 101 一大四小(横-长视频) + FourGrid = 102 // 102 四宫格(横-长视频/ACG) + SixGrid = 103 // 103 六宫格(横-长视频) + Horizontal15Scroll = 104 // 104 横屏1.5滑动 + Horizontal25Scroll = 105 // 105 横屏2.5滑动 + HorizontalList = 106 // 106 横屏列表展示 + HorizontalSingle = 107 // 107 横屏大图(单个列表展示) + VerticalFourGrid = 201 // 四宫格(视频/ACG) + VerticalSixGrid = 202 // 六宫格(视频/ACG) + VerticalNineGrid = 203 // 九宫格(视频/ACG) + Vertical15Scroll = 204 // 竖屏1.5滑动(ACG) + Vertical25Scroll = 205 // 竖屏2.5滑动(ACG) + GuessYouLike = 301 // 猜你喜欢 +) + +var ShowTypeHashVale = map[int]int{ + OneLargeAndFourSmall: 5, // 101 一大四小(横-长视频) + FourGrid: 4, // 102 四宫格(横-长视频) + SixGrid: 6, // 103 六宫格(横-长视频) + Horizontal15Scroll: 12, // 104 横屏1.5滑动 + Horizontal25Scroll: 12, // 105 横屏2.5滑动 + HorizontalList: 3, // 106 横屏列表展示 + HorizontalSingle: 3, // 107 横屏大图(单个列表展示) + VerticalFourGrid: 4, // 2 四宫格(竖-短视频) + VerticalSixGrid: 6, // 4 六宫格(竖-短视频) + VerticalNineGrid: 9, // 5 九宫格(竖-长视频) + Vertical15Scroll: 12, // 7 竖屏1.5滑动 + Vertical25Scroll: 12, // 7 竖屏2.5滑动 + +} + +var ACGShowTypeHashVale = map[int]int{ + VerticalFourGrid: 4, // 2 四宫格(竖-短视频) + VerticalSixGrid: 6, // 4 六宫格(竖-短视频) + VerticalNineGrid: 9, // 5 九宫格(竖-长视频) + Vertical15Scroll: 12, // 7 竖屏1.5滑动 + Vertical25Scroll: 12, // 7 竖屏2.5滑动 +} + +// Section 专题配置 +type Section struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + SectionName string `json:"sectionName" bson:"sectionName" binding:"required"` // 亚模块下的专题名称 + SectionTitle string `json:"sectionTitle" bson:"sectionTitle"` // 专题的标题 + SectionCover *string `json:"sectionCover" bson:"sectionCover"` // 亚模块封面 + SubModuleID primitive.ObjectID `json:"subModuleID" bson:"subModuleID" binding:"required"` // 亚模块id + OriginalUserID *uint64 `json:"originalUserID" bson:"originalUserID,omitempty"` // 原创博主用户ID,原创模块需要使用 + Status *uint8 `json:"status" bson:"status" binding:"required"` // 状态,0-关闭,1-展示 + Sort *int `json:"sort" bson:"sort" binding:"required"` // 排序 + Hot bool `json:"hot" bson:"hot"` // 是否显示hot标识 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` // 刷新时间 + DeletedAt *time.Time `json:"deletedAt" bson:"deletedAt,omitempty"` // 删除时间,不能去除omitempty + ShowType int `json:"showType" bson:"showType,omitempty"` // 展示样式 + Tags *[]string `json:"tags" bson:"tags,omitempty"` // 所属标签 + TagIds *[]primitive.ObjectID `json:"tagIds" bson:"tagIds,omitempty"` // 所属标签Id +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type EditSelector struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty" binding:"required"` + SectionName *string `json:"sectionName,omitempty" bson:"sectionName,omitempty"` + SectionCover *string `json:"sectionCover" bson:"sectionCover,omitempty"` + SubModuleID *primitive.ObjectID `json:"subModuleID,omitempty" bson:"subModuleID,omitempty" binding:"required"` + Status *uint8 `json:"status,omitempty" bson:"status,omitempty"` + Sort *int `json:"sort,omitempty" bson:"sort,omitempty"` + Hot *bool `json:"hot" bson:"hot"` // 是否显示hot标识 + OriginalUserID *uint64 `json:"originalUserID,omitempty" bson:"originalUserID,omitempty"` + UpdatedAt time.Time `json:"updatedAt,omitempty" bson:"updatedAt"` + DeletedAt *time.Time `json:"deletedAt,omitempty" bson:"deletedAt,omitempty"` + ShowType *int `json:"showType,omitempty" bson:"showType,omitempty"` + Tags *[]string `json:"tags" bson:"tags,omitempty"` // 标签 + TagIds []primitive.ObjectID `json:"tagIds" bson:"tagIds,omitempty"` // 所属标签Id +} + +type QuerySelector struct { + ID *primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty" form:"id,omitempty" swaggertype:"string"` + SectionName *string `json:"sectionName,omitempty" bson:"sectionName,omitempty" form:"sectionName,omitempty"` + SubModuleID *primitive.ObjectID `json:"subModuleID,omitempty" bson:"subModuleID,omitempty" form:"subModuleID,omitempty" swaggertype:"string"` + Status *uint8 `json:"status,omitempty" bson:"status,omitempty" form:"status,omitempty"` +} + +type ListResp struct { + Total int64 `json:"total"` // 总数 + HasNext bool `json:"hasNext"` // 是否还有下一页 + List []SectionDetail `json:"list"` // 列表 +} + +type SectionDetail struct { + Section + ModuleInfo + OriginalBloggerInfo +} + +type ModuleInfo struct { + ModuleName string `json:"moduleName"` // 模块名称 + SubModuleName string `json:"subModuleName"` // 子模块名称 + SectionLimit int `json:"sectionLimit"` // 专题数量限制 +} + +type OriginalBloggerInfo struct { + Name string `json:"name"` // 博主姓名 + Portrait string `json:"portrait"` // 头像 + OfficialCert bool `json:"officialCert"` // 是否官方认证 + IsMadou bool `json:"isMadou"` // 是否工作室 +} + +type SectionModule struct { + Section `bson:",inline"` + SectionModule []moduleconfmod.ModuleConf `bson:"sectionMoudle"` +} + +type ModuleSection struct { + moduleconfmod.ModuleConf // 模块,子模块信息 + Sections []Section `json:"sections"` // 子模块下的所有专题 +} + +// AllSectionConf 专题配置 +type AllSectionConf struct { + ID primitive.ObjectID `json:"id" bson:"id"` // 文档ID + ModuleName string `json:"moduleName" bson:"moduleName" ` // 版块名称 + AllSection []SectionConf `json:"allSection" bson:"allSection"` // 所有专题 +} + +type SectionConf struct { + ID primitive.ObjectID `json:"id" bson:"id"` // 文档ID + SectionName string `json:"sectionName" bson:"sectionName" ` // 专题名称 +} diff --git a/models/v/modulevidmod/modulevid.go b/models/v/modulevidmod/modulevid.go new file mode 100644 index 0000000..0a315bf --- /dev/null +++ b/models/v/modulevidmod/modulevid.go @@ -0,0 +1,238 @@ +package modulevidmod + +import ( + "context" + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.SectionVideo + +// InitIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "sectionID", Value: 1}, {Key: "videoID", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "sectionID", Value: 1}, {Key: "sortCode", Value: -1}, {Key: "videoReviewedAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "sectionID", Value: 1}, {Key: "newsType", Value: 1}, {Key: "sortCode", Value: -1}, {Key: "videoReviewedAt", Value: -1}}, + }, + // 改类型的时候,需要根据videoID变更newsType + { + Keys: bson.D{{Key: "videoID", Value: 1}}, + }, + { + Keys: bson.D{{Key: "sectionID", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InsertOne 插入一条数据 +func InsertOne(p *SectionVideo) error { + p.CreatedAt = time.Now() + _, err := coll(nil).InsertOne(p) + return err +} + +func UpdateNewsTypeByVideoID(videoID primitive.ObjectID, newsType string) (err error) { + filter := bson.M{"videoID": videoID} + _, err = coll(nil).UpdateOne(filter, bson.M{"$set": bson.M{"newsType": newsType}}) + return err +} + +// UpdateOne 更新一条数据 +func UpdateOne(set *EditSelector) error { + set.UpdatedAt = time.Now() + var filter primitive.M + if set.ID != nil && !set.ID.IsZero() { + filter = bson.M{"_id": set.ID} + } else if set.SectionID != nil && !set.SectionID.IsZero() && set.VideoID != nil && !set.VideoID.IsZero() { + filter = bson.M{"sectionID": set.SectionID, "videoID": set.VideoID} + } + _, err := coll(nil).UpdateOne(filter, bson.M{"$set": set}) + return err +} + +// DeleteOne 删除一条数据 +func DeleteOne(id primitive.ObjectID) (err error) { + _, err = coll(nil).DeleteById(id) + return +} + +// BatchDeleteVideo 批量删除专题下视频 +func BatchDeleteVideo(ctx context.Context, sectionID primitive.ObjectID, vids []primitive.ObjectID) error { + _, err := coll(nil).DeleteMany(bson.M{"sectionID": sectionID, "videoID": bson.M{"$in": vids}}) + return err +} + +// DeleteManyVideo 批量删除专题下视频 +func DeleteManyVideo(ids []primitive.ObjectID) error { + _, err := coll(nil).DeleteMany(bson.M{"_id": bson.M{"$in": ids}}) + return err +} + +// DeleteManyByVids 批量删除专题下视频,根据视频ID +func DeleteManyByVids(vids []primitive.ObjectID) error { + _, err := coll(nil).DeleteMany(bson.M{"videoID": bson.M{"$in": vids}}) + return err +} + +// Search 根据条件检索 +func Search(q *QuerySelector, page commod.Page) (videos []SectionVideo, hasNext bool, total int64, err error) { + filter, err := common.ToBsonM(q) + if err != nil { + return + } + skip := int64(page.Skip()) + limit := int64(page.Limit() + 1) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: page.GetSort(), + } + if err = coll(nil).Find(&videos, filter, &opts); err != nil { + return + } + total, err = coll(nil).Count(filter) + if uint64(len(videos)) > page.Limit() { + hasNext = true + videos = videos[:page.Limit()] + } + return +} + +// Find 查询所有 +func Find(filter primitive.M, opts ...*options.FindOptions) ([]SectionVideo, error) { + var videos []SectionVideo + return videos, coll(nil).Find(&videos, filter, opts...) +} + +func GetBySectionIDWithPage(sectionID primitive.ObjectID, page commod.Page) (videoIDs []primitive.ObjectID, hasNext bool, err error) { + projection := bson.D{ + {Key: "_id", Value: 0}, + {Key: "videoID", Value: 1}, + } + opts := options.FindOptions{} + size := page.Limit() + opts.SetProjection(projection).SetSort(page.GetSort()).SetSkip(int64(page.Skip())).SetLimit(int64(size + 1)) + var p []struct { + VideoID primitive.ObjectID `bson:"videoID"` + } + if err = coll(nil).Find(&p, bson.M{"sectionID": sectionID}, &opts); err != nil { + return + } + if uint64(len(p)) > size { + hasNext = true + p = p[:size] + } + videoIDs = make([]primitive.ObjectID, len(p)) + for i, v := range p { + videoIDs[i] = v.VideoID + } + return +} + +// GetSectionVideosBySectionID 根据专题获取视频列表 +func GetSectionVideosBySectionID(sectionID primitive.ObjectID) (list []SectionVideo, err error) { + err = coll(nil).Find(&list, bson.M{"sectionID": sectionID}, options.Find().SetSort(bson.D{{Key: "sortCode", Value: -1}, {Key: "videoReviewedAt", Value: -1}})) + return +} + +// SectionVideosBySectionID 根据专题获取视频列表 +func SectionVideosBySectionID(sectionID primitive.ObjectID, opts *options.FindOptions) ([]SectionVideo, error) { + var out []SectionVideo + return out, coll(nil).Find(&out, bson.M{"sectionID": sectionID}, opts) +} + +func GetVideosBySectionIDs(sectionIDs []primitive.ObjectID) (list []SectionVideo, err error) { + err = coll(nil).Find(&list, bson.M{"sectionID": bson.M{"$in": sectionIDs}}) + return +} + +// GetSectionVideosBySectionIDSort 根据专题获取视频列表 +func GetSectionVideosBySectionIDSort(sectionID primitive.ObjectID, limit int64) (list []SectionVideo, err error) { + err = coll(nil).Find(&list, bson.M{"sectionID": sectionID}, options.Find().SetSort(bson.D{{Key: "SortCode", Value: -1}}).SetLimit(limit)) + return +} + +func GetBySectionIDAndVids(sectionID primitive.ObjectID, vids []primitive.ObjectID) (videos []SectionVideo, err error) { + filter := bson.M{"videoID": bson.M{"$in": vids}} + if !sectionID.IsZero() { + filter["sectionID"] = sectionID + } + err = coll(nil).Find(&videos, filter) + return +} + +func GetBySectionID(sectionID primitive.ObjectID, isSorted bool) (videoIDs []primitive.ObjectID, err error) { + projection := bson.D{ + {Key: "_id", Value: 0}, + {Key: "videoID", Value: 1}, + } + opts := options.FindOptions{} + opts.SetProjection(projection) + var p []struct { + VideoID primitive.ObjectID `bson:"videoID"` + } + filter := bson.M{"sectionID": sectionID} + if isSorted { + filter["sortCode"] = bson.M{"$gt": 0} + opts.SetSort(bson.D{{Key: "sortCode", Value: -1}, {Key: "videoReviewedAt", Value: -1}}) + } + if err = coll(nil).Find(&p, filter, &opts); err != nil { + return + } + videoIDs = make([]primitive.ObjectID, len(p)) + for i, v := range p { + videoIDs[i] = v.VideoID + } + return +} + +func InsertMany(records []SectionVideo) error { + opts := options.InsertMany().SetOrdered(false) + _, err := coll(nil).InsertMany(records, opts) + if !db.IsMongoDupKey(err) { + return err + } + return nil +} + +func GetSectionsByVideoId(id primitive.ObjectID) ([]SectionVideo, error) { + if id.IsZero() { + return nil, nil + } + var svs []SectionVideo + return svs, coll(nil).Find(&svs, bson.M{"videoID": id}) +} + +func GetSectionsByVideoIds(ids []primitive.ObjectID) ([]SectionVideo, error) { + var svs []SectionVideo + return svs, coll(nil).Find(&svs, bson.M{"videoID": bson.M{"$in": ids}}) +} diff --git a/models/v/modulevidmod/type.go b/models/v/modulevidmod/type.go new file mode 100644 index 0000000..423eedc --- /dev/null +++ b/models/v/modulevidmod/type.go @@ -0,0 +1,45 @@ +package modulevidmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// SectionVideo 专题视频 +type SectionVideo struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + SectionID primitive.ObjectID `json:"sectionID" bson:"sectionID" binding:"required"` // 专题id + NewsType string `json:"newsType" bson:"newsType"` // 视频类型 + VideoID primitive.ObjectID `json:"videoID" bson:"videoID" binding:"required"` // 视频id + VideoReviewedAt time.Time `json:"videoReviewedAt" bson:"videoReviewedAt"` // 视频审核时间,冗余字段,方便排序 + SortCode int `json:"sortCode" bson:"sortCode"` // 专题内视频排序 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` // 刷新时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type EditSelector struct { + ID *primitive.ObjectID `json:"id" bson:"_id,omitempty"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + SortCode *int `json:"sortCode" bson:"sortCode,omitempty"` + SectionID *primitive.ObjectID `json:"sectionID" bson:"sectionID,omitempty"` + VideoID *primitive.ObjectID `json:"videoID" bson:"videoID,omitempty"` +} + +type QuerySelector struct { + SectionID primitive.ObjectID `json:"sectionID,omitempty" bson:"sectionID,omitempty" form:"sectionID,omitempty"` // 专题id + VideoID primitive.ObjectID `json:"videoID,omitempty" bson:"videoID,omitempty" form:"videoID,omitempty"` // 视频id +} + +// BatchDeleteVideoRequest 批量删除专题下视频request +type BatchDeleteVideoRequest struct { + SectionID primitive.ObjectID `json:"sectionID" swaggertype:"string" binding:"required"` // 专题ID + VideoIDs []primitive.ObjectID `json:"videoIDs" swaggertype:"array,string" binding:"required"` // 视频ID列表 +} diff --git a/models/v/nakedchatmod/app.go b/models/v/nakedchatmod/app.go new file mode 100755 index 0000000..47d0fa8 --- /dev/null +++ b/models/v/nakedchatmod/app.go @@ -0,0 +1,43 @@ +package nakedchatmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// NakedChatSimple 移动端返回列表内容 +type NakedChatSimple struct { + ID primitive.ObjectID `json:"id"` // 文档id + Title string `json:"title"` // 标题 + Cover string `json:"cover"` // 封面 + Price uint64 `json:"price"` // 价格 n金币/分钟 + Age int `json:"age"` // 年龄 单位 岁 + Weight int `json:"weight"` // 体重 单位 kg + Height int `json:"height"` // 身高 单位 cm + Cup string `json:"cup"` // 罩杯 + SaleNum int64 `json:"saleNum"` // 销售数量(多少人看过) + CreatedAt time.Time `json:"createdAt"` // 文档创建时间 + UpdatedAt time.Time `json:"updatedAt"` // 文档更新时间 +} + +// NakedChatInfo 移动端返回内容 +type NakedChatInfo struct { + ID primitive.ObjectID `json:"id"` // 文档id + Title string `json:"title"` // 标题 + Cover string `json:"cover"` // 封面 + Price uint64 `json:"price"` // 价格 n金币/分钟 + Options []uint64 `json:"options"` // 可供购买选项 ,多少分钟 + Images []string `json:"images"` // 图片列表 + Video string `json:"video"` // 展示的视频 + Contact string `json:"contact "` // 联系方式 + Age int `json:"age"` // 年龄 单位 岁 + Weight int `json:"weight"` // 体重 单位 kg + Height int `json:"height"` // 身高 单位 cm + Cup string `json:"cup"` // 罩杯 + SaleNum int64 `json:"saleNum"` // 销售数量(多少人看过) + BusinessHours string `json:"businessHours"` // 连线时间 + Summary string `json:"summary"` // 简介 + CreatedAt time.Time `json:"createdAt"` // 文档创建时间 + UpdatedAt time.Time `json:"updatedAt"` // 文档更新时间 +} diff --git a/models/v/nakedchatmod/nakedchat.go b/models/v/nakedchatmod/nakedchat.go new file mode 100755 index 0000000..53388ec --- /dev/null +++ b/models/v/nakedchatmod/nakedchat.go @@ -0,0 +1,149 @@ +package nakedchatmod + +import ( + "encoding/json" + "errors" + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []NakedChat, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// GetAll 查询全部文档 +func GetAll(filter primitive.M, sort bson.D) (out []*NakedChat, err error) { + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAll", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (NakedChat, error) { + v := NakedChat{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d NakedChat) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// IncSaleNum 增加购买数量(真实) +func IncSaleNum(t *db.MongoTool, id primitive.ObjectID, num int) error { + filter := bson.M{"_id": id} + _, err := coll(nil).UpdateOne(filter, bson.M{"$inc": bson.M{"saleNum": num}}) + return err +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} diff --git a/models/v/nakedchatmod/type.go b/models/v/nakedchatmod/type.go new file mode 100755 index 0000000..f9ad247 --- /dev/null +++ b/models/v/nakedchatmod/type.go @@ -0,0 +1,71 @@ +package nakedchatmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.NakedChat + +type NakedChat struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + Title string `json:"title" bson:"title"` // 标题 + Cover string `json:"cover" bson:"cover"` // 封面 + Price uint64 `json:"price" bson:"price"` // 价格 n金币/分钟 + Options []uint64 `json:"options" bson:"options"` // 可供购买选项 ,多少分钟 + Images []string `json:"images" bson:"images"` // 图片列表 + Video string `json:"video" bson:"video"` // 展示的视频 + Contact string `json:"contact" bson:"contact"` // 联系方式 + Age int `json:"age" bson:"age"` // 年龄 单位 岁 + Weight int `json:"weight" bson:"weight"` // 体重 单位 kg + Height int `json:"height" bson:"height"` // 身高 单位 cm + Cup string `json:"cup" bson:"cup"` // 罩杯 + SaleNum int64 `json:"saleNum" bson:"saleNum"` // 销售数量 + FakeSaleNum int64 `json:"fakeSaleNum" bson:"fakeSaleNum"` // 销售数量(假) + BusinessHours string `json:"businessHours" bson:"businessHours"` // 连线时间 + Summary string `json:"summary" bson:"summary"` // 简介 + Status int `json:"status" bson:"status"` // 0-下架 1-上架 + Mid primitive.ObjectID `json:"mid" bson:"mid"` // 模块id + SortCode int64 `json:"sortCode" bson:"sortCode"` // 排序号 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 文档创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 文档更新时间 +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 todo: 在models/Init/init.go中调用初始化 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "_id", Value: 1}}, + }, + { + Keys: bson.D{{Key: "title", Value: 1}}, + }, + { + Keys: bson.D{{Key: "mid", Value: 1}, {Key: "sortCode", Value: -1}, {Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} diff --git a/models/v/nakedchatordermod/app.go b/models/v/nakedchatordermod/app.go new file mode 100755 index 0000000..80fbe2f --- /dev/null +++ b/models/v/nakedchatordermod/app.go @@ -0,0 +1,22 @@ +package nakedchatordermod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// NakedChatOrderInfo 移动端返回内容 +type NakedChatOrderInfo struct { + ID primitive.ObjectID `json:"id"` // 文档id + Nid primitive.ObjectID `json:"nid"` // 裸聊id + Uid uint64 `json:"uid"` // uid + UserContact string `json:"userContact "` // 用户联系方式 + Num uint64 `json:"num"` // 购买数量 + Price uint64 `json:"price"` // 购买的单价 + Amount int64 `json:"amount"` // 总额 + Remark string `json:"remark"` // 订单备注信息 + Status int `json:"status"` // 0-已下单 1-已完成 + CreatedAt time.Time `json:"createdAt"` // 文档创建时间 + UpdatedAt time.Time `json:"updatedAt"` // 文档更新时间 +} diff --git a/models/v/nakedchatordermod/nakedchatorder.go b/models/v/nakedchatordermod/nakedchatorder.go new file mode 100755 index 0000000..780c38e --- /dev/null +++ b/models/v/nakedchatordermod/nakedchatorder.go @@ -0,0 +1,142 @@ +package nakedchatordermod + +import ( + "encoding/json" + "errors" + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []NakedChatOrder, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// GetAll 查询全部文档 +func GetAll(filter primitive.M, sort bson.D) (out []*NakedChatOrder, err error) { + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAll", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (NakedChatOrder, error) { + v := NakedChatOrder{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d NakedChatOrder) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} diff --git a/models/v/nakedchatordermod/type.go b/models/v/nakedchatordermod/type.go new file mode 100755 index 0000000..80a6e14 --- /dev/null +++ b/models/v/nakedchatordermod/type.go @@ -0,0 +1,60 @@ +package nakedchatordermod + +import ( + "91porn-server/models/v/nakedchatmod" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.NakedChatOrder + +type NakedChatOrder struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + Nid primitive.ObjectID `json:"nid" bson:"nid"` // 裸聊id + Uid uint64 `json:"uid" bson:"uid"` // uid + UserContact string `json:"userContact " bson:"userContact"` // 用户联系方式 + Num uint64 `json:"num" bson:"num"` // 购买数量 + Price uint64 `json:"price" bson:"price"` // 购买的单价 + Amount int64 `json:"amount" bson:"amount"` // 总额 + Remark string `json:"remark" bson:"remark"` // 订单备注信息 + Snapshot nakedchatmod.NakedChat `json:"snapshot" bson:"snapshot"` // 裸聊产品快照 + Status int `json:"status" bson:"status"` // 0-已下单 1-已完成 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 文档创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 文档更新时间 +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 todo: 在models/Init/init.go中调用初始化 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "_id", Value: 1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} diff --git a/models/v/newactivity/activitymodel.go b/models/v/newactivity/activitymodel.go new file mode 100644 index 0000000..0a7b87e --- /dev/null +++ b/models/v/newactivity/activitymodel.go @@ -0,0 +1,139 @@ +package newactivity + +import ( + "time" + + "91porn-server/common/timeutil" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func BulkAddModels(insertSqls []mongo.WriteModel) error { + if len(insertSqls) > 0 { + _, err := collModel(nil).Bulk(insertSqls) + return err + } + return nil +} + +// 嫩模列表 +func ModelList() (res ListRes, err error) { + res = ListRes{} + if err = collModel(nil).Aggregate(&res.Models, []bson.M{ + { + "$group": bson.M{ + "_id": "$location", + "models": bson.M{ + "$push": bson.M{ + "id": "$id", + "name": "$name", + "location": "$location", + "bgImg": "$bgImg", + "video": "$video", + }, + }, + }, + }, + { + "$project": bson.M{ + "location": "$_id", + "models": 1, + "_id": 0, + }, + }, + { + "$sort": bson.M{ + "models.id": 1, + }, + }, + }); err != nil { + return + } + num := struct { + JoinPersons int64 `bson:"num"` //参与人数 + }{} + err = collJoin(nil).FindOne(&num, bson.M{}) + res.JoinPersons = num.JoinPersons + return +} + +// 单个嫩模 +func FindOne(modelId uint32) (res FindOneRes, err error) { + res = FindOneRes{} + date := timeutil.BeginningOfDay(time.Now()) + if time.Now().Hour() >= 20 { + date = date.Add(24 * time.Hour) + } + if err = collModel(nil).AggregateDecode(&res, []bson.M{ + { + "$match": bson.M{ + "id": modelId, + }, + }, + { + "$lookup": bson.M{ + "from": "activity_gift_stock", + "pipeline": []bson.M{ + { + "$match": bson.M{ + "modelId": modelId, + "date": date, + }, + }, + {"$project": bson.M{ + "_id": 0, + "giftLeft": 1, + "soldOut": 1, + }}, + }, + "as": "gift_stock", + }, + }, + { + "$unwind": bson.M{ + "path": "$gift_stock", + "preserveNullAndEmptyArrays": true, + }, + }, + { + "$project": bson.M{ + "_id": 0, + "id": 1, + "name": 1, + "bgImg": 1, + "location": 1, + "video": 1, + "giftLeft": "$gift_stock.giftLeft", + "soldOut": "$gift_stock.soldOut", + }, + }, + }); err != nil { + return + } + opts := options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}}) + conf := bson.M{ + "modelId": modelId, + "date": date, + } + if res.SoldOut { + conf["soldOut"] = true + } + err = collSold(nil).Find(&res.SoldList, conf, opts) + return +} + +// 嫩模列表 +func UpdateJoinNum(num int64) error { + _, err := collJoin(nil).UpsertOne(bson.M{}, bson.M{"$set": bson.M{"num": num}}) + return err +} + +// 获取参与人数 +func GetJoinNum() (int64, error) { + num := struct { + JoinPersons int64 `bson:"num"` //参与人数 + }{} + return num.JoinPersons, collJoin(nil).FindOne(&num, bson.M{}) +} diff --git a/models/v/newactivity/activitysold.go b/models/v/newactivity/activitysold.go new file mode 100644 index 0000000..8a9bf66 --- /dev/null +++ b/models/v/newactivity/activitysold.go @@ -0,0 +1,78 @@ +package newactivity + +import ( + "fmt" + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func ExistsOrder(num uint32, req WinRecords) (exists bool, err error) { + conf := bson.M{ + "date": req.Date, + "userId": req.UserId, + "modelId": req.ModelId, + } + if num == 0 { + conf["soldOut"] = true + } else { + conf["serials"] = num + } + count, err := collSold(nil).Count(conf) + return count > 0, err +} + +func BuyPage(req BuyPageReq) (res BuyPageRes, err error) { + conf := bson.M{} + if req.ModelId != nil { + conf["modelId"] = req.ModelId + } + if req.UserId != nil { + conf["userId"] = req.UserId + } + if req.UserName != nil { + conf["userName"] = bson.M{"$regex": req.UserName} + } + if !req.Date.IsZero() { + conf["date"] = req.Date + } + if !req.Start.IsZero() && !req.End.IsZero() { + conf["createdAt"] = bson.M{"$gte": req.Start, "$lt": req.End} + } + res = BuyPageRes{} + opts := options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}}) + opts.SetSkip((req.Page - 1) * req.PageSize) + opts.SetLimit(req.PageSize) + if err = collSold(nil).Find(&res.Data, conf, opts); err != nil { + return + } + res.Total, err = collSold(nil).Count(conf) + if err != nil { + return + } + num := struct { + Num int64 `bson:"num"` + }{} + err = collJoin(nil).FindOne(&num, bson.M{}) + res.JoinPersons = num.Num + return +} + +// 嫩模列表 +func InsertSoldLog(t *db.MongoTool, log *SoldRecord) (id primitive.ObjectID, err error) { + log.CreatedAt = time.Now() + log.UpdatedAt = time.Now() + result, err := collSold(t).InsertOne(log) + if err != nil { + fmt.Println("insert error", err) + return + } + if id, ok := result.InsertedID.(primitive.ObjectID); ok { + return id, nil + } + return +} diff --git a/models/v/newactivity/activitystock.go b/models/v/newactivity/activitystock.go new file mode 100644 index 0000000..76697c3 --- /dev/null +++ b/models/v/newactivity/activitystock.go @@ -0,0 +1,52 @@ +package newactivity + +import ( + "errors" + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func BulkStocks(insertSqls []mongo.WriteModel) (err error) { + if len(insertSqls) > 0 { + _, err = collStock(nil).Bulk(insertSqls) + } + return +} + +// 嫩模列表 +func UpdateStock(t *db.MongoTool, req BuyReq, date time.Time) (giftLeft uint32, location string, err error) { + res := struct { + GiftLeft uint32 `bson:"giftLeft"` //剩余礼物数目 + Location string `bson:"location"` //模特地区 + }{} + conf := bson.M{ + "modelId": req.ModelId, + "date": date, + "soldOut": false, + } + var update bson.M + if req.BuyOut { + update = bson.M{"$set": bson.M{"soldOut": true}} + } else { + conf["giftLeft"] = bson.M{"$gte": req.Quantity} + update = bson.M{"$inc": bson.M{"giftLeft": -req.Quantity}} + } + opts := options.FindOneAndUpdate().SetProjection(bson.M{ + "_id": 0, + "giftLeft": 1, + "location": 1, + }) + if err = collStock(t).FindOneAndUpdateReturnTiny(&res, conf, update, false, opts); err != nil { + if err == mongo.ErrNoDocuments { + err = errors.New("礼物卖完了😯") + } + } + giftLeft = res.GiftLeft + location = res.Location + return +} diff --git a/models/v/newactivity/activitywin.go b/models/v/newactivity/activitywin.go new file mode 100644 index 0000000..62b2f4a --- /dev/null +++ b/models/v/newactivity/activitywin.go @@ -0,0 +1,46 @@ +package newactivity + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func AddWinRecord(req WinRecords) error { + _, err := collWin(nil).UpsertOne(bson.M{ + "date": req.Date, + "modelId": req.ModelId, + }, bson.M{"$set": req}) + return err +} + +func WinRecordsByDate(date time.Time) ([]WinRecords, error) { + list := make([]WinRecords, 0) + return list, collWin(nil).Find(&list, bson.M{"date": date}) +} + +func WinRecordPage(req WinRcdPageReq) (res WinRcdPageRes, err error) { + conf := bson.M{} + if req.ModelId != nil { + conf["modelId"] = req.ModelId + } + if req.UserId != nil { + conf["userId"] = req.UserId + } + if req.UserName != nil { + conf["userName"] = bson.M{"$regex": req.UserName} + } + if !req.Date.IsZero() { + conf["date"] = req.Date + } + res = WinRcdPageRes{} + opts := options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}}) + opts.SetSkip((req.Page - 1) * req.PageSize) + opts.SetLimit(req.PageSize) + if err = collWin(nil).Find(&res.Data, conf, opts); err != nil { + return + } + res.Total, err = collWin(nil).Count(conf) + return +} diff --git a/models/v/newactivity/type.go b/models/v/newactivity/type.go new file mode 100644 index 0000000..8239c6d --- /dev/null +++ b/models/v/newactivity/type.go @@ -0,0 +1,164 @@ +package newactivity + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models" +) + +var ( + mdb *db.MongoDB + stockdb *db.MongoDB + solddb *db.MongoDB + windb *db.MongoDB + joindb *db.MongoDB +) + +const ( + activity_model_table = models.ActivityModels + activity_gift_stock = models.ActivityGitStock + activity_sold_record = models.ActivitySoldRecord + activity_win_record = models.ActivityWinRecord + activity_join_persons = models.ActivityJoinPersons +) + +func collModel(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(activity_model_table) + } + return t.Coll(activity_model_table) +} + +func collStock(t *db.MongoTool) *db.MongoTool { + if t == nil { + return stockdb.Coll(activity_gift_stock) + } + return t.Coll(activity_gift_stock) +} + +func collSold(t *db.MongoTool) *db.MongoTool { + if t == nil { + return solddb.Coll(activity_sold_record) + } + return t.Coll(activity_sold_record) +} + +func collWin(t *db.MongoTool) *db.MongoTool { + if t == nil { + return windb.Coll(activity_win_record) + } + return t.Coll(activity_win_record) +} + +func collJoin(t *db.MongoTool) *db.MongoTool { + if t == nil { + return joindb.Coll(activity_join_persons) + } + return t.Coll(activity_join_persons) +} + +type Model struct { + Id uint32 `json:"id" bson:"id"` //id + Name string `json:"name" bson:"name"` //名字 + BgImg []string `json:"bgImg" bson:"bgImg"` //背景大图 + Location string `json:"location" bson:"location"` //地区 + Video string `json:"video" bson:"video"` //视频 +} + +type ListRes struct { + JoinPersons int64 `json:"JoinPersons" bson:"JoinPersons"` //活动参与人数 + Models []LocationModels `json:"models" bson:"models"` //模特 +} + +type LocationModels struct { + Location string `json:"location" bson:"location"` //地区 + Models []Model `json:"models" bson:"models"` //模特 +} + +type FindOneRes struct { + Id uint32 `json:"id" bson:"id"` //id + Name string `json:"name" bson:"name"` //名字 + BgImg []string `json:"bgImg" bson:"bgImg"` //背景大图 + Location string `json:"location" bson:"location"` //地区 + Video string `json:"video" bson:"video"` //视频 + GiftLeft int32 `json:"giftLeft" bson:"giftLeft"` //剩余礼物数目 + SoldOut bool `json:"soldOut" bson:"soldOut"` //是否已被买断 + SoldList []SoldRecord `json:"soldList" bson:"soldList"` //卖出礼物清单 +} + +type GiftStock struct { + ModelId uint32 `json:"modelId" bson:"modelId"` //模特id + Date time.Time `json:"date" bson:"date"` //日期 + GiftLeft int32 `json:"giftLeft" bson:"giftLeft"` //剩余礼物数目 + SoldOut bool `json:"soldOut" bson:"soldOut"` //是否已被买断 +} + +type SoldRecord struct { + Date time.Time `json:"date" bson:"date"` //日期 + ModelId uint32 `json:"modelId" bson:"modelId"` //模特id + UserId uint32 `json:"userId" bson:"userId"` //用户Id + UserName string `json:"userName" bson:"userName"` //用户昵称 + UserLogo string `json:"userLogo" bson:"userLogo"` //用户头像 + Quantity int32 `json:"quantity" bson:"quantity"` //礼物数目 + SoldOut bool `json:"soldOut" bson:"soldOut"` //是否是买断 + Serials []uint32 `json:"serials" bson:"serials"` //礼物序号 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //更新时间 +} + +type BuyReq struct { + ModelId uint32 `json:"modelId"` //模特id + Quantity int32 `json:"quantity"` //礼物数目 + BuyOut bool `json:"buyOut"` //是否买断 +} + +type BuyPageReq struct { + Page int64 `json:"page"` //页码 + PageSize int64 `json:"pageSize"` //条数 + UserId *uint32 `json:"userId"` //用户id + UserName *string `json:"userName"` //用户昵称 + ModelId *uint32 `json:"modelId"` //模特id + Date time.Time `json:"date"` //期号 + Start time.Time `json:"start"` //开始时间 + End time.Time `json:"end"` //结束时间 +} + +type BuyPageRes struct { + Total int64 `json:"total"` //页码 + Data []SoldRecord `json:"data"` //页码 + JoinPersons int64 `json:"joinPersons"` //参与人数 +} + +type WinRecords struct { + Date time.Time `json:"date" bson:"date"` //日期 + UserId uint32 `json:"userId" bson:"userId"` //用户id + UserName string `json:"userName" bson:"userName"` //用户昵称 + UserLogo string `json:"userLogo" bson:"userLogo"` //用户头像 + WinNum uint32 `json:"winNum" bson:"winNum"` //中奖号码 + ModelId uint32 `json:"modelId" bson:"modelId"` //模特Id + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //更新时间 +} + +type WinRcdPageReq struct { + Page int64 `json:"page"` //页码 + PageSize int64 `json:"pageSize"` //条数 + UserId *uint32 `json:"userId"` //用户id + UserName *string `json:"userName"` //用户昵称 + ModelId *uint32 `json:"modelId"` //模特id + Date time.Time `json:"date"` //期号 +} + +type WinRcdPageRes struct { + Total int64 `json:"total"` //页码 + Data []WinRecords `json:"data"` //页码 +} + +func Init() { + mdb = db.Init(activity_model_table) + stockdb = db.Init(activity_gift_stock) + joindb = db.Init(activity_join_persons) + solddb = db.Init(activity_sold_record) + windb = db.Init(activity_win_record) +} diff --git a/models/v/noticefmtmod/list.go b/models/v/noticefmtmod/list.go new file mode 100644 index 0000000..ba4d28c --- /dev/null +++ b/models/v/noticefmtmod/list.go @@ -0,0 +1,98 @@ +package noticefmtmod + +import ( + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Matcher = pageopt.Matcher + +// EnableMatch +type EnableMatch struct { + Enable *bool +} + +func (t *EnableMatch) New() Matcher { + return pageopt.NewAssignMatch("enable", t.Enable) +} + +// TitleMatch +type TitleMatch struct { + Title *string +} + +func (t *TitleMatch) New() Matcher { + return pageopt.NewAssignMatch("title", t.Title) +} + +// NoticeCodeMatch +type NoticeCodeMatch struct { + NoticeCode *string +} + +func (t *NoticeCodeMatch) New() Matcher { + return pageopt.NewAssignMatch("noticeCode", t.NoticeCode) +} + +// SenderMatch +type SenderMatch struct { + Sender *Sender +} + +func (t *SenderMatch) New() Matcher { + return pageopt.NewAssignMatch("sender", t.Sender) +} + +// ReceiverMatch +type ReceiverMatch struct { + Receiver *Receiver +} + +func (t *ReceiverMatch) New() Matcher { + return pageopt.NewAssignMatch("receiver", t.Receiver) +} + +// NoticeTypeMatch +type NoticeTypeMatch struct { + NoticeType *NoticeType +} + +func (t *NoticeTypeMatch) New() Matcher { + return pageopt.NewAssignMatch("noticeType", t.NoticeType) +} + +type UIDListMatch struct { + UID *uint64 +} + +func (u *UIDListMatch) New() Matcher { + return pageopt.NewAssignMatch("uidList", u.UID) +} + +type UIDListInMatch struct { + UIDList []uint64 +} + +func (u *UIDListInMatch) New() Matcher { + return pageopt.NewInMatch("uidList", u.UIDList) +} + +func List(sort bson.D, skip, limit int64, matchs ...Matcher) ([]NoticeFmt, error) { + filter := pageopt.MergeM(matchs) + opt := (&options.FindOptions{}) + if len(sort) != 0 { + opt.SetSort(sort) + } + opt.SetSkip(skip) + opt.SetLimit(limit) + + list := make([]NoticeFmt, 0, limit) + return list, coll(nil).Find(&list, filter, opt) +} + +func Count(matchs ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchs) + return coll(nil).Count(filter) +} diff --git a/models/v/noticefmtmod/notice.go b/models/v/noticefmtmod/notice.go new file mode 100644 index 0000000..b7c5cc5 --- /dev/null +++ b/models/v/noticefmtmod/notice.go @@ -0,0 +1,142 @@ +package noticefmtmod + +import ( + "fmt" + "sort" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// M M from Map +type M = bson.M + +var mdb *db.MongoDB + +const table = models.NoticeFmt + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "noticeCode", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "sender", Value: -1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: -1}}, + }, + { + Keys: bson.D{{Key: "receiver", Value: -1}}, + }, + { + Keys: bson.D{{Key: "noticeType", Value: -1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func InsertOne(doc InsertDoc) error { + sort.Sort(doc.UIDList) + sort.Sort(doc.SpecifyNotice.SpecifyTimeList) + docM, err := common.ToBsonM(doc) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "ToBsonM", err)) + return err + } + now := time.Now() + docM["updatedAt"] = now + docM["createdAt"] = now + if _, err = coll(nil).InsertOne(docM); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertOne", err)) + return err + } + return nil +} + +func UpdateOne(id ObjectID, doc UpdateDoc) error { + docM, err := common.ToBsonM(doc) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateOne", table, "ToBsonM", err)) + return err + } + now := time.Now() + docM["updatedAt"] = now + if _, err = coll(nil).UpsertOne(M{"_id": id}, M{"$set": docM}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateOne", table, "UpdateOne", err)) + return err + } + return nil +} + +func DeleteOne(id ObjectID) error { + filter := M{ + "_id": id, + } + if _, err := coll(nil).DeleteOne(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteOne", table, "DeleteOne", err)) + return err + } + return nil +} + +func GetAllUserNotice(matchs ...Matcher) ([]NoticeFmt, error) { + receiver := AllUser + matchs = append(matchs, (&ReceiverMatch{&receiver}).New()) + filter := pageopt.MergeM(matchs) + var list []NoticeFmt + return list, coll(nil).Find(&list, filter) +} + +func GetSomeUserNotice(uid uint64, matchs ...Matcher) ([]NoticeFmt, error) { + receiver := SomeUser + matchs = append(matchs, + (&ReceiverMatch{&receiver}).New(), + (&UIDListMatch{&uid}).New(), + ) + filter := pageopt.MergeM(matchs) + var list []NoticeFmt + return list, coll(nil).Find(&list, filter) +} + +func GetNoticeFmtListByUID(uid uint64, matchs ...Matcher) ([]NoticeFmt, error) { + someUserNotice, err := GetSomeUserNotice(uid, matchs...) + if err != nil { + return nil, err + } + allUserNotice, err := GetAllUserNotice(matchs...) + if err != nil { + return nil, err + } + return append(someUserNotice, allUserNotice...), nil +} + +func DeleteBeforeCreatedAt(t *db.MongoTool, tm time.Time) error { + _, err := coll(t).DeleteMany(bson.M{"createdAt": bson.M{"$lt": tm}}) + return err +} diff --git a/models/v/noticefmtmod/noticeimpl.go b/models/v/noticefmtmod/noticeimpl.go new file mode 100644 index 0000000..f6a3317 --- /dev/null +++ b/models/v/noticefmtmod/noticeimpl.go @@ -0,0 +1,169 @@ +package noticefmtmod + +import ( + "fmt" + "sort" + "time" + + "91porn-server/common/timeutil/timeslice" +) + +// TimeSlice +type TimeSlice = timeslice.TimeSlice + +// UIDSlice +type UIDSlice []uint64 + +func (u UIDSlice) Len() int { + return len(u) +} + +func (u UIDSlice) Swap(i, j int) { + u[i], u[j] = u[j], u[i] +} + +func (u UIDSlice) Less(i, j int) bool { + return u[i] < u[j] +} + +// 定时通知 从指定的开始时间起,根据设置的时间间隔周期性通知 +type RegularNotice struct { + StartAt *time.Time `bson:"regularStartAt,omitempty"` //开始时间 + TimeUnit *TimeUnit `bson:"regularTimeUnit,omitempty"` //单位 分钟/小时/天/周 + TimeCount *int64 `bson:"regularTimeCount,omitempty"` //量 +} + +func (t *RegularNotice) NextSendTimeList(current time.Time, lastSendAt time.Time) TimeSlice { + if t.StartAt == nil || t.TimeUnit == nil || t.TimeCount == nil { + return TimeSlice{} + } + if t.StartAt.After(current) { + return TimeSlice{} + } + per := t.TimeUnit.ToDuration() * time.Duration(*t.TimeCount) + if per == 0 { + return TimeSlice{} + } + curDelta := current.Sub(*t.StartAt) + nextSendTime := t.StartAt.Add(curDelta / per * per) + if lastSendAt.Before(nextSendTime) || lastSendAt.IsZero() { + return TimeSlice{nextSendTime} + } + return TimeSlice{} +} + +func (t *RegularNotice) Rule() string { + nextSendTime := t.NextSendTimeList(time.Now(), time.Time{}) + nextSendTimeStr := "无" + if nextSendTime.Len() != 0 { + nextSendTimeStr = nextSendTime[0].String() + } + return fmt.Sprintf("从 %s 开始, 每 %d %v, 发送消息\n 下次发送时间: %s", t.StartAt, t.TimeCount, t.TimeUnit, nextSendTimeStr) +} + +// 计划通知 根据指定的开始时间、结束时间和通知次数发起通知 +type PlanNotice struct { + StartAt *time.Time `bson:"planStartAt,omitempty"` //开始时间 + EndAt *time.Time `bson:"planEndAt,omitempty"` //结束时间 + PlanCount *int64 `bson:"planPlanCount,omitempty"` //执行次数 +} + +func (t *PlanNotice) NextSendTimeList(current time.Time, lastSendAt time.Time) TimeSlice { + if t.StartAt == nil || t.EndAt == nil || t.PlanCount == nil || *t.PlanCount == 0 { + return TimeSlice{} + } + if t.StartAt.After(current) || current.After(*t.EndAt) { + return TimeSlice{} + } + totalDelta := t.EndAt.Sub(*t.StartAt) + curDelta := t.EndAt.Sub(current) + per := totalDelta / time.Duration(*t.PlanCount) + nextSendTime := t.StartAt.Add(curDelta / per * per) + if lastSendAt.Before(nextSendTime) || lastSendAt.IsZero() { + return TimeSlice{nextSendTime} + } + return TimeSlice{} +} + +func (t *PlanNotice) Rule() string { + nextSendTime := t.NextSendTimeList(time.Now(), time.Time{}) + nextSendTimeStr := "无" + if nextSendTime.Len() != 0 { + nextSendTimeStr = nextSendTime[0].String() + } + return fmt.Sprintf("从 %s 开始 至 %s 结束, 计划发送 %d 次消息\n 下次发送时间: %s", t.StartAt, t.EndAt, t.PlanCount, nextSendTimeStr) +} + +// 规定通知 到达指定的多个时间点时通知 +type SpecifyNotice struct { + SpecifyTimeList TimeSlice `bson:"specifyTimeList,omitempty"` //指定执行时间数组 +} + +func (t *SpecifyNotice) NextSendTimeList(current time.Time, lastSendAt time.Time) TimeSlice { + if len(t.SpecifyTimeList) == 0 { + return TimeSlice{} + } + sort.Sort(t.SpecifyTimeList) + index := sort.Search(t.SpecifyTimeList.Len(), func(i int) bool { + return t.SpecifyTimeList[i].After(current) + }) + index = index - 1 //前一个才是已经过去的时间 + if index < 0 { + index = 0 + } + nextSendTime := t.SpecifyTimeList[index] + if lastSendAt.Before(nextSendTime) || lastSendAt.IsZero() { + return TimeSlice{nextSendTime} + } + return TimeSlice{} +} + +func (t *SpecifyNotice) Rule() string { + nextSendTime := t.NextSendTimeList(time.Now(), time.Time{}) + nextSendTimeStr := "无" + if nextSendTime.Len() != 0 { + nextSendTimeStr = nextSendTime[0].String() + } + return fmt.Sprintf("指定时间: %+v 发送\n 下次发送时间: %s", t.SpecifyTimeList, nextSendTimeStr) +} + +// 注册后通知 用户注册后开始计时, 消耗指定的时间后通知一次 +type AfterRegistNotice struct { + TimeUnit *TimeUnit `bson:"afterTimeUnit,omitempty"` //单位 分钟/小时/天/周 + TimeCount *int64 `bson:"afterTimeCount,omitempty"` //量 +} + +func (t *AfterRegistNotice) Rule() string { + return fmt.Sprintf("用户注册后 %d %v 发送", t.TimeCount, t.TimeUnit) +} + +type AfterRegistNoticeEx struct { + AfterRegistNotice + CreatedAt time.Time +} + +func (a *AfterRegistNoticeEx) NextSendTimeList(current time.Time, lastSendAt time.Time) TimeSlice { + if a.TimeCount == nil { + return TimeSlice{} + } + interval := a.TimeUnit.ToDuration() * time.Duration(*a.TimeCount) + sendTime := a.CreatedAt.Add(interval) + if current.After(sendTime) && lastSendAt.IsZero() { + return TimeSlice{sendTime} + } + return TimeSlice{} +} + +type MailNotice struct { + SendAt *time.Time `bson:"mailSendAt,omitempty"` //发送时间 +} + +func (m *MailNotice) NextSendTimeList(current time.Time, lastSendAt time.Time) TimeSlice { + if m.SendAt == nil { + return TimeSlice{} + } + if current.After(*m.SendAt) && lastSendAt.IsZero() { + return TimeSlice{*m.SendAt} + } + return TimeSlice{} +} diff --git a/models/v/noticefmtmod/type.go b/models/v/noticefmtmod/type.go new file mode 100644 index 0000000..044c862 --- /dev/null +++ b/models/v/noticefmtmod/type.go @@ -0,0 +1,149 @@ +package noticefmtmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +//用于通知业务:系统消息,活动助手 + +// ObjectID +type ObjectID = primitive.ObjectID + +// Sender 发送者 +type Sender string + +const ( + //System 系统消息 + System Sender = "SYSTEM" + //ActAsst 活动助手 + ActAsst Sender = "ACTASST" + //Reward 打赏 + Reward Sender = "REWARD" +) + +// Receiver 接收者 +type Receiver string + +const ( + //System 所有用户 + AllUser Receiver = "ALL_USER" + //ActAsst 部分用户 + SomeUser Receiver = "SOME_USER" +) + +// TimeUnit 时间单位 +type TimeUnit string + +const ( + Day TimeUnit = "DAY" + Hour TimeUnit = "HOUR" + Minute TimeUnit = "MINUTE" + Second TimeUnit = "SECOND" +) + +func (t *TimeUnit) ToDuration() time.Duration { + switch *t { + case Day: + return time.Hour * 24 + case Hour: + return time.Hour + case Minute: + return time.Minute + case Second: + return time.Second + default: + return 0 + } +} + +// 类型 +type NoticeType string + +const ( + RegularNoticeType NoticeType = "REGULAR_NOTICE_TYPE" //定时通知 + PlanNoticeType NoticeType = "PLAN_NOTICE_TYPE" //计划通知 + SpecifyNoticeType NoticeType = "SPECIFY_NOTICE_TYPE" //规定通知 + AfterRegistNoticeType NoticeType = "AFTER_REGIST_NOTICE" //注册后通知 + MailNoticeType NoticeType = "MAIL_NOTICE_TYPE" //邮件通知 +) + +// 通知格式 +type NoticeFmt struct { + ID ObjectID `bson:"_id,omitempty"` //_id + NoticeCode string `bson:"noticeCode"` //通知码 唯一标记不同的通知计划,可代替_id,方便复制数据 + Sender Sender `bson:"sender"` //发送者 + Receiver Receiver `bson:"receiver"` //接受者 + UIDList UIDSlice `bson:"uidList,omitempty"` //用户列表 + Title string `bson:"title"` //标题 + Content string `bson:"content"` //内容 + Enable bool `bson:"enable"` //消息开关 true:允许通知Receiver,否则不允许通知Receiver + Remark string `bson:"remark"` //备注 + NoticeType NoticeType `bson:"noticeType"` //通知类型 + + RegularNotice `bson:",inline"` //定时通知 在开始时间和结束时间内,根据指定的时间单位周期性通知 + PlanNotice `bson:",inline"` //计划通知 根据指定的开始时间、通知间隔和通知次数发起周期性通知 + SpecifyNotice `bson:",inline"` //规定通知 到达指定的多个时间点时通知 + AfterRegistNotice `bson:",inline"` //注册后通知 用户注册后开始计时, 消耗指定的时间后通知一次 + MailNotice `bson:",inline"` //邮件通知只通知一次 + + UpdatedAt time.Time `bson:"updatedAt"` //文档更新时间 + CreatedAt time.Time `bson:"createdAt"` //文档创建时间 + + //打赏需要 + RewarderName string `bson:"rewarderName,omitempty"` + RewarderPortrait string `bson:"rewarderPortrait,omitempty"` + Coins string `bson:"coins,omitempty"` + VidCover string `bson:"vidCover,omitempty"` + VidObjId primitive.ObjectID `bson:"vidObjId,omitempty"` +} + +type InsertDoc struct { + NoticeCode string `bson:"noticeCode"` //通知码 唯一标记不同的通知计划,可代替_id,方便复制数据 + Sender Sender `bson:"sender"` //发送者 + Receiver Receiver `bson:"receiver"` //接受者 + UIDList UIDSlice `bson:"uidList,omitempty"` //用户列表 + Title string `bson:"title"` //标题 + Content string `bson:"content"` //内容 + Enable bool `bson:"enable"` //消息开关 true:允许通知Receiver,否则不允许通知Receiver + Remark string `bson:"remark"` //备注 + NoticeType NoticeType `bson:"noticeType"` //通知类型 + + RegularNotice `bson:",inline"` //定时通知 在开始时间和结束时间内,根据指定的时间单位周期性通知 + PlanNotice `bson:",inline"` //计划通知 根据指定的开始时间、通知间隔和通知次数发起周期性通知 + SpecifyNotice `bson:",inline"` //规定通知 到达指定的多个时间点时通知 + AfterRegistNotice `bson:",inline"` //注册后通知 用户注册后开始计时, 消耗指定的时间后通知一次 + MailNotice `bson:",inline"` //邮件通知只通知一次 + + //打赏需要 + RewarderName string `bson:"rewarderName,omitempty"` + RewarderPortrait string `bson:"rewarderPortrait,omitempty"` + Coins string `bson:"coins,omitempty"` + VidCover string `bson:"vidCover,omitempty"` + VidObjId primitive.ObjectID `bson:"vidObjId,omitempty"` +} + +type UpdateDoc struct { + Sender *Sender `bson:"sender"` //发送者 + Receiver *Receiver `bson:"receiver"` //接受者 + UIDList UIDSlice `bson:"uidList,omitempty"` //用户列表 + Title *string `bson:"title"` //标题 + Content *string `bson:"content"` //内容 + Enable *bool `bson:"enable"` //消息开关 true:允许通知Receiver,否则不允许通知Receiver + Remark *string `bson:"remark"` //备注 + NoticeType *NoticeType `bson:"noticeType"` //通知类型 + + RegularNotice `bson:",inline"` //定时通知 在开始时间和结束时间内,根据指定的时间单位周期性通知 + PlanNotice `bson:",inline"` //计划通知 根据指定的开始时间、通知间隔和通知次数发起周期性通知 + SpecifyNotice `bson:",inline"` //规定通知 到达指定的多个时间点时通知 + AfterRegistNotice `bson:",inline"` //注册后通知 用户注册后开始计时, 消耗指定的时间后通知一次 + MailNotice `bson:",inline"` //邮件通知 只通知一次 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/noticerecdmod/list.go b/models/v/noticerecdmod/list.go new file mode 100644 index 0000000..6d3b502 --- /dev/null +++ b/models/v/noticerecdmod/list.go @@ -0,0 +1,46 @@ +package noticerecdmod + +import ( + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type RecordSlice []NoticeRecord + +func (r RecordSlice) Len() int { + return len(r) +} + +func (r RecordSlice) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + +func (r RecordSlice) Less(i, j int) bool { + return r[i].LastSendAt.Before(r[j].LastSendAt) +} + +type Matcher = pageopt.Matcher + +type UIDMatch = pageopt.UIDMatch + +type NoticeCodeInMatch struct { + NoticeCodes []string +} + +func (u *NoticeCodeInMatch) New() Matcher { + return pageopt.NewInMatch("noticeCode", u.NoticeCodes) +} + +func List(sort bson.D, skip, limit int64, matchs ...Matcher) (RecordSlice, error) { + filter := pageopt.MergeM(matchs) + opt := (&options.FindOptions{}) + if len(sort) != 0 { + opt.SetSort(sort) + } + opt.SetSkip(skip) + opt.SetLimit(limit) + list := make(RecordSlice, 0, limit) + return list, coll(nil).Find(&list, filter, opt) +} diff --git a/models/v/noticerecdmod/noticeRecord.go b/models/v/noticerecdmod/noticeRecord.go new file mode 100644 index 0000000..b9209d5 --- /dev/null +++ b/models/v/noticerecdmod/noticeRecord.go @@ -0,0 +1,144 @@ +package noticerecdmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.NoticeRecd + +type M = bson.M + +var mdb *db.MongoDB + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "noticeCode", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } + +} + +func GetRecordSlice(uid uint64, noticeCodeList []string) (RecordSlice, error) { + if len(noticeCodeList) == 0 { + return RecordSlice{}, nil + } + filter := M{ + "uid": uid, + "noticeCode": M{"$in": noticeCodeList}, + } + list := make(RecordSlice, 0) + return list, coll(nil).Find(&list, filter) +} + +func GetLastSendAtMap(uid uint64, noticeCodeList []string) (map[string]time.Time, error) { + if len(noticeCodeList) == 0 { + return make(map[string]time.Time), nil + } + list, err := GetRecordSlice(uid, noticeCodeList) + if err != nil { + return nil, err + } + m := make(map[string]time.Time) + for _, v := range list { + m[v.NoticeCode] = v.LastSendAt + } + return m, nil +} + +func UpsertLastSendAt(uid uint64, lastSendAtMap map[string]time.Time) error { + now := time.Now() + writes := make([]mongo.WriteModel, len(lastSendAtMap)) + i := 0 + for noticeCode, lastSendAt := range lastSendAtMap { + writes[i] = mongo.NewUpdateOneModel().SetFilter(bson.M{ + "uid": uid, + "noticeCode": noticeCode, + "lastSendAt": M{"$lt": lastSendAt}, + }).SetUpdate(bson.M{ + "$setOnInsert": M{ + "uid": uid, + "noticeCode": noticeCode, + "lastReadAt": time.Time{}, + "createdAt": now, + }, + "$set": M{ + "lastSendAt": lastSendAt, + "updatedAt": now, + }, + }).SetUpsert(true) + i++ + } + if len(writes) == 0 { + return nil + } + opt := (&options.BulkWriteOptions{}).SetOrdered(false) //设为无序,触发并行写,提升写效率 + if _, err := coll(nil).Bulk(writes, opt); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpsertLastSendAt", table, "Bulk", err), + log.Any("uid", uid), + log.Any("lastSendAtMap", lastSendAtMap), + ) + return err + } + return nil +} + +func UpdateReadTime(readAt time.Time, uid uint64, noticeCodeList []string) error { + if len(noticeCodeList) == 0 { + return nil + } + filter := M{ + "uid": uid, + "noticeCode": M{"$in": noticeCodeList}, + "lastReadAt": M{"$lt": readAt}, + } + update := M{ + "$set": M{ + "lastReadAt": readAt, + "updatedAt": time.Now(), + }, + } + _, err := coll(nil).UpdateMany(filter, update) + return err +} + +func GetLastReadAtMap(uid uint64, noticeCodeList []string) (map[string]time.Time, error) { + if len(noticeCodeList) == 0 { + return make(map[string]time.Time), nil + } + list, err := GetRecordSlice(uid, noticeCodeList) + if err != nil { + return nil, err + } + m := make(map[string]time.Time) + for _, v := range list { + m[v.NoticeCode] = v.LastReadAt + } + return m, nil +} diff --git a/models/v/noticerecdmod/trend.go b/models/v/noticerecdmod/trend.go new file mode 100644 index 0000000..55908e3 --- /dev/null +++ b/models/v/noticerecdmod/trend.go @@ -0,0 +1,42 @@ +package noticerecdmod + +import "time" + +// 动态类型 +type TrendType string + +const ( + Fans TrendType = "fans" //粉丝 + Like TrendType = "like" //点赞 + Cmet TrendType = "cmet" //评论 + Itte TrendType = "itte" //互动 +) + +// UpdateTrendReadTime 更新动态的阅读时间 +func UpdateTrendReadTime(trend TrendType, uid uint64, readAt time.Time) error { + filter := M{ + "uid": uid, + "noticeCode": trend, + } + noticeRecord := NoticeRecord{} + if err := coll(nil).FindOne(¬iceRecord, filter); err != nil { + return err + } + if noticeRecord.LastReadAt.After(readAt) { + return nil + } + filter["lastReadAt"] = M{"$lt": readAt} + update := M{ + "$setOnInsert": M{ + "uid": uid, + "noticeCode": trend, + "createdAt": time.Now(), + }, + "$set": M{ + "lastReadAt": readAt, + "updatedAt": time.Now(), + }, + } + _, err := coll(nil).UpsertOne(filter, update) + return err +} diff --git a/models/v/noticerecdmod/type.go b/models/v/noticerecdmod/type.go new file mode 100644 index 0000000..62b7525 --- /dev/null +++ b/models/v/noticerecdmod/type.go @@ -0,0 +1,31 @@ +package noticerecdmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ObjectID +type ObjectID = primitive.ObjectID + +type D = bson.D + +// 通知记录 +type NoticeRecord struct { + ID ObjectID `bson:"_id,omitempty"` + UID uint64 `bson:"uid"` + NoticeCode string `bson:"noticeCode"` //通知码 唯一标记通知内容和计划 + LastSendAt time.Time `bson:"lastSendAt"` //发送消息的最后时间 + LastReadAt time.Time `bson:"lastReadAt"` //发送消息的最后时间 + UpdatedAt time.Time `bson:"updatedAt"` //文档更新时间 + CreatedAt time.Time `bson:"createdAt"` //文档创建时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/officialWebsitemod/album.go b/models/v/officialWebsitemod/album.go new file mode 100644 index 0000000..062002b --- /dev/null +++ b/models/v/officialWebsitemod/album.go @@ -0,0 +1,58 @@ +package officialWebsitemod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// Album 创作者专辑 +// 表: official_website_album +type Album struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + HeroID primitive.ObjectID `json:"heroID" bson:"heroID"` //创作者ID + Title string `json:"title" bson:"title"` + SeoSlug string `json:"seoSlug" bson:"seoSlug,omitempty"` // SEO 语义化唯一标识(纯小写字母) + Description string `json:"description" bson:"description"` + Cover string `json:"cover" bson:"cover"` + IsHot bool `json:"isHot" bson:"isHot"` + Tags []Tag `json:"tags" bson:"tags"` + SortModel `bson:",inline"` + BaseModel `bson:",inline"` +} + +func (a *Album) tableName() string { return tableAlbum } + +func (a *Album) FindOne(filter M) error { + return findOneByTable(a.tableName(), a, filter) +} + +func (a *Album) FindMany(filter M, opts ...*options.FindOptions) ([]Album, error) { + out := make([]Album, 0) + err := findManyByTable(a.tableName(), &out, filter, opts...) + return out, err +} + +func (a *Album) Count(filter M) (int64, error) { + return countByTable(a.tableName(), filter) +} + +func (a *Album) Create() error { + a.CreatedAt = time.Now().UTC() + a.UpdatedAt = a.CreatedAt + id, err := createByTable(a.tableName(), a) + if err != nil { + return err + } + a.ID = id + return nil +} + +func (a Album) Update(filter M, update M) (int64, error) { + return updateByTable(a.tableName(), filter, update) +} + +func (a Album) Delete(filter M) (int64, error) { + return deleteByTable(a.tableName(), filter) +} diff --git a/models/v/officialWebsitemod/basic_data.go b/models/v/officialWebsitemod/basic_data.go new file mode 100644 index 0000000..d014574 --- /dev/null +++ b/models/v/officialWebsitemod/basic_data.go @@ -0,0 +1,70 @@ +package officialWebsitemod + +import ( + "91porn-server/common/stderr" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// BasicData 官方网站公用数据结构,包括基础静态数据和CMS动态数据 +// 表: official_website_basic_data +type BasicData struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` + Description string `json:"description" bson:"description"` + + // 轮播图和创作者轮博分开 + Banner []Banner `json:"banner" bson:"banner"` + HeroBanner []HeroBanner `json:"heroBanner" bson:"heroBanner"` + BannersDescription string `json:"bannersDescription" bson:"bannersDescription"` + BannersDuration int `json:"bannersDuration" bson:"bannersDuration"` + Masterpiece []Masterpiece `json:"masterpiece" bson:"masterpiece"` + Business []Business `json:"business" bson:"business"` + AboutUs AboutUs `json:"aboutUs" bson:"aboutUs"` + FAQ []FAQ `json:"faq" bson:"faq"` + HomePageCMS []CMSData `json:"homePageCMS" bson:"homePageCMS"` + HomePageCMSDescription string `json:"homePageCMSDescription" bson:"homePageCMSDescription"` + RecruitCMS []CMSData `json:"recruitCMS" bson:"recruitCMS"` + Tags []Tag `json:"tags" bson:"tags"` // 标签列表 + + BaseModel `bson:",inline"` +} + +func (b *BasicData) tableName() string { + return tableBasicData +} + +func (b *BasicData) FindOne() error { + if b == nil { + return stderr.ErrDataInvalid + } + return findOneByTable(b.tableName(), b, nil) +} + +func (b *BasicData) Create() error { + if b == nil { + return stderr.ErrDataInvalid + } + oldOne := &BasicData{} + if err := oldOne.FindOne(); err == nil && oldOne.ID != primitive.NilObjectID { + return stderr.OfficialWebsiteBasicDataExist + } + + b.CreatedAt = time.Now().UTC() + b.UpdatedAt = b.CreatedAt + id, err := createByTable(b.tableName(), b) + if err != nil { + return err + } + b.ID = id + return nil +} + +func (b *BasicData) Update(filter M, update M) (int64, error) { + return updateByTable(b.tableName(), filter, update) +} + +func (b *BasicData) Delete(filter M) (int64, error) { + return deleteByTable(b.tableName(), filter) +} diff --git a/models/v/officialWebsitemod/business.go b/models/v/officialWebsitemod/business.go new file mode 100644 index 0000000..af4cab3 --- /dev/null +++ b/models/v/officialWebsitemod/business.go @@ -0,0 +1,54 @@ +package officialWebsitemod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type BusinessData struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` + Description string `json:"description" bson:"description"` + Type string `json:"type" bson:"type"` // 业务类型,如"adPlacement", "contentProduction", "subRevenue" + ServiceOverview string `json:"serviceOverview" bson:"serviceOverview"` + CollaborationProcess string `json:"collaborationProcess" bson:"collaborationProcess"` + CaseStudies string `json:"caseStudies" bson:"caseStudies"` + BaseModel `bson:",inline"` +} + +func (b *BusinessData) tableName() string { return tableBusiness } + +func (b *BusinessData) FindOne(filter M) error { + return findOneByTable(b.tableName(), &b, filter) +} + +func (b *BusinessData) FindMany(filter M, opts ...*options.FindOptions) ([]BusinessData, error) { + out := make([]BusinessData, 0) + err := findManyByTable(b.tableName(), &out, filter, opts...) + return out, err +} + +func (b *BusinessData) Count(filter M) (int64, error) { + return countByTable(b.tableName(), filter) +} + +func (b BusinessData) Create() error { + b.CreatedAt = time.Now().UTC() + b.UpdatedAt = b.CreatedAt + id, err := createByTable(b.tableName(), b) + if err != nil { + return err + } + b.ID = id + return nil +} + +func (b *BusinessData) Update(filter M, update M) (int64, error) { + return updateByTable(b.tableName(), filter, update) +} + +func (b *BusinessData) Delete(filter M) (int64, error) { + return deleteByTable(b.tableName(), filter) +} diff --git a/models/v/officialWebsitemod/common_methods.go b/models/v/officialWebsitemod/common_methods.go new file mode 100644 index 0000000..d7686a3 --- /dev/null +++ b/models/v/officialWebsitemod/common_methods.go @@ -0,0 +1,125 @@ +package officialWebsitemod + +import ( + "errors" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func notDeletedFilter() M { + return M{ + "$or": bson.A{ + M{"deletedAt": M{"$exists": false}}, + M{"deletedAt": nil}, + M{"deletedAt": time.Time{}}, + }, + } +} + +func withNotDeletedFilter(filter M) M { + if filter == nil { + return notDeletedFilter() + } else { + copied := M{} + for k, v := range filter { + copied[k] = v + } + filter = copied + } + + if _, ok := filter["deletedAt"]; ok { + return filter + } + + return M{ + "$and": bson.A{ + filter, + notDeletedFilter(), + }, + } +} + +func findOneByTable(table string, out interface{}, filter M) error { + filter = withNotDeletedFilter(filter) + return coll(nil, table).FindOne(out, filter) +} + +func findManyByTable(table string, out interface{}, filter M, opts ...*options.FindOptions) error { + filter = withNotDeletedFilter(filter) + return coll(nil, table).Find(out, filter, opts...) +} + +func countByTable(table string, filter M) (int64, error) { + filter = withNotDeletedFilter(filter) + return coll(nil, table).Count(filter) +} + +func createByTable(table string, doc interface{}) (primitive.ObjectID, error) { + res, err := coll(nil, table).InsertOne(doc) + if err != nil { + return primitive.NilObjectID, err + } + id, ok := res.InsertedID.(primitive.ObjectID) + if !ok { + return primitive.NilObjectID, errors.New("inserted id is not ObjectID") + } + return id, nil +} + +func createManyByTable(table string, docs []Video) ([]primitive.ObjectID, error) { + insertIds, err := coll(nil, table).InsertMany(docs) + if err != nil { + return nil, err + } + var ids []primitive.ObjectID + for _, v := range insertIds.InsertedIDs { + id, ok := v.(primitive.ObjectID) + if !ok { + return nil, errors.New("inserted id is not ObjectID") + } + ids = append(ids, id) + } + return ids, nil +} + +func updateByTable(table string, filter M, update M) (int64, error) { + if len(filter) == 0 { + return 0, errors.New("update filter is empty") + } + if len(update) == 0 { + return 0, errors.New("update doc is empty") + } + res, err := coll(nil, table).UpdateOne(filter, update) + if err != nil { + return 0, err + } + return res.ModifiedCount, nil +} + +func updateManyByTable(table string, filter M, update M) (int64, error) { + if len(filter) == 0 { + return 0, errors.New("update filter is empty") + } + if len(update) == 0 { + return 0, errors.New("update doc is empty") + } + res, err := coll(nil, table).UpdateMany(filter, update) + if err != nil { + return 0, err + } + return res.ModifiedCount, nil +} + +func deleteByTable(table string, filter M) (int64, error) { + if len(filter) == 0 { + return 0, errors.New("delete filter is empty") + } + res, err := coll(nil, table).DeleteMany(filter) + if err != nil { + return 0, err + } + return res.DeletedCount, nil +} diff --git a/models/v/officialWebsitemod/common_types.go b/models/v/officialWebsitemod/common_types.go new file mode 100644 index 0000000..365de0d --- /dev/null +++ b/models/v/officialWebsitemod/common_types.go @@ -0,0 +1,101 @@ +package officialWebsitemod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Banner struct { + Name string `json:"name" bson:"name"` + Description string `json:"description" bson:"description"` + Image string `json:"image" bson:"image"` + PhoneImage string `json:"phoneImage" bson:"phoneImage"` + Thumbnail string `json:"thumbnail" bson:"thumbnail"` + URL string `json:"url" bson:"url"` +} + +type HeroBanner struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + HeroID primitive.ObjectID `json:"heroId" bson:"heroId"` + Name string `json:"name" bson:"name"` + Description string `json:"description" bson:"description"` + Image string `json:"image" bson:"image"` + URL string `json:"url" bson:"url"` + Duration int `json:"duration" bson:"duration"` +} + +type Masterpiece struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Description string `json:"description" bson:"description"` + Cover string `json:"cover" bson:"cover"` + Type string `json:"type" bson:"type"` // 作品类型,如"video", "article", "album" +} + +type Business struct { + Name string `json:"name" bson:"name"` + EnglishName string `json:"englishName" bson:"englishName"` + Description string `json:"description" bson:"description"` + Cover string `json:"cover" bson:"cover"` + Detail []BusinessDetail `json:"detail" bson:"detail"` +} + +type BusinessDetail struct { + Cover string `json:"cover" bson:"cover"` + Description string `json:"description" bson:"description"` +} + +type AboutUs struct { + Description string `json:"description" bson:"description"` + Companies []CompanyInfo `json:"companies" bson:"companies"` + Values string `json:"values" bson:"values"` + ValuesImg string `json:"valuesImg" bson:"valuesImg"` +} + +type CompanyInfo struct { + Name string `json:"name" bson:"name"` + Address string `json:"address" bson:"address"` + Description string `json:"description" bson:"description"` + Contact []ContactInfo `json:"contact" bson:"contact"` +} + +type Source struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + IsActive bool `json:"isActive" bson:"isActive"` + Type string `json:"type" bson:"type"` + Domain Domain `json:"domain" bson:"domain"` +} + +type Domain struct { + Weight int `json:"weight" bson:"weight"` + Url string `json:"url" bson:"url"` + Desc string `json:"desc" bson:"desc"` + Status int `json:"status" bson:"status"` +} + +type FAQ struct { + Question string `json:"question" bson:"question"` + Answer string `json:"answer" bson:"answer"` +} + +type CMSData struct { + Description string `json:"description" bson:"description"` + Value string `json:"value" bson:"value"` +} + +type ContactInfo struct { + Type string `json:"type" bson:"type"` + Value string `json:"value" bson:"value"` +} + +type SortModel struct { + Sort int64 `json:"sort" bson:"sort"` + IsActive bool `json:"isActive" bson:"isActive"` +} + +type BaseModel struct { + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + DeletedAt *time.Time `json:"deletedAt" bson:"deletedAt"` +} diff --git a/models/v/officialWebsitemod/hero.go b/models/v/officialWebsitemod/hero.go new file mode 100644 index 0000000..9167ea7 --- /dev/null +++ b/models/v/officialWebsitemod/hero.go @@ -0,0 +1,62 @@ +package officialWebsitemod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// Hero 创作者 +// 表: official_website_hero +type Hero struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + SeoSlug string `json:"seoSlug" bson:"seoSlug,omitempty"` // SEO 语义化唯一标识(纯小写字母) + Avatar string `json:"avatar" bson:"avatar"` + Height int64 `json:"height" bson:"height"` + Bust int64 `json:"bust" bson:"bust"` + Waist int64 `json:"waist" bson:"waist"` + Hip int64 `json:"hip" bson:"hip"` + Description string `json:"description" bson:"description"` + FansCount int64 `json:"fansCount" bson:"fansCount"` + Cover string `json:"cover" bson:"cover"` + Partners []primitive.ObjectID `json:"partners" bson:"partners"` //合作伙伴ID列表 + SortModel `bson:",inline"` + BaseModel `bson:",inline"` +} + +func (h *Hero) tableName() string { return tableHero } + +func (h *Hero) FindOne(filter M) error { + return findOneByTable(h.tableName(), &h, filter) +} + +func (h *Hero) FindMany(filter M, opts ...*options.FindOptions) ([]Hero, error) { + out := make([]Hero, 0) + err := findManyByTable(h.tableName(), &out, filter, opts...) + return out, err +} + +func (h *Hero) Count(filter M) (int64, error) { + return countByTable(h.tableName(), filter) +} + +func (h *Hero) Create() error { + h.CreatedAt = time.Now().UTC() + h.UpdatedAt = h.CreatedAt + id, err := createByTable(h.tableName(), h) + if err != nil { + return err + } + h.ID = id + return nil +} + +func (h *Hero) Update(filter M, update M) (int64, error) { + return updateByTable(h.tableName(), filter, update) +} + +func (h *Hero) Delete(filter M) (int64, error) { + return deleteByTable(h.tableName(), filter) +} diff --git a/models/v/officialWebsitemod/init.go b/models/v/officialWebsitemod/init.go new file mode 100644 index 0000000..a46d3ac --- /dev/null +++ b/models/v/officialWebsitemod/init.go @@ -0,0 +1,57 @@ +package officialWebsitemod + +import ( + "fmt" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type M = bson.M + +var mdb *db.MongoDB + +const ( + tableBasicData = "official_website_basic_data" + tableHero = "official_website_hero" + tableAlbum = "official_website_album" + tableVideo = "official_website_video" + tableTag = "official_website_tag" + tableRecruitForm = "official_website_recruit_form" + tableJobList = "official_website_job" + tableNews = "official_website_news" + tableNewsDetail = "official_website_news_detail" + tablePartner = "official_website_partner" + tableBusiness = "official_website_business" + tablePhotograph = "official_website_photograph" +) + +func coll(t *db.MongoTool, tableName string) *db.MongoTool { + if t == nil { + return mdb.Coll(tableName) + } + return t.Coll(tableName) +} + +func Init() { + mdb = db.Init(tableBasicData) + initIndex() +} + +func initIndex() { + // 需要 seoSlug 唯一标识的表:稀疏唯一索引(空值不参与唯一约束,配合 bson omitempty) + seoSlugTables := []string{tableHero, tableAlbum, tableVideo, tablePhotograph, tableNews} + for _, table := range seoSlugTables { + if _, err := coll(nil, table).CreateIndex([]mongo.IndexModel{ + { + Keys: bson.D{{Key: "seoSlug", Value: 1}}, + Options: options.Index().SetUnique(true).SetSparse(true), + }, + }); err != nil { + panic(fmt.Sprintf("officialWebsite seoSlug index err [%s] ==> [%+v]", table, err)) + } + } +} diff --git a/models/v/officialWebsitemod/job.go b/models/v/officialWebsitemod/job.go new file mode 100644 index 0000000..f22a0a1 --- /dev/null +++ b/models/v/officialWebsitemod/job.go @@ -0,0 +1,56 @@ +package officialWebsitemod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// JobList 招聘页岗位列表 +// 表: official_website_job_list +type Job struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` + Description string `json:"description" bson:"description"` + Requirements string `json:"requirements" bson:"requirements"` + Benefits string `json:"benefits" bson:"benefits"` + JDUrl string `json:"jdUrl" bson:"jdUrl"` + SortModel `bson:",inline"` + BaseModel `bson:",inline"` +} + +func (j *Job) tableName() string { return tableJobList } + +func (j *Job) FindOne(filter M) error { + return findOneByTable(j.tableName(), &j, filter) +} + +func (j *Job) FindMany(filter M, opts ...*options.FindOptions) ([]Job, error) { + out := make([]Job, 0) + err := findManyByTable(j.tableName(), &out, filter, opts...) + return out, err +} + +func (j *Job) Count(filter M) (int64, error) { + return countByTable(j.tableName(), filter) +} + +func (j *Job) Create() error { + j.CreatedAt = time.Now().UTC() + j.UpdatedAt = j.CreatedAt + id, err := createByTable(j.tableName(), j) + if err != nil { + return err + } + j.ID = id + return nil +} + +func (j *Job) Update(filter M, update M) (int64, error) { + return updateByTable(j.tableName(), filter, update) +} + +func (j *Job) Delete(filter M) (int64, error) { + return deleteByTable(j.tableName(), filter) +} diff --git a/models/v/officialWebsitemod/news.go b/models/v/officialWebsitemod/news.go new file mode 100644 index 0000000..493e939 --- /dev/null +++ b/models/v/officialWebsitemod/news.go @@ -0,0 +1,58 @@ +package officialWebsitemod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// News 资讯 +// 表: official_website_news +type News struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` + SeoSlug string `json:"seoSlug" bson:"seoSlug,omitempty"` // SEO 语义化唯一标识(纯小写字母) + Description string `json:"description" bson:"description"` + Cover string `json:"cover" bson:"cover"` + Url string `json:"url" bson:"url"` + Tags []Tag `json:"tags" bson:"tags"` + Detail string `json:"detail" bson:"detail"` + SortModel `bson:",inline"` + BaseModel `bson:",inline"` +} + +func (n *News) tableName() string { return tableNews } + +func (n *News) FindOne(filter M) error { + return findOneByTable(n.tableName(), &n, filter) +} + +func (n *News) FindMany(filter M, opts ...*options.FindOptions) ([]News, error) { + out := make([]News, 0) + err := findManyByTable(n.tableName(), &out, filter, opts...) + return out, err +} + +func (n *News) Count(filter M) (int64, error) { + return countByTable(n.tableName(), filter) +} + +func (n *News) Create() error { + n.CreatedAt = time.Now().UTC() + n.UpdatedAt = n.CreatedAt + id, err := createByTable(n.tableName(), n) + if err != nil { + return err + } + n.ID = id + return nil +} + +func (n *News) Update(filter M, update M) (int64, error) { + return updateByTable(n.tableName(), filter, update) +} + +func (n *News) Delete(filter M) (int64, error) { + return deleteByTable(n.tableName(), filter) +} diff --git a/models/v/officialWebsitemod/partner.go b/models/v/officialWebsitemod/partner.go new file mode 100644 index 0000000..44007b4 --- /dev/null +++ b/models/v/officialWebsitemod/partner.go @@ -0,0 +1,53 @@ +package officialWebsitemod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Partner struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Description string `json:"description" bson:"description"` + Logo string `json:"logo" bson:"logo"` + Url string `json:"url" bson:"url"` + SortModel `bson:",inline"` + BaseModel `bson:",inline"` +} + +func (p *Partner) tableName() string { return tablePartner } + +func (p *Partner) FindOne(filter M) error { + return findOneByTable(p.tableName(), &p, filter) +} + +func (p *Partner) FindMany(filter M, opts ...*options.FindOptions) ([]Partner, error) { + out := make([]Partner, 0) + err := findManyByTable(p.tableName(), &out, filter, opts...) + return out, err +} + +func (p *Partner) Count(filter M) (int64, error) { + return countByTable(p.tableName(), filter) +} + +func (p *Partner) Create() error { + p.CreatedAt = time.Now().UTC() + p.UpdatedAt = p.CreatedAt + id, err := createByTable(p.tableName(), p) + if err != nil { + return err + } + p.ID = id + return nil +} + +func (p *Partner) Update(filter M, update M) (int64, error) { + return updateByTable(p.tableName(), filter, update) +} + +func (p *Partner) Delete(filter M) (int64, error) { + return deleteByTable(p.tableName(), filter) +} diff --git a/models/v/officialWebsitemod/photograph.go b/models/v/officialWebsitemod/photograph.go new file mode 100644 index 0000000..97b3fe5 --- /dev/null +++ b/models/v/officialWebsitemod/photograph.go @@ -0,0 +1,58 @@ +package officialWebsitemod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Photograph struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + HeroID primitive.ObjectID `json:"heroId" bson:"heroId"` + Name string `json:"name" bson:"name"` + SeoSlug string `json:"seoSlug" bson:"seoSlug,omitempty"` // SEO 语义化唯一标识(纯小写字母) + Description string `json:"description" bson:"description"` + Cover string `json:"cover" bson:"cover"` + Photos []string `json:"photos" bson:"photos"` + Tags []Tag `json:"tags" bson:"tags"` + SortModel `bson:",inline"` + BaseModel `bson:",inline"` +} + +func (p *Photograph) tableName() string { + return tablePhotograph +} + +func (p *Photograph) FindOne(filter M) error { + return findOneByTable(p.tableName(), p, filter) +} + +func (p *Photograph) FindMany(filter M, opts ...*options.FindOptions) ([]Photograph, error) { + out := make([]Photograph, 0) + err := findManyByTable(p.tableName(), &out, filter, opts...) + return out, err +} + +func (p *Photograph) Count(filter M) (int64, error) { + return countByTable(p.tableName(), filter) +} + +func (p *Photograph) Create() error { + p.CreatedAt = time.Now().UTC() + p.UpdatedAt = p.CreatedAt + id, err := createByTable(p.tableName(), p) + if err != nil { + return err + } + p.ID = id + return nil +} + +func (p *Photograph) Update(filter M, update M) (int64, error) { + return updateByTable(p.tableName(), filter, update) +} + +func (p *Photograph) Delete(filter M) (int64, error) { + return deleteByTable(p.tableName(), filter) +} diff --git a/models/v/officialWebsitemod/recruit_form.go b/models/v/officialWebsitemod/recruit_form.go new file mode 100644 index 0000000..2e1f666 --- /dev/null +++ b/models/v/officialWebsitemod/recruit_form.go @@ -0,0 +1,57 @@ +package officialWebsitemod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// RecruitForm 招募页面表单 +// 表: official_website_recruit_form +type RecruitForm struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Sex int `json:"sex" bson:"sex"` + Age int `json:"age" bson:"age"` + Country string `json:"country" bson:"country"` + Address string `json:"address" bson:"address"` + Contact []ContactInfo `json:"contact" bson:"contact"` + Description string `json:"description" bson:"description"` + BaseModel `bson:",inline"` +} + +func (r *RecruitForm) tableName() string { return tableRecruitForm } + +func (r *RecruitForm) FindOne(filter M) error { + return findOneByTable(r.tableName(), &r, filter) +} + +func (r *RecruitForm) FindMany(filter M, opts ...*options.FindOptions) ([]RecruitForm, error) { + out := make([]RecruitForm, 0) + err := findManyByTable(r.tableName(), &out, filter, opts...) + return out, err +} + +func (r *RecruitForm) Count(filter M) (int64, error) { + return countByTable(r.tableName(), filter) +} + +func (r *RecruitForm) Create() error { + r.CreatedAt = time.Now().UTC() + r.UpdatedAt = r.CreatedAt + id, err := createByTable(r.tableName(), r) + if err != nil { + return err + } + r.ID = id + return nil +} + +func (r *RecruitForm) Update(filter M, update M) (int64, error) { + return updateByTable(r.tableName(), filter, update) +} + +func (r *RecruitForm) Delete(filter M) (int64, error) { + return deleteByTable(r.tableName(), filter) +} diff --git a/models/v/officialWebsitemod/tag.go b/models/v/officialWebsitemod/tag.go new file mode 100644 index 0000000..df61b15 --- /dev/null +++ b/models/v/officialWebsitemod/tag.go @@ -0,0 +1,60 @@ +package officialWebsitemod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const ( + OfficialWebsiteTagTypeVideo = "video" + OfficialWebsiteTagTypeHero = "hero" + OfficialWebsiteTagTypeAlbum = "album" +) + +// Tag 标签 +// 表: official_website_tag +type Tag struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Type string `json:"type" bson:"type"` + IsHot bool `json:"isHot" bson:"isHot"` + SortModel `bson:",inline"` + BaseModel `bson:",inline"` +} + +func (t *Tag) tableName() string { return tableTag } + +func (t *Tag) FindOne(filter M) error { + return findOneByTable(t.tableName(), &t, filter) +} + +func (t *Tag) FindMany(filter M, opts ...*options.FindOptions) ([]Tag, error) { + out := make([]Tag, 0) + err := findManyByTable(t.tableName(), &out, filter, opts...) + return out, err +} + +func (t *Tag) Count(filter M) (int64, error) { + return countByTable(t.tableName(), filter) +} + +func (t *Tag) Create() error { + t.CreatedAt = time.Now().UTC() + t.UpdatedAt = t.CreatedAt + id, err := createByTable(t.tableName(), t) + if err != nil { + return err + } + t.ID = id + return nil +} + +func (t *Tag) Update(filter M, update M) (int64, error) { + return updateByTable(t.tableName(), filter, update) +} + +func (t *Tag) Delete(filter M) (int64, error) { + return deleteByTable(t.tableName(), filter) +} diff --git a/models/v/officialWebsitemod/video.go b/models/v/officialWebsitemod/video.go new file mode 100644 index 0000000..16b1740 --- /dev/null +++ b/models/v/officialWebsitemod/video.go @@ -0,0 +1,76 @@ +package officialWebsitemod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// Video 创作者视频 +// 表: official_website_video +type Video struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + AlbumID primitive.ObjectID `json:"albumId" bson:"albumId"` + HeroID primitive.ObjectID `json:"heroId" bson:"heroId"` //创作者ID + Title string `json:"title" bson:"title"` + SeoSlug string `json:"seoSlug" bson:"seoSlug,omitempty"` // SEO 语义化唯一标识(纯小写字母) + Description string `json:"description" bson:"description"` + Cover string `json:"cover" bson:"cover"` + Url string `json:"url" bson:"url"` + Tags []Tag `json:"tags" bson:"tags"` + IsHot bool `json:"isHot" bson:"isHot"` + WatchCount int64 `json:"watchCount" bson:"watchCount"` + LikeCount int64 `json:"likeCount" bson:"likeCount"` + CommentCount int64 `json:"commentCount" bson:"commentCount"` + CollectCount int64 `json:"collectCount" bson:"collectCount"` + SortModel `bson:",inline"` + BaseModel `bson:",inline"` +} + +func (v *Video) tableName() string { return tableVideo } + +func (v *Video) FindOne(filter M) error { + return findOneByTable(v.tableName(), &v, filter) +} + +func (v *Video) FindMany(filter M, opts ...*options.FindOptions) ([]Video, error) { + out := make([]Video, 0) + err := findManyByTable(v.tableName(), &out, filter, opts...) + return out, err +} + +func (v *Video) Count(filter M) (int64, error) { + return countByTable(v.tableName(), filter) +} + +func (v *Video) Create() error { + v.CreatedAt = time.Now().UTC() + v.UpdatedAt = v.CreatedAt + id, err := createByTable(v.tableName(), v) + if err != nil { + return err + } + v.ID = id + return nil +} + +func (v *Video) InsertMany(videos []Video) (ids []primitive.ObjectID, err error) { + //var docs []interface{} + //for _, video := range videos { + // docs = append(docs, video) + //} + return createManyByTable(v.tableName(), videos) +} + +func (v *Video) Update(filter M, update M) (int64, error) { + return updateByTable(v.tableName(), filter, update) +} + +func (v *Video) UpdateMany(filter M, update M) (int64, error) { + return updateManyByTable(v.tableName(), filter, update) +} + +func (v *Video) Delete(filter M) (int64, error) { + return deleteByTable(v.tableName(), filter) +} diff --git a/models/v/officialmod/app.go b/models/v/officialmod/app.go new file mode 100644 index 0000000..6e1c128 --- /dev/null +++ b/models/v/officialmod/app.go @@ -0,0 +1,6 @@ +package officialmod + +// QueryCond 查询列表 +type QueryCond struct { + Type string `form:"type" json:"type" binding:"required"` +} diff --git a/models/v/officialmod/official.go b/models/v/officialmod/official.go new file mode 100644 index 0000000..ecfcfbd --- /dev/null +++ b/models/v/officialmod/official.go @@ -0,0 +1,186 @@ +package officialmod + +import ( + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/localcache" + "91porn-server/common/log" + "91porn-server/models" + "encoding/json" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +var mdb *db.MongoDB + +const table = models.OfficialConfig + +// Coll 获取表名 +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{"officialName", 1}}, + }, + { + Keys: bson.D{{"officialType", 1}}, + }, + { + Keys: bson.D{{"updatedAt", 1}}, + }, + { + Keys: bson.D{{"isActive", 1}}, + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } + return +} + +// InsertOne 新增 +func InsertOne(t *OfficialConfig) (data primitive.ObjectID, err error) { + result, err := coll(nil).InsertOne(&t) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "Marshal", err)) + return + } + err = data.UnmarshalJSON(byteID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "UnmarshalJSON", err)) + return + } + return +} + +// QueryAll 查询配置列表 +func QueryAll(in *QueryAllCond, opts ...*options.FindOptions) ([]*OfficialConfig, error) { + var out []*OfficialConfig = []*OfficialConfig{} + fliter := bson.M{} + if in.Status != nil { + fliter["isActive"] = in.Status + } + if in.OfficialType != "" { + fliter["officialType"] = in.OfficialType + } + if err := coll(nil).Find(&out, fliter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAll", models.Activity, "Find", err), + log.Any("filter", fliter), + ) + return nil, err + } + return out, nil +} + +// Count 查询总条数 +func Count(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Count", models.Activity, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// UpdateOneOfficiaByID 编辑标签 +func UpdateOneOfficiaByID(id primitive.ObjectID, doc OfficiaUpdateDoc) (err error) { + docM, err := common.ToBsonM(doc) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateOneOfficiaByID", table, "ToBsonM", err), + log.Any("id", id), + log.Any("doc", doc), + ) + return + } + update := bson.M{} + update["$set"] = docM + _, err = coll(nil).UpdateOne(bson.M{"_id": id}, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateOneOfficiaByID", table, "UpdateOne", err), + log.Any("id", id), + log.Any("doc", doc), + ) + return + } + return +} + +// DeleteOfficias 删除标签 +func DeleteOfficias(ids []primitive.ObjectID) (int64, error) { + if ids == nil { + ids = []primitive.ObjectID{} + } + cond := bson.M{"_id": bson.M{"$in": ids}} + result, err := coll(nil).DeleteMany(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteOfficias", table, "DeleteMany", err), log.Any("ids", ids)) + return 0, err + } + return result.DeletedCount, nil +} + +// FindTagByID 根据id获取标签信息 +func FindOneTagByID(id primitive.ObjectID) (data OfficialConfig, err error) { + err = coll(nil).FindOne(&data, bson.M{"_id": id}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneTagByID", table, "FindOne", err), + log.Any("id", id), + ) + return + } + return +} + +// 查询配置列表,app使用 +func Query(officialType string) ([]*OfficialConfigApp, error) { + var out []*OfficialConfigApp = []*OfficialConfigApp{} + fliter := bson.M{"officialType": officialType, "isActive": true} + opts := (&options.FindOptions{}).SetSort(bson.D{{"sort", -1}}) + if err := coll(nil).Find(&out, fliter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Query", models.Activity, "Find", err), + log.Any("filter", fliter), + ) + return nil, err + } + return out, nil +} + +func QueryOfficeList() ([]*OfficialConfigApp, error) { + cfg, ok := localcache.C.Get(GetOfficialList) + if ok { + return cfg.([]*OfficialConfigApp), nil + } + + var out []*OfficialConfigApp + filter := bson.M{"officialType": "1", "isActive": true, "isHomepageAds": true} + opts := (&options.FindOptions{}).SetSort(bson.D{{"sort", -1}}) + if err := coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Query", models.Activity, "QueryOfficeList", err), + log.Any("filter", filter), + ) + return nil, err + } + common.Go(func() { + localcache.C.Set(GetOfficialList, out, 5*time.Minute) + }) + return out, nil +} diff --git a/models/v/officialmod/type.go b/models/v/officialmod/type.go new file mode 100644 index 0000000..47e726b --- /dev/null +++ b/models/v/officialmod/type.go @@ -0,0 +1,64 @@ +package officialmod + +import ( + "91porn-server/common/db" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +// Init 初始化标签model索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +const ( + GetOfficialList = "GetOfficialList" +) + +type PositionType int + +const ( + ZJ = 1 // 装机必备APP + GC = 2 // 国产APP +) + +// OfficialConfig 官方配置 +type OfficialConfig struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 官方id + OfficialName string `json:"officialName" bson:"officialName"` // 官方名字 + OfficialDesc string `json:"officialDesc" bson:"officialDesc"` // 官方描述 + OfficialImg string `json:"officialImg" bson:"officialImg"` // 官方图片 + OfficialUrl string `json:"officialUrl" bson:"officialUrl"` // 官方链接 + Position int `json:"position" bson:"position"` // 官方位置 + OfficialType string `json:"officialType" bson:"officialType"` // 官方类型,1:下载 2:社区 + IsActive bool `json:"isActive" bson:"isActive"` // 开关 + Sort int64 `json:"sort" bson:"sort"` // 排序 + IsHomepageAds bool `json:"isHomepageAds" bson:"isHomepageAds"` // 是否首页广告开关 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 +} + +// OfficiaUpdateDoc 官方更新信息 +type OfficiaUpdateDoc struct { + OfficialName *string `bson:"officialName,omitempty"` // 官方名字 + OfficialDesc *string `bson:"officialDesc,omitempty"` // 官方描述 + OfficialImg *string `bson:"officialImg,omitempty"` // 官方图片 + OfficialUrl *string `bson:"officialUrl,omitempty"` // 官方链接 + CreatedAt time.Time `bson:"createdAt,omitempty"` // 创建时 + UpdatedAt time.Time `bson:"updatedAt,omitempty"` // 更新时间 + IsActive *bool `bson:"isActive,omitempty"` // 是否启用 + Sort *int64 `bson:"sort,omitempty"` // 是否启用 + Position *int `json:"position" bson:"position"` // 官方位置 + IsHomepageAds *bool `bson:"isHomepageAds,omitempty"` // 是否首页广告开关 +} + +// OfficialConfigApp 官方配置app返回实体 +type OfficialConfigApp struct { + OfficialName string `json:"officialName" bson:"officialName"` // 官方名字 + OfficialDesc string `json:"officialDesc" bson:"officialDesc"` // 官方描述 + OfficialImg string `json:"officialImg" bson:"officialImg"` // 官方图片 + OfficialUrl string `json:"officialUrl" bson:"officialUrl"` // 官方链接 + Position int `json:"position" bson:"position"` // 官方位置 + OfficialType string `json:"officialType" bson:"officialType"` // 官方类型,1:下载 2:社区 +} diff --git a/models/v/officialmod/web.go b/models/v/officialmod/web.go new file mode 100644 index 0000000..010542d --- /dev/null +++ b/models/v/officialmod/web.go @@ -0,0 +1,50 @@ +package officialmod + +import ( + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +// QueryAllCond 查询列表 +type QueryAllCond struct { + OfficialType string `form:"officialType" json:"officialType"` + Status *bool `form:"isActive" json:"officialName"` // 状态---启用/禁用 + PageNumber int64 `form:"pageNumber" json:"pageNumber" binding:"required,min=1"` // 当前页 + PageSize int64 `form:"pageSize" json:"pageSize" binding:"required,min=10,max=50"` // 每页条数 +} + +func (q *QueryAllCond) Options() *options.FindOptions { + return options.Find().SetLimit(q.PageSize).SetSkip((q.PageNumber - 1) * q.PageSize).SetSort(bson.M{"updatedAt": -1}).SetSort(bson.M{"sort": -1}) +} + +type WebOfficialDeleteReqInfo struct { + IDs []primitive.ObjectID `form:"id" json:"id" binding:"required"` +} + +type WebOfficialAddReqInfo struct { + OfficialName string `form:"officialName" json:"officialName" binding:"required"` + OfficialDesc string `form:"officialDesc" json:"officialDesc" binding:"omitempty"` + OfficialImg string `form:"officialImg" json:"officialImg" binding:"omitempty"` + OfficialUrl string `form:"officialUrl" json:"officialUrl" binding:"required"` + Position int `json:"position" bson:"position"` // 官方位置 + OfficialType string `form:"officialType" json:"officialType" binding:"required"` + IsActive bool `form:"isActive" json:"isActive" binding:"omitempty"` + Sort int64 `form:"sort" json:"sort" binding:"omitempty"` //排序 + IsHomepageAds bool `form:"isHomepageAds" json:"isHomepageAds" binding:"omitempty"` + CreatedAt time.Time `bson:"createdAt" json:"createdAt" binding:"omitempty"` // 创建时间 + UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt" binding:"omitempty"` // 更新时间 +} + +type WebTagUpdateReqInfo struct { + ID primitive.ObjectID `form:"id" json:"id" binding:"required"` + OfficialName string `form:"officialName" json:"officialName" binding:"omitempty"` + OfficialDesc string `form:"officialDesc" json:"officialDesc" binding:"omitempty"` + OfficialImg string `form:"officialImg" json:"officialImg" binding:"omitempty"` + OfficialUrl string `form:"officialUrl" json:"officialUrl" binding:"omitempty"` + IsActive bool `form:"isActive" json:"isActive" binding:"omitempty"` + Sort int64 `form:"sort" json:"sort" binding:"omitempty"` // 排序 + Position int `form:"position" json:"position" bson:"position"` // 官方位置 + IsHomepageAds bool `form:"isHomepageAds" json:"isHomepageAds" binding:"omitempty"` +} diff --git a/models/v/oncetaskmod/oncetask.go b/models/v/oncetaskmod/oncetask.go new file mode 100644 index 0000000..7997677 --- /dev/null +++ b/models/v/oncetaskmod/oncetask.go @@ -0,0 +1,104 @@ +package oncetaskmod + +import ( + "errors" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/mongo/options" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.OnceTask + +// InitIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "type", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type OnceTaskTypeEnum int64 + +const ( + OnceTaskTypeUserBuyVip OnceTaskTypeEnum = 0 // 购买vip + OnceTaskTypeBindMobile OnceTaskTypeEnum = 1 // 绑定手机号 + Download OnceTaskTypeEnum = 2 // 下载APP + OnceTaskTypeUserBuyCoin OnceTaskTypeEnum = 3 // 购买金币 +) + +type OnceTask struct { + ID primitive.ObjectID `json:"_id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` // 任务标题 + Desc string `json:"desc" bson:"desc"` // 任务描述 + Img string `json:"img" bson:"img"` // 任务图片 + Prizes []primitive.ObjectID `json:"prizes" bson:"prizes"` // 任务奖励 + Type OnceTaskTypeEnum `json:"type" bson:"type"` // 任务类型. 1 邀请用户; 2 绑定手机号 + FinishCondition uint64 `json:"finishCondition" bson:"finishCondition"` // 达成条件 + Link string `json:"link" bson:"link"` // 跳转链接 + Status bool `json:"status" bson:"status"` // 是否启用 + SortNum int `json:"sortNum" bson:"sortNum"` // 排序 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +func GetOnceTaskValid(t *db.MongoTool) ([]OnceTask, error) { + var ot []OnceTask + return ot, coll(t).Find(&ot, bson.M{"status": true}) +} + +func GetOnceTaskAll(t *db.MongoTool) ([]OnceTask, error) { + var ot []OnceTask + op := options.Find().SetSort(bson.D{{"sortNum", 1}}) + return ot, coll(t).Find(&ot, bson.M{}, op) +} + +func GetOnceTaskValidByType(t *db.MongoTool, tType OnceTaskTypeEnum) (*OnceTask, error) { + var ot OnceTask + if err := coll(t).FindOne(&ot, bson.M{"status": true, "type": tType}); err != nil { + return nil, err + } + if ot.ID.IsZero() { + return nil, errors.New("not found") + } + return &ot, nil +} + +func GetTaskByID(t *db.MongoTool, id primitive.ObjectID) (*OnceTask, error) { + var dt OnceTask + if err := coll(t).FindOne(&dt, bson.M{"_id": id}); err != nil { + return nil, err + } + if dt.ID.IsZero() { + return nil, errors.New("not found") + } + if !dt.Status { + return nil, errors.New("task not active") + } + return &dt, nil +} diff --git a/models/v/oncetaskmod/web.go b/models/v/oncetaskmod/web.go new file mode 100644 index 0000000..4934e37 --- /dev/null +++ b/models/v/oncetaskmod/web.go @@ -0,0 +1,43 @@ +package oncetaskmod + +import ( + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AddOnceReq struct { + Title *string `json:"title"` // 任务标题 + Desc *string `json:"desc"` // 任务描述 + Img *string `json:"img"` // 任务图片 + Prizes []primitive.ObjectID `json:"prizes"` // 任务奖励 + Type *OnceTaskTypeEnum `json:"type"` // 任务类型. 0 购买vip; 1 绑定手机号 + FinishCondition *uint64 `json:"finishCondition"` // 达成条件 + Link *string `json:"link"` // 跳转链接 + Status *bool `json:"status"` // 是否启用 + SortNum *int `json:"sortNum"` // 排序 +} + +func AddOnceTask(t *db.MongoTool, ot OnceTask) error { + _, err := coll(t).InsertOne(ot) + return err +} + +type EditOnceReq struct { + ID primitive.ObjectID `json:"_id" binding:"required"` // 被编辑的任务id + Title *string `json:"title"` // 任务标题 + Desc *string `json:"desc"` // 任务描述 + Img *string `json:"img"` // 任务图片 + Prizes []primitive.ObjectID `json:"prizes"` // 任务奖励 + Type *OnceTaskTypeEnum `json:"type"` // 任务类型. 0 购买vip; 1 绑定手机号 + FinishCondition *uint64 `json:"finishCondition"` // 达成条件 + Link *string `json:"link"` // 跳转链接 + Status *bool `json:"status"` // 是否启用 + SortNum *int `json:"sortNum"` // 排序 +} + +func UpdateOnceTask(t *db.MongoTool, id primitive.ObjectID, edt bson.M) error { + _, err := coll(t).UpdateOne(bson.M{"_id": id}, bson.M{"$set": edt}) + return err +} diff --git a/models/v/operationlogmod/operationlog.go b/models/v/operationlogmod/operationlog.go new file mode 100644 index 0000000..d1b6bed --- /dev/null +++ b/models/v/operationlogmod/operationlog.go @@ -0,0 +1,70 @@ +package operationlogmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.OperationLog + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// 添加 +func Insert(l OperationLog) error { + l.CreatedAt = time.Now() + if _, err := coll(nil).InsertOne(l); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return err + } + return nil +} + +func InsertMany(logs []OperationLog) error { + _, err := coll(nil).InsertMany(logs) + return err +} + +// FindMany +func FindManyOperationLog(filter bson.M, opts *options.FindOptions) (data []OperationLog, err error) { + err = coll(nil).Find(&data, filter, opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindManyOperationLog", table, "FindMany", err), log.Any("blockType", filter)) + return + } + return +} + +func Count(query bson.M) (total int64) { + total, err := coll(nil).Count(query) + if err != nil { + log.Error("models operationLog Count error", log.E(err)) + return + } + return +} diff --git a/models/v/operationlogmod/type.go b/models/v/operationlogmod/type.go new file mode 100644 index 0000000..e90555f --- /dev/null +++ b/models/v/operationlogmod/type.go @@ -0,0 +1,28 @@ +package operationlogmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// @table:operation_log +type OperationLog struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + UserID int64 `json:"userID" bson:"userID"` //用户ID + OperationAPI string `json:"operationAPI" bson:"operationAPI"` //操作类型 + OperationType int `json:"operationType" bson:"operationType"` //操作类型 + BeforeContent string `json:"beforeContent" bson:"beforeContent"` //操作前内容 + AfterContent string `json:"afterContent" bson:"afterContent"` //操作后内容 + Reason string `json:"reason" bson:"reason"` //操作原因 + CreatedUser string `json:"createdUser" bson:"createdUser" binding:"required"` //创建人 + CreatedID int64 `json:"createdID" bson:"createdID" binding:"required"` //创建人id + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/operationlogmod/web.go b/models/v/operationlogmod/web.go new file mode 100644 index 0000000..3953024 --- /dev/null +++ b/models/v/operationlogmod/web.go @@ -0,0 +1,17 @@ +package operationlogmod + +import "time" + +// GetOperationLogListReq 获取帖子、用户操作日志列表 请求数据 +type GetOperationLogListReq struct { + UserID *int64 `form:"userID,omitempty" json:"userID,omitempty" bson:"userID,omitempty"` //用户ID + Content *string `form:"content,omitempty" json:"content,omitempty" bson:"content,omitempty"` //操作内容 + CreatedUser *string `form:"createdUser,omitempty" json:"createdUser,omitempty" bson:"createdUser,omitempty"` //操作人 + CreatedAt *time.Time `form:"createdAt,omitempty" json:"createdAt,omitempty" bson:"createdAt,omitempty"` //操作时间 +} + +// GetOperationLogListResp 获取帖子、用户操作日志列表 回复数据 +type GetOperationLogListResp struct { + Total int64 `json:"total" bson:"total"` + List []OperationLog `json:"list" bson:"list"` +} diff --git a/models/v/payaudiobookmod/pay.go b/models/v/payaudiobookmod/pay.go new file mode 100644 index 0000000..97bd8b1 --- /dev/null +++ b/models/v/payaudiobookmod/pay.go @@ -0,0 +1,118 @@ +package payaudiobookmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.PayAudioBook + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "objID", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Insert 插入一条购买记录 +func Insert(p PayAudioBookHistory, t *db.MongoTool) error { + now := time.Now() + f := bson.M{ + "uid": p.UID, + "objID": p.ObjID, + } + set := bson.M{ + "$addToSet": bson.M{"episodeNumberSet": bson.M{"$each": p.EpisodeNumberSet}}, + "$set": bson.M{"updatedAt": now}, + "$setOnInsert": bson.M{"createdAt": now}, + } + if _, err := coll(t).UpsertOne(f, set); err != nil { + log.Error("InsertVideoPayRecord error", log.Any("p", p), log.E(err)) + return err + } + return nil +} + +func FindManyPayAudioBookHistoryByUID(uid uint64) ([]*PayAudioBookHistory, error) { + vl := make([]*PayAudioBookHistory, 0) + if err := coll(nil).Find(&vl, bson.M{"uid": uid}); err != nil { + log.Error("FindManyPayAudioBookHistoryByUID error", log.Any("uid", uid), log.E(err)) + return nil, err + } + return vl, nil +} + +// 判断当前视频是否有人购买过 +func IsVidIfBePay(id primitive.ObjectID) bool { + var pay *PayAudioBookHistory + if err := coll(nil).FindOne(&pay, bson.M{"videoID": id}); err != nil { + log.Error("IsVidIfBePay error", log.Any("videoID", id), log.E(err)) + return false + } + return pay != nil +} + +// DelVideoPayRecord 删除一条购买记录 +func DelVideoPayRecord(videoID primitive.ObjectID, uid uint64) error { + if _, err := coll(nil).DeleteOne(bson.M{"videoID": videoID, "uid": uid}); err != nil { + log.Error("DelVideoPayRecord DeleteOne error", log.Any("videoID", videoID), log.Any("uid", uid)) + return err + } + return nil +} + +func FindByUIDAndObjID(uid uint64, objID primitive.ObjectID) (data PayAudioBookHistory, err error) { + if err = coll(nil).FindOne(&data, bson.M{"uid": uid, "objID": objID}); err != nil { + log.Error("FindByUID error", log.Any("uid", uid), log.E(err)) + return + } + return +} + +func FindByUID(uid uint64, pageNumber int64, pageSize int64) ([]*PayAudioBookHistory, bool, error) { + data := make([]*PayAudioBookHistory, 0) + skip := (pageNumber - 1) * pageSize + limit := pageSize + 1 + opt := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + if err := coll(nil).Find(&data, bson.M{"uid": uid}, &opt); err != nil { + log.Error("FindByUID error", log.Any("uid", uid), log.E(err)) + return nil, false, err + } + hasNext := false + if len(data) > int(pageSize) { + hasNext = true + data = data[:pageSize] + } + return data, hasNext, nil +} diff --git a/models/v/payaudiobookmod/type.go b/models/v/payaudiobookmod/type.go new file mode 100644 index 0000000..da3e4e9 --- /dev/null +++ b/models/v/payaudiobookmod/type.go @@ -0,0 +1,24 @@ +package payaudiobookmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// PayAudioBookHistory 购买影片记录 +type PayAudioBookHistory struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + UID uint64 `json:"uid" bson:"uid"` + EpisodeNumberSet []int `json:"episodeNumberSet" bson:"episodeNumberSet"` //集数 + ObjID primitive.ObjectID `json:"objID" bson:"objID"` //小说id + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/paymentguidemod/model.go b/models/v/paymentguidemod/model.go new file mode 100644 index 0000000..33e7952 --- /dev/null +++ b/models/v/paymentguidemod/model.go @@ -0,0 +1,381 @@ +package paymentguidemod + +import ( + "context" + "fmt" + "strings" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +func Init() { + mdb = db.Init(models.PaymentGuide) + initIndexes() +} + +func guideColl(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(models.PaymentGuide) + } + return t.Coll(models.PaymentGuide) +} + +func impressionColl() *db.MongoTool { + return mdb.Coll(models.PaymentGuideImpression) +} + +func activeGuideIndexKeys() bson.D { + return bson.D{ + {Key: "scene", Value: 1}, + {Key: "enable", Value: 1}, + {Key: "sort", Value: -1}, + {Key: "updatedAt", Value: -1}, + {Key: "_id", Value: -1}, + } +} + +func initIndexes() { + if _, err := guideColl(nil).CreateIndex([]mongo.IndexModel{ + { + Keys: bson.D{ + {Key: "scene", Value: 1}, + {Key: "enable", Value: 1}, + {Key: "sort", Value: -1}, + {Key: "startAt", Value: 1}, + {Key: "endAt", Value: 1}, + }, + }, + { + Keys: activeGuideIndexKeys(), + }, + }); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", models.PaymentGuide, err)) + } + //if err := impressionColl().DropIndexIfExists("uid_1_configId_1_scene_1"); err != nil { + // panic(fmt.Sprintf("%s model drop legacy index err ==>[%+v]", models.PaymentGuideImpression, err)) + //} + if _, err := impressionColl().CreateIndex([]mongo.IndexModel{ + { + Keys: bson.D{ + {Key: "uid", Value: 1}, + {Key: "configId", Value: 1}, + {Key: "scene", Value: 1}, + {Key: "contentVersion", Value: 1}, + }, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "requestId", Value: 1}}, + }, + }); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", models.PaymentGuideImpression, err)) + } +} + +func Insert(p *PaymentGuide) error { + p.Normalize() + if err := p.Validate(); err != nil { + return err + } + now := time.Now() + p.ID = primitive.NewObjectID() + p.CreatedAt = now + p.UpdatedAt = now + _, err := guideColl(nil).InsertOne(p) + return err +} + +// InsertMany validates every configuration before writing and inserts the +// bounded batch in one transaction so callers never observe a partial batch. +func InsertMany(ctx context.Context, configs []PaymentGuide) error { + if len(configs) == 0 { + return fmt.Errorf("payment guide configs are required") + } + if len(configs) > MaxBatchSceneCount { + return fmt.Errorf("payment guide configs cannot contain more than %d entries", MaxBatchSceneCount) + } + now := time.Now() + for i := range configs { + configs[i].Normalize() + if err := configs[i].Validate(); err != nil { + return fmt.Errorf("config %d: %w", i, err) + } + configs[i].ID = primitive.NewObjectID() + configs[i].CreatedAt = now + configs[i].UpdatedAt = now + } + // Use the request context and disable the legacy in-place transaction + // re-entry. A transient error is returned instead of rerunning writes in an + // already-started transaction, and cancelled requests stop database work. + return mdb.TransCtx(ctx, func(t *db.MongoTool) error { + _, err := guideColl(t).InsertMany(configs) + return err + }, (&db.TransOpts{}).SetReEntry(0)) +} + +func Update(p *PaymentGuide) error { + if p.ID.IsZero() { + return fmt.Errorf("id is required") + } + p.Normalize() + if err := p.Validate(); err != nil { + return err + } + p.UpdatedAt = time.Now() + set := bson.M{ + "scene": p.Scene, + "segments": p.Segments, + "style": p.Style, + "title": p.Title, + "description": p.Description, + "cover": p.Cover, + "videoIds": p.VideoIDs, + "videoLimit": p.VideoLimit, + "productId": p.ProductID, + "durationSeconds": p.DurationSeconds, + "action": p.Action, + "enable": p.Enable, + "sort": p.Sort, + "updatedAt": p.UpdatedAt, + } + update := bson.M{"$set": set} + unset := bson.M{} + if p.StartAt == nil { + unset["startAt"] = "" + } else { + set["startAt"] = *p.StartAt + } + if p.EndAt == nil { + unset["endAt"] = "" + } else { + set["endAt"] = *p.EndAt + } + if len(unset) > 0 { + update["$unset"] = unset + } + result, err := guideColl(nil).UpdateOne(bson.M{"_id": p.ID}, update) + if err != nil { + return err + } + if result.MatchedCount == 0 { + return mongo.ErrNoDocuments + } + return nil +} + +func Delete(id primitive.ObjectID) error { + if id.IsZero() { + return fmt.Errorf("id is required") + } + _, err := guideColl(nil).DeleteById(id) + return err +} + +func GetByID(id primitive.ObjectID) (PaymentGuide, error) { + var out PaymentGuide + err := guideColl(nil).FindOne(&out, bson.M{"_id": id}) + return out, err +} + +func List(scene string, skip, limit int64) ([]PaymentGuide, int64, bool, error) { + filter := paymentGuideListFilter(scene) + total, err := guideColl(nil).Count(filter) + if err != nil { + return nil, 0, false, err + } + var list []PaymentGuide + err = guideColl(nil).Find(&list, filter, options.Find(). + SetSort(bson.D{{Key: "sort", Value: -1}, {Key: "updatedAt", Value: -1}, {Key: "_id", Value: -1}}). + SetSkip(skip). + SetLimit(limit+1)) + if err != nil { + return nil, 0, false, err + } + hasNext := len(list) > int(limit) + if hasNext { + list = list[:limit] + } + return list, total, hasNext, nil +} + +func paymentGuideListFilter(scene string) bson.M { + filter := bson.M{"scene": bson.M{"$in": ConfigurableScenes()}} + if scene != "" { + filter["scene"] = scene + } + return filter +} + +func FindActive(scene, segment string, now time.Time) (*PaymentGuide, error) { + configs, err := FindActiveByScenes([]string{scene}, segment, now) + if err != nil { + return nil, err + } + config, exists := configs[strings.ToUpper(strings.TrimSpace(scene))] + if !exists { + return nil, nil + } + return &config, nil +} + +// FindActiveByScenes returns at most one highest-priority active configuration +// for every requested scene. The scene batch is deliberately bounded by the +// supported-scene count so Ping never fans out into per-scene database reads. +func FindActiveByScenes(scenes []string, segment string, now time.Time) (map[string]PaymentGuide, error) { + normalizedScenes, err := normalizeSceneBatch(scenes) + if err != nil { + return nil, err + } + result := make(map[string]PaymentGuide, len(normalizedScenes)) + if len(normalizedScenes) == 0 { + return result, nil + } + + segment = strings.ToUpper(strings.TrimSpace(segment)) + segmentFilters := bson.A{ + bson.M{"segments": bson.M{"$exists": false}}, + bson.M{"segments": nil}, + bson.M{"segments": bson.M{"$size": 0}}, + } + if segment != "" { + segmentFilters = append(segmentFilters, bson.M{"segments": segment}) + } + filter := bson.M{ + "scene": bson.M{"$in": normalizedScenes}, + "enable": true, + "$and": bson.A{ + bson.M{"$or": bson.A{ + bson.M{"startAt": bson.M{"$exists": false}}, + bson.M{"startAt": nil}, + bson.M{"startAt": bson.M{"$lte": now}}, + }}, + bson.M{"$or": bson.A{ + bson.M{"endAt": bson.M{"$exists": false}}, + bson.M{"endAt": nil}, + bson.M{"endAt": bson.M{"$gt": now}}, + }}, + bson.M{"$or": segmentFilters}, + }, + } + pipeline := []bson.M{ + {"$match": filter}, + {"$sort": bson.D{ + {Key: "scene", Value: 1}, + {Key: "sort", Value: -1}, + {Key: "updatedAt", Value: -1}, + {Key: "_id", Value: -1}, + }}, + {"$group": bson.D{ + {Key: "_id", Value: "$scene"}, + {Key: "config", Value: bson.D{{Key: "$first", Value: "$$ROOT"}}}, + }}, + {"$replaceRoot": bson.D{{Key: "newRoot", Value: "$config"}}}, + } + var configs []PaymentGuide + aggregateOpts := options.Aggregate(). + SetHint(activeGuideIndexKeys()). + SetMaxTime(2 * time.Second) + if err = guideColl(nil).Aggregate(&configs, pipeline, aggregateOpts); err != nil { + return nil, err + } + for _, config := range configs { + result[config.Scene] = config + } + return result, nil +} + +func normalizeSceneBatch(scenes []string) ([]string, error) { + if len(scenes) > MaxBatchSceneCount { + return nil, fmt.Errorf("scenes cannot contain more than %d entries", MaxBatchSceneCount) + } + seen := make(map[string]struct{}, len(scenes)) + normalized := make([]string, 0, len(scenes)) + for _, scene := range scenes { + scene = strings.ToUpper(strings.TrimSpace(scene)) + if !ValidScene(scene) { + return nil, fmt.Errorf("unsupported scene: %s", scene) + } + if _, exists := seen[scene]; exists { + continue + } + seen[scene] = struct{}{} + normalized = append(normalized, scene) + } + return normalized, nil +} + +func HasImpression(uid uint64, configID primitive.ObjectID, scene, contentVersion string) (bool, error) { + count, err := impressionColl().Count(bson.M{ + "uid": uid, + "configId": configID, + "scene": scene, + "contentVersion": contentVersion, + }) + return count > 0, err +} + +// FindImpressionConfigIDs performs one bounded lookup for the ordinary Ping +// scenes and returns the configurations already shown to the user. +func FindImpressionConfigIDs(uid uint64, configIDs []primitive.ObjectID) (map[primitive.ObjectID]bool, error) { + result := make(map[primitive.ObjectID]bool, len(configIDs)) + if uid == 0 || len(configIDs) == 0 { + return result, nil + } + if len(configIDs) > MaxBatchSceneCount { + return nil, fmt.Errorf("configIds cannot contain more than %d entries", MaxBatchSceneCount) + } + seen := make(map[primitive.ObjectID]struct{}, len(configIDs)) + uniqueIDs := make([]primitive.ObjectID, 0, len(configIDs)) + for _, configID := range configIDs { + if configID.IsZero() { + return nil, fmt.Errorf("configId cannot be empty") + } + if _, exists := seen[configID]; exists { + continue + } + seen[configID] = struct{}{} + uniqueIDs = append(uniqueIDs, configID) + } + type impressionRef struct { + ConfigID primitive.ObjectID `bson:"configId"` + } + var rows []impressionRef + findOpts := options.Find(). + SetProjection(bson.M{"_id": 0, "configId": 1}). + SetLimit(int64(len(uniqueIDs))) + if err := impressionColl().Find(&rows, bson.M{ + "uid": uid, + "configId": bson.M{"$in": uniqueIDs}, + "contentVersion": "", + }, findOpts); err != nil { + return nil, err + } + for _, row := range rows { + result[row.ConfigID] = true + } + return result, nil +} + +func RecordImpression(item Impression) error { + item.ID = primitive.NilObjectID + item.CreatedAt = time.Now() + _, err := impressionColl().UpsertOne( + bson.M{ + "uid": item.UID, + "configId": item.ConfigID, + "scene": item.Scene, + "contentVersion": item.ContentVersion, + }, + bson.M{"$setOnInsert": item}, + ) + return err +} diff --git a/models/v/paymentguidemod/type.go b/models/v/paymentguidemod/type.go new file mode 100644 index 0000000..bea33fb --- /dev/null +++ b/models/v/paymentguidemod/type.go @@ -0,0 +1,242 @@ +package paymentguidemod + +import ( + "fmt" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + // SceneHomeNewUser is kept only for backward compatibility with legacy + // records and clients. It is no longer configurable or returned to App/Web. + SceneHomeNewUser = "HOME_NEW_USER" + SceneHomeNewUserFreeTrial = "HOME_NEW_USER_FREE_TRIAL" + SceneHomeOldUser = "HOME_OLD_USER" + SceneVideoPreviewEnd = "VIDEO_PREVIEW_END" + SceneDiscountCountdown = "DISCOUNT_COUNTDOWN" + SceneVideoBack = "VIDEO_BACK" + SceneVIPCenter = "VIP_CENTER" + SceneVIPContentUpdate = "VIP_CONTENT_UPDATE" +) + +const ( + DefaultVIPContentVideoLimit int64 = 4 + MaxVIPContentVideoLimit int64 = 20 + MaxBatchSceneCount = 7 +) + +const ( + SegmentNewNeverPaid = "NEW_NEVER_PAID" + SegmentOldNeverPaid = "OLD_NEVER_PAID" + SegmentPaidUpgrade = "PAID_UPGRADE" + SegmentMaxVIP = "MAX_VIP" + SegmentNormal = "NORMAL" + SegmentUnregistered = "UNREGISTERED" +) + +var validScenes = map[string]struct{}{ + SceneHomeNewUser: {}, + SceneHomeNewUserFreeTrial: {}, + SceneHomeOldUser: {}, + SceneVideoPreviewEnd: {}, + SceneDiscountCountdown: {}, + SceneVideoBack: {}, + SceneVIPCenter: {}, + SceneVIPContentUpdate: {}, +} + +var configurableScenes = []string{ + SceneHomeNewUserFreeTrial, + SceneHomeOldUser, + SceneVideoPreviewEnd, + SceneDiscountCountdown, + SceneVideoBack, + SceneVIPCenter, + SceneVIPContentUpdate, +} + +var validSegments = map[string]struct{}{ + SegmentNewNeverPaid: {}, + SegmentOldNeverPaid: {}, + SegmentPaidUpgrade: {}, + SegmentMaxVIP: {}, + SegmentNormal: {}, + SegmentUnregistered: {}, +} + +type Action struct { + Type string `json:"type" bson:"type"` + Value string `json:"value" bson:"value"` +} + +type PaymentGuide struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Scene string `json:"scene" bson:"scene"` + Segments []string `json:"segments" bson:"segments"` + Style string `json:"style" bson:"style"` + Title string `json:"title" bson:"title"` + Description string `json:"description" bson:"description"` + Cover string `json:"cover" bson:"cover"` + VideoIDs []string `json:"videoIds" bson:"videoIds"` + VideoLimit int64 `json:"videoLimit,omitempty" bson:"videoLimit,omitempty"` + ProductID string `json:"productId" bson:"productId"` + DurationSeconds int64 `json:"durationSeconds" bson:"durationSeconds"` + Action Action `json:"action" bson:"action"` + Enable bool `json:"enable" bson:"enable"` + Sort int `json:"sort" bson:"sort"` + StartAt *time.Time `json:"startAt,omitempty" bson:"startAt,omitempty"` + EndAt *time.Time `json:"endAt,omitempty" bson:"endAt,omitempty"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +type Impression struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + UID uint64 `json:"uid" bson:"uid"` + ConfigID primitive.ObjectID `json:"configId" bson:"configId"` + Scene string `json:"scene" bson:"scene"` + ContentVersion string `json:"contentVersion" bson:"contentVersion"` + VideoID string `json:"videoId" bson:"videoId"` + RequestID string `json:"requestId" bson:"requestId"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +func ValidScene(scene string) bool { + _, ok := validScenes[scene] + return ok +} + +// ConfigurableScenes returns the bounded scene list exposed to App/Web. The +// legacy HOME_NEW_USER code remains valid for old data but is intentionally +// absent from this list. +func ConfigurableScenes() []string { + return append([]string(nil), configurableScenes...) +} + +func ConfigurableScene(scene string) bool { + for _, candidate := range configurableScenes { + if scene == candidate { + return true + } + } + return false +} + +func (p *PaymentGuide) Normalize() { + p.Scene = strings.TrimSpace(strings.ToUpper(p.Scene)) + p.Style = strings.TrimSpace(strings.ToUpper(p.Style)) + p.Title = strings.TrimSpace(p.Title) + p.ProductID = strings.TrimSpace(p.ProductID) + p.Action.Type = strings.TrimSpace(strings.ToUpper(p.Action.Type)) + p.Action.Value = strings.TrimSpace(p.Action.Value) + if p.Action.Type == "VIP_PRODUCT" { + if p.ProductID == "" { + p.ProductID = p.Action.Value + } + if p.Action.Value == "" { + p.Action.Value = p.ProductID + } + } + seen := make(map[string]struct{}, len(p.Segments)) + segments := make([]string, 0, len(p.Segments)) + for _, segment := range p.Segments { + segment = strings.TrimSpace(strings.ToUpper(segment)) + if segment == "" { + continue + } + if _, exists := seen[segment]; exists { + continue + } + seen[segment] = struct{}{} + segments = append(segments, segment) + } + p.Segments = segments +} + +func (p PaymentGuide) Validate() error { + if !ValidScene(p.Scene) { + return fmt.Errorf("unsupported scene: %s", p.Scene) + } + for _, segment := range p.Segments { + if _, ok := validSegments[segment]; !ok { + return fmt.Errorf("unsupported segment: %s", segment) + } + } + if p.Style == "" { + return fmt.Errorf("style is required") + } + if p.Title == "" { + return fmt.Errorf("title is required") + } + if p.DurationSeconds < 0 { + return fmt.Errorf("durationSeconds cannot be negative") + } + if p.VideoLimit < 0 || p.VideoLimit > MaxVIPContentVideoLimit { + return fmt.Errorf("videoLimit must be between 0 and %d", MaxVIPContentVideoLimit) + } + if p.StartAt != nil && p.EndAt != nil && !p.EndAt.After(*p.StartAt) { + return fmt.Errorf("endAt must be later than startAt") + } + switch p.Action.Type { + case "VIP_PRODUCT": + if p.Action.Value == "" { + return fmt.Errorf("action.value is required for %s", p.Action.Type) + } + if p.ProductID != p.Action.Value { + return fmt.Errorf("productId must match action.value for VIP_PRODUCT") + } + case "INTERNAL", "EXTERNAL": + if p.Action.Value == "" { + return fmt.Errorf("action.value is required for %s", p.Action.Type) + } + case "NONE": + if p.Action.Value != "" { + return fmt.Errorf("action.value must be empty for NONE") + } + default: + return fmt.Errorf("unsupported action.type: %s", p.Action.Type) + } + return nil +} + +// EffectiveVideoLimit returns the configured video count with a safe default +// for legacy configurations that do not contain videoLimit. +func (p PaymentGuide) EffectiveVideoLimit() int64 { + if p.VideoLimit <= 0 { + return DefaultVIPContentVideoLimit + } + if p.VideoLimit > MaxVIPContentVideoLimit { + return MaxVIPContentVideoLimit + } + return p.VideoLimit +} + +// VIPContentVideoLimit keeps the scene-specific call site readable while +// sharing the same defaulting rules used by the Web configuration page. +func (p PaymentGuide) VIPContentVideoLimit() int64 { + return p.EffectiveVideoLimit() +} + +func (p PaymentGuide) ActiveAt(now time.Time) bool { + if !p.Enable { + return false + } + if p.StartAt != nil && p.StartAt.After(now) { + return false + } + return p.EndAt == nil || p.EndAt.After(now) +} + +func (p PaymentGuide) MatchesSegment(segment string) bool { + if len(p.Segments) == 0 { + return true + } + for _, allowed := range p.Segments { + if allowed == segment { + return true + } + } + return false +} diff --git a/models/v/paymentguidemod/type_test.go b/models/v/paymentguidemod/type_test.go new file mode 100644 index 0000000..8061cae --- /dev/null +++ b/models/v/paymentguidemod/type_test.go @@ -0,0 +1,184 @@ +package paymentguidemod + +import ( + "context" + "reflect" + "strings" + "testing" + "time" + + "go.mongodb.org/mongo-driver/bson" +) + +func TestActiveGuideIndexKeysMatchBatchQueryOrder(t *testing.T) { + want := bson.D{ + {Key: "scene", Value: 1}, + {Key: "enable", Value: 1}, + {Key: "sort", Value: -1}, + {Key: "updatedAt", Value: -1}, + {Key: "_id", Value: -1}, + } + if got := activeGuideIndexKeys(); !reflect.DeepEqual(got, want) { + t.Fatalf("activeGuideIndexKeys() = %v, want %v", got, want) + } +} + +func TestPaymentGuideNormalizeAndValidate(t *testing.T) { + config := PaymentGuide{ + Scene: "video_preview_end", + Segments: []string{"old_never_paid", "OLD_NEVER_PAID"}, + Style: "bottom_sheet", + Title: "开通会员", + Action: Action{Type: "vip_product", Value: "product-id"}, + } + config.Normalize() + if err := config.Validate(); err != nil { + t.Fatal(err) + } + if config.Scene != SceneVideoPreviewEnd || len(config.Segments) != 1 { + t.Fatalf("unexpected normalized config: %+v", config) + } +} + +func TestPaymentGuideActiveAt(t *testing.T) { + now := time.Now() + start := now.Add(-time.Minute) + end := now.Add(time.Minute) + config := PaymentGuide{Enable: true, StartAt: &start, EndAt: &end} + if !config.ActiveAt(now) { + t.Fatal("config should be active") + } +} + +func TestPaymentGuideVIPContentVideoLimit(t *testing.T) { + if DefaultVIPContentVideoLimit != 4 { + t.Fatalf("DefaultVIPContentVideoLimit = %d, want 4", DefaultVIPContentVideoLimit) + } + tests := []struct { + name string + limit int64 + want int64 + }{ + {name: "legacy default", limit: 0, want: DefaultVIPContentVideoLimit}, + {name: "minimum", limit: 1, want: 1}, + {name: "configured", limit: 12, want: 12}, + {name: "maximum", limit: MaxVIPContentVideoLimit, want: MaxVIPContentVideoLimit}, + {name: "defensive clamp", limit: MaxVIPContentVideoLimit + 1, want: MaxVIPContentVideoLimit}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := PaymentGuide{VideoLimit: tt.limit} + if got := config.VIPContentVideoLimit(); got != tt.want { + t.Fatalf("VIPContentVideoLimit() = %d, want %d", got, tt.want) + } + }) + } +} + +func TestPaymentGuideBatchSceneLimitMatchesSupportedScenes(t *testing.T) { + if len(ConfigurableScenes()) != MaxBatchSceneCount { + t.Fatalf("len(ConfigurableScenes()) = %d, MaxBatchSceneCount = %d", len(ConfigurableScenes()), MaxBatchSceneCount) + } +} + +func TestLegacyHomeNewUserSceneIsNotConfigurable(t *testing.T) { + if !ValidScene(SceneHomeNewUser) { + t.Fatal("legacy HOME_NEW_USER must remain recognizable") + } + if ConfigurableScene(SceneHomeNewUser) { + t.Fatal("legacy HOME_NEW_USER must not be returned as configurable") + } + if !ConfigurableScene(SceneHomeNewUserFreeTrial) { + t.Fatal("HOME_NEW_USER_FREE_TRIAL must be configurable") + } +} + +func TestConfigurableScenesReturnsCopy(t *testing.T) { + scenes := ConfigurableScenes() + scenes[0] = "CHANGED" + if ConfigurableScenes()[0] != SceneHomeNewUserFreeTrial { + t.Fatal("ConfigurableScenes must not expose mutable package state") + } +} + +func TestPaymentGuideListFilterExcludesLegacyScene(t *testing.T) { + filter := paymentGuideListFilter("") + sceneFilter, ok := filter["scene"].(bson.M) + if !ok { + t.Fatalf("unexpected scene filter: %#v", filter["scene"]) + } + scenes, ok := sceneFilter["$in"].([]string) + if !ok { + t.Fatalf("unexpected scene inclusion filter: %#v", sceneFilter["$in"]) + } + for _, scene := range scenes { + if scene == SceneHomeNewUser { + t.Fatalf("legacy HOME_NEW_USER must not be listed: %#v", scenes) + } + } + if filter := paymentGuideListFilter(SceneVIPCenter); filter["scene"] != SceneVIPCenter { + t.Fatalf("explicit scene filter was not preserved: %#v", filter) + } +} + +func TestDiscountCountdownIsSupportedScene(t *testing.T) { + if !ValidScene(SceneDiscountCountdown) { + t.Fatal("DISCOUNT_COUNTDOWN must be a supported payment-guide scene") + } +} + +func TestPaymentGuideInsertManyRejectsUnboundedBatchesBeforeDatabaseAccess(t *testing.T) { + tests := []struct { + name string + configs []PaymentGuide + match string + }{ + {name: "empty", configs: nil, match: "required"}, + {name: "too many", configs: make([]PaymentGuide, MaxBatchSceneCount+1), match: "more than"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := InsertMany(context.Background(), tt.configs) + if err == nil || !strings.Contains(err.Error(), tt.match) { + t.Fatalf("InsertMany() error = %v, want match %q", err, tt.match) + } + }) + } +} + +func TestNormalizeSceneBatch(t *testing.T) { + scenes, err := normalizeSceneBatch([]string{" home_new_user ", "HOME_NEW_USER", "vip_center"}) + if err != nil { + t.Fatal(err) + } + if len(scenes) != 2 || scenes[0] != SceneHomeNewUser || scenes[1] != SceneVIPCenter { + t.Fatalf("normalizeSceneBatch() = %#v", scenes) + } + if _, err = normalizeSceneBatch([]string{"UNKNOWN"}); err == nil { + t.Fatal("normalizeSceneBatch must reject unsupported scenes") + } + if _, err = normalizeSceneBatch(make([]string, MaxBatchSceneCount+1)); err == nil { + t.Fatal("normalizeSceneBatch must reject an unbounded batch") + } +} + +func TestPaymentGuideValidateVideoLimit(t *testing.T) { + valid := PaymentGuide{ + Scene: SceneVIPContentUpdate, + Style: "BOTTOM_SHEET", + Title: "VIP内容更新", + Action: Action{Type: "NONE"}, + VideoLimit: MaxVIPContentVideoLimit, + } + if err := valid.Validate(); err != nil { + t.Fatalf("Validate() unexpected error: %v", err) + } + + for _, limit := range []int64{-1, MaxVIPContentVideoLimit + 1} { + invalid := valid + invalid.VideoLimit = limit + if err := invalid.Validate(); err == nil { + t.Fatalf("Validate() with videoLimit %d should fail", limit) + } + } +} diff --git a/models/v/prdcthsomod/app.go b/models/v/prdcthsomod/app.go new file mode 100644 index 0000000..e6c3ca8 --- /dev/null +++ b/models/v/prdcthsomod/app.go @@ -0,0 +1,55 @@ +package prdcthsomod + +import ( + "91porn-server/models/commod" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// PaymentLogBody 商品交易订单请求参数 +type PaymentLogBody struct { + ProductID primitive.ObjectID `bson:"productID" json:"productID" binding:"required" ` //产品id + Name string `bson:"name" json:"name" binding:"required" ` //商品名字 + Amount int64 `bson:"amount" json:"amount" binding:"required"` //成交价格 + ProductType int `bson:"productType" json:"productType"` //产品类型 +} + +// ProductHistoryQueryReq 通用查询参数 +type ProductHistoryQueryReq struct { + UID *uint64 `form:"uid" json:"uid,omitempty" bson:"uid"` // 用户id + ProductType *commod.ProductType `form:"productType" json:"productType" ` // 产品类型 // 0-会员卡 21-预售卡 + AdvanceOrderStatus *int `form:"advanceOrderStatus" json:"advanceOrderStatus"` // 预售订单状态 // 2-预付成功 4-尾款支付成功 + Start *time.Time `form:"start" json:"start"` // 开始时间 + End *time.Time `form:"end" json:"end"` // 结束时间 + commod.Page +} + +type ProductHistoryQueryResp struct { + Total int64 `json:"total"` + List []*ProductHistory `json:"list"` +} + +func (req *ProductHistoryQueryReq) GetCond() bson.M { + cond := bson.M{} + if req.UID != nil { + cond["uid"] = *req.UID + } + if req.ProductType != nil { + cond["productType"] = *req.ProductType + } + if req.AdvanceOrderStatus != nil { + cond["advanceOrderStatus"] = *req.AdvanceOrderStatus + } + if req.Start != nil && req.End != nil { + cond["createdAt"] = bson.M{"$gte": req.Start, "$lt": req.End} + } + return cond +} + +func (req *ProductHistoryQueryReq) GetOpt() *options.FindOptions { + opt := options.Find().SetSkip(req.Skip64()).SetLimit(req.Limit64()).SetSort(bson.D{{Key: "createdAt", Value: -1}}) + return opt +} diff --git a/models/v/prdcthsomod/paymentLog.go b/models/v/prdcthsomod/paymentLog.go new file mode 100644 index 0000000..9a63b46 --- /dev/null +++ b/models/v/prdcthsomod/paymentLog.go @@ -0,0 +1,53 @@ +package prdcthsomod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson" +) + +type M = bson.M + +// PayVipTotalAmountByCreatedTime 购买VIP的总金额 +func PayVipTotalAmountByCreatedTime(start time.Time, end time.Time) (int64, error) { + filter := M{ + "createdAt": M{ + "$gte": start, + "$lt": end, + }, + "productType": VIP, + } + list := make([]struct { + Amount int64 `bson:"amount"` + }, 0) + if err := coll(nil).Find(&list, filter); err != nil { + return 0, err + } + var totalAmount int64 + for _, v := range list { + totalAmount += v.Amount + } + return totalAmount, nil +} + +// PayVipUIDSByCreatedTime 购买VIP的用户UID +func PayVipUIDSByCreatedTime(start time.Time, end time.Time) ([]uint64, error) { + filter := M{ + "createdAt": M{ + "$gte": start, + "$lt": end, + }, + "productType": VIP, + } + list := make([]struct { + UID uint64 `bson:"uid"` + }, 0) + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + uidList := make([]uint64, len(list)) + for i, v := range list { + uidList[i] = v.UID + } + return uidList, nil +} diff --git a/models/v/prdcthsomod/producthistory.go b/models/v/prdcthsomod/producthistory.go new file mode 100644 index 0000000..95801ad --- /dev/null +++ b/models/v/prdcthsomod/producthistory.go @@ -0,0 +1,213 @@ +package prdcthsomod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/commod" + "91porn-server/models/v/productmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.ProductHistory + +// initIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "productID", Value: 1}}, + }, + { + Keys: bson.D{{Key: "name", Value: 1}}, + }, + { + Keys: bson.D{{Key: "amount", Value: 1}}, + }, + { + Keys: bson.D{{Key: "productType", Value: 1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + // 用户运营导出预售状态相关的订单 + { + Keys: bson.D{{Key: "advanceOrderStatus", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "deductType", Value: 1}, {Key: "createdAt", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{ + {Key: "experimentId", Value: 1}, + {Key: "experimentVariant", Value: 1}, + {Key: "productID", Value: 1}, + }, + Options: options.Index(). + SetName("vip_experiment_gold_order_statistics"). + SetPartialFilterExpression(bson.M{"experimentId": bson.M{"$gt": ""}}), + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func IsBoughtVIP(uid uint64) (bool, error) { + p := ProductHistory{} + if err := coll(nil).FindOne(&p, bson.M{"uid": uid, "productType": VIP}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsBoughtVIP", table, "FindOne", err), log.Any("uid", uid)) + return false, err + } + return p.UID != 0, nil +} + +// GetUserLastVip 获取用户最后充值的VIP +func GetUserLastVip(uid uint64) (productID primitive.ObjectID, productName string, amount int64) { + p := ProductHistory{} + opt := options.FindOne().SetSort(bson.D{{Key: "createdAt", Value: -1}}) + if err := coll(nil).FindOne(&p, bson.M{"uid": uid, "productType": bson.M{"$in": []commod.ProductType{productmod.VIP, productmod.NewUser, productmod.AdvanceCard}}}, opt); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsBoughtVIP", table, "FindOne", err), log.Any("uid", uid)) + return primitive.NilObjectID, "", 0 + } + if p.ProductType == productmod.AdvanceCard { + return productID, "", 0 + } + if p.IsUpgrade { + // 假设A:100 B:300 C:500 如果A连续升到B,不这么处理,就会变成B=》C要付款300,实际应该给200差价就好 + return p.ProductID, p.Name, p.PurchasePrice + } + return p.ProductID, p.Name, p.Amount +} + +// InsertProductHistory 插入一条数据 +func InsertProductHistory(t *db.MongoTool, p *ProductHistory) error { + p.CreatedAt = time.Now() + p.UpdatedAt = time.Now() + res, err := coll(t).InsertOne(p) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertProductHistory", table, "InsertOne", err)) + return err + } + p.ID = res.InsertedID.(primitive.ObjectID) + return nil +} + +func InsertManyProductHistory(t *db.MongoTool, p []ProductHistory) error { + if _, err := coll(t).InsertMany(p); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertProductHistory", table, "InsertOne", err)) + return err + } + return nil +} + +// FindProductHistorys 查询所有ProductHistory类型 +func FindProductHistorys(cond bson.M, opts *options.FindOptions) (total int64, data []*ProductHistory, err error) { + data = make([]*ProductHistory, 0) + if err = coll(nil).Find(&data, cond, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProductHistorys", table, "Find", err), log.Any("cond", cond)) + return + } + total, err = coll(nil).Count(cond) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProductHistorys", table, "Count", err), log.Any("cond", cond)) + } + return +} + +// FindList +func FindList(cond bson.M, opts *options.FindOptions) (data []*ProductHistory, err error) { + data = make([]*ProductHistory, 0) + if err = coll(nil).Find(&data, cond, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindList", table, "Find", err), log.Any("cond", cond)) + return + } + return +} + +func FindUserProductHistory(uid uint64) (list []ProductHistory, err error) { + f := bson.M{"uid": uid, "productType": bson.M{"$in": []commod.ProductType{productmod.VIP, productmod.OTHER, + productmod.MeetingCard, productmod.VideoDiscount, + productmod.VideoFreeCard, productmod.VideoDiscount, productmod.VideoFreeCard, + }}} + opts := options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}}) + if err = coll(nil).Find(&list, f, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProductHistorys", table, "Find", err), log.Any("query", f)) + return + } + return +} + +func FindUserProductHistoryByType(uid uint64) (list []ProductHistory, err error) { + f := bson.M{"uid": uid, "productType": bson.M{"$in": []commod.ProductType{productmod.GameAdvanceCard}}} + opts := options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}}) + if err = coll(nil).Find(&list, f, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProductHistorys", table, "Find", err), log.Any("query", f)) + return + } + return +} + +// FindProductHistorysByUID 查询所有ProductHistory类型 +func FindProductHistorysByUID(uid, pageNumber, pageSizse uint64) (total int64, data []*ProductHistory, hasNext bool, err error) { + data = make([]*ProductHistory, 0) + f := bson.M{"uid": uid, "productType": bson.M{"$in": []commod.ProductType{productmod.VIP, productmod.OTHER, + productmod.MeetingCard, productmod.VideoDiscount, + productmod.VideoFreeCard, productmod.VideoDiscount, productmod.VideoFreeCard, + }}} + skip := int64(pageSizse * (pageNumber - 1)) + limit := int64(pageSizse + 1) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + total, err = coll(nil).Count(f) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %ss fail error:%+v:", "FindProductHistorys", table, "Count", err), log.Any("query", f)) + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProductHistorys", table, "Find", err), log.Any("query", f)) + return + } + if len(data) > int(pageSizse) { + hasNext = true + data = data[:pageSizse] + } + return +} + +// FindMany FindMany +func FindMany(start, end time.Time, opts *options.FindOptions) (data []*ProductStat, err error) { + cond := bson.M{"createdAt": bson.M{"$gte": start, "$lt": end}} + data = make([]*ProductStat, 0) + if err = coll(nil).Find(&data, cond, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProductHistorys", table, "Find", err), log.Any("cond", cond)) + return + } + return +} diff --git a/models/v/prdcthsomod/type.go b/models/v/prdcthsomod/type.go new file mode 100644 index 0000000..e347508 --- /dev/null +++ b/models/v/prdcthsomod/type.go @@ -0,0 +1,93 @@ +package prdcthsomod + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models/commod" + "91porn-server/models/v/productmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + VIP = commod.VIP //product 0 + VIDEO = commod.VIDEO //VIDEO视频 + MODEL = commod.MODEL //MODEL嫩模 + GAME = commod.GAME //GAME 游戏币 + MeetingCard = commod.MeetingCard //约会卡 + OTHER = commod.OTHER //OTHER 1 + VideoDiscount = commod.VideoDiscount //视频折扣卡 + VideoFreeCard = commod.VideoFreeCard //视频免费卡 + CoinMonthCard = commod.CoinMonthCard //金币月卡 + Media = commod.Media //动漫整本 + AdvanceCard = commod.AdvanceCard // 预售卡 + GameAdvanceCard = commod.GameAdvanceCard // 游戏售卡 + WhoringCard = commod.WhoringCard // 白嫖卡 + ImGroup = commod.WhoringCard // 加入群 24 +) +const ( + COMPLETE = iota //COMPLETE 交易完成 + CANCEL //CANCEL 交易撤销 +) + +var mdb *db.MongoDB + +type DiscDoc = commod.DiscDoc + +// ProductHistory 商品交易订单 +type ProductHistory struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id" ` // 交易订单号 + UID uint64 `bson:"uid" json:"uid"` // 用户id + ProductID primitive.ObjectID `bson:"productID" json:"productID" ` // 产品id + Name string `bson:"name" json:"name" ` // 商品名字 + Amount int64 `bson:"amount" json:"amount" ` // 花费的余额 + Income int64 `bson:"income" json:"income"` // 花费的收益 + ProductType commod.ProductType `bson:"productType" json:"productType" ` // 产品类型 // 0-会员卡 21-预售卡 + AdvanceOrderStatus int `bson:"advanceOrderStatus" json:"advanceOrderStatus"` // 预售订单状态 // 2-预付成功 4-尾款支付成功 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + SysType string `json:"sysType" bson:"sysType"` // 设备系统类型 ios pc android + GameCode string `json:"gameCode" bson:"gameCode"` // 购买游戏码 + ProductSnapShot *productmod.Product `json:"productSnapShot" bson:"productSnapShot"` // 产品购买时快照 + IsUpgrade bool `json:"isUpgrade" bson:"isUpgrade"` // 是否VIP升级 + CurrentVipName string `json:"currentVipName" bson:"currentVipName"` // 当前VIP名称 + CurrentVipPrice int64 `json:"currentVipPrice" bson:"currentVipPrice"` // 当前VIP价格 + PurchasePrice int64 `json:"purchasePrice" bson:"purchasePrice"` // 原价购买价格 + ExperimentID string `json:"experimentId,omitempty" bson:"experimentId,omitempty"` // VIP卡片A/B实验ID + ExperimentVariant string `json:"experimentVariant,omitempty" bson:"experimentVariant,omitempty"` // VIP卡片A/B实验分组 + SessionID string `json:"sessionId,omitempty" bson:"sessionId,omitempty"` // VIP卡片页面会话ID + DiscDoc `bson:",inline"` +} + +type ProductHistoryExport struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id" xlsx:"交易订单号"` // 交易订单号 + UID uint64 `bson:"uid" json:"uid" xlsx:"用户id"` // 用户id + ProductID primitive.ObjectID `bson:"productID" json:"productID" xlsx:"会员卡id"` // 会员卡id + Name string `bson:"name" json:"name" xlsx:"商品名字"` // 商品名字 + Duration int `bson:"duration" json:"duration" xlsx:"会员卡持续天数"` // 会员卡持续天数 + VipLevel int `bson:"vipLevel" json:"vipLevel" xlsx:"购买时的会员卡等级"` // 购买时的会员卡等级 + IsUpgrade bool `json:"isUpgrade" bson:"isUpgrade" xlsx:"是否VIP升级"` // 是否VIP升级 + OriginalPrice int64 `bson:"originalPrice" json:"originalPrice" xlsx:"原价(金币)"` // 原价(金币) + DiscountedPriceIos int64 `bson:"discountedPriceIos,omitempty" json:"discountedPriceIos,omitempty" xlsx:"ios现价/升级价(金币)"` // ios现价/升级价(金币) + DiscountedPriceAnd int64 `bson:"discountedPriceAnd,omitempty" json:"discountedPriceAnd,omitempty" xlsx:"安卓现价/升级价(金币)"` // 安卓现价/升级价(金币) + Amount int64 `bson:"amount" json:"amount" xlsx:"花费的金币"` // 花费的余额 + Income int64 `bson:"income" json:"income" xlsx:"花费的收益"` // 花费的收益 + ProductType string `bson:"productType" json:"productType" xlsx:"产品类型"` // 产品类型 // 0-会员卡 21-预售卡 + AdvanceOrderStatus string `bson:"advanceOrderStatus" json:"advanceOrderStatus" xlsx:"预售订单状态"` // 预售订单状态 // 2-预付成功 4-尾款支付成功 + SysType string `json:"sysType" bson:"sysType" xlsx:"设备系统类型"` // 设备系统类型 ios pc android + CreatedAt time.Time `json:"createdAt" bson:"createdAt" xlsx:"创建时间"` // 创建时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type ProductStat struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id" ` //交易订单号 + ProductID primitive.ObjectID `bson:"productID" json:"productID" ` //产品id + Amount int64 `bson:"amount" json:"amount" ` //成交价格 + Income int64 `bson:"income" json:"income"` //花费的作品收益 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} diff --git a/models/v/prdcthsomod/vip_experiment.go b/models/v/prdcthsomod/vip_experiment.go new file mode 100644 index 0000000..b7be812 --- /dev/null +++ b/models/v/prdcthsomod/vip_experiment.go @@ -0,0 +1,53 @@ +package prdcthsomod + +import ( + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// VIPExperimentGoldOrderStat contains successful VIP card purchases paid with coins. +type VIPExperimentGoldOrderStat struct { + Variant string `json:"variant" bson:"variant"` + ProductID primitive.ObjectID `json:"productId" bson:"productId"` + GoldPaidOrders int `json:"goldPaidOrders" bson:"goldPaidOrders"` + GoldPaidAmount int64 `json:"goldPaidAmount" bson:"goldPaidAmount"` +} + +func VIPExperimentGoldOrderStatistics(experimentID string) ([]VIPExperimentGoldOrderStat, error) { + var raw []struct { + ID struct { + Variant string `bson:"variant"` + ProductID primitive.ObjectID `bson:"productId"` + } `bson:"_id"` + GoldPaidOrders int `bson:"goldPaidOrders"` + GoldPaidAmount int64 `bson:"goldPaidAmount"` + } + err := coll(nil).Aggregate(&raw, []bson.M{ + {"$match": bson.M{"experimentId": experimentID}}, + {"$group": bson.M{ + "_id": bson.M{ + "variant": "$experimentVariant", + "productId": "$productID", + }, + "goldPaidOrders": bson.M{"$sum": 1}, + "goldPaidAmount": bson.M{"$sum": bson.M{"$add": bson.A{ + bson.M{"$ifNull": bson.A{"$amount", 0}}, + bson.M{"$ifNull": bson.A{"$income", 0}}, + }}}, + }}, + {"$sort": bson.D{{Key: "_id.variant", Value: 1}, {Key: "goldPaidOrders", Value: -1}}}, + }) + if err != nil { + return nil, err + } + result := make([]VIPExperimentGoldOrderStat, 0, len(raw)) + for _, item := range raw { + result = append(result, VIPExperimentGoldOrderStat{ + Variant: item.ID.Variant, + ProductID: item.ID.ProductID, + GoldPaidOrders: item.GoldPaidOrders, + GoldPaidAmount: item.GoldPaidAmount, + }) + } + return result, nil +} diff --git a/models/v/prize_record_mod/app.go b/models/v/prize_record_mod/app.go new file mode 100644 index 0000000..4a3e403 --- /dev/null +++ b/models/v/prize_record_mod/app.go @@ -0,0 +1,67 @@ +package prize_record_mod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type PrizeType int +type Location int + +// 奖品类型 1-金币 2-游戏币 3-会员卡 4-vip 5-金币加赠券 6-金币观影券 7-实物 +const ( + PrizeTypeGold PrizeType = iota + 1 // 1、金币 + PrizeTypeGameGold // 2、棋牌游戏币 + PrizeTypeVIPCard // 3、会员卡 + PrizeTypeVIP // 4、VIP + PrizeTypeGoldCoinBonus // 5、金币加赠券 + PrizeTypeGoldWatch // 6、金币观影券 + PrizeTypeInKind // 7、实物奖励 + PrizeTypeAiUndress // 8、AI脱衣次数 + PrizeTypeGoldDiscountCoupon // 9、AI金币抵扣券 +) + +const ( + Turntable Location = iota + 1 // 大转盘 + LuckPrize // 幸运奖 +) + +// AppUploadReq 优惠券上传请求 +type AppUploadReq struct { + Count int64 `json:"count" bson:"count"` // 抽奖次数 + Gold int64 `json:"gold" bson:"gold"` // 抽奖花费金币. 如果使用的是免费次数, 该值为0 + Prizes []*Prize `json:"prizes" bson:"prizes"` // 奖品情况 +} + +// AppDeleteReq 优惠券删除请求 +type AppDeleteReq struct { + ID primitive.ObjectID `json:"id" bson:"id"` // 优惠券删除 +} + +func (receiver *AppDeleteReq) Filter() bson.M { + return bson.M{"_id": receiver.ID} +} + +type Prize struct { + ID primitive.ObjectID `json:"id" bson:"id,omitempty"` // 自增id + AppId int `json:"appId" bson:"appId"` // AppId + Name string `json:"name" bson:"name"` // 奖品名称 + CursorLand int `json:"cursorLand" bson:"cursorLand"` // 光标落点 + PrizeType PrizeType `json:"prizeType" bson:"prizeType"` // 奖品类型 1-金币 2-游戏币 3-会员卡 4-vip 5-金币加赠券 6-金币观影券 7-实物 + Count int32 `json:"count" bson:"count"` // 奖品数量 + Price int64 `json:"price" bson:"price"` // 奖品价值 + Sort int32 `json:"sort" bson:"sort"` // 奖品排序 + Weights int `json:"weights" bson:"weights"` // 中奖权重 + WaterLevel int64 `json:"waterLevel" bson:"waterLevel"` // 水位 + LowAttenuation int64 `json:"lowAttenuation" bson:"lowAttenuation"` // 低消 + Cistern int64 `json:"cistern" bson:"cistern"` // 蓄水池 + PrizeLocation Location `json:"prizeLocation" bson:"prizeLocation"` // 礼包位置 1-大转盘 2-幸运奖 + Status bool `json:"status" bson:"status"` // 启用/禁用 + Image string `json:"image" bson:"image"` // 图片地址 + ValidDate int `json:"validDate" bson:"validDate"` // 过期天数 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 修改时间 + CreateTime time.Time `json:"createTime" bson:"createTime"` // 创建时间 + Param1 string `json:"param1" json:"param1"` // 扩容字段 +} diff --git a/models/v/prize_record_mod/hanlder.go b/models/v/prize_record_mod/hanlder.go new file mode 100644 index 0000000..1204f6e --- /dev/null +++ b/models/v/prize_record_mod/hanlder.go @@ -0,0 +1,404 @@ +package prize_record_mod + +import ( + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/backpackmod" + "91porn-server/models/v/coupon_record_mod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + "errors" + "fmt" + "sync" + "time" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mu = sync.Mutex{} + +type PrizeRecordHandler interface { + Run(t *db.MongoTool) error + GetTransactionLog() []txnmod.TransactionLog +} + +func Run(u usermod.User, p Prize, transType txnmod.TransType) (PrizeRecordHandler, error) { + switch p.PrizeType { + case PrizeTypeGold: + // 1、金币 + return newAddGold(u, p, transType), nil + case PrizeTypeVIPCard: + // 3、会员卡 + return newAddVIPCard(u, p, transType), nil + case PrizeTypeVIP: + // 4、VIP + return newAddVIP(u, p, transType), nil + case PrizeTypeGoldWatch: + // 6、金币观影券 + return newAddGoldWatch(u, p, transType), nil + case PrizeTypeGoldCoinBonus: + // 5、金币加赠券 + return newAddGoldCoinBonus(u, p, transType), nil + case PrizeTypeInKind: + // 7、实物奖励 + return newAddInKind(u, p, transType), nil + default: + return nil, errors.New("未知类型") + } +} + +// addGold 新增用户金币 +type addGold struct { + u usermod.User + p Prize + transType txnmod.TransType + wallet *walletmod.Wallet +} + +// newAddGold 处理程序初始化 +func newAddGold(u usermod.User, p Prize, transType txnmod.TransType) PrizeRecordHandler { + return &addGold{ + u: u, + p: p, + transType: transType, + } +} + +// Run 运行 +func (h *addGold) Run(t *db.MongoTool) error { + mu.Lock() + defer mu.Unlock() + walletInfo, err := walletmod.CreditAmount(t, h.p.Price, h.u.UID) + if err != nil { + return err + } + h.wallet = walletInfo + return nil +} + +// GetTransactionLog 用户金币资金流水记录 +func (h *addGold) GetTransactionLog() (out []txnmod.TransactionLog) { + out = append(out, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: h.u.UID, + Amount: h.p.Price, + ActualAmount: float64(h.p.Price), + TranType: h.transType.Key(), + TranTypeInt: int64(h.transType), + Desc: fmt.Sprintf("%s+金币[%d个]", h.transType.Key(), h.p.Price), + RealAmount: func() decimal.Decimal { + if h.wallet == nil { + return decimal.NewFromInt(0) + } + return h.wallet.RealAmount() + }(), + }) + return +} + +// addVIPDiscount 新增用户VIP折扣 +type addVIPDiscount struct { + u usermod.User + p Prize + transType txnmod.TransType + wallet *walletmod.Wallet +} + +// newAddVIPDiscount 处理程序初始化 +func newAddVIPDiscount(u usermod.User, p Prize, transType txnmod.TransType) PrizeRecordHandler { + return &addVIPDiscount{ + u: u, + p: p, + transType: transType, + } +} + +// Run VIP折扣运行 +func (h *addVIPDiscount) Run(t *db.MongoTool) error { + mu.Lock() + defer mu.Unlock() + // 处理会员折扣 + var ( + goodsList []backpackmod.Backpack + now time.Time = time.Now() + ) + for i := 0; i < int(h.p.Count); i++ { + goodsList = append(goodsList, backpackmod.Backpack{ + UID: h.u.UID, + GoodsName: h.p.Name, + GoodsType: backpackmod.VIPDiscount, + GoodsValue: h.p.Price, + GoodsOrigin: h.transType.Key(), + GoodsDesc: h.p.Name, + Status: backpackmod.Unused, + ExpiredTime: now.AddDate(0, 0, int(h.p.ValidDate)), + CreateTime: now, + }) + } + if len(goodsList) <= 0 { + return nil + } + if err := backpackmod.AddGoodsMany(t, h.u.UID, goodsList); err != nil { + return err + } + return nil +} + +// GetTransactionLog 用户金币资金流水记录 +func (h *addVIPDiscount) GetTransactionLog() (out []txnmod.TransactionLog) { + out = append(out, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: h.u.UID, + Money: decimal.NewFromInt(h.p.Price), + TranType: h.transType.Key(), + TranTypeInt: int64(h.transType), + Desc: fmt.Sprintf("%s-棋牌金币[%d个]", h.transType.Key(), h.p.Price), + }) + return +} + +// addVIP 新增用户VIP +type addVIP struct { + u usermod.User + p Prize + transType txnmod.TransType + wallet *walletmod.Wallet +} + +// newAddVIP 处理程序初始化 +func newAddVIP(u usermod.User, p Prize, transType txnmod.TransType) PrizeRecordHandler { + return &addVIP{ + u: u, + p: p, + transType: transType, + } +} + +// Run VIP运行 +func (h *addVIP) Run(t *db.MongoTool) error { + mu.Lock() + defer mu.Unlock() + // 处理会员 + if _, err := usermod.Update(h.u.UID, usermod.UserSelector{VipExpireDate: &h.u.VipExpireDate, VipLevel: &h.u.VipLevel}); err != nil { + return err + } + return nil +} + +// GetTransactionLog 用户VIP流水记录 +func (h *addVIP) GetTransactionLog() (out []txnmod.TransactionLog) { + return +} + +// addGoldCoinBonus 新增金币加赠券 +type addGoldCoinBonus struct { + u usermod.User + p Prize + transType txnmod.TransType + wallet *walletmod.Wallet +} + +// newAddGoldCoinBonus 处理程序初始化 +func newAddGoldCoinBonus(u usermod.User, p Prize, transType txnmod.TransType) PrizeRecordHandler { + return &addGoldCoinBonus{ + u: u, + p: p, + transType: transType, + } +} + +// Run 金币加赠券运行 +func (h *addGoldCoinBonus) Run(t *db.MongoTool) error { + mu.Lock() + defer mu.Unlock() + time.Sleep(time.Duration(10) * time.Millisecond) + // 获取观影券是否存在 + cId := HandleCId(h.p.ID) + c, err := coupon_record_mod.FindOneByCId(cId) + if err != nil { + return err + } + if !c.ID.IsZero() { + return stderr.New(400, "coupon_record is exist") + } + + // 保存用户优惠券 + now := time.Now() + newCo := coupon_record_mod.CouponRecord{ + CID: cId, + PID: h.p.ID, + UID: h.u.UID, + UserName: h.u.Name, + Name: h.p.Name, + Count: h.p.Count, + Price: h.p.Price, + Type: coupon_record_mod.PrizeTypeGoldCoinBonus, + Used: false, + IsDelete: false, + ExpireTime: now.Add(time.Hour * 24 * time.Duration(h.p.ValidDate)), + } + + err = coupon_record_mod.InsertOne(newCo) + if err != nil { + return err + } + return nil +} + +// GetTransactionLog 金币观影券流水记录 +func (h *addGoldCoinBonus) GetTransactionLog() (out []txnmod.TransactionLog) { + return +} + +// addGoldWatch 新增金币观影券 +type addGoldWatch struct { + u usermod.User + p Prize + transType txnmod.TransType + wallet *walletmod.Wallet +} + +// newAddGoldWatch s处理程序初始化 +func newAddGoldWatch(u usermod.User, p Prize, transType txnmod.TransType) PrizeRecordHandler { + return &addGoldWatch{ + u: u, + p: p, + transType: transType, + } +} + +// Run 金币观影券运行 +func (h *addGoldWatch) Run(t *db.MongoTool) error { + mu.Lock() + defer mu.Unlock() + cId := HandleCId(h.p.ID) + // 获取观影券是否存在 + c, err := coupon_record_mod.FindOneByCId(cId) + if err != nil { + return err + } + if !c.ID.IsZero() { + return stderr.New(400, "coupon_record is exist") + } + + // 保存用户优惠券 + now := time.Now() + newCo := coupon_record_mod.CouponRecord{ + CID: cId, + PID: h.p.ID, + UID: h.u.UID, + UserName: h.u.Name, + Name: h.p.Name, + Count: h.p.Count, + Price: h.p.Price, + Type: coupon_record_mod.PrizeTypeGoldWatch, + Used: false, + IsDelete: false, + ExpireTime: now.Add(time.Hour * 24 * time.Duration(h.p.ValidDate)), + } + + err = coupon_record_mod.InsertOne(newCo) + if err != nil { + return err + } + return nil +} + +// GetTransactionLog 金币观影券流水记录 +func (h *addGoldWatch) GetTransactionLog() (out []txnmod.TransactionLog) { + return +} + +// 会员卡处理 +type addVIPCard struct { + u usermod.User + p Prize + transType txnmod.TransType +} + +// 处理程序初始化 +func newAddVIPCard(u usermod.User, p Prize, transType txnmod.TransType) PrizeRecordHandler { + return &addVIPCard{ + u: u, + p: p, + transType: transType, + } +} + +// Run VIP会员卡运行 +func (h *addVIPCard) Run(t *db.MongoTool) error { + mu.Lock() + defer mu.Unlock() + set := usermod.UserSelector{VipExpireDate: &h.u.VipExpireDate, VipLevel: &h.u.VipLevel, PayVidDiscount: &h.u.PayVidDiscount} + if err := usermod.UpdateUserById(t, h.u.UID, set); err != nil { + return err + } + return nil +} + +// GetTransactionLog 资金流水记录 +func (h *addVIPCard) GetTransactionLog() (out []txnmod.TransactionLog) { + return +} + +func CheckVipRenew(u *usermod.User, p *productmod.Product) (time.Time, int, int) { + now := time.Now() + end := time.Time{} + level := u.VipLevel + payVidDiscount := u.PayVidDiscount + d, _ := time.ParseDuration("24h") + d = d * time.Duration(p.Duration) + if u.VipExpireDate.After(now) { //renew + end = u.VipExpireDate.Add(d) + } else { + end = now.Add(time.Duration(d)) + } + if p.VipLevel > level { //当前用户的vip等级比这次购买的大,使用用户的 + level = p.VipLevel + } + if p.PayVidDiscount > 0 && payVidDiscount == 0 { + payVidDiscount = p.PayVidDiscount + } + if p.PayVidDiscount > 0 && p.PayVidDiscount < payVidDiscount { + payVidDiscount = p.PayVidDiscount + } + return end, level, payVidDiscount +} + +// addInKinds 派发实物 +type addInKinds struct { + u usermod.User + p Prize + transType txnmod.TransType +} + +// 处理程序初始化 +func newAddInKind(u usermod.User, p Prize, transType txnmod.TransType) PrizeRecordHandler { + return &addInKinds{ + u: u, + p: p, + transType: transType, + } +} + +// Run 具体派发流程 +func (h *addInKinds) Run(t *db.MongoTool) error { + log.Info(fmt.Sprintf("用户ID:%d;奖品ID:%v;正在派发奖品", h.u.UID, h.p.ID)) + + log.Info(fmt.Sprintf("用户ID:%d;奖品ID:%v;派发奖品成功", h.u.UID, h.p.ID)) + return nil +} + +// GetTransactionLog 派发日志记录 +func (h *addInKinds) GetTransactionLog() (out []txnmod.TransactionLog) { + return +} + +func HandleCId(id primitive.ObjectID) string { + return id.Hex() + common.UUID() +} diff --git a/models/v/prize_record_mod/type.go b/models/v/prize_record_mod/type.go new file mode 100644 index 0000000..bcbd65e --- /dev/null +++ b/models/v/prize_record_mod/type.go @@ -0,0 +1,25 @@ +package prize_record_mod + +import ( + "91porn-server/models/v/prizemod" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// PrizeRecord 用户抽奖记录 +type PrizeRecord struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档ID + ActivityID primitive.ObjectID `json:"activityId" bson:"activityId"` // 活动ID + VIPCardID primitive.ObjectID `json:"vipCardId" bson:"vipCardId"` // 会员卡ID + UID uint64 `json:"uid" bson:"uid"` // 用户ID + UserName string `json:"userName" bson:"userName"` // 用户名 + Name string `json:"name" bson:"name"` // 奖品名称 + Type prizemod.PrizeType `json:"type" bson:"type"` // 奖品类型 + Count int32 `json:"count" bson:"count"` // 奖品数量 + Price int64 `json:"price" bson:"price"` // 奖品价值 + Value int64 `json:"value" bson:"value"` // 折扣率/活跃值 + Level prizemod.PrizeLevel `json:"level" bson:"level"` // 奖品等级 + DrawPrice int64 `json:"drawPrice" bson:"drawPrice"` // 单次抽奖价格(金币) + CreateTime time.Time `json:"createTime" bson:"createTime"` // 创建时间 +} diff --git a/models/v/prizemod/hanlder.go b/models/v/prizemod/hanlder.go new file mode 100644 index 0000000..a9ad678 --- /dev/null +++ b/models/v/prizemod/hanlder.go @@ -0,0 +1,443 @@ +package prizemod + +import ( + "91porn-server/models/v/taskrecordmod" + "91porn-server/models/v/videocoupon" + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models/v/backpackmod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type PrizeHanlder interface { + Run(t *db.MongoTool) error + GetTransactionLog() []txnmod.TransactionLog +} + +func Run(uid uint64, p Prize, transType txnmod.TransType) (PrizeHanlder, error) { + switch p.Type { + case CurrentValue: + return newAddCurrentValue(uid, p, transType), nil + case Gold: + return newAddGold(uid, p, transType), nil + case VIPCard: + u, err := usermod.FindUserByUID(uid) + if err != nil { + return nil, err + } + // 查询会员卡详情 + productDetail, err := productmod.FindProduct(p.VIPCardID, "android") + if err != nil { + return nil, err + } + if productDetail == nil { + return nil, fmt.Errorf("product not found. id: %v", p.VIPCardID) + } + return newAddVIPCard(u, p, transType, productDetail), nil + case VIPDiscount: + return newAddVIPDiscount(uid, p, transType), nil + case InKind: + return nil, nil + case Integral: + return newAddIntegral(uid, p, transType), nil + case AIUndress: + return newAddAIUndress(uid, p, transType), nil + case AIChangeFace: + return newAddAIChangeFace(uid, p, transType), nil + case VideoCoupon: + return newAddVideoCoupon(uid, p, transType), nil + default: + return nil, errors.New("未知类型") + } +} + +// 新增活跃值 +type addCurrentValue struct { + uid uint64 + p Prize + transType txnmod.TransType +} + +// 处理程序初始化 +func newAddCurrentValue(uid uint64, p Prize, transType txnmod.TransType) PrizeHanlder { + return &addCurrentValue{ + uid: uid, + p: p, + transType: transType, + } +} + +// 运行 +func (h *addCurrentValue) Run(t *db.MongoTool) error { + return taskrecordmod.CurrentValueUpdate(t, h.uid, h.p.Value) +} + +// 资金流水记录 +func (h *addCurrentValue) GetTransactionLog() []txnmod.TransactionLog { + return nil +} + +// 新增金币 +type addGold struct { + uid uint64 + p Prize + transType txnmod.TransType + wallet *walletmod.Wallet +} + +// 处理程序初始化 +func newAddGold(uid uint64, p Prize, transType txnmod.TransType) PrizeHanlder { + return &addGold{ + uid: uid, + p: p, + transType: transType, + } +} + +// 运行 +func (h *addGold) Run(t *db.MongoTool) error { + walletInfo, err := walletmod.CreditAmount(t, h.p.Price, h.uid) + if err != nil { + return err + } + h.wallet = walletInfo + return nil +} + +// 资金流水记录 +func (h *addGold) GetTransactionLog() []txnmod.TransactionLog { + return []txnmod.TransactionLog{txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: h.uid, + Amount: h.p.Price, + ActualAmount: float64(h.p.Price), + TranType: h.transType.Key(), + TranTypeInt: int64(h.transType), + Desc: fmt.Sprintf("%s-金币[%d个]", h.transType.Key(), h.p.Price), + RealAmount: h.wallet.RealAmount(), + }} +} + +// 会员卡处理 +type addVIPCard struct { + u *usermod.User + p Prize + transType txnmod.TransType + productDetail *productmod.Product +} + +// 处理程序初始化 +func newAddVIPCard(u *usermod.User, p Prize, transType txnmod.TransType, productDetail *productmod.Product) PrizeHanlder { + return &addVIPCard{ + u: u, + p: p, + transType: transType, + productDetail: productDetail, + } +} + +// 运行 +func (h *addVIPCard) Run(t *db.MongoTool) error { + vipExpire, vipLevel, payVidDiscount := checkVipRenew(h.u, h.productDetail) + sel := usermod.UserSelector{VipExpireDate: &vipExpire, VipLevel: &vipLevel, PayVidDiscount: &payVidDiscount} + if h.productDetail.GoldVideoFreeDay > 0 { + expire := time.Time{} + if h.u.GoldVideoFreeExpire.IsZero() || h.u.GoldVideoFreeExpire.Before(time.Now()) { + expire = time.Now().AddDate(0, 0, h.productDetail.GoldVideoFreeDay) + } else { + expire = h.u.GoldVideoFreeExpire.AddDate(0, 0, h.productDetail.GoldVideoFreeDay) + } + sel.GoldVideoFreeExpire = &expire + } + + if err := usermod.UpdateVIP(t, h.u.UID, h.u.VipExpireDate, sel); err != nil { + return err + } + return nil +} + +// 资金流水记录 +func (h *addVIPCard) GetTransactionLog() (out []txnmod.TransactionLog) { + return []txnmod.TransactionLog{txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: h.u.UID, + Amount: h.p.Price, + ActualAmount: float64(h.p.Price), + TranType: h.transType.Key(), + TranTypeInt: int64(h.transType), + Desc: fmt.Sprintf("%s-VIP[%d天]", h.transType.Key(), h.productDetail.Duration), + }} +} + +func checkVipRenew(u *usermod.User, p *productmod.Product) (time.Time, int, int) { + now := time.Now() + var end time.Time + level := u.VipLevel + payVidDiscount := u.PayVidDiscount + d := time.Hour * 24 * time.Duration(p.Duration) + if u.VipExpireDate.After(now) { //renew + end = u.VipExpireDate.Add(d) + } else { + end = now.Add(time.Duration(d)) + } + if p.VipLevel > level { //当前用户的vip等级比这次购买的大,使用用户的 + level = p.VipLevel + } + if p.PayVidDiscount > 0 && p.PayVidDiscount < payVidDiscount { + payVidDiscount = p.PayVidDiscount + } + return end, level, payVidDiscount +} + +// 会员折扣卷处理 +type addVIPDiscount struct { + uid uint64 + p Prize + transType txnmod.TransType +} + +// 处理程序初始化 +func newAddVIPDiscount(uid uint64, p Prize, transType txnmod.TransType) PrizeHanlder { + return &addVIPDiscount{ + uid: uid, + p: p, + transType: transType, + } +} + +// 运行 +func (h *addVIPDiscount) Run(t *db.MongoTool) error { + goodsList := make([]backpackmod.Backpack, h.p.Count) + now := time.Now() + for i := 0; i < int(h.p.Count); i++ { + goodsList[i] = backpackmod.Backpack{ + UID: h.uid, + GoodsName: h.p.Name, + GoodsType: backpackmod.VIPDiscount, + GoodsValue: h.p.Value, + GoodsOrigin: h.transType.Key(), + GoodsDesc: h.p.Desc, + Status: backpackmod.Unused, + ExpiredTime: now.AddDate(0, 0, int(h.p.ValidityTime)), + CreateTime: now, + } + } + return backpackmod.AddGoodsMany(t, h.uid, goodsList) +} + +// 资金流水记录 +func (h *addVIPDiscount) GetTransactionLog() []txnmod.TransactionLog { + return nil +} + +// addIntegral 新增积分 +type addIntegral struct { + uid uint64 + p Prize + transType txnmod.TransType + wallet *walletmod.Wallet +} + +// newAddIntegral 处理程序初始化 +func newAddIntegral(uid uint64, p Prize, transType txnmod.TransType) PrizeHanlder { + return &addIntegral{ + uid: uid, + p: p, + transType: transType, + } +} + +// Run 运行 +func (h *addIntegral) Run(t *db.MongoTool) error { + walletInfo, err := walletmod.CreditIntegral(t, h.p.Price, h.uid) + if err != nil { + return err + } + h.wallet = walletInfo + return nil +} + +// GetTransactionLog 资金流水记录 +func (h *addIntegral) GetTransactionLog() []txnmod.TransactionLog { + return []txnmod.TransactionLog{txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: h.uid, + Integral: h.p.Price, + ActualIntegral: float64(h.p.Price), + TranType: h.transType.Key(), + TranTypeInt: int64(h.transType), + Desc: fmt.Sprintf("%s-积分[%d个]", h.transType.Key(), h.p.Price), + RealIntegral: h.wallet.RealIntegral(), + }} +} + +// --------------------------- +// addAIUndress 新增ai脱衣次数 +type addAIUndress struct { + uid uint64 + p Prize + transType txnmod.TransType + wallet *walletmod.Wallet +} + +// newAddAIUndress 处理程序初始化 +func newAddAIUndress(uid uint64, p Prize, transType txnmod.TransType) PrizeHanlder { + return &addAIUndress{ + uid: uid, + p: p, + transType: transType, + } +} + +// Run 运行 +func (h *addAIUndress) Run(t *db.MongoTool) error { + count := int64(h.p.Count) + price := h.p.Price + aiUndressFreeTimes := count * price + p := walletmod.CreditPlan{} + p.AiUndressFreeTimes = &aiUndressFreeTimes + walletInfo, err := walletmod.Credit(t, p, h.uid) + if err != nil { + return err + } + h.wallet = walletInfo + return nil +} + +// GetTransactionLog 资金流水记录 +func (h *addAIUndress) GetTransactionLog() []txnmod.TransactionLog { + return []txnmod.TransactionLog{txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: h.uid, + Amount: int64(h.p.Count), + ActualAmount: float64(h.p.Count), + TranType: h.transType.Key(), + TranTypeInt: int64(h.transType), + Desc: fmt.Sprintf("%s-AI脱衣次数[%v次]", h.transType.Key(), h.p.Count), + RealAmount: h.wallet.RealAmount(), + }} +} + +// --------------------------- +// addAIChangeFace 新增ai换脸次数 +type addAIChangeFace struct { + uid uint64 + p Prize + transType txnmod.TransType + wallet *walletmod.Wallet +} + +// newAddAIChangeFace 处理程序初始化 +func newAddAIChangeFace(uid uint64, p Prize, transType txnmod.TransType) PrizeHanlder { + return &addAIChangeFace{ + uid: uid, + p: p, + transType: transType, + } +} + +// Run 运行 +func (h *addAIChangeFace) Run(t *db.MongoTool) error { + count := int64(h.p.Count) + price := h.p.Price + aiChangeFaceImgFreeTimes := count * price + p := walletmod.CreditPlan{} + p.AiUndressFreeTimes = &aiChangeFaceImgFreeTimes + walletInfo, err := walletmod.Credit(t, p, h.uid) + if err != nil { + return err + } + h.wallet = walletInfo + return nil +} + +// GetTransactionLog 资金流水记录 +func (h *addAIChangeFace) GetTransactionLog() []txnmod.TransactionLog { + return []txnmod.TransactionLog{txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: h.uid, + Amount: int64(h.p.Count), + ActualAmount: float64(h.p.Count), + TranType: h.transType.Key(), + TranTypeInt: int64(h.transType), + Desc: fmt.Sprintf("%s-AI图片换脸次数[%v次]", h.transType.Key(), h.p.Count), + RealAmount: h.wallet.RealAmount(), + }} +} + +// --------------------------- +// addVideoCoupon 新增观影券 +type addVideoCoupon struct { + uid uint64 + p Prize + transType txnmod.TransType + wallet *walletmod.Wallet +} + +// newAddVideoCoupon 处理程序初始化 +func newAddVideoCoupon(uid uint64, p Prize, transType txnmod.TransType) PrizeHanlder { + return &addVideoCoupon{ + uid: uid, + p: p, + transType: transType, + } +} + +// Run 运行 +func (h *addVideoCoupon) Run(t *db.MongoTool) error { + now := time.Now() + for i := 0; i < int(h.p.Count); i++ { + videoCoupon := videocoupon.UserGoldVideoCoupon{ + UID: h.uid, + Num: int(h.p.Price), + Used: false, + Source: videocoupon.GoldVideoCouponSourceSign, + UpdatedAt: now, + CreatedAt: now, + } + if err := videocoupon.InsertOne(videoCoupon); err != nil { + continue + } + backpack := backpackmod.Backpack{ + UID: h.uid, + GoodsType: backpackmod.SIGNCoupon, + GoodsValue: int64(videoCoupon.Num), + GoodsOrigin: string(videocoupon.GoldVideoCouponSourceSign), + Status: backpackmod.Unused, + ExpiredTime: now.AddDate(0, 0, 7), + CreateTime: now, + } + err := backpackmod.AddGoods(nil, h.uid, backpack) + if err != nil { + continue + } + } + walletInfo, err := walletmod.GetWallet(h.uid) + if err != nil { + return err + } + h.wallet = walletInfo + return nil +} + +// GetTransactionLog 资金流水记录 +func (h *addVideoCoupon) GetTransactionLog() []txnmod.TransactionLog { + return []txnmod.TransactionLog{txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: h.uid, + Amount: int64(h.p.Count), + ActualAmount: float64(h.p.Count), + TranType: h.transType.Key(), + TranTypeInt: int64(h.transType), + Desc: fmt.Sprintf("%s-观影券[面额%v,%d张]", h.transType.Key(), h.p.Price, h.p.Count), + RealAmount: h.wallet.RealAmount(), + }} +} diff --git a/models/v/prizemod/prize.go b/models/v/prizemod/prize.go new file mode 100644 index 0000000..56071f4 --- /dev/null +++ b/models/v/prizemod/prize.go @@ -0,0 +1,205 @@ +package prizemod + +import ( + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(models.ActivityPrize) + } + return t.Coll(models.ActivityPrize) +} + +// initIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "activityId", Value: 1}, {Key: "status", Value: 1}, {Key: "sort", Value: -1}}, + }, + { + Keys: bson.D{{Key: "activityId", Value: 1}, {Key: "status", Value: 1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}, {Key: "type", Value: 1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}}, + }, + { + Keys: bson.D{{Key: "type", Value: 1}}, + }, + { + Keys: bson.D{{Key: "level", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createTime", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", models.ActivityPrize, err)) + } +} + +// 获取指定活动奖品列表 +func GetPrizeListByActivityID(id primitive.ObjectID) ([]*Prize, error) { + out := []*Prize{} + if err := coll(nil).Find(&out, bson.M{"activityId": id, "status": true}, options.Find(). + SetSort(bson.M{"sort": 1})); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetPrizeListByActivityID", models.ActivityPrize, "Find", err), + log.Any("id", id), + ) + return nil, err + } + return out, nil +} + +// 获取指定活动大奖列表 +func GetHighPrizeListByActivityID(id primitive.ObjectID) ([]*Prize, error) { + out := []*Prize{} + if err := coll(nil).Find(&out, bson.M{"activityId": id, "status": true, "level": High}, options.Find(). + SetSort(bson.M{"sort": 1})); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetPrizeListByActivityID", models.ActivityPrize, "Find", err), + log.Any("id", id), + ) + return nil, err + } + return out, nil +} + +// 获取优惠卷列表 +func GetCouponList(couponType int) ([]*Prize, error) { + out := []*Prize{} + if err := coll(nil).Find(&out, bson.M{"status": true, "type": couponType}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCouponList", models.ActivityPrize, "Find", err), + log.Any("couponType", couponType), + ) + return nil, err + } + return out, nil +} + +// 通过ID获取奖品列表 +func GetPrizeListByIDs(ids []primitive.ObjectID) ([]*Prize, error) { + out := make([]*Prize, 0) + if len(ids) == 0 { + return out, nil + } + if err := coll(nil).Find(&out, bson.M{"_id": bson.M{"$in": ids}, "status": true}, options.Find().SetSort(bson.M{"type": 1})); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByUID", models.ActivityPrize, "Find", err), + log.Any("ids", ids), + ) + return nil, err + } + return out, nil +} + +// 获取所有开启的奖品 +func GetPrizeList() (map[primitive.ObjectID]Prize, error) { + out := make([]Prize, 0) + if err := coll(nil).Find(&out, bson.M{"status": true}, options.Find()); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetPrizeList", models.ActivityPrize, "Find", err)) + return nil, err + } + var m map[primitive.ObjectID]Prize = make(map[primitive.ObjectID]Prize, len(out)) + for _, v := range out { + m[v.ID] = v + } + return m, nil +} + +// 新增奖品 +func AddPrize(in AddPrizeCond) (interface{}, error) { + result, err := coll(nil).InsertOne(&Prize{ + ActivityID: in.ActivityID, + VIPCardID: in.VIPCardID, + Name: in.Name, + Type: in.Type, + Price: in.Price, + Count: in.Count, + Value: in.Value, + Desc: in.Desc, + Weights: in.Weights, + Level: in.Level, + Status: false, + ValidityTime: in.ValidityTime, + Image: in.Image, + Sort: in.Sort, + UpdateTime: time.Now(), + CreateTime: time.Now(), + }) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AddPrize", models.ActivityPrize, "InsertOne", err), + log.Any("in", in), + ) + return nil, err + } + return result, nil +} + +// 修改活动奖品 +func ModifyPrize(filter, update primitive.M) error { + result, err := coll(nil).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ModifyPrize", models.ActivityPrize, "UpdateOne", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + if result.ModifiedCount == 0 { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail", "ModifyPrize", models.ActivityPrize, "result.ModifiedCount == 0"), + log.Any("filter", filter), + log.Any("update", update), + ) + return errors.New("activity UpdateOne ModifiedCount err") + } + return nil +} + +// 查询活动奖品列表 +func QueryAllPrize(filter primitive.M, opts ...*options.FindOptions) ([]*Prize, error) { + out := []*Prize{} + if err := coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllPrize", models.ActivityPrize, "Find", err), + log.Any("filter", filter), + ) + return nil, err + } + return out, nil +} + +// CountPrize 查询奖品总条数 +func CountPrize(filter primitive.M) (int64, error) { + count, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountPrize", models.ActivityPrize, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } + return count, nil +} + +// QueryDropDownBox 奖品下拉框 +func QueryDropDownBox() ([]*Prize, error) { + out := []*Prize{} + if err := coll(nil).Find(&out, bson.M{"status": true}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryDropDownBox", models.ActivityPrize, "Find", err)) + return nil, err + } + return out, nil +} diff --git a/models/v/prizemod/type.go b/models/v/prizemod/type.go new file mode 100644 index 0000000..a80ce86 --- /dev/null +++ b/models/v/prizemod/type.go @@ -0,0 +1,64 @@ +package prizemod + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type PrizeType int + +const ( + CurrentValue PrizeType = iota + 1 // 活跃值 + Gold // 金币 + _ // - + _ // - + VIPCard // 会员卡 + VIPDiscount // 会员折扣卷 + InKind // 实物 + Integral // 积分 + AIUndress // ai 脱衣 + AIChangeFace // ai 换脸 + VideoCoupon // 观影券 +) + +type PrizeLevel int + +const ( + Low PrizeLevel = iota + 1 // 初级 --- 免费可获取 + Middle // 中级 --- 放分可获取 + High // 高级 --- 奖池流水达到一定条件可获取 +) + +type Prize struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档ID + ActivityID primitive.ObjectID `json:"activityId" bson:"activityId"` // 活动ID + VIPCardID primitive.ObjectID `json:"vipCardId" bson:"vipCardId"` // 会员卡ID + Name string `json:"name" bson:"name"` // 奖品名称 + Type PrizeType `json:"type" bson:"type"` // 奖品类型 + Count int32 `json:"count" bson:"count"` // 奖品数量 + Price int64 `json:"price" bson:"price"` // 奖品价值 + Sort int32 `json:"sort" bson:"sort"` // 奖品排序 + Desc string `json:"desc" bson:"desc"` // 奖品描述 + Value int64 `json:"value" bson:"value"` // 折扣率/活跃值 + Weights decimal.Decimal `json:"weights" bson:"weights"` // 中奖权重 + Level PrizeLevel `json:"level" bson:"level"` // 奖品等级 + Status bool `json:"status" bson:"status"` // 启用/禁用 + ValidityTime int64 `json:"validityTime" bson:"validityTime"` // 奖品有效期 单位: 天 + Image string `json:"image" bson:"image"` // 图片地址 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 修改时间 + CreateTime time.Time `json:"createTimt" bson:"createTimt"` // 创建时间 +} + +func (p *Prize) GetPrice() int64 { + return p.Price * int64(p.Count) +} + +func Init() { + mdb = db.Init(models.ActivityPrize) + initIndex() +} diff --git a/models/v/prizemod/web.go b/models/v/prizemod/web.go new file mode 100644 index 0000000..285c95b --- /dev/null +++ b/models/v/prizemod/web.go @@ -0,0 +1,127 @@ +package prizemod + +import ( + "time" + + "91porn-server/models/commod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// 新增活动奖品 +type AddPrizeCond struct { + ActivityID primitive.ObjectID `json:"activityId"` // 活动ID + VIPCardID primitive.ObjectID `json:"vipCardId"` // 会员卡ID + Name string `json:"name" binding:"required"` // 奖品名称 + Type PrizeType `json:"type" binding:"required"` // 奖品类型 + Count int32 `json:"count"` // 奖品数量 + Price int64 `json:"price"` // 奖品价值 + Sort int32 `json:"sort"` // 奖品排序 + Desc string `json:"desc"` // 奖品描述 + Value int64 `json:"value"` // 折扣率/活跃值 + Weights decimal.Decimal `json:"weights"` // 中奖权重 + Level PrizeLevel `json:"level"` // 奖品等级 + Status bool `json:"status"` // 启用/禁用 + ValidityTime int64 `json:"validityTime"` // 奖品有效期 单位: 天 + Image string `json:"image"` // 图片地址 +} + +// 修改活动奖品 +type ModifyPrizeCond struct { + ID primitive.ObjectID `json:"id" binding:"required"` // 文档ID + ActivityID *primitive.ObjectID `json:"activityId"` // 活动ID + VIPCardID *primitive.ObjectID `json:"vipCardId"` // 会员卡ID + Name *string `json:"name"` // 奖品名称 + Type *PrizeType `json:"type"` // 奖品类型 + Count *int32 `json:"count"` // 奖品数量 + Price *int64 `json:"price"` // 奖品价值 + Sort *int32 `json:"sort"` // 奖品排序 + Desc *string `json:"desc"` // 奖品描述 + Value *int64 `json:"value"` // 折扣率/活跃值 + Weights *decimal.Decimal `json:"weights"` // 中奖权重 + Level *PrizeLevel `json:"level"` // 奖品等级 + Status *bool `json:"status"` // 启用/禁用 + ValidityTime *int64 `json:"validityTime"` // 奖品有效期 单位: 天 + Image *string `json:"image"` // 图片地址 +} + +func (m *ModifyPrizeCond) Cond() primitive.M { + return bson.M{"_id": m.ID} +} + +func (m *ModifyPrizeCond) Bson() primitive.M { + set := bson.M{"updatedAt": time.Now()} + if m.ActivityID != nil { + set["activityId"] = m.ActivityID + } + if m.VIPCardID != nil { + set["vipCardId"] = m.VIPCardID + } + if m.Name != nil { + set["name"] = m.Name + } + if m.Type != nil { + set["type"] = m.Type + } + if m.Price != nil { + set["price"] = m.Price + } + if m.Value != nil { + set["value"] = m.Value + } + if m.Count != nil { + set["count"] = m.Count + } + if m.Sort != nil { + set["sort"] = m.Sort + } + if m.Desc != nil { + set["desc"] = m.Desc + } + if m.Weights != nil { + set["weights"] = m.Weights + } + if m.Level != nil { + set["level"] = m.Level + } + if m.Status != nil { + set["status"] = m.Status + } + if m.ValidityTime != nil { + set["validityTime"] = m.ValidityTime + } + if m.Image != nil { + set["image"] = m.Image + } + return bson.M{"$set": set} +} + +// 查询活动奖品列表 +type QueryAllPrizeCond struct { + Status *bool `form:"status"` // 状态---启用/禁用 + Type *PrizeType `form:"type"` // 奖品类型 + ActivityID *string `form:"activityId"` // 活动ID + commod.Page +} + +func (q *QueryAllPrizeCond) Options() *options.FindOptions { + return options.Find().SetLimit(int64(q.PageSize)).SetSkip(int64((q.PageNumber - 1) * q.PageSize)).SetSort(bson.M{"createdAt": -1}) +} + +func (q *QueryAllPrizeCond) Query() primitive.M { + fliter := bson.M{} + if q.Status != nil { + fliter["status"] = q.Status + } + if q.Type != nil { + fliter["type"] = q.Type + } + if q.ActivityID != nil { + id, _ := primitive.ObjectIDFromHex(*q.ActivityID) + fliter["activityId"] = id + } + return fliter +} diff --git a/models/v/productbenefitmod/productBenefit.go b/models/v/productbenefitmod/productBenefit.go new file mode 100644 index 0000000..0cc1d44 --- /dev/null +++ b/models/v/productbenefitmod/productBenefit.go @@ -0,0 +1,136 @@ +package productbenefitmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.ProductBenefit + +// InitIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "privilege", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// FindProductBenefit 查询 +func FindProductBenefit(pageNum, pageSize uint64) (p []ProductBenefit, err error) { + opts := (&options.FindOptions{}). + SetSort(bson.D{{Key: "sortCode", Value: -1}}). + SetSkip(int64((pageNum - 1) * pageSize)). + SetLimit(int64(pageSize)) + var back []ProductBenefit + if err = coll(nil).Find(&back, nil, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProductBenefit", table, "Find", err)) + return nil, err + } + return back, nil +} + +// FindAllProductBenefit 查询 +func FindAllProductBenefit() (p []ALlProductBenefit, err error) { + opts := (&options.FindOptions{}). + SetSort(bson.D{{Key: "sortCode", Value: -1}}) + var back []ALlProductBenefit + if err = coll(nil).Find(&back, nil, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProductBenefit", table, "Find", err)) + return nil, err + } + return back, nil +} + +// FindProductBenefitByPrivilege 根据privilege查询 +func FindProductBenefitByPrivilege(privileges *[]int) (p []ProductBenefit, err error) { + opts := options.Find().SetSort(bson.D{{"sortCode", -1}}) + var back []ProductBenefit + if err = coll(nil).Find(&back, bson.M{"privilege": bson.M{"$in": privileges}}, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProductBenefit", table, "Find", err)) + return nil, err + } + return back, nil +} + +// InsertProductBenefit 插入一条数据 +func InsertProductBenefit(p *ProductBenefit) (err error) { + p.CreatedAt = time.Now() + if _, err = coll(nil).InsertOne(p); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertProductBenefit", table, "InsertOne", err)) + } + return +} + +// UpdateProductBenefit 修改Product类型 +func UpdateProductBenefit(id string, set *ProductBenefitSelector) (err error) { + set.UpdatedAt = time.Now() + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateProductBenefit", table, "ObjectIDFromHex", err), + log.Any("id", id), + log.Any("set", set), + ) + return + } + if _, err = coll(nil).UpdateOne(bson.M{"_id": oid}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateProductBenefit", table, "UpdateOne", err), + log.Any("oid", oid), + log.Any("set", set), + ) + } + return +} + +// RemoveProductBenefit 删除Product类型 +func RemoveProductBenefit(id string) (err error) { + OID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RemoveProductBenefit", table, "ObjectIDFromHex", err), + log.Any("id", id), + ) + return + } + if _, err = coll(nil).DeleteOne(bson.M{"_id": OID}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RemoveProductBenefit", table, "DeleteOne", err), + log.Any("OID", OID), + ) + } + return +} + +func FindOne(t *db.MongoTool, cond bson.M, opts ...*options.FindOneOptions) (data *ProductBenefit, err error) { + if err = coll(nil).FindOne(&data, cond, opts...); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOne", table, "FindOne", err), + log.Any("cond", cond), + ) + return + } + return +} diff --git a/models/v/productbenefitmod/type.go b/models/v/productbenefitmod/type.go new file mode 100644 index 0000000..0c0589d --- /dev/null +++ b/models/v/productbenefitmod/type.go @@ -0,0 +1,29 @@ +package productbenefitmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +// ProductBenefit 商品权益 +type ProductBenefit struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` // id + Img string `bson:"img" json:"img"` // 图标 + Privilege int `bson:"privilege" json:"privilege"` // 特权 + PrivilegeName string `bson:"privilegeName" json:"privilegeName"` // 特权名称 + PrivilegeDesc string `bson:"privilegeDesc" json:"privilegeDesc"` // 特权描述 + IsCore bool `bson:"isCore" json:"isCore"` // 是否核心特权 + SortCode int `bson:"sortCode" json:"sortCode"` // 排序 + UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"` // 更新时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/productbenefitmod/web.go b/models/v/productbenefitmod/web.go new file mode 100644 index 0000000..2cdb1a2 --- /dev/null +++ b/models/v/productbenefitmod/web.go @@ -0,0 +1,46 @@ +package productbenefitmod + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "time" + + "91porn-server/models/commod" +) + +// ProductBenefitSelector 修改结构体 +type ProductBenefitSelector struct { + ID string `form:"id" json:"id"` + Img *string `bson:"img,omitempty" json:"img,omitempty" ` // 图标 + PrivilegeName *string `bson:"privilegeName,omitempty" json:"privilegeName,omitempty"` // 特权名称 + PrivilegeDesc *string `bson:"privilegeDesc,omitempty" json:"privilegeDesc,omitempty"` // 特权描述 "特权1,特权2" + Privilege *int `bson:"privilege,omitempty" json:"privilege,omitempty"` // 特权 + IsCore *bool `bson:"isCore,omitempty" json:"isCore,omitempty"` // 是否核心特权 + SortCode *int `bson:"sortCode" json:"sortCode"` // 排序 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +type ProductWeb struct { + Img string `bson:"img" json:"img" ` //背景图 + PrivilegeName string `bson:"privilegeName" json:"privilegeName"` //特权名称 + PrivilegeDesc string `bson:"privilegeDesc" json:"privilegeDesc"` //特权描述 "特权1,特权2" + Privilege int `bson:"privilege" json:"privilege"` //特权 + SortCode int `bson:"sortCode" json:"sortCode"` // 排序 + UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +type ProductBenefitReq struct { + commod.Page +} + +// ProductBenefitDel 修改结构体 +type ProductBenefitDel struct { + ID string `form:"id" json:"id"` +} + +// ALlProductBenefit 商品权益 +type ALlProductBenefit struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` // id + Privilege int `bson:"privilege" json:"privilege"` // 特权 + PrivilegeName string `bson:"privilegeName" json:"privilegeName"` // 特权名称 +} diff --git a/models/v/productmod/app.go b/models/v/productmod/app.go new file mode 100644 index 0000000..364df6a --- /dev/null +++ b/models/v/productmod/app.go @@ -0,0 +1,16 @@ +package productmod + +type VipPositionList struct { + ShowType int64 `json:"showType"` //展示样式 + Position string `json:"position"` + List []Product `json:"list"` +} + +type RespVipInfoList struct { + List []ProductList `json:"list"` +} + +type ProductList struct { + ID string `bson:"id" json:"id"` // 会员卡ID + Name string `bson:"productName" json:"productName" ` // 会员卡名字 +} diff --git a/models/v/productmod/product.go b/models/v/productmod/product.go new file mode 100644 index 0000000..fa8fa7f --- /dev/null +++ b/models/v/productmod/product.go @@ -0,0 +1,322 @@ +package productmod + +import ( + "fmt" + "strings" + "time" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.Product + +// InitIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "vipLevel", Value: 1}}, + }, + { + Keys: bson.D{{Key: "productType", Value: 1}}, + }, + { + Keys: bson.D{{Key: "discountedPrice", Value: 1}}, + }, + { + Keys: bson.D{{Key: "productName", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func (this *Product) DiscountedPriceFill(sys string) { + sys = common.HandleSysType(sys) + if strings.Contains(sys, constant.SysTypeIOS) { + this.DiscountedPrice = *this.DiscountedPriceIos + } else { + this.DiscountedPrice = *this.DiscountedPriceAnd + } +} + +// FindProduct 查询 +func FindProduct(id primitive.ObjectID, sys string) (p *Product, err error) { + if err = coll(nil).FindOne(&p, bson.M{"_id": id}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProduct", table, "FindOne", err), log.Any("id", id)) + return + } + if p == nil || p.ID.IsZero() { + log.Info("product FindProduct not found", log.Any("id", id)) + return nil, nil + } + p.DiscountedPriceFill(sys) + return +} + +// FindByProductType 查询 +func FindByProductType(productType commod.ProductType) (data []Product, err error) { + data = make([]Product, 0) + opts := options.FindOptions{Sort: bson.D{{Key: "sort", Value: 1}}} + if err = coll(nil).Find(&data, bson.M{"productType": productType, "status": true}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProduct", table, "FindOne", err), log.Any("productType", productType)) + return + } + return +} + +// FindByProductTypes 查询 +func FindByProductTypes(productTypes []commod.ProductType) (data []Product, err error) { + data = make([]Product, 0) + opts := options.FindOptions{Sort: bson.D{{Key: "sort", Value: 1}}} + if err = coll(nil).Find(&data, bson.M{"productType": bson.M{"$in": productTypes}, "status": true}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProduct", table, "Find", err), log.Any("productType", productTypes)) + return + } + return +} + +// FindActiveDramaCards 返回配置了短剧权益天数的在售商品。 +func FindActiveDramaCards() (data []Product, err error) { + opts := options.Find().SetSort(bson.D{{Key: "sort", Value: 1}, {Key: "_id", Value: 1}}) + err = coll(nil).Find(&data, bson.M{"status": true, "dramaDays": bson.M{"$gt": 0}}, opts) + return +} + +// FindByVipLevel 查询 +func FindByVipLevel(vipLevel int) (data []Product, err error) { + data = make([]Product, 0) + if err = coll(nil).Find(&data, bson.M{"vipLevel": vipLevel, "status": true}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindByVipLevel", table, "FindOne", err), log.Any("vipLevel", vipLevel)) + return + } + return +} + +func FindByProductIDs(ids []primitive.ObjectID) ([]Product, error) { + var data []Product + return data, coll(nil).Find(&data, bson.M{"_id": bson.M{"$in": ids}}) +} + +// InsertProduct 插入一条数据 +func InsertProduct(p *Product) error { + p.CreatedAt = time.Now() + if _, err := coll(nil).InsertOne(p); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertProduct", table, "InsertOne", err)) + return err + } + return nil +} + +// UpdateProduct 修改Product类型 +func UpdateProduct(id string, set *ProductSelector) error { + set.UpdatedAt = time.Now() + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateProduct", table, "ObjectIDFromHex", err), + log.Any("id", id), + log.Any("set", set), + ) + return err + } + if _, err = coll(nil).UpdateOne(bson.M{"_id": oid}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateProduct", table, "UpdateOne", err), + log.Any("oid", oid), + log.Any("set", set), + ) + return err + } + return nil +} + +// UpdateProductRights 清空产品权益 +func UpdateProductRights(id string) error { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateProduct", table, "ObjectIDFromHex", err), + log.Any("id", id), + ) + return err + } + if _, err := coll(nil).UpdateOne(bson.M{"_id": oid}, bson.M{"$set": bson.M{"newPrivilege": nil}}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateProduct", table, "UpdateOne", err), + log.Any("oid", oid), + ) + return err + } + return nil +} + +// RemoveProduct 删除Product类型 +func RemoveProduct(id string) error { + OID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RemoveProduct", table, "ObjectIDFromHex", err), + log.Any("id", id), + ) + return err + } + if _, err = coll(nil).DeleteOne(bson.M{"_id": OID}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RemoveProduct", table, "DeleteOne", err), + log.Any("OID", OID), + ) + return err + } + return nil +} + +// FindStd 查询vip和约会卡 +func FindStd(req ProductListWeb) ([]ProductWeb, error) { + query := bson.M{} + if req.Status != nil { + query["status"] = req.Status + } + if req.ProductType != nil { + query["productType"] = req.ProductType + } + opts := options.FindOptions{ + Sort: bson.D{{Key: "sort", Value: 1}}, + } + if req.ProductType == nil { + query["productType"] = bson.M{"$in": []commod.ProductType{GameAdvanceCard, AdvanceCard, VIP, MeetingCard, OTHER, NewUser, PHYSICALGOODS, WhoringCard, commod.VideoDiscount, commod.VideoFreeCard, commod.CoinMonthCard}} + } + data := make([]ProductWeb, 0) + if err := coll(nil).Find(&data, query, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProductsByProductType", table, "Find", err)) + return data, err + } + return data, nil +} + +// FindProductsByProductType 查询vip和约会卡 +func FindProductsByProductType(status *bool, newUser bool, sys string) ([]Product, error) { + query := bson.M{} + if status != nil { + query["status"] = *status + } + types := []commod.ProductType{VIP, MeetingCard, OTHER, PHYSICALGOODS, VideoDiscount, VideoFreeCard, CoinMonthCard, AdvanceCard, GameAdvanceCard, WhoringCard} + if newUser { + types = append(types, NewUser) + } + opts := options.FindOptions{ + Sort: bson.D{{Key: "sort", Value: 1}}, + } + query["productType"] = bson.M{"$in": types} + data := make([]Product, 0) + if err := coll(nil).Find(&data, query, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProductsByProductType", table, "Find", err)) + return data, err + } + for i := range data { + data[i].DiscountedPriceFill(sys) + } + return data, nil +} + +func FindOne(t *db.MongoTool, cond bson.M, opts ...*options.FindOneOptions) (data *Product, err error) { + if err = coll(nil).FindOne(&data, cond, opts...); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOne", table, "FindOne", err), + log.Any("cond", cond), + ) + return + } + return +} + +// ListToIDs 获取商品列表 +func ListToIDs(ids []primitive.ObjectID, sys string) ([]*Product, error) { + var items []*Product + if err := coll(nil).Find(&items, bson.M{"_id": bson.M{"$in": ids}}); err != nil { + return nil, err + } + for i := range items { + items[i].DiscountedPriceFill(sys) + } + return items, nil +} + +func ListByIDsMap(ids []primitive.ObjectID) (map[primitive.ObjectID]*Product, error) { + var m = make(map[primitive.ObjectID]*Product, 0) + list, err := ListToIDs(ids, constant.SysTypeIOS) + if err != nil { + return nil, err + } + for _, product := range list { + m[product.ID] = product + } + return m, nil +} + +// CheckboxByType 通过类型获取商品复选框列表 +func CheckboxByType(t commod.ProductType) ([]*ProductCheckbox, error) { + var items []*ProductCheckbox + return items, coll(nil).Find(&items, bson.M{"productType": t}) +} + +// FindMany 查询vip和约会卡 +func FindMany() ([]ProductWeb, error) { + query := bson.M{} + opts := options.FindOptions{ + Sort: bson.D{{Key: "sort", Value: 1}}, + } + data := make([]ProductWeb, 0) + if err := coll(nil).Find(&data, query, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProductsByProductType", table, "Find", err)) + return data, err + } + return data, nil +} + +// GetLargestDiscountVIPCard 获取最大折扣VIP卡(PayVidDiscount越小表示折扣力度越大) +func GetLargestDiscountVIPCard() (int, error) { + sort := bson.D{{Key: "payVidDiscount", Value: 1}} + filter := bson.M{ + "productType": commod.VIP, + "status": true, + "payVidDiscount": bson.M{ + "$gt": 0, + }, + } + opts := options.FindOptions{} + opts.SetSort(sort).SetLimit(1) + discounts := []Product{} + if err := coll(nil).Find(&discounts, filter, &opts); err != nil { + return 0, err + } + if len(discounts) <= 0 { + return 0, nil + } + return discounts[0].PayVidDiscount, nil +} + +//func GetRecommendVip() (data *Product, err error) { +// return FindOne(nil, bson.M{ +// "isRecommend": true, +// "status": true, +// }) +//} diff --git a/models/v/productmod/type.go b/models/v/productmod/type.go new file mode 100644 index 0000000..826da3f --- /dev/null +++ b/models/v/productmod/type.go @@ -0,0 +1,196 @@ +package productmod + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + VIP = commod.VIP // product 0 + VIDEO = commod.VIDEO // VIDEO视频 + MODEL = commod.MODEL // MODEL嫩模 + MeetingCard = commod.MeetingCard // 约会卡 + OTHER = commod.OTHER // OTHER 1 + NewUser = commod.NEWUSERCard // 新手卡 + PHYSICALGOODS = commod.PhysicalGoods // 线上商品 + VideoDiscount = commod.VideoDiscount // 视频折扣卡 + VideoFreeCard = commod.VideoFreeCard // 视频免费卡 + CoinMonthCard = commod.CoinMonthCard // 金币月卡 + AdvanceCard = commod.AdvanceCard // 预售卡 + GameAdvanceCard = commod.GameAdvanceCard // 游戏预售卡 + WhoringCard = commod.WhoringCard // 白嫖卡 +) + +const ( + _ = iota + RedisSetKey = "productmod" +) + +const ( + _ = iota + LevelOne // 1 普通会员 + LevelTwo // 2 超级会员 + LevelThree // 3 暗网会员 +) + +var mdb *db.MongoDB + +// Product 商品类型 +type Product struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"productID" ` // id + VipLevel int `bson:"vipLevel" json:"vipLevel"` // product等级 1 普通vip 2 超级vip 3 暗网会员 + Name string `bson:"productName" json:"productName" ` // 商品名字 + Alias string `bson:"alias" json:"alias"` // 别名 + Desc string `bson:"desc" json:"desc"` // 内容描述 + Duration int `bson:"duration" json:"duration"` // 持续天数 + Type int `bson:"type" json:"type"` // 会员卡类型 + SendGame bool `bson:"sendGame" json:"sendGame"` // 是否赠送VIP + BGImg string `bson:"bgImg" json:"bgImg"` // 背景图 (或者为预售卡预售状态背景图) + EndBGImg string `bson:"endBgImg" json:"endBgImg"` // 预售卡尾款背景图 + OriginalPrice int64 `bson:"originalPrice" json:"originalPrice"` // 原价 + DiscountedPrice int64 `bson:"discountedPrice" json:"discountedPrice"` // 现价/升级价 单位角(金币) + DiscountedPriceIos *int64 `bson:"discountedPriceIos,omitempty" json:"discountedPriceIos,omitempty"` // ios现价/升级价 + DiscountedPriceAnd *int64 `bson:"discountedPriceAnd,omitempty" json:"discountedPriceAnd,omitempty"` // 安卓现价/升级价 + ProductType commod.ProductType `bson:"productType" json:"productType"` // 产品类型 + Sort int `bson:"sort" json:"sort"` // 排序字段 + Status bool `bson:"status" json:"status"` // false 下架 //true 上架 + Position string `bson:"position" json:"position"` // 位置 + UnitPriceDisplay bool `bson:"unitPriceDisplay" json:"unitPriceDisplay"` // 单价展示开关 + ActionDesc string `bson:"actionDesc" json:"actionDesc"` // 活动描述 + PrivilegeDesc string `bson:"privilegeDesc" json:"privilegeDesc"` // 特权描述 "特权1,特权2" + ShowCountdownTime int `bson:"showCountdownTime" json:"showCountdownTime"` // 新手卡倒计时 24 + IsAmountPay bool `bson:"isAmountPay" json:"isAmountPay"` // 是否可以用金币购买 + IsHomePopUp bool `bson:"isHomePopUp" json:"isHomePopUp"` // 是否首页弹窗 + TimesAWeek int `bson:"timesAWeek" json:"timesAWeek"` // 一周几次 + VideoDiscount int `bson:"videoDiscount" json:"videoDiscount"` // 视频折扣率(金币视频折扣卡) + Privilege []int `bson:"privilege" json:"privilege"` // 特权 + GiveCoin int64 `json:"giveCoin" bson:"giveCoin"` // 购买赠送金币 + EveryDayGiveCoin int64 `json:"everyDayGiveCoin" bson:"everyDayGiveCoin"` // 金币月卡每日赠送金币 + GiveFruitCoin int64 `json:"giveFruitCoin" bson:"giveFruitCoin"` // 购买赠送果币 + GoldVideoFreeDay int `json:"goldVideoFreeDay" bson:"goldVideoFreeDay"` // 金币视频免费天数 + GoldVideoFreeLimit int64 `json:"goldVideoFreeLimit" bson:"goldVideoFreeLimit"` // 金币视频免费限制门槛 + ChanSplitMod int `json:"chanSplitMod" bson:"chanSplitMod"` // 渠道分成模式 0不分成 1 正常分成 + ServiceTime int64 `json:"serviceTime" bson:"serviceTime"` // 服务时长(单位:分钟) + PayVidDiscount int `json:"payVidDiscount" bson:"payVidDiscount"` // 支付视频折扣 + GoldVideoCouponNum int `json:"goldVideoCouponNum" bson:"goldVideoCouponNum"` // 赠送观影券金币数量 + GoldVideoCouponCount int `json:"goldVideoCouponCount" bson:"goldVideoCouponCount"` // 赠送观影券数量 + NewName string `bson:"newName" json:"newName"` // 新商品名字 + NewBgImg string `bson:"newBgImg" json:"newBgImg" ` // 新背景图(或者为预售卡预售状态选中状态背景图),兼容老版本, 新版本(安卓4.3.5,ios4.3.1)以后都用这个字段 + EndBGSelectImg string `bson:"endBgSelectImg" json:"endBgSelectImg"` // 预售卡尾款背景图(选中) + ExclusiveOffer string `bson:"exclusiveOffer" json:"exclusiveOffer"` // 专属特惠 + VipCardDesc string `bson:"vipCardDesc" json:"vipCardDesc"` // vip卡描述 + NewPrivilege []PrivilegeInfo `bson:"newPrivilege" json:"newPrivilege"` // 新特权,兼容老版本, 新版本(安卓4.3.5,ios4.3.1)以后都用这个字段 + AiUndressCount uint64 `bson:"aiUndressCount" json:"aiUndressCount"` // 购买赠送AI免费脱衣次数 + DownloadCount int64 `bson:"downloadCount" json:"downloadCount"` // 赠送下载次数 + LuckyDrawCount int64 `bson:"luckyDrawCount" json:"luckyDrawCount"` // 抽奖次数 + ChatPrice int64 `bson:"chatPrice" json:"chatPrice"` // 私聊价格 + SignDays uint64 `bson:"signDays" json:"signDays"` // 签到天数 + AdvanceAmount int64 `bson:"advanceAmount" json:"advanceAmount"` // 预付金额/升级价 + BalanceAmount int64 `bson:"balanceAmount" json:"balanceAmount"` // 尾款金额 + AllGoldVideoFree bool `json:"allGoldVideoFree" bson:"allGoldVideoFree"` // 所有金币视频免费 + StartTime time.Time `bson:"startTime" json:"startTime"` // 支付尾款开始时间 + EndTime time.Time `bson:"endTime" json:"endTime"` // 支付尾款结束时间 + ActivityTime time.Time `json:"activityTime" bson:"activityTime,omitempty"` // 活动时间 + BroadcastDays int `json:"broadcastDays" bson:"broadcastDays"` // 直播有效时间,单位天 + DramaDays int `json:"dramaDays" bson:"dramaDays" binding:"gte=0,lte=99999"` // 短剧权益有效时间,单位天 + PrepaidPrivilege *AdvanceCardPrepaidPrivilege `json:"prepaidPrivilege" bson:"prepaidPrivilege"` // 预售卡预付权益 + AdvanceVipLevel int `json:"advanceVipLevel" bson:"advanceVipLevel"` // 预付卡等级 + AdvanceDuration int `json:"advanceDuration" bson:"advanceDuration"` // 预付卡持续天数 + AdvanceExpires time.Time `json:"advanceExpires" bson:"advanceExpires"` // 预付卡会员到期时间 + AllowedUpgradeCards []CardsInfo `json:"allowedUpgradeCards" bson:"allowedUpgradeCards"` // 允许升级到此卡的会员卡 + UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"` // 更新时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + // 以下为动态升级信息 + IsUpgrade bool `json:"isUpgrade" bson:"-"` // 是否VIP升级 + CurrentVipName string `json:"currentVipName" bson:"-"` // 当前VIP名称 + CurrentVipPrice int64 `json:"currentVipPrice" bson:"-"` // 当前VIP价格 + PurchasePrice int64 `json:"purchasePrice" bson:"-"` // 原价购买价格 +} + +type CardsInfo struct { + ProductID primitive.ObjectID `json:"productID" bson:"productID"` // ID + ProductName string `bson:"productName" json:"productName" ` // 商品名字 +} + +// CheckUpgrade 检查指定会员卡是否可以升级 +func (p *Product) CheckUpgrade(pid primitive.ObjectID) bool { + if len(p.AllowedUpgradeCards) == 0 { + return false + } + + for _, card := range p.AllowedUpgradeCards { + if pid == card.ProductID { + return true + } + } + + return false +} + +type AdvanceCardPrepaidPrivilege struct { + CoinVideoLimitPerDay int64 `json:"coinVideoLimitPerDay" bson:"coinVideoLimitPerDay"` // 每日金币视频免费次数 + LuckyDrawLimitPerDay int64 `json:"luckyDrawLimitPerDay" bson:"luckyDrawLimitPerDay"` // 每日抽奖次数限制 + AiUndressLimitPerDay int64 `json:"aiUndressLimitPerDay" bson:"aiUndressLimitPerDay"` // 每日ai脱衣次数限制 + DownloadLimitPerDay int64 `json:"downloadLimitPerDay" bson:"downloadLimitPerDay"` // 每日下载次数限制 +} + +// PrivilegeInfo 特权详情 +type PrivilegeInfo struct { + //特权图片 + Image string `bson:"img" json:"img"` + //特权名称 + Name string `bson:"privilegeName" json:"privilegeName"` + //特权描述 + Desc string `bson:"privilegeDesc" json:"privilegeDesc"` + //是否核心特权 + IsCore bool `bson:"isCore" json:"isCore"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +var privilegeMap = map[Privilege]string{ + HDLine: "专属高清线路", + Chat: "私信随意聊", + highQuality: "优质资源", + PriorityReview: "帖子优先审核", + FreeComment: "评论区霸主", + UnlimitedViewing: "视频无限观看", + ExclusiveCustomerService: "专属客服", + Freeportrait: "修改个人头像", + FreeSignature: "修改个性签名", + FreeBrowseNovels: "浏览小说", + FreeAudioBook: "畅听有声小说", + FreeDrama: "免费看全部短剧", +} + +const ( + _ = iota + HDLine Privilege = 1 //专属高清线路 + Chat Privilege = 2 //私信随意聊 + highQuality Privilege = 3 //优质资源 + PriorityReview Privilege = 4 //帖子优先审核 + FreeComment Privilege = 5 //评论区霸主 + UnlimitedViewing Privilege = 6 //视频无限观看 + ExclusiveCustomerService Privilege = 7 //专属客服 + Freeportrait Privilege = 8 //修改个人头像 + FreeSignature Privilege = 9 //修改个性签名 + FreeBrowseNovels Privilege = 10 //浏览小说 + FreeAudioBook Privilege = 11 //畅听有声小说 + FreeDrama Privilege = 12 //免费看全部短剧 +) + +type Privilege int64 + +func (t Privilege) Key() string { + if key, ok := privilegeMap[t]; ok { + return key + } + return "" +} diff --git a/models/v/productmod/web.go b/models/v/productmod/web.go new file mode 100644 index 0000000..faec727 --- /dev/null +++ b/models/v/productmod/web.go @@ -0,0 +1,162 @@ +package productmod + +import ( + "time" + + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ProductSelector 修改结构体 +type ProductSelector struct { + VipLevel *int `bson:"vipLevel,omitempty" json:"vipLevel,omitempty"` // product等级 + Name *string `bson:"productName,omitempty" json:"productName,omitempty"` // 商品名字 + Alias *string `bson:"alias,omitempty" json:"alias,omitempty" ` // 别名 + Desc *string `bson:"desc,omitempty" json:"desc,omitempty" ` // 内容描述 + Duration *int `bson:"duration,omitempty" json:"duration,omitempty" ` // 持续天数 + BGImg *string `bson:"bgImg,omitempty" json:"bgImg,omitempty" ` // 背景图 + EndBGImg *string `bson:"endBgImg" json:"endBgImg"` // 预售卡尾款背景图 + OriginalPrice *int64 `bson:"originalPrice,omitempty" json:"originalPrice,omitempty"` // 原价 + DiscountedPriceIos *int64 `bson:"discountedPriceIos,omitempty" json:"discountedPriceIos,omitempty"` // ios现价 + DiscountedPriceAnd *int64 `bson:"discountedPriceAnd,omitempty" json:"discountedPriceAnd,omitempty"` // 安卓现价 + ProductType *int `bson:"productType,omitempty" json:"productType,omitempty" ` // 产品类型 + Sort *int `bson:"sort,omitempty" json:"sort,omitempty" ` // 排序字段 + Status *bool `bson:"status,omitempty" json:"status,omitempty"` // 0 下架 //1 下架 + Position *string `bson:"position,omitempty" json:"position,omitempty"` // 位置 + UnitPriceDisplay *bool `bson:"unitPriceDisplay,omitempty" json:"unitPriceDisplay,omitempty"` // 单价展示开关 + ActionDesc *string `bson:"actionDesc,omitempty" json:"actionDesc,omitempty"` // 活动描述 + PrivilegeDesc *string `bson:"privilegeDesc,omitempty" json:"privilegeDesc,omitempty"` // 特权描述 "特权1,特权2" + ShowCountdownTime *int `bson:"showCountdownTime,omitempty" json:"showCountdownTime,omitempty"` // 新手卡倒计时 24 + IsAmountPay *bool `bson:"isAmountPay,omitempty" json:"isAmountPay,omitempty"` // 是否可以用金币购买 + TimesAWeek *int `bson:"timesAWeek,omitempty" json:"timesAWeek,omitempty"` // 一周几次 + LouFengDiscount *int `bson:"louFengDiscount,omitempty" json:"louFengDiscount,omitempty"` // 楼凤折扣率 + LoufengBookDiscount *int `bson:"loufengBookDiscount,omitempty" json:"loufengBookDiscount,omitempty"` // 楼凤预约折扣 + LoufengBookDiscountDays *int `bson:"loufengBookDiscountDays,omitempty" json:"loufengBookDiscountDays,omitempty"` // 楼凤预约折扣时间 + Privilege *[]int `bson:"privilege,omitempty" json:"privilege,omitempty"` // 特权 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 + GiveCoin int64 `json:"giveCoin" bson:"giveCoin"` // 购买赠送金币 + EveryDayGiveCoin int64 `json:"everyDayGiveCoin" bson:"everyDayGiveCoin"` // 金币月卡每日赠送金币 + GiveGameCoin *int64 `json:"giveGameCoin" bson:"giveGameCoin,omitempty"` // 购买赠送游戏金币 + LouFengUnlockTimes *int `json:"louFengUnlockTimes" bson:"louFengUnlockTimes,omitempty"` // 赠送楼凤解锁次数 + ChanSplitMod *int `json:"chanSplitMod,omitempty" bson:"chanSplitMod,omitempty"` // 渠道分成模式 0不分成 1 正常分成 + GoldVideoFreeDay *int `json:"goldVideoFreeDay" bson:"goldVideoFreeDay,omitempty"` // 金币视频免费天数 + GoldVideoFreeLimit *int `json:"goldVideoFreeLimit" bson:"goldVideoFreeLimit"` // 金币视频免费限制门槛 + ServiceTime *int64 `json:"serviceTime,omitempty" bson:"serviceTime,omitempty"` // 服务时长(单位:分钟) + GiveFruitCoin *int64 `json:"giveFruitCoin,omitempty" bson:"giveFruitCoin,omitempty"` // 购买赠送果币 + PayVidDiscount *int `json:"payVidDiscount,omitempty" bson:"payVidDiscount,omitempty"` // 支付视频折扣 + GoldVideoCouponNum *int `json:"goldVideoCouponNum,omitempty" bson:"goldVideoCouponNum,omitempty"` // 赠送观影券金币数量 + GoldVideoCouponCount *int `json:"goldVideoCouponCount,omitempty" bson:"goldVideoCouponCount,omitempty"` // 赠送观影券数量 + NewName *string `bson:"newName,omitempty" json:"newName,omitempty"` // 新商品名字 + NewBgImg *string `bson:"newBgImg,omitempty" json:"newBgImg,omitempty" ` // 新背景图 + EndBGSelectImg *string `bson:"endBgSelectImg" json:"endBgSelectImg"` // 预售卡尾款背景图(选中) + ExclusiveOffer *string `bson:"exclusiveOffer,omitempty" json:"exclusiveOffer,omitempty"` // 是否专属特惠 + VipCardDesc *string `bson:"vipCardDesc,omitempty" json:"vipCardDesc,omitempty"` // vip卡描述 + NewPrivilege []*PrivilegeInfo `bson:"newPrivilege,omitempty" json:"newPrivilege,omitempty"` // 新特权 + AiUndressCount *uint64 `bson:"aiUndressCount,omitempty" json:"aiUndressCount,omitempty"` // ai脱衣次数 + DownloadCount *int64 `bson:"downloadCount,omitempty" json:"downloadCount,omitempty"` // 赠送下载次数 + LuckyDrawCount *int64 `bson:"luckyDrawCount,omitempty" json:"luckyDrawCount,omitempty"` // 抽奖次数 + ChatPrice *int64 `bson:"chatPrice,omitempty" json:"chatPrice,omitempty"` // 私聊价格 + SignDays *uint64 `json:"signDays,omitempty" bson:"signDays,omitempty"` // 签到天数 + AdvanceAmount *int64 `bson:"advanceAmount,omitempty" json:"advanceAmount,omitempty"` // 预付金额 + BalanceAmount *int64 `bson:"balanceAmount,omitempty" json:"balanceAmount,omitempty"` // 尾款金额 + AllGoldVideoFree *bool `json:"allGoldVideoFree,omitempty" bson:"allGoldVideoFree,omitempty"` // 所有金币视频免费 + StartTime *time.Time `bson:"startTime,omitempty" json:"startTime,omitempty"` // 开始时间 + EndTime *time.Time `bson:"endTime,omitempty" json:"endTime,omitempty"` // 结束时间 + ActivityTime *time.Time `json:"activityTime,omitempty" bson:"activityTime,omitempty"` // 活动时间 + Type *int `bson:"type,omitempty" json:"type,omitempty"` // 会员卡类型 + SendGame *bool `bson:"sendGame,omitempty" json:"sendGame,omitempty"` // 是否赠送游戏 + IsHomePopUp *bool `bson:"isHomePopUp,omitempty" json:"isHomePopUp,omitempty"` // 是否首页弹窗 + BroadcastDays *int `json:"broadcastDays,omitempty" bson:"broadcastDays,omitempty"` // 直播有效时间,单位天 + DramaDays *int `json:"dramaDays,omitempty" bson:"dramaDays,omitempty"` // 短剧权益有效时间,单位天 + PrepaidPrivilege *AdvanceCardPrepaidPrivilege `json:"prepaidPrivilege,omitempty" bson:"prepaidPrivilege,omitempty"` // 预售卡预付权益 + AdvanceVipLevel *int `json:"advanceVipLevel,omitempty" bson:"advanceVipLevel,omitempty"` // 预付卡等级 + AdvanceDuration *int `json:"advanceDuration,omitempty" bson:"advanceDuration,omitempty"` // 预付卡持续天数 + AdvanceExpires *time.Time `json:"advanceExpires,omitempty" bson:"advanceExpires,omitempty"` // 预付卡会员到期时间 + AllowedUpgradeCards *[]*CardsInfo `json:"allowedUpgradeCards,omitempty" bson:"allowedUpgradeCards,omitempty"` // 允许升级到此卡的会员卡 +} + +type ProductWeb struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id" ` // ID + VipLevel int `bson:"vipLevel" json:"vipLevel"` // product等级 1 普通vip 2 超级vip + Name string `bson:"productName" json:"productName" ` // 商品名字 + Alias string `bson:"alias" json:"alias"` // 别名 + Desc string `bson:"desc" json:"desc" ` // 内容描述 + Duration int `bson:"duration" json:"duration" binding:"lte=99999"` // 持续天数 + BGImg string `bson:"bgImg" json:"bgImg" ` // 背景图 + EndBGImg string `bson:"endBgImg" json:"endBgImg"` // 预售卡尾款背景图 + OriginalPrice int64 `bson:"originalPrice" json:"originalPrice" ` // 原价 + DiscountedPrice int64 `bson:"discountedPrice" json:"discountedPrice"` // 现价 + DiscountedPriceIos int64 `bson:"discountedPriceIos" json:"discountedPriceIos"` // ios现价 + DiscountedPriceAnd int64 `bson:"discountedPriceAnd" json:"discountedPriceAnd"` // 安卓现价 + ProductType commod.ProductType `bson:"productType" json:"productType"` // 产品类型 + Sort int `bson:"sort" json:"sort"` // 排序字段 + Status bool `bson:"status" json:"status"` // 0 下架 //1 下架 + Position string `bson:"position" json:"position"` // 位置 + UnitPriceDisplay bool `bson:"unitPriceDisplay" json:"unitPriceDisplay"` // 单价展示开关 + ActionDesc string `bson:"actionDesc" json:"actionDesc"` // 活动描述 + PrivilegeDesc string `bson:"privilegeDesc" json:"privilegeDesc"` // 特权描述 "特权1,特权2" + ShowCountdownTime int `bson:"showCountdownTime" json:"showCountdownTime"` // 新手卡倒计时 24 + IsAmountPay bool `bson:"isAmountPay" json:"isAmountPay"` // 是否可以用金币购买 + TimesAWeek int `bson:"timesAWeek" json:"timesAWeek"` // 一周几次 + LouFengDiscount int `bson:"louFengDiscount" json:"louFengDiscount"` // 楼凤折扣率 + LoufengBookDiscount int `bson:"loufengBookDiscount" json:"loufengBookDiscount"` // 楼凤预约 + LoufengBookDiscountDays int `bson:"loufengBookDiscountDays" json:"loufengBookDiscountDays"` // 楼凤预约折扣时间 + Privilege []int `bson:"privilege" json:"privilege"` // 特权 + UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"` // 更新时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + GiveCoin int64 `json:"giveCoin" bson:"giveCoin"` // 购买赠送金币 + GiveGameCoin int64 `json:"giveGameCoin" bson:"giveGameCoin"` // 购买赠送游戏金币 + LouFengUnlockTimes int `json:"louFengUnlockTimes" bson:"louFengUnlockTimes"` // 赠送楼凤解锁次数 + GoldVideoFreeDay int `json:"goldVideoFreeDay" bson:"goldVideoFreeDay" binding:"lte=99999"` // 金币视频免费天数 + GoldVideoFreeLimit int `json:"goldVideoFreeLimit" bson:"goldVideoFreeLimit"` // 金币视频免费限制门槛 + ChanSplitMod int `json:"chanSplitMod" bson:"chanSplitMod"` // 渠道分成模式 0不分成 1 正常分成 + ServiceTime int64 `json:"serviceTime" bson:"serviceTime"` // 服务时长(单位:分钟) + GiveFruitCoin int64 `json:"giveFruitCoin" bson:"giveFruitCoin"` // 购买赠送果币 + PayVidDiscount int `json:"payVidDiscount" bson:"payVidDiscount"` // 支付视频折扣 + GoldVideoCouponNum int `json:"goldVideoCouponNum" bson:"goldVideoCouponNum"` // 赠送观影券金币数量 + GoldVideoCouponCount int `json:"goldVideoCouponCount" bson:"goldVideoCouponCount"` // 赠送观影券数量 + VideoDiscount int `json:"videoDiscount" bson:"videoDiscount"` // 视频折扣卡-视频折扣率 + NewName string `bson:"newName" json:"newName"` // 新商品名字 + NewBgImg string `bson:"newBgImg" json:"newBgImg" ` // 新背景图 + EndBGSelectImg string `bson:"endBgSelectImg" json:"endBgSelectImg"` // 预售卡尾款背景图(选中) + ExclusiveOffer string `bson:"exclusiveOffer" json:"exclusiveOffer"` // 是否可以用金币购买 + VipCardDesc string `bson:"vipCardDesc" json:"vipCardDesc"` // vip卡描述 + NewPrivilege []PrivilegeInfo `bson:"newPrivilege" json:"newPrivilege"` // vip卡描述 + EveryDayGiveCoin int64 `json:"everyDayGiveCoin" bson:"everyDayGiveCoin"` // 金币月卡每日赠送金币 + AiUndressCount uint64 `bson:"aiUndressCount" json:"aiUndressCount"` // ai脱衣次数 + DownloadCount int64 `bson:"downloadCount" json:"downloadCount"` // 赠送下载次数 + LuckyDrawCount int64 `bson:"luckyDrawCount" json:"luckyDrawCount"` // 抽奖次数 + AllGoldVideoFree bool `json:"allGoldVideoFree" bson:"allGoldVideoFree"` // 所有金币视频免费 + AdvanceAmount int64 `bson:"advanceAmount" json:"advanceAmount"` // 预付金额 + BalanceAmount int64 `bson:"balanceAmount" json:"balanceAmount"` // 尾款金额 + Type int `bson:"type" json:"type"` // 会员卡类型 + IsHomePopUp bool `bson:"isHomePopUp" json:"isHomePopUp"` // 是否首页弹窗 + SendGame bool `bson:"sendGame,omitempty" json:"sendGame,omitempty"` // 是否赠送游戏 + StartTime time.Time `bson:"startTime" json:"startTime"` // 开始时间 + EndTime time.Time `bson:"endTime" json:"endTime"` // 结束时间 + ActivityTime time.Time `json:"activityTime" bson:"activityTime,omitempty"` // 活动时间 + ChatPrice int64 `bson:"chatPrice" json:"chatPrice"` // 私聊价格 + SignDays uint64 `bson:"signDays" json:"signDays"` // 签到天数 + BroadcastDays int `json:"broadcastDays" bson:"broadcastDays"` // 直播有效时间,单位天 + DramaDays int `json:"dramaDays" bson:"dramaDays" binding:"gte=0,lte=99999"` // 短剧权益有效时间,单位天 + PrepaidPrivilege *AdvanceCardPrepaidPrivilege `json:"prepaidPrivilege" bson:"prepaidPrivilege"` // 预售卡预付权益 + AdvanceVipLevel int `json:"advanceVipLevel" bson:"advanceVipLevel"` // 预付卡等级 + AdvanceDuration int `json:"advanceDuration" bson:"advanceDuration"` // 预付卡持续天数 + AdvanceExpires time.Time `json:"advanceExpires" bson:"advanceExpires"` // 预付卡会员到期时间 + AllowedUpgradeCards []CardsInfo `json:"allowedUpgradeCards" bson:"allowedUpgradeCards"` // 允许升级到此卡的会员卡 +} + +type CheckboxListCond struct { + ProductType commod.ProductType `form:"productType" json:"productType"` //产品类型 +} + +type ProductCheckbox struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + Name string `bson:"productName" json:"productName"` //商品名字 +} + +type ProductListWeb struct { + Status *bool `form:"status" json:"status,omitempty"` //true 上架 //false 下架 + ProductType *int `form:"productType" json:"productType,omitempty"` // 产品类型 +} diff --git a/models/v/productposimod/productposi.go b/models/v/productposimod/productposi.go new file mode 100644 index 0000000..fc05daa --- /dev/null +++ b/models/v/productposimod/productposi.go @@ -0,0 +1,161 @@ +package productposimod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.ProductPosition + +// InitIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Find 查询 +func Find(id primitive.ObjectID) (p *ProductPosition, err error) { + if err = coll(nil).FindOne(&p, bson.M{"_id": id}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindProduct", table, "FindOne", err), log.Any("id", id)) + return + } + if p == nil || p.ID.IsZero() { + log.Info("product FindProduct not found", log.Any("id", id)) + return nil, nil + } + return +} + +// FindByIDs 根据id批量查询 +func FindByIDs(ids []primitive.ObjectID) (positions []ProductPosition, err error) { + err = coll(nil).Find(&positions, bson.M{"_id": bson.M{"$in": ids}}) + return +} + +// FindNamesWithStatusTrue 查询 +func FindNamesWithStatusTrue() (nameMap map[string]string, showTypeMap map[string]int64, dataList []string, err error) { + res := make([]ProductPosition, 0) + opt := options.FindOptions{ + Sort: bson.D{{Key: "sort", Value: 1}}, + } + if err = coll(nil).Find(&res, bson.M{"status": true}, &opt); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindNamesWithStatusTrue", table, "Find", err)) + return + } + nameMap = make(map[string]string) + showTypeMap = make(map[string]int64) + dataList = make([]string, len(res)) + for i := range res { + nameMap[res[i].ID.Hex()] = res[i].Name + showTypeMap[res[i].ID.Hex()] = res[i].ShowType + dataList[i] = res[i].ID.Hex() + } + return +} + +// FindAllNameMap +func FindAllNameMap() (ret map[string]ProductPosition, err error) { + data := []ProductPosition{} + if err = coll(nil).Find(&data, bson.M{"status": true}); err != nil { + return + } + ret = make(map[string]ProductPosition) + for _, p := range data { + ret[p.Name] = p + } + return +} + +// FindNames 查询 +func FindNames() (data []string, err error) { + res := make([]ProductPosition, 0) + opt := options.FindOptions{ + Sort: bson.D{{Key: "sort", Value: 1}}, + } + if err = coll(nil).Find(&res, bson.M{"status": true}, &opt); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindNamesWithStatusTrue", table, "Find", err)) + return + } + data = make([]string, len(res)) + for i := range res { + data[i] = res[i].Name + } + return +} + +// FindAll 查询 +func FindAll() (data []ProductPosition, err error) { + data = make([]ProductPosition, 0) + opt := options.FindOptions{ + Sort: bson.D{{Key: "sort", Value: 1}}, + } + if err = coll(nil).Find(&data, bson.M{}, &opt); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindAll", table, "Find", err)) + return + } + return +} + +// Insert 插入一条数据 +func Insert(p *ProductPosition) error { + p.CreatedAt = time.Now() + if _, err := coll(nil).InsertOne(p); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return err + } + return nil +} + +// UpdateProduct 修改Product类型 +func Update(set *ProductPositionSelector) (err error) { + set.UpdatedAt = time.Now() + if _, err = coll(nil).UpdateOne(bson.M{"_id": set.ID}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("set", set), + ) + } + return +} + +// Remove 删除Product类型 +func Remove(id string) (err error) { + OID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Remove", table, "ObjectIDFromHex", err), + log.Any("id", id), + ) + return + } + if _, err = coll(nil).DeleteOne(bson.M{"_id": OID}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Remove", table, "DeleteOne", err), + log.Any("OID", OID), + ) + } + return +} diff --git a/models/v/productposimod/type.go b/models/v/productposimod/type.go new file mode 100644 index 0000000..b006263 --- /dev/null +++ b/models/v/productposimod/type.go @@ -0,0 +1,32 @@ +package productposimod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +const ( + Horiz int = 1 //横 + Vert int = 2 //竖 +) + +// ProductPosition 商品类型 +type ProductPosition struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id" ` //id + Name string `bson:"name" json:"name" ` //商品名字 + ShowType int64 `bson:"showType" json:"showType"` //展示样式 + Sort int `bson:"sort" json:"sort"` //排序字段 + Status bool `bson:"status" json:"status"` //false 下架 //true 下架 + UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"` + CreatedAt time.Time `bson:"createdAt" json:"createdAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/productposimod/web.go b/models/v/productposimod/web.go new file mode 100644 index 0000000..f13dbdb --- /dev/null +++ b/models/v/productposimod/web.go @@ -0,0 +1,17 @@ +package productposimod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ProductPositionSelector 修改结构体 +type ProductPositionSelector struct { + ID primitive.ObjectID `json:"id" bson:"_id"` + ShowType *int64 `bson:"showType,omitempty" json:"showType,omitempty"` //展示样式 1 横 2 竖线 + Name *string `bson:"name,omitempty" json:"name,omitempty"` //商品名字 + Sort *int `bson:"sort,omitempty" json:"sort,omitempty" ` //排序字段 + Status *bool `bson:"status,omitempty" json:"status,omitempty"` //0 下架 //1 下架 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} diff --git a/models/v/proxyincomemod/proxyincome.go b/models/v/proxyincomemod/proxyincome.go new file mode 100644 index 0000000..4af13d8 --- /dev/null +++ b/models/v/proxyincomemod/proxyincome.go @@ -0,0 +1,184 @@ +package proxyincomemod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/timeutil" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.ProxyIncomeLog + +// IncomeLog 推广收益记录表 +type StandReq struct { + UID uint64 `form:"uid" json:"uid,omitempty" bson:"uid"` // 用户UID +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "originUID", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// InsertIncomeLog 插入记录 +func InsertIncomeLog(t *db.MongoTool, p *ProxyIncomeLog) error { + p.CreatedAt = time.Now() + if _, err := coll(nil).InsertOne(p); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertIncomeLog", table, "InsertOne", err), log.Any("p", p)) + return err + } + return nil +} + +// FindIncomeOfMonth 查询每月收益 +func FindIncomeOfMonth(uid uint64) (totalMoney int64, totalPerfomance int64, err error) { + firstDay, lastDay := timeutil.MonthStartEndTime(time.Now()) + pipelines := []bson.M{ + {"$match": bson.M{"uid": uid, "createdAt": bson.M{"$gte": firstDay, "$lt": lastDay}}}, + {"$group": bson.M{"_id": nil, "totalMoney": bson.M{"$sum": "$money"}, "totalPerformance": bson.M{"$sum": "$performance"}}}, + {"$project": bson.M{"totalMoney": 1, "totalPerformance": 1}}, + } + type res struct { + TotalMoney int64 `json:"totalMoney"` + TotalPerformance int64 `json:"totalPerformance"` + } + data := make([]res, 0) + if err = coll(nil).Aggregate(&data, pipelines); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindIncomeOfMonth", table, "Aggregate", err), log.Any("uid", uid)) + return + } + if len(data) > 0 { + totalMoney = data[0].TotalMoney + totalPerfomance = data[0].TotalPerformance + } + return +} + +// FindIncomeLogs 查询收益详情 +func FindIncomeLogs(uid uint64, pageNumber int, pageSize int) (total int64, incomeLogs []*ProxyIncomeLog, hasNext bool, err error) { + incomeLogs = make([]*ProxyIncomeLog, 0) + total, err = coll(nil).Count(bson.M{"uid": uid}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindIncomeLogs", table, "Count", err), log.Any("uid", uid)) + return + } + skip := int64((pageNumber - 1) * pageSize) + limit := int64(pageSize + 1) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + if err = coll(nil).Find(&incomeLogs, bson.M{"uid": uid}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindIncomeLogs", table, "Aggregate", err), log.Any("uid", uid)) + return + } + if len(incomeLogs) > pageSize { + hasNext = true + incomeLogs = incomeLogs[:pageSize] + } + return +} + +// FindIncomeLogs 查询收益详情 +func FindList(cond bson.M, opts *options.FindOptions) (total int64, incomeLogs []*ProxyIncomeLog, err error) { + incomeLogs = make([]*ProxyIncomeLog, 0) + total, err = coll(nil).Count(cond) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindList", table, "Count", err), log.Any("cond", cond)) + return + } + if err = coll(nil).Find(&incomeLogs, cond, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindList", table, "Find", err), log.Any("cond", cond)) + } + return +} + +// GetTotalIncome 计算总收益 +func GetTotalIncome(uid uint64, begin *string, end *string) (int64, error) { + cond := bson.M{"uid": uid} + if begin != nil && end != nil { + cond["createdAt"] = bson.M{"$gte": timeutil.StrTimeToTime(*begin), "$lte": timeutil.StrTimeToTime(*end)} + } + pipelines := []bson.M{ + {"$match": cond}, + {"$group": bson.M{"_id": nil, "totalMoney": bson.M{"$sum": "$money"}}}, + } + type I struct { + TotalMoney int64 `json:"totalMoney" bson:"totalMoney"` + } + res := []I{} + if err := coll(nil).Aggregate(&res, pipelines); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetTotalIncome", table, "Aggregate", err), + log.Any("uid", uid), + log.Any("begin", begin), + log.Any("end", end), + ) + return 0, err + } + var tMoney int64 = 0 + if len(res) > 0 { + tMoney = res[0].TotalMoney + } + return tMoney, nil +} + +// FindAllIncome 查询总收益 +func FindAllIncome(uid uint64) (lv1 int64, lv2 int64, lv3 int64, lv4 int64, total int64, err error) { + pipelines := []bson.M{ + {"$match": bson.M{"uid": uid}}, + {"$group": bson.M{"_id": "$agentLevel", "totalMoney": bson.M{"$sum": "$money"}}}, + } + type I struct { + ID int64 `json:"id" bson:"_id"` + TotalMoney int64 `json:"totalMoney"` + } + res := make([]I, 0) + if err = coll(nil).Aggregate(&res, pipelines); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindAllIncome", table, "Aggregate", err), log.Any("uid", uid)) + return + } + for _, v := range res { + switch v.ID { + case 1: + lv1 = v.TotalMoney + total += lv1 + case 2: + lv2 = v.TotalMoney + total += lv2 + case 3: + lv3 = v.TotalMoney + total += lv3 + case 4: + lv4 = v.TotalMoney + total += lv4 + } + } + return +} diff --git a/models/v/proxyincomemod/type.go b/models/v/proxyincomemod/type.go new file mode 100644 index 0000000..6110e41 --- /dev/null +++ b/models/v/proxyincomemod/type.go @@ -0,0 +1,29 @@ +package proxyincomemod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +// IncomeLog 推广收益记录表 +type ProxyIncomeLog struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //表id + UID uint64 `form:"uid" json:"uid" bson:"uid"` // 用户UID + Performance int64 `form:"performance" json:"performance" bson:"performance"` //绩效 + AgentLevel int `form:"agentLevel" json:"agentLevel" bson:"agentLevel"` //代理等级 + Money int64 `form:"money" json:"money" bson:"money"` //收益金额(推广收益) + RechargeID primitive.ObjectID `form:"rechargeID" json:"rechargeID" bson:"rechargeID"` //充值订单号 + Invitee uint64 `form:"originUID" json:"originUID" bson:"originUID"` //被邀请用户id + VipLevel int `json:"vipLevel" bson:"vipLevel"` //会员等级 + CreatedAt time.Time `form:"createdAt" json:"createdAt" bson:"createdAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/proxymod/app.go b/models/v/proxymod/app.go new file mode 100644 index 0000000..2c16075 --- /dev/null +++ b/models/v/proxymod/app.go @@ -0,0 +1,19 @@ +package proxymod + +// InvitationBindReq 推广绑定请求参数 +type InvitationBindReq struct { + UID uint64 `form:"uid" json:"uid" bson:"uid"` //邀请用户id + PromoCode string `bson:"promotionCode" json:"promotionCode"` //邀请码 +} + +// UIDList uid切片类型结构体 +type UIDList struct { + ID uint64 `bson:"_id"` + IDS []uint64 `bson:"ids"` + Total int64 `bson:"total"` +} + +type InvitationRes struct { + IsBoughtVIP bool `json:"isBought"` + Invitation +} diff --git a/models/v/proxymod/h5.go b/models/v/proxymod/h5.go new file mode 100644 index 0000000..a3ba3b6 --- /dev/null +++ b/models/v/proxymod/h5.go @@ -0,0 +1,24 @@ +package proxymod + +type InvitActUser struct { + Name string `json:"name"` //用户名 + UID uint64 `json:"uid"` //用户id + Portrait string `json:"portrait"` //头像 + TotalCoins int64 `json:"totalCoins"` //总金币 + ReCoins int64 `json:"reCoins"` //未领金币 + CoinsLv1 int64 `json:"coinsLv1"` //一级推广获益金币 + CoinsLv2 int64 `json:"coinsLv2"` //二级推广获益金币 + CoinsLv3 int64 `json:"coinsLv3"` //三级推广获益金币 + CoinsLv4 int64 `json:"coinsLv4"` //四级推广获益金币 +} + +type InvitRankUser struct { + Name string `json:"name"` //用户名 + Portrait string `json:"portrait"` //头像 + Coins int64 `json:"coins"` //金币数 +} + +type InvitActInfo struct { + User InvitActUser `json:"user"` + Ranks []InvitRankUser `json:"ranks"` +} diff --git a/models/v/proxymod/proxy.go b/models/v/proxymod/proxy.go new file mode 100644 index 0000000..394dca3 --- /dev/null +++ b/models/v/proxymod/proxy.go @@ -0,0 +1,536 @@ +package proxymod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.Invitation + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "invitee", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "inviteTime", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// GetInvitation 获取一级代理 +func GetInvitation(uid uint64) ([]uint64, error) { + data := []UIDList{} + if err := coll(nil).Aggregate(&data, []bson.M{ + {"$match": bson.M{"uid": uid}}, + {"$project": bson.M{"_id": 0, "invitee": 1}}, + {"$group": bson.M{"_id": 1, "ids": bson.M{"$push": "$invitee"}}}, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInvitation", table, "Aggregate", err), log.Any("uid", uid)) + return nil, err + } + if len(data) == 0 { + return make([]uint64, 0), nil + } + return data[0].IDS, nil +} + +// GetInvrLv1 获取上一级代理 +func GetInvrLv1(uid uint64) (uint64, error) { + data := Invitation{} + if err := coll(nil).FindOne(&data, bson.M{"invitee": uid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInvrLv1", table, "FindOne", err), log.Any("uid", uid)) + return 0, err + } + return data.UID, nil +} + +// GetInvrLv1Map 获取上一级代理uid Map +func GetInvrLv1Map(uidList []uint64) (map[uint64]uint64, error) { + if len(uidList) == 0 { + return make(map[uint64]uint64), nil + } + filter := bson.M{ + "invitee": bson.M{ + "$in": uidList, + }, + } + invrList := make([]Invitation, 0, len(uidList)) + if err := coll(nil).Find(&invrList, filter); err != nil { + return nil, err + } + m := make(map[uint64]uint64, len(invrList)) + for _, invr := range invrList { + m[invr.Invitee] = invr.UID + } + return m, nil +} + +// GetInvitationByUID 获取下级代理绑定记录 +func GetInvitationByUID(uid uint64, pageNumber int64, pageSize int64) (data []*Invitation, hasNext bool, err error) { + data = make([]*Invitation, 0) + skip := pageSize * (pageNumber - 1) + limit := pageSize + 1 + opt := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + if err = coll(nil).Find(&data, bson.M{"uid": uid}, &opt); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInvitationByUID", table, "Find", err), log.Any("uid", uid)) + return + } + if len(data) > int(pageSize) { + hasNext = true + data = data[:pageSize] + } + return +} + +// GetInveNext 获取下级级代理 +func GetInveNext(uids []uint64) (ids []uint64, total int64, err error) { + data := []UIDList{} + if err = coll(nil).Aggregate(&data, []bson.M{ + {"$match": bson.M{"uid": bson.M{"$in": uids}}}, + {"$project": bson.M{"_id": 0, "invitee": 1}}, + {"$group": bson.M{"_id": 1, "ids": bson.M{"$push": "$invitee"}, "total": bson.M{"$sum": 1}}}, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInveNext", table, "Aggregate", err), log.Any("uids", uids)) + return + } + if len(data) > 0 { + ids = data[0].IDS + total = data[0].Total + } + return +} + +// GetInviteesCount 获取下级代理总数 +func GetInviteesCount(uids []uint64, startTime time.Time, endTime time.Time) (count int64, err error) { + count, err = coll(nil).Count(bson.M{"createdAt": bson.M{"$gte": startTime, "$lte": endTime}, "uid": bson.M{"$in": uids}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInviteesCount", table, "Count", err), + log.Any("uids", uids), + log.Any("startTime", startTime), + log.Any("endTime", endTime), + ) + return + } + return +} + +// GetInviteesCount 获取下级代理总数map +func GetInviteesCountMap(uids []uint64) (map[uint64]int64, error) { + if len(uids) == 0 { + return make(map[uint64]int64), nil + } + pipeLine := []bson.M{ + { + "$match": bson.M{ + "uid": bson.M{"$in": uids}, + }, + }, + { + "$group": bson.M{ + "_id": "$uid", + "count": bson.M{ + "$sum": 1, + }, + }, + }, + } + list := make([]struct { + UID uint64 `bson:"_id"` + Count int64 `bson:"count"` + }, len(uids)) + if err := coll(nil).Aggregate(&list, pipeLine); err != nil { + return nil, err + } + countMap := make(map[uint64]int64, len(list)) + for _, v := range list { + countMap[v.UID] = v.Count + } + return countMap, nil +} + +// GetInve 获取N级代理 +func GetInve(uids []uint64, lev int) ([]uint64, error) { + data := []UIDList{} + if err := coll(nil).Aggregate(&data, []bson.M{ + {"$match": bson.M{"uid": bson.M{"$in": uids}}}, + {"$project": bson.M{"_id": 0, "invitee": 1}}, + {"$group": bson.M{"_id": 1, "ids": bson.M{"$push": "$invitee"}}}, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInve", table, "Aggregate", err), + log.Any("uids", uids), + log.Any("lev", lev), + ) + return nil, err + } + if len(data) == 0 { + return nil, nil + } + ids := data[0].IDS + if lev == 1 || len(ids) <= 0 { + return ids, nil + } + return GetInve(ids, lev-1) +} + +func CountByUID(uid uint64) (i int64, err error) { + count, err := coll(nil).Count(bson.M{"uid": uid}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountByUID", table, "Count", err), log.Any("uid", uid)) + return 0, err + } + return count, nil +} +func CountByInviteeID(uid uint64) (i int64, err error) { + count, err := coll(nil).Count(bson.M{"invitee": uid}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountByInviteeID", table, "Count", err), log.Any("uid", uid)) + return 0, err + } + return count, nil +} + +func CountPayUserByUID(uid uint64) (i int64, err error) { + count, err := coll(nil).Count(bson.M{"uid": uid, "isRecharge": true}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountPayUserByUID", table, "Count", err), log.Any("uid", uid)) + return 0, err + } + return count, nil +} + +func CountAfter22_02_10(uid uint64) (i int64, err error) { + count, err := coll(nil).Count(bson.M{"uid": uid, "createdAt": bson.M{"$gt": time.Unix(1612890000, 0)}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountByUID", table, "Count", err), log.Any("uid", uid)) + return 0, err + } + return count, nil +} + +func CountByUIDAndTime(uid uint64, startTime time.Time) (i int64, err error) { + count, err := coll(nil).Count(bson.M{"uid": uid, "createdAt": bson.M{"$gt": startTime}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountByUIDAndTime", table, "Count", err), log.Any("uid", uid)) + return 0, err + } + return count, nil +} + +// GetInveNextDetails 获取下级代理详情 +func GetInveNextDetails(uids []uint64) (invs []Invitation, err error) { + invs = make([]Invitation, 0) + if err = coll(nil).Find(&invs, bson.M{"uid": bson.M{"$in": uids}}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInveNextDetails", table, "Find", err), log.Any("uids", uids)) + return + } + return +} + +// GetInvr 获取上级代理 +func GetInvr(uid uint64, lev int, ids *[]uint64) error { + i := Invitation{} + if err := coll(nil).FindOne(&i, bson.M{"invitee": uid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInvr", table, "FindOne", err), + log.Any("uid", uid), + log.Any("lev", lev), + log.Any("ids", ids), + ) + return err + } + if i.UID == 0 { + return nil + } + *ids = append(*ids, i.UID) + if lev == 1 { + return nil + } + return GetInvr(i.UID, lev-1, ids) +} + +// GetInvrDetails 获取上级代理详情 +func GetInvrDetails(uid uint64, lev int) ([]Invitation, error) { + ids := make([]Invitation, 0) + bufI := Invitation{} + if err := coll(nil).FindOne(&bufI, bson.M{"invitee": uid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInvrDetails", table, "FindOne", err), + log.Any("uid", uid), + log.Any("lev", lev), + log.Any("ids", ids), + ) + return nil, err + } + if bufI.UID == 0 { + return ids, nil + } + ids = append(ids, bufI) + if lev == 1 { + return ids, nil + } + list, err := GetInvrDetails(bufI.UID, lev-1) + if err != nil { + return ids, nil + } + ids = append(ids, list...) + return ids, nil +} + +// WebGetUIDList 绑定记录列表,获取某邀请码的邀请关系和收益 +func WebGetUIDList(start time.Time, end time.Time) (data []UIDList, err error) { + data = make([]UIDList, 0) + p := []bson.M{ + {"$match": bson.M{"createdAt": bson.M{"$gte": start, "$lt": end}}}, + {"$group": bson.M{"_id": "$uid", "ids": bson.M{"$push": "$invitee"}, "promotionCode": bson.M{"$first": "$promotionCode"}}}, + } + if err = coll(nil).Aggregate(&data, p); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "WebGetUIDList", table, "Aggregate", err), + log.Any("start", start), + log.Any("end", end), + ) + return + } + return +} + +// LoopInviteError 循环推广错误 +type LoopInviteError struct { + Parent uint64 + UID uint64 +} + +func (l LoopInviteError) Error() string { + return fmt.Sprintf("Loop Invite Error! parent: %d uid: %d", l.Parent, l.UID) +} + +// InsertOne 插入一条邀请关系 +func InsertOne(invitation Invitation) error { + count, err := CountByInviteeID(invitation.Invitee) + if err != nil { + return err + } + if count != 0 { + return LoopInviteError{invitation.Invitee, invitation.UID} + } + if _, err = coll(nil).InsertOne(invitation); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertOne", err)) + return err + } + return nil +} + +// FindMany 查询所有 +func FindMany(filter bson.M, opts *options.FindOptions) (total int64, data []*Invitation, err error) { + data = make([]*Invitation, 0) + if err = coll(nil).Find(&data, filter, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindMany", table, "Find", err), log.Any("filter", filter)) + return + } + total, err = coll(nil).Count(filter) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindMany", table, "Count", err), log.Any("filter", filter)) + } + return +} + +// ProxyUserCount 一段时间内邀请用户数 +func ProxyUserCount(start time.Time, end time.Time) (map[uint64]int64, []uint64, error) { + m := make(map[uint64]int64) + pipeLine := []bson.M{ + {"$match": bson.M{"createdAt": bson.M{"$gte": start, "$lt": end}}}, + {"$group": bson.M{"_id": "$uid", "count": bson.M{"$sum": 1}}}, + } + docList := []struct { + UID uint64 `bson:"_id"` + Count int64 `bson:"count"` + }{} + uids := []uint64{} + if err := coll(nil).Aggregate(&docList, pipeLine); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ProxyUserCount", table, "Aggregate", err), + log.Any("start", start), + log.Any("end", end), + ) + return m, uids, err + } + for _, doc := range docList { + m[doc.UID] = doc.Count + uids = append(uids, doc.UID) + } + return m, uids, nil +} + +// InvitListByCreatedAt 通过CreatedAt获取Invitation List +func InvitListByCreatedAt(start, end time.Time) ([]Invitation, error) { + filter := bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + invitList := make([]Invitation, 0) + if err := coll(nil).Find(&invitList, filter); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InvitListByCreatedAt", table, "Find", err), + log.Any("start", start), + log.Any("end", end), + ) + return nil, err + } + return invitList, nil +} + +// GetParentInvitList 获取指定uidList的父级Invitation List +func GetParentInvitList(uids []uint64) ([]Invitation, error) { + filter := bson.M{ + "invitee": bson.M{"$in": uids}, + } + list := make([]Invitation, 0, len(uids)) + if err := coll(nil).Find(&list, filter); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetParentInvitList", table, "Find", err), log.Any("uids", uids)) + return nil, err + } + return list, nil +} + +// GetSuperiorInvitList 获取上级Invitation +func GetSuperiorInvitList(invitees []uint64, depth int) ([]Invitation, error) { + invitList := []Invitation{} + for i := 0; i < depth; i++ { + if len(invitees) == 0 { + break + } + parentInvitList, err := GetParentInvitList(invitees) + if err != nil { + return nil, err + } + parentUIDList := make([]uint64, len(parentInvitList)) + for i, v := range parentInvitList { + parentUIDList[i] = v.UID + } + invitList = append(invitList, parentInvitList...) + invitees = parentUIDList + } + return invitList, nil +} + +func NextProxyCount(uid uint64, start time.Time, end time.Time) (int64, error) { + f := bson.M{"uid": uid, "createdAt": bson.M{"$gte": start, "$lt": end}} + c, err := coll(nil).Count(f) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "NextProxyCount", table, "count", err), log.Any("filter", f)) + return 0, err + } + return c, nil +} + +// UserInvitees 获取用户推广人数 +func UserInvitees(startTime time.Time, endTime time.Time) (map[uint64]int, error) { + data := make(map[uint64]int) + infos := []InviteeCount{} + pipeline := []bson.M{ + { + "$match": bson.M{"createdAt": bson.M{"$gt": startTime, "$lte": endTime}}, + }, + { + "$group": bson.M{"_id": "$uid", "count": bson.M{"$sum": 1}}, + }, + } + if err := coll(nil).Aggregate(&infos, pipeline); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UserInvitees", table, "Aggregate", err), + log.Any("startTime", startTime), + log.Any("endTime", endTime)) + return data, err + } + for _, v := range infos { + data[v.UID] = v.Count + } + return data, nil +} + +func StatCenterSyncInviteList(time time.Time, size int64) ([]Invitation, error) { + opt := (&options.FindOptions{}) + opt.SetLimit(size) + opt.SetSort(bson.M{"inviteTime": 1}) + filter := bson.M{"inviteTime": bson.M{"$gt": time}} + data := make([]Invitation, 0) + return data, coll(nil).Find(&data, filter, opt) +} + +func NewStatCenterSyncInviteList(id string, size int64) ([]Invitation, error) { + opt := (&options.FindOptions{}) + opt.SetLimit(size) + opt.SetSort(bson.M{"_id": 1}) + _id, _ := primitive.ObjectIDFromHex(id) + filter := bson.M{"_id": bson.M{"$gt": _id}} + data := make([]Invitation, 0) + return data, coll(nil).Find(&data, filter, opt) +} + +// FindManyByApp app分页查询所有 +func FindManyByApp(uid, pageNumber, pageSize uint64) (total int64, data []*Invitation, err error) { + filter := bson.M{"uid": uid} + opts := &options.FindOptions{} + opts.SetSkip(int64((pageNumber - 1) * pageSize)).SetLimit(int64(pageSize + 1)) + opts.SetSort(bson.D{{Key: "createdAt", Value: -1}}) + data = make([]*Invitation, 0) + if err = coll(nil).Find(&data, filter, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindManyByApp", table, "Find", err), log.Any("filter", filter)) + return + } + total, err = coll(nil).Count(filter) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindManyByApp", table, "Count", err), log.Any("filter", filter)) + } + return +} + +// FindTotal 查询所有总数 +func FindTotal(uid uint64) (total int64, err error) { + filter := bson.M{"uid": uid} + total, err = coll(nil).Count(filter) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindTotal", table, "Count", err), log.Any("filter", filter)) + } + return +} + +// UpdateByUid 修改 +func UpdateByUid(t *db.MongoTool, uid, proxyUserId uint64) error { + cond := bson.M{"isRecharge": true, "updateAt": time.Now()} + if _, err := coll(t).UpdateOne(bson.M{"invitee": uid, "uid": proxyUserId}, bson.M{"$set": cond}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateByUid", table, "UpdateOne", err), + log.Any("uid", uid), + ) + return err + } + return nil +} diff --git a/models/v/proxymod/type.go b/models/v/proxymod/type.go new file mode 100644 index 0000000..43c19af --- /dev/null +++ b/models/v/proxymod/type.go @@ -0,0 +1,43 @@ +package proxymod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const FreeDay int64 = 3 + +var mdb *db.MongoDB + +const ( + LoginType = "login" + BindType = "bind" +) + +// Invitation 邀请关系 +type Invitation struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 推广邀请id + UID uint64 `json:"uid" bson:"uid"` // 邀请用户id + Invitee uint64 `json:"invitee" bson:"invitee"` // 被邀请的用户id,也就是一级用户 + InviteePortrait string `json:"inviteePortrait" bson:"inviteePortrait"` // 被邀请人头像 + InviteeName string `json:"inviteeName" bson:"inviteeName"` // 被邀请的用户姓名 + InviteTime time.Time `json:"inviteTime" bson:"inviteTime"` // 手动绑定时间 + InviteCode string `json:"inviteCode" bson:"InviteCode"` // 邀请码 + IsRecharge bool `json:"isRecharge" bson:"isRecharge"` // 邀请用户是否充值 + UpdateAt time.Time `json:"updateAt" bson:"updateAt"` // 更新时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创时时间 +} + +// InviteeCount 用户推广人数统计 +type InviteeCount struct { + UID uint64 ` json:"uid" bson:"_id"` //邀请用户id + Count int `json:"count" bson:"count"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/proxymod/web.go b/models/v/proxymod/web.go new file mode 100644 index 0000000..4e9eada --- /dev/null +++ b/models/v/proxymod/web.go @@ -0,0 +1,16 @@ +package proxymod + +import "time" + +// Invitation 邀请关系 +type InvitationQueryReq struct { + UID *uint64 `form:"uid" json:"uid,omitempty" bson:"uid"` //邀请用户id +} + +// InvitationWebRes 查询结构返回体 +type InvitationWebRes struct { + UID uint64 `json:"uid"` + IID uint64 `json:"iid"` + Lv int `json:"lv"` + CreatedAt time.Time `json:"createdAt"` +} diff --git a/models/v/proxyrecordmod/record.go b/models/v/proxyrecordmod/record.go new file mode 100644 index 0000000..f93d57b --- /dev/null +++ b/models/v/proxyrecordmod/record.go @@ -0,0 +1,51 @@ +package proxyrecordmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +const table = models.InviteLog + +// initIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "promotionCode", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InsertInvitationRecord 推广记录插入 +func InsertInvitationRecord(iR *InvitationRecord) (err error) { + iR.CreatedAt = time.Now() + if _, err = coll(nil).InsertOne(iR); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertInvitationRecord", table, "InsertOne", err)) + return + } + return +} diff --git a/models/v/proxyrecordmod/type.go b/models/v/proxyrecordmod/type.go new file mode 100644 index 0000000..4da32c1 --- /dev/null +++ b/models/v/proxyrecordmod/type.go @@ -0,0 +1,25 @@ +package proxyrecordmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +// InvitationRecord 邀请记录 +type InvitationRecord struct { + ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` //推广邀请id + UID uint64 `json:"uid" bson:"uid"` //邀请用户id + PromotionCode string `json:"promotionCode" bson:"promotionCode"` //邀请码 + URL string `json:"url" bson:"url"` + CreatedAt time.Time `bson:"createdAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/proxyrecordmod/web.go b/models/v/proxyrecordmod/web.go new file mode 100644 index 0000000..5ce2469 --- /dev/null +++ b/models/v/proxyrecordmod/web.go @@ -0,0 +1,19 @@ +package proxyrecordmod + +import "time" + +// Invitation 邀请关系 +type InvitationQueryReq struct { + ID *string `form:"id" json:"_id,omitempty" bson:"_id,omitempty"` //推广邀请id + UID *uint64 `form:"uid" json:"uid,omitempty" bson:"uid"` //邀请用户id + Invitees *uint64 `form:"invitee" json:"invitee,omitempty" bson:"invitee"` //被邀请的用户id,也就是一级用户 + PromotionCode *string `form:"promotionCode" bson:"promotionCode,omitempty" json:"promotionCode"` //邀请码 +} + +// InvitationWebRes 查询结构返回体 +type InvitationWebRes struct { + UID uint64 `json:"uid"` + IID uint64 `json:"iid"` + Lv int `json:"lv"` + CreatedAt time.Time `json:"createdAt"` +} diff --git a/models/v/pushmod/type.go b/models/v/pushmod/type.go new file mode 100644 index 0000000..f6d9010 --- /dev/null +++ b/models/v/pushmod/type.go @@ -0,0 +1,35 @@ +package pushmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// PushModel 视频推送模型 +type PushModel struct { + VideoID primitive.ObjectID `json:"videoID" bson:"videoID"` + Title string `json:"title" bson:"title"` + Type string `json:"type" bson:"type"` //视频类型 COVER-图集 SP-短视频 + PushAt1 time.Time `json:"pushAt1" bson:"pushAt1"` + PushAt2 time.Time `json:"pushAt2" bson:"pushAt2"` + PushAt3 time.Time `json:"pushAt3" bson:"pushAt3"` + IsCurcle bool `json:"isCurcle" bson:"isCurcle"` + Position int `json:"position" bson:"position"` //推送位置 0 所有 1锁屏 2官方 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 +} + +// 推荐视频推送模型 +type RecoPushModel struct { + VideoID primitive.ObjectID `json:"videoID" bson:"videoID"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 +} + +type PushModelDoc = PushModel + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/pushmod/vid.go b/models/v/pushmod/vid.go new file mode 100644 index 0000000..6c008da --- /dev/null +++ b/models/v/pushmod/vid.go @@ -0,0 +1,145 @@ +package pushmod + +import ( + "fmt" + "math" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "type", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +var mdb *db.MongoDB + +const table = models.PushModel + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InsertOne 插入一条数据 +func InsertOne(doc PushModelDoc) error { + _, err := coll(nil).InsertOne(doc) + return err +} + +// Delete 删除数据 +func Delete(cond bson.M) (int64, error) { + result, err := coll(nil).DeleteMany(cond) + if err != nil { + return 0, err + } + return result.DeletedCount, err +} + +// GetTotalCnt 获取标签视频总数 +func GetTotalCnt(cond bson.M) (int64, error) { + total, err := coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetTotalCnt", table, "Count", err), log.Any("cond", cond)) + return 0, err + } + return total, nil +} + +// GetSkipSize 计算跳转 +func GetSkipSize(page, size uint64, cond bson.M) (int, int, int64, error) { + total, err := GetTotalCnt(cond) + if err != nil { + return 0, 0, 0, err + } + totalpages := uint64(math.Ceil(float64(total) / float64(size))) + if page > totalpages { + page = totalpages + } + if page < 1 { + page = 1 + } + return int((page - 1) * size), int(totalpages), total, nil +} + +// GetPushList 条件获取推送视频列表 +func GetPushList(page, size uint64, cond bson.M, sort bson.D) ([]*PushModel, int, int64, error) { + skip, totalPages, total, err := GetSkipSize(page, size, cond) + if err != nil { + return nil, 0, 0, err + } + opts := options.FindOptions{} + if sort != nil { + opts.SetSort(sort) + } + opts.SetSkip(int64(skip)).SetLimit(int64(size)) + var back []*PushModel + if err = coll(nil).Find(&back, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetPushList", table, "Find", err), log.Any("cond", cond)) + return nil, totalPages, total, err + } + return back, totalPages, total, nil +} + +// IsExist 判断视频是否存在 +func IsExist(id primitive.ObjectID) (bool, error) { + return coll(nil).Exists(bson.M{"videoID": id}) +} + +// 获取短视频强推列表 +func GetPushSpVidForRecommend(line time.Time, newsType string, page, size uint64) ([]string, time.Time, error) { + var maxCreateLine time.Time + cond := bson.M{"type": newsType} + pushVidInfos, err := getPushVidForRecommend(line, cond, page, size) + if err != nil { + return nil, maxCreateLine, nil + } + vids := make([]primitive.ObjectID, 0, len(pushVidInfos)) + for _, i := range pushVidInfos { + if i == nil { + continue + } + if i.CreatedAt.After(maxCreateLine) { + maxCreateLine = i.CreatedAt + } + vids = append(vids, i.VideoID) + } + arr := common.ObjectIDs2String(vids) + return arr, maxCreateLine, nil +} + +// getPushVidForRecommend 获取官方强推视频列表 +func getPushVidForRecommend(line time.Time, cond bson.M, page, size uint64) ([]*RecoPushModel, error) { + sort := bson.D{{Key: "createdAt", Value: 1}} + cond["createdAt"] = bson.M{"$gt": line} + opt := options.FindOptions{} + opt.SetProjection(bson.M{"videoID": 1, "createdAt": 1}) + opt.SetLimit(int64(size)).SetSkip(int64((page - 1) * size)).SetSort(sort) + var back []*RecoPushModel + if err := coll(nil).Find(&back, cond, &opt); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getPushVidForRecommend", table, "Find", err), log.Any("cond", cond)) + return nil, err + } + return back, nil +} diff --git a/models/v/pushmod/web.go b/models/v/pushmod/web.go new file mode 100644 index 0000000..cd9dccd --- /dev/null +++ b/models/v/pushmod/web.go @@ -0,0 +1,31 @@ +package pushmod + +import "91porn-server/models/commod" + +// AddReq 推送添加 +type AddReq struct { + VideoID string `form:"videoID" json:"videoID"` + Title string `form:"title" json:"title"` +} + +// ListReq 推送列表 +type ListReq struct { + VideoID string `form:"videoID" json:"videoID"` + commod.Page +} + +// DeleteReq 推送删除请求 +type DeleteReq struct { + IDs []string `form:"ids" json:"ids"` +} + +// ListResp 推送视频列表应答 +type ListResp struct { + VInfos []*PushModel `json:"vInfos"` + Total int64 `json:"total"` +} + +// OperateResult 更新或者删除的操作返回结果 +type OperateResult struct { + Count int64 `json:"count"` +} diff --git a/models/v/questionnremod/questionnre.go b/models/v/questionnremod/questionnre.go new file mode 100644 index 0000000..fc0e1e8 --- /dev/null +++ b/models/v/questionnremod/questionnre.go @@ -0,0 +1,56 @@ +package questionnremod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.Questionnaire + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "level", Value: 1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "level", Value: 1}, {Key: "questionnaireId", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func Insert(q Questionnaire) error { + q.CreatedAt = time.Now() + _, err := coll(nil).InsertOne(q) + return err +} + +func GetByUID(uid uint64) ([]Questionnaire, error) { + data := make([]Questionnaire, 0) + f := bson.M{"uid": uid} + return data, coll(nil).Find(&data, f) +} diff --git a/models/v/questionnremod/type.go b/models/v/questionnremod/type.go new file mode 100644 index 0000000..1a41dac --- /dev/null +++ b/models/v/questionnremod/type.go @@ -0,0 +1,36 @@ +package questionnremod + +import ( + "time" + + "91porn-server/common/db" +) + +var mdb *db.MongoDB + +type Questionnaire struct { + QuestionId uint64 `json:"questionId" bson:"_id"` + QuestionnaireId uint64 `json:"questionnaireId" bson:"questionnaireId"` + Level int `json:"level" bson:"level"` + Questions []Question `json:"questions" bson:"questions"` //答案 + Codes []string `json:"codes" bson:"codes"` + UID uint64 `json:"uid" bson:"uid"` //用户id + Gold int `json:"gold" bson:"gold"` //金币 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 +} + +type Question struct { + Question int `json:"question" bson:"question"` //问题序号 + Answer string `json:"answer" bson:"answer"` //答案序号 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type SubmitRes struct { + QuestionId uint64 `json:"questionId"` + Gold int `json:"gold" bson:"gold"` //金币 + Codes []string `json:"codes" bson:"codes"` +} diff --git a/models/v/quicksearchmod/app.go b/models/v/quicksearchmod/app.go new file mode 100755 index 0000000..2c0d8f4 --- /dev/null +++ b/models/v/quicksearchmod/app.go @@ -0,0 +1,36 @@ +package quicksearchmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// QuickSearchInfo 移动端返回内容 +type QuickSearchInfo struct { + ID primitive.ObjectID `json:"id"` // 配置ID + Enabled bool `json:"enabled"` // 是否开启 + Title string `json:"title"` // 展示标题 + SearchKeyword string `json:"searchKeyword"` // 搜索关键字 + Link string `json:"link"` // 跳转链接 + Type int `json:"type"` // 配置类型 1:置顶,2:大家都在搜 + CreatedAt time.Time `json:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt"` // 更新时间 +} + +// AppQuickSearchBaseInfo 移动端返回内容 +type AppQuickSearchBaseInfo struct { + ID primitive.ObjectID `json:"id"` // 配置ID + Title string `json:"title"` // 展示标题 + SearchKeyword string `json:"searchKeyword"` // 搜索关键字 + Link string `json:"link"` // 跳转链接 + LinkType int `json:"linkType" bson:"linkType"` // 跳转链接类型 + Type int `json:"type"` // 配置类型 1:置顶,2:大家都在搜 + UserID uint64 `json:"userID" bson:"userID"` // 发表评论的 用户ID + UserName string `json:"userName" bson:"userName"` // 发表评论的 用户昵称 + UserPortrait string `json:"userPortrait" bson:"userPortrait" ` // 头像 URL + Gender int `json:"gender" bson:"gender"` // 性别 + VipLevel int `json:"vipLevel" bson:"vipLevel"` // VIP等级 + VipExpireDate time.Time `json:"vipExpireDate" bson:"vipExpireDate"` // VIP到期时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 +} diff --git a/models/v/quicksearchmod/quicksearch.go b/models/v/quicksearchmod/quicksearch.go new file mode 100755 index 0000000..3af7026 --- /dev/null +++ b/models/v/quicksearchmod/quicksearch.go @@ -0,0 +1,139 @@ +package quicksearchmod + +import ( + "encoding/json" + "errors" + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []QuickSearch, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "createdAt", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// GetAll 查询全部文档 +func GetAll(filter primitive.M) (out []*QuickSearch, err error) { + opts := options.Find().SetSort(bson.D{{Key: "_id", Value: -1}}) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAll", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (QuickSearch, error) { + v := QuickSearch{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d QuickSearch) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} diff --git a/models/v/quicksearchmod/type.go b/models/v/quicksearchmod/type.go new file mode 100755 index 0000000..f315c17 --- /dev/null +++ b/models/v/quicksearchmod/type.go @@ -0,0 +1,56 @@ +package quicksearchmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.QuickSearch + +type QuickSearch struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 配置ID + Enabled bool `json:"enabled" bson:"enabled"` // 是否开启 + Title string `json:"title" bson:"title"` // 展示标题 + SearchKeyword string `json:"searchKeyword" bson:"searchKeyword"` // 搜索关键字 + Link string `json:"link" bson:"link"` // 跳转链接 + LinkType int `json:"linkType" bson:"linkType"` // 跳转链接类型 1:内链跳转,2:外链跳转 + Type int `json:"type" bson:"type"` // 配置类型 1:评论置顶,2:评论大家都在搜 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "enabled", Value: 1}}, + }, + { + Keys: bson.D{{Key: "title", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} diff --git a/models/v/rchgamegoldmod/gold.go b/models/v/rchgamegoldmod/gold.go new file mode 100644 index 0000000..27e32e6 --- /dev/null +++ b/models/v/rchgamegoldmod/gold.go @@ -0,0 +1,156 @@ +package rchgamegoldmod + +import ( + "encoding/json" + "errors" + "fmt" + + "91porn-server/app/appg" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/models" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.GameGold + +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "coins", Value: 1}, {Key: "price", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "active", Value: 1}, {Key: "coins", Value: -1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +var trueActiveList = "gameGoldTrueActiveList" + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InsertOne 插入一条数据 +func InsertGold(g *Gold) error { + defer redisCachDel(trueActiveList) + if _, err := coll(nil).InsertOne(g); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertGold", table, "InsertOne", err), log.Any("g", g)) + return err + } + return nil +} + +// Delete 删除数据 +func DeleteGold(cond bson.M) (int64, error) { + defer redisCachDel(trueActiveList) + result, err := coll(nil).DeleteMany(cond) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteGold", table, "DeleteMany", err), log.Any("cond", cond)) + return 0, err + } + return result.DeletedCount, err +} + +// Update 金币配置更新 +func UpdateGold(cond bson.M, updt bson.M) (int64, error) { + defer redisCachDel(trueActiveList) + result, err := coll(nil).UpdateMany(cond, bson.M{"$set": updt}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateGold", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("updt", updt), + ) + return 0, err + } + return result.ModifiedCount, nil +} + +// GetGoldListByActiveTrue 条件获取金币配置表 +func GetGoldByID(id primitive.ObjectID) (*Gold, error) { + var g Gold + if err := coll(nil).FindOneByID(&g, id); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetGoldByID", table, "FindOneByID", err), log.Any("id", id)) + return nil, err + } + return &g, nil +} + +// GetGoldListByActiveTrue 条件获取金币配置表 +func GetGoldListByActiveTrue() ([]*Gold, error) { + var back []*Gold + redisKey := redisconst.DataCachKey(table, trueActiveList) + redisc := getRedis() + if redisc == nil || !redisc.Exists(redisKey) { + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "coins", Value: 1}}) + if err := coll(nil).Find(&back, bson.M{"active": true}, &opts); err != nil { + log.ZapLog.Warn("recharge GetGoldListByActiveTrue Find fail", log.E(err)) + return nil, err + } + if redisc == nil { + return back, nil + } + jsonBytes, err := json.Marshal(back) + if err != nil { + return back, err + } + _ = redisc.Set(redisKey, string(jsonBytes), redisconst.DataCachExpire) + return back, err + } + str, err := redisc.Get(redisKey) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetGoldListByActiveTrue", table, "Find", err)) + return nil, err + } + if str == nil { + return nil, errors.New("redis key is null") + } + if err = json.Unmarshal([]byte(*str), &back); err != nil { + log.Error(fmt.Sprintf("解析数据异常[%v]", err)) + return nil, err + } + return back, nil +} + +// GetGoldList 条件获取金币配置表 +func GetGoldList() ([]*Gold, error) { + opts := options.FindOptions{} + opts.SetSort(bson.M{"coins": 1}) + var back []*Gold + if err := coll(nil).Find(&back, bson.M{}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetGoldList", table, "Find", err)) + return nil, err + } + return back, nil +} + +func redisCachDel(m string) { + redisKey := redisconst.DataCachKey(table, m) + _, _ = getRedis().Del(redisKey) +} + +func getRedis() *redis.Client { + if appg.Redis != nil { + return appg.Redis + } + if webg.Redis != nil { + return webg.Redis + } + return nil +} diff --git a/models/v/rchgamegoldmod/type.go b/models/v/rchgamegoldmod/type.go new file mode 100644 index 0000000..e4857a5 --- /dev/null +++ b/models/v/rchgamegoldmod/type.go @@ -0,0 +1,51 @@ +package rchgamegoldmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +// Gold 金币配置表 +type Gold struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` //金币名称 + Coins int64 `json:"coins" bson:"coins"` //购买金币数 + Price int64 `json:"price" bson:"price"` //价格 单位分 + CouponDesc string `json:"couponDesc" bson:"couponDesc"` //优惠描述 + GiveVipDays int `json:"giveVipDays" bson:"giveVipDays"` //赠送vip天数 + LouFengUnlockTimes int `json:"louFengUnlockTimes" bson:"louFengUnlockTimes"` //赠送楼凤解锁次数 + GiveGameCoin int64 `json:"giveGameCoin" bson:"giveGameCoin"` //赠送游戏金币 + NotFirst bool `json:"notFirst" bson:"notFirst"` //非首充依然赠送 + Active bool `json:"active" bson:"active"` //是否激活 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //刷新时间 +} + +type PayChannelRes struct { + Type string `bson:"type" json:"type"` //充值方式 + TypeName string `form:"typeName" json:"typeName" bson:"typeName" binding:"required"` //类型名称:支付宝,微信,银联 + Channel string `form:"channel" json:"channel" bson:"channel"` //渠道类型 鲨鱼 金鱼 + IncrAmount int64 `json:"incrAmount,omitempty" bson:"incrAmount,omitempty"` //增加的优惠额度 + IncTax float64 `json:"incTax,omitempty" bson:"incTax,omitempty"` //按比率增加额外优惠额 0-1之间 ,如果 incrAmount 与 incrTax 同时存在 以 incrAmount 为准 +} + +// GoldRes 充值金额列表 +type GoldRes struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` //表id + Amount int64 `json:"amount" bson:"amount" binding:"required"` //货币数量 + Money int64 `json:"money" bson:"money"` //货币价格 + TypeName string `json:"typeName" bson:"typeName" binding:"required"` //类型名称 + RechargeType []PayChannelRes `json:"rchgType" bson:"rchgType"` //充值方式 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +const RedisKey = "rchgamegoldmod" diff --git a/models/v/rchgamegoldmod/web.go b/models/v/rchgamegoldmod/web.go new file mode 100644 index 0000000..e2a7a70 --- /dev/null +++ b/models/v/rchgamegoldmod/web.go @@ -0,0 +1,33 @@ +package rchgamegoldmod + +import "time" + +// DelReq 删除请求 +type DelReq struct { + IDs []string `form:"ids" json:"ids"` +} + +// OpeResp 操作应答 +type OpeResp struct { + Count int64 `json:"count"` +} + +// GoldReq 金币配置添加 +type GoldReq struct { + Name string `form:"name" json:"name"` //金币名称 + Coins int64 `form:"coins" json:"coins"` //购买金币数 + Price int64 `form:"price" json:"price"` //价格 + CouponDesc string `form:"couponDesc" json:"couponDesc"` //优惠描述 + GiveVipDays int `form:"giveVipDays" json:"giveVipDays"` //赠送vip天数 + LouFengUnlockTimes int `form:"louFengUnlockTimes" json:"louFengUnlockTimes"` //赠送楼凤解锁次数 + GiveGameCoin int64 `form:"giveGameCoin" json:"giveGameCoin"` //赠送游戏金币 + NotFirst bool `form:"notFirst" json:"notFirst"` //非首充依然赠送 + Active bool `form:"active" json:"active"` //是否激活 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +// EditGoldReq 金币编辑请求 +type EditGoldReq struct { + ID string `form:"id" json:"id"` + GoldReq +} diff --git a/models/v/rchgamtmod/gold.go b/models/v/rchgamtmod/gold.go new file mode 100644 index 0000000..13850e5 --- /dev/null +++ b/models/v/rchgamtmod/gold.go @@ -0,0 +1,151 @@ +package rchgamtmod + +import ( + "encoding/json" + "errors" + "fmt" + + "91porn-server/app/appg" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/models" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.Gold + +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "coins", Value: 1}, {Key: "price", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +var trueActiveList = "trueActiveList" + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InsertOne 插入一条数据 +func InsertGold(g *Gold) error { + defer redisCachDel(trueActiveList) + if _, err := coll(nil).InsertOne(g); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertGold", table, "InsertOne", err), log.Any("g", g)) + return err + } + return nil +} + +// Delete 删除数据 +func DeleteGold(cond bson.M) (int64, error) { + defer redisCachDel(trueActiveList) + result, err := coll(nil).DeleteMany(cond) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteGold", table, "DeleteMany", err), log.Any("cond", cond)) + return 0, err + } + return result.DeletedCount, err +} + +// Update 金币配置更新 +func UpdateGold(cond bson.M, updt bson.M) (int64, error) { + defer redisCachDel(trueActiveList) + result, err := coll(nil).UpdateMany(cond, bson.M{"$set": updt}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateGold", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("updt", updt), + ) + return 0, err + } + return result.ModifiedCount, nil +} + +// GetGoldListByActiveTrue 条件获取金币配置表 +func GetGoldByID(id primitive.ObjectID) (*Gold, error) { + var g Gold + if err := coll(nil).FindOneByID(&g, id); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetGoldByID", table, "FindOneByID", err), log.Any("id", id)) + return nil, err + } + return &g, nil +} + +// GetGoldListByActiveTrue 条件获取金币配置表 +func GetGoldListByActiveTrue() ([]*Gold, error) { + var back []*Gold + redisKey := redisconst.DataCachKey(table, trueActiveList) + var redisc *redis.Client + if redisc == nil || !redisc.Exists(redisKey) { + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "coins", Value: 1}}) + err := coll(nil).Find(&back, bson.M{"active": true}, &opts) + if err != nil { + log.ZapLog.Warn("recharge GetGoldListByActiveTrue Find fail", log.E(err)) + return nil, err + } + if redisc == nil { + return back, err + } + jsonBytes, err := json.Marshal(back) + if err != nil { + return back, err + } + _ = redisc.Set(redisKey, string(jsonBytes), redisconst.DataCachExpire) + return back, err + } + str, err := redisc.Get(redisKey) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetGoldListByActiveTrue", table, "Find", err)) + return nil, err + } + if str == nil { + return back, errors.New("redis key is null") + } + return back, json.Unmarshal([]byte(*str), &back) +} + +// GetGoldList 条件获取金币配置表 +func GetGoldList() ([]*Gold, error) { + opts := options.FindOptions{} + opts.SetSort(bson.M{"coins": 1}) + var back []*Gold + err := coll(nil).Find(&back, bson.M{}, &opts) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetGoldList", table, "Find", err)) + return nil, err + } + return back, nil +} + +func redisCachDel(m string) { + redisKey := redisconst.DataCachKey(table, m) + _, _ = getRedis().Del(redisKey) +} + +func getRedis() *redis.Client { + if appg.Redis != nil { + return appg.Redis + } + if webg.Redis != nil { + return webg.Redis + } + return nil +} diff --git a/models/v/rchgamtmod/type.go b/models/v/rchgamtmod/type.go new file mode 100644 index 0000000..7e98781 --- /dev/null +++ b/models/v/rchgamtmod/type.go @@ -0,0 +1,64 @@ +package rchgamtmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +// Gold 金币配置表 +type Gold struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` //金币名称 + Coins int64 `json:"coins" bson:"coins"` //购买金币数 + Price int64 `json:"price" bson:"price"` //价格 + Active bool `json:"active" bson:"active"` //是否激活 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //刷新时间 +} + +type PayChannelRes struct { + //充值方式 + Type string `bson:"type" json:"type"` + //类型名称:支付宝,微信,银联 + TypeName string `form:"typeName" json:"typeName" bson:"typeName" binding:"required"` + //渠道类型 鲨鱼 金鱼 + Channel string `form:"channel" json:"channel" bson:"channel"` + //增加的优惠额度 + IncrAmount int64 `json:"incrAmount,omitempty" bson:"incrAmount,omitempty"` + //按比率增加额外优惠额 0-1之间 ,如果 incrAmount 与 incrTax 同时存在 以 incrAmount 为准 + IncTax float64 `json:"incTax,omitempty" bson:"incTax,omitempty"` +} + +// GoldRes 充值金额列表 +type GoldRes struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` //表id + Amount int64 `json:"amount" bson:"amount" binding:"required"` //货币数量 + Money int64 `json:"money" bson:"money"` //货币价格 + TypeName string `json:"typeName" bson:"typeName" binding:"required"` //类型名称 + RechargeType []PayChannelRes `json:"rchgType" bson:"rchgType"` //充值方式 +} + +// GoldRes 游戏充值金额列表 +type GameGoldRes struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` //表id + Amount int64 `json:"amount" bson:"amount" binding:"required"` //货币数量 + Money int64 `json:"money" bson:"money"` //货币价格 + CouponDesc string `json:"couponDesc" bson:"couponDesc"` //优惠描述 + GiveVipDays int `json:"giveVipDays" bson:"giveVipDays"` //赠送vip天数 + LouFengUnlockTimes int `json:"louFengUnlockTimes" bson:"louFengUnlockTimes"` //赠送楼凤解锁次数 + GiveGameCoin int64 `json:"giveGameCoin" bson:"giveGameCoin"` //赠送游戏金币 + TypeName string `json:"typeName" bson:"typeName" binding:"required"` //类型名称 + RechargeType []PayChannelRes `json:"rchgType" bson:"rchgType"` //充值方式 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +const RedisKey = "rchgamtmod" diff --git a/models/v/rchgamtmod/web.go b/models/v/rchgamtmod/web.go new file mode 100644 index 0000000..322655b --- /dev/null +++ b/models/v/rchgamtmod/web.go @@ -0,0 +1,28 @@ +package rchgamtmod + +import "time" + +// DelReq 删除请求 +type DelReq struct { + IDs []string `form:"ids" json:"ids"` +} + +// OpeResp 操作应答 +type OpeResp struct { + Count int64 `json:"count"` +} + +// GoldReq 金币配置添加 +type GoldReq struct { + Name string `form:"name" json:"name"` //金币名称 + Coins int64 `form:"coins" json:"coins"` //购买金币数 + Price int64 `form:"price" json:"price"` //价格 + Active bool `form:"active" json:"active"` //是否激活 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +// EditGoldReq 金币编辑请求 +type EditGoldReq struct { + ID string `form:"id" json:"id"` + GoldReq +} diff --git a/models/v/rchgchanmod/paychannel.go b/models/v/rchgchanmod/paychannel.go new file mode 100644 index 0000000..fe0b13b --- /dev/null +++ b/models/v/rchgchanmod/paychannel.go @@ -0,0 +1,190 @@ +package rchgchanmod + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "91porn-server/app/appg" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/models" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.PayChannel + +var rchgChansCach = "rchgChansCach:%s" + +// initIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "cid", Value: 1}, {Key: "payType", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "active", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InsertOne 插入一条数据 +func InsertPayChannel(p *PayChannel) error { + defer redisCachDel(rchgChansCach) + if _, err := coll(nil).InsertOne(p); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertPayChannel", table, "InsertOne", err)) + return err + } + return nil +} + +// Delete 删除数据 +func DeletePayChannel(ids []string) (int64, error) { + defer redisCachDel(rchgChansCach) + objIDs := make([]primitive.ObjectID, len(ids)) + for i, v := range ids { + objIDs[i], _ = primitive.ObjectIDFromHex(v) + } + result, err := coll(nil).DeleteMany(bson.M{"_id": bson.M{"$in": objIDs}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeletePayChannel", table, "DeleteMany", err)) + return 0, err + } + return result.DeletedCount, err +} + +// UpdateAmountTypesByCID +func UpdateAmountTypesByCID(cid string, amountTypes []int64) (int64, error) { + defer redisCachDel(rchgChansCach) + result, err := coll(nil).UpdateOne(bson.M{"cid": cid}, bson.M{"$set": bson.M{"amountTypes": amountTypes, "updatedAt": time.Now()}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdatePayChannelBaseOnCID", table, "UpdateOne", err), + log.Any("cid", cid), + log.Any("amountTypes", amountTypes), + ) + return 0, err + } + return result.ModifiedCount, nil +} + +// Update +func UpdatePayChannel(id string, set EditInfo) (int64, error) { + defer redisCachDel(rchgChansCach) + set.UpdatedAt = time.Now() + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdatePayChannel", table, "ObjectIDFromHex", err), + log.Any("id", id), + log.Any("set", set), + ) + return 0, err + } + result, err := coll(nil).UpdateOne(bson.M{"_id": objID}, bson.M{"$set": set}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdatePayChannel", table, "UpdateOne", err), + log.Any("id", id), + log.Any("set", set), + ) + return 0, err + } + return result.ModifiedCount, nil +} + +// GetPayChannels 条件获取支付渠道列表 +func GetPayChannels() ([]*PayChannel, error) { + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "active", Value: -1}, {Key: "updatedAt", Value: -1}}) + var back []*PayChannel + if err := coll(nil).Find(&back, bson.M{}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetPayChannels", table, "Find", err)) + return nil, err + } + return back, nil +} + +// GetPayChannelsByMoney 条件获取支付渠道列表 +func GetPayChannelsByMoney(money int64, payType string) ([]*PayChannel, error) { + var back []*PayChannel + if err := coll(nil).Aggregate(&back, []bson.M{ + {"$match": bson.M{"payType": payType}}, + {"$match": bson.M{"maxMoney": bson.M{"$gte": money}}}, + {"$match": bson.M{"minMoney": bson.M{"$lte": money}}}, + {"$match": bson.M{"active": true}}, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetPayChannelsByMoney", table, "Aggregate", err)) + return nil, err + } + return back, nil +} + +// GetPayChannelsByMoney 条件获取支付渠道列表 +func GetActiveTruePayChannels(mode string) ([]PayChannel, error) { + var back []PayChannel + redisKey := redisconst.DataCachKey(table, fmt.Sprintf(rchgChansCach, mode)) + redisc := getRedis() + if redisc == nil || !redisc.Exists(redisKey) { + modeSet := getModeSet(mode) + if err := coll(nil).Find(&back, bson.M{"active": true, "model_sup": bson.M{"$bitsAnySet": modeSet}}); err != nil { + log.ZapLog.Warn("recharge GetPayChannel Find fail", log.E(err)) + return nil, err + } + if redisc == nil { + return back, nil + } + jsonBytes, err := json.Marshal(back) + if err != nil { + return back, err + } + _ = redisc.Set(redisKey, string(jsonBytes), redisconst.DataCachExpire) + return back, nil + } + str, err := redisc.Get(redisKey) + if err != nil { + return back, err + } + if str == nil { + return back, errors.New("redis key is null") + } + return back, json.Unmarshal([]byte(*str), &back) +} + +func redisCachDel(m string) { + redisKey := redisconst.DataCachKey(table, m) + _, _ = getRedis().Del(redisKey) +} + +func getRedis() *redis.Client { + if appg.Redis != nil { + return appg.Redis + } + if webg.Redis != nil { + return webg.Redis + } + return nil +} diff --git a/models/v/rchgchanmod/type.go b/models/v/rchgchanmod/type.go new file mode 100644 index 0000000..cd38384 --- /dev/null +++ b/models/v/rchgchanmod/type.go @@ -0,0 +1,77 @@ +package rchgchanmod + +import ( + "time" + + "91porn-server/common/constant" + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +type CATTEGORY string + +const ( + CATTEGORY_FIXED CATTEGORY = "fixed" //固定 + CATTEGORY_ACTIVE CATTEGORY = "active" //活动 +) + +// PayChannel 支付渠道 +type PayChannel struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + ChannelName string `json:"channelName" bson:"channelName"` //支付渠道名字 + CID string `json:"cid" bson:"cid"` //渠道id + PayType string `json:"payType" bson:"payType"` //支付方式 + MinMoney int `json:"minMoney" bson:"minMoney,omitempty"` //支持最小支付金额 + MaxMoney int `json:"maxMoney" bson:"maxMoney,omitempty"` //支持最大支付金额 + Weight int `json:"weight" bson:"weight"` //权重 + Active bool `json:"active" bson:"active"` //是否激活 + Rate string `json:"rate" bson:"rate"` //渠道费率 + ActivePeriod string `json:"activePeriod" bson:"activePeriod"` //激活时间段 6-23,7-5 + Category CATTEGORY `json:"category" bson:"category"` //金额类别 固定 fixed 活动 active + AmountTypes []int64 `json:"amountTypes" bson:"amountTypes,omitempty"` //金额种类 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //刷新时间 +} + +type PayChannelRes struct { + Type string `bson:"type" json:"type"` //充值方式 + TypeName string `form:"typeName" json:"typeName" bson:"typeName" binding:"required"` //类型名称:支付宝,微信,银联 + Channel string `form:"channel" json:"channel" bson:"channel"` //渠道类型 鲨鱼 金鱼 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +const ( + _ int = iota + modelSup_URL + modelSup_SDK +) + +type modelSet int64 + +func getModelBit(mode uint) int64 { + return int64(1 << (mode - 1)) +} +func (this *modelSet) setMode(model int) { + temp := getModelBit(uint(model)) + *this |= modelSet(temp) +} +func getModeSet(mode string) modelSet { + var modeSet modelSet = 0 + if mode == "" { + modeSet.setMode(modelSup_URL) + } + if mode == constant.RCHG_Mode_SDK { + modeSet.setMode(modelSup_URL) + modeSet.setMode(modelSup_SDK) + } + temp := getModelBit(uint(modeSet)) + modeSet |= modelSet(temp) + return modeSet +} diff --git a/models/v/rchgchanmod/web.go b/models/v/rchgchanmod/web.go new file mode 100644 index 0000000..37b004f --- /dev/null +++ b/models/v/rchgchanmod/web.go @@ -0,0 +1,51 @@ +package rchgchanmod + +import "time" + +// ChannelReq 新增支付渠道 +type ChannelReq struct { + ChannelName string `form:"channelName" json:"channelName"` //支付渠道名字 + CID string `form:"cid" json:"cid"` //渠道id + PayType string `form:"payType" json:"payType"` //支付方式 + MinMoney int `form:"minMoney,omitempty" json:"minMoney,omitempty"` //支持最小支付金额 + MaxMoney int `form:"maxMoney,omitempty" json:"maxMoney,omitempty"` //支持最大支付金额 + Weight int `form:"weight" json:"weight"` //权重 + Active bool `json:"active" bson:"active"` //是否激活 + Rate string `json:"rate" bson:"rate"` //渠道费率 + ActivePeriod string `json:"activePeriod" bson:"activePeriod"` //激活时间段 6-23,7-5 + Category CATTEGORY `json:"category,omitempty" bson:"category,omitempty"` //金额类别 固定 fixed 活动 active + AmountTypes []int64 `json:"amountTypes,omitempty" bson:"amountTypes,omitempty"` //金额种类 + +} + +// EditInfo 支付渠道编辑 +type EditInfo struct { + ChannelName *string `json:"channelName,omitempty" bson:"channelName,omitempty"` //支付渠道名字 + CID *string `json:"cid,omitempty" bson:"cid,omitempty"` //渠道id + PayType *string `bson:"payType,omitempty" json:"payType,omitempty"` //支付方式 + MinMoney *int `bson:"minMoney,omitempty" json:"minMoney,omitempty"` //支持最小支付金额 + MaxMoney *int `bson:"maxMoney,omitempty" json:"maxMoney,omitempty"` //支持最大支付金额 + Weight *int `bson:"weight,omitempty" json:"weight,omitempty"` //权重 + Active *bool `json:"active,omitempty" bson:"active,omitempty"` //是否激活 + Rate *string `json:"rate,omitempty" bson:"rate,omitempty"` //通道费率 + ActivePeriod *string `json:"activePeriod,omitempty" bson:"activePeriod,omitempty"` //激活时间段 6-23,7-5 + Category *CATTEGORY `json:"category,omitempty" bson:"category,omitempty"` //金额类别 固定 fixed 活动 active + AmountTypes *([]int64) `json:"amountTypes,omitempty" bson:"amountTypes,omitempty"` //金额种类 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +// EditReq 渠道编辑请求 +type EditReq struct { + ID string `form:"id" json:"id"` + EditInfo +} + +// DelReq 删除请求 +type DelReq struct { + IDs []string `form:"ids" json:"ids"` +} + +// OpeResp 操作应答 +type OpeResp struct { + Count int64 `json:"count"` +} diff --git a/models/v/rchgordmod/app.go b/models/v/rchgordmod/app.go new file mode 100644 index 0000000..edc9a8c --- /dev/null +++ b/models/v/rchgordmod/app.go @@ -0,0 +1,103 @@ +package rchgordmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type UpserSelector struct { + UID *uint64 `json:"uid,omitempty" bson:"uid,omitempty"` //用户id + OID *string `json:"oid,omitempty" bson:"oid,omitempty"` //支付平台订单号 + TraderId *string `json:"traderId,omitempty" bson:"traderId,omitempty"` //商户ID + UserIP *string `json:"userIp,omitempty" bson:"userIp,omitempty"` //用户IP + DevID *string `json:"devID,omitempty" bson:"devID,omitempty"` //设备id + Name *string `json:"name,omitempty" bson:"name,omitempty"` //用户名称 + Tel *string `json:"tel,omitempty" bson:"tel,omitempty"` //用户手机号 + DevType *string `json:"devType,omitempty" bson:"devType,omitempty"` //设备系统类型 ios pc android + Amount int64 `json:"amount" bson:"amount"` //金币数量 + Money *int64 `json:"money,omitempty" bson:"money,omitempty"` //充值金额 单位角 + PayMoney *int64 `json:"payMoney,omitempty" bson:"payMoney,omitempty"` //实际到账金额 + RechargeType *string `json:"rechargeType,omitempty" bson:"rechargeType,omitempty"` //充值类型 + Channel *string `json:"channel,omitempty" bson:"channel,omitempty"` //渠道类型 鲨鱼 金鱼 代充 + Status *int `json:"status" bson:"status"` //1进行中 2付款失败 3付款成功 4已经退款 + StatusDesc *string `json:"statusDesc,omitempty" bson:"statusDesc,omitempty"` //状态描述 + NotifyTime *int64 `json:"notifyTime,omitempty" bson:"notifyTime,omitempty"` //第三方回调的时间 + NotifyStatus *int `json:"notifyStatus,omitempty" bson:"notifyStatus,omitempty"` //通知第三方上分情况状态 + LockStatus *int `json:"lockStatus,omitempty" bson:"lockStatus,omitempty"` //订单操作锁定状态 用户订单更新操作 + FailureAt *time.Time `json:"failureAt,omitempty" bson:"failureAt,omitempty"` //回调失败时间 + SuccessAt *time.Time `json:"successAt,omitempty" bson:"successAt,omitempty"` //回调成功时间 + PaymentAt *time.Time `json:"paymentAt,omitempty" bson:"paymentAt,omitempty"` //支付时间 + UpdatedAt *time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` //刷新时间 + ProductType *int `json:"productType,omitempty" bson:"productType,omitempty"` //0站群 1棋牌 + ChanShareMod *int `json:"chanShareMod,omitempty" bson:"chanShareMod,omitempty"` //渠道分成类型 0 不分成 (默认) 1 金币充值分成 2正常vip消费分成 + DiscDoc DiscDoc `bson:",inline"` +} + +type ReplaceEditSelector struct { + OID *string `json:"oid,omitempty" bson:"oid,omitempty"` //支付平台订单号 + PayMoney *int64 `json:"payMoney,omitempty" bson:"payMoney,omitempty"` //实际到账金额 + Status *int `json:"status" bson:"status,omitempty"` //1进行中 2付款失败 3付款成功 4已经退款 + StatusDesc *string `json:"statusDesc,omitempty" bson:"statusDesc,omitempty"` //状态描述 + LockStatus *int `json:"lockStatus,omitempty" bson:"lockStatus,omitempty"` //订单操作锁定状态 用户订单更新操作 + NotifyStatus *int `json:"notifyStatus,omitempty" bson:"notifyStatus,omitempty"` //通知第三方上分情况状态 + Remark *string `json:"remark,omitempty" bson:"remark,omitempty"` //备注 + VipID primitive.ObjectID `json:"vipID,omitempty" bson:"vipID,omitempty"` //是否是vip冲 hi + ChanShareMod *int `json:"chanShareMod,omitempty" bson:"chanShareMod,omitempty"` //渠道分成类型 0 不分成 (默认) 1 金币充值分成 2正常vip消费分成 + ProgressAt *time.Time `json:"progressAt,omitempty" bson:"progressAt,omitempty"` //第三方下单成功时间 + FailureAt *time.Time `json:"failureAt,omitempty" bson:"failureAt,omitempty"` //回调失败时间 + SuccessAt *time.Time `json:"successAt,omitempty" bson:"successAt,omitempty"` //回调成功时间 + PaymentAt *time.Time `json:"paymentAt,omitempty" bson:"paymentAt,omitempty"` //支付时间 + UpdatedAt time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` //刷新时间 + ProductChildType *ChildType `json:"productChildType" bson:"productChildType"` //产品子类型 + ServiceID primitive.ObjectID `json:"serviceId" bson:"serviceId"` //裸聊服务ID + ProductID primitive.ObjectID `json:"productID" bson:"productID"` //产品ID +} + +type EditSelector struct { + OID *string `json:"oid,omitempty" bson:"oid,omitempty"` //支付平台订单号 + PayMoney *int64 `json:"payMoney,omitempty" bson:"payMoney,omitempty"` //实际到账金额 + Status int `json:"status" bson:"status"` //1进行中 2付款失败 3付款成功 4已经退款 + StatusDesc *string `json:"statusDesc,omitempty" bson:"statusDesc,omitempty"` //状态描述 + NotifyStatus *int `json:"notifyStatus,omitempty" bson:"notifyStatus,omitempty"` //通知第三方上分情况状态 + ProgressAt *time.Time `json:"progressAt,omitempty" bson:"progressAt,omitempty"` //第三方下单成功时间 + FailureAt *time.Time `json:"failureAt,omitempty" bson:"failureAt,omitempty"` //回调失败时间 + SuccessAt *time.Time `json:"successAt,omitempty" bson:"successAt,omitempty"` //回调成功时间 + PaymentAt *time.Time `json:"paymentAt,omitempty" bson:"paymentAt,omitempty"` //支付时间 + RefundAt *time.Time `json:"refundAt,omitempty" bson:"refundAt,omitempty"` //退款时间 + UpdatedAt time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` //刷新时间 + Mode *string `json:"mode,omitempty" bson:"mode,omitempty"` //支付模式 + Channel *string `json:"channel,omitempty" bson:"channel,omitempty"` //渠道类型 鲨鱼 金鱼 代充 + Rate *string `json:"rate,omitempty" bson:"rate,omitempty"` //渠道费率 + ProductType *int `json:"productType,omitempty" bson:"productType,omitempty"` //支付产品类型 0 站群 1 棋牌 + ChanShareMod *int `json:"chanShareMod,omitempty" bson:"chanShareMod,omitempty"` //渠道分成类型 + Remark *string `json:"remark,omitempty" bson:"remark,omitempty"` //备注 + +} + +type RechargToPayP struct { + RechargeType string `json:"rechargeType" binding:"required"` //充值类型 + ProductID primitive.ObjectID `json:"productID"` //充值金币类型id + VipID primitive.ObjectID `json:"vipID"` //是否是vip冲 hi + ProductType int `json:"productType"` //充值产品 0 站群 1 棋牌 + Money int64 `json:"money"` //金额 + ProductChildType ChildType `json:"productChildType"` //产品子类型 + ServiceID primitive.ObjectID `json:"serviceId"` //裸聊服务ID +} + +// RechargeOrder 账户充值流水 App返回给前端 +type RechargeOrderAppRes struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //流水id + OID string `json:"oid" bson:"oid"` //支付平台订单号 + UID uint64 `json:"uid" bson:"uid"` //用户id + Name string `json:"name" bson:"name"` //用户名称 + Tel string `json:"tel" bson:"tel"` //用户手机号 + Money int64 `json:"money" bson:"money"` //充值金额 订单金额 李秋山确认 + PayMoney int64 `json:"payMoney" bson:"payMoney"` //实际到账金额 用户实际支付金额 李秋山确认 + Status int `json:"status" bson:"status"` //1进行中 2付款失败 3付款成功 4已经退款 + StatusDesc string `json:"statusDesc" bson:"statusDesc"` //状态描述 + Remark string `json:"remark" bson:"remark"` //备注 + FailureAt time.Time `json:"failureAt" bson:"failureAt"` //回调失败时间 + SuccessAt time.Time `json:"successAt" bson:"successAt"` //回调成功时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 +} diff --git a/models/v/rchgordmod/discStat.go b/models/v/rchgordmod/discStat.go new file mode 100644 index 0000000..066623e --- /dev/null +++ b/models/v/rchgordmod/discStat.go @@ -0,0 +1,162 @@ +package rchgordmod + +import ( + "strings" + "time" + + "91porn-server/common/pageopt" + "91porn-server/common/ysinterface/disc" +) + +type DistrictStatKey struct { + DiscSeqe `bson:",inline"` //商区码 + DevType string `bson:"devType"` //系统类型 iOS Android +} + +func (d DistrictStatKey) GetDiscCode() string { + return d.DistrictCode +} + +func (d DistrictStatKey) GetPromSeqe() string { + return d.PromSeqe +} + +func (d DistrictStatKey) GetSysType() string { + return d.DevType +} + +func (d DistrictStatKey) String() string { + if d.DiscSeqe.String() == "" { + return "" + } + return strings.ToUpper(strings.Join([]string{d.DiscSeqe.String(), d.DevType}, "-")) +} + +type DistrictStater = disc.DistrictStater + +// DistrictOrderCountMap districtCode->订单数 map +func DistrictOrderCountMap(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + filter := pageopt.MergeM(mats) + list := []DistrictStatKey{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater]int64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v] += 1 + } + } + return m, nil +} + +// DistrictOrderMoneyMap districtCode->完成订单数 map +func DistrictOrderMoneyMap(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + filter := pageopt.MergeM(mats) + var list []struct { + DistrictStatKey `bson:",inline"` //商区码 + Money int64 `bson:"money"` //充值金额 订单金额 李秋山确认 + } + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater]int64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v.DistrictStatKey] += v.Money + } + } + return m, nil +} + +// DistrictCompleteOrderCountMap districtCode->订单金额 map +func DistrictCompleteOrderCountMap(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, (&SuccessAtGTEAndLTMatch{&start, &end}).New()) + filter := pageopt.MergeM(mats) + list := []DistrictStatKey{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater]int64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v] += 1 + } + } + return m, nil +} + +// DistrictCompleteOrderPayMoneyMap districtCode->完成订单的用户实际支付金额 map +func DistrictCompleteOrderPayMoneyMap(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, (&SuccessAtGTEAndLTMatch{&start, &end}).New()) + filter := pageopt.MergeM(mats) + var list []struct { + DistrictStatKey `bson:",inline"` //商区码 + PayMoney int64 `bson:"payMoney"` //实际到账金额 实际支付金额 李秋山确认 + } + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater]int64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v.DistrictStatKey] += v.PayMoney + } + } + return m, nil +} + +// DiscCompleteOrderUIDSMap districtCode->完成订单的用户列表 map key:District +func DiscCompleteOrderUIDSMap(start, end time.Time, mats ...Matcher) (map[DistrictStater][]uint64, error) { + mats = append(mats, (&SuccessAtGTEAndLTMatch{&start, &end}).New()) + filter := pageopt.MergeM(mats) + var list []struct { + DistrictCode string `json:"districtCode" bson:"districtCode"` //商区码 + UID uint64 `bson:"uid"` //uid + } + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater][]uint64, len(list)) + for _, v := range list { + key := DistrictStatKey{ + DiscSeqe: DiscSeqe{DistrictCode: v.DistrictCode}, + } + if key.String() != "" { + m[key] = append(m[key], v.UID) + } + } + return m, nil +} + +// DistrictCompleteOrderUIDSMap districtCode->完成订单的用户列表 map key:DiscSeqeSystem +func DistrictCompleteOrderUIDSMap(start, end time.Time, mats ...Matcher) (map[DistrictStater][]uint64, error) { + mats = append(mats, (&SuccessAtGTEAndLTMatch{&start, &end}).New()) + filter := pageopt.MergeM(mats) + var list []struct { + DistrictStatKey `bson:",inline"` //商区码 + UID uint64 `bson:"uid"` //uid + } + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater][]uint64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v.DistrictStatKey] = append(m[v.DistrictStatKey], v.UID) + } + } + return m, nil +} + +// 时间内购买VIP次数 +func CompleteOrderCountByDiscCode(start, end time.Time, discCode string) (int64, error) { + mats := []Matcher{ + (&SuccessAtGTEAndLTMatch{GTE: &start, LT: &end}).New(), + (&DistrictCodeMatch{DistrictCode: &discCode}).New(), + } + filter := pageopt.MergeM(mats) + return coll(nil).Count(filter) +} diff --git a/models/v/rchgordmod/list.go b/models/v/rchgordmod/list.go new file mode 100644 index 0000000..de04d43 --- /dev/null +++ b/models/v/rchgordmod/list.go @@ -0,0 +1,204 @@ +package rchgordmod + +import ( + "time" + + "91porn-server/common/log" + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Matcher = pageopt.Matcher + +// ChannelMatch +type ChannelMatch struct { + Channel *string +} + +func (c *ChannelMatch) New() Matcher { + return pageopt.NewAssignMatch("channel", c.Channel) +} + +// StatusMatch +type StatusMatch struct { + Status *int +} + +func (s *StatusMatch) New() Matcher { + return pageopt.NewAssignMatch("status", s.Status) +} + +// IDMatch +type IDMatch = pageopt.IDMatch + +// UIDMatch +type UIDMatch = pageopt.UIDMatch + +// UIDInMatch +type UIDInMatch = pageopt.UIDInMatch + +// RechargeTypeMatch +type RechargeTypeMatch struct { + RechargeType *string +} + +func (s *RechargeTypeMatch) New() Matcher { + return pageopt.NewAssignMatch("rechargeType", s.RechargeType) +} + +// RechargeTypeINMatch +type RechargeTypeINMatch struct { + RechargeTypes []RechType +} + +func (s *RechargeTypeINMatch) New() Matcher { + return pageopt.NewInMatch("rechargeType", s.RechargeTypes) +} + +// DistrictCodeMatch +type DistrictCodeMatch struct { + DistrictCode *string +} + +func (s *DistrictCodeMatch) New() Matcher { + return pageopt.NewAssignMatch("districtCode", s.DistrictCode) +} + +// PromSeqeMatch +type PromSeqeMatch struct { + PromSeqe *string +} + +func (s *PromSeqeMatch) New() Matcher { + return pageopt.NewAssignMatch("promSeqe", s.PromSeqe) +} + +// IsDirectMatch +type IsDirectMatch struct { + IsDirect *bool +} + +func (b *IsDirectMatch) New() Matcher { + return pageopt.NewAssignMatch("isDirect", b.IsDirect) +} + +type DevTypeMatch struct { + DevType *string +} + +func (d *DevTypeMatch) New() Matcher { + return pageopt.NewAssignMatch("devType", d.DevType) +} + +// StatusInMatch +type StatusInMatch struct { + Status []int +} + +func (d *StatusInMatch) New() Matcher { + return pageopt.NewInMatch("status", d.Status) +} + +// NotifyStatusMatch +type NotifyStatusMatch struct { + NotifyStatus []int +} + +func (d *NotifyStatusMatch) New() Matcher { + return pageopt.NewInMatch("notifyStatus", d.NotifyStatus) +} + +// ProgressAtGTEAndLTMatch +type ProgressAtGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (s *ProgressAtGTEAndLTMatch) New() Matcher { + return pageopt.NewGTEAndLTMatch("progressAt", s.GTE, s.LT) +} + +// FailureAtGTEAndLTMatch +type FailureAtGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (s *FailureAtGTEAndLTMatch) New() Matcher { + return pageopt.NewGTEAndLTMatch("failureAt", s.GTE, s.LT) +} + +// SuccessAtGTEAndLTMatch +type SuccessAtGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (s *SuccessAtGTEAndLTMatch) New() Matcher { + return pageopt.NewGTEAndLTMatch("successAt", s.GTE, s.LT) +} + +//PaymentAtGTEAndLTMatch +//type PaymentAtGTEAndLTMatch struct { +// GTE *time.Time +// LT *time.Time +//} +// +//func (s *PaymentAtGTEAndLTMatch) New() Matcher { +// return pageopt.NewGTEAndLTMatch("paymentAt", s.GTE, s.LT) +//} + +// CreatedAtGTEAndLTMatch +type CreatedAtGTEAndLTMatch = pageopt.CreatedAtGTEAndLTMatch + +// CreatedAtLTMatch +type CreatedAtLTMatch = pageopt.CreatedAtLTMatch + +type Sort = bson.D + +type M = bson.M + +var Sort_CreatedAt_n1 = Sort{{Key: "createdAt", Value: -1}} + +type RechargeOrderSlice []*RechargeOrder + +func (this RechargeOrderSlice) Map() map[primitive.ObjectID]*RechargeOrder { + m := map[primitive.ObjectID]*RechargeOrder{} + for _, v := range this { + m[v.ID] = v + } + return m +} + +func List(sort Sort, skip, limit *int64, matchers ...Matcher) (RechargeOrderSlice, error) { + filter := pageopt.MergeM(matchers) + opt := (&options.FindOptions{}) + if len(sort) != 0 { + opt.SetSort(sort) + } + if skip != nil { + opt.SetSkip(*skip) + } + if limit != nil { + opt.SetLimit(*limit) + } + list := RechargeOrderSlice{} + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.Error("rchgordmod List error", log.E(err)) + return nil, err + } + return list, nil +} + +func Count(matchers ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchers) + count, err := coll(nil).Count(filter) + if err != nil { + log.Error("rchgordmod Count error", log.E(err)) + return 0, err + } + return count, nil +} diff --git a/models/v/rchgordmod/rchgorder.go b/models/v/rchgordmod/rchgorder.go new file mode 100644 index 0000000..e1340d1 --- /dev/null +++ b/models/v/rchgordmod/rchgorder.go @@ -0,0 +1,775 @@ +/* + * @Description: 账户充值流水 + * @Author: your name + * @Date: 2019-08-29 20:45:27 + * @LastEditTime: 2019-08-30 13:44:36 + * @LastEditors: Please set LastEditors + */ +package rchgordmod + +import ( + "context" + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/common/timeutil" + "91porn-server/common/timeutil/timerange" + "91porn-server/models" + "91porn-server/models/v/usermod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.RechargeOrder + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitARDIndex 设置账户充值流水表索引 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "oid", Value: 1}}, + // Options: options.Index().SetUnique(true).SetPartialFilterExpression(bson.M{"oid": bson.M{"$gt": ""}}), + }, + { + Keys: bson.D{{Key: "channel", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "progressAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "paymentAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "failureAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "successAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "districtCode", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "isDirect", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "traderId", Value: 1}}, + }, + { + Keys: bson.D{{Key: "rechargeType", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "devType", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "productType", Value: 1}, {Key: "status", Value: 1}}, + }, + { + Keys: bson.D{{Key: "deductType", Value: 1}, {Key: "createdAt", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{ + {Key: "experimentId", Value: 1}, + {Key: "experimentVariant", Value: 1}, + {Key: "productID", Value: 1}, + {Key: "status", Value: 1}, + }, + Options: options.Index(). + SetName("vip_experiment_order_statistics"). + SetPartialFilterExpression(bson.M{"experimentId": bson.M{"$gt": ""}}), + }, + { + Keys: bson.D{{Key: "sourcePage", Value: 1}, {Key: "createdAt", Value: -1}}, + Options: options.Index(). + SetName("order_source_created_at"). + SetPartialFilterExpression(bson.M{"sourcePage": bson.M{"$gt": ""}}), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("RechageOrderIndex err+%v", err)) + } +} + +// InsertRechargeOrder 插入一条充值订单 +func InsertRechargeOrder(a *RechargeOrder) (err error) { + a.CreatedAt = time.Now() + a.UpdatedAt = a.CreatedAt + res, err := coll(nil).InsertOne(a) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertRechargeOrder", table, "InsertOne", err)) + return + } + a.ID = res.InsertedID.(primitive.ObjectID) + return +} + +// FindRechargesOrders 查询充值订单 +func FindRechargesOrders(cond bson.M, opts *options.FindOptions) (total int64, data []*RechargeOrder, err error) { + data = make([]*RechargeOrder, 0) + if err = coll(nil).Find(&data, cond, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindRechargesOrders", table, "Find", err), + log.Any("cond", cond), + ) + return + } + total, err = coll(nil).Count(cond) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindRechargesOrders", table, "Count", err), + log.Any("cond", cond), + ) + return + } + + return +} + +func FindOne(t *db.MongoTool, cond bson.M, opts ...*options.FindOneOptions) (data *RechargeOrder, err error) { + if err = coll(nil).FindOne(&data, cond, opts...); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOne", table, "FindOne", err), + log.Any("cond", cond), + ) + return + } + return +} + +// FindRechargesOrders 查询充值订单 +func FindMyOrders(uid, pgSz, pgCt uint64) (total int64, data []*RechargeOrder, hasNext bool, err error) { + data = make([]*RechargeOrder, 0) + opts := options.FindOptions{} + skip := int64((pgCt - 1) * pgSz) + limit := int64(pgSz + 1) + opts.Skip = &skip + opts.Limit = &limit + opts.SetSort(bson.D{{Key: "createdAt", Value: -1}}) + startTime := timeutil.BeforeMinute(20) + cond := bson.M{ + "$or": []bson.M{ + {"uid": uid, "status": bson.M{"$gt": Processing}}, + {"uid": uid, "status": bson.M{"$lte": Processing}, "createdAt": bson.M{"$gte": startTime}}, + }, + } + if err = coll(nil).Find(&data, cond, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindMyOrders", table, "Find", err), + log.Any("uid", uid), + log.Any("pgSz", pgSz), + log.Any("pgCt", pgCt), + ) + return + } + total, err = coll(nil).Count(cond) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindMyOrders", table, "Count", err), + log.Any("uid", uid), + log.Any("pgSz", pgSz), + log.Any("pgCt", pgCt), + ) + } + if len(data) > int(pgSz) { + hasNext = true + data = data[:pgSz] + } + return +} + +// Update 修改订单 +func UpdateRechargeOrder(id string, set RechargeUpdateReq) error { + set.UpdatedAt = time.Now() + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateRechargeOrder", table, "ObjectIDFromHex", err), + log.Any("id", id), + log.Any("set", set), + ) + return err + } + if _, err = coll(nil).UpdateOne(bson.M{"_id": objID}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateRechargeOrder", table, "UpdateOne", err), + log.Any("id", id), + log.Any("set", set), + ) + return err + } + return nil +} + +// CallBackModifyWithProcessing 回调修改订单 +func CallBackModifyWithProcessing(ctx context.Context, t *db.MongoTool, id primitive.ObjectID, set EditSelector) error { + set.UpdatedAt = time.Now() + if _, err := coll(t).UpdateOne(bson.M{"_id": id, "status": Processing}, bson.M{"$set": set}); err != nil { + log.WarnX(ctx, fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("id", id), + log.Any("set", set), + ) + return err + } + return nil +} + +// Update 修改订单 +func Update(t *db.MongoTool, id primitive.ObjectID, set EditSelector) error { + set.UpdatedAt = time.Now() + if _, err := coll(t).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("id", id), + log.Any("set", set), + ) + return err + } + return nil +} + +// 根据第三方订单号修改 +func UpdateBaseOnOidAndStatus(t *db.MongoTool, oid string, status int, set ReplaceEditSelector) error { + set.UpdatedAt = time.Now() + if _, err := coll(t).UpdateOne(bson.M{"oid": oid, "status": status}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateBaseOnOid", table, "UpdateOne", err), + log.Any("id", oid), + log.Any("set", set), + ) + return err + } + return nil +} + +func UpdateBaseOnOid(t *db.MongoTool, oid string, set ReplaceEditSelector) error { + set.UpdatedAt = time.Now() + if _, err := coll(t).UpdateOne(bson.M{"oid": oid}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateBaseOnOid", table, "UpdateOne", err), + log.Any("id", oid), + log.Any("set", set), + ) + return err + } + return nil +} + +// Upsert 新增或修改订单 +func Upsert(t *db.MongoTool, oid string, upsert UpserSelector) (id primitive.ObjectID, err error) { + updatedAt := time.Now() + upsert.UpdatedAt = &updatedAt + upertRest, err := coll(t).UpsertOne(bson.M{"oid": oid}, bson.M{"$setOnInsert": bson.M{"createdAt": time.Now()}, "$set": upsert}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Upsert", table, "UpsertOne", err), + log.Any("oid", oid), + log.Any("upsert", upsert), + ) + } + if tid, ok := upertRest.UpsertedID.(primitive.ObjectID); ok { + id = tid + } + return +} + +// FindRechargeByID 根据订单号查询订单 +func FindRechargeOrderByID(id string) (r RechargeOrder, err error) { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindRechargeOrderByID", table, "ObjectIDFromHex", err), + log.Any("id", id), + ) + return + } + if err = coll(nil).FindOne(&r, bson.M{"_id": objID}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindRechargeOrderByID", table, "FindOne", err), + log.Any("objID", objID), + ) + } + return +} + +// FindRechargeByID 根据第三方订单号查询订单 +func FindRechargeOrderByThirdOrderId(oid string) (r RechargeOrder, err error) { + if err = coll(nil).FindOne(&r, bson.M{"oid": oid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindRechargeOrderByID", table, "FindOne", err), + log.Any("objID", oid), + ) + } + return +} + +// FindRechargeByIDForApp 根据第三方订单号查询订单ForApp +func FindRechargeOrderByThirdOrderIdForApp(oid string) (r *RechargeOrderAppRes, err error) { + if err = coll(nil).FindOne(&r, bson.M{"oid": oid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindRechargeOrderByThirdOrderIdForApp", table, "FindOne", err), + log.Any("objID", oid), + ) + } + return +} + +// TotalMoney 统计总订单金额 +func TotalMoney(matchers ...Matcher) (int64, error) { + var ret struct { + Money int64 `bson:"money"` //充值金额 订单金额 李秋山确认 + } + filter := []bson.M{ + { + "$match": pageopt.MergeM(matchers), + }, + { + "$group": bson.M{ + "_id": nil, + "money": bson.M{"$sum": "$money"}, + }, + }, + } + if err := coll(nil).AggregateDecode(&ret, filter); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UsersTotalRecharge", table, "Aggregate", err), + log.Any("matchers", matchers), + ) + return 0, err + } + return ret.Money, nil +} + +// GetSuccessOrderUIDListByTime +func GetSuccessOrderUIDListByTime(start time.Time, end time.Time) ([]uint64, error) { + filter := (&SuccessAtGTEAndLTMatch{&start, &end}).New().Filter() + list := make([]struct { + UID uint64 `bson:"uid"` + }, 0) + if err := coll(nil).Find(&list, filter); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetSuccessOrderUIDListByTime", table, "Find", err), + log.Any("start", start), + log.Any("end", end), + ) + return nil, err + } + uidList := make([]uint64, len(list)) + for i, v := range list { + uidList[i] = v.UID + } + return uidList, nil +} + +// 根据uid获取用户的成功订单 +func GetSuccessOrderUidList(uid, pageSize, pageNum uint64) (total int64, data []RechargeOrder, hasNext bool, err error) { + filter := bson.M{"uid": uid} + skip := int64((pageNum - 1) * pageSize) + limit := int64(pageSize) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + if err = coll(nil).Find(&data, filter, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindMyOrders", table, "Find", err), + log.Any("uid", uid), + log.Any("pgSz", pageSize), + log.Any("pgCt", pageNum), + ) + return + } + total, err = coll(nil).Count(filter) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindMyOrders", table, "Count", err), + log.Any("uid", uid), + log.Any("pgSz", pageSize), + log.Any("pgCt", pageNum), + ) + } + return +} + +// GetNewAndRechargeUIDSByTime 新增并充值uid List +func GetNewAndRechargeUIDSByTime(createStart, createEnd, recStart, recEnd time.Time) ([]uint64, error) { + createUIDList, err := usermod.UIDListByCreateTime(createStart, createEnd) + if err != nil { + return nil, err + } + filter := (&SuccessAtGTEAndLTMatch{&recStart, &recEnd}).New().Filter() + recList := make([]struct { + UID uint64 `bson:"uid"` + }, 0) + if err = coll(nil).Find(&recList, filter); err != nil { + return nil, err + } + //充值用户 Map + recUserMap := make(map[uint64]byte, len(recList)) + for _, rec := range recList { + recUserMap[rec.UID] = 1 + } + uidList := make([]uint64, 0, len(recUserMap)) + for _, uid := range createUIDList { + if _, ok := recUserMap[uid]; ok { + uidList = append(uidList, uid) + } + } + return uidList, nil +} + +// GetChannelNewAndRechargeUIDSMapByTime 渠道新增并充值用户映射 +func GetChannelNewAndRechargeUIDSMapByTime(start time.Time, end time.Time) (map[string][]uint64, error) { + creatUserStartTime := timerange.LocDayRange(start).Head + creatUserEndTime := timerange.LocDayRange(end).Tail + chMap, err := usermod.ChannelMapByCreateTime(creatUserStartTime, creatUserEndTime) + if err != nil { + return nil, err + } + filter := (&SuccessAtGTEAndLTMatch{&start, &end}).New().Filter() + list := make([]struct { + UID uint64 `bson:"uid"` + }, 0) + if err = coll(nil).Find(&list, filter); err != nil { + return nil, err + } + recUIDMap := make(map[uint64]byte, len(list)) + for _, v := range list { + recUIDMap[v.UID] = 1 + } + ret := make(map[string][]uint64) + for uid, channel := range chMap { + if _, ok := recUIDMap[uid]; ok { + ret[channel] = append(ret[channel], uid) + } + } + return ret, nil +} + +// GetChannelNewAndRechargeAmountMapByTime 渠道新增并充值金额映射 +func GetChannelNewAndRechargeAmountMapByTime(start time.Time, end time.Time) (map[string]int64, error) { + creatUserStartTime := timerange.LocDayRange(start).Head + creatUserEndTime := timerange.LocDayRange(end).Tail + chMap, err := usermod.ChannelMapByCreateTime(creatUserStartTime, creatUserEndTime) + if err != nil { + return nil, err + } + filter := (&SuccessAtGTEAndLTMatch{GTE: &start, LT: &end}).New().Filter() + list := make([]struct { + UID uint64 `bson:"uid"` + Money int64 `bson:"money"` + }, 0) + if err = coll(nil).Find(&list, filter); err != nil { + return nil, err + } + moneyMap := make(map[uint64]int64, len(list)) + for _, v := range list { + moneyMap[v.UID] += v.Money + } + ret := make(map[string]int64) + for uid, channel := range chMap { + money := moneyMap[uid] + ret[channel] += money + } + return ret, nil +} + +// OnlineRechargeAmount OnlineRechargeAmount +func RechargeAmount(start time.Time, end time.Time, mats ...Matcher) (int64, error) { + mats = append(mats, (&SuccessAtGTEAndLTMatch{&start, &end}).New()) + filter := pageopt.MergeM(mats) + list := make([]struct { + Money int64 `bson:"money"` + }, 0) + if err := coll(nil).Find(&list, filter); err != nil { + return 0, err + } + var totalMoney int64 + for _, v := range list { + totalMoney += v.Money + } + return totalMoney, nil +} + +// ChannelOrderCountMap channel->订单数 map +func ChannelOrderCountMap(start, end time.Time) (map[string]int64, error) { + filter := (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New().Filter() + var list []struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //流水id + Channel string `json:"channel" bson:"channel"` //渠道类型 鲨鱼 金鱼 + } + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[string]int64, len(list)) + for _, v := range list { + m[v.Channel] += 1 + } + return m, nil +} + +// ChannelCompleteOrderCountMap channel->完成订单数 map +func ChannelCompleteOrderCountMap(start, end time.Time) (map[string]int64, error) { + filter := (&SuccessAtGTEAndLTMatch{&start, &end}).New().Filter() + var list []struct { + ID primitive.ObjectID `bson:"_id,omitempty"` //流水id + Channel string `bson:"channel"` //渠道类型 鲨鱼 金鱼 + } + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[string]int64, len(list)) + for _, v := range list { + m[v.Channel] += 1 + } + return m, nil +} + +// ChannelOrderMoneyMap channel->订单金额 map +func ChannelOrderMoneyMap(start, end time.Time) (map[string]int64, error) { + filter := (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New().Filter() + var list []struct { + Channel string `bson:"channel"` //渠道类型 鲨鱼 金鱼 + Money int64 `bson:"money"` //充值金额 订单金额 李秋山确认 + } + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[string]int64, len(list)) + for _, v := range list { + m[v.Channel] += v.Money + } + return m, nil +} + +// ChannelCompleteOrderPayMoneyMap channel->完成订单的用户实际支付金额 map +func ChannelCompleteOrderPayMoneyMap(start, end time.Time) (map[string]int64, error) { + filter := (&SuccessAtGTEAndLTMatch{&start, &end}).New().Filter() + var list []struct { + Channel string `bson:"channel"` //渠道类型 鲨鱼 金鱼 + PayMoney int64 `bson:"payMoney"` //实际到账金额 实际支付金额 李秋山确认 + } + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[string]int64, len(list)) + for _, v := range list { + m[v.Channel] += v.PayMoney + } + return m, nil +} + +var ChannelPaymentOrderCountMap = ChannelCompleteOrderCountMap + +var ChannelPaymentMoneyMap = ChannelCompleteOrderPayMoneyMap + +// SuccessRateMap 每小时成功率 +func SuccessRateMap(channel string, start, end time.Time, scaleMinute int64) (map[int64]float64, error) { + var list []struct { + CreatedAt time.Time `bson:"createdAt"` //创建时间 + Status int `bson:"status"` //1进行中 2付款失败 3付款成功 4已经退款 + } + matList := []pageopt.Matcher{ + (&ChannelMatch{&channel}).New(), + (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New(), + } + filter := pageopt.MergeM(matList) + opt := (&options.FindOptions{}).SetProjection(M{ + "createdAt": 1, + "status": 1, + }) + if err := coll(nil).Find(&list, filter, opt); err != nil { + return nil, err + } + sucOrdCountMap := make(map[int64]int64) + orderCountMap := make(map[int64]int64) + successRateMap := make(map[int64]float64) + tempTime := start + for end.After(tempTime) { + tim := timerange.RecentMinute(tempTime, scaleMinute) + p := tim.Unix() + sucOrdCountMap[p] = 0 + orderCountMap[p] = 0 + successRateMap[p] = 0.0 + tempTime = tempTime.Add(time.Duration(scaleMinute) * time.Minute) + } + for _, v := range list { + tim := timerange.RecentMinute(v.CreatedAt, scaleMinute) + p := tim.Unix() + if v.Status == SUCCESS { + sucOrdCountMap[p] += 1 + } + orderCountMap[p] += 1 + } + lastTime := end + if len(list) > 0 { + lastTime = list[len(list)-1].CreatedAt + } + tempTime = start + for lastTime.After(tempTime) { + tim := timerange.RecentMinute(tempTime, scaleMinute) + p := tim.Unix() + if orderCountMap[p] != 0 { + successRateMap[p] = float64(int(float64(sucOrdCountMap[p])/float64(orderCountMap[p])*1000)) / 10 + } + tempTime = tempTime.Add(time.Duration(scaleMinute) * time.Minute) + } + return successRateMap, nil +} + +func GetSuccessRechargeCountMapByHour(discCode string, start, end time.Time, mats ...Matcher) ([]int, map[int]int64, int64, error) { + mats = append(mats, + (&DistrictCodeMatch{&discCode}).New(), + (&SuccessAtGTEAndLTMatch{&start, &end}).New()) + filter := pageopt.MergeM(mats) + opt := (&options.FindOptions{}).SetProjection(M{ + "successAt": 1, + }) + var list []struct { + SuccessAt time.Time `bson:"successAt"` //创建时间 + } + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.Error("rchgordmod GetRechargeCountMapByHour error", log.E(err)) + return nil, nil, 0, err + } + hs := []int{} + m := make(map[int]int64) + startH := start.Hour() + lastH := end.Hour() + (end.Day()-start.Day())*24 + for i := startH; i <= lastH; i++ { + hs = append(hs, i) + m[i] = 0 + } + var total int64 + for _, v := range list { + hour := v.SuccessAt.Hour() + (v.SuccessAt.Day()-start.Day())*24 + m[hour] += 1 + total++ + } + return hs, m, total, nil +} + +func UpRechOrder(mt *db.MongoTool, tradeNo string, oid string, amount int64, progressAt time.Time) error { + id, _ := primitive.ObjectIDFromHex(tradeNo) + c, err := coll(nil).Count(bson.M{"_id": id}) + if err != nil { + return nil + } + if c <= 0 { + return errors.New("tradeNo不存在") + } + tStr := time.Now().Local() + filter := bson.M{"_id": id, "status": Processing} + mur, err := coll(mt).UpdateOne(filter, + bson.M{"$set": bson.M{ + "status": SUCCESS, + "statusDesc": "success", + "oid": oid, + "payMoney": amount, + "updatedAt": tStr, + "progressAt": progressAt, + "successAt": progressAt, + "paymentAt": progressAt, + /*"finishedAt": tStr*/}}) + if err != nil { + log.Warn("transplat:UpRechOrder rchgOderColl.UpdateOne failed", log.E(err), log.Any("filter", filter)) + return err + } + if mur.ModifiedCount != 1 { + return errors.New("UpdateErr") + } + return nil +} + +// 获取用户昨日棋牌充值金额 +func GetUserChessSuccessOrderAmout(uid uint64, now time.Time) (int64, error) { + /** TODO: 统计用户昨日棋牌充值总金额 + 查询条件 + 1.指定用户 + 2.棋牌充值 + 3.成功订单 + 4.订单时间(大于等于昨日凌晨时间;小于今日凌晨时间) + */ + match := bson.M{"uid": uid, + "productType": 1, + "status": 3, + "paymentAt": bson.M{"$gte": timeutil.BeginningOfDay(now.AddDate(0, 0, -1)), "$lt": timeutil.BeginningOfDay(now)}, + } + pipelines := []bson.M{ + {"$match": match}, + {"$group": bson.M{"_id": nil, "totalAmount": bson.M{"$sum": "$payMoney"}}}, + {"$project": bson.M{"totalAmount": 1}}, + } + var res struct { + TotalAmount int64 `bson:"totalAmount"` + } + if err := coll(nil).AggregateDecode(&res, pipelines); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetUserChessSuccessOrderAmout", table, "AggregateDecode", err), log.Any("uid", uid)) + return 0, err + } + return decimal.NewFromInt(res.TotalAmount).Div(decimal.NewFromInt(100)).IntPart(), nil +} + +// 首冲判定 +func IsFirstRecharge(uid uint64) (bool, error) { + filter := bson.M{"uid": uid, "productType": 1, "status": SUCCESS} + count, err := coll(nil).Count(filter) + if err != nil { + return false, err + } + return count == 0, nil +} + +func StatCenterSyncRecharge(start time.Time, size int64) ([]RechargeOrder, error) { + end := time.Now().Add(-time.Second * 240) //等用户注册信息先同步 + opt := (&options.FindOptions{}) + opt.SetLimit(size) + opt.SetSort(bson.M{"successAt": 1}) + filter := bson.M{"status": 3, "successAt": bson.M{"$gt": start, "$lte": end}} + data := make([]RechargeOrder, 0) + return data, coll(nil).Find(&data, filter, opt) +} + +func StatCenterSyncOrder(start time.Time, size int64) ([]RechargeOrder, error) { + end := time.Now().Add(-time.Second * 240) //等用户注册信息先同步 + opt := (&options.FindOptions{}) + opt.SetLimit(size) + opt.SetSort(bson.M{"createdAt": 1}) + filter := bson.M{"createdAt": bson.M{"$gt": start, "$lte": end}} + data := make([]RechargeOrder, 0) + return data, coll(nil).Find(&data, filter, opt) +} + +func GetOrderByTime(start, end time.Time) ([]RechargeStat, error) { + filter := bson.M{"status": 3, "productType": bson.M{"$ne": 1}, "successAt": bson.M{"$gt": start, "$lte": end}} + data := make([]RechargeStat, 0) + return data, coll(nil).Find(&data, filter) +} + +func NewStatCenterSyncRecharge(id string, size int64) ([]RechargeOrder, error) { + _id, _ := primitive.ObjectIDFromHex(id) + opt := (&options.FindOptions{}).SetLimit(size).SetSort(bson.M{"_id": 1}) + filter := bson.M{"_id": bson.M{"$gt": _id}} + data := make([]RechargeOrder, 0) + return data, coll(nil).Find(&data, filter, opt) +} + +// RefundCallBackModify 退款回调修改订单 +func RefundCallBackModify(ctx context.Context, t *db.MongoTool, id primitive.ObjectID, set EditSelector) error { + set.UpdatedAt = time.Now() + if _, err := coll(t).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}); err != nil { + log.WarnX(ctx, fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("id", id), + log.Any("set", set), + ) + return err + } + return nil +} diff --git a/models/v/rchgordmod/type.go b/models/v/rchgordmod/type.go new file mode 100644 index 0000000..c20f456 --- /dev/null +++ b/models/v/rchgordmod/type.go @@ -0,0 +1,193 @@ +package rchgordmod + +import ( + "91porn-server/middleware/ua" + "time" + + "91porn-server/common/db" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + DEFAULT = iota //默认 + Processing //1进行中 + FAILURE //2失败 + SUCCESS //3成功 + REFUND //4退款 +) + +const ( + UnLock = iota //0锁定 订单操作锁 解锁 + Lock //1锁定 订单操作锁 加锁 目前在代充中用到 +) + +// 通知第三方上分结果 +const ( + Notify_DEFAULT = iota //默认 + Notify_SUCCESS //1 成功通知第三方 + Notify_FAILURE //2 通知第三方失败 +) + +type Status int + +var status = map[Status]string{ + Processing: "processing", + FAILURE: "failure", + SUCCESS: "success", + REFUND: "refund", +} + +func (s Status) Desc() string { + if desc, ok := status[s]; ok { + return desc + } + return "" +} + +// 支付类型 +type RechType string + +const ( + Rech_WeChat RechType = "wechat" //微信 + Rech_AliPay RechType = "alipay" //支付宝 + Rech_Union RechType = "union" //支付宝 + Rech_DaiChong RechType = "daichong" //代充 +) + +type ChildType int + +const ( + Gold ChildType = iota + 1 // 购买金币 + VIP // 购买会员卡 + FruitCoin // 购买果币 + NudeChatService // 购买裸聊服务 +) + +const ( + GoldFish = "jinyu" + Shark = "shayu" + DaBaiSha = "dabaisha" + Lucky = "lezhifu" + Rongyi = "rongyi" + GoldfishPlus = "dajinyu" + IZhiFu = "izhifu" + DXZhiFu = "dxzhifu" + LeiHuo = "leihuo" + ZhongFu = "zhongfu" + YiLianFu = "yilianfu" + YinSe = "yinse" +) +const ( + ChanShareNO = iota //不分成 + ChanShareCoin //金币分成 + ChanShareModVIP //vip分成 +) + +var mdb *db.MongoDB + +type DiscSeqe = commod.DiscSeqe + +type DiscDoc = commod.DiscDoc + +// RechargeOrder 账户充值流水 +type RechargeOrder struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //流水id + UID uint64 `json:"uid" bson:"uid"` //用户id + DevID string `json:"devID" bson:"devID"` //设备id + OID string `json:"oid" bson:"oid"` //支付平台订单号 + TraderId string `json:"traderId" bson:"traderId"` //商户ID + UserIP string `json:"userIP" bson:"userIP"` //用户ip + Name string `json:"name" bson:"name"` //用户名称 + Tel string `json:"tel" bson:"tel"` //用户手机号 + PayAct string `json:"payAct" bson:"payAct"` //支付账号 + DevType string `json:"devType" bson:"devType"` //设备系统类型 ios pc android + Amount int64 `json:"amount" bson:"amount"` //虚拟币数量(金币/果币) + Money int64 `json:"money" bson:"money"` //充值金额 订单金额 李秋山确认 + PayMoney int64 `json:"payMoney" bson:"payMoney"` //实际到账金额 用户实际支付金额 李秋山确认 + RechargeType string `json:"rechargeType" bson:"rechargeType"` //充值类型(支付类型) + ProductID primitive.ObjectID `json:"productID" bson:"productID"` //充值金币类型id + ProductName string `json:"productName" bson:"productName"` //充值产品名 + VipID primitive.ObjectID `json:"vipID" bson:"vipID"` //是否是vip冲 hi + Channel string `json:"channel" bson:"channel"` //渠道类型 鲨鱼 金鱼 + Proof string `json:"proof" bson:"proof"` //代充平台 支付凭证 + Status int `json:"status" bson:"status"` //1进行中 2付款失败 3付款成功 4已经退款 + StatusDesc string `json:"statusDesc" bson:"statusDesc"` //状态描述 + LockStatus int `json:"lockStatus" bson:"lockStatus"` //订单操作锁定状态 用户订单更新操作 0-未锁定 可用于编辑订单 1-已锁定 不可编辑 + NotifyTime int64 `json:"notifyTime" bson:"notifyTime"` //第三方回调本地订单的时间 + NotifyStatus int `json:"notifyStatus" bson:"notifyStatus"` //通知第三方上分情况状态 1-成功 2-失败 + Remark string `json:"remark" bson:"remark"` //备注 + ProgressAt time.Time `json:"progressAt" bson:"progressAt"` //第三方下单成功时间 + RefundAt time.Time `json:"refundAt" bson:"refundAt"` // 退款失败时间 + FailureAt time.Time `json:"failureAt" bson:"failureAt"` //回调失败时间 + SuccessAt time.Time `json:"successAt" bson:"successAt"` //回调成功时间 + PaymentAt time.Time `json:"paymentAt" bson:"paymentAt"` //支付时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //刷新时间 + Rate string `json:"rate" bson:"rate"` //渠道费率 + ProductType int `json:"productType" bson:"productType"` //支付产品类型 0 站群 1 棋牌 + BuyType commod.BuyType `json:"buyType" bson:"buyType"` //充值购买类型 + ServiceID primitive.ObjectID `json:"serviceId" bson:"serviceId"` //裸聊服务ID + DiscDoc `bson:",inline"` //商区 + ChanShareMod int `json:"chanShareMod" bson:"chanShareMod"` //渠道分成类型 + Category int `json:"category" bson:"category"` //充值类别 0金币 1会员 + GoldExtraID *primitive.ObjectID `json:"goldExtraID" bson:"goldExtraID,omitempty"` // 金币加购券 + Ua ua.UA `json:"ua" bson:"ua"` + SourcePage string `json:"sourcePage,omitempty" bson:"sourcePage,omitempty"` // 下单来源页面 + SourceRef string `json:"sourceRef,omitempty" bson:"sourceRef,omitempty"` // 来源业务对象 + VideoID string `json:"videoId,omitempty" bson:"videoId,omitempty"` // 来源视频ID + ActivityID string `json:"activityId,omitempty" bson:"activityId,omitempty"` // 来源活动ID + ExperimentID string `json:"experimentId,omitempty" bson:"experimentId,omitempty"` // A/B实验ID + ExperimentVariant string `json:"experimentVariant,omitempty" bson:"experimentVariant,omitempty"` // A/B实验分组 + SessionID string `json:"sessionId,omitempty" bson:"sessionId,omitempty"` // 前端会话ID + MediaID primitive.ObjectID `json:"mediaId,omitempty" bson:"mediaId,omitempty"` // 短剧ID + ContentID primitive.ObjectID `json:"contentId,omitempty" bson:"contentId,omitempty"` // 短剧单集ID + CheckoutContextID string `json:"checkoutContextId,omitempty" bson:"checkoutContextId,omitempty"` // 短剧付费墙上下文 + CouponID string `json:"couponId,omitempty" bson:"couponId,omitempty"` // 会员抵扣券ID(活动服券号,不透明串,支付结果回调时原样回传) + DeductAmount int64 `json:"deductAmount,omitempty" bson:"deductAmount,omitempty"` // 抵扣金额(分),已按上限封顶后的实际抵扣额 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type RechargeStat struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //流水id + UID uint64 `json:"uid" bson:"uid"` //用户id + DevID string `json:"devID" bson:"devID"` //设备id + OID string `json:"oid" bson:"oid"` //支付平台订单号 + TraderId string `json:"traderId" bson:"traderId"` //商户ID + UserIP string `json:"userIP" bson:"userIP"` //用户ip + Name string `json:"name" bson:"name"` //用户名称 + Tel string `json:"tel" bson:"tel"` //用户手机号 + PayAct string `json:"payAct" bson:"payAct"` //支付账号 + DevType string `json:"devType" bson:"devType"` //设备系统类型 ios pc android + Amount int64 `json:"amount" bson:"amount"` //虚拟币数量(金币/果币) + Money int64 `json:"money" bson:"money"` //充值金额 订单金额 李秋山确认 + PayMoney int64 `json:"payMoney" bson:"payMoney"` //实际到账金额 用户实际支付金额 李秋山确认 + RechargeType string `json:"rechargeType" bson:"rechargeType"` //充值类型(支付类型) + ProductID primitive.ObjectID `json:"productID" bson:"productID"` //充值金币类型id + VipID primitive.ObjectID `json:"vipID" bson:"vipID"` //是否是vip冲 hi + Channel string `json:"channel" bson:"channel"` //渠道类型 鲨鱼 金鱼 + Proof string `json:"proof" bson:"proof"` //代充平台 支付凭证 + Status int `json:"status" bson:"status"` //1进行中 2付款失败 3付款成功 4已经退款 + StatusDesc string `json:"statusDesc" bson:"statusDesc"` //状态描述 + LockStatus int `json:"lockStatus" bson:"lockStatus"` //订单操作锁定状态 用户订单更新操作 0-未锁定 可用于编辑订单 1-已锁定 不可编辑 + NotifyTime int64 `json:"notifyTime" bson:"notifyTime"` //第三方回调本地订单的时间 + NotifyStatus int `json:"notifyStatus" bson:"notifyStatus"` //通知第三方上分情况状态 1-成功 2-失败 + Remark string `json:"remark" bson:"remark"` //备注 + ProgressAt time.Time `json:"progressAt" bson:"progressAt"` //第三方下单成功时间 + FailureAt time.Time `json:"failureAt" bson:"failureAt"` //回调失败时间 + SuccessAt time.Time `json:"successAt" bson:"successAt"` //回调成功时间 + PaymentAt time.Time `json:"paymentAt" bson:"paymentAt"` //支付时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //刷新时间 + Rate string `json:"rate" bson:"rate"` //渠道费率 + ProductType int `json:"productType" bson:"productType"` //支付产品类型 0 站群 1 棋牌 + BuyType commod.BuyType `json:"buyType" bson:"buyType"` //充值购买类型 + ServiceID primitive.ObjectID `json:"serviceId" bson:"serviceId"` //裸聊服务ID + DiscDoc `bson:",inline"` //商区 + ChanShareMod int `json:"chanShareMod" bson:"chanShareMod"` //渠道分成类型 +} diff --git a/models/v/rchgordmod/vip_experiment.go b/models/v/rchgordmod/vip_experiment.go new file mode 100644 index 0000000..a8a4093 --- /dev/null +++ b/models/v/rchgordmod/vip_experiment.go @@ -0,0 +1,69 @@ +package rchgordmod + +import ( + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type VIPExperimentOrderStat struct { + Variant string `json:"variant" bson:"variant"` + ProductID primitive.ObjectID `json:"productId" bson:"productId"` + CreatedOrders int `json:"createdOrders" bson:"createdOrders"` + PaidOrders int `json:"paidOrders" bson:"paidOrders"` + RefundedOrders int `json:"refundedOrders" bson:"refundedOrders"` + OrderAmount int64 `json:"orderAmount" bson:"orderAmount"` + PaidAmount int64 `json:"paidAmount" bson:"paidAmount"` + GoldPaidOrders int `json:"goldPaidOrders" bson:"goldPaidOrders"` + GoldPaidAmount int64 `json:"goldPaidAmount" bson:"goldPaidAmount"` +} + +func VIPExperimentOrderStatistics(experimentID string) ([]VIPExperimentOrderStat, error) { + var raw []struct { + ID struct { + Variant string `bson:"variant"` + ProductID primitive.ObjectID `bson:"productId"` + } `bson:"_id"` + CreatedOrders int `bson:"createdOrders"` + PaidOrders int `bson:"paidOrders"` + RefundedOrders int `bson:"refundedOrders"` + OrderAmount int64 `bson:"orderAmount"` + PaidAmount int64 `bson:"paidAmount"` + } + err := coll(nil).Aggregate(&raw, []bson.M{ + {"$match": bson.M{"experimentId": experimentID}}, + {"$group": bson.M{ + "_id": bson.M{ + "variant": "$experimentVariant", + "productId": "$productID", + }, + "createdOrders": bson.M{"$sum": 1}, + "paidOrders": bson.M{"$sum": bson.M{"$cond": bson.A{ + bson.M{"$eq": bson.A{"$status", SUCCESS}}, 1, 0, + }}}, + "refundedOrders": bson.M{"$sum": bson.M{"$cond": bson.A{ + bson.M{"$eq": bson.A{"$status", REFUND}}, 1, 0, + }}}, + "orderAmount": bson.M{"$sum": "$money"}, + "paidAmount": bson.M{"$sum": bson.M{"$cond": bson.A{ + bson.M{"$eq": bson.A{"$status", SUCCESS}}, "$payMoney", 0, + }}}, + }}, + {"$sort": bson.D{{Key: "_id.variant", Value: 1}, {Key: "createdOrders", Value: -1}}}, + }) + if err != nil { + return nil, err + } + result := make([]VIPExperimentOrderStat, 0, len(raw)) + for _, item := range raw { + result = append(result, VIPExperimentOrderStat{ + Variant: item.ID.Variant, + ProductID: item.ID.ProductID, + CreatedOrders: item.CreatedOrders, + PaidOrders: item.PaidOrders, + RefundedOrders: item.RefundedOrders, + OrderAmount: item.OrderAmount, + PaidAmount: item.PaidAmount, + }) + } + return result, nil +} diff --git a/models/v/rchgordmod/web.go b/models/v/rchgordmod/web.go new file mode 100644 index 0000000..90f63af --- /dev/null +++ b/models/v/rchgordmod/web.go @@ -0,0 +1,78 @@ +package rchgordmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// RechargeUpdateReq 修改参数 +type RechargeUpdateReq struct { + StatusDesc *string `json:"statusDesc" bson:"statusDesc"` // 状态描述 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //更新时间 +} + +type RchgQueryReq struct { + ID *string `form:"id" json:"_id,omitempty" bson:"_id"` //流水id + UID *uint64 `form:"uid" json:"uid,omitempty" bson:"uid"` //用户id + DevID *string `form:"devID" json:"devID,omitempty" bson:"devID"` //设备id + OID *string `form:"oid" json:"oid,omitempty" bson:"oid"` //支付平台订单号 + TraderId *string `form:"traderId" json:"traderId,omitempty" bson:"traderId"` //商户ID + UserIP *string `form:"userIP" json:"userIP,omitempty" bson:"userIP"` //用户ip + Name *string `form:"name" json:"name,omitempty" bson:"name"` //用户名称 + Tel *string `form:"tel" json:"tel,omitempty" bson:"tel"` //用户手机号 + PayAct *string `form:"payAct" json:"payAct,omitempty" bson:"payAct"` //支付账号 + DevType *string `form:"devType" json:"devType,omitempty" bson:"devType"` //设备系统类型 ios pc android + Amount *int64 `form:"amount" json:"amount,omitempty" bson:"amount"` //蝴蝶币数量 + Money *int64 `form:"money" json:"money,omitempty" bson:"money"` //充值金额 + PayMoney *int64 `form:"payMoney" json:"payMoney,omitempty" bson:"payMoney"` //实际到账金额 + RechargeType *string `form:"rechargeType" json:"rechargeType,omitempty" bson:"rechargeType"` //充值类型 + Channel *string `form:"channel" json:"channel,omitempty" bson:"channel"` //渠道类型 鲨鱼 金鱼 + Account *string `form:"bankAccount" json:"bankAccount,omitempty" bson:"bankAccount"` //银行账户,支付宝或微信账户 + BackName *string `form:"backName" json:"backName,omitempty" bson:"backName"` //银行名称,如果是银行类型提现,必填 + TranOrder *string `form:"tranOrder" json:"tranOrder,omitempty" bson:"tranOrder"` //交易充值的订单号 + Status *int `form:"status" json:"status,omitempty" bson:"status"` //0代表进行中 1付款成功,可退款 2付款失败 3付款成功 4已经退款 + ProductType *int `form:"productType" json:"productType,omitempty" bson:"productType"` //产品类型 0站群 1棋牌 + SourcePage *string `form:"sourcePage" json:"sourcePage,omitempty" bson:"sourcePage"` + SourceRef *string `form:"sourceRef" json:"sourceRef,omitempty" bson:"sourceRef"` + VideoID *string `form:"videoId" json:"videoId,omitempty" bson:"videoId"` + ActivityID *string `form:"activityId" json:"activityId,omitempty" bson:"activityId"` + ExperimentID *string `form:"experimentId" json:"experimentId,omitempty" bson:"experimentId"` + ExperimentVariant *string `form:"experimentVariant" json:"experimentVariant,omitempty" bson:"experimentVariant"` + SessionID *string `form:"sessionId" json:"sessionId,omitempty" bson:"sessionId"` +} + +type RchargeOrderExport struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //流水id + UID uint64 `json:"uid" bson:"uid"` //用户id + DevID string `json:"devID" bson:"devID" ` //设备id + OID string `json:"oid" bson:"oid"` //支付平台订单号 + TraderId string `json:"traderId,omitempty" bson:"traderId,omitempty"` //商户ID + UserIP string `json:"userIP" bson:"userIP"` //用户ip + Name string `json:"name" bson:"name" ` //用户名称 + Tel string `json:"tel" bson:"tel" ` //用户手机号 + PayAct string `json:"payAct" bson:"payAct" ` //支付账号 + DevType string `json:"devType" bson:"devType"` //设备系统类型 ios pc android + Amount string `json:"amount" bson:"amount" ` //蝴蝶币数量 + Money string `json:"money" bson:"money"` //充值金额 订单金额 李秋山确认 + PayMoney string `json:"payMoney" bson:"payMoney"` //实际到账金额 用户实际支付金额 李秋山确认 + RechargeType string `json:"rechargeType" bson:"rechargeType"` //充值类型 + ProductID primitive.ObjectID `json:"productID" bson:"productID"` //充值金币类型id + VipID primitive.ObjectID `json:"vipID" bson:"vipID"` //是否是vip冲 hi + Channel string `json:"channel" bson:"channel"` //渠道类型 鲨鱼 金鱼 + Status int `json:"status" bson:"status" ` // 1进行中 2付款失败 3付款成功 4已经退款 + StatusDesc string `json:"statusDesc" bson:"statusDesc"` // 状态描述 + ProgressAt time.Time `json:"progressAt" bson:"progressAt"` //第三方下单成功时间 + FailureAt time.Time `json:"failureAt" bson:"failureAt"` //回调失败时间 + SuccessAt time.Time `json:"successAt" bson:"successAt"` //回调成功时间 + PaymentAt time.Time `json:"paymentAt" bson:"paymentAt"` //支付时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //刷新时间 + SourcePage string `json:"sourcePage" bson:"sourcePage"` + SourceRef string `json:"sourceRef" bson:"sourceRef"` + VideoID string `json:"videoId" bson:"videoId"` + ActivityID string `json:"activityId" bson:"activityId"` + ExperimentID string `json:"experimentId" bson:"experimentId"` + ExperimentVariant string `json:"experimentVariant" bson:"experimentVariant"` + SessionID string `json:"sessionId" bson:"sessionId"` +} diff --git a/models/v/recmdtag/app.go b/models/v/recmdtag/app.go new file mode 100644 index 0000000..3b3b33d --- /dev/null +++ b/models/v/recmdtag/app.go @@ -0,0 +1,26 @@ +package recmdtag + +import ( + "91porn-server/models/commod" + "91porn-server/models/v/vidmod" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ( + AppGetShortDiscoverListReq struct { + commod.Page + Type int `json:"type" form:"type" binding:"required,oneof=1 2 3 4 5"` // 1-最热 2-最新 3-推荐 4 热卖 5 指定tag + TagID string `json:"tagID" form:"tagID" binding:"-"` + } + AppGetShortDiscoverListRep struct { + HasNext bool `json:"hasNext"` + Videos []*vidmod.VideoInfo `json:"videos"` + Tags []*TagInfo `json:"tags"` + } + + // TagInfo 标签信息 + TagInfo struct { + ID primitive.ObjectID `json:"id" bson:"_id"` //标签id + Name string `json:"name" bson:"tagName"` //标签名字 + } +) diff --git a/models/v/recmdtag/error.go b/models/v/recmdtag/error.go new file mode 100644 index 0000000..32e4a58 --- /dev/null +++ b/models/v/recmdtag/error.go @@ -0,0 +1,17 @@ +package recmdtag + +// ErrRecmdTagNotExist +type ErrRecmdTagNotExist struct { +} + +func (d ErrRecmdTagNotExist) Error() string { + return "hot word not exist error" +} + +// RecmdTag +type ErrRecmdTagHasExist struct { +} + +func (d ErrRecmdTagHasExist) Error() string { + return "recmd tag has exist error" +} diff --git a/models/v/recmdtag/list.go b/models/v/recmdtag/list.go new file mode 100644 index 0000000..beb7311 --- /dev/null +++ b/models/v/recmdtag/list.go @@ -0,0 +1,84 @@ +package recmdtag + +import ( + "91porn-server/common/log" + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Matcher = pageopt.Matcher + +// TypesMatch +type TypesMatch struct { + Types *RecmdType +} + +func (n *TypesMatch) New() Matcher { + return pageopt.NewAssignMatch("types", n.Types) +} + +// NameMatch +type NameMatch struct { + Name *string +} + +func (n *NameMatch) New() Matcher { + return pageopt.NewAssignMatch("name", n.Name) +} + +// EnableMatch +type EnableMatch struct { + Enable *bool +} + +func (i *EnableMatch) New() Matcher { + return pageopt.NewAssignMatch("enable", i.Enable) +} + +// TIDInMatch +type TIDInMatch struct { + TIDs []ObjectID +} + +func (t *TIDInMatch) New() Matcher { + return pageopt.NewInMatch("tid", t.TIDs) +} + +// IDMatch +type IDMatch = pageopt.IDMatch + +type Sort = bson.D + +var Sort_sortCode_N1 = Sort{{Key: "sortCode", Value: -1}} + +func List(sort Sort, skip, limit *int64, matchers ...Matcher) ([]RecmdTag, error) { + filter := pageopt.MergeM(matchers) + opt := (&options.FindOptions{}) + if len(sort) != 0 { + opt.SetSort(sort) + } + if skip != nil { + opt.SetSkip(*skip) + } + if limit != nil { + opt.SetLimit(*limit) + } + list := []RecmdTag{} + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.Error("recmdtag List error", log.E(err)) + return nil, err + } + return list, nil +} + +func Count(matchers ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchers) + count, err := coll(nil).Count(filter) + if err != nil { + log.Error("recmdtag Count error", log.E(err)) + return 0, err + } + return count, nil +} diff --git a/models/v/recmdtag/m3u8sign.go b/models/v/recmdtag/m3u8sign.go new file mode 100644 index 0000000..d3c8281 --- /dev/null +++ b/models/v/recmdtag/m3u8sign.go @@ -0,0 +1,10 @@ +package recmdtag + +import "91porn-server/models/v/vidmod" + +// SignM3u8 抖音短视频发现列表,供 m3u8ticket 零反射签票。 +func (r AppGetShortDiscoverListRep) SignM3u8(s vidmod.M3u8Signer) { + vidmod.SignM3u8Infos(s, r.Videos) +} + +var _ vidmod.M3u8Signable = AppGetShortDiscoverListRep{} diff --git a/models/v/recmdtag/recmdtag.go b/models/v/recmdtag/recmdtag.go new file mode 100644 index 0000000..462a671 --- /dev/null +++ b/models/v/recmdtag/recmdtag.go @@ -0,0 +1,128 @@ +package recmdtag + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/common/stderr" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// M M from Map +type M = bson.M + +var mdb *db.MongoDB + +const table = models.RecmdTag + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "name", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "enable", Value: 1}, {Key: "sort_1", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func InsertOne(doc InsertDoc) error { + if doc.CreatedAt.IsZero() { + doc.CreatedAt = time.Now() + } + if _, err := coll(nil).InsertOne(doc); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertOne", err), log.Any("doc", doc)) + if err == stderr.InsertExistError { + return ErrRecmdTagHasExist{} + } + switch exception := err.(type) { + case mongo.WriteException: + for _, e := range exception.WriteErrors { + if e.Code == 11000 { //https://docs.mongodb.com/manual/reference/method/db.collection.insert/index.html + return ErrRecmdTagHasExist{} + } + } + } + return err + } + return nil +} + +func updateOne(doc UpdateDoc, mats ...Matcher) error { + if doc.UpdatedAt.IsZero() { + doc.UpdatedAt = time.Now() + } + filter := pageopt.MergeM(mats) + ret, err := coll(nil).UpdateOne(filter, bson.M{"$set": doc}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "updateOne", table, "updateOne", err), + log.Any("filter", filter), + log.Any("doc", doc), + ) + return err + } + if ret != nil && ret.ModifiedCount == 0 { + return ErrRecmdTagNotExist{} + } + return nil +} + +func UpdateOneByID(id ObjectID, doc UpdateDoc) error { + return updateOne(doc, (&IDMatch{ID: &id}).New()) +} + +func DeleteOne(id ObjectID) error { + if _, err := coll(nil).DeleteOne(M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteOne", table, "DeleteOne", err), + log.Any("id", id), + ) + if err == mongo.ErrNoDocuments { + return ErrRecmdTagNotExist{} + } + return err + } + return nil +} + +func idMap(mats ...Matcher) (map[ObjectID]ObjectID, error) { + filter := pageopt.MergeM(mats) + opt := (&options.FindOptions{}).SetProjection(M{ + "_id": 1, + "tid": 1, + }) + list := []struct { + ID ObjectID `bson:"_id"` + TID ObjectID `bson:"tid"` + }{} + if err := coll(nil).Find(&list, filter, opt); err != nil { + return nil, err + } + m := make(map[ObjectID]ObjectID, len(list)) + for _, v := range list { + m[v.TID] = v.ID + } + return m, nil +} + +func IDMapByTIDs(tids []ObjectID) (map[ObjectID]ObjectID, error) { + return idMap((&TIDInMatch{tids}).New()) +} diff --git a/models/v/recmdtag/type.go b/models/v/recmdtag/type.go new file mode 100644 index 0000000..6e008e1 --- /dev/null +++ b/models/v/recmdtag/type.go @@ -0,0 +1,50 @@ +package recmdtag + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +type RecmdType int64 + +const ( + Def RecmdType = 0 //默认 + Hot RecmdType = 1 //最热 + New RecmdType = 2 //最新 + KindShortVideo RecmdType = 6 //短视频推荐标签 +) + +// 热词 +type RecmdTag struct { + ID ObjectID `bson:"_id,omitempty"` + Types RecmdType `bson:"types"` //种类 + TID ObjectID `bson:"tid"` //标签ID + Name string `bson:"name"` //名字 + SortCode int64 `bson:"sortCode"` //正序 + Enable bool `bson:"enable"` //使能 true:开 + Remark string `bson:"remark"` //备注 + UpdatedAt time.Time `bson:"updatedAt"` //刷新时间 + CreatedAt time.Time `bson:"createdAt"` //创建时间 +} + +type InsertDoc RecmdTag + +type UpdateDoc struct { + Types *RecmdType `bson:"types,omitempty"` //种类 + TID *ObjectID `bson:"tid,omitempty"` + Name *string `bson:"name,omitempty"` //名字 + SortCode *int64 `bson:"sortCode,omitempty"` //正序 + Enable *bool `bson:"enable,omitempty"` //使能 true:开 + Remark *string `bson:"remark,omitempty"` //备注 + UpdatedAt time.Time `bson:"updatedAt"` //刷新时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/recommod/app.go b/models/v/recommod/app.go new file mode 100644 index 0000000..c07edf7 --- /dev/null +++ b/models/v/recommod/app.go @@ -0,0 +1,32 @@ +package recommod + +import "91porn-server/models/v/vidmod" + +// BaseInfo 推荐用户信息 +type BaseInfo struct { + UID uint64 `json:"uid"` + Name string `json:"name"` + Gender string `json:"gender"` + Portrait string `json:"portrait"` + HasLocked bool `json:"hasLocked"` //已禁止登陆 + HasBanned bool `json:"hasBanned"` //已禁言 + CollectionCount int `json:"collectionCount"` //作品总数 +} + +// UserListResp 主播推荐应答 +type UserListResp struct { + List []BaseInfo `json:"list"` + HasNext bool `json:"hasNext"` +} + +// VideoListResp 视频推荐应答 +type VideoListResp struct { + //视频信息 + VInfos []*vidmod.VideoInfo `json:"vInfos"` + //总页数 + TotalPages int `json:"totalPages"` + //环形队列非空时始终可继续读取 + HasNext bool `json:"hasNext"` + //当前全局推荐队列版本,仅用于排查 + QueueVersion string `json:"queueVersion,omitempty"` +} diff --git a/models/v/recommod/m3u8sign.go b/models/v/recommod/m3u8sign.go new file mode 100644 index 0000000..a3bb036 --- /dev/null +++ b/models/v/recommod/m3u8sign.go @@ -0,0 +1,10 @@ +package recommod + +import "91porn-server/models/v/vidmod" + +// SignM3u8 推荐视频列表(/api/app/recommend/vid/list),供 m3u8ticket 零反射签票。 +func (r VideoListResp) SignM3u8(s vidmod.M3u8Signer) { + vidmod.SignM3u8Infos(s, r.VInfos) +} + +var _ vidmod.M3u8Signable = VideoListResp{} diff --git a/models/v/recommod/reco.go b/models/v/recommod/reco.go new file mode 100644 index 0000000..fc68e97 --- /dev/null +++ b/models/v/recommod/reco.go @@ -0,0 +1,201 @@ +package recommod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb, rdb *db.MongoDB + +const table = models.UserRecoRecord + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func collRead() *db.MongoTool { + return rdb.Coll(table) +} + +// initIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "type", Value: 1}, {Key: "mark", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func insertRecoRecord(uid uint64, newsType string, typ string, mark string, line time.Time) error { + now := time.Now() + record := RecoRecord{ + UID: uid, + Type: typ, + Mark: mark, + TimeLine: line, + UpdatedAt: now, + CreatedAt: now, + } + if vidmod.IsSP(newsType) { + record.TimeLine = line + } + if vidmod.IsCover(newsType) { + record.CoverTimeLine = line + } + if _, err := coll(nil).InsertOne(&record); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "insertRecoRecord", table, "InsertOne", err), + log.Any("uid", uid), + log.Any("typ", typ), + log.Any("mark", mark), + log.Any("line", line), + log.Any("newsType", newsType), + ) + return err + } + return nil +} + +func updateRecoRecord(uid uint64, newsType string, typ string, mark string, line time.Time) (int64, error) { + now := time.Now() + cond := bson.M{"uid": uid, "type": typ, "mark": mark} + update := bson.M{} + if vidmod.IsSP(newsType) { + update = bson.M{"$set": bson.M{"timeLine": line, "updatedAt": now}} + } + if vidmod.IsCover(newsType) { + update = bson.M{"$set": bson.M{"coverTimeLine": line, "updatedAt": now}} + } + result, err := coll(nil).UpdateOne(cond, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "updateRecoRecord", table, "UpdateOne", err), + log.Any("uid", uid), + log.Any("typ", typ), + log.Any("mark", mark), + log.Any("line", line), + log.Any("newsType", newsType), + ) + } + if result == nil { + return 0, err + } + return result.MatchedCount, err +} + +func findRecoRecord(uid uint64, typs []string) ([]RecoRecord, error) { + var record []RecoRecord + cond := bson.M{"uid": uid, "type": bson.M{"$in": typs}} + if err := coll(nil).Find(&record, cond); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "findRecoRecord", table, "Find", err), + log.Any("uid", uid), + log.Any("typs", typs), + ) + return record, err + } + return record, nil +} + +func findRecoRecordWithMark(uid uint64, typ string, marks []string) ([]RecoRecord, error) { + var record []RecoRecord + cond := bson.M{"uid": uid, "type": typ, "mark": bson.M{"$in": marks}} + if err := collRead().Find(&record, cond); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "findRecoRecordWithMark", table, "Find", err), + log.Any("uid", uid), + log.Any("typ", typ), + log.Any("marks", marks), + ) + return record, err + } + return record, nil +} + +// SetRecoTimeLines 刷新维度的推荐时间轴 +func SetRecoTimeLines(uid uint64, newsType string, set map[string]time.Time) error { + for typ, line := range set { + if line.IsZero() { + continue + } + matchCnt, err := updateRecoRecord(uid, newsType, typ, "", line) + if err != nil { + return err + } + if matchCnt == 0 { + if err = insertRecoRecord(uid, newsType, typ, "", line); err != nil { + return err + } + } + } + return nil +} + +// SetRecoTimeLinesWithMark 刷新维度的推荐时间轴 +func SetRecoTimeLinesWithMark(uid uint64, newsType string, typ string, set map[string]time.Time) error { + for mark, line := range set { + if line.IsZero() { + continue + } + matchCnt, err := updateRecoRecord(uid, newsType, typ, mark, line) + if err != nil { + return err + } + if matchCnt == 0 { + if err = insertRecoRecord(uid, newsType, typ, mark, line); err != nil { + return err + } + } + } + return nil +} + +// GetRecoTimeLines 获取维度的推荐时间轴 +func GetRecoTimeLines(uid uint64, newsType string, typs []string) (map[string]time.Time, error) { + m := make(map[string]time.Time) + records, err := findRecoRecord(uid, typs) + for _, r := range records { + if vidmod.IsSP(newsType) { + m[r.Type] = ReviseTimeline(r.TimeLine) + } + if vidmod.IsCover(newsType) { + m[r.Type] = ReviseTimeline(r.CoverTimeLine) + } + } + return m, err +} + +// GetRecoTimeLinesWithMark 获取维度的推荐时间轴 +func GetRecoTimeLinesWithMark(uid uint64, newsType, typ string, marks []string) (map[string]time.Time, error) { + m := make(map[string]time.Time) + records, err := findRecoRecordWithMark(uid, typ, marks) + for _, r := range records { + if vidmod.IsSP(newsType) { + m[r.Mark] = ReviseTimeline(r.TimeLine) + } + if vidmod.IsCover(newsType) { + m[r.Mark] = ReviseTimeline(r.CoverTimeLine) + } + } + return m, err +} + +func ReviseTimeline(line time.Time) time.Time { + tenMonthsBefore := time.Now().AddDate(0, -10, 0) + if line.Before(tenMonthsBefore) || line.After(time.Now()) { + return tenMonthsBefore + } + return line +} diff --git a/models/v/recommod/type.go b/models/v/recommod/type.go new file mode 100644 index 0000000..95750ce --- /dev/null +++ b/models/v/recommod/type.go @@ -0,0 +1,24 @@ +package recommod + +import ( + "time" + + "91porn-server/common/db" +) + +// RecoRecord 推荐记录 +type RecoRecord struct { + UID uint64 `json:"uid" bson:"uid"` + Type string `json:"type" bson:"type"` //记录推荐维度, chosenVideo,tagVideo,sameCityVideo,newVideo,unPopularVideo,pushVideo + Mark string `json:"mark" bson:"mark"` //标示字段,维度下的区分字段 + TimeLine time.Time `json:"timeLine" bson:"timeLine"` //推荐进度 + CoverTimeLine time.Time `json:"coverTimeLine" bson:"coverTimeLine"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +func Init() { + mdb = db.Init(table) + rdb = db.InitRead(table) + initIndex() +} diff --git a/models/v/reptmod/list.go b/models/v/reptmod/list.go new file mode 100644 index 0000000..cb53dd5 --- /dev/null +++ b/models/v/reptmod/list.go @@ -0,0 +1,53 @@ +package reptmod + +import ( + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Matcher = pageopt.Matcher + +// InObjTypeMatch +type InObjTypeMatch struct { + Typs []ReportObjType +} + +func (i *InObjTypeMatch) New() Matcher { + return pageopt.NewInMatch("objType", i.Typs) +} + +// TypesMatch +type TypesMatch struct { + Types *string +} + +func (t *TypesMatch) New() Matcher { + return pageopt.NewAssignMatch("types", t.Types) +} + +// CreatedAtGTEAndLTMatch +type CreatedAtGTEAndLTMatch = pageopt.CreatedAtGTEAndLTMatch + +func List(sort bson.D, skip, limit int64, matchers ...pageopt.Matcher) ([]Report, error) { + filter := pageopt.MergeM(matchers) + opt := (&options.FindOptions{}). + SetSort(sort). + SetSkip(skip). + SetLimit(limit) + reportList := make([]Report, 0, limit) + if err := coll(nil).Find(&reportList, filter, opt); err != nil { + return nil, err + } + return reportList, nil +} + +func Count(matchers ...pageopt.Matcher) (int64, error) { + filter := pageopt.MergeM(matchers) + count, err := coll(nil).Count(filter) + if err != nil { + return 0, err + } + return count, nil +} diff --git a/models/v/reptmod/report.go b/models/v/reptmod/report.go new file mode 100644 index 0000000..416eeaa --- /dev/null +++ b/models/v/reptmod/report.go @@ -0,0 +1,123 @@ +package reptmod + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.Report + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "objType", Value: 1}}, + }, + { + Keys: bson.D{{Key: "objID", Value: 1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{{Key: "objUID", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}}, + Options: options.Index().SetSparse(true), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// setOnInsert SetOnInsert 未匹配到文档则插入, 否则不做任何操作 +// @return true:已经举报过 false:未举报过 +func setOnInsert(update ReportDoc, repeatTime time.Duration) (bool, error) { + filterDoc := ReportDoc{ + UID: update.UID, + ObjType: update.ObjType, + ObjID: update.ObjID, + ObjUID: update.ObjUID, + } + filter, err := common.ToBsonM(filterDoc) + if err != nil { + return false, fmt.Errorf("coll:%s SetOnInsert filter ToBsonM fail, error:%+v ", table, err) + } + now := time.Now() + filter["createdAt"] = bson.M{ + "$gte": now.Add(-1 * repeatTime), + } + update.CreatedAt = &now + updateM, err := common.ToBsonM(update) + if err != nil { + return false, fmt.Errorf("coll:%s SetOnInsert update ToBsonM fail, error:%+v ", table, err) + } + result, err := coll(nil).UpsertOne(filter, bson.M{ + "$setOnInsert": updateM, + }) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "findRecoRecordWithMark", table, "Find", err), + log.Any("update", update), + log.Any("repeatTime", repeatTime), + ) + return false, err + } + return result.MatchedCount != 0, err +} + +// 举报 +func Do(uid uint64, objType ReportObjType, objID *primitive.ObjectID, objUID *uint64, types string, repeatTime time.Duration) (bool, error) { + doc := ReportDoc{ + UID: &uid, + ObjType: &objType, + Types: &types, + } + if objType == User { + doc.ObjUID = objUID //缺省字段 + } else { + doc.ObjID = objID + } + return setOnInsert(doc, repeatTime) +} + +// 举报已处理 +func SetStatus(id primitive.ObjectID, status bool) error { + filter := bson.M{"_id": id} + update := bson.M{"$set": bson.M{"status": status}} + if !status { + update = bson.M{"$unset": bson.M{"status": 1}} + } + if _, err := coll(nil).UpdateOne(filter, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "findRecoRecordWithMark", table, "Find", err), + log.Any("id", id), + log.Any("status", status), + ) + return err + } + return nil +} diff --git a/models/v/reptmod/type.go b/models/v/reptmod/type.go new file mode 100644 index 0000000..b02675b --- /dev/null +++ b/models/v/reptmod/type.go @@ -0,0 +1,45 @@ +package reptmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ReportObjType string + +const ( + Video ReportObjType = "video" + Comment ReportObjType = "comment" + User ReportObjType = "user" +) + +// Report 举报 +type Report struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + UID uint64 `bson:"uid"` // 举报者uid + ObjType ReportObjType `bson:"objType"` // 举报对象类型: video、comment、user + ObjID *primitive.ObjectID `bson:"objID,omitempty"` // 举报对象类型非user,缺省参数 + ObjUID *uint64 `bson:"objUID,omitempty"` // 举报对象类型是user,缺省参数 + Types string `bson:"types"` // 类型:内容违规/账号违规/侵权/其他 + Status bool `bson:"status"` // 举报已经处理 + CreatedAt time.Time `bson:"createdAt"` // 创建时间 +} + +type ReportDoc struct { + ID *primitive.ObjectID `bson:"_id,omitempty"` + UID *uint64 `bson:"uid,omitempty"` // 举报者uid + ObjType *ReportObjType `bson:"objType,omitempty"` // 举报对象类型 video、comment、user + ObjID *primitive.ObjectID `bson:"objID,omitempty"` // 举报对象类型非user,选用 + ObjUID *uint64 `bson:"objUID,omitempty"` // 举报对象类型是user,选用 + Types *string `bson:"types,omitempty"` // 类型:内容违规/账号违规/侵权/其他 + IsDone *bool `bson:"done,omitempty"` // 举报已经处理 + CreatedAt *time.Time `bson:"createdAt,omitempty"` // 创建时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/repttypemod/repttype.go b/models/v/repttypemod/repttype.go new file mode 100644 index 0000000..a74fcf3 --- /dev/null +++ b/models/v/repttypemod/repttype.go @@ -0,0 +1,62 @@ +package repttypemod + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.ReportTypes + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "name", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// InserOne InserOne +func InserOne(name string) error { + now := time.Now() + doc := ReportTypes{ + Name: &name, + CreatedAt: &now, + } + docM, err := common.ToBsonM(doc) + if err != nil { + return fmt.Errorf("coll:%s SetOnInsert filter ToBsonM fail, error:%+v ", table, err) + } + _, err = coll(nil).InsertOne(docM) + return err +} + +// List ReportTypes Array +func List() (list []ReportTypes, err error) { + err = coll(nil).Find(&list, bson.M{}) + return +} diff --git a/models/v/repttypemod/type.go b/models/v/repttypemod/type.go new file mode 100644 index 0000000..cfd14b3 --- /dev/null +++ b/models/v/repttypemod/type.go @@ -0,0 +1,20 @@ +package repttypemod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ReportTypes struct { + ID *primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name *string `json:"name" bson:"name,omitempty"` + CreatedAt *time.Time `bson:"createdAt,omitempty"` // 创建时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/rjctmpltmod/rjctmplt.go b/models/v/rjctmpltmod/rjctmplt.go new file mode 100644 index 0000000..a1638ca --- /dev/null +++ b/models/v/rjctmpltmod/rjctmplt.go @@ -0,0 +1,81 @@ +package rjctmpltmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.RejectTemplate + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initRejectTemplateIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// InsertOne 插入单条 +func InsertOne(c RejectTemplate) error { + c.CreatedAt = time.Now() + c.UpdatedAt = c.CreatedAt + if _, err := coll(nil).InsertOne(c); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertOne", err)) + return err + } + return nil +} + +// FindMany 查找所有 +func FindMany(rType string) ([]RejectTemplate, error) { + opts := options.FindOptions{ + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + var data []RejectTemplate + if err := coll(nil).Find(&data, bson.M{"type": rType}, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindMany", table, "find", err)) + return data, err + } + return data, nil +} + +// Update 修改 +func Update(set Edit) error { + set.UpdatedAt = time.Now() + if _, err := coll(nil).UpdateOne(bson.M{"_id": set.ID}, bson.M{"$set": set}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err)) + return err + } + return nil +} + +// Update 修改 +func DeleteMany(ids []primitive.ObjectID) error { + if _, err := coll(nil).DeleteMany(bson.M{"_id": bson.M{"$in": ids}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteMany", table, "DeleteMany", err)) + return err + } + return nil +} diff --git a/models/v/rjctmpltmod/type.go b/models/v/rjctmpltmod/type.go new file mode 100644 index 0000000..27ce8d2 --- /dev/null +++ b/models/v/rjctmpltmod/type.go @@ -0,0 +1,30 @@ +package rjctmpltmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type CmtType string + +const ( + Video = "video" + User = "user" +) + +// RejectTemplate 拒绝理由模板 评论 +type RejectTemplate struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //全局唯一ID + Type string `json:"type" bson:"type"` //类型 vid-视频相关 user-用户相关 + Content string `json:"content" bson:"content"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //评论发表时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //评论发表时间 +} diff --git a/models/v/rjctmpltmod/web.go b/models/v/rjctmpltmod/web.go new file mode 100644 index 0000000..8a755fe --- /dev/null +++ b/models/v/rjctmpltmod/web.go @@ -0,0 +1,17 @@ +package rjctmpltmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Edit struct { + ID primitive.ObjectID `json:"id" bson:"_id"` + Content string `json:"content" bson:"content"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //评论发表时间 +} + +type FindReq struct { + Type string `json:"type" bson:"type"` +} diff --git a/models/v/rolemod/role.go b/models/v/rolemod/role.go new file mode 100644 index 0000000..96e3410 --- /dev/null +++ b/models/v/rolemod/role.go @@ -0,0 +1,132 @@ +package rolemod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.Role + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "path", Value: 1}, {Key: "host", Value: 1}, {Key: "method", Value: 1}}, + Options: options.Index().SetUnique(true).SetSparse(true), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// 获取全部Rules +func Rules() (data []RoleResp, err error) { + if err = coll(nil).Find(&data, bson.M{}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "List", table, "Find", err)) + return + } + return data, nil +} + +func RoleAll(t *db.MongoTool, role string) (data []RoleResp, err error) { + if err = coll(t).Find(&data, bson.M{"authorized_roles": role}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "List", table, "Find", err)) + return + } + return +} + +func UpdateRole(inc []Incr) error { + return webg.VideoDB.Trans(func(tool *db.MongoTool) error { + //先清除角色权限 + if err := RemoveRole(tool, inc[0].AuthorizedRoles); err != nil { + return err + } + return InsertRole(tool, inc) + }) +} + +// 去除原先的角色权限 +func RemoveRole(t *db.MongoTool, role string) error { + res, err := RoleAll(t, role) + if err != nil { + return err + } + write := make([]mongo.WriteModel, len(res)) + opts := options.BulkWrite() + opts.SetOrdered(false) + for i, v := range res { + filter := bson.M{ + "_id": v.ID, + } + update := bson.M{ + "$pull": bson.M{"authorized_roles": role, "forbidden_roles": role}, + } + write[i] = mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update) + } + if _, err = coll(t).Bulk(write, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Insert", err), log.Any("role", role)) + return err + } + return nil +} + +// InsertRole 插入一条数据 +func InsertRole(t *db.MongoTool, inc []Incr) error { + write := make([]mongo.WriteModel, len(inc)) + opts := options.BulkWrite() + opts.SetOrdered(false) + for i, v := range inc { + if v.Method == "" { + v.Method = "*" + } + if v.Host == "" { + v.Host = "*" + } + filter := bson.M{"id": v.RoleID, "host": v.Host, "path": v.Path, "method": v.Method} + update := bson.M{ + "$setOnInsert": bson.M{ + "createdAt": time.Now(), + "id": v.RoleID, + "host": v.Host, + "path": v.Path, + "method": v.Method, + }, + "$set": bson.M{ + "updatedAt": time.Now(), + }, + "$addToSet": bson.M{ + "authorized_roles": v.AuthorizedRoles, + "forbidden_roles": v.ForbiddenRoles, + }, + } + write[i] = mongo.NewUpdateOneModel(). + SetUpsert(true). + SetFilter(filter). + SetUpdate(update) + } + if _, err := coll(t).Bulk(write, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Insert", err), log.Any("inc", inc)) + return err + } + return nil +} diff --git a/models/v/rolemod/type.go b/models/v/rolemod/type.go new file mode 100644 index 0000000..f3e2dad --- /dev/null +++ b/models/v/rolemod/type.go @@ -0,0 +1,51 @@ +package rolemod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 用于记录权限配置 +type RoleConf struct { + ID primitive.ObjectID `json:"_id" bson:"_id,omitempty"` + RoleID int `json:"id" bson:"id,omitempty"` + Host string `json:"host" bson:"host"` // Host 定义资源的Host,允许使用增强的通配符。 + Path string `json:"path" bson:"path"` // Path 定义资源的Path,允许使用增强的通配符。 + Method string `json:"method" bson:"method"` // Method 定义资源的Method,允许使用增强的通配符。 + AuthorizedRoles []string `json:"authorized_roles" bson:"authorized_roles"` // AuthorizedRoles定义允许访问资源的角色 + ForbiddenRoles []string `json:"forbidden_roles" bson:"forbidden_roles"` + AllowAnyone bool `json:"allow_anyone" bson:"allow_anyone"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +// Incr 插入结构体 +type Incr struct { + RoleID int `json:"id" bson:"id"` + Host string `json:"host" bson:"host"` // Host 定义资源的Host,允许使用增强的通配符。 + Path string `json:"path" bson:"path" binding:"required"` // Path 定义资源的Path,允许使用增强的通配符。 + Method string `json:"method" bson:"method"` // Method 定义资源的Method,允许使用增强的通配符。 + AuthorizedRoles string `json:"authorized_roles" bson:"authorized_roles" binding:"required"` // AuthorizedRoles定义允许访问资源的角色 + ForbiddenRoles string `json:"forbidden_roles" bson:"forbidden_roles"` + AllowAnyone bool `json:"allow_anyone" bson:"allow_anyone"` +} + +// EditDoc 编辑结构体 +type EditDoc struct { + RoleID *int `json:"id" bson:"id"` + Host *string `json:"host" bson:"host" binding:"required"` // Host 定义资源的Host,允许使用增强的通配符。 + Path *string `json:"path" bson:"path" binding:"required"` // Path 定义资源的Path,允许使用增强的通配符。 + Method *string `json:"method" bson:"method" binding:"required" ` // Method 定义资源的Method,允许使用增强的通配符。 + AuthorizedRoles []string `json:"authorized_roles" bson:"authorized_roles"` // AuthorizedRoles定义允许访问资源的角色 + ForbiddenRoles []string `json:"forbidden_roles" bson:"forbidden_roles"` + AllowAnyone *bool `json:"allow_anyone" bson:"allow_anyone"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/rolemod/web.go b/models/v/rolemod/web.go new file mode 100644 index 0000000..06d3446 --- /dev/null +++ b/models/v/rolemod/web.go @@ -0,0 +1,33 @@ +package rolemod + +import ( + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListParam struct { + ListFilter + commod.Page +} + +type ListFilter struct { + UID uint64 `form:"uid" json:"uid,omitempty" bson:"uid,omitempty"` //用户ID +} + +type WebResp struct { + Total int64 `json:"total"` + List interface{} `json:"list"` +} + +// 用于记录权限配置 +type RoleResp struct { + ID primitive.ObjectID `json:"_id" bson:"_id,omitempty"` + RoleID int `json:"id" bson:"id"` + Host string `json:"host" bson:"host"` // Host 定义资源的Host,允许使用增强的通配符。 + Path string `json:"path" bson:"path"` // Path 定义资源的Path,允许使用增强的通配符。 + Method string `json:"method" bson:"method"` // Method 定义资源的Method,允许使用增强的通配符。 + AuthorizedRoles []string `json:"authorized_roles" bson:"authorized_roles"` // AuthorizedRoles定义允许访问资源的角色 + ForbiddenRoles []string `json:"forbidden_roles" bson:"forbidden_roles"` + AllowAnyone bool `json:"allow_anyone" bson:"allow_anyone"` +} diff --git a/models/v/scenebannermod/model.go b/models/v/scenebannermod/model.go new file mode 100644 index 0000000..12cebb0 --- /dev/null +++ b/models/v/scenebannermod/model.go @@ -0,0 +1,150 @@ +package scenebannermod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +func Init() { + mdb = db.Init(models.SceneBanner) + // 旧版本通过 scene_1 唯一索引限制每个场景只能配置一条 Banner。 + // 多图轮播需要先移除该索引,已有 Banner 数据不受影响。 + if err := coll().DropIndexIfExists("scene_1"); err != nil { + panic(fmt.Sprintf("%s model drop legacy index err ==>[%+v]", models.SceneBanner, err)) + } + if _, err := coll().CreateIndex([]mongo.IndexModel{ + { + Keys: bson.D{ + {Key: "scene", Value: 1}, + {Key: "sort", Value: -1}, + {Key: "updatedAt", Value: -1}, + }, + }, + { + Keys: bson.D{ + {Key: "scene", Value: 1}, + {Key: "enable", Value: 1}, + {Key: "startAt", Value: 1}, + {Key: "endAt", Value: 1}, + }, + }, + }); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", models.SceneBanner, err)) + } +} + +func coll() *db.MongoTool { + return mdb.Coll(models.SceneBanner) +} + +func Insert(p *SceneBanner) error { + p.Normalize() + if err := p.Validate(); err != nil { + return err + } + now := time.Now() + p.ID = primitive.NewObjectID() + p.CreatedAt = now + p.UpdatedAt = now + _, err := coll().InsertOne(p) + return err +} + +func Update(p *SceneBanner) error { + if p.ID.IsZero() { + return fmt.Errorf("id is required") + } + p.Normalize() + if err := p.Validate(); err != nil { + return err + } + p.UpdatedAt = time.Now() + set := bson.M{ + "scene": p.Scene, + "imageUrl": p.ImageURL, + "mediaType": p.MediaType, + "linkType": p.LinkType, + "linkValue": p.LinkValue, + "sort": p.Sort, + "enable": p.Enable, + "updatedAt": p.UpdatedAt, + } + update := bson.M{"$set": set} + unset := bson.M{} + if p.StartAt == nil { + unset["startAt"] = "" + } else { + set["startAt"] = *p.StartAt + } + if p.EndAt == nil { + unset["endAt"] = "" + } else { + set["endAt"] = *p.EndAt + } + if len(unset) > 0 { + update["$unset"] = unset + } + result, err := coll().UpdateOne(bson.M{"_id": p.ID}, update) + if err != nil { + return err + } + if result.MatchedCount == 0 { + return mongo.ErrNoDocuments + } + return nil +} + +func Delete(id primitive.ObjectID) error { + if id.IsZero() { + return fmt.Errorf("id is required") + } + _, err := coll().DeleteById(id) + return err +} + +func List(scene string) ([]SceneBanner, error) { + filter := bson.M{} + if scene != "" { + filter["scene"] = scene + } + var list []SceneBanner + err := coll().Find(&list, filter, bannerFindOptions()) + return list, err +} + +func FindActive(scene string, now time.Time) ([]SceneBanner, error) { + filter := bson.M{ + "scene": scene, + "enable": true, + "$and": bson.A{ + bson.M{"$or": bson.A{ + bson.M{"startAt": bson.M{"$exists": false}}, + bson.M{"startAt": nil}, + bson.M{"startAt": bson.M{"$lte": now}}, + }}, + bson.M{"$or": bson.A{ + bson.M{"endAt": bson.M{"$exists": false}}, + bson.M{"endAt": nil}, + bson.M{"endAt": bson.M{"$gt": now}}, + }}, + }, + } + var list []SceneBanner + err := coll().Find(&list, filter, bannerFindOptions()) + return list, err +} + +func bannerFindOptions() *options.FindOptions { + return options.Find(). + SetSort(bson.D{{Key: "sort", Value: -1}, {Key: "updatedAt", Value: -1}}) +} diff --git a/models/v/scenebannermod/type.go b/models/v/scenebannermod/type.go new file mode 100644 index 0000000..9b79c4e --- /dev/null +++ b/models/v/scenebannermod/type.go @@ -0,0 +1,85 @@ +package scenebannermod + +import ( + "fmt" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + SceneCommentTop = "COMMENT_TOP" + + MediaTypeImage = "IMAGE" + MediaTypeGIF = "GIF" + + LinkTypeInternal = "INTERNAL" + LinkTypeExternal = "EXTERNAL" + LinkTypeNone = "NONE" +) + +type SceneBanner struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Scene string `json:"scene" bson:"scene"` + ImageURL string `json:"imageUrl" bson:"imageUrl"` + MediaType string `json:"mediaType" bson:"mediaType"` + LinkType string `json:"linkType" bson:"linkType"` + LinkValue string `json:"linkValue" bson:"linkValue"` + Sort int `json:"sort" bson:"sort"` + Enable bool `json:"enable" bson:"enable"` + StartAt *time.Time `json:"startAt,omitempty" bson:"startAt,omitempty"` + EndAt *time.Time `json:"endAt,omitempty" bson:"endAt,omitempty"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +func ValidScene(scene string) bool { + return scene == SceneCommentTop +} + +func (p *SceneBanner) Normalize() { + p.Scene = strings.TrimSpace(strings.ToUpper(p.Scene)) + p.ImageURL = strings.TrimSpace(p.ImageURL) + p.MediaType = strings.TrimSpace(strings.ToUpper(p.MediaType)) + p.LinkType = strings.TrimSpace(strings.ToUpper(p.LinkType)) + p.LinkValue = strings.TrimSpace(p.LinkValue) +} + +func (p SceneBanner) Validate() error { + if !ValidScene(p.Scene) { + return fmt.Errorf("unsupported scene: %s", p.Scene) + } + if p.ImageURL == "" { + return fmt.Errorf("imageUrl is required") + } + if p.MediaType != MediaTypeImage && p.MediaType != MediaTypeGIF { + return fmt.Errorf("unsupported mediaType: %s", p.MediaType) + } + switch p.LinkType { + case LinkTypeInternal, LinkTypeExternal: + if p.LinkValue == "" { + return fmt.Errorf("linkValue is required for %s", p.LinkType) + } + case LinkTypeNone: + if p.LinkValue != "" { + return fmt.Errorf("linkValue must be empty when linkType is NONE") + } + default: + return fmt.Errorf("unsupported linkType: %s", p.LinkType) + } + if p.StartAt != nil && p.EndAt != nil && !p.EndAt.After(*p.StartAt) { + return fmt.Errorf("endAt must be later than startAt") + } + return nil +} + +func (p SceneBanner) ActiveAt(now time.Time) bool { + if !p.Enable { + return false + } + if p.StartAt != nil && p.StartAt.After(now) { + return false + } + return p.EndAt == nil || p.EndAt.After(now) +} diff --git a/models/v/scenebannermod/type_test.go b/models/v/scenebannermod/type_test.go new file mode 100644 index 0000000..082ec70 --- /dev/null +++ b/models/v/scenebannermod/type_test.go @@ -0,0 +1,47 @@ +package scenebannermod + +import ( + "reflect" + "testing" + "time" + + "go.mongodb.org/mongo-driver/bson" +) + +func TestSceneBannerValidate(t *testing.T) { + banner := SceneBanner{ + Scene: "comment_top", + ImageURL: "https://example.com/banner.gif", + MediaType: "gif", + LinkType: "internal", + LinkValue: "video://detail?id=1", + } + banner.Normalize() + if err := banner.Validate(); err != nil { + t.Fatal(err) + } + if banner.Scene != SceneCommentTop || banner.MediaType != MediaTypeGIF { + t.Fatalf("unexpected normalized banner: %+v", banner) + } +} + +func TestSceneBannerActiveAt(t *testing.T) { + now := time.Now() + start := now.Add(-time.Minute) + end := now.Add(time.Minute) + banner := SceneBanner{Enable: true, StartAt: &start, EndAt: &end} + if !banner.ActiveAt(now) { + t.Fatal("banner should be active") + } +} + +func TestBannerFindOptionsSupportsCarousel(t *testing.T) { + opts := bannerFindOptions() + if opts.Limit != nil { + t.Fatalf("carousel query must not limit result count: %d", *opts.Limit) + } + wantSort := bson.D{{Key: "sort", Value: -1}, {Key: "updatedAt", Value: -1}} + if !reflect.DeepEqual(opts.Sort, wantSort) { + t.Fatalf("unexpected carousel sort: %#v", opts.Sort) + } +} diff --git a/models/v/sensitivewordmod/crud.go b/models/v/sensitivewordmod/crud.go new file mode 100644 index 0000000..b601ddf --- /dev/null +++ b/models/v/sensitivewordmod/crud.go @@ -0,0 +1,165 @@ +package sensitivewordmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + + +var mdb *db.MongoDB + +const table = models.SensitiveWord + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "word", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "category", Value: 1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// Add 新增单条 +func Add(w *SensitiveWord) error { + now := time.Now() + w.CreatedAt = now + w.UpdatedAt = now + w.Status = StatusEnabled + if _, err := coll(nil).InsertOne(w); err != nil { + log.Error(fmt.Sprintf("[sensitivewordmod] Add fail: %+v", err)) + return err + } + return nil +} + +// UpsertByWord 按主词 upsert(导入时使用) +func UpsertByWord(w *SensitiveWord) error { + now := time.Now() + filter := bson.M{"word": w.Word} + update := bson.M{ + "$set": bson.M{ + "category": w.Category, + "updatedAt": now, + }, + "$setOnInsert": bson.M{ + "status": StatusEnabled, + "createdAt": now, + }, + } + if _, err := coll(nil).UpsertOne(filter, update); err != nil { + log.Error(fmt.Sprintf("[sensitivewordmod] UpsertByWord fail: %+v", err), log.Any("word", w.Word)) + return err + } + return nil +} + +// Update 更新单条 +func Update(id primitive.ObjectID, fields bson.M) error { + fields["updatedAt"] = time.Now() + cond := bson.M{"_id": id} + if _, err := coll(nil).UpdateOne(cond, bson.M{"$set": fields}); err != nil { + log.Error(fmt.Sprintf("[sensitivewordmod] Update fail: %+v", err)) + return err + } + return nil +} + +// Delete 批量删除 +func Delete(ids []primitive.ObjectID) (int64, error) { + cond := bson.M{"_id": bson.M{"$in": ids}} + result, err := coll(nil).DeleteMany(cond) + if err != nil { + log.Error(fmt.Sprintf("[sensitivewordmod] Delete fail: %+v", err)) + return 0, err + } + return result.DeletedCount, nil +} + +// List 分页查询 +func List(req *ListReq) ([]*SensitiveWord, int64, error) { + cond := buildCond(req) + total, err := coll(nil).Count(cond) + if err != nil { + return nil, 0, err + } + + page := req.Page + size := req.Size + if page < 1 { + page = 1 + } + if size < 1 { + size = 20 + } + skip := (page - 1) * size + + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "createdAt", Value: -1}}). + SetSkip(skip). + SetLimit(size) + + var list []*SensitiveWord + if err = coll(nil).Find(&list, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[sensitivewordmod] List find fail: %+v", err)) + return nil, total, err + } + return list, total, nil +} + +// FindAll 查询全部(导出时使用) +func FindAll(req *ListReq) ([]*SensitiveWord, error) { + cond := buildCond(req) + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "category", Value: 1}, {Key: "createdAt", Value: 1}}) + + var list []*SensitiveWord + if err := coll(nil).Find(&list, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[sensitivewordmod] FindAll fail: %+v", err)) + return nil, err + } + return list, nil +} + +func buildCond(req *ListReq) bson.M { + cond := bson.M{} + if req.Category != "" { + cond["category"] = req.Category + } + if req.Status != nil { + cond["status"] = *req.Status + } + if req.Keyword != "" { + cond["word"] = bson.M{"$regex": req.Keyword, "$options": "i"} + } + return cond +} diff --git a/models/v/sensitivewordmod/match.go b/models/v/sensitivewordmod/match.go new file mode 100644 index 0000000..58846a3 --- /dev/null +++ b/models/v/sensitivewordmod/match.go @@ -0,0 +1,64 @@ +package sensitivewordmod + +import ( + "strings" + + "91porn-server/common/log" +) + +// LoadEnabledTerms 加载启用状态的敏感词词条。 +// DB 失败时降级返回空切片,调用方应视为"词库为空,不命中",避免阻塞业务流程。 +func LoadEnabledTerms() []string { + enabled := StatusEnabled + list, err := FindAll(&ListReq{Status: &enabled}) + if err != nil { + log.Error("sensitivewordmod LoadEnabledTerms fail", log.E(err)) + return nil + } + out := make([]string, 0, len(list)) + for _, w := range list { + if w.Word != "" { + out = append(out, w.Word) + } + } + return out +} + +// MatchHits 对单个文本做 substring 命中检测,返回去重后的命中词。 +// 与 skd/service/contentreviewser/matcher.findHits 行为一致。 +func MatchHits(input string, terms []string) []string { + if input == "" || len(terms) == 0 { + return nil + } + seen := make(map[string]struct{}) + hits := make([]string, 0) + for _, t := range terms { + if t == "" { + continue + } + if _, ok := seen[t]; ok { + continue + } + if strings.Contains(input, t) { + seen[t] = struct{}{} + hits = append(hits, t) + } + } + return hits +} + +// FormatHitDetail 把三类字段的命中拼成 "标题命中 XX 内容命中 YY 富文本命中 ZZ"。 +// 任一字段未命中则该段省略;全部为空时返回空串。 +func FormatHitDetail(titleHits, contentHits, richHits []string) string { + parts := make([]string, 0, 3) + if len(titleHits) > 0 { + parts = append(parts, "标题命中 "+strings.Join(titleHits, "、")) + } + if len(contentHits) > 0 { + parts = append(parts, "内容命中 "+strings.Join(contentHits, "、")) + } + if len(richHits) > 0 { + parts = append(parts, "富文本命中 "+strings.Join(richHits, "、")) + } + return strings.Join(parts, " ") +} diff --git a/models/v/sensitivewordmod/type.go b/models/v/sensitivewordmod/type.go new file mode 100644 index 0000000..b51600d --- /dev/null +++ b/models/v/sensitivewordmod/type.go @@ -0,0 +1,51 @@ +package sensitivewordmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Status 启用状态 +const ( + StatusEnabled = 1 + StatusDisabled = 0 +) + +// SensitiveWord 敏感词 +type SensitiveWord struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Category string `json:"category" bson:"category"` // 一级分类 + Word string `json:"word" bson:"word"` // 词条 + Status int `json:"status" bson:"status"` // 1-启用 0-禁用 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +// ListReq 查询请求 +type ListReq struct { + Page int64 `json:"page" form:"page"` + Size int64 `json:"size" form:"size"` + Category string `json:"category" form:"category"` + Keyword string `json:"keyword" form:"keyword"` // 按词条模糊搜索 + Status *int `json:"status" form:"status"` +} + +// AddReq 新增请求 +type AddReq struct { + Category string `json:"category" binding:"required"` + Word string `json:"word" binding:"required"` +} + +// UpdateReq 编辑请求 +type UpdateReq struct { + ID string `json:"id" binding:"required"` + Category string `json:"category"` + Word string `json:"word"` + Status *int `json:"status"` +} + +// DeleteReq 删除请求 +type DeleteReq struct { + IDs []string `json:"ids" binding:"required"` +} diff --git a/models/v/sessionmod/app.go b/models/v/sessionmod/app.go new file mode 100644 index 0000000..470b39f --- /dev/null +++ b/models/v/sessionmod/app.go @@ -0,0 +1,33 @@ +package sessionmod + +import ( + "91porn-server/models/commod" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// QueryCond 查询列表 +type QueryCond struct { + commod.Page +} + +// QuerySessionIdCond 查询会话id +type QuerySessionIdCond struct { + TakeUid uint64 `json:"takeUid" form:"takeUid"` // 接收者uid +} + +// SessionApp 会话app返回实体 +type SessionApp struct { + ID primitive.ObjectID `json:"id" bson:"_id"` // id + SendUid uint64 `json:"sendUid" bson:"sendUid"` // 发送者uid + TakeUid uint64 `json:"takeUid" bson:"takeUid"` // 接收者uid + UserId uint64 `json:"userId" bson:"userId"` // 用户uid + PeerImUserID int64 `json:"peerImUserId" bson:"peerImUserId"` // 对端 IM 平台用户 ID + UserName string `json:"userName" bson:"userName"` // 用户姓名 + UserAvatar string `json:"userAvatar" bson:"userAvatar"` // 用户头像 + SessionId string `json:"sessionId" bson:"sessionId"` // sessionId + NoReadNum int64 `json:"noReadNum"` // 未读消息数 + PreContent string `json:"preContent" bson:"preContent"` // 预览消息内容,存最新一条 + CreatedAt time.Time `json:"createdAt" bson:"updatedAt"` // 创建时间,返回更新时间 +} diff --git a/models/v/sessionmod/session.go b/models/v/sessionmod/session.go new file mode 100644 index 0000000..3c4f2bd --- /dev/null +++ b/models/v/sessionmod/session.go @@ -0,0 +1,134 @@ +package sessionmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.ChatSession + +// Coll 获取表名 +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{"sendUid", 1}}, + }, + { + Keys: bson.D{{"takeUid", 1}}, + }, + { + Keys: bson.D{{"sessionId", 1}}, + }, + { + Keys: bson.D{{"updatedAt", 1}}, + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } + return +} + +// InsertOne 新增 +func InsertOne(mt *db.MongoTool, t *Session) (err error) { + _, err = coll(mt).InsertOne(&t) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertOne", err)) + return + } + return +} + +// Count 查询总条数 +func Count(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Count", models.Activity, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// FindOneByFilter 根据条件查询单个信息 +func FindOneByFilter(cond bson.M) (data Session, err error) { + err = coll(nil).FindOne(&data, cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneByFilter", table, "FindOne", err), + log.Any("cond", cond), + ) + return + } + return +} + +func FindManyByFilter(cond bson.M, opts ...*options.FindOptions) (data []Session, err error) { + data = make([]Session, 0) + err = coll(nil).Find(&data, cond, opts...) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindManyByFilter", table, "Find", err), + log.Any("cond", cond), + ) + return + } + return +} + +// 查询列表,app使用 +func Query(cond bson.M, req *QueryCond) (data []SessionApp, hasNext bool, err error) { + data = make([]SessionApp, 0) + skip := (req.PageNumber - 1) * req.PageSize + sort := bson.D{{"updatedAt", -1}} + opts := options.Find().SetSkip(int64(skip)).SetLimit(int64(req.PageSize) + 1).SetSort(sort) + err = coll(nil).Find(&data, cond, opts) + if err != nil { + log.Error("Query", log.Any("cond", cond), log.Any("skip", skip), log.Any("limit", req.PageSize), log.E(err)) + return + } + if len(data) > int(req.PageSize) { + hasNext = true + data = data[:req.PageSize] + } + return +} + +// UpdOneByFilter 批量更新 +func UpdOneByFilter(t *db.MongoTool, cond bson.M, set bson.M) (err error) { + _, err = coll(t).UpdateOne(cond, set) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdOneByFilter", table, "UpdateOne", err), log.Any("cond", cond), log.Any("set", cond)) + return err + } + return +} + +// Delete 根据条件删除 +func Delete(cond bson.M) (err error) { + _, err = coll(nil).DeleteOne(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Delete", table, "DeleteOne", err), + log.Any("cond", cond), + ) + return + } + return +} diff --git a/models/v/sessionmod/type.go b/models/v/sessionmod/type.go new file mode 100644 index 0000000..db3d601 --- /dev/null +++ b/models/v/sessionmod/type.go @@ -0,0 +1,25 @@ +package sessionmod + +import ( + "91porn-server/common/db" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Init 初始化标签model索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +// Session 会话,sendId是当前用户,则会话是有当前用户主动发起的,反之,对方用户主动发起 +type Session struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 会话id + SendUid uint64 `json:"sendUid" bson:"sendUid"` // 发送者uid + TakeUid uint64 `json:"takeUid" bson:"takeUid"` // 接收者uid + SessionId string `json:"sessionId" bson:"sessionId"` // sessionid + PreContent string `json:"preContent" bson:"preContent"` // 预览消息内容,存最新一条 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 +} diff --git a/models/v/settingmod/setting.go b/models/v/settingmod/setting.go new file mode 100644 index 0000000..17cf935 --- /dev/null +++ b/models/v/settingmod/setting.go @@ -0,0 +1,85 @@ +package settingmod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// SettingUpdateReq 配置信息 +type SettingSelector struct { + BackGround *string `json:"backGround,omitempty" bson:"backGround,omitempty"` //背景图 +} + +const table = models.Setting + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("setting model set index err ==>[%+v]", err)) + } +} + +func InsertSetting(t *db.MongoTool, s *Setting) error { + if _, err := coll(t).InsertOne(s); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertSetting", table, "InsertOne", err)) + return err + } + return nil +} + +// UpdateSetting 修改用户信息 +func UpdateSetting(uid uint64, set SettingSelector) error { + if _, err := coll(nil).UpsertOne(bson.M{"uid": uid}, bson.M{ + "$set": set, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateSetting", table, "UpsertOne", err), + log.Any("uid", uid), + log.Any("set", set), + ) + return err + } + return nil +} + +// FindSetting ()查询 +func FindSetting(uid uint64) (s *Setting, err error) { + if err = coll(nil).FindOne(s, bson.M{"uid": uid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindSetting", table, "FindOne", err), + log.Any("uid", uid), + ) + return + } + return +} + +// FindSettingByID ()查询 +func FindSettingByID(id primitive.ObjectID) (s *Setting, err error) { + if err = coll(nil).FindOneByID(s, id); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindSettingByID", table, "FindOneByID", err), + log.Any("id", id), + ) + return + } + return +} diff --git a/models/v/settingmod/type.go b/models/v/settingmod/type.go new file mode 100644 index 0000000..5fb005c --- /dev/null +++ b/models/v/settingmod/type.go @@ -0,0 +1,32 @@ +package settingmod + +import ( + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + //ALL 全部人 + ALL = iota + //FRIEND 仅好友 + FRIEND + //SELF 仅自己 + SELF + //NO 禁止任何人 + NO +) + +var mdb *db.MongoDB + +// Setting 配置信息 +type Setting struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + UID uint64 `json:"uid" bson:"uid"` + BackGround string `json:"backGround" bson:"backGround"` //背景图 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/signrecordmod/app.go b/models/v/signrecordmod/app.go new file mode 100755 index 0000000..c1241b0 --- /dev/null +++ b/models/v/signrecordmod/app.go @@ -0,0 +1,21 @@ +package signrecordmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// SignRecordInfo 移动端返回内容 +type SignRecordInfo struct { + ID primitive.ObjectID `json:"id"` // 文档id + PID primitive.ObjectID `json:"pid"` // 白嫖卡ID + UID uint64 `json:"uid"` // 用户ID + TotalDays int64 `json:"totalDays"` // 总打卡天数 + CurrentSignDays int64 `json:"currentSignDays"` // 连续打卡天数 + ForgetSignDays int64 `json:"forgetSignDays"` // 忘记打卡天数 + RenewalSignDays int64 `json:"renewalSignDays"` // 补签打卡天数 + SignTime time.Time `json:"signTime"` // 打卡时间 + CreatedAt time.Time `json:"createdAt"` // 创建时间 + UpdateTime time.Time `json:"updateTime"` // 更新时间 +} diff --git a/models/v/signrecordmod/signrecord.go b/models/v/signrecordmod/signrecord.go new file mode 100755 index 0000000..c35b584 --- /dev/null +++ b/models/v/signrecordmod/signrecord.go @@ -0,0 +1,227 @@ +package signrecordmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "encoding/json" + "errors" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetList 获取列表 +func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []SignRecord, count int64, hasNext bool, err error) { + count, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Count", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort).SetSkip(skip).SetLimit(limit + 1) + if err = coll(nil).Find(&res, cond, opts); err != nil { + return + } + // 判断下一页 + if len(res) > int(limit) { + hasNext = true + res = res[:limit] + } + + return +} + +// GetAll 查询全部文档 +func GetAll(filter primitive.M, sort bson.D) (out []*SignRecord, err error) { + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAll", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (SignRecord, error) { + v := SignRecord{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d SignRecord) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// InsertOne 插入记录 +func InsertOne(t *db.MongoTool, d SignRecord) (data primitive.ObjectID, err error) { + filter := bson.M{"uid": d.UID} + + updateCond := bson.M{"totalDays": d.TotalDays, "forgetSignDays": d.ForgetSignDays, + "currentSignDays": d.CurrentSignDays, "endTime": d.EndTime, "signTime": d.SignTime, + "updateTime": d.UpdateTime, "renewalSignDays": d.RenewalSignDays, "createdAt": d.CreatedAt, + "recordTime": d.RecordTime, "pid": d.PID} + + err = coll(t).FindOneAndUpsert(&d, filter, bson.M{"$set": updateCond}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneAndUpdate", table, "InsertOne", err)) + return + } + return +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteMany(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id)) + } + } + + return err +} + +// GetInfoByCond 根据条件查询文档 +func GetInfoByCond(filter primitive.M) (out *SignRecord, err error) { + opts := options.FindOne() + if err = coll(nil).FindOne(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfoByCond", table, "FindOne", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// GetInfoByUID 根据条件查询文档 +func GetInfoByUID(uid uint64) (out []*SignRecord, err error) { + opts := options.Find() + filter := bson.M{"uid": uid} + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfoByUID", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// CreditIncByCond 根据条件新增数据 +func CreditIncByCond(t *db.MongoTool, filter bson.M, cond bson.M) (*SignRecord, error) { + w := SignRecord{} + if err := coll(t).FindOneAndUpsert(&w, filter, bson.M{ + "$inc": cond, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditIncByCond", table, "FindOneAndUpsert", err), + log.Any("cond", cond), + log.Any("filter", filter), + ) + return nil, err + } + return &w, nil +} + +// UpdateDataByCond 根据条件新增数据 +func UpdateDataByCond(t *db.MongoTool, filter bson.M, cond bson.M) (*SignRecord, error) { + w := SignRecord{} + if err := coll(t).FindOneAndUpsert(&w, filter, cond); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditIncByCond", table, "FindOneAndUpsert", err), + log.Any("cond", cond), + log.Any("filter", filter), + ) + return nil, err + } + return &w, nil +} + +// UpdateByCond 更新数据 +func UpdateByCond(t *db.MongoTool, cond primitive.M, data primitive.M) (int64, error) { + result, err := coll(t).UpdateOne(cond, bson.M{"$set": data}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateByCond", table, "UpdateOne", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} diff --git a/models/v/signrecordmod/type.go b/models/v/signrecordmod/type.go new file mode 100755 index 0000000..ff26001 --- /dev/null +++ b/models/v/signrecordmod/type.go @@ -0,0 +1,64 @@ +package signrecordmod + +import ( + "91porn-server/common/db" + "91porn-server/models" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.SignRecord + +type SignRecord struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + PID primitive.ObjectID `json:"pid" bson:"pid"` // 白嫖卡ID + UID uint64 `json:"uid" bson:"uid"` // 用户ID + TotalDays int64 `json:"totalDays" bson:"totalDays"` // 总打卡天数 + CurrentSignDays int64 `json:"currentSignDays" bson:"currentSignDays"` // 连续打卡天数 + ForgetSignDays int64 `json:"forgetSignDays" bson:"forgetSignDays"` // 忘记打卡天数 + RenewalSignDays int64 `json:"renewalSignDays" bson:"renewalSignDays"` // 补签打卡天数 + HasReturn bool `json:"hasReturn" bson:"hasReturn"` // 是否返现 + RecordTime []Record `json:"recordTime" bson:"recordTime"` // 忘记打卡记录 + SignTime time.Time `json:"signTime" bson:"signTime"` // 打卡时间 + EndTime time.Time `json:"endTime" bson:"endTime"` // 打卡结束时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 更新时间 +} + +type Record struct { + SignTime string `json:"signTime" bson:"signTime"` // 打卡时间 + IsSign bool `json:"isSign" bson:"isSign"` // 是否打卡 +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "pid", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} diff --git a/models/v/signrecordmod/web.go b/models/v/signrecordmod/web.go new file mode 100644 index 0000000..30cd202 --- /dev/null +++ b/models/v/signrecordmod/web.go @@ -0,0 +1 @@ +package signrecordmod diff --git a/models/v/signtaskmod/app.go b/models/v/signtaskmod/app.go new file mode 100644 index 0000000..501dfea --- /dev/null +++ b/models/v/signtaskmod/app.go @@ -0,0 +1,11 @@ +package signtaskmod + +type ReceiveTaskReq struct { + TaskID string `json:"taskId" bson:"taskId" binding:"required"` // 任务ID + Type int `json:"type" bson:"type" binding:"required"` // 任务类型 1:每日任务 2:一次性任务 3:成长任务 +} + +type DoTaskReq struct { + TaskID string `json:"taskId" bson:"taskId" binding:"required"` // 任务ID + Type int `json:"type" bson:"type" binding:"required"` // 任务类型 +} diff --git a/models/v/signtaskmod/config.go b/models/v/signtaskmod/config.go new file mode 100644 index 0000000..abfffb9 --- /dev/null +++ b/models/v/signtaskmod/config.go @@ -0,0 +1,151 @@ +package signtaskmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "errors" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.SignTaskConfig + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "status", Value: 1}, {Key: "finishCondition", Value: -1}}, + }, + { + Keys: bson.D{{Key: "type", Value: 1}, {Key: "status", Value: 1}, {Key: "finishCondition", Value: -1}}, + }, + { + Keys: bson.D{{Key: "finishCondition", Value: 1}, {Key: "type", Value: 1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}}, + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("user model set index err ==>[%+v]", err)) + } +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func ConfigList() ([]*TaskConfig, error) { + var out []*TaskConfig = make([]*TaskConfig, 0) + if err := coll(nil).Find(&out, bson.M{"status": true}, options.Find().SetSort(bson.M{"finishCondition": 1})); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ConfigList", models.TaskConfig, "Find", err)) + return nil, err + } + return out, nil +} + +func GetConfigList() ([]*TaskConfig, error) { + var out []*TaskConfig = make([]*TaskConfig, 0) + if err := coll(nil).Find(&out, bson.M{"status": true}, options.Find().SetSort(bson.M{"finishCondition": 1})); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ConfigList", models.TaskConfig, "Find", err)) + return nil, err + } + return out, nil +} + +func GetConfigByID(id primitive.ObjectID) (*TaskConfig, error) { + var out *TaskConfig = new(TaskConfig) + if err := coll(nil).FindOne(&out, bson.M{"_id": id, "status": true}); err != nil { + return nil, err + } + return out, nil +} + +func IsExistConfig(finishCondition int64, configType ConfigType) (bool, error) { + count, err := coll(nil).Count(bson.M{"finishCondition": finishCondition, "type": configType}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsExistConfig", models.TaskConfig, "Count", err), + log.Any("finishCondition", finishCondition), + log.Any("configType", configType), + ) + return false, err + } + return count > 0, nil +} + +func AddConfig(sc TaskConfig) error { + if _, err := coll(nil).InsertOne(&sc); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AddConfig", models.TaskConfig, "InsertOne", err), + log.Any("sc", sc), + ) + return err + } + return nil +} + +func ModifyConfig(filter, update primitive.M) error { + result, err := coll(nil).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ModifyConfig", models.TaskConfig, "UpdateOne", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + if result.ModifiedCount == 0 { + err = errors.New("prize UpdateOne ModifiedCount err") + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ModifyConfig", models.TaskConfig, "result.ModifiedCount == 0", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + return nil +} + +func QueryAllConfig(filter primitive.M, opts ...*options.FindOptions) ([]*TaskConfig, error) { + var out []*TaskConfig = []*TaskConfig{} + if err := coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllConfig", models.TaskConfig, "Find", err), + log.Any("filter", filter), + ) + return nil, err + } + return out, nil +} + +func CountConfig(filter primitive.M) (int64, error) { + count, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountConfig", models.TaskConfig, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } + return count, nil +} + +func QueryTaskConfigByCond(filter primitive.M) (*TaskConfig, error) { + var out *TaskConfig + if err := coll(nil).FindOne(&out, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryTaskConfigByCond", models.TaskConfig, "Find", err), + log.Any("filter", filter), + ) + return nil, err + } + return out, nil +} diff --git a/models/v/signtaskmod/type.go b/models/v/signtaskmod/type.go new file mode 100644 index 0000000..02ee53e --- /dev/null +++ b/models/v/signtaskmod/type.go @@ -0,0 +1,26 @@ +package signtaskmod + +import ( + "91porn-server/common/db" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +type ConfigType int + +type TaskConfig struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档ID + Title string `json:"title" bson:"title"` // 任务标题 + Img string `json:"img" bson:"img"` // 任务图片 + Desc string `json:"desc" bson:"desc"` // 任务描述 + FinishCondition int64 `json:"finishCondition" bson:"finishCondition"` // 达成条件 + Prizes []primitive.ObjectID `json:"prizes" bson:"prizes"` // 奖品列表 + ExtraPrizes []primitive.ObjectID `json:"extraPrizes" bson:"extraPrizes"` // 额外奖品列表 + Link string `json:"link" bson:"link"` // 跳转链接 + Status bool `json:"status" bson:"status"` // 启用/禁用 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 修改时间 + CreateTime time.Time `json:"createTime" bson:"createTime"` // 创建时间 +} diff --git a/models/v/signtaskmod/web.go b/models/v/signtaskmod/web.go new file mode 100644 index 0000000..150bcde --- /dev/null +++ b/models/v/signtaskmod/web.go @@ -0,0 +1,87 @@ +package signtaskmod + +import ( + "time" + + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type AddConfigCond struct { + Title string `json:"title"` // 任务标题 + Desc string `json:"desc"` // 任务描述 + FinishCondition int64 `json:"finishCondition"` // 达成条件 Todo:连续签到多少天 + Prizes []primitive.ObjectID `json:"prizes"` // 奖品列表 + ExtraPrizes []primitive.ObjectID `json:"extraPrizes"` // 额外奖品列表 + Img string `json:"img" bson:"img"` // 任务图片 + Link string `json:"link" bson:"link"` // 跳转链接 + Status bool `json:"status"` // 启用/禁用 +} + +type ModifyConfigCond struct { + ID primitive.ObjectID `json:"id"` // 任务id + Title *string `json:"title"` // 任务标题 + Desc *string `json:"desc"` // 任务描述 + FinishCondition *int64 `json:"finishCondition"` // 达成条件 Todo:连续签到多少天 + Prizes []primitive.ObjectID `json:"prizes"` // 奖品列表 + ExtraPrizes []primitive.ObjectID `json:"extraPrizes"` // 额外奖品列表 + Img *string `json:"img" bson:"img"` // 任务图片 + Link *string `json:"link" bson:"link"` // 跳转链接 + Status *bool `json:"status"` // 启用/禁用 +} + +func (m *ModifyConfigCond) Cond() primitive.M { + return bson.M{"_id": m.ID} +} + +func (m *ModifyConfigCond) Bson() primitive.M { + set := bson.M{"updateTime": time.Now()} + if m.FinishCondition != nil { + set["finishCondition"] = m.FinishCondition + } + if m.Title != nil { + set["title"] = m.Title + } + if m.Desc != nil { + set["desc"] = m.Desc + } + if m.FinishCondition != nil { + set["finishCondition"] = m.FinishCondition + } + if m.Prizes != nil { + set["prizes"] = m.Prizes + } + if m.ExtraPrizes != nil { + set["extraPrizes"] = m.ExtraPrizes + } + if m.Img != nil { + set["img"] = m.Img + } + if m.Link != nil { + set["link"] = m.Link + } + if m.Status != nil { + set["status"] = m.Status + } + return bson.M{"$set": set} +} + +type QueryAllConfigCond struct { + Status *bool `form:"status,omitempty"` // 启用/禁用 + commod.Page +} + +func (q *QueryAllConfigCond) Options() *options.FindOptions { + return options.Find().SetLimit(int64(q.PageSize)).SetSkip(int64((q.PageNumber - 1) * q.PageSize)).SetSort(bson.M{"finishCondition": 1}) +} + +func (q *QueryAllConfigCond) Query() primitive.M { + fliter := bson.M{} + if q.Status != nil { + fliter["status"] = q.Status + } + return fliter +} diff --git a/models/v/sourcemod/source.go b/models/v/sourcemod/source.go new file mode 100644 index 0000000..9e61335 --- /dev/null +++ b/models/v/sourcemod/source.go @@ -0,0 +1,312 @@ +package sourcemod + +import ( + "encoding/json" + "fmt" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/cachev2" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.Source + +const ( + pingListCacheKey = "source:ping-list:v1" + pingListCacheTTL = time.Minute +) + +type pingListCache struct { + Domains []string `json:"domains"` + Sources []*SourceRes `json:"sources"` +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "type", Value: 1}}, + Options: options.Index().SetUnique(true).SetSparse(true), + }, + { + Keys: bson.D{{Key: "domain", Value: 1}}, + }, + { + Keys: bson.D{{Key: "isActive", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// PingList 返回 +func PingList() (rd []string, rs []*SourceRes) { + // 域名配置体积较大且更新频率低,通过共享缓存避免每次启动请求都读取 MongoDB。 + result := pingListCache{} + _, err := cachev2.Classes(). + CacheTime(pingListCacheTTL). + Key(pingListCacheKey). + ResBind(&result). + Cache(loadPingList) + if err != nil { + log.Error("PingList cache error", log.E(err)) + result = loadPingList() + } + return result.Domains, result.Sources +} + +func loadPingList() pingListCache { + return buildPingList(List()) +} + +func buildPingList(data []*SourceRes) pingListCache { + if len(data) == 0 { + return pingListCache{} + } + dataLen := len(data) + d := make([]string, 0, dataLen) + s := make([]*SourceRes, 0, dataLen) + for _, v := range data { + switch v.Type { + case Ping: + for _, v := range v.Domain { + d = append(d, v.Url) + } + case Image, Audio, Common, Group, Telegram, Guide, PROXYRULE, FAQ, QRCODE, WELFARE, ACT, PromoteURL, AppStore, BusinessCooperation, ShareURL, PreSaleBgImg: + s = append(s, v) + case Vid: + s = append(s, v) + } + } + d = d[:len(d):len(d)] + s = s[:len(s):len(s)] + return pingListCache{Domains: d, Sources: s} +} + +func clearPingListCache() { + if _, err := cachev2.Classes().Delete(pingListCacheKey); err != nil { + log.Error("clear PingList cache error", log.E(err)) + } +} + +// GetCdnURL GetCdnURL +func GetCdnURL() (rd []Domain) { + data := List() + if len(data) == 0 { + return + } + var d []Domain + for _, v := range data { + switch v.Type { + case Common, Vid: + d = append(d, v.Domain...) + } + } + rd = d + return +} + +// List 返回 +func List() (array []*SourceRes) { + if err := coll(nil).Find(&array, bson.M{"isActive": true}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "List", table, "Find", err)) + } + return +} + +// List 返回 +func WebList() (array []*SourceRes) { + if err := coll(nil).Find(&array, bson.M{}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "WebList", table, "Find", err)) + } + return +} + +// GetCustomerStat 获取客服状态 +func GetCustomerStat() bool { + var s *SourceRes + if err := coll(nil).FindOne(&s, bson.M{"type": Customer}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCustomerStat", table, "FindOne", err)) + return true + } + if s == nil { + return true + } + return s.IsActive +} + +// 随机获取推广页域名 +func GetRandomPromotionURL() string { + if cnt, err := appg.Redis.SCard(redisconst.LandDomainCacheKey); cnt == 0 || err != nil { + purls := GetPromoteURLArray() + if len(purls) > 0 { + _, _ = appg.Redis.SAdd(redisconst.LandDomainCacheKey, purls) + } + } + purl, err := appg.Redis.SRandMember(redisconst.LandDomainCacheKey) + if err != nil { + return getPromoteURL() + } + return purl +} + +// 获取推广url数组 +func GetPromoteURLArray() []string { + data := GetPromoteURLList() + purls := make([]string, 0) + if data != nil { + purls = make([]string, 0, len(data.Domain)) + for _, v := range data.Domain { + purls = append(purls, v.Url) + } + } + return purls +} + +// GetPromoteURLList 返回 +func GetPromoteURLList() (data *SourceWebRes) { + if err := coll(nil).FindOne(&data, bson.M{"type": PromoteURL, "isActive": true}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetPromoteURLList", table, "FindOne", err)) + } + return +} + +// getPromoteURL 返回 +func getPromoteURL() string { + var s *SourceRes + if err := coll(nil).FindOne(&s, bson.M{"type": PromoteURL, "isActive": true}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getPromoteURL", table, "FindOne", err)) + } + if s != nil { + return s.Domain[0].Url + } + return "" +} + +// Insert SourceInsert +func Insert(s *Source) error { + s.UpdatedAt = time.Now() + s.CreatedAt = time.Now() + if _, err := coll(nil).InsertOne(&s); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return err + } + clearPingListCache() + return nil +} + +// Update 修改 +func Update(id primitive.ObjectID, sType SourceType, set *SourceEdit) error { + set.UpdatedAt = time.Now() + if _, err := coll(nil).UpdateOne(bson.M{"_id": id, "type": sType}, bson.M{"$set": set}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("id", id), + log.Any("sType", sType), + log.Any("set", set), + ) + return err + } + clearPingListCache() + return nil +} + +// EditDomain 修改 +func EditDomain(id primitive.ObjectID, edit *SourceEdit) error { + edit.UpdatedAt = time.Now() + update, _ := common.ToBsonM(edit) + if _, err := coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": update}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "EditDomain", table, "UpdateOne", err), + log.Any("id", id), + log.Any("edit", *edit), + ) + return err + } + clearPingListCache() + return nil +} + +// Remove SourceRemove +func Remove(ids []primitive.ObjectID) error { + if _, err := coll(nil).DeleteMany(bson.M{"_id": bson.M{"$in": ids}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Remove", table, "DeleteMany", err), + log.Any("ids", ids), + ) + return err + } + clearPingListCache() + return nil +} + +func GetWeightCdnURL() string { + var cho []common.Choice + data, err := appg.Redis.Get(redisconst.CdnCacheKey) + //不存在cdn缓存 + if err != nil || data == nil { + var choice []common.Choice + cdn := GetCdnURL() + for _, v := range cdn { + if v.Weight == 0 { + continue + } + choice = append(choice, common.Choice{Weight: int(v.Weight), Item: v.Url}) + } + cbyte, _ := json.Marshal(choice) + _ = appg.Redis.Set(redisconst.CdnCacheKey, cbyte, redisconst.CdnCacheExpire) + c, err := common.WeightedChoice(choice) + if err != nil { + if len(cdn) > 0 { + return cdn[0].Url + } + } + return c.Item.(string) + } + _ = json.Unmarshal([]byte(*data), &cho) + c, _ := common.WeightedChoice(cho) + return c.Item.(string) +} + +func GetVideoAndImgCdn() (videoCdn []string, imgCdn []string) { + data := List() + if len(data) == 0 { + return + } + dataLen := len(data) + videoCdn = make([]string, 0, dataLen) + imgCdn = make([]string, 0, dataLen) + for _, v := range data { + if v.Type == Vid { + for _, d := range v.Domain { + videoCdn = append(videoCdn, d.Url) + } + } + if v.Type == Image { + for _, d := range v.Domain { + imgCdn = append(imgCdn, d.Url) + } + } + } + return +} diff --git a/models/v/sourcemod/source_cache_test.go b/models/v/sourcemod/source_cache_test.go new file mode 100644 index 0000000..71b4682 --- /dev/null +++ b/models/v/sourcemod/source_cache_test.go @@ -0,0 +1,33 @@ +package sourcemod + +import ( + "testing" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestBuildPingList(t *testing.T) { + pingID := primitive.NewObjectID() + videoID := primitive.NewObjectID() + ignoredID := primitive.NewObjectID() + data := []*SourceRes{ + {ID: pingID, Type: Ping, IsActive: true, Domain: []Domain{{Url: "https://api-1.example"}, {Url: "https://api-2.example"}}}, + {ID: videoID, Type: Vid, IsActive: true, Domain: []Domain{{Url: "https://video.example"}}}, + {ID: ignoredID, Type: Customer, IsActive: true, Domain: []Domain{{Url: "https://customer.example"}}}, + } + + got := buildPingList(data) + if len(got.Domains) != 2 || got.Domains[0] != "https://api-1.example" || got.Domains[1] != "https://api-2.example" { + t.Fatalf("buildPingList() domains = %#v", got.Domains) + } + if len(got.Sources) != 1 || got.Sources[0].ID != videoID { + t.Fatalf("buildPingList() sources = %#v", got.Sources) + } +} + +func TestBuildPingListEmpty(t *testing.T) { + got := buildPingList(nil) + if len(got.Domains) != 0 || len(got.Sources) != 0 { + t.Fatalf("buildPingList(nil) = %#v", got) + } +} diff --git a/models/v/sourcemod/type.go b/models/v/sourcemod/type.go new file mode 100644 index 0000000..4aa212f --- /dev/null +++ b/models/v/sourcemod/type.go @@ -0,0 +1,101 @@ +package sourcemod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type SourceType string + +const ( + Ping SourceType = "API" //APP 服务域名 + Common SourceType = "COMMON" //CDN域名 COMMON:视频,图片共用 + Vid SourceType = "VID" //CDN域名 VID:视频专用 + Image SourceType = "IMAGE" //CDN域名 IMAGE:图片专用 + Audio SourceType = "AUDIO" //CDN域名 AUDIO:有声小说专用 + Group SourceType = "GROUP" //GROUP:官方群号地址(土豆) + Telegram SourceType = "TELEGRAM" //官方tg + PromoteURL SourceType = "LAND" //LAND: 落地页域名 + Guide SourceType = "GUIDE" //GUIDE: 导航域名 + Customer SourceType = "CUST" //CUST:客服 + Disc SourceType = "DISC" //DISC:商区 + PROXYRULE SourceType = "PROXYRULE" //PROXYRULE:代理规则 + FAQ SourceType = "FAQ" //FAQ:问题常见 + QRCODE SourceType = "QRCODE" //QRCODE:二维码链接 + WELFARE SourceType = "WELFARE" //WELFARE:福利页面连接 + ACT SourceType = "ACT" //ACT:活动连接 + AppStore SourceType = "AppStore" //应用商店地址 + BusinessCooperation SourceType = "BusinessCooperation" //商务合作 + ShareURL SourceType = "SHARE" //SHARE 分享页域名 + PreSaleBgImg SourceType = "preSaleBgImg" //预售卡动态图 +) + +type Status int64 + +const WxBlock Status = 1 //被微信封禁 + +// Source 资源域名配置 +type Source struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Domain []Domain `json:"domain" bson:"domain" binding:"required"` + Type SourceType `json:"type" bson:"type"` + IsActive bool `json:"isActive" bson:"isActive"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +// TODO 此处需要后期删除 +// SourceRes 返回原生版本资源域名配置 +type SourcePreviousRes struct { + ID primitive.ObjectID `json:"id" bson:"_id" swaggerignore:"true"` + //域名 + Domain string `json:"domain" bson:"domain"` + //是否激活 + IsActive bool `json:"isActive" bson:"isActive"` + //资源类型 COMMON:视频,图片共用 VID:视频URL IMAGE:图片URL LAND:落地页域名 + Type SourceType `json:"type" bson:"type" swaggertype:"integer"` +} + +// SourceRes 返回资源域名配置 +type SourceRes struct { + ID primitive.ObjectID `json:"id" bson:"_id" swaggerignore:"true"` + //域名 + Domain []Domain `json:"domain" bson:"domain"` + //是否激活 + IsActive bool `json:"isActive" bson:"isActive"` + //资源类型 COMMON:视频,图片共用 VID:视频URL IMAGE:图片URL LAND:落地页域名 AppStore:商店地址 + Type SourceType `json:"type" bson:"type" swaggertype:"integer"` +} + +type Domain struct { + //权重 + Weight int64 `json:"weight" bson:"weight,omitempty"` + //地址 + Url string `json:"url" bson:"url"` + //备注 + Desc string `json:"desc" bson:"desc"` + //默认正常状态为0 1:表示该url被微信封禁 + Status int64 `json:"status" bson:"status"` +} + +// SourceWebRes 返回资源域名配置 +type SourceWebRes struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Domain []Domain `json:"domain" bson:"domain"` + IsActive bool `json:"isActive" bson:"isActive"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} +type SourceEdit struct { + Domain []Domain `json:"domain" bson:"domain,omitempty"` //网址 + IsActive bool `json:"isActive" bson:"isActive"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/sysconfmod/free_trial_config_test.go b/models/v/sysconfmod/free_trial_config_test.go new file mode 100644 index 0000000..10c0119 --- /dev/null +++ b/models/v/sysconfmod/free_trial_config_test.go @@ -0,0 +1,16 @@ +package sysconfmod + +import "testing" + +func TestFreeTrialBadgeConfigDefaultEnabled(t *testing.T) { + for _, item := range initData { + if item.VCode != string(VCodeFreeTrialBadgeEnabled) { + continue + } + if item.Type != CfgTypeBool || item.Value != "true" { + t.Fatalf("unexpected free trial badge config: type=%s value=%s", item.Type, item.Value) + } + return + } + t.Fatal("freeTrialBadgeEnabled config is not initialized") +} diff --git a/models/v/sysconfmod/payment_guide_config_test.go b/models/v/sysconfmod/payment_guide_config_test.go new file mode 100644 index 0000000..285fed4 --- /dev/null +++ b/models/v/sysconfmod/payment_guide_config_test.go @@ -0,0 +1,22 @@ +package sysconfmod + +import "testing" + +func TestPaymentGuideConfigDefaultsEnabled(t *testing.T) { + found := make(map[VCode]bool) + for _, item := range initData { + code := VCode(item.VCode) + if code != VCodePaymentGuideEnabled && code != VCodePaymentGuideHomeEnabled { + continue + } + if item.GpCode != string(GPCodePopup) || item.Type != CfgTypeBool || item.Value != "true" { + t.Fatalf("unexpected payment guide config %s: group=%s type=%s value=%s", code, item.GpCode, item.Type, item.Value) + } + found[code] = true + } + for _, code := range []VCode{VCodePaymentGuideEnabled, VCodePaymentGuideHomeEnabled} { + if !found[code] { + t.Fatalf("%s config is not initialized", code) + } + } +} diff --git a/models/v/sysconfmod/short_drama_card_test.go b/models/v/sysconfmod/short_drama_card_test.go new file mode 100644 index 0000000..4eee74e --- /dev/null +++ b/models/v/sysconfmod/short_drama_card_test.go @@ -0,0 +1,16 @@ +package sysconfmod + +import "testing" + +func TestShortDramaCardConfigIsInitializedAsSelect(t *testing.T) { + for _, item := range initData { + if item.VCode != string(VCodeShortDramaCardID) { + continue + } + if item.Type != CfgTypeSelect { + t.Fatalf("short drama card config type = %q, want %q", item.Type, CfgTypeSelect) + } + return + } + t.Fatal("short drama card config is missing from initData") +} diff --git a/models/v/sysconfmod/sysconf.go b/models/v/sysconfmod/sysconf.go new file mode 100755 index 0000000..2b3ae74 --- /dev/null +++ b/models/v/sysconfmod/sysconf.go @@ -0,0 +1,178 @@ +package sysconfmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "encoding/json" + "errors" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// GetAllConfig 获取全部配置 +func GetAllConfig() (res ConfMap, err error) { + var data []SysConf + if err = coll(nil).Find(&data, bson.M{}, options.Find().SetSort(bson.D{{Key: "_id", Value: -1}})); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByGCode", table, "Find", err)) + return res, err + } + if len(data) == 0 { + return res, errors.New("record not found") + } + + res = make(ConfMap, len(data)) + for _, v := range data { + res[v.VCode] = v.Value + } + + return +} + +// BatchGetInfoByVCode 按 vCode 列表批量获取配置项 +func BatchGetInfoByVCode(vCodes []VCode) (res []SysConf, err error) { + if err = coll(nil).Find(&res, bson.M{"vCode": bson.M{"$in": vCodes}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "BatchGetInfoByVCode", table, "Find", err), + log.Any("vCodes", vCodes), + ) + return nil, err + } + + if len(res) == 0 { + return nil, errors.New("record not found") + } + + return +} + +// GetByVCode 获取单个配置项。 +func GetByVCode(code VCode) (*SysConf, error) { + var out SysConf + if err := coll(nil).FindOne(&out, bson.M{"vCode": string(code)}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByVCode", table, "FindOne", err), + log.Any("vCode", code), + ) + return nil, err + } + if out.ID.IsZero() { + return nil, errors.New("record not found") + } + return &out, nil +} + +// GetByGpCode 获取分组配置 +func GetByGpCode(code GPCode) (res ConfMap, err error) { + var data []SysConf + if err = coll(nil).Find(&data, bson.M{"gpCode": code}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByGCode", table, "Find", err), + log.Any("gpCode", code), + ) + return nil, err + } + if len(data) == 0 { + return nil, errors.New("record not found") + } + + res = make(ConfMap, len(data)) + for _, v := range data { + res[v.VCode] = v.Value + } + + return +} + +// GetAll 查询全部文档 +func GetAll(filter primitive.M, sort bson.D) (out []*SysConf, err error) { + if len(sort) == 0 { + sort = bson.D{{Key: "_id", Value: -1}} + } + opts := options.Find().SetSort(sort) + + // 检查 filter 是否为空 + if filter == nil { + filter = primitive.M{} + } + // 使用切片初始化 out + out = make([]*SysConf, 0) + if err = coll(nil).Find(&out, filter, opts); err != nil { + log.Error("[METHOD-GetAll] Model "+table+" Find fail error:"+err.Error(), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// GetInfo 通过id获取详细信息 +func GetInfo(id primitive.ObjectID) (SysConf, error) { + v := SysConf{} + if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err), + log.Any("id", id), + ) + return v, err + } + + if v.ID.IsZero() { + return v, errors.New("record not found") + } + + return v, nil +} + +// Insert 插入记录 +func Insert(t *db.MongoTool, d SysConf) (data primitive.ObjectID, err error) { + result, err := coll(t).InsertOne(&d) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err)) + return + } + return +} + +// UpdateByID 根据id更新数据 +func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) { + cond := bson.M{"_id": id} + return update(t, cond, data) +} + +// update 更新数据 +func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) { + result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err), + log.Any("cond", cond), + log.Any("update", data), + ) + return 0, err + } + + return result.ModifiedCount, nil +} + +// DeleteByID 删除数据 +func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error { + _, err := coll(t).DeleteOne(bson.M{"_id": id}) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil + } else { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteOne", err), log.Any("id", id)) + } + } + + return err +} diff --git a/models/v/sysconfmod/trans.go b/models/v/sysconfmod/trans.go new file mode 100644 index 0000000..0fb80ae --- /dev/null +++ b/models/v/sysconfmod/trans.go @@ -0,0 +1,82 @@ +package sysconfmod + +import ( + "91porn-server/common/log" + "encoding/json" + "strconv" +) + +type ConfMap map[string]string + +func (c ConfMap) getValue(code VCode) string { + obj, ok := c[string(code)] + if !ok { + log.Error("配置项 " + string(code) + " 未找到配置值!") + return "" + } + + return obj +} + +// GetStrSlice 获取字符串数组 +func (c ConfMap) GetStrSlice(code VCode) []string { + var res []string + v := c.getValue(code) + err := json.Unmarshal([]byte(v), &res) + if err != nil { + log.Error("配置项 " + string(code) + " 不是有效的数组值!") + } + + return res +} + +// GetObject 对象类型配置 +func (c ConfMap) GetObject(code VCode) map[string]string { + res := make(map[string]string) + v := c.getValue(code) + + err := json.Unmarshal([]byte(v), &res) + if err != nil { + log.Error("配置项 " + string(code) + " 不是有效的对象值!") + } + + return res +} + +// GetString 字符串配置项 +func (c ConfMap) GetString(code VCode) string { + return c.getValue(code) +} + +// GetBool 布尔配置项 +func (c ConfMap) GetBool(code VCode) bool { + v := c.getValue(code) + res, err := strconv.ParseBool(v) + if err != nil { + res = false + } + + return res +} + +// GetInt 数字型配置项 +func (c ConfMap) GetInt(code VCode) int64 { + v := c.getValue(code) + res, err := strconv.ParseInt(v, 10, 64) + if err != nil { + res = 0 + } + + return res +} + +// GetFloat 浮点型配置项 +func (c ConfMap) GetFloat(code VCode) float64 { + v := c.getValue(code) + res, err := strconv.ParseFloat(v, 64) + if err != nil { + res = 0.00 + } + + return res +} diff --git a/models/v/sysconfmod/type.go b/models/v/sysconfmod/type.go new file mode 100755 index 0000000..c1b5c05 --- /dev/null +++ b/models/v/sysconfmod/type.go @@ -0,0 +1,121 @@ +package sysconfmod + +import ( + "fmt" + "time" + + "go.mongodb.org/mongo-driver/mongo/options" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.SysConf + +const ( + CfgTypeText = "text" + CfgTypeString = "string" + CfgTypeImg = "img" + CfgTypeObject = "object" + CfgTypeInt = "int" + CfgTypeFloat = "float" + CfgTypeBool = "bool" + CfgTypeTextArr = "text-array" + CfgTypeStrArr = "string-array" + CfgTypeSelect = "select" +) + +type SysConf struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + GroupName string `json:"groupName" bson:"groupName"` // 分组名 + GpCode string `json:"gpCode" bson:"gpCode"` // 分组编码 + VCode string `json:"vCode" bson:"vCode"` // 变量名 + Title string `json:"title" bson:"title"` // 变量标题 + Tip string `json:"tip" bson:"tip"` // 变量描述 + Type string `json:"type" bson:"type"` // 类型:text,string,img,int,bool,object,text-array,string-array + Value string `json:"value" bson:"value"` // 变量值 + SelectValues []SelectItem `json:"selectValues" bson:"selectValues"` // 下拉框筛选项 + IsRequired bool `json:"is_required" bson:"is_required"` // 是否必填 + SortOrder int `json:"sort_order" bson:"sort_order"` // 排序值 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 +} + +type SelectItem struct { + Key string `json:"key" bson:"key"` + Value string `json:"value" bson:"value"` +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Init 初始化索引 +func Init() { + mdb = db.Init(table) + initIndex() + initConf() +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "gpCode", Value: 1}}, + }, + { + Keys: bson.D{{Key: "vCode", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// initConf 初始化配置数据 +func initConf() { + for _, d := range initData { + // 不存在则整条插入 + if !isExists(d.VCode) { + d.CreatedAt = time.Now() + d.UpdatedAt = time.Now() + _, _ = Insert(nil, d) + continue + } + + // 已存在则仅同步「描述类」元数据(分组/标题/描述/类型/是否必填/排序, + // 运营后台无法编辑这些字段,以代码为准),保留运营维护的 value 与 selectValues。 + _, _ = update(nil, bson.M{"vCode": d.VCode}, map[string]interface{}{ + "groupName": d.GroupName, + "gpCode": d.GpCode, + "title": d.Title, + "tip": d.Tip, + "type": d.Type, + "is_required": d.IsRequired, + "sort_order": d.SortOrder, + "updatedAt": time.Now(), + }) + } +} + +// EnsureInitData 暴露初始化配置入口,供启动后首次访问时补齐缺失配置。 +func EnsureInitData() { + initConf() +} + +func isExists(code string) bool { + count, err := coll(nil).Count(bson.M{"vCode": code}) + if err != nil { + return false + } + return count > 0 +} diff --git a/models/v/sysconfmod/var.go b/models/v/sysconfmod/var.go new file mode 100755 index 0000000..382a1c5 --- /dev/null +++ b/models/v/sysconfmod/var.go @@ -0,0 +1,1395 @@ +package sysconfmod + +type GPCode string + +const ( + GPCodeCommon GPCode = "common" // 通用配置 + GPCodeAdvance GPCode = "advance" // 预售卡相关配置 + GPCodeAdver GPCode = "adver" // 广告相关配置 + GPCodeIM GPCode = "im" // IM 配置 + GPCodePopup GPCode = "popup" // 弹窗配置 + GPCodePay GPCode = "pay" // 支付方式配置 +) + +func (c GPCode) GetName() string { + switch c { + case GPCodeCommon: + return "通用配置" + case GPCodeAdver: + return "广告相关配置" + case GPCodeAdvance: + return "预售卡页面配置" + case GPCodeIM: + return "IM配置" + case GPCodePopup: + return "弹窗配置" + case GPCodePay: + return "支付方式配置" + } + + return "默认配置" +} + +type VCode string + +const ( + VCodeAiBubble VCode = "aiBubble" // AI女友气泡内容 + VCodeAiCharacterImg VCode = "aiCharacterImg" // AI卡通人物 + VCodeBroadcast VCode = "broadcast" // 直播开关 + VCodeStoreOpen VCode = "storeOpen" // 商城开关 + VCodeLotteryEnable VCode = "lotteryEnable" // 是否开启抽奖 + VCodeLotteryIcon VCode = "lotteryIcon" // 抽奖浮标 + VCodeLotteryUrl VCode = "lotteryIconUrl" // 抽奖地址 + VCodeHotSearchTerms VCode = "hotSearchTerms" // 热搜词 + VCodeSearchHintWord VCode = "searchHintWord" // 搜索输入框提示词 + VCodeFestivalUi VCode = "festivalUi" // 当前节日ui + VCodeHotRankingTags VCode = "hotRankingTags" // 热点榜单关联标签 + SendMsgPrice VCode = "sendMsgPrice" // 发送金币消耗金币 + + VCodeAiImageToVideoPrice VCode = "aiImageToVideoPrice" // AI图生视频金币 + VCodeAiTextToImagePrice VCode = "aiTextToImagePrice" // AI绘图金币 + VCodeAiUndressPrice VCode = "aiUndressPrice" // AI脱衣价格 + VCodeAiGirlFriend VCode = "aiGirlFriend" // ai女友 // 弃用 + VCodeAiUndress VCode = "aiUndress" // ai脱衣 // 弃用 + VCodeAiImageChangeFace VCode = "aiImageChangeFace" // ai图片换脸 // 弃用 + VCodeAiVideoChangeFace VCode = "aiVideoChangeFace" // ai视频换脸 // 弃用 + VCodeAiTextToNovelPrice VCode = "aiTextToNovelPrice" // ai小说价格 + + VCodeQMDL VCode = "qmdl" // 全民代理官网链接 + VCodeDarkWebVipName VCode = "darkWebVipName" // 暗网会员卡名 + VCodeDarkWebVipId VCode = "darkWebVipId" // 暗网会员卡ID + VCodePrivateZoneVipName VCode = "privateZoneVipName" // 私密圈推荐会员卡名 + VCodePrivateZoneVipId VCode = "privateZoneVipId" // 私密圈推荐会员卡ID + VCodeRecommendVipId VCode = "recommendVipId" // 特邀会员 + VCodeNewbieSaleTime VCode = "newbieSaleTime" // 新人特惠销售时间 单位 小时 + VCodePersonalCenterBackground VCode = "personalCenterBackground" // 个人中心背景图 + VCodeTotalWatchCount VCode = "totalWatchCount" // 非VIP用户免费观看总次数 + VCodeVideo1 VCode = "video1" // 创作技巧视频1 + VCodeVideo2 VCode = "video2" // 创作技巧视频2 + VCodeReturnSaleVipIds VCode = "returnSaleVipIds" // 返场售卖会员卡id + VCodeOldReturnSaleTime VCode = "oldReturnSaleTime" // 返场促销时间 + VCodeFreeMark VCode = "freeMark" // 免费角标是否展示 + VCodeFreeTrialBadgeEnabled VCode = "freeTrialBadgeEnabled" // VIP视频免费观看试看角标开关 + VCodeVipMark VCode = "vipMark" // vip角标是否展示 + VCodeCoinMark VCode = "coinMark" // 金币角标是否展示 + VCodeAiSort VCode = "aiSort" // 前端AI布局排序 + VCodeAiSwitch VCode = "aiSwitch" // 前端AI开关 + VCodeSignIcon VCode = "signIcon" // 签到图标 + VCodeDarkWebEnable VCode = "darkWebEnable" // 暗网是否开启 + VCodeDarkWebImg VCode = "darkWebImg" // 暗网图片 + VCodeDarkWebIcon VCode = "darkWebIcon" // 暗网图标 + VCodeDarkWebIconName VCode = "darkWebIconName" // 暗网图标名称 + VCodePaymentGuideEnabled VCode = "paymentGuideEnabled" // 付费引导全局开关 + VCodePaymentGuideHomeEnabled VCode = "paymentGuideHomeEnabled" // 首页付费引导共用开关 + VCodeShortDramaCardID VCode = "shortDramaCardId" // 默认短剧卡ID + VCodeShortDramaEntryPopup VCode = "shortDramaEntryPopupEnabled" // 短剧首次进入悬浮窗总开关 + VCodeDefaultEntryPage VCode = "defaultEntryPage" // 默认进入页面 + VCodeDefaultEntryAudience VCode = "defaultEntryAudience" // 默认进入页面生效人群 + VCodeDramaTopicHotSort VCode = "shortDramaTopicHotSort" // 短剧系统专题-热门推荐排序 + VCodeDramaTopicLatestSort VCode = "shortDramaTopicLatestSort" // 短剧系统专题-最新上架排序 + VCodeDramaTopicEveryoneSort VCode = "shortDramaTopicEveryoneSort" // 短剧系统专题-大家爱看排序 + + // 预售卡相关 + VCodeAdvancePageBackground VCode = "advancePageBackground" // 大背景 + VCodeAdvancePageVidBackground VCode = "advancePageVidBackground" // 金币视频区背景 + VCodeAdvanceButtonBackground VCode = "advanceButtonBackground" // 支付按钮 立即预定 + VCodeAdvanceButtonWaitBackground VCode = "advanceButtonWaitBackground" // 支付按钮 已预定,未到支付时间 + VCodeAdvanceButtonProcBackground VCode = "advanceButtonProcBackground" // 支付按钮 已预定,去支付尾款 + VCodeAdvanceEnterBgWait VCode = "advanceEnterBgWait" // 入口按钮背景(未付款) + VCodeAdvanceEnterBgProc VCode = "advanceEnterBgProc" // 入口按钮背景(付尾款) + VCodeAdvancePopBgWait VCode = "advancePopBgWait" // 弹窗背景(未预定) + VCodeAdvancePopBgProc VCode = "advancePopBgProc" // 弹窗背景(已预定) + VCodeAdvanceBanner VCode = "advanceBanner" // 立即预定 + VCodeAdvanceBannerWait VCode = "advanceBannerWait" // 已预定,未到支付时间 + VCodeAdvanceBannerProc VCode = "advanceBannerProc" // 已预定,去支付尾款 + + // 广告ab测展示类型 + VCodeAdverAbTestShowType VCode = "adverAbTestShowType" // 广告ab测展示类型 + VCodeAdverAbTestSwitch VCode = "adverAbTestSwitch" // 广告ab测开关 + + // 新人免广告 + VCodeNewUserAdFreeSwitch VCode = "newUserAdFreeSwitch" // 新人广告开关(新人免广告总开关) + VCodeNewUserAdFreeHours VCode = "newUserAdFreeHours" // 新人免广告时限(小时) + VCodeNewUserAdFreePositions VCode = "newUserAdFreePositions" // 新人免广告-广告位列表 + + // IM 在线广告透传相关配置 + VCodeIMAdEnable VCode = "imAdEnable" // IM 在线广告透传开关 + VCodeIMAdIntervalMinutes VCode = "imAdIntervalMinutes" // 推送间隔(分钟) + VCodeIMAdMaxUsers VCode = "imAdMaxUsers" // 单次最多扫描用户数 + // 各广告位独立开关 + VCodeIMAdPosChatNotificationBar VCode = "imAdPosChatNotificationBar" // 会话页顶部通知条 + VCodeIMAdPosChatFloatingGifBall VCode = "imAdPosChatFloatingGifBall" // 会话页悬浮GIF球 + VCodeIMAdPosListFeedNative VCode = "imAdPosListFeedNative" // 消息列表内原生广告位 + VCodeIMAdPosListBanner VCode = "imAdPosListBanner" // 列表页顶部轮播 + + // 保留 JHS 已使用的 vCode,便于复用后台配置和运维口径。 + VCodeH265MaxRunning VCode = "hevcMaxRunning" // H265 云转码最大并发任务数 +) + +// 支付方式展示配置 +const ( + VCodePayTypeName VCode = "payTypeName" // 支付方式显示名称(type -> 展示名称) + VCodePayTypeSort VCode = "payTypeSort" // 支付方式展示顺序(type 数组,越靠前越先展示) +) + +// 用户分层弹窗 VCode +const ( + VCodeUserPaymentStatusPopupNewUserPopup VCode = "newUserPopup" // 用户分层弹窗-新用户-弹窗 + VCodeUserPaymentStatusPopupNewUserFloating VCode = "newUserFloating" // 用户分层弹窗-新用户-浮窗 + VCodeUserPaymentStatusPopupNewUserPlayPage VCode = "newUserPlayPage" // 用户分层弹窗-新用户-播放页 + VCodeUserPaymentStatusPopupNewUserMeTab VCode = "newUserMeTab" // 用户分层弹窗-新用户-我的TAB + VCodeUserPaymentStatusPopupNewUserVipCardID VCode = "newUserVipCardID" // 用户分层弹窗-新用户-会员卡ID + VCodeUserPaymentStatusPopupNewUserCountdown VCode = "newUserCountdown" // 用户分层弹窗-新用户-倒计时开关 + + VCodeUserPaymentStatusPopupUnpayUnder7dayPopup VCode = "unpayUnder7dayPopup" // 用户分层弹窗-7天内未付款-弹窗 + VCodeUserPaymentStatusPopupUnpayUnder7dayFloating VCode = "unpayUnder7dayFloating" // 用户分层弹窗-7天内未付款-浮窗 + VCodeUserPaymentStatusPopupUnpayUnder7dayPlayPage VCode = "unpayUnder7dayPlayPage" // 用户分层弹窗-7天内未付款-播放页 + VCodeUserPaymentStatusPopupUnpayUnder7dayMeTab VCode = "unpayUnder7dayMeTab" // 用户分层弹窗-7天内未付款-我的TAB + VCodeUserPaymentStatusPopupUnpayUnder7dayVipCardID VCode = "unpayUnder7dayVipCardID" // 用户分层弹窗-7天内未付款-会员卡ID + VCodeUserPaymentStatusPopupUnpayUnder7dayCountdown VCode = "unpayUnder7dayCountdown" // 用户分层弹窗-7天内未付款-倒计时开关 + + VCodeUserPaymentStatusPopupUnpayUnder7dayPopupNoCountdown VCode = "unpayUnder7dayPopupNoCountdown" // 用户分层弹窗-7天内未付款-弹窗-无倒计时 + VCodeUserPaymentStatusPopupUnpayUnder7dayFloatingNoCountdown VCode = "unpayUnder7dayFloatingNoCountdown" // 用户分层弹窗-7天内未付款-浮窗-无倒计时 + VCodeUserPaymentStatusPopupUnpayUnder7dayPlayPageNoCountdown VCode = "unpayUnder7dayPlayPageNoCountdown" // 用户分层弹窗-7天内未付款-播放页-无倒计时 + VCodeUserPaymentStatusPopupUnpayUnder7dayMeTabNoCountdown VCode = "unpayUnder7dayMeTabNoCountdown" // 用户分层弹窗-7天内未付款-我的TAB-无倒计时 + + VCodeUserPaymentStatusPopupUnpayOver7dayPopup VCode = "unpayOver7dayPopup" // 用户分层弹窗-7天后未付款-弹窗 + VCodeUserPaymentStatusPopupUnpayOver7dayFloating VCode = "unpayOver7dayFloating" // 用户分层弹窗-7天后未付款-浮窗 + VCodeUserPaymentStatusPopupUnpayOver7dayPlayPage VCode = "unpayOver7dayPlayPage" // 用户分层弹窗-7天后未付款-播放页 + VCodeUserPaymentStatusPopupUnpayOver7dayMeTab VCode = "unpayOver7dayMeTab" // 用户分层弹窗-7天后未付款-我的TAB + VCodeUserPaymentStatusPopupUnpayOver7dayVipCardID VCode = "unpayOver7dayVipCardID" // 用户分层弹窗-7天后未付款-会员卡ID + VCodeUserPaymentStatusPopupUnpayOver7dayCountdown VCode = "unpayOver7dayCountdown" // 用户分层弹窗-7天后未付款-倒计时开关 + + VCodeUserPaymentStatusPopupPayNeedUpgradePopup VCode = "payNeedUpgradePopup" // 用户分层弹窗-已付款需要续费升级-弹窗 + VCodeUserPaymentStatusPopupPayNeedUpgradeFloating VCode = "payNeedUpgradeFloating" // 用户分层弹窗-已付款需要续费升级-浮窗 + VCodeUserPaymentStatusPopupPayNeedUpgradePlayPage VCode = "payNeedUpgradePlayPage" // 用户分层弹窗-已付款需要续费升级-播放页 + VCodeUserPaymentStatusPopupPayNeedUpgradeMeTab VCode = "payNeedUpgradeMeTab" // 用户分层弹窗-已付款需要续费升级-我的TAB + VCodeUserPaymentStatusPopupPayNeedUpgradeVipCardID VCode = "payNeedUpgradeVipCardID" // 用户分层弹窗-已付款需要续费升级-会员卡ID + VCodeUserPaymentStatusPopupPayNeedUpgradeCountdown VCode = "payNeedUpgradeCountdown" // 用户分层弹窗-已付款需要续费升级-倒计时开关 + + VCodeUserPaymentStatusPopupMaxVIPLevel VCode = "maxVIPLevel" // 用户分层弹窗-VIP最大等级 +) + +var initData = []SysConf{ + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeDramaTopicHotSort), + Title: "短剧系统专题-热门推荐排序", + Tip: "由短剧专题管理接口维护", + Type: CfgTypeInt, + Value: "500", + IsRequired: true, + SortOrder: 703, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeDramaTopicLatestSort), + Title: "短剧系统专题-最新上架排序", + Tip: "由短剧专题管理接口维护", + Type: CfgTypeInt, + Value: "400", + IsRequired: true, + SortOrder: 702, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeDramaTopicEveryoneSort), + Title: "短剧系统专题-大家爱看排序", + Tip: "由短剧专题管理接口维护", + Type: CfgTypeInt, + Value: "300", + IsRequired: true, + SortOrder: 701, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeH265MaxRunning), + Title: "H265最大正在转码数", + Tip: "H265云转码中视频数量上限", + Type: CfgTypeInt, + Value: "400", + IsRequired: true, + SortOrder: 800, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeAiBubble), + Title: "AI女友气泡弹窗", + Tip: "可以配置多段文本,弹窗将随机展示其中一条", + Type: CfgTypeTextArr, + Value: "[]", + IsRequired: true, + SortOrder: 1, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeAiCharacterImg), + Title: "AI卡通人物", + Tip: "AI卡通人物形象图(77*70)", + Type: CfgTypeImg, + Value: "", + IsRequired: true, + SortOrder: 2, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeBroadcast), + Title: "直播开关", + Tip: "直播开关", + Type: CfgTypeBool, + Value: "0", + IsRequired: false, + SortOrder: 950, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeStoreOpen), + Title: "商城是否开启", + Tip: "控制原味商城按钮展示", + Type: CfgTypeBool, + Value: "0", + IsRequired: true, + SortOrder: 1, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(SendMsgPrice), + Title: "私信价格", + Tip: "私信价格", + Type: CfgTypeInt, + Value: "100", + IsRequired: true, + SortOrder: 999, + }, + { + GroupName: GPCodeAdvance.GetName(), + GpCode: string(GPCodeAdvance), + VCode: string(VCodeAdvancePageBackground), + Title: "大背景", + Tip: "预售页大背景", + Type: CfgTypeImg, + Value: "cf230705/image/1y6/287/2w/1ga/c7855baca5203ca28c51d1bcdefc3d57.webp", + IsRequired: true, + SortOrder: 50, + }, + { + GroupName: GPCodeAdvance.GetName(), + GpCode: string(GPCodeAdvance), + VCode: string(VCodeAdvancePageVidBackground), + Title: "金币视频区背景", + Tip: "金币视频区背景", + Type: CfgTypeImg, + Value: "cf230705/image/2v1/2wg/296/2ru/54be0989f5668e4272d9c349c89f8817.webp", + IsRequired: true, + SortOrder: 49, + }, + { + GroupName: GPCodeAdvance.GetName(), + GpCode: string(GPCodeAdvance), + VCode: string(VCodeAdvanceButtonBackground), + Title: "支付按钮-立即预定", + Tip: "支付按钮-立即预定", + Type: CfgTypeImg, + Value: "cf230705/image/2y0/1wr/239/oc/b8d892d767db012dc92a10a0985fb7fb.png", + IsRequired: true, + SortOrder: 40, + }, + { + GroupName: GPCodeAdvance.GetName(), + GpCode: string(GPCodeAdvance), + VCode: string(VCodeAdvanceButtonWaitBackground), + Title: "支付按钮-已预定", + Tip: "支付按钮 已预定,未到支付时间", + Type: CfgTypeImg, + Value: "cf230705/image/1be/1mp/1y6/2h4/c04a2dbd7d91f2706192fbb48c6714f0.png", + IsRequired: true, + SortOrder: 39, + }, + { + GroupName: GPCodeAdvance.GetName(), + GpCode: string(GPCodeAdvance), + VCode: string(VCodeAdvanceButtonProcBackground), + Title: "支付按钮-付尾款", + Tip: "支付按钮 已预定,去支付尾款", + Type: CfgTypeImg, + Value: "cf230705/image/1wi/k6/26z/2gp/fe8f778f7dd1ea9dc5d1629589f78793.png", + IsRequired: true, + SortOrder: 38, + }, + { + GroupName: GPCodeAdvance.GetName(), + GpCode: string(GPCodeAdvance), + VCode: string(VCodeAdvanceEnterBgWait), + Title: "入口按钮背景(未付款)", + Tip: "入口按钮背景(未付款)", + Type: CfgTypeImg, + Value: "cf230705/image/75/2bj/1wf/2q8/7f5d79d57d4692b485df32b0a474c7c0.png", + IsRequired: true, + SortOrder: 35, + }, + { + GroupName: GPCodeAdvance.GetName(), + GpCode: string(GPCodeAdvance), + VCode: string(VCodeAdvanceEnterBgProc), + Title: "入口按钮背景(付尾款)", + Tip: "入口按钮背景(付尾款)", + Type: CfgTypeImg, + Value: "cf230705/image/2hl/1cg/1wi/11u/1e47fd8c40af3229549325222fa77d9e.png", + IsRequired: true, + SortOrder: 34, + }, + { + GroupName: GPCodeAdvance.GetName(), + GpCode: string(GPCodeAdvance), + VCode: string(VCodeAdvancePopBgWait), + Title: "弹窗背景(未预定)", + Tip: "弹窗背景(未预定)", + Type: CfgTypeImg, + Value: "cf230705/image/26r/qt/2sj/19l/c2268a97e6540155df499378b11c27fa.webp", + IsRequired: true, + SortOrder: 30, + }, + { + GroupName: GPCodeAdvance.GetName(), + GpCode: string(GPCodeAdvance), + VCode: string(VCodeAdvancePopBgProc), + Title: "弹窗背景(已预定)", + Tip: "弹窗背景(已预定)", + Type: CfgTypeImg, + Value: "cf230705/image/2zc/27y/oy/lc/d6ce9717f914375f18a375157172c4ba.webp", + IsRequired: true, + SortOrder: 29, + }, + { + GroupName: GPCodeAdvance.GetName(), + GpCode: string(GPCodeAdvance), + VCode: string(VCodeAdvanceBanner), + Title: "BANNER-立即预定", + Tip: "BANNER-立即预定", + Type: CfgTypeImg, + Value: "cf230705/image/27n/31/2a7/27d/92a152ab1ac78d88538d413978668816.webp", + IsRequired: true, + SortOrder: 20, + }, + { + GroupName: GPCodeAdvance.GetName(), + GpCode: string(GPCodeAdvance), + VCode: string(VCodeAdvanceBannerWait), + Title: "BANNER-已预定", + Tip: "已预定,未到支付时间", + Type: CfgTypeImg, + Value: "cf230705/image/2a1/hn/21t/2xq/20f4306afa2100c6214e2fc1f3c05c94.webp", + IsRequired: true, + SortOrder: 19, + }, + { + GroupName: GPCodeAdvance.GetName(), + GpCode: string(GPCodeAdvance), + VCode: string(VCodeAdvanceBannerProc), + Title: "BANNER-付尾款", + Tip: "已预定,去支付尾款", + Type: CfgTypeImg, + Value: "cf230705/image/19q/1d7/2f9/1bo/d5571d82840372cad43289b95c03abd3.webp", + IsRequired: true, + SortOrder: 18, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeHotSearchTerms), + Title: "热搜词", + Tip: "热搜词", + Type: CfgTypeStrArr, + Value: "[]", + IsRequired: false, + SortOrder: 898, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeSearchHintWord), + Title: "搜索提示词", + Tip: "搜索提示词", + Type: CfgTypeStrArr, + Value: "[]", + IsRequired: false, + SortOrder: 898, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeFestivalUi), + Title: "当前节日ui", + Tip: "当前节日ui", + Type: CfgTypeSelect, + Value: "默认", + SelectValues: []SelectItem{ + SelectItem{Key: "默认", Value: "默认"}, + SelectItem{Key: "春节", Value: "春节"}, + SelectItem{Key: "清明节", Value: "清明节"}, + SelectItem{Key: "端午节", Value: "端午节"}}, + IsRequired: false, + SortOrder: 898, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeLotteryEnable), + Title: "是否开启抽奖", + Tip: "是否开启抽奖", + Type: CfgTypeBool, + Value: "0", + IsRequired: false, + SortOrder: 995, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeLotteryIcon), + Title: "抽奖浮标图", + Tip: "抽奖浮标图(60x60),浮动在右下角", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeLotteryUrl), + Title: "抽奖地址", + Tip: "抽奖地址", + Type: CfgTypeString, + Value: "", + IsRequired: false, + SortOrder: 994, + }, + + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeAiGirlFriend), + Title: "AI女友开关", + Tip: "AI女友开关", + Type: CfgTypeBool, + Value: "true", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeAiUndress), + Title: "AI脱衣开关", + Tip: "AI脱衣开关", + Type: CfgTypeBool, + Value: "true", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeAiImageChangeFace), + Title: "AI图片换脸开关", + Tip: "AI图片换脸开关", + Type: CfgTypeBool, + Value: "true", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeAiVideoChangeFace), + Title: "AI视频换脸开关", + Tip: "AI视频换脸开关", + Type: CfgTypeBool, + Value: "true", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeShortDramaCardID), + Title: "默认短剧卡", + Tip: "短剧付费墙默认选中的会员卡", + Type: CfgTypeSelect, + Value: "", + IsRequired: false, + SortOrder: 993, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeDefaultEntryPage), + Title: "默认进入页面", + Tip: "配置用户进入 App 后默认展示首页或短剧频道", + Type: CfgTypeSelect, + Value: "home", + SelectValues: []SelectItem{ + {Key: "首页", Value: "home"}, + {Key: "短剧", Value: "drama"}, + }, + IsRequired: true, + SortOrder: 992, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeDefaultEntryAudience), + Title: "默认进入页面生效人群", + Tip: "新用户包含首次进入 App 以及更新版本后首次进入的用户", + Type: CfgTypeSelect, + Value: "new_user", + SelectValues: []SelectItem{ + {Key: "新用户", Value: "new_user"}, + {Key: "全部用户", Value: "all_users"}, + }, + IsRequired: true, + SortOrder: 991, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeQMDL), + Title: "全民代理链接", + Tip: "全民代理链接", + Type: CfgTypeString, + Value: "ttz.fm", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeDarkWebVipName), + Title: "暗网会员卡名字", + Tip: "暗网会员卡名字", + Type: CfgTypeString, + Value: "至尊卡/帝王卡", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeDarkWebVipId), + Title: "暗网会员卡id", + Tip: "暗网会员卡id", + Type: CfgTypeString, + Value: "", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeRecommendVipId), + Title: "特邀会员卡id", + Tip: "特邀会员卡id", + Type: CfgTypeStrArr, + Value: "[]", + IsRequired: false, + SortOrder: 994, + }, + + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeAiUndressPrice), + Title: "AI脱衣价格", + Tip: "AI脱衣价格", + Type: CfgTypeInt, + Value: "200", + IsRequired: true, + SortOrder: 999, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeAiImageToVideoPrice), + Title: "AI图生视频价格", + Tip: "AI图生视频价格", + Type: CfgTypeInt, + Value: "288", + IsRequired: true, + SortOrder: 999, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeAiTextToImagePrice), + Title: "AI绘图价格", + Tip: "AI绘图价格", + Type: CfgTypeInt, + Value: "200", + IsRequired: true, + SortOrder: 999, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeAiTextToNovelPrice), + Title: "AI小说价格", + Tip: "AI小说价格", + Type: CfgTypeInt, + Value: "500", + IsRequired: true, + SortOrder: 999, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeNewbieSaleTime), + Title: "新人特惠销售时间", + Tip: "时间单位:小时", + Type: CfgTypeInt, + Value: "24", + IsRequired: false, + SortOrder: 994, + }, + + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodePrivateZoneVipName), + Title: "私密圈推荐会员卡名", + Tip: "私密圈推荐会员卡名", + Type: CfgTypeString, + Value: "至尊卡/帝王卡", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodePrivateZoneVipId), + Title: "私密圈推荐会员卡ID", + Tip: "私密圈推荐会员卡ID", + Type: CfgTypeString, + Value: "", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeHotRankingTags), + Title: "热点榜单关联标签", + Tip: "热点榜单关联标签", + Type: CfgTypeStrArr, + Value: "[]", + IsRequired: false, + SortOrder: 898, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodePersonalCenterBackground), + Title: "个人中心背景图", + Tip: "个人中心背景图", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeTotalWatchCount), + Title: "非VIP免费观看次数", + Tip: "非VIP用户每日可免费观看次数(0表示不可免费观看)", + Type: CfgTypeInt, + Value: "3", + IsRequired: true, + SortOrder: 900, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeVideo1), + Title: "创作中心视频1", + Tip: "创作中心视频1", + Type: CfgTypeString, + Value: "", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeVideo2), + Title: "创作中心视频2", + Tip: "创作中心视频2", + Type: CfgTypeString, + Value: "", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeOldReturnSaleTime), + Title: "返场促销时间", + Tip: "针对注册时间大于24小时的用户,促销时间单位:小时", + Type: CfgTypeInt, + Value: "24", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeReturnSaleVipIds), + Title: "返场售卖会员卡id", + Tip: "返场售卖会员卡id", + Type: CfgTypeStrArr, + Value: "[]", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeFreeMark), + Title: "免费角标是否展示", + Tip: "免费角标是否展示", + Type: CfgTypeBool, + Value: "true", + IsRequired: true, + SortOrder: 999, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeFreeTrialBadgeEnabled), + Title: "VIP视频免费观看试看角标", + Tip: "控制符合免费观看条件的VIP视频是否展示免费试看角标", + Type: CfgTypeBool, + Value: "true", + IsRequired: true, + SortOrder: 1000, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeVipMark), + Title: "VIP角标是否展示", + Tip: "VIP角标是否展示", + Type: CfgTypeBool, + Value: "true", + IsRequired: true, + SortOrder: 999, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeCoinMark), + Title: "金币角标是否展示", + Tip: "金币角标是否展示", + Type: CfgTypeBool, + Value: "true", + IsRequired: true, + SortOrder: 999, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeAiSort), + Title: "AI科技排序配置", + Tip: "位置表示: 1:AI脱衣 2:AI视频换脸 3:AI图片换脸 4:图生视频 5:文生图 6:AI小说 7:AI女友 输入值: 1至6 代表排序(越小越靠前)", + Type: CfgTypeObject, + Value: "{\"1\":\"1\",\"2\":\"2\",\"3\":\"3\",\"4\":\"4\",\"5\":\"5\",\"6\":\"6\",\"7\":\"7\"}", + IsRequired: false, + SortOrder: 898, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeAiSwitch), + Title: "AI科技开关配置", + Tip: "位置表示: 1:AI脱衣 2:AI视频换脸 3:AI图片换脸 4:图生视频 5:文生图 6:AI小说 7:AI女友 输入值:1:打开 0:关闭 (二选一)", + Type: CfgTypeObject, + Value: "{\"1\":\"1\",\"2\":\"1\",\"3\":\"1\",\"4\":\"1\",\"5\":\"1\",\"6\":\"1\",\"7\":\"1\"}", + IsRequired: false, + SortOrder: 898, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeSignIcon), + Title: "右上角签到图标", + Tip: "右上角签到图标", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeDarkWebEnable), + Title: "暗网是否开启", + Tip: "暗网是否开启", + Type: CfgTypeBool, + Value: "true", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeDarkWebImg), + Title: "暗网图片", + Tip: "暗网图片", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeDarkWebIcon), + Title: "暗网图标", + Tip: "暗网图标", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeCommon.GetName(), + GpCode: string(GPCodeCommon), + VCode: string(VCodeDarkWebIconName), + Title: "暗网图标名称", + Tip: "暗网图标名称", + Type: CfgTypeString, + Value: "", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeAdver.GetName(), + GpCode: string(GPCodeAdver), + VCode: string(VCodeAdverAbTestShowType), + Title: "广告ab测展示类型", + Tip: "-1-首次免广告 0-所有广告都可以观看 其他数字N-新人注册N分钟内免广告", + Type: CfgTypeObject, + Value: "{\"A\":\"0\",\"B\":\"-1\",\"C\":\"30\"}", + IsRequired: false, + SortOrder: 898, + }, + { + GroupName: GPCodeAdver.GetName(), + GpCode: string(GPCodeAdver), + VCode: string(VCodeAdverAbTestSwitch), + Title: "广告ab测开关", + Tip: "广告ab测开关, 如果关闭,新用户则不会被进行分组", + Type: CfgTypeBool, + Value: "true", + IsRequired: false, + SortOrder: 994, + }, + { + GroupName: GPCodeAdver.GetName(), + GpCode: string(GPCodeAdver), + VCode: string(VCodeNewUserAdFreeSwitch), + Title: "新人广告开关", + Tip: "新人免广告总开关;开启后,新注册用户在【新人免广告时限】内 /ping/domain 与 /ping/domain/h5 不返回广告信息;关闭则正常返回广告", + Type: CfgTypeBool, + Value: "false", + IsRequired: false, + SortOrder: 993, + }, + { + GroupName: GPCodeAdver.GetName(), + GpCode: string(GPCodeAdver), + VCode: string(VCodeNewUserAdFreeHours), + Title: "新人免广告时限(小时)", + Tip: "新用户注册后免广告的时长,单位:小时;仅在【新人广告开关】开启时生效", + Type: CfgTypeInt, + Value: "24", + IsRequired: false, + SortOrder: 992, + }, + { + GroupName: GPCodeAdver.GetName(), + GpCode: string(GPCodeAdver), + VCode: string(VCodeNewUserAdFreePositions), + Title: "新人免广告-广告位列表", + Tip: "数组,元素为广告位编码(pos),如 [\"1\",\"2\"];命中【新人广告开关】的新用户,列表内广告位不返回广告;空数组表示不屏蔽任何广告位", + Type: CfgTypeStrArr, + Value: "[]", + IsRequired: false, + SortOrder: 991, + }, + // ===== IM 配置 ===== + { + GroupName: GPCodeIM.GetName(), + GpCode: string(GPCodeIM), + VCode: string(VCodeIMAdEnable), + Title: "IM 在线广告透传开关", + Tip: "开关:开启后定时向当前在线的 IM 用户透传广告刷新通知", + Type: CfgTypeBool, + Value: "false", + IsRequired: false, + SortOrder: 1001, + }, + { + GroupName: GPCodeIM.GetName(), + GpCode: string(GPCodeIM), + VCode: string(VCodeIMAdIntervalMinutes), + Title: "推送间隔(分钟)", + Tip: "两次推送之间的最小间隔,单位分钟,默认 60", + Type: CfgTypeInt, + Value: "60", + IsRequired: false, + SortOrder: 1002, + }, + { + GroupName: GPCodeIM.GetName(), + GpCode: string(GPCodeIM), + VCode: string(VCodeIMAdMaxUsers), + Title: "最大候选用户数", + Tip: "单次推送扫描的最大用户数,默认 1000", + Type: CfgTypeInt, + Value: "1000", + IsRequired: false, + SortOrder: 1003, + }, + { + GroupName: GPCodeIM.GetName(), + GpCode: string(GPCodeIM), + VCode: string(VCodeIMAdPosChatNotificationBar), + Title: "广告位-会话页顶部通知条", + Tip: "开关:是否向该广告位推送(IM_CHAT_NOTIFICATION_BAR)", + Type: CfgTypeBool, + Value: "false", + IsRequired: false, + SortOrder: 1010, + }, + { + GroupName: GPCodeIM.GetName(), + GpCode: string(GPCodeIM), + VCode: string(VCodeIMAdPosChatFloatingGifBall), + Title: "广告位-会话页悬浮GIF球", + Tip: "开关:是否向该广告位推送(IM_CHAT_FLOATING_GIF_BALL)", + Type: CfgTypeBool, + Value: "false", + IsRequired: false, + SortOrder: 1011, + }, + { + GroupName: GPCodeIM.GetName(), + GpCode: string(GPCodeIM), + VCode: string(VCodeIMAdPosListFeedNative), + Title: "广告位-消息列表内原生广告位", + Tip: "开关:是否向该广告位推送(IM_LIST_FEED_NATIVE)", + Type: CfgTypeBool, + Value: "false", + IsRequired: false, + SortOrder: 1012, + }, + { + GroupName: GPCodeIM.GetName(), + GpCode: string(GPCodeIM), + VCode: string(VCodeIMAdPosListBanner), + Title: "广告位-列表页顶部轮播", + Tip: "开关:是否向该广告位推送(IM_LIST_BANNER)", + Type: CfgTypeBool, + Value: "false", + IsRequired: false, + SortOrder: 1013, + }, + + // ===== 用户分层弹窗配置 ===== + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeShortDramaEntryPopup), + Title: "短剧首次进入悬浮窗", + Tip: "关闭后所有用户均不展示短剧入口悬浮窗", + Type: CfgTypeBool, + Value: "true", + IsRequired: true, + SortOrder: 10001, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodePaymentGuideEnabled), + Title: "付费引导全局开关", + Tip: "关闭后所有新版付费引导场景均不展示", + Type: CfgTypeBool, + Value: "true", + IsRequired: true, + SortOrder: 10000, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodePaymentGuideHomeEnabled), + Title: "首页付费引导开关", + Tip: "同时控制首页新用户免费试看和首页老用户付费引导场景", + Type: CfgTypeBool, + Value: "true", + IsRequired: true, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupNewUserPopup), + Title: "用户分层弹窗-新用户-弹窗", + Tip: "用户分层弹窗-新用户-弹窗", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupNewUserFloating), + Title: "用户分层弹窗-新用户-浮窗", + Tip: "用户分层弹窗-新用户-浮窗", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupNewUserPlayPage), + Title: "用户分层弹窗-新用户-播放页", + Tip: "用户分层弹窗-新用户-播放页", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupNewUserMeTab), + Title: "用户分层弹窗-新用户-我的TAB", + Tip: "用户分层弹窗-新用户-我的TAB", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupNewUserVipCardID), + Title: "用户分层弹窗-新用户-会员卡ID", + Tip: "用户分层弹窗-新用户-会员卡ID", + Type: CfgTypeString, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupNewUserCountdown), + Title: "用户分层弹窗-新用户-倒计时", + Tip: "用户分层弹窗-新用户-倒计时", + Type: CfgTypeBool, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayUnder7dayPopup), + Title: "用户分层弹窗-7天内未付款-弹窗", + Tip: "用户分层弹窗-7天内未付款-弹窗", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayUnder7dayFloating), + Title: "用户分层弹窗-7天内未付款-浮窗", + Tip: "用户分层弹窗-7天内未付款-浮窗", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayUnder7dayPlayPage), + Title: "用户分层弹窗-7天内未付款-播放页", + Tip: "用户分层弹窗-7天内未付款-播放页", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayUnder7dayMeTab), + Title: "用户分层弹窗-7天内未付款-我的TAB", + Tip: "用户分层弹窗-7天内未付款-我的TAB", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayUnder7dayPopupNoCountdown), + Title: "用户分层弹窗-7天内未付款-弹窗-无倒计时", + Tip: "用户分层弹窗-7天内未付款-弹窗-无倒计时", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayUnder7dayFloatingNoCountdown), + Title: "用户分层弹窗-7天内未付款-浮窗-无倒计时", + Tip: "用户分层弹窗-7天内未付款-浮窗-无倒计时", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayUnder7dayPlayPageNoCountdown), + Title: "用户分层弹窗-7天内未付款-播放页-无倒计时", + Tip: "用户分层弹窗-7天内未付款-播放页-无倒计时", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayUnder7dayMeTabNoCountdown), + Title: "用户分层弹窗-7天内未付款-我的TAB-无倒计时", + Tip: "用户分层弹窗-7天内未付款-我的TAB-无倒计时", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayUnder7dayVipCardID), + Title: "用户分层弹窗-7天内未付款-会员卡ID", + Tip: "用户分层弹窗-7天内未付款-会员卡ID", + Type: CfgTypeString, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayUnder7dayCountdown), + Title: "用户分层弹窗-7天内未付款-倒计时", + Tip: "用户分层弹窗-7天内未付款-倒计时", + Type: CfgTypeBool, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayOver7dayPopup), + Title: "用户分层弹窗-7天后未付款-弹窗", + Tip: "用户分层弹窗-7天后未付款-弹窗", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayOver7dayFloating), + Title: "用户分层弹窗-7天后未付款-浮窗", + Tip: "用户分层弹窗-7天后未付款-浮窗", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayOver7dayPlayPage), + Title: "用户分层弹窗-7天后未付款-播放页", + Tip: "用户分层弹窗-7天后未付款-播放页", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayOver7dayMeTab), + Title: "用户分层弹窗-7天后未付款-我的TAB", + Tip: "用户分层弹窗-7天后未付款-我的TAB", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayOver7dayVipCardID), + Title: "用户分层弹窗-7天后未付款-会员卡ID", + Tip: "用户分层弹窗-7天后未付款-会员卡ID", + Type: CfgTypeString, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupUnpayOver7dayCountdown), + Title: "用户分层弹窗-7天后未付款-倒计时", + Tip: "用户分层弹窗-7天后未付款-倒计时", + Type: CfgTypeBool, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupPayNeedUpgradePopup), + Title: "用户分层弹窗-已付款需要续费升级-弹窗", + Tip: "用户分层弹窗-已付款需要续费升级-弹窗", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupPayNeedUpgradeFloating), + Title: "用户分层弹窗-已付款需要续费升级-浮窗", + Tip: "用户分层弹窗-已付款需要续费升级-浮窗", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupPayNeedUpgradePlayPage), + Title: "用户分层弹窗-已付款需要续费升级-播放页", + Tip: "用户分层弹窗-已付款需要续费升级-播放页", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupPayNeedUpgradeMeTab), + Title: "用户分层弹窗-已付款需要续费升级-我的TAB", + Tip: "用户分层弹窗-已付款需要续费升级-我的TAB", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupPayNeedUpgradeVipCardID), + Title: "用户分层弹窗-已付款需要续费升级-会员卡ID", + Tip: "用户分层弹窗-已付款需要续费升级-会员卡ID", + Type: CfgTypeString, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupPayNeedUpgradeCountdown), + Title: "用户分层弹窗-已付款需要续费升级-倒计时", + Tip: "用户分层弹窗-已付款需要续费升级-倒计时", + Type: CfgTypeBool, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + { + GroupName: GPCodePopup.GetName(), + GpCode: string(GPCodePopup), + VCode: string(VCodeUserPaymentStatusPopupMaxVIPLevel), + Title: "用户分层弹窗-VIP最大等级", + Tip: "用户分层弹窗-VIP最大等级", + Type: CfgTypeImg, + Value: "", + IsRequired: false, + SortOrder: 9999, + }, + // ============================ + + // ===== 支付方式配置 ===== + { + GroupName: GPCodePay.GetName(), + GpCode: string(GPCodePay), + VCode: string(VCodePayTypeName), + Title: "支付方式显示名称", + Tip: "支付方式在充值页展示的名称,key为支付方式(alipay/wechat/union/quickUnion/usdt/daichong),value为展示名称;未配置的支付方式沿用系统默认名称", + Type: CfgTypeObject, + Value: `{"alipay":"支付宝(官方强烈推荐)","wechat":"微信(当前通道拥挤)","quickUnion":"快捷银联","union":"银联","usdt":"USDT(官方)","daichong":"代充"}`, + IsRequired: false, + SortOrder: 700, + }, + { + GroupName: GPCodePay.GetName(), + GpCode: string(GPCodePay), + VCode: string(VCodePayTypeSort), + Title: "支付方式展示顺序", + Tip: "支付方式在充值页的展示顺序,数组元素为支付方式(alipay/wechat/union/quickUnion/usdt/daichong),越靠前越先展示;未列出的支付方式排在最后", + Type: CfgTypeStrArr, + Value: `["alipay","wechat","quickUnion","union","usdt","daichong"]`, + IsRequired: false, + SortOrder: 699, + }, +} diff --git a/models/v/systemmod/base.go b/models/v/systemmod/base.go new file mode 100644 index 0000000..cd4d71e --- /dev/null +++ b/models/v/systemmod/base.go @@ -0,0 +1,14 @@ +package systemmod + +import ( + "91porn-server/common/db" + "91porn-server/models" +) + +var cdb *db.MongoDB + +const cTable = models.SystemConfig + +func Init() { + configIndex() +} diff --git a/models/v/systemmod/config.go b/models/v/systemmod/config.go new file mode 100644 index 0000000..ea2786d --- /dev/null +++ b/models/v/systemmod/config.go @@ -0,0 +1,79 @@ +package systemmod + +import ( + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +func configColl(t *db.MongoTool) *db.MongoTool { + if t == nil { + return cdb.Coll(cTable) + } + return t.Coll(cTable) +} + +func configIndex() { + cdb = db.Init(cTable) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "type", Value: 1}, {Key: "location", Value: 1}}, + }, + { + Keys: bson.D{{Key: "isActive", Value: 1}}, + }, + } + if _, err := configColl(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("system_config model set index err ==>[%+v]", err)) + } +} + +func GetByTypeLocation(t commod.SystemConfigType, lc commod.SystemLocationCode) (*Config, error) { + var ( + now = time.Now() + c = &Config{ + Type: t, + Location: lc, + IsActive: true, + CreatedAt: now, + UpdatedAt: now, + } + ) + if err := configColl(nil).FindOne(&c, bson.M{"type": t, "location": lc}); err != nil { + return nil, err + } + if c.ID.IsZero() { + result, err := configColl(nil).InsertOne(&c) + if err != nil { + return nil, err + } + if result.InsertedID == nil { + return nil, errors.New("result.InsertedID is nil") + } + c.ID = result.InsertedID.(primitive.ObjectID) + } + return c, nil +} + +func List() ([]*Config, error) { + items := make([]*Config, 0) + return items, configColl(nil).Find(&items, bson.M{"isActive": true}) +} + +func Edit(filter, update primitive.M) error { + result, err := configColl(nil).UpdateOne(filter, update) + if err != nil { + return err + } + if result.ModifiedCount == 0 { + return errors.New("ModifiedCount is nil") + } + return nil +} diff --git a/models/v/systemmod/type.go b/models/v/systemmod/type.go new file mode 100644 index 0000000..8b5dd47 --- /dev/null +++ b/models/v/systemmod/type.go @@ -0,0 +1,20 @@ +package systemmod + +import ( + "time" + + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Config 系统配置 +type Config struct { + ID primitive.ObjectID `form:"id" json:"id" bson:"_id,omitempty"` //配置ID + Type commod.SystemConfigType `json:"type" bson:"type" swaggertype:"integer"` //配置类型 + Location commod.SystemLocationCode `json:"location" bson:"location" swaggertype:"integer"` //配置位置编号 + Content string `json:"content" bson:"content"` //配置内容 + IsActive bool `json:"isActive" bson:"isActive"` //是否激活 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 +} diff --git a/models/v/tagconfmod/tagconf.go b/models/v/tagconfmod/tagconf.go new file mode 100644 index 0000000..f033dc8 --- /dev/null +++ b/models/v/tagconfmod/tagconf.go @@ -0,0 +1,85 @@ +package tagconfmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.TagConf + +var mdb *db.MongoDB + +// InitIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "tagID", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InsertOne 插入一条数据 +func InsertOne(p *TagConf) error { + p.CreatedAt = time.Now() + _, err := coll(nil).InsertOne(p) + return err +} + +// UpdateOne 更新一条数据 +func UpdateOne(set EditSelector) error { + set.UpdatedAt = time.Now() + _, err := coll(nil).UpdateOne(bson.M{"_id": set.ID}, bson.M{"$set": set}) + return err +} + +// DeleteOne 删除一条数据 +func DeleteOne(id primitive.ObjectID) error { + _, err := coll(nil).DeleteOne(bson.M{"_id": id}) + return err +} + +// List 查询列表 +func List(page commod.Page) (list []TagConf, total int64, hasNext bool, err error) { + sort := bson.D{{Key: "createdAt", Value: -1}} + opts := options.FindOptions{} + opts.SetSkip(int64(page.Skip())).SetLimit(int64(page.Limit()) + 1).SetSort(sort) + if err = coll(nil).Find(&list, nil, &opts); err != nil { + return + } + if uint64(len(list)) > page.Limit() { + hasNext = true + list = list[:page.Limit()] + } + total, err = coll(nil).Count(bson.M{}) + return +} + +// GetAllTagConf 获取所有后台配置标签 +func GetAllTagConf() (tags []TagConf, err error) { + sort := bson.D{{Key: "sort", Value: -1}} + opts := options.FindOptions{} + opts.SetSort(sort) + err = coll(nil).Find(&tags, nil, &opts) + return +} diff --git a/models/v/tagconfmod/type.go b/models/v/tagconfmod/type.go new file mode 100644 index 0000000..4752fd1 --- /dev/null +++ b/models/v/tagconfmod/type.go @@ -0,0 +1,34 @@ +package tagconfmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type TagConf struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + TagID primitive.ObjectID `json:"tagID" bson:"tagID" binding:"required"` // 标签id + Sort *int `json:"sort" bson:"sort"` // 排序 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` // 刷新时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type EditSelector struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty" binding:"required"` // ID + TagID primitive.ObjectID `json:"tagID" bson:"tagID" binding:"required"` // 标签id + Sort *int `json:"sort" bson:"sort,omitempty"` // 排序 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` // 更新时间 +} + +type ListResponse struct { + List []TagConf `json:"list"` // 列表 + HasNext bool `json:"hasNext"` // 是否还有下一页 +} diff --git a/models/v/tagmod/app.go b/models/v/tagmod/app.go new file mode 100644 index 0000000..cc835a4 --- /dev/null +++ b/models/v/tagmod/app.go @@ -0,0 +1,40 @@ +package tagmod + +import "go.mongodb.org/mongo-driver/bson/primitive" + +// RespInfo 标签详情响应信息 +type RespInfo struct { + ID primitive.ObjectID `json:"id"` // 标签id + Name string `json:"name" binding:"required"` // 标签名字 + CoverImg string `json:"coverImg"` // 封面图片 + Description string `json:"description"` // 文字说明 + HasCollected bool `json:"hasCollected"` // 已收藏 + PlayCount int64 `json:"playCount"` // 对应视屏的总播放量 + VideoCount int64 `json:"videoCount"` // 对应视频的总数 +} + +// ListRespInfo 专题标签列表响应信息 +type ListRespInfo struct { + TagID string `json:"tagId"` // 标签id + TagName string `json:"tagName"` // 标签名字 + TagDesc string `json:"tagDesc"` // 标签名字 + CoverImg string `json:"coverImg"` // 封面图片 + Description string `json:"description"` // 文字说明 + TPlayCount int64 `json:"tPlayCount"` // 总历史点击数 + VidInfo interface{} +} + +// TagInfo TagInfo +type TagInfo struct { + ID primitive.ObjectID `json:"id,omitempty"` // 标签id + Name string `json:"name,omitempty"` // 标签名字 + CoverImg string `json:"coverImg,omitempty"` // 封面图片 + Description string `json:"description,omitempty"` // 文字说明 + PlayCount int64 `json:"playCount,omitempty"` // 播放量 + VidCount int64 `json:"vidCount,omitempty"` // 帖子数量 + FollowCount int64 `json:"followCount,omitempty"` // 关注数量 +} + +type TagInfoRes struct { + List []TagInfo `json:"list"` // 广告列表 +} diff --git a/models/v/tagmod/es.go b/models/v/tagmod/es.go new file mode 100644 index 0000000..50768e9 --- /dev/null +++ b/models/v/tagmod/es.go @@ -0,0 +1,63 @@ +package tagmod + +import ( + "fmt" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" +) + +var es *elastic.Client + +const ESTable = models.ESInfoTagTable + +func InitESIndex() { + es = elastic.Init() + var setting = elastic.M{ + "settings": elastic.M{ + "number_of_shards": elastic.NumberOfShards, + "number_of_replicas": elastic.NumberOfReplicas, + "analysis": elastic.M{ + "analyzer": elastic.M{ + "ik": elastic.M{ + "tokenizer": elastic.AnalyzerIkMaxWord, + }, + }, + }, + }, + "mappings": elastic.M{ + "properties": elastic.M{ + "tagName": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkMaxWord, + "search_analyzer": elastic.AnalyzerIkMaxWord, + }, + }, + }, + } + if err := es.CreateIndices(ESTable, setting); err != nil { + panic(fmt.Sprintf("%s index indeices err ==>[%+v]", ESTable, err)) + } +} + +func Search(keywords string, from int64, size int64) (data []ESTagSource, err error) { + query := elastic.M{ + "query": elastic.M{ + "match": elastic.M{ + "tagName": keywords, + }, + }, + "from": from, + "size": size, + } + if err = es.Search(ESTable, &data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Search", table, "CommonSearch", err), + log.Any("keywords", keywords), + log.Any("from", from), + log.Any("size", size), + ) + return + } + return +} diff --git a/models/v/tagmod/tag.go b/models/v/tagmod/tag.go new file mode 100644 index 0000000..e33485e --- /dev/null +++ b/models/v/tagmod/tag.go @@ -0,0 +1,583 @@ +package tagmod + +import ( + "encoding/json" + "fmt" + "math/rand" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/commod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.Tag + +// Coll 获取表名 +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "tagName", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "videoCount", Value: 1}, {Key: "isWonder", Value: 1}}, + }, + { + Keys: bson.D{{Key: "isWonder", Value: 1}}, //提高发现精彩的搜索速度 + Options: options.Index().SetSparse(true), + }, + { //发现精彩索引 + Keys: bson.D{{Key: "wonderSortCode", Value: -1}, {Key: "tPlayCount", Value: -1}}, + }, + { + Keys: bson.D{{Key: "tPlayCount", Value: -1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "isCommonUsed", Value: 1}}, + }, + { + Keys: bson.D{{Key: "usedSortCode", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// Insert ()插入收藏消息记录 +func InsertOne(t *Tag) (data primitive.ObjectID, err error) { + result, err := coll(nil).InsertOne(&t) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "InsertOne", err)) + return + } + byteID, err := json.Marshal(result.InsertedID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "Marshal", err)) + return + } + if err = data.UnmarshalJSON(byteID); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertOne", table, "UnmarshalJSON", err)) + return + } + return +} +func InsertMany(t []Tag) (err error) { + _, err = coll(nil).InsertMany(t) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v", "InsertMany", table, "InsertMany", err)) + return + } + return +} + +// FindTagsList 后台根据条件获取标签列表 +func FindTagsAndCount(showWonder int, content string, stdQuery commod.StdQuery) (data []Tag, total int64, err error) { + query := bson.M{"tagName": primitive.Regex{Pattern: content, Options: "i"}} + switch showWonder { + case 1: + query["isWonder"] = bson.M{"$eq": true} + case 2: + query["$or"] = []bson.M{{"isWonder": bson.M{"$eq": false}}, {"isWonder": bson.M{"$exists": false}}} + default: + } + *stdQuery.Order = append(*stdQuery.Order, commod.OrderBy{Key: "createdAt", Desc: true}) + if err = coll(nil).Find(&data, query, commod.ConvertToListQuery(stdQuery)); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindTagsAndCount", table, "Find", err), + log.Any("showWonder", showWonder), + log.Any("content", content), + ) + return + } + total, err = coll(nil).Count(query) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindTagsAndCount", table, "Count", err), + log.Any("showWonder", showWonder), + log.Any("content", content), + ) + return + } + return +} + +// FindPureTags +func FindPureTags(content string, stdQuery commod.StdQuery) (data []WebPureTagListRespInfo, err error) { + query := bson.M{"isCommonUsed": true, "tagName": primitive.Regex{Pattern: content, Options: "i"}} + opts := options.Find().SetProjection(bson.M{"_id": 1, "tagName": 1}) + *stdQuery.Order = append(*stdQuery.Order, commod.OrderBy{Key: "usedSortCode", Desc: true}) + if err = coll(nil).Find(&data, query, commod.ConvertToListQuery(stdQuery), opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindPureTags", table, "Find", err), + log.Any("content", content), + ) + return + } + return +} + +func PureTagsCount(content string) (total int64, err error) { + query := bson.M{"isCommonUsed": true, "tagName": primitive.Regex{Pattern: content, Options: "i"}} + total, err = coll(nil).Count(query) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "PureTagsCount", table, "Count", err), + log.Any("content", content), + ) + return + } + return +} + +// GetTagsByRegexName 根据标签名搜索标签 +func FindTagsByRegexName(content string, stdQuery commod.StdQuery) (data []Tag, err error) { + filter := bson.M{"tagName": bson.M{"$regex": content}} + if err = coll(nil).Find(&data, filter, commod.ConvertToListQuery(stdQuery)); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindTagsByRegexName", table, "Find", err), + log.Any("content", content), + ) + return + } + return +} + +// GetTagsByKeyword 根据标签名搜索标签 +func GetTagsByKeyword(keyword string, skip, limit int64) (data []Tag, err error) { + filter := bson.M{"tagName": bson.M{ + "$regex": fmt.Sprintf("^%s", keyword), + }} + opt := (&options.FindOptions{}). + SetSort(bson.D{{Key: "tPlayCount", Value: -1}}). + SetSkip(skip). + SetLimit(limit) + if err = coll(nil).Find(&data, filter, opt); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetTagsByKeyword", table, "Find", err), + log.Any("keyword", keyword), + log.Any("skip", skip), + log.Any("limit", limit), + ) + return + } + return +} + +// GetTagByName 根据标签名获取标签 +func FindOneTagByName(tagName string) (data Tag, err error) { + if err = coll(nil).FindOne(&data, bson.M{"tagName": tagName}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneTagByName", table, "FindOne", err), + log.Any("tagName", tagName), + ) + return + } + return +} + +// FindOneTagByNames 根据标签名获取标签 +func FindOneTagByNames(tagNames []string) (data []Tag, err error) { + if err = coll(nil).Find(&data, bson.M{"tagName": bson.M{"$in": tagNames}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneTagByNames", table, "Find", err), + log.Any("tagNames", tagNames), + ) + return + } + return +} + +// GetTagIDByName 获取标签id +func GetTagIDByName(name string) (primitive.ObjectID, error) { + d, err := FindOneTagByName(name) + return d.ID, err +} + +// GetTagListByKeyword 根据标签名获取标签 +func GetTagListByKeyword(keyword string, sort bson.D, skip, limit int64) ([]Tag, error) { + opt := &options.FindOptions{} + if len(sort) != 0 { + opt.SetSort(sort) + } + opt.SetSkip(skip) + opt.SetLimit(limit) + filter := bson.M{ + "tagName": bson.M{ + "$regex": fmt.Sprintf("^%s", keyword), + }, + } + tagList := make([]Tag, 0, limit) + if err := coll(nil).Find(&tagList, filter, opt); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetTagListByKeyword", table, "Find", err), + log.Any("keyword", keyword), + log.Any("sort", sort), + log.Any("skip", skip), + log.Any("limit", limit), + ) + return nil, err + } + return tagList, nil +} + +// GetTagsByIDS 根据id获取标签 +func FindTagsByIDS(ids []primitive.ObjectID) (data []Tag, err error) { + if len(ids) == 0 { + return + } + if err = coll(nil).Find(&data, bson.M{"_id": bson.M{"$in": ids}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindTagsByIDS", table, "Find", err), + log.Any("ids", ids), + ) + return + } + return +} + +func FindTagsMapByIDS(ids []primitive.ObjectID) (map[primitive.ObjectID]Tag, error) { + tags, err := FindTagsByIDS(ids) + if err != nil { + return nil, err + } + var tMap = make(map[primitive.ObjectID]Tag) + for _, v := range tags { + tMap[v.ID] = v + } + return tMap, nil +} + +// FindTagByID 根据id获取标签信息 +func FindOneTagByID(id primitive.ObjectID) (data Tag, err error) { + if err = coll(nil).FindOne(&data, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneTagByID", table, "FindOne", err), + log.Any("id", id), + ) + return + } + return +} + +// FindOneTagByIds 根据id获取标签信息 +func FindOneTagByIds(ids []primitive.ObjectID) (data []Tag, err error) { + op := options.Find().SetSort(bson.D{{"wonderSortCode", -1}}) + if err = coll(nil).Find(&data, bson.M{"_id": bson.M{"$in": ids}, "isActive": true}, op); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneTagByIds", table, "Find", err), + log.Any("ids", ids), + ) + return + } + return +} + +// TagsCount 获取标签数量 +func FindTagsTotalCount() (total int64, err error) { + total, err = coll(nil).Count(bson.M{"videoCount": bson.M{"$gt": 0}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindTagsTotalCount", table, "Count", err)) + return + } + return +} + +func IncreaseTagsCollectCount(ids []primitive.ObjectID, inc int) (err error) { + _, err = coll(nil).UpdateMany(bson.M{"_id": bson.M{"$in": ids}}, bson.M{"$inc": bson.M{"collCount": inc}, "$set": bson.M{"updatedAt": time.Now()}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncreaseTagsCollectCount", table, "UpdateMany", err), + log.Any("ids", ids), + log.Any("inc", inc), + ) + return + } + return +} + +// TagsCount 根据搜索内容获取标签数量 +func FindTagsCountByRegexName(content string) (total int64, err error) { + filter := bson.M{ + "tagName": bson.M{ + "$regex": fmt.Sprintf("^%s", content), + }, + "videoCount": bson.M{"$gt": 0}, + } + total, err = coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindTagsCountByRegexName", table, "Count", err), + log.Any("content", content), + ) + return + } + return +} + +// UpdateTag 编辑标签 +func UpdateOneTagByID(id primitive.ObjectID, doc TagUpdateDoc) (err error) { + docM, err := common.ToBsonM(doc) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateOneTagByID", table, "ToBsonM", err), + log.Any("id", id), + log.Any("doc", doc), + ) + return + } + update := bson.M{} + if doc.IsWonder != nil { + isWonder := *doc.IsWonder + if !isWonder { + update["$unset"] = bson.M{"isWonder": 1} //维护稀疏字段,提高搜素效率 + delete(docM, "isWonder") + } + } + update["$set"] = docM + if _, err = coll(nil).UpdateOne(bson.M{"_id": id}, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateOneTagByID", table, "UpdateOne", err), + log.Any("id", id), + log.Any("doc", doc), + ) + return + } + return +} + +// IncreaseTagCollectCount 增加收藏数 +func IncreaseTagCollectCount(inc int, ids ...primitive.ObjectID) (err error) { + var query = bson.M{} + if len(ids) == 1 { + query["_id"] = ids[0] + } else { + query["_id"] = bson.M{"$in": ids} + } + if _, err = coll(nil).UpdateOne(query, bson.M{"$inc": bson.M{"collCount": inc}, "$set": bson.M{"updatedAt": time.Now()}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncreaseTagCollectCount", table, "UpdateOne", err), + log.Any("id", ids), + log.Any("inc", inc), + ) + return + } + return +} + +// IncreaseTagVidCount 增加收藏数 +func IncreaseTagVidCount(id primitive.ObjectID, inc int) (err error) { + if _, err = coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$inc": bson.M{"videoCount": inc}, "$set": bson.M{"updatedAt": time.Now()}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncreaseTagVidCount", table, "UpdateOne", err), + log.Any("id", id), + log.Any("inc", inc), + ) + return + } + return +} + +// IncTagVidCountByMap 增加/减少视频数 +func IncTagVidCountByMap(mids map[primitive.ObjectID]int) (err error) { + var ( + infos = map[int][]primitive.ObjectID{} + now = time.Now() + ) + // 合并 + for id, n := range mids { + if n == 0 { + continue + } + infos[n] = append(infos[n], id) + } + + for n, ids := range infos { + updater := bson.M{ + "$inc": bson.M{"videoCount": n}, + "$set": bson.M{"updatedAt": now}, + } + filter := bson.M{} + if n <= 0 { + filter["videoCount"] = bson.M{"$gte": -n} + } + if len(ids) == 1 { + filter["_id"] = ids[0] + _, err = coll(nil).UpdateOne(filter, updater) + } else { + filter["_id"] = bson.M{"$in": ids} + _, err = coll(nil).UpdateMany(filter, updater) + } + + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncTagVidCountByMap", table, "Update", err), + log.Any("ids", ids), + log.Any("inc", n), + ) + } + } + return +} + +// WonderTags 发现精彩TagList +func WonderTags(skip, limit int64) ([]vidmod.TagInfo, error) { + opt := (&options.FindOptions{}). + SetSort(bson.D{{Key: "wonderSortCode", Value: -1}, {Key: "tPlayCount", Value: -1}}). + SetSkip(skip). + SetLimit(limit) + filter := bson.M{ + "isWonder": true, + "videoCount": bson.M{"$gt": 0}, + } + info := make([]vidmod.TagInfo, 0, limit) + if err := coll(nil).Find(&info, filter, opt); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "WonderTags", table, "Find", err), + log.Any("skip", skip), + log.Any("limit", limit), + ) + return nil, err + } + return info, nil +} + +// TagMap tagID->Tag map +func TagMap(ids []primitive.ObjectID) (map[primitive.ObjectID]Tag, error) { + if len(ids) == 0 { + return make(map[primitive.ObjectID]Tag), nil + } + taglist := make([]Tag, 0, len(ids)) + filter := bson.M{ + "_id": bson.M{"$in": ids}, + } + if err := coll(nil).Find(&taglist, filter); err != nil { + return nil, err + } + m := make(map[primitive.ObjectID]Tag) + for _, v := range taglist { + m[v.ID] = v + } + return m, nil +} + +// TagListByFakePlayCount TagListByFakePlayCount +func TagListByFakePlayCount(skip, limit int64) ([]Tag, error) { + tagList := make([]Tag, 0, limit) + opt := (&options.FindOptions{}). + SetSort(bson.D{ + {Key: "usedSortCode", Value: -1}, // 总播放量 = 真播放量+假播放量 夏沫确认 + }). + SetSkip(skip). + SetLimit(limit) + return tagList, coll(nil).Find(&tagList, bson.M{"isActive": true}, opt) +} + +func IsExists(tagID primitive.ObjectID) bool { + count, err := coll(nil).Count(bson.M{"_id": tagID}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsExists", table, "Count", err), + log.Any("tagID", tagID), + ) + return false + } + return count > 0 +} + +func IncreaseTagLikeCount(tagID primitive.ObjectID) (err error) { + if _, err = coll(nil).UpdateOne(bson.M{"_id": tagID}, bson.M{"$inc": bson.M{"likeCount": 1}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncreaseTagLikeCount", table, "UpdateOne", err), + log.Any("tagID", tagID), + ) + return + } + return +} + +const totalCount = 500 + +func GetListRandom(limit int64) ([]Tag, error) { + randSkip := rand.Int63n(totalCount) + sort := bson.D{{Key: "tPlayCount", Value: -1}} + tagList := make([]Tag, 0, limit) + opt := (&options.FindOptions{}). + SetSort(sort). + SetSkip(randSkip). + SetLimit(limit) + return tagList, coll(nil).Find(&tagList, bson.M{}, opt) +} + +// GetTagListByUpdateTimeRange 获取ES同步数据 +func GetTagListByUpdateTimeRange(start time.Time, end time.Time) (data []Tag, err error) { + var query = bson.M{ + "updatedAt": bson.M{"$gte": start, "$lt": end}, + } + if err = coll(nil).Find(&data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetTagListByUpdateTimeRange", table, "Find", err), + log.Any("start", start), + log.Any("end", end), + ) + return + } + return +} + +// DeleteTags 删除标签 +func DeleteTags(ids []primitive.ObjectID) (int64, error) { + if ids == nil { + ids = []primitive.ObjectID{} + } + cond := bson.M{"_id": bson.M{"$in": ids}} + result, err := coll(nil).DeleteMany(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteTags", table, "DeleteMany", err), log.Any("ids", ids)) + return 0, err + } + return result.DeletedCount, nil +} + +// GetTagListMostPlayed 分页获取播放量靠前的tag +func GetTagListMostPlayed(page commod.Page) (tags []Tag, hasNext bool, err error) { + opts := options.FindOptions{} + sort := bson.D{{Key: "tPlayCount", Value: -1}, {Key: "videoCount", Value: -1}} + opts.SetSort(sort).SetSkip(int64(page.Skip())).SetLimit(int64(page.Limit()) + 1) + filter := bson.M{"isActive": true} + if err = coll(nil).Find(&tags, filter, &opts); err != nil { + return + } + if uint64(len(tags)) > page.Limit() { + hasNext = true + tags = tags[:page.Limit()] + } + return +} + +// GetCommonRecmdTags 根据条件获取标签,不分页 +func GetCommonRecmdTags() ([]Tag, error) { + opts := options.Find().SetSort(bson.D{{Key: "usedSortCode", Value: -1}}) + filter := bson.M{"isCommonUsed": true, "isWonder": true, "isActive": true} + var tags []Tag + return tags, coll(nil).Find(&tags, filter, opts) +} + +// GetLibraryTagList 分页获取播放量靠前的tag +func GetLibraryTagList(page commod.Page) (tags []Tag, hasNext bool, err error) { + opts := options.FindOptions{} + opts.SetLimit(40) + opts.SetSort(bson.D{{Key: "wonderSortCode", Value: -1}}) + + filter := bson.M{"isActive": true, "isWonder": true} + if err = coll(nil).Find(&tags, filter, &opts); err != nil { + return + } + if uint64(len(tags)) > page.Limit() { + hasNext = true + tags = tags[:page.Limit()] + } + return +} diff --git a/models/v/tagmod/type.go b/models/v/tagmod/type.go new file mode 100644 index 0000000..41970bd --- /dev/null +++ b/models/v/tagmod/type.go @@ -0,0 +1,92 @@ +package tagmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Init 初始化标签model索引 +func Init() { + mdb = db.Init(table) + initIndex() +} + +// Tag 标签表,存放所有的标签信息 +type Tag struct { + ID primitive.ObjectID `bson:"_id,omitempty"` // 标签id + TagName string `bson:"tagName"` // 标签名字 前端使用name + TagDesc string `bson:"tagDesc"` // 标签描述 + HotMark string `bson:"hotMark"` // 热门标签 + CoverImg string `bson:"coverImg"` // 封面图片 + Description string `bson:"description"` // 文字说明 + SortCode int64 `bson:"sortCode"` // 排序 + VPlayCount int64 `bson:"vPlayCount"` // 有效播放量 + TPlayCount int64 `bson:"tPlayCount"` // 总播放量 = 真播放量+假播放量 夏沫确认 + FakePlayCount int64 `bson:"fakePlayCount"` // 播放量(假数据) + PlayRating float64 `bson:"playRating"` // 有效播放率 + CollCount int64 `bson:"collCount"` // 收藏数 + LikeCount int64 `bson:"likeCount"` // 喜欢数: 当对通过标签获取的视频点赞时,计数+1 + FakeCollCount int64 `bson:"fakeCollCount"` // 收藏数(假数据) + VideoCount int64 `bson:"videoCount"` // 使用此标签的视频数量 + IsActive bool `bson:"isActive"` // 标签开关 + IsWonder bool `bson:"isWonder,omitempty"` // 是否推送精彩 false 缺省字段 + WonderSortCode int64 `bson:"wonderSortCode"` // 精彩标签排序码 + IsCommonUsed bool `bson:"isCommonUsed"` // 是否是常用标签 + UsedSortCode int64 `bson:"usedSortCode"` // 常用排序 + CreatedAt time.Time `bson:"createdAt"` // 创建时 + UpdatedAt time.Time `bson:"updatedAt"` // 更新时间 +} + +type ESTag struct { + ID primitive.ObjectID `json:"id"` // 标签id + TagName string `json:"tagName"` // 标签名字 前端使用name + TagDesc string `json:"tagDesc"` // 标签描述 + SortCode int64 `json:"sortCode"` // 排序 + VPlayCount int64 `json:"vPlayCount"` // 有效播放量 + TPlayCount int64 `json:"tPlayCount"` // 总播放量 = 真播放量+假播放量 夏沫确认 + FakePlayCount int64 `json:"fakePlayCount"` // 播放量(假数据) + PlayRating float64 `json:"playRating"` // 有效播放率 + CollCount int64 `json:"collCount"` // 收藏数 + LikeCount int64 `json:"likeCount"` // 喜欢数: 当对通过标签获取的视频点赞时,计数+1 + FakeCollCount int64 `json:"fakeCollCount"` // 收藏数(假数据) + VideoCount int64 `json:"videoCount"` // 使用此标签的视频数量 + IsActive bool `json:"isActive"` // 标签开关 + IsWonder bool `json:"isWonder"` // 是否推送精彩 false 缺省字段 + WonderSortCode int64 `json:"wonderSortCode"` // 精彩标签排序码 + CreatedAt time.Time `json:"createdAt"` // 创建时 + UpdatedAt time.Time `json:"updatedAt"` // 更新时间 +} + +type ESTagSource struct { + ID primitive.ObjectID `json:"_id"` + Source ESTag `json:"_source"` +} + +// TagUpdateDoc 标签更新信息 +type TagUpdateDoc struct { + TagName *string `bson:"tagName,omitempty"` // 标签名字 前端使用name + TagDesc *string `bson:"tagDesc,omitempty"` // 标签描述 + CoverImg *string `bson:"coverImg,omitempty"` // 封面图片 + SortCode *int64 `bson:"sortCode,omitempty"` // 排序 + VPlayCount *int64 `bson:"vPlayCount,omitempty"` // 有效播放量 + TPlayCount *int64 `bson:"tPlayCount,omitempty"` // 总播放量 + FakePlayCount *int64 `bson:"fakePlayCount,omitempty"` // 播放量(假数据) + PlayRating *float64 `bson:"playRating,omitempty"` // 有效播放率 + CollCount *int64 `bson:"collCount,omitempty"` // 收藏数 + FakeCollCount *int64 `bson:"fakeCollCount,omitempty"` // 收藏数(假数据) + VideoCount *int64 `bson:"videoCount,omitempty"` // 使用此标签的视频数量 + IsWonder *bool `bson:"isWonder,omitempty"` // 是否推送精彩 + WonderSortCode *int64 `bson:"wonderSortCode,omitempty"` // 精彩标签排序码 + IsCommonUsed *bool `bson:"isCommonUsed,omitempty"` // 是否是常用标签 + UsedSortCode *int64 `bson:"usedSortCode,omitempty"` // 常用排序 + UpdatedAt time.Time `bson:"updatedAt,omitempty"` // 更新时间 + IsActive *bool `bson:"isActive,omitempty"` // 是否启用 + HotMark *string `bson:"hotMark,omitempty"` // 封面图片 +} + +func (t *Tag) TagID() primitive.ObjectID { + return t.ID +} diff --git a/models/v/tagmod/web.go b/models/v/tagmod/web.go new file mode 100644 index 0000000..b4245a0 --- /dev/null +++ b/models/v/tagmod/web.go @@ -0,0 +1,80 @@ +package tagmod + +import ( + "time" + + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// WebTagListRespInfo 后台响应信息 +type WebTagListRespInfo struct { + ID primitive.ObjectID `json:"id"` + TagName string `json:"name"` // 标签名 + TagDesc string `json:"tagDesc"` // 标签描述 + CoverImg string `json:"coverImg"` // 封面 + SortCode int64 `json:"sortCode"` // 排序码 + VPlayCount int64 `json:"vPlayCount"` // 有效播放量 + TPlayCount int64 `json:"tPlayCount"` // 总播放量 + FakePlayCount int64 `json:"fakePlayCount"` // 总播放量(假数据) + PlayRating float32 `json:"playRating"` // 有效播放率 + CollCount int64 `json:"collCount"` // 收藏数 + FakeCollCount int64 `json:"fakeCollCount" bson:"fakeCollCount"` // 收藏数(假数据) + VideoCount int64 `json:"videoCount"` // 视频数 + IsWonder bool `json:"isWonder"` // 是否推送至精彩 + WonderSortCode int64 `json:"wonderSortCode" bson:"wonderSortCode"` // 精彩标签排序 + WID string `json:"wID"` // 推送至精彩的标签的_id + IsRecmd bool `json:"isRecmd"` // true:是推荐标签 + IsCommonUsed bool `json:"isCommonUsed"` // 是否是常用标签 + UsedSortCode int64 `json:"usedSortCode"` // 常用排序 + RID primitive.ObjectID `json:"rid"` // 推荐标签的ID + CreatedAt time.Time `json:"createdAt"` // 创建时 + UpdatedAt time.Time `json:"updatedAt"` // 更新时间 + IsActive bool `json:"isActive"` // 标签开关 + HotMark string `json:"hotMark"` // 热门标签 +} + +type WebPureTagListRespInfo struct { + ID primitive.ObjectID `json:"id" bson:"_id"` + TagName string `json:"name" bson:"tagName"` // 标签名 +} + +type WebTagListReqInfo struct { + Key string `form:"key" json:"key" binding:"omitempty"` + Value int `form:"value" json:"value" binding:"omitempty"` + Content string `form:"content" json:"content" binding:"omitempty"` + FilterValue int `form:"filterValue" json:"filterValue" binding:"omitempty"` + Page commod.Page +} + +type WebTagUpdateReqInfo struct { + ID primitive.ObjectID `form:"id" json:"id" binding:"required"` + TagName string `form:"name" json:"name" binding:"required"` + TagDesc string `form:"tagDesc" json:"tagDesc" binding:"omitempty"` + HotMark string `form:"hotMark" json:"hotMark" binding:"omitempty"` + CoverImg string `form:"coverImg" json:"coverImg" binding:"required"` + FakePlayCount int64 `form:"fakePlayCount" json:"fakePlayCount" binding:"omitempty"` + SortCode int64 `form:"sortCode" json:"sortCode" binding:"omitempty"` + IsWonder bool `form:"isWonder" json:"isWonder" binding:"omitempty"` + WonderSortCode int64 `form:"wonderSortCode" json:"wonderSortCode" binding:"omitempty"` + IsCommonUsed bool `form:"isCommonUsed" json:"isCommonUsed" binding:"omitempty"` + UsedSortCode int64 `form:"usedSortCode" json:"usedSortCode" binding:"omitempty"` + IsActive bool `form:"isActive" json:"isActive" binding:"omitempty"` +} + +type WebTagDeleteReqInfo struct { + IDs []primitive.ObjectID `form:"id" json:"id" binding:"required"` +} + +type WebTagAddReqInfo struct { + TagName string `form:"name" json:"name" binding:"required"` + TagDesc string `form:"tagDesc" json:"tagDesc" binding:"omitempty"` + HotMark string `form:"hotMark" json:"hotMark" binding:"omitempty"` + CoverImg string `form:"coverImg" json:"coverImg"` + SortCode int64 `form:"sortCode" json:"sortCode" binding:"omitempty"` + IsWonder bool `form:"isWonder" json:"isWonder" binding:"omitempty"` + WonderSortCode int64 `form:"wonderSortCode" json:"wonderSortCode" binding:"omitempty"` + UsedSortCode int64 `form:"usedSortCode" json:"usedSortCode" binding:"omitempty"` + IsCommonUsed bool `form:"isCommonUsed" json:"isCommonUsed" binding:"omitempty"` +} diff --git a/models/v/taskmod/app.go b/models/v/taskmod/app.go new file mode 100644 index 0000000..a419117 --- /dev/null +++ b/models/v/taskmod/app.go @@ -0,0 +1,11 @@ +package taskmod + +type ReceiveTaskReq struct { + TaskID string `json:"taskId" bson:"taskId" binding:"required"` // 任务ID + Type int `json:"type" bson:"type" binding:"required"` // 任务类型 1:每日任务 2:一次性任务 3:成长任务 +} + +type DoTaskReq struct { + TaskID string `json:"taskId" bson:"taskId" binding:"required"` // 任务ID + Type int `json:"type" bson:"type" binding:"required"` // 任务类型 +} diff --git a/models/v/taskmod/config.go b/models/v/taskmod/config.go new file mode 100644 index 0000000..1a43f80 --- /dev/null +++ b/models/v/taskmod/config.go @@ -0,0 +1,151 @@ +package taskmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "errors" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.TaskConfig + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "status", Value: 1}, {Key: "finishCondition", Value: -1}}, + }, + { + Keys: bson.D{{Key: "type", Value: 1}, {Key: "status", Value: 1}, {Key: "finishCondition", Value: -1}}, + }, + { + Keys: bson.D{{Key: "finishCondition", Value: 1}, {Key: "type", Value: 1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}}, + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("user model set index err ==>[%+v]", err)) + } +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func ConfigList() ([]*TaskConfig, error) { + var out []*TaskConfig = make([]*TaskConfig, 0) + if err := coll(nil).Find(&out, bson.M{"status": true}, options.Find().SetSort(bson.M{"finishCondition": 1})); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ConfigList", models.TaskConfig, "Find", err)) + return nil, err + } + return out, nil +} + +func GetConfigListByType(t ConfigType) ([]*TaskConfig, error) { + var out []*TaskConfig = make([]*TaskConfig, 0) + if err := coll(nil).Find(&out, bson.M{"type": t, "status": true}, options.Find().SetSort(bson.M{"finishCondition": 1})); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ConfigList", models.TaskConfig, "Find", err)) + return nil, err + } + return out, nil +} + +func GetConfigByID(id primitive.ObjectID) (*TaskConfig, error) { + var out *TaskConfig = new(TaskConfig) + if err := coll(nil).FindOne(&out, bson.M{"_id": id, "status": true}); err != nil { + return nil, err + } + return out, nil +} + +func IsExistConfig(finishCondition int64, configType ConfigType) (bool, error) { + count, err := coll(nil).Count(bson.M{"finishCondition": finishCondition, "type": configType}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsExistConfig", models.TaskConfig, "Count", err), + log.Any("finishCondition", finishCondition), + log.Any("configType", configType), + ) + return false, err + } + return count > 0, nil +} + +func AddConfig(sc TaskConfig) error { + if _, err := coll(nil).InsertOne(&sc); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AddConfig", models.TaskConfig, "InsertOne", err), + log.Any("sc", sc), + ) + return err + } + return nil +} + +func ModifyConfig(filter, update primitive.M) error { + result, err := coll(nil).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ModifyConfig", models.TaskConfig, "UpdateOne", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + if result.ModifiedCount == 0 { + err = errors.New("prize UpdateOne ModifiedCount err") + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ModifyConfig", models.TaskConfig, "result.ModifiedCount == 0", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + return nil +} + +func QueryAllConfig(filter primitive.M, opts ...*options.FindOptions) ([]*TaskConfig, error) { + var out []*TaskConfig = []*TaskConfig{} + if err := coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllConfig", models.TaskConfig, "Find", err), + log.Any("filter", filter), + ) + return nil, err + } + return out, nil +} + +func CountConfig(filter primitive.M) (int64, error) { + count, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountConfig", models.TaskConfig, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } + return count, nil +} + +func QueryTaskConfigByCond(filter primitive.M) (*TaskConfig, error) { + var out *TaskConfig + if err := coll(nil).FindOne(&out, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryTaskConfigByCond", models.TaskConfig, "Find", err), + log.Any("filter", filter), + ) + return nil, err + } + return out, nil +} diff --git a/models/v/taskmod/type.go b/models/v/taskmod/type.go new file mode 100644 index 0000000..6784ec5 --- /dev/null +++ b/models/v/taskmod/type.go @@ -0,0 +1,45 @@ +package taskmod + +import ( + "91porn-server/common/db" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +var mdb *db.MongoDB + +type ConfigType int + +const ( + GrowthBuyGold = iota + 1 // 成长任务-购买金币 + GrowthBuyInviteUser // 邀请任务 + GrowthBuyLogin // 登陆任务 + + // 以下都是旧的,暂时没用 + //Sign // 签到福利 + //JewelBox // 宝箱福利 + //ConsumerFeedback // 消费回馈(站群) + //OnceTask // 新手一次福利 4 + //DailyTask // 日常福利 5 +) + +type TaskConfig struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档ID + Title string `json:"title" bson:"title"` // 任务标题 + Img string `json:"img" bson:"img"` // 任务图片 + Desc string `json:"desc" bson:"desc"` // 任务描述 + FinishCondition int64 `json:"finishCondition" bson:"finishCondition"` // 达成条件 + Type ConfigType `json:"type" bson:"type"` // 配置类型 + Prizes []primitive.ObjectID `json:"prizes" bson:"prizes"` // 奖品列表 + Detail []TaskDetail `json:"detail" bson:"detail"` // 任务详情(进阶任务使用) + Link string `json:"link" bson:"link"` // 跳转链接 + Status bool `json:"status" bson:"status"` // 启用/禁用 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 修改时间 + CreateTime time.Time `json:"createTime" bson:"createTime"` // 创建时间 +} + +type TaskDetail struct { + Prize primitive.ObjectID `json:"prize" bson:"prize"` // 奖品 + SubTitle string `json:"subTitle"` // 子标题 + FinishCondition uint64 `json:"finishCondition" bson:"finishCondition"` // 达成条件 +} diff --git a/models/v/taskmod/web.go b/models/v/taskmod/web.go new file mode 100644 index 0000000..745b315 --- /dev/null +++ b/models/v/taskmod/web.go @@ -0,0 +1,92 @@ +package taskmod + +import ( + "time" + + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type AddConfigCond struct { + Title string `json:"title"` // 任务标题 + Desc string `json:"desc"` // 任务描述 + //FinishCondition int64 `json:"finishCondition"` // 达成条件 Todo:连续签到多少天 + Type ConfigType `json:"type" binding:"required"` // 配置类型 1-购买金币 + //Prizes []primitive.ObjectID `json:"prizes"` // 奖品列表(暂时不需要) + Img string `json:"img" bson:"img"` // 任务图片 + Detail []TaskDetail `json:"detail" bson:"detail"` // 任务详情(进阶任务使用) + Link string `json:"link" bson:"link"` // 跳转链接 + Status bool `json:"status"` // 启用/禁用 +} + +type ModifyConfigCond struct { + ID primitive.ObjectID `json:"id"` // 任务id + Title *string `json:"title"` // 任务标题 + Desc *string `json:"desc"` // 任务描述 + //FinishCondition int64 `json:"finishCondition"` // 达成条件 Todo:连续签到多少天 + Type *ConfigType `json:"type" binding:"required"` // 配置类型 1-购买金币 + //Prizes []primitive.ObjectID `json:"prizes"` // 奖品列表(暂时不需要) + Img *string `json:"img" bson:"img"` // 任务图片 + Detail []TaskDetail `json:"detail" bson:"detail"` // 任务详情(进阶任务使用) + Link *string `json:"link" bson:"link"` // 跳转链接 + Status *bool `json:"status"` // 启用/禁用 +} + +func (m *ModifyConfigCond) Cond() primitive.M { + return bson.M{"_id": m.ID} +} + +func (m *ModifyConfigCond) Bson() primitive.M { + set := bson.M{"updateTime": time.Now()} + //if m.FinishCondition != nil { + // set["finishCondition"] = m.FinishCondition + //} + if m.Title != nil { + set["title"] = m.Title + } + if m.Desc != nil { + set["desc"] = m.Desc + } + if m.Type != nil { + set["type"] = m.Type + } + //if len(m.Prizes) > 0 { + // set["prizes"] = m.Prizes + //} + if len(m.Detail) > 0 { + set["detail"] = m.Detail + } + if m.Img != nil { + set["img"] = m.Img + } + if m.Img != nil { + set["img"] = m.Img + } + if m.Link != nil { + set["link"] = m.Link + } + if m.Status != nil { + set["status"] = m.Status + } + return bson.M{"$set": set} +} + +type QueryAllConfigCond struct { + Status *bool `form:"status,omitempty"` // 启用/禁用 + commod.Page +} + +func (q *QueryAllConfigCond) Options() *options.FindOptions { + return options.Find().SetLimit(int64(q.PageSize)).SetSkip(int64((q.PageNumber - 1) * q.PageSize)).SetSort(bson.M{"finishCondition": 1}) +} + +func (q *QueryAllConfigCond) Query() primitive.M { + fliter := bson.M{} + if q.Status != nil { + fliter["status"] = q.Status + } + return fliter +} diff --git a/models/v/taskrecordmod/app.go b/models/v/taskrecordmod/app.go new file mode 100644 index 0000000..b5a7c81 --- /dev/null +++ b/models/v/taskrecordmod/app.go @@ -0,0 +1,21 @@ +package taskrecordmod + +type TaskType int + +const ( + //DailyTaskTypeUserInviteOne TaskType = 1 // 累计邀请1人 + //DailyTaskTypeUserInviteThree TaskType = 2 // 累计邀请3人 + //DailyTaskTypeUserInviteTen TaskType = 3 // 累计邀请10人 + //DailyTaskTypeUserInviteThirty TaskType = 4 // 累计邀请30人 + + DailyTaskTypeAdsClick TaskType = 1 // 每日广告点击 + DailyTaskTypeUserInvite TaskType = 2 // 每日邀请 + DailyUserLogin TaskType = 5 // 每日登录 + DailyComment TaskType = 6 // 每日评论 + DailyPublish TaskType = 7 // 每日发布 + OnceTaskTypeUserBuyVip TaskType = 8 // 购买vip + Download TaskType = 9 // 下载APP + DailyTaskCommentSetGod TaskType = 10 // 评论被设置成神评 + DailyDoCollect TaskType = 11 // 每日收藏 + DailyUnlockByGold TaskType = 12 // 每日解锁(video/acg) +) diff --git a/models/v/taskrecordmod/taskRecord.go b/models/v/taskrecordmod/taskRecord.go new file mode 100644 index 0000000..8e0d040 --- /dev/null +++ b/models/v/taskrecordmod/taskRecord.go @@ -0,0 +1,452 @@ +package taskrecordmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/timeutil" + "91porn-server/models" + "errors" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/mongo/options" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +const table = models.Task + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{"createdAt", -1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: -1}, {Key: "taskId", Value: -1}, {Key: "recordTime", Value: -1}}, + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("user model set index err ==>[%+v]", err)) + } +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func InsertOne(cfg Task) error { + if _, err := coll(nil).InsertOne(cfg); err != nil { + return err + } + return nil +} + +// 添加任务记录 +func AddTaskRecord(uid uint64) (taskRecord *Task, err error) { + now := time.Now() + receiveTime := timeutil.BeginningOfDay(now) + taskRecord = &Task{ + UID: uid, + SignRecord: []primitive.ObjectID{}, + SignTime: receiveTime, + JewelBoxRecord: []primitive.ObjectID{}, + JewelBoxReceiveTime: receiveTime, + ConsumerFeedbackRecord: []primitive.ObjectID{}, + ConsumerFeedbackReceiveTime: receiveTime, + CreateTime: now, + } + if _, err = coll(nil).InsertOne(&taskRecord); err != nil { + return nil, err + } + return +} + +func InsertTask(t *db.MongoTool, filter bson.M, update bson.M) error { + var item *Task + if err := coll(t).FindOneAndUpsert(&item, filter, bson.M{"$set": update}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertTask", table, "FindOneAndUpsert", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + return nil +} + +// CurrentCountUpdate 评论值更新 +func CurrentCountUpdate(t *db.MongoTool, filter bson.M) error { + _, err := coll(t).UpdateOne(filter, bson.M{"$inc": bson.M{"count": 1}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CurrentCountUpdate", models.Task, "UpdateOne", err), + log.Any("filter", filter), + ) + return err + } + return nil +} + +func GetByUID(uid uint64) (*Task, error) { + var out Task + if err := coll(nil).FindOne(&out, bson.M{"uid": uid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByUID", models.Task, "FindOne", err), + log.Any("uid", uid), + ) + return nil, err + } + if out.ID.IsZero() { + return nil, nil + } + return &out, nil +} + +// SignUpdate 签到更新 +func SignUpdate(t *db.MongoTool, uid uint64, taskID primitive.ObjectID) error { + update := bson.M{ + "$set": bson.M{"signTime": time.Now()}, + "$addToSet": bson.M{"signRecord": taskID}, + } + result, err := coll(t).UpdateOne(bson.M{"uid": uid}, update) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "SignUpdate", models.Task, "UpdateOne", err), + log.Any("uid", uid), + ) + return err + } + if result.ModifiedCount == 0 { + err = errors.New("task UpdateOne ModifiedCount err") + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "SignUpdate", models.Task, "result.ModifiedCount == 0", err), + log.Any("uid", uid), + log.Any("addToSet", taskID), + ) + return err + } + return nil +} + +// 活跃值更新 +func CurrentValueUpdate(t *db.MongoTool, uid uint64, currentValue int64) error { + result, err := coll(t).UpdateOne(bson.M{"uid": uid}, bson.M{"$inc": bson.M{"currentValue": currentValue}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CurrentValueUpdate", models.Task, "UpdateOne", err), + log.Any("uid", uid), + log.Any("currentValue", currentValue), + ) + return err + } + if result.ModifiedCount == 0 { + err = errors.New("task UpdateOne ModifiedCount err") + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CurrentValueUpdate", models.Task, "result.ModifiedCount == 0", err), + log.Any("uid", uid), + log.Any("currentValue", currentValue), + ) + return err + } + return nil +} + +// 签到重置 +func SignReset(uid uint64) (out *Task, err error) { + now := timeutil.BeginningOfDay(time.Now()) + if err = coll(nil).FindOneAndUpdate(&out, bson.M{"uid": uid}, bson.M{ + "$set": bson.M{ + "currentValue": 0, + "signTime": now, + "jewelBoxRecord": []primitive.ObjectID{}, + "jewelBoxReceiveTime": now, + "signRecord": []primitive.ObjectID{}, + }, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "SignReset", models.Task, "FindOneAndUpdate", err), + log.Any("uid", uid), + ) + return + } + return +} + +// 消费返利重置 +func ConsumerFeedbackReset(uid uint64) (out *Task, err error) { + now := timeutil.BeginningOfDay(time.Now()) + err = coll(nil).FindOneAndUpdate(&out, bson.M{"uid": uid}, bson.M{ + "$set": bson.M{ + "consumerFeedbackRecord": []primitive.ObjectID{}, + "consumerFeedbackReceiveTime": now, + }, + }) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ConsumerFeedbackReset", models.Task, "FindOneAndUpdate", err), + log.Any("uid", uid), + ) + return + } + return +} + +// 充值有礼重置 +func ChessRechargePoliteReset(uid uint64) (out *Task, err error) { + now := timeutil.BeginningOfDay(time.Now()) + if err = coll(nil).FindOneAndUpdate(&out, bson.M{"uid": uid}, bson.M{ + "$set": bson.M{ + "chessRechargePoliteRecord": []primitive.ObjectID{}, + "chessRechargePoliteReceiveTime": now, + }, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ConsumerFeedbackReset", models.Task, "FindOneAndUpdate", err), + log.Any("uid", uid), + ) + return + } + return +} + +// 福利记录更新 +func BoonRecordUpdate(t *db.MongoTool, uid uint64, taskID primitive.ObjectID, configType ConfigType) error { + update := bson.M{ + "$set": bson.M{"signTime": time.Now()}, + } + switch configType { + case Sign: + update["$addToSet"] = bson.M{"signRecord": taskID} + update["$set"] = bson.M{"signTime": time.Now()} + case JewelBox: + update["$addToSet"] = bson.M{"jewelBoxRecord": taskID} + update["$set"] = bson.M{"jewelBoxReceiveTime": time.Now()} + case ConsumerFeedback: + update["$addToSet"] = bson.M{"consumerFeedbackRecord": taskID} + update["$set"] = bson.M{"consumerFeedbackReceiveTime": time.Now()} + } + result, err := coll(t).UpdateOne(bson.M{"uid": uid}, update) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "SignUpdate", models.Task, "UpdateOne", err), + log.Any("uid", uid), + ) + return err + } + if result.ModifiedCount == 0 { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail", "SignUpdate", models.Task, "result.ModifiedCount == 0"), + log.Any("uid", uid), + log.Any("addToSet", taskID), + ) + return errors.New("task UpdateOne ModifiedCount err") + } + return nil +} + +// 是否完成一次性任务 +func IsOnceTaskComplete(t *db.MongoTool, uid uint64, taskId primitive.ObjectID) (bool, error) { + var tsk Task + if err := coll(t).FindOne(&tsk, bson.M{"uid": uid, "type": OnceTask}); err != nil { + return false, err + } + if tsk.ID.IsZero() { + return false, nil + } + for _, v := range tsk.OnceTask { + if taskId == v { + return true, nil + } + } + return false, nil +} + +// IsDownloadTaskComplete 是否完成一次性任务 - 下载任务 +func IsDownloadTaskComplete(t *db.MongoTool, uid uint64, taskId primitive.ObjectID) (int, error) { + var tsk Task + if err := coll(t).FindOne(&tsk, bson.M{"uid": uid, "taskId": taskId}); err != nil { + return 0, err + } + if tsk.ID.IsZero() { + return 0, nil + } + + return tsk.Status, nil +} + +func CompleteOnceTask(t *db.MongoTool, uid uint64, taskId primitive.ObjectID) error { + var tsk Task + if err := coll(t).FindOne(&tsk, bson.M{"uid": uid, "type": OnceTask}); err != nil { + return err + } + now := time.Now() + if tsk.ID.IsZero() { + receiveTime := timeutil.BeginningOfDay(now) + _, err := coll(t).InsertOne(&Task{ + UID: uid, + Type: OnceTask, + SignRecord: []primitive.ObjectID{}, + SignTime: receiveTime, + JewelBoxRecord: []primitive.ObjectID{}, + JewelBoxReceiveTime: receiveTime, + ConsumerFeedbackRecord: []primitive.ObjectID{}, + ConsumerFeedbackReceiveTime: receiveTime, + OnceTask: []primitive.ObjectID{taskId}, + CreateTime: now, + }) + return err + } + for _, v := range tsk.OnceTask { + if v == taskId { // 已经完成了 + return nil + } + } + if len(tsk.OnceTask) == 0 { + _, err := coll(t).UpdateOne(bson.M{"_id": tsk.ID}, bson.M{"$set": bson.M{"onceTask": []primitive.ObjectID{taskId}}}) + return err + } + _, err := coll(t).UpdateOne(bson.M{"_id": tsk.ID}, bson.M{"$addToSet": bson.M{"onceTask": taskId}}) + return err +} + +func GetUserRecordByTime(uid uint64, taskId primitive.ObjectID, record time.Time) (*Task, error) { + var out *Task + if err := coll(nil).FindOne(&out, bson.M{"uid": uid, "taskId": taskId, "recordTime": record}); err != nil { + log.Error( + fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetUserRecordByTime", models.Task, "FindOne", err), + log.Any("uid", uid), + ) + return nil, err + } + return out, nil +} + +// GetUserTaskRecordByUid 查询用户任务记录 +func GetUserTaskRecordByUid(uid uint64, t ConfigType, taskId primitive.ObjectID) (*Task, error) { + var out *Task + if err := coll(nil).FindOne(&out, bson.M{"uid": uid, "type": t, "taskId": taskId}); err != nil { + log.Error( + fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetUserTaskRecordByUid", models.Task, "FindOne", err), + log.Any("uid", uid), + ) + return nil, err + } + return out, nil +} + +// GetUserTask 查询用户任务记录 +func GetUserTask(uid uint64, taskId primitive.ObjectID) (Task, error) { + var out Task + if err := coll(nil).FindOne(&out, bson.M{"uid": uid, "taskId": taskId}); err != nil { + log.Error( + fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetUserTaskRecordByUid", models.Task, "FindOne", err), + log.Any("uid", uid), + ) + return out, err + } + return out, nil +} + +func UpdateStatusById(t *db.MongoTool, id primitive.ObjectID, status int) (out *Task, err error) { + update := bson.M{"$set": bson.M{"updateTime": time.Now(), "status": status}, "$inc": bson.M{"stage": 1}} + if err = coll(t).FindOneAndUpdate(&out, bson.M{"_id": id}, update); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateStatusById", models.Task, "UpdateStatusById", err), + log.Any("id", id.Hex()), + ) + return + } + return +} + +func UpsertOne(filter, updateDate bson.M) (err error) { + if _, err = coll(nil).UpsertOne(filter, updateDate); err != nil { + log.Error("UpsertOne error", + log.Any("filter", filter), + log.Any("updateDate", updateDate), + log.E(err)) + return err + } + return +} + +// GetSignRecord 取用户签到任务记录 +func GetSignRecord(uid uint64) (*Task, error) { + var out *Task + if err := coll(nil).FindOne(&out, bson.M{"uid": uid, "type": Sign}); err != nil { + log.Error( + fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetUserRecordByType", models.Task, "FindOne", err), + log.Any("uid", uid), + ) + return nil, err + } + return out, nil +} + +// TaskSignUpdate 更新用户签到记录 +func TaskSignUpdate(t *db.MongoTool, uid uint64, taskID primitive.ObjectID) error { + filter := bson.M{ + "uid": uid, + "type": Sign, + } + update := bson.M{"$addToSet": bson.M{"signRecord": taskID}} + result, err := coll(t).UpdateOne(filter, update) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RecordUpdate", models.Task, "UpdateOne", err), + log.Any("uid", uid), + ) + return err + } + if result.ModifiedCount == 0 { + err = errors.New("task sign UpdateOne ModifiedCount err") + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RecordUpdate", models.Task, "result.ModifiedCount == 0", err), + log.Any("uid", uid), + log.Any("taskID", taskID), + log.Any("configType:sign", Sign), + ) + return err + } + return nil +} + +// TaskSignInit 任务记录初始化 +func TaskSignInit(uid uint64) (*Task, error) { + now := time.Now() + receiveTime := timeutil.BeginningOfDay(now) + taskRecord := &Task{ + UID: uid, + SignRecord: []primitive.ObjectID{}, + SignTime: receiveTime, + JewelBoxRecord: []primitive.ObjectID{}, + JewelBoxReceiveTime: receiveTime, + ConsumerFeedbackRecord: []primitive.ObjectID{}, + ConsumerFeedbackReceiveTime: receiveTime, + TaskType: 0, + Type: Sign, + CreateTime: now, + } + if _, err := coll(nil).InsertOne(&taskRecord); err != nil { + return nil, err + } + return taskRecord, nil +} + +// TaskSignReset 任务重置 +func TaskSignReset(uid uint64) (*Task, error) { + var ( + out *Task + now = timeutil.BeginningOfDay(time.Now()) + ) + if err := coll(nil).FindOneAndUpdate(&out, bson.M{"uid": uid, "type": Sign}, bson.M{ + "$set": bson.M{ + "signRecord": []primitive.ObjectID{}, + "signTime": now, + }, + }, options.FindOneAndUpdate().SetReturnDocument(options.After)); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "TaskReset", models.Task, "FindOneAndUpdate", err), + log.Any("uid", uid), + log.Any("configType:sign", Sign), + ) + return nil, err + } + return out, nil +} diff --git a/models/v/taskrecordmod/type.go b/models/v/taskrecordmod/type.go new file mode 100644 index 0000000..a9d9980 --- /dev/null +++ b/models/v/taskrecordmod/type.go @@ -0,0 +1,74 @@ +package taskrecordmod + +import ( + "91porn-server/common/db" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +type ConfigType int + +const ( + OldSign ConfigType = iota + 1 // 1:签到福利 + JewelBox // 2:宝箱福利 + ConsumerFeedback // 3:消费回馈(站群) + OnceTask // 4:新手一次福利 + DailyTask // 5:日常福利 + GrowTask // 6:成长任务 + Sign // 7:新的签到福利 +) + +const ( + UnReceived = 1 // 未领取 + Received = 2 // 已领取 +) + +type Task struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档ID + UID uint64 `json:"uid" bson:"uid"` // 用户ID + CurrentValue int64 `json:"currentValue" bson:"currentValue"` // 活跃值 + TaskID primitive.ObjectID `json:"taskId" bson:"taskId"` // 任务ID + Type ConfigType `json:"type" bson:"type"` // 任务分类 + TaskType TaskType `json:"taskType" bson:"taskType"` // 任务类型 + Status int `json:"status" bson:"status"` // 领取状态 + Count int `json:"count" bson:"count"` // 领取次数 + JewelBoxRecord []primitive.ObjectID `json:"jewelBoxRecord" bson:"jewelBoxRecord"` // 宝箱领取记录 + JewelBoxReceiveTime time.Time `json:"jewelBoxReceiveTime" bson:"jewelBoxReceiveTime"` // 宝箱领取时间 + SignRecord []primitive.ObjectID `json:"signRecord" bson:"signRecord"` // 签到记录 + SignTime time.Time `json:"signTime" bson:"signTime"` // 签到时间 + Stage int `json:"stage" bson:"stage"` // 当前的阶段(只有成长任务使用) + ConsumerFeedbackRecord []primitive.ObjectID `json:"consumerFeedbackRecord" bson:"consumerFeedbackRecord"` // 消费返利领取记录 + ConsumerFeedbackReceiveTime time.Time `json:"consumerFeedbackReceiveTime" bson:"consumerFeedbackReceiveTime"` // 消费返利领取时间 + OnceTask []primitive.ObjectID `json:"onceTask" bson:"onceTask"` // 一次性任务领奖记录 + RecordTime time.Time `json:"recordTime" bson:"recordTime"` // 记录时间 + UpdateTime time.Time `json:"updateTime" bson:"updateTime"` // 更新时间 + CreateTime time.Time `json:"createTime" bson:"createTime"` // 创建时间 +} + +// HasReceived 是否领取福利 +func (t *Task) HasReceived(id primitive.ObjectID, configType ConfigType) bool { + switch configType { + case Sign: + for _, v := range t.SignRecord { + if v == id { + return true + } + } + case JewelBox: + for _, v := range t.JewelBoxRecord { + if v == id { + return true + } + } + case ConsumerFeedback: + for _, v := range t.ConsumerFeedbackRecord { + if v == id { + return true + } + } + } + return false +} diff --git a/models/v/taskrecordmod/web.go b/models/v/taskrecordmod/web.go new file mode 100644 index 0000000..0da3cb2 --- /dev/null +++ b/models/v/taskrecordmod/web.go @@ -0,0 +1 @@ +package taskrecordmod diff --git a/models/v/tonemod/list.go b/models/v/tonemod/list.go new file mode 100644 index 0000000..9d71b33 --- /dev/null +++ b/models/v/tonemod/list.go @@ -0,0 +1,56 @@ +package tonemod + +import ( + "fmt" + + "91porn-server/common/log" + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Matcher = pageopt.Matcher + +// EnableMatch +type EnableMatch struct { + Enable *bool +} + +func (i *EnableMatch) New() Matcher { + return pageopt.NewAssignMatch("enable", i.Enable) +} + +// List +func List(sort D, skip, limit int64, matchers ...Matcher) ([]Tone, error) { + filter := pageopt.MergeM(matchers) + opt := &options.FindOptions{} + if len(sort) != 0 { + opt.SetSort(sort) + } + opt.SetSkip(skip) + opt.SetLimit(limit) + toneList := make([]Tone, 0, limit) + if err := coll(nil).Find(&toneList, filter, opt); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "List", table, "Find", err), + log.Any("sort", sort), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("matchers", matchers), + ) + return nil, err + } + return toneList, nil +} + +// Count +func Count(matchers ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchers) + count, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Count", table, "Count", err), + log.Any("matchers", matchers), + ) + return 0, err + } + return count, nil +} diff --git a/models/v/tonemod/tone.go b/models/v/tonemod/tone.go new file mode 100644 index 0000000..aaba0b5 --- /dev/null +++ b/models/v/tonemod/tone.go @@ -0,0 +1,77 @@ +package tonemod + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.Tone + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "types", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "sortKey", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// UpdateTo +func UpdateTo(id ObjectID, doc ToneDoc) error { + now := time.Now() + doc.UpdatedAt = &now + update, err := common.ToBsonM(doc) + if err != nil { + return fmt.Errorf("coll:%s UpdateTo ToBsonM fail, error:%+v ", models.Tone, err) + } + if _, err := coll(nil).UpdateOne(M{"_id": id}, bson.M{"$set": update}); err != nil { + return fmt.Errorf("coll:%s UpdateTo fail, error:%+v id:%+v update:%+v", models.Tone, err, id, update) + } + return nil +} + +// TonePages TonePages +func TonePages(sort D, skip int64, limit int64) (page TonePage, err error) { + list, err := List(sort, skip, limit) + if err != nil { + return TonePage{}, err + } + count, err := Count() + if err != nil { + return TonePage{}, err + } + return TonePage{ + int(count), + list, + }, nil +} diff --git a/models/v/tonemod/toneValue.go b/models/v/tonemod/toneValue.go new file mode 100644 index 0000000..4b0987e --- /dev/null +++ b/models/v/tonemod/toneValue.go @@ -0,0 +1,22 @@ +package tonemod + +import "encoding/json" + +// ToneValue +type ToneValue struct { + OfficialRecomPlayCount int64 `json:"officialRecomPlayCount"` + GoldCoinAreaPayCount int64 `josn:"goldCoinAreaPayCount"` + LatestUploadPlayCount int64 `json:"latestUploadPlayCount"` + MostLikesPlayCount int64 `json:"mostLikesPlayCount"` + PlayAtMostPlayCount int64 `json:"playAtMostPlayCount"` + MostCommentsPlayCount int64 `json:"mostCommentsPlayCount"` + FreeAreaPlayCount int64 `json:"freeAreaPlayCount"` +} + +func (v ToneValue) MarshalBinary() ([]byte, error) { + return json.Marshal(v) +} + +func (v *ToneValue) UnmarshalBinary(b []byte) error { + return json.Unmarshal(b, v) +} diff --git a/models/v/tonemod/type.go b/models/v/tonemod/type.go new file mode 100644 index 0000000..53903b3 --- /dev/null +++ b/models/v/tonemod/type.go @@ -0,0 +1,54 @@ +package tonemod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +// M M from Map +type M = bson.M + +// D D from Doc +type D = bson.D + +// A A from Array +type A = []interface{} + +type TonePage struct { + Total int `json:"total" bson:"total"` + List []Tone `json:"list" bson:"list"` +} + +// 音色最热模型asd +type Tone struct { + ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` //主题名 + Types string `json:"types" bson:"types"` //类型 + Cover string `json:"cover" bson:"cover"` //封面 + SortKey int `json:"sortKey" bson:"sortKey"` //排序 + Enable bool `json:"enable" bson:"enable"` //使能 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //刷新时间 +} + +type ToneDoc struct { + ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + Name *string `json:"name" bson:"name,omitempty"` //主题名 + Types *string `json:"types" bson:"types,omitempty"` //类型 + Cover *string `json:"cover" bson:"cover,omitempty"` //封面 + SortKey *int `json:"sortKey" bson:"sortKey,omitempty"` //排序 + Enable *bool `json:"enable" bson:"enable,omitempty"` //使能 + CreatedAt *time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt *time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //刷新时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/tonerecomod/toneRecom.go b/models/v/tonerecomod/toneRecom.go new file mode 100644 index 0000000..aadbd04 --- /dev/null +++ b/models/v/tonerecomod/toneRecom.go @@ -0,0 +1,108 @@ +package tonerecomod + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type ThemeType = string + +const ( + MostLikes ThemeType = "mostLikes" //最多点赞 + PlayAtMost ThemeType = "playAtMost" //最多播放 + MostComments ThemeType = "mostComments" //最多评论 + OfficialRecom ThemeType = "officialRecom" //官方推荐 + GoldCoinArea ThemeType = "goldCoinArea" //金币专区 + LatestUpload ThemeType = "latestUpload" //最新上传 + FreeArea ThemeType = "freeArea" //免费专区 +) + +var mdb *db.MongoDB + +const table = models.ToneRecom + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "vid", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "sortKey", Value: -1}}, //大的排前面 + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", models.ToneRecom, err)) + } +} + +// Insert ()插入消息记录 +func InsertMany(offiRecoms []OffiRecomDoc) error { + now := time.Now() + docs := make([]interface{}, len(offiRecoms)) + for i, offiRecom := range offiRecoms { + offiRecom.CreatedAt = &now + m, err := common.ToBsonM(offiRecom) + if err != nil { + return fmt.Errorf("tone OffiRecom InsertMany ToBsonM fail, error:%+v ", err) + } + docs[i] = m + } + if _, err := coll(nil).InsertMany(docs); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertMany", table, "InsertMany", err), + log.Any("offiRecoms", offiRecoms), + ) + return err + } + return nil +} + +// Delete Delete +func DeleteMany(idArray []ObjectID) (err error) { + if _, err = coll(nil).DeleteMany(M{"_id": M{"$in": idArray}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteMany", table, "DeleteMany", err), + log.Any("idArray", idArray), + ) + return + } + return nil +} + +func GetOffiRecomList(skip int64, limit int64, enable bool) ([]ObjectID, error) { + opt := (&options.FindOptions{}). + SetSort(D{{Key: "sortKey", Value: -1}}). + SetSkip(skip). + SetLimit(limit) + filter := M{ + "enable": enable, + } + offiRecomList := make([]OffiRecom, limit) + if err := coll(nil).Find(&offiRecomList, filter, opt); err != nil { + return nil, err + } + vidList := make([]ObjectID, len(offiRecomList)) + for i, v := range offiRecomList { + vidList[i] = v.VID + } + return vidList, nil +} diff --git a/models/v/tonerecomod/type.go b/models/v/tonerecomod/type.go new file mode 100644 index 0000000..24466be --- /dev/null +++ b/models/v/tonerecomod/type.go @@ -0,0 +1,50 @@ +package tonerecomod + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// M M from Map +type M = bson.M + +// D D from Doc +type D = bson.D + +type ObjectID = primitive.ObjectID + +// A A from Array +type A = bson.A + +type VidInfo = vidmod.VideoBase + +type OffiRecomPage struct { + Total int `json:"total" bson:"total"` + List []VidInfo `json:"list" bson:"list"` //封面 +} + +type OffiRecom struct { + ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + VID primitive.ObjectID `json:"vid,omitempty" bson:"vid,omitempty"` + SortKey int `json:"sortKey" bson:"sortKey,omitempty"` //排序 + Enable bool `json:"enable" bson:"enable,omitempty"` //使能 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 +} + +type OffiRecomDoc struct { + ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + VID primitive.ObjectID `json:"vid,omitempty" bson:"vid,omitempty"` + SortKey *int `json:"sortKey" bson:"sortKey,omitempty"` //排序 + Enable *bool `json:"enable" bson:"enable,omitempty"` //使能 + CreatedAt *time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/tonerecomod/web.go b/models/v/tonerecomod/web.go new file mode 100644 index 0000000..b8ae869 --- /dev/null +++ b/models/v/tonerecomod/web.go @@ -0,0 +1,63 @@ +package tonerecomod + +import ( + "fmt" + + "91porn-server/common/log" + "91porn-server/models" +) + +// OffiRecomPages OffiRecomPages +func OffiRecomPages(sort D, skip int64, limit int64) (page OffiRecomPage, err error) { + pipeline := []M{ + { + "$facet": M{ + "total": A{M{"$count": "total"}}, + "list": A{ + M{"$sort": sort}, + M{"$skip": skip}, + M{"$limit": limit}, + M{ + "$lookup": M{ + "from": models.VideoInfo, + "localField": "vid", + "foreignField": "_id", + "as": "video_info", + }, + }, + M{ + "$unwind": "$video_info", + }, + M{ + "$set": M{ + "video_info.sortKey": "$sortKey", + "video_info.enable": "$enable", + }, + }, + M{ + "$replaceWith": "$video_info", + }, + M{ + "$lookup": M{ + "from": models.Tag, + "localField": "tags", + "foreignField": "_id", + "as": "tags", + }, + }, + }}, + }, + { + "$project": M{ + "total": M{"$arrayElemAt": A{"$total.total", 0}}, + "list": 1, + }, + }, + } + coll := mdb.Coll(models.ToneRecom) + if err = coll.AggregateDecode(&page, pipeline); err != nil { + log.ZapLog.Error(fmt.Sprintf("coll:%s Page error:%+v:", models.ToneRecom, err)) + return + } + return +} diff --git a/models/v/txnactmod/app.go b/models/v/txnactmod/app.go new file mode 100644 index 0000000..a0c793d --- /dev/null +++ b/models/v/txnactmod/app.go @@ -0,0 +1,25 @@ +package txnactmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// EchgAccount 银行账户 +type TransactionActRes struct { + ID primitive.ObjectID `json:"id" bson:"_id"` + ActName string `json:"actName" bson:"actName"` //账户姓名 + Act string ` json:"act" bson:"act"` //账户号 + BankCode string ` json:"bankCode" bson:"bankCode"` //银行代号 + CardType string `json:"cardType" bson:"cardType"` //卡类型 +} + +// EchgAccount 银行账户 +type TransactionActSelector struct { + ActName *string `json:"actName,omitempty" bson:"actName,omitempty"` //账户姓名 + Act *string ` json:"act,omitempty" bson:"act,omitempty"` //账户号 + BankCode *string ` json:"bankCode,omitempty" bson:"bankCode,omitempty"` //银行代号 + CardType *string `json:"cardType,omitempty" bson:"cardType,omitempty"` //卡类型 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt" swaggertype:"-"` +} diff --git a/models/v/txnactmod/txnact.go b/models/v/txnactmod/txnact.go new file mode 100644 index 0000000..5366aa9 --- /dev/null +++ b/models/v/txnactmod/txnact.go @@ -0,0 +1,144 @@ +package txnactmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func Init() { + mdb = db.Init(table) + initIndex() +} + +const table = models.TransactionAct + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "act", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("user model set index err ==>[%+v]", err)) + } +} + +// InsertWithdrawChannel 插入一条数据 +func Insert(t *TransactionAct) error { + t.UpdatedAt = time.Now() + t.CreatedAt = t.UpdatedAt + if _, err := coll(nil).InsertOne(t); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err)) + return err + } + return nil +} + +// DeleteWithdrawChannel delete +func DeleteByID(id primitive.ObjectID) (int64, error) { + result, err := coll(nil).DeleteOne(bson.M{"_id": id}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteOne", err), + log.Any("id", id), + ) + return 0, err + } + return result.DeletedCount, err +} + +func FindOneByID(id primitive.ObjectID) (TransactionAct, error) { + ta := TransactionAct{} + if err := coll(nil).FindOne(&ta, bson.M{"_id": id}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneByID", table, "FindOne", err), + log.Any("id", id), + ) + return ta, err + } + return ta, nil +} + +// UpdateWithdrawChannel 金币配置更新 +func Update(id primitive.ObjectID, set *TransactionActSelector) (int64, error) { + set.UpdatedAt = time.Now() + result, err := coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("id", id), + ) + return 0, err + } + return result.ModifiedCount, nil +} + +// GetWithdrawChannels 条件获取支付渠道列表 +func FindManyByActType(uid uint64, actType ActType) ([]*TransactionActRes, error) { + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "updatedAt", Value: -1}}) + back := make([]*TransactionActRes, 0) + if err := coll(nil).Find(&back, bson.M{"uid": uid, "aType": actType}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("uid", uid), + log.Any("actType", actType), + ) + return nil, err + } + return back, nil +} + +// 条件获取支付渠道列表 +func FindMany(query Query) ([]*TransactionAct, error) { + opts := options.FindOptions{} + back := make([]*TransactionAct, 0) + b, _ := bson.Marshal(query) + f := make(bson.M) + if err := bson.Unmarshal(b, &f); err != nil { + return nil, err + } + if err := coll(nil).Find(&back, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindMany", table, "Find", err), + log.Any("query", query), + ) + return nil, err + } + return back, nil +} + +// 条件获取支付渠道通道 +func FindOne(query Query) (TransactionAct, error) { + back := TransactionAct{} + b, _ := bson.Marshal(query) + f := make(bson.M) + if err := bson.Unmarshal(b, &f); err != nil { + return back, err + } + if err := coll(nil).FindOne(&back, f); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOne", table, "FindOne", err), + log.Any("query", query), + ) + return back, err + } + return back, nil +} diff --git a/models/v/txnactmod/type.go b/models/v/txnactmod/type.go new file mode 100644 index 0000000..38051bd --- /dev/null +++ b/models/v/txnactmod/type.go @@ -0,0 +1,32 @@ +package txnactmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ActType int + +const ( + _ ActType = iota + Alipay + Bank +) + +var mdb *db.MongoDB + +// TransactionAct 银行账户 +type TransactionAct struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + UID uint64 `json:"uid" bson:"uid"` + ActName string `json:"actName" bson:"actName"` //账户姓名 + Act string `json:"act" bson:"act"` //账户号 + BankCode string `json:"bankCode" bson:"bankCode"` //银行代号 + CardType string `json:"cardType" bson:"cardType"` //卡类型 + AType ActType `json:"aType" bson:"aType"` //账户类型 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} diff --git a/models/v/txnactmod/web.go b/models/v/txnactmod/web.go new file mode 100644 index 0000000..cb92cd6 --- /dev/null +++ b/models/v/txnactmod/web.go @@ -0,0 +1,23 @@ +package txnactmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Edit struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + ActName *string `json:"actName,omitempty" bson:"actName,omitempty"` //账户姓名 + Act *string `json:"act,omitempty" bson:"act,omitempty"` //账户号 + CardType *string `json:"cardType,omitempty" bson:"cardType,omitempty"` //卡类型 + AType *ActType `json:"aType,omitempty" bson:"aType,omitempty"` //账户类型 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +type Query struct { + ActName *string `json:"actName,omitempty" bson:"actName,omitempty"` //账户姓名 + Act *string `json:"act,omitempty" bson:"act,omitempty"` //账户号 + CardType *string `json:"cardType,omitempty" bson:"cardType,omitempty"` //卡类型 + AType *ActType `json:"aType,omitempty" bson:"aType,omitempty"` //账户类型 +} diff --git a/models/v/txnmod/discStat.go b/models/v/txnmod/discStat.go new file mode 100644 index 0000000..0d2c1bb --- /dev/null +++ b/models/v/txnmod/discStat.go @@ -0,0 +1,190 @@ +package txnmod + +import ( + "fmt" + "strings" + "time" + + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/common/ysinterface/disc" + "91porn-server/models/commod" +) + +type DiscSeqe = commod.DiscSeqe + +type DistrictStatKey struct { + DiscSeqe `bson:",inline"` //商区码 + SysType string `bson:"sysType"` //系统类型 iOS Android +} + +func (d DistrictStatKey) GetDiscCode() string { + return strings.ToUpper(d.DistrictCode) +} + +func (d DistrictStatKey) GetPromSeqe() string { + return d.PromSeqe +} + +func (d DistrictStatKey) GetSysType() string { + return d.SysType +} + +func (d DistrictStatKey) String() string { + if d.DiscSeqe.String() == "" { + return "" + } + return strings.ToUpper(strings.Join([]string{d.DiscSeqe.String(), d.SysType}, "-")) +} + +type DistrictStater = disc.DistrictStater + +// DiscSeqeTransCount +func DiscSeqeTransCount(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + filter := pageopt.MergeM(mats) + list := []DistrictStatKey{} + if err := coll(nil).Find(&list, filter); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DiscSeqeTransCount", table, "Find", err)) + return nil, err + } + m := make(map[DistrictStater]int64) + for _, v := range list { + if v.String() != "" { + m[v] += 1 + } + } + return m, nil +} + +// DiscSeqeTransAmount +func DiscSeqeTransAmount(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + filter := pageopt.MergeM(mats) + list := []struct { + DistrictStatKey `bson:",inline"` //商区码 + Amount int64 `bson:"amount"` + }{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater]int64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v.DistrictStatKey] += v.Amount + } + } + return m, nil +} + +// DiscTransUIDSMap +func DiscTransUIDSMap(start, end time.Time, mats ...Matcher) (map[DistrictStater][]uint64, error) { + mats = append(mats, (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + filter := pageopt.MergeM(mats) + list := []struct { + DistrictCode string `json:"districtCode" bson:"districtCode"` //商区码 + UID uint64 `bson:"uid"` + }{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater][]uint64, len(list)) + for _, v := range list { + key := DistrictStatKey{ + DiscSeqe: DiscSeqe{DistrictCode: v.DistrictCode}, + } + if key.String() != "" { + m[key] = append(m[key], v.UID) + } + } + return m, nil +} + +// DiscSeqeTransUIDSMap +func DiscSeqeTransUIDSMap(start, end time.Time, mats ...Matcher) (map[DistrictStater][]uint64, error) { + mats = append(mats, (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + filter := pageopt.MergeM(mats) + list := []struct { + DistrictStatKey `bson:",inline"` //商区码 + UID uint64 `bson:"uid"` + }{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater][]uint64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v.DistrictStatKey] = append(m[v.DistrictStatKey], v.UID) + } + } + return m, nil +} + +// DiscSeqeTransTaxAmount +func DiscSeqeTransTaxAmount(start, end time.Time, mats ...Matcher) (map[DistrictStater]float64, error) { + mats = append(mats, (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + filter := pageopt.MergeM(mats) + list := []struct { + DistrictStatKey `bson:",inline"` //商区码 + TaxAmount float64 `bson:"taxAmount"` //税额 + }{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater]float64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v.DistrictStatKey] += v.TaxAmount + } + } + return m, nil +} + +// DiscSeqeBuyVipCount 时间内购买VIP次数 +func DiscSeqeBuyVipCount(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + payVIP := PayVIP.Key() + mats = append(mats, (&TranTypeMatch{&payVIP}).New()) + return DiscSeqeTransCount(start, end, mats...) +} + +// DiscSeqeBuyVipCount 时间内购买VIP金额 +func DiscSeqeBuyVipAmount(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + payVIP := PayVIP.Key() + mats = append(mats, (&TranTypeMatch{&payVIP}).New()) + return DiscSeqeTransAmount(start, end, mats...) +} + +// DiscBuyVipUIDSMap 时间内购买用户列表 +func DiscBuyVipUIDSMap(start, end time.Time, mats ...Matcher) (map[DistrictStater][]uint64, error) { + payVIP := PayVIP.Key() + mats = append(mats, (&TranTypeMatch{&payVIP}).New()) + return DiscTransUIDSMap(start, end, mats...) +} + +// DiscSeqeBuyVipUIDSMap 时间内购买用户列表 +func DiscSeqeBuyVipUIDSMap(start, end time.Time, mats ...Matcher) (map[DistrictStater][]uint64, error) { + payVIP := PayVIP.Key() + mats = append(mats, (&TranTypeMatch{&payVIP}).New()) + return DiscSeqeTransUIDSMap(start, end, mats...) +} + +// DiscSeqeBuyVidCount 时间内购买VID次数 +func DiscSeqeBuyVidCount(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + payVID := PayVID.Key() + mats = append(mats, (&TranTypeMatch{&payVID}).New()) + return DiscSeqeTransCount(start, end, mats...) +} + +// DiscSeqeBuyVidAmount 时间内购买VID金额 +func DiscSeqeBuyVidAmount(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + payVID := PayVID.Key() + mats = append(mats, (&TranTypeMatch{&payVID}).New()) + return DiscSeqeTransAmount(start, end, mats...) +} + +// DiscSeqeWorksIncomeTaxAmount 时间内购买VID税额 +func DiscSeqeWorksIncomeTaxAmount(start, end time.Time, mats ...Matcher) (map[DistrictStater]float64, error) { + worksIncome := WorksIncome.Key() + mats = append(mats, (&TranTypeMatch{&worksIncome}).New()) + return DiscSeqeTransTaxAmount(start, end, mats...) +} diff --git a/models/v/txnmod/fund_ledger.go b/models/v/txnmod/fund_ledger.go new file mode 100644 index 0000000..ade3adc --- /dev/null +++ b/models/v/txnmod/fund_ledger.go @@ -0,0 +1,214 @@ +package txnmod + +import ( + "bytes" + "errors" + "sort" + "time" + + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const ledgerQueryTimeout = 5 * time.Second +const maxLedgerWindow int64 = 100000 + +var ledgerSort = bson.D{{Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}} + +// Fund records have no global time index. Only a concrete user's ledger can +// include them without introducing a collection scan into the admin listing. +func hasLedgerUID(filter bson.M) bool { + switch uid := filter["uid"].(type) { + case uint64: + return uid > 0 + case int64: + return uid > 0 + case int: + return uid > 0 + case int32: + return uid > 0 + } + return false +} + +// Normalize at read time: no historical transaction or wallet is rewritten. +// The indexable user/time match precedes projection and derived-field filters. +func fundLedgerPipeline(filter bson.M, ordered bool) []bson.M { + if !hasLedgerUID(filter) { + return nil + } + if kind, ok := filter["tranType"].(string); ok && kind != AiGirlfriendTransferIn.Key() && kind != AiGirlfriendTransferOut.Key() { + return nil + } + match := bson.M{"uid": filter["uid"], "category": 0, "fundType": bson.M{"$in": []int{1, 2}}} + if period, ok := filter["createdAt"]; ok { + match["createdAt"] = period + } + in := bson.M{"$eq": bson.A{"$fundType", 1}} + amount := bson.M{"$cond": bson.A{in, bson.M{"$multiply": bson.A{"$amount", -1}}, "$amount"}} + kind := bson.M{"$cond": bson.A{in, AiGirlfriendTransferIn.Key(), AiGirlfriendTransferOut.Key()}} + pipeline := []bson.M{{"$match": match}} + if ordered { + pipeline = append(pipeline, bson.M{"$sort": ledgerSort}) + } + pipeline = append(pipeline, + bson.M{"$project": bson.M{ + "_id": 1, "uid": 1, "createdAt": 1, + "purchaseOrder": "$_id", "amount": amount, + "actualAmount": bson.M{"$toDouble": amount}, + "integral": bson.M{"$literal": 0}, + "tranType": kind, + "tranTypeInt": bson.M{"$cond": bson.A{in, AiGirlfriendTransferIn, AiGirlfriendTransferOut}}, + "desc": bson.M{"$ifNull": bson.A{"$desc", kind}}, + "realAmount": bson.M{"$toDecimal": "$balance"}, + }}, + bson.M{"$match": filter}, + ) + return pipeline +} + +func mergeLedgerRows(ordinary, transfers []*TransactionLog, skip, limit int64) []*TransactionLog { + rows := append(ordinary, transfers...) + sort.SliceStable(rows, func(i, j int) bool { + if !rows[i].CreatedAt.Equal(rows[j].CreatedAt) { + return rows[i].CreatedAt.After(rows[j].CreatedAt) + } + return bytes.Compare(rows[i].ID[:], rows[j].ID[:]) > 0 + }) + if skip >= int64(len(rows)) { + return []*TransactionLog{} + } + end := int64(len(rows)) + if limit > 0 && skip+limit < end { + end = skip + limit + } + return rows[skip:end] +} + +// Read only the prefix needed from each source, then paginate the merged order. +// An explicit window and server timeout bound old monthly and export requests. +func findLedgerRows(filter bson.M, skip, limit int64) ([]*TransactionLog, error) { + if skip < 0 || limit < 0 || skip > maxLedgerWindow || limit > maxLedgerWindow || (limit > 0 && skip > maxLedgerWindow-limit) { + return nil, errors.New("ledger query window is too large; narrow the time range") + } + window := skip + limit + if limit == 0 { + window = maxLedgerWindow + 1 + } + rows := []*TransactionLog{} + pipeline := fundLedgerPipeline(filter, true) + opt := options.Find().SetSort(ledgerSort).SetMaxTime(ledgerQueryTimeout) + if !hasLedgerUID(filter) { + opt.SetSort(bson.D{{Key: "createdAt", Value: -1}}) + } + if pipeline == nil && limit > 0 { + opt.SetSkip(skip).SetLimit(limit) + } else { + opt.SetLimit(window) + } + if err := coll(nil).Find(&rows, filter, opt); err != nil { + return nil, err + } + if pipeline == nil && limit > 0 { + return rows, nil + } + transfers := []*TransactionLog{} + if pipeline != nil { + pipeline = append(pipeline, bson.M{"$limit": window}) + if err := mdb.Coll(models.FundTransferLog).Aggregate(&transfers, pipeline, options.Aggregate().SetMaxTime(ledgerQueryTimeout)); err != nil { + return nil, err + } + } + if limit == 0 && int64(len(rows)+len(transfers)) > maxLedgerWindow { + return nil, errors.New("ledger query window is too large; narrow the time range") + } + return mergeLedgerRows(rows, transfers, skip, limit), nil +} + +func countLedgerRows(filter bson.M) (int64, error) { + count, err := coll(nil).Count(filter, options.Count().SetMaxTime(ledgerQueryTimeout)) + if err != nil { + return 0, err + } + pipeline := fundLedgerPipeline(filter, false) + if pipeline == nil { + return count, nil + } + var result []struct { + Count int64 `bson:"count"` + } + pipeline = append(pipeline, bson.M{"$count": "count"}) + if err := mdb.Coll(models.FundTransferLog).Aggregate(&result, pipeline, options.Aggregate().SetMaxTime(ledgerQueryTimeout)); err != nil { + return 0, err + } + if len(result) > 0 { + count += result[0].Count + } + return count, nil +} + +func getLedgerCoinLogs(page, size uint64, filter bson.M) ([]*TransactionLog, uint64, int64, error) { + if size == 0 || size > uint64(maxLedgerWindow) { + return nil, 0, 0, errors.New("invalid ledger page size") + } + total, err := countLedgerRows(filter) + if err != nil { + return nil, 0, 0, err + } + pages := (uint64(total) + size - 1) / size + if pages == 0 { + return []*TransactionLog{}, 0, 0, nil + } + if page == 0 { + page = 1 + } + if page > pages { + page = pages + } + if page-1 > uint64(maxLedgerWindow)/size { + return nil, pages, total, errors.New("ledger query window is too large; narrow the time range") + } + rows, err := findLedgerRows(filter, int64((page-1)*size), int64(size)) + return rows, pages, total, err +} + +func FindLedgerByTime(uid uint64, start, end time.Time) ([]*TransactionLog, error) { + return findLedgerRows(bson.M{"uid": uid, "createdAt": bson.M{"$gte": start, "$lt": end}}, 0, 0) +} + +func LastLedgerTime(uid uint64, end time.Time) (TransactionLog, error) { + rows, err := findLedgerRows(bson.M{"uid": uid, "createdAt": bson.M{"$lt": end}}, 0, 1) + if err != nil || len(rows) == 0 { + return TransactionLog{}, err + } + return *rows[0], nil +} + +// These are wallet inflow/outflow totals, not recharge, creator or agent revenue. +func sumLedgerAmount(uid uint64, start, end time.Time, income bool) (float64, error) { + op := "$lt" + if income { + op = "$gt" + } + filter := bson.M{"uid": uid, "createdAt": bson.M{"$gte": start, "$lt": end}, "actualAmount": bson.M{op: 0}} + group := bson.M{"$group": bson.M{"_id": nil, "total": bson.M{"$sum": "$actualAmount"}}} + pipelines := [][]bson.M{{{"$match": filter}, group}, append(fundLedgerPipeline(filter, false), group)} + var total float64 + for i, name := range []string{table, models.FundTransferLog} { + var result []struct { + Total float64 `bson:"total"` + } + if i == 1 && !hasLedgerUID(filter) { + continue + } + if err := mdb.Coll(name).Aggregate(&result, pipelines[i], options.Aggregate().SetMaxTime(ledgerQueryTimeout)); err != nil { + return 0, err + } + if len(result) > 0 { + total += result[0].Total + } + } + return total, nil +} diff --git a/models/v/txnmod/list.go b/models/v/txnmod/list.go new file mode 100644 index 0000000..c4b03dd --- /dev/null +++ b/models/v/txnmod/list.go @@ -0,0 +1,128 @@ +package txnmod + +import ( + "91porn-server/common/log" + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Matcher = pageopt.Matcher + +// TranTypeMatch +type TranTypeMatch struct { + TransType *string +} + +func (s *TranTypeMatch) New() Matcher { + return pageopt.NewAssignMatch("tranType", s.TransType) +} + +// DistrictCodeMatch +type DistrictCodeMatch struct { + DistrictCode *string +} + +func (s *DistrictCodeMatch) New() Matcher { + return pageopt.NewAssignMatch("districtCode", s.DistrictCode) +} + +// DistrictCodeINMatch +type DistrictCodeINMatch struct { + DistrictCodeList []string +} + +func (d *DistrictCodeINMatch) New() Matcher { + return pageopt.NewInMatch("districtCode", d.DistrictCodeList) +} + +// PromSeqeMatch +type PromSeqeMatch struct { + Seqe *string +} + +func (d *PromSeqeMatch) New() Matcher { + return pageopt.NewAssignMatch("promSeqe", d.Seqe) +} + +// SysTypeMatch +type SysTypeMatch struct { + SysType *string +} + +func (d *SysTypeMatch) New() Matcher { + return pageopt.NewAssignMatch("sysType", d.SysType) +} + +// IsDirectMatch +type IsDirectMatch struct { + IsDirect *bool +} + +func (b *IsDirectMatch) New() Matcher { + return pageopt.NewAssignMatch("isDirect", b.IsDirect) +} + +// CreatedAtGTEAndLTMatch +type CreatedAtGTEAndLTMatch = pageopt.CreatedAtGTEAndLTMatch + +// UIDMatch +type UIDMatch = pageopt.UIDMatch + +// UIDInMatch +type UIDInMatch = pageopt.UIDInMatch + +// IDMatch +type IDMatch = pageopt.IDMatch + +type Sort = bson.D + +var Sort_CreatedAt_n1 = Sort{{Key: "createdAt", Value: -1}} + +func List(sort Sort, skip, limit *int64, matchers ...Matcher) ([]TransactionLog, error) { + filter := pageopt.MergeM(matchers) + opt := (&options.FindOptions{}) + if len(sort) != 0 { + opt.SetSort(sort) + } + if skip != nil { + opt.SetSkip(*skip) + } + if limit != nil { + opt.SetLimit(*limit) + } + list := []TransactionLog{} + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.Error("txnmod List error", log.E(err)) + return nil, err + } + return list, nil +} + +func Count(matchers ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchers) + count, err := coll(nil).Count(filter) + if err != nil { + log.Error("txnmod Count error", log.E(err)) + return 0, err + } + return count, nil +} + +func AmountSum(matchers ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchers) + pipeline := []M{} + pipeline = append(pipeline, M{"$match": filter}) + pipeline = append(pipeline, M{"$group": M{"_id": nil, + "amount": M{"$sum": "$amount"}, + }}) + var amount struct { + Amount int64 `bson:"amount"` //金币 + } + if err := coll(nil).AggregateDecode(&amount, pipeline); err != nil { + log.Error("txnmod Sum error", log.E(err)) + return 0, err + } + return amount.Amount, nil +} diff --git a/models/v/txnmod/transaction.go b/models/v/txnmod/transaction.go new file mode 100644 index 0000000..7dc901f --- /dev/null +++ b/models/v/txnmod/transaction.go @@ -0,0 +1,654 @@ +/* + * @Description: In User Settings Edit + * @Author: your name + * @Date: 2019-08-29 19:55:45 + * @LastEditTime: 2019-08-30 19:39:56 + * @LastEditors: Please set LastEditors + */ +package txnmod + +import ( + "fmt" + "math" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/common/timeutil" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.Transaction + +// InitIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "purchaseOrder", Value: 1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "amount", Value: 1}}, + }, + { + Keys: bson.D{{Key: "actualAmount", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "uniqueOrder", Value: 1}}, + Options: options.Index().SetSparse(true).SetUnique(true), + }, + + { + Keys: bson.D{{Key: "tranType", Value: 1}, {Key: "createdAt", Value: -1}}, + // Options: options.Index().SetUnique(true).SetSparse(true), + }, + { + Keys: bson.D{{Key: "tranType", Value: 1}, {Key: "districtCode", Value: 1}, {Key: "promSeqe", Value: 1}, {Key: "createdAt", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{{Key: "tranType", Value: 1}, {Key: "districtCode", Value: 1}, {Key: "promSeqe", Value: 1}, {Key: "sysType", Value: 1}, {Key: "createdAt", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{{Key: "tranTypeInt", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "tranTypeInt", Value: 1}, {Key: "districtCode", Value: 1}, {Key: "promSeqe", Value: 1}, {Key: "createdAt", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{{Key: "tranTypeInt", Value: 1}, {Key: "districtCode", Value: 1}, {Key: "promSeqe", Value: 1}, {Key: "sysType", Value: 1}, {Key: "createdAt", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{{Key: "isDirect", Value: 1}, {Key: "createdAt", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "tranTypeInt", Value: 1}, {Key: "money", Value: 1}, {Key: "fruitCoin", Value: 1}, {Key: "createdAt", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{{Key: "deductType", Value: 1}, {Key: "createdAt", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Insert 新增 +func InsertTransactionLog(mt *db.MongoTool, t *TransactionLog) error { + //Insert 插入一条交易流水 + t.CreatedAt = time.Now() + if _, err := coll(mt).InsertOne(t); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertTransactionLog", table, "InsertOne", err)) + return err + } + return nil +} + +func InsertManyTransactionLog(mt *db.MongoTool, trans []TransactionLog) error { + ops := options.InsertMany().SetOrdered(false) + if _, err := coll(mt).InsertMany(trans, ops); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertTransactionLog", table, "InsertOne", err)) + return err + } + return nil +} + +// FindMany 查询所有 +func FindTransactionLogs(filter bson.M, opts *options.FindOptions) (total int64, data []*TransactionLog, err error) { + data = make([]*TransactionLog, 0) + if err = coll(nil).Find(&data, filter, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindTransactionLogs", table, "Find", err), + log.Any("filter", filter), + ) + return + } + total, err = coll(nil).Count(filter) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindTransactionLogs", table, "Count", err), + log.Any("filter", filter), + ) + } + return +} + +// getTotalCnt 获取标签视频总数 +func getTotalCnt(cond bson.M) (int64, error) { + total, err := coll(nil).Count(cond) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getTotalCnt", table, "Count", err), + log.Any("cond", cond), + ) + return 0, err + } + return total, nil +} + +// GetSkipSize 计算跳转 +func getSkipSize(page, size uint64, cond bson.M) (uint64, uint64, int64, error) { + total, err := getTotalCnt(cond) + if err != nil { + return 0, 0, 0, err + } + totalpages := uint64(math.Ceil(float64(total) / float64(size))) + if page > totalpages { + page = totalpages + } + if page < 1 { + page = 1 + } + return (page - 1) * size, totalpages, total, nil +} + +// GetCoinLogs 条件获取金币日志(时间倒序) +func GetCoinLogs(page, size uint64, cond map[string]interface{}) ([]*TransactionLog, uint64, int64, error) { + if !hasLedgerUID(bson.M(cond)) { + // Preserve the existing global listing and export path until the fund + // collection has an approved index for cross-user time queries. + skip, totalPages, total, err := getSkipSize(page, size, bson.M(cond)) + if err != nil { + return nil, 0, 0, err + } + opts := options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}}). + SetSkip(int64(skip)).SetLimit(int64(size)) + var rows []*TransactionLog + err = coll(nil).Find(&rows, bson.M(cond), opts) + return rows, totalPages, total, err + } + return getLedgerCoinLogs(page, size, bson.M(cond)) +} + +func GetTranTypes() (data []interface{}, err error) { + data, err = coll(nil).Distinct("tranType", bson.M{}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetTranTypes", table, "Distinct", err)) + return nil, err + } + for _, kind := range []string{AiGirlfriendTransferIn.Key(), AiGirlfriendTransferOut.Key()} { + found := false + for _, existing := range data { + if existing == kind { + found = true + break + } + } + if !found { + data = append(data, kind) + } + } + return +} + +// FindManyByTime 查询所有 +func FindManyByTime(uid uint64, start time.Time, end time.Time) (data []*TransactionLog, err error) { + data = make([]*TransactionLog, 0) + opts := options.FindOptions{ + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + if err = coll(nil).Find(&data, bson.M{"uid": uid, "createdAt": bson.M{"$gte": start, "$lt": end}}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindManyByTime", table, "Find", err), + log.Any("uid", uid), + log.Any("start", start), + log.Any("end", end), + ) + return + } + return +} + +// HasNext 查询所有 +func HasNext(uid uint64, start time.Time) (hasNext bool, err error) { + var data TransactionLog + if err = coll(nil).FindOne(&data, bson.M{"uid": uid, "createdAt": bson.M{"$lt": start}}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "HasNext", table, "FindOne", err), + log.Any("uid", uid), + log.Any("start", start), + ) + return + } + return !data.ID.IsZero(), err +} + +// LastTime 查询所有 +func LastTime(uid uint64, end time.Time) (data TransactionLog, err error) { + opt := options.FindOneOptions{ + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + if err = coll(nil).FindOne(&data, bson.M{"uid": uid, "createdAt": bson.M{"$lt": end}}, &opt); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "LastTime", table, "FindOne", err), + log.Any("uid", uid), + log.Any("end", end), + ) + return + } + return data, nil +} + +func GetBuyVipCountMapByHour(discCode string, start, end time.Time, mats ...Matcher) ([]int, map[int]int64, int64, error) { + payVIP := PayVIP.Key() + mats = append(mats, + (&TranTypeMatch{&payVIP}).New(), + (&DistrictCodeMatch{&discCode}).New(), + (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + filter := pageopt.MergeM(mats) + opt := (&options.FindOptions{}).SetProjection(M{ + "createdAt": 1, + }) + var list []struct { + CreatedAt time.Time `bson:"createdAt"` //创建时间 + } + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.Error("txnmod GetBuyVipCountMap error", log.E(err)) + return nil, nil, 0, err + } + m := make(map[int]int64) + startH := start.Hour() + lastH := end.Hour() + (end.Day()-start.Day())*24 + hs := make([]int, lastH-startH+1) + for i, j := startH, 0; i <= lastH; i++ { + hs[j] = i + m[i] = 0 + j++ + } + var total int64 + for _, v := range list { + hour := v.CreatedAt.Hour() + (v.CreatedAt.Day()-start.Day())*24 + m[hour] += 1 + total++ + } + return hs, m, total, nil +} + +// FindIncome 查询所有 +func FindIncome(uid uint64, pageNumebr, pageSize uint64) (data []*TransactionLog, hasNext bool, err error) { + data = make([]*TransactionLog, 0) + f := bson.M{"uid": uid, "actualAmount": bson.M{"$gt": 0}} + skip := int64(pageSize * (pageNumebr - 1)) + limit := int64(pageSize + 1) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindTransactionLogs", table, "Find", err), + log.Any("filter", f), + ) + return + } + if len(data) > int(pageSize) { + hasNext = true + data = data[:pageSize] + } + return +} + +// FindPoxyIncomeOfMonth 查询每月收益 +func FindPoxyIncomeOfMonth(uid uint64) (totalMoney int64, totalPerfomance int64, err error) { + firstDay, lastDay := timeutil.MonthStartEndTime(time.Now()) + pipelines := []bson.M{ + {"$match": bson.M{"uid": uid, "tranTypeInt": ProxyIncome, "createdAt": bson.M{"$gte": firstDay, "$lt": lastDay}}}, + {"$group": bson.M{"_id": nil, "totalMoney": bson.M{"$sum": "$actualAmount"}, "totalPerformance": bson.M{"$sum": "$performance"}}}, + {"$project": bson.M{"totalMoney": 1, "totalPerformance": 1}}, + } + type res struct { + TotalMoney int64 `bson:"totalMoney"` + TotalPerformance int64 `bson:"totalPerformance"` + } + data := make([]res, 0) + if err = coll(nil).Aggregate(&data, pipelines); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindIncomeOfMonth", table, "Aggregate", err), log.Any("uid", uid)) + return + } + if len(data) > 0 { + totalMoney = data[0].TotalMoney + totalPerfomance = data[0].TotalPerformance + } + return +} + +// FindTransactionLogsByTransType 查询所有 +func FindTransactionLogsByTransType(uid uint64, pageNumber int64, pageSize int64, tranType TransType) (data []*TransactionLog, hasNext bool, err error) { + data = make([]*TransactionLog, 0) + skip := (pageNumber - 1) * pageSize + limit := pageSize + 1 + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + if err = coll(nil).Find(&data, bson.M{"uid": uid, "tranTypeInt": tranType}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindTransactionLogsByTransType", table, "Find", err), log.Any("uid", uid)) + return + } + if len(data) > int(pageSize) { + hasNext = true + data = data[:pageSize] + } + return +} + +// GetProxyIncomeForToday 查询所有 +func GetProxyIncomeForToday(uid uint64, start time.Time, end time.Time) (data []AgentIncomeRes, err error) { + pipelines := []bson.M{ + {"$match": bson.M{"uid": uid, "tranTypeInt": ProxyIncome, "createdAt": bson.M{"$gte": start, "$lt": end}}}, + {"$group": bson.M{"_id": "$agentLevel", "totalAmount": bson.M{"$sum": "$actualAmount"}, "totalPerformance": bson.M{"$sum": "$performance"}}}, + {"$project": bson.M{"totalAmount": 1, "totalPerformance": 1}}, + } + data = make([]AgentIncomeRes, 0) + if err = coll(nil).Aggregate(&data, pipelines); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetProxyIncomeOfDay", table, "Find", err), log.Any("uid", uid)) + return + } + return +} + +// GetIncomeByType 查询所有 +func GetIncomeByType(uid uint64, tp TransType, start time.Time, end time.Time) (totalAmount float64, totalPer int64, err error) { + match := bson.M{"uid": uid, "tranTypeInt": tp, "createdAt": bson.M{"$gte": start, "$lt": end}} + log.Info(fmt.Sprintf("%v", match)) + pipelines := []bson.M{ + {"$match": match}, + {"$group": bson.M{"_id": nil, "totalAmount": bson.M{"$sum": "$actualAmount"}, "totalPerformance": bson.M{"$sum": "$performance"}}}, + {"$project": bson.M{"totalAmount": 1, "totalPerformance": 1}}, + } + var res struct { + TotalAmount float64 `bson:"totalAmount"` + TotalPerformance int64 `bson:"totalPerformance"` + } + if err = coll(nil).AggregateDecode(&res, pipelines); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetProxyIncomeOfDay", table, "Find", err), log.Any("uid", uid)) + return + } + totalAmount = res.TotalAmount + totalPer = res.TotalPerformance + return +} + +// IsProxyExist 代理分成是否存在 +func IsProxyExist(uid uint64, objId primitive.ObjectID) bool { + filter := bson.M{"uid": uid, "purchaseOrder": objId, "tranType": ProxyIncome.Key()} + count, err := coll(nil).Count(filter) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsProxyExist", table, "Count", count), log.Any("uid", uid), log.Any("objId", objId)) + //查询异常的时候, 默认不分成, 用户找来可以人工修复, 项目不能亏 + return true + } + if count > 0 { + return true + } + return false +} + +// Income 收益 +func Income(uid uint64, start time.Time, end time.Time) (totalAmount float64, err error) { + return sumLedgerAmount(uid, start, end, true) +} + +// Expenditure 支出 +func Expenditure(uid uint64, start time.Time, end time.Time) (totalAmount float64, err error) { + return sumLedgerAmount(uid, start, end, false) +} + +// Ibills 账单 +func Ibills(uid uint64, pageNumebr, pageSize uint64, start time.Time, end time.Time) (data []TransactionLog, hasNext bool, err error) { + data = make([]TransactionLog, 0) + f := bson.M{ + "uid": uid, + "createdAt": bson.M{"$gte": start, "$lt": end}, + "tranTypeInt": bson.M{"$in": []TransType{ + AiGirlfriendTransferIn, AiGirlfriendTransferOut, Rchg, PayVID, WithdrawTransfer, PayVIP, WithdrawRefund, AdminCreaditAmount, AdminDebitAmount, OfficialRech, MeetingCard, + GameCoin, ChaseScore, PayReward, Other, LouFeng, AudioBook, LouFengRefund, LouFengDiscount, GameRewards, VipCardGive, + LouFengMianFei, LuckyDraw, PayAvVID, TranType_WLSysGive, SignBoon, JewelBoxBoon, LouFengConsumerRebate, ChessRechargePolite, + JiuGongGeDraw, CurrencyGive, GoldCouplePayVID, BookLoufeng, Active2023Cost, Active2023Reward, RaffleDeduction, PrizeRecord, + DailyTaskAdsClick, DailyTaskUserInvite, OnceTaskBuyVIP, OnceTaskBindMobile, ReceiveIntegral, BuyVIP, IntegralExchangeVip, + ReceiveIntegral, ProxyIncome, WorksIncome, SendMsgDebitIncomeGold, SendMsgDebitIncomeGoldReturn, SendMsgDebitAmountGold, + SendMsgDebitAmountGoldReturn, AiChangefaceDebitGold, AiChangefaceDebitGoldReturn, AiChangefaceDebitInComeGold, AiChangefaceDebitIncomeGoldReturn, + AiChangeFaceImgDebitGold, AiChangeFaceImgReturnGold, AiUndressDebitFreeTimes, AiUndressDebitFreeTimesReturn, AiUndressDebitIncomeGold, + AiUndressDebitIncomeGoldReturn, AiChangeFaceImgDebitIncomeGold, AiChangeFaceImgReturnIncomeGold, AiChangeFaceImgDebitFreeTimes, + AiChangeFaceImgDebitFreeTimesReturn, AiUndress, AiUndressRefund, AiUndressInc, AiUndressIncBackend, AiUndressDebitGold, AiUndressDebitGoldReturn, + VipCardGiveAiUndressFreeCount, AdminAddDownloadCount, AdminDebitDownloadCount, BuyAdvanceVIP, BuyBalanceVIP, BuyGameAdvanceVIP, + BuyWhoringCard, ReSignDebitAmount, SuccessSignReturnAmount, GodCommentAward, IntegralExchangeAICount, AiMateCurrencyExchange, + AiImageToVideoDebitGold, AiImageToVideoDebitGoldReturn, AiImageToVideoDebitInComeGold, AiImageToVideoDebitIncomeGoldReturn, + AiTextToImageDebitGold, AiTextToImageDebitGoldReturn, AiTextToImageDebitInComeGold, AiTextToImageDebitIncomeGoldReturn, BuyAcg, + AiTextToNovelDebitGold, AiTextToNovelDebitGoldReturn, AiTextToNovelDebitInComeGold, AiTextToNovelDebitIncomeGoldReturn, + }}, + //"amount": bson.M{"$ne": 0}, + "$or": []bson.M{ + bson.M{"amount": bson.M{"$ne": 0}}, + bson.M{"integral": bson.M{"$ne": 0}}, + bson.M{"tranTypeInt": bson.M{"$in": []TransType{AiGirlfriendTransferIn, AiGirlfriendTransferOut}}}, + }, + } + if pageSize == 0 || pageSize >= uint64(maxLedgerWindow) { + return nil, false, fmt.Errorf("invalid ledger page size") + } + if pageNumebr == 0 { + pageNumebr = 1 + } + if pageNumebr-1 > uint64(maxLedgerWindow)/pageSize { + return nil, false, fmt.Errorf("ledger query window is too large; narrow the time range") + } + rows, err := findLedgerRows(f, int64(pageSize*(pageNumebr-1)), int64(pageSize+1)) + if err != nil { + return nil, false, err + } + hasNext = len(rows) > int(pageSize) + if hasNext { + rows = rows[:pageSize] + } + for _, row := range rows { + data = append(data, *row) + } + return data, hasNext, nil +} + +// HasNextMonth 查询当前月之前的最迟数据的的时间 +func HasNextMonth(uid uint64, start time.Time) (data TransactionLog, err error) { + opt := options.FindOne() + opt.Sort = bson.D{{Key: "createdAt", Value: -1}} + err = coll(nil).FindOne(&data, bson.M{"uid": uid, "createdAt": bson.M{"$lt": start}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "HasNext", table, "FindOne", err), + log.Any("uid", uid), + log.Any("start", start), + ) + return + } + return +} + +func BuyLoufengCount(uid uint64) (int64, error) { + return coll(nil).Count(bson.M{"uid": uid, "tranTypeInt": bson.M{"$in": []TransType{LouFeng, BookLoufeng}}}) +} + +// 时间内购买VIP次数 +func BuyVipCount(start, end time.Time, mats ...Matcher) (int64, error) { + payVIP := PayVIP.Key() + mats = append(mats, (&TranTypeMatch{&payVIP}).New(), (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + filter := pageopt.MergeM(mats) + count, err := coll(nil).Count(filter) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "BuyVipCount", table, "Count", err)) + } + return count, err +} + +// //产品购买记录 +func BuyProductLog(uid uint64, tranTypeInt TransType, pids []primitive.ObjectID) (data []TransactionLog, err error) { + objIDs := make([]string, len(pids)) + for i := range pids { + objIDs[i] = pids[i].Hex() + } + data = make([]TransactionLog, 0) + opt := options.FindOptions{ + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + f := bson.M{"uid": uid, "tranTypeInt": tranTypeInt, "productID": bson.M{"$in": objIDs}} + if err = coll(nil).Find(&data, f, &opt); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "BuyProductLog", table, "Find", err)) + return + } + return +} + +// 产品购买记录 单个 +func BuyProductLogSingle(uid uint64, tranTypeInt TransType, pid string) (data TransactionLog, err error) { + f := bson.M{"uid": uid, "tranTypeInt": tranTypeInt, "productID": pid} + if err = coll(nil).FindOne(&data, f); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "BuyProductLogSingle", table, "Find", err)) + return + } + return +} + +// 产品购买记录 +func BuyProductLogWithPage(uid uint64, tranTypeInt TransType, page commod.Page) (data []TransactionLog, hasNext bool, err error) { + data = make([]TransactionLog, 0) + skip := int64(page.Skip()) + limit := int64(page.Limit() + 1) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + f := bson.M{"uid": uid, "tranTypeInt": tranTypeInt} + if tranTypeInt == PayAvVID { + f["productID"] = bson.M{"$exists": true} + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "BuyProductLog", table, "Find", err)) + return + } + if uint64(len(data)) > page.PageSize { + hasNext = true + data = data[:page.PageSize] + } + return +} + +// Income 账单 +func WorksIncomebills(uid uint64, pageNumebr, pageSize uint64) (data []TransactionLog, hasNext bool, err error) { + data = make([]TransactionLog, 0) + f := bson.M{"uid": uid, "tranTypeInt": WorksIncome} + skip := int64(pageSize * (pageNumebr - 1)) + limit := int64(pageSize + 1) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindTransactionLogs", table, "Find", err), + log.Any("filter", f), + ) + return + } + if len(data) > int(pageSize) { + hasNext = true + data = data[:pageSize] + } + return +} + +// 是否购买了某个产品 +func IsBuyProduct(uid uint64, id primitive.ObjectID, tranTypeInt TransType) (bool, error) { + f := bson.M{"uid": uid, "productID": id.Hex(), "tranTypeInt": tranTypeInt} + count, err := coll(nil).Count(f) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "BuyVipCount", table, "Count", count)) + return false, err + } + return count > 0, nil +} + +// QueryAll 条件查询列表 +func QueryAll(filter bson.M, opts ...*options.FindOptions) ([]TransactionLog, error) { + var items []TransactionLog + return items, coll(nil).Find(&items, filter, opts...) +} + +// TransStat 交易统计 +func TransStat(start, end time.Time) ([]TransactionR, error) { + var items []TransactionR + f := bson.M{"createdAt": bson.M{"$gt": start, "$lte": end}, "tranTypeInt": LouFeng} + return items, coll(nil).Find(&items, f) +} + +// IncomeLeaderboard 收益榜单 +func IncomeLeaderboard(bind interface{}, filter bson.M, limit int) error { + pip := []bson.M{ + {"$match": filter}, + {"$group": bson.M{"_id": "$uid", "income": bson.M{"$sum": "$actualAmount"}}}, + {"$sort": bson.M{"income": -1}}, + {"$limit": limit}, + } + if err := coll(nil).Aggregate(bind, pip); err != nil { + log.Info(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncomeLeaderboard", table, "Aggregate", err)) + return err + } + return nil +} + +// FindByCond 根据条件查询 +func FindByCond(filter primitive.M) (TransactionLog, error) { + opt := options.FindOneOptions{ + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + var data TransactionLog + if err := coll(nil).FindOne(&data, filter, &opt); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "LastTime", table, "FindByCond", err), + log.Any("filter", filter), + ) + return data, err + } + return data, nil +} + +// CheckRepurchaseByTransTypes 判断是否购买过,返回yes/no +func CheckRepurchaseByTransTypes(uid uint64, types []TransType) (isRepurchase string, err error) { + if len(types) == 0 { + return "no", nil + } + has, err := coll(nil).Exists(bson.M{"uid": uid, "tranTypeInt": bson.M{"$in": types}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CheckRepurchaseByTransTypes", table, "Exists", err), + log.Any("uid", uid), + log.Any("types", types), + ) + return "no", err + } + if has { + return "yes", nil + } + return "no", nil +} diff --git a/models/v/txnmod/type.go b/models/v/txnmod/type.go new file mode 100644 index 0000000..c845705 --- /dev/null +++ b/models/v/txnmod/type.go @@ -0,0 +1,462 @@ +package txnmod + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models/commod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var tranMap = map[TransType]string{ + AiGirlfriendTransferIn: "AI女友上分", + AiGirlfriendTransferOut: "AI女友下分", + Rchg: "充值", + PayVID: "购买视频", + WithdrawTransfer: "提现转账", + PayVIP: "VIP购买", + WithdrawRefund: "提现失败退款", + WorksIncome: "作品收益", + ActIncome: "活动收益", + ProxyIncome: "代理收益", + AdminCreaditAmount: "官方增加金币", + AdminDebitAmount: "官方减去金币", + NengModel: "购买嫩模", + OfficialRech: "官方充值", + MeetingCard: "购买约会卡", + GameCoin: "购买游戏币", + ChaseScore: "官方追分", + RewardIncome: "打赏收益", + PayReward: "打赏", + CoinMonthCard: "金币月卡", + Other: "购买特殊卡", + LouFeng: "购买楼凤", + AudioBook: "购买有声小说", + LouFengRefund: "楼凤退款", + LouFengDiscount: "楼凤优惠卡", + GameRewards: "游戏奖励", + VipCardGive: "购买会员卡赠送金币", + LouFengMianFei: "楼凤全免卡", + LuckyDraw: "幸运抽奖", + PayAvVID: "购买AV解说视频", + AvCommentVideoIncome: "AV解说视频收益", + TranType_WLSysGive: "棋牌游戏活动赠送", + VipCardGiveGameCoin: "购买会员卡赠送游戏金币", + WaLiProxyIncome: "瓦力推广收益", + AppStoreRewardIncome: "应用中心下载奖励", + SignBoon: "签到福利", + JewelBoxBoon: "宝箱福利", + LouFengConsumerRebate: "消费返利(楼凤)", + ChessRechargePolite: "充值有礼(棋牌)", + JiuGongGeDraw: "九宫格抽奖", + NudeChatConsumption: "裸聊消费", + FruitCoinRecharge: "果币充值", + BuyNudeChatService: "购买裸聊服务", + NudeChatIncome: "裸聊收益", + CurrencyGive: "货币赠送", + BuyVIP: "充值购买会员卡", + NudeChatRefund: "裸聊退款", + AdminCreditFruitCoin: "官方增加果币", + AdminDebitFruitCoin: "官方减去果币", + VideoDiscount: "视频折扣卡", + GoldCouplePayVID: "金币抵用券购买视频", + VideoFreeCard: "视频免费卡", + Active2023Cost: "2023春节活动消耗金币", + Active2023Reward: "2023春节抽奖活动获得金币", + Active2023VIP: "2023春节抽奖活动获得VIP卡", + RaffleDeduction: "抽奖扣款记录", + PrizeRecord: "抽奖记录", + DailyTaskAdsClick: "每日广告下载任务", + DailyTaskUserInvite: "每日邀请任务", + OnceTaskBuyVIP: "新手任务: 购买VIP/金币", + OnceTaskBindMobile: "新手任务: 绑定手机", + AiUndress: "AI脱衣", + AiUndressRefund: "AI脱衣返还次数(后台未通过时返还次数)", + AiUndressInc: "AI脱衣新增次数", + AiUndressIncBackend: "AI脱衣后台赠送次数", + AiUndressDebitGold: "AI脱衣扣款金币记录", + AiUndressDebitGoldReturn: "AI脱衣扣款金币记录返还", + IntegralExchangeVip: "VIP", + DailyComment: "每日评论记录", + ReceiveIntegral: "领取积分", + GiveDownload: "赠送下载次数", + AiChangefaceDebitGold: "AI换脸扣款金币记录", + AiChangefaceDebitGoldReturn: "AI换脸扣款金币记录返还", + AiChangefaceDebitInComeGold: "AI换脸扣款收益金币记录", + AiChangefaceDebitIncomeGoldReturn: "AI换脸扣款收益金币记录返还", + SendMsgDebitIncomeGold: "用户发送私信扣除收益金币", + SendMsgDebitIncomeGoldReturn: "用户发送私信收益金币返还", + SendMsgDebitAmountGold: "用户发送私信扣除金币", + SendMsgDebitAmountGoldReturn: "用户发送私信扣除金币返还", + AdminAddAiUndressFreeTimes: "官方增加AI脱衣免费次数", + AdminDebitAiUndressFreeTimes: "官方减去AI脱衣免费次数", + AiChangeFaceImgDebitGold: "AI图片换脸金币扣款记录", + AiChangeFaceImgReturnGold: "AI图片换脸金币返还", + AiUndressDebitFreeTimes: "AI脱衣扣款免费次数记录", + AiUndressDebitFreeTimesReturn: "AI脱衣免费次数返回", + AiUndressDebitIncomeGold: "AI脱衣扣款收益金币记录", + AiUndressDebitIncomeGoldReturn: "AI脱衣收益金币返还", + AiChangeFaceImgDebitIncomeGold: "AI图片换脸收益金币扣款记录", + AiChangeFaceImgReturnIncomeGold: "AI图片换脸收益金币返还", + AiChangeFaceImgDebitFreeTimes: "AI图片换脸免费次数扣款记录", + AiChangeFaceImgDebitFreeTimesReturn: "AI图片换脸免费次数返回", + VipCardGiveAiUndressFreeCount: "购买会员卡赠送AI脱衣免费次数", + AdminAddVIP: "官方新增VIP", + AdminAddDownloadCount: "官方增加下载次数", + AdminDebitDownloadCount: "官方扣除加下载次数", + BuyAdvanceVIP: "充值购买预售卡", + BuyBalanceVIP: "充值购买预售尾卡", + BuyGameAdvanceVIP: "充值购买游戏预售卡", + AdminAddLotteryTimesCount: "官方增加抽奖次数", + AdminDebitLotteryTimesCount: "官方扣除加抽奖次数", + GiveLotteryTimesCount: "充值赠送抽奖次数", + IntegralExchangeInKind: "实物", + IntegralExchangeAICount: "AI黑科技券", + IntegralExchangeFreeCount: "幸运抽奖次数", + IntegralExchangeGoldCoinBonus: "金币加赠券", + IntegralExchangeGoldWatch: "金币观影券", + GodCommentAward: "神评奖励", + BuyWhoringCard: "购买白嫖卡", + ReSignDebitAmount: "补签打卡扣除金币", + SuccessSignReturnAmount: "完成打卡返回金币", + AiImageToVideoDebitGold: "AI图生视频金币扣款记录", + AiImageToVideoDebitGoldReturn: "AI图生视频金币返还", + AiImageToVideoDebitInComeGold: "AI图生视频收益金币扣款记录", + AiImageToVideoDebitIncomeGoldReturn: "AI图生视频收益金币返还", + AiTextToImageDebitGold: "AI绘图金币扣款记录", + AiTextToImageDebitGoldReturn: "AI绘图金币返还", + AiTextToImageDebitInComeGold: "AI绘图收益金币扣款记录", + AiTextToImageDebitIncomeGoldReturn: "AI绘图收益金币返还", + AiMateChat: "AI女友聊天花费积分", + AiMateCurrencyExchange: "AI伴侣货币兑换", + AdminAiMateSet: "官方设置AI伴侣货币值", + AiTextToNovelDebitGold: "AI小说金币扣款记录", + AiTextToNovelDebitGoldReturn: "AI小说金币返还", + AiTextToNovelDebitInComeGold: "AI小说收益金币扣款记录", + AiTextToNovelDebitIncomeGoldReturn: "AI小说收益金币返还", + AdminCreaditIntegral: "官方增加积分", + AdminDebitIntegral: "官方减去积分", + + StoreBuyGoods: "商城购买普通商品", + StoreBuyNudeChat: "商城购买裸聊", + StorePublishWish: "商城发布许愿单", + StoreGoodsOrderRefund: "商城普通商品订单退款", + StoreNudeChatOrderRefund: "商城裸聊订单退款", + StoreWishRefund: "商城许愿单退款", + StoreWishEdit: "商城许愿单修改价格扣款", + BuyAcg: "购买ACG动漫", + JoinGroup: "加入群组", + BuyNakedChat: "购买裸聊", + + OrderRefund: "官方充值订单退款", + OrderRefundVip: "官方充值VIP订单退款", + RefundVipCardGive: "官方充值VIP赠送金币退款", + + ActivityRewardGold: "活动发放金币", + ActivityRewardVIP: "活动发放VIP", + ActivityRewardGoldBonusCoupon: "活动发放金币加赠券", + ActivityRewardGoldVideoCoupon: "活动发放金币观影券", + ActivityRewardAiChangeFaceFree: "活动发放AI换脸免费次数", + ActivityRewardAiUndressFree: "活动发放AI脱衣免费次数", + ActivityRewardIntegral: "活动发放积分", + ActivityRewardPhysical: "活动发放实物奖品", + ActivityDeductGold: "活动扣除金币", + ActivityDeductIntegral: "活动扣除积分", + ActivityDeductLotteryTimes: "活动扣除抽奖免费次数", +} + +// TranType2ProductType 交易类型到产品类型映射 +var TranType2ProductType = map[TransType]commod.ProductType{ + PayVIP: commod.VIP, + MeetingCard: commod.MeetingCard, + Other: commod.OTHER, + BuyVIP: commod.VIP, + VideoDiscount: commod.VideoDiscount, + VideoFreeCard: commod.VideoFreeCard, +} + +const ( + Rchg TransType = iota + 1 // 充值 + PayVID TransType = 2 // 购买视频 + WithdrawTransfer TransType = 3 // 提现转账 + PayVIP TransType = 4 // VIP购买(金币) + WithdrawRefund TransType = 5 // 提现失败退款 + WorksIncome TransType = 6 // 作品收益 + ActIncome TransType = 7 // 活动收益 + ProxyIncome TransType = 8 // 代理收益 + AdminCreaditAmount TransType = 9 // 官方增加金币 + AdminDebitAmount TransType = 10 // 官方减去金币 + NengModel TransType = 11 // 购买嫩模 弃用 + OfficialRech TransType = 12 // 官方充值 + MeetingCard TransType = 13 // 购买约会卡 + GameCoin TransType = 14 // 购买游戏币 + ChaseScore TransType = 15 // 从用户处追分,扣减用户金币 + RewardIncome TransType = 16 // 打赏收益 + PayReward TransType = 17 // 打赏 + CoinMonthCard TransType = 18 // 金币月卡 + Other TransType = 20 // 购买约会卡 + LouFeng TransType = 21 // 购买楼凤联系方式 + AudioBook TransType = 22 // 购买有声小说 + LouFengRefund TransType = 23 // 楼凤退款(包含预约) + LouFengDiscount TransType = 24 // 楼凤优惠卡 + GameRewards TransType = 25 // 游戏奖励 + VipCardGive TransType = 26 // 购买会员卡赠送金币 + LouFengMianFei TransType = 27 // 楼凤全免卡 + LuckyDraw TransType = 28 // 幸运抽奖 + PayAvVID TransType = 36 // 购买AV解说视频 + AvCommentVideoIncome TransType = 37 // AV解说视频收益 + TranType_WLSysGive TransType = 38 // 棋牌游戏活动赠送金币 + VipCardGiveGameCoin TransType = 39 // 购买会员卡赠送棋牌金币 + WaLiProxyIncome TransType = 40 // 瓦力代理收益 + AppStoreRewardIncome TransType = 41 // 应用中心下载奖励 + SignBoon TransType = 42 // 签到福利 + JewelBoxBoon TransType = 43 // 宝箱福利 + LouFengConsumerRebate TransType = 44 // 消费返利(楼凤) + ChessRechargePolite TransType = 45 // 充值有礼(棋牌) + JiuGongGeDraw TransType = 46 // 九宫格抽奖 + NudeChatConsumption TransType = 47 // 裸聊消费 + FruitCoinRecharge TransType = 48 // 果币充值 + BuyNudeChatService TransType = 49 // 购买裸聊服务 + NudeChatIncome TransType = 50 // 裸聊收益 + CurrencyGive TransType = 51 // 货币赠送 + BuyVIP TransType = 52 // 充值购买会员卡 + NudeChatRefund TransType = 53 // 裸聊退款 + AdminCreditFruitCoin TransType = 54 // 官方增加果币 + AdminDebitFruitCoin TransType = 55 // 官方减去果币 + VideoDiscount TransType = 56 // 视频折扣卡 + VideoFreeCard TransType = 57 // 视频免费卡 + GoldCouplePayVID TransType = 58 // 金币抵用券购买视频 + BookLoufeng TransType = 59 // 预约 + Active2023Cost TransType = 61 // 2023春节活动抽奖扣除金币 + Active2023Reward TransType = 62 // 2023春节活动获得金币 + Active2023VIP TransType = 63 // 2023新春抽奖获得VIP卡 + RaffleDeduction TransType = 64 //抽奖扣款记录 + PrizeRecord TransType = 65 //抽奖记录 + AiUndressDebitGoldReturn TransType = 66 // AI脱衣金币返回 + DailyTaskAdsClick TransType = 70 // 每日活动: 下载广告app + DailyTaskUserInvite TransType = 71 // 每日活动: 用户邀请 + OnceTaskBuyVIP TransType = 80 // 一次性任务: 购买VIP + OnceTaskBindMobile TransType = 81 // 一次性任务: 绑定手机号 + AiUndress TransType = 82 // AI脱衣 + AiUndressRefund TransType = 83 // AI脱衣返还次数(后台未通过时返还次数) + AiUndressInc TransType = 84 // AI脱衣新增次数 + AiUndressIncBackend TransType = 85 // AI脱衣后台赠送 + AiUndressDebitGold TransType = 86 // AI脱衣金币扣款记录 + IntegralExchangeVip TransType = 87 // 积分兑换VIP + DailyComment TransType = 88 // 每日评论记录 + ReceiveIntegral TransType = 89 // 领取积分 + GiveDownload TransType = 90 // 赠送下载次数 + AiChangefaceDebitGold TransType = 91 // AI换脸金币扣款记录 + AiChangefaceDebitGoldReturn TransType = 92 // AI换脸金币返还 + AiChangefaceDebitInComeGold TransType = 93 // AI换脸收益金币扣款记录 + AiChangefaceDebitIncomeGoldReturn TransType = 94 // AI换脸收益金币返还 + SendMsgDebitIncomeGold TransType = 95 // 用户发送私信扣除收益金币 + SendMsgDebitIncomeGoldReturn TransType = 96 // 用户发送私信收益金币返还 + SendMsgDebitAmountGold TransType = 97 // 用户发送私信扣除金币 + SendMsgDebitAmountGoldReturn TransType = 98 // 用户发送私信扣除金币返还 + AdminAddAiUndressFreeTimes TransType = 99 // 官方增加AI脱衣免费次数 + AdminDebitAiUndressFreeTimes TransType = 100 // 官方减去AI脱衣免费次数 + AiChangeFaceImgDebitGold TransType = 101 // AI图片换脸金币扣款记录 + AiChangeFaceImgReturnGold TransType = 102 // AI图片换脸金币返还 + AiUndressDebitFreeTimes TransType = 103 // AI脱衣免费次数扣款记录 + AiUndressDebitFreeTimesReturn TransType = 104 // AI脱衣免费次数返回 + AiUndressDebitIncomeGold TransType = 105 // AI脱衣收益金币扣款记录 + AiUndressDebitIncomeGoldReturn TransType = 106 // AI脱衣收益金币返回 + AiChangeFaceImgDebitIncomeGold TransType = 107 // AI图片换脸收益金币扣款记录 + AiChangeFaceImgReturnIncomeGold TransType = 108 // AI图片换脸收益金币返还 + AiChangeFaceImgDebitFreeTimes TransType = 109 // AI图片换脸免费次数扣款记录 + AiChangeFaceImgDebitFreeTimesReturn TransType = 110 // AI图片换脸免费次数返回 + VipCardGiveAiUndressFreeCount TransType = 111 // 购买会员卡赠送AI脱衣免费次数 + AdminAddVIP TransType = 112 // 官方新增VIP + AdminAddDownloadCount TransType = 113 // 官方增加下载次数 + AdminDebitDownloadCount TransType = 114 // 官方扣除加下载次数 + BuyAdvanceVIP TransType = 115 // 充值购买预售卡 + BuyBalanceVIP TransType = 117 // 充值购买预售尾卡 + BuyGameAdvanceVIP TransType = 116 // 充值购买游戏预售卡 + AdminAddLotteryTimesCount TransType = 120 // 官方增加抽奖次数 + AdminDebitLotteryTimesCount TransType = 121 // 官方扣除加抽奖次数 + GiveLotteryTimesCount TransType = 122 // 赠送抽奖次数 + BuyWhoringCard TransType = 123 // 充值购买白嫖卡 + ReSignDebitAmount TransType = 124 // 补签打卡扣除金币 + SuccessSignReturnAmount TransType = 125 // 完成打卡返回金币 + IntegralExchangeInKind TransType = 126 // 积分兑换实物 + IntegralExchangeAICount TransType = 127 // 积分兑换AI黑科技券 + IntegralExchangeFreeCount TransType = 128 // 积分兑换幸运抽奖次数 + IntegralExchangeGoldCoinBonus TransType = 129 // 积分兑换金币加赠券 + IntegralExchangeGoldWatch TransType = 130 // 积分兑换金币观影券 + GodCommentAward TransType = 131 // 神评奖励 + OnceTaskTypeUserBuyCoin TransType = 132 // 一次性任务: 购买金币 + + AiMateCurrencyExchange TransType = 137 // AI伴侣货币兑换 + AiMateChat TransType = 138 // AI女友聊天花费积分 + AdminAiMateSet TransType = 139 // 官方设置AI伴侣货币值 + AdminCreaditIntegral TransType = 143 // 官方增加积分 + AdminDebitIntegral TransType = 144 // 官方减去积分 + AiImageToVideoDebitGold TransType = 202 // AI图生视频金币扣款记录 + AiImageToVideoDebitGoldReturn TransType = 203 // AI图生视频金币返还 + AiImageToVideoDebitInComeGold TransType = 204 // AI图生视频收益金币扣款记录 + AiImageToVideoDebitIncomeGoldReturn TransType = 205 // AI图生视频收益金币返还 + AiTextToImageDebitGold TransType = 206 // AI绘图金币扣款记录 + AiTextToImageDebitGoldReturn TransType = 207 // AI绘图金币返还 + AiTextToImageDebitInComeGold TransType = 208 // AI绘图收益金币扣款记录 + AiTextToImageDebitIncomeGoldReturn TransType = 209 // AI绘图收益金币返还 + AiTextToNovelDebitGold TransType = 210 // AI小说金币扣款记录 + AiTextToNovelDebitGoldReturn TransType = 211 // AI小说金币返还 + AiTextToNovelDebitInComeGold TransType = 212 // AI小说收益金币扣款记录 + AiTextToNovelDebitIncomeGoldReturn TransType = 213 // AI小说收益金币返还 + + BuyAcg TransType = 166 // 购买acg动漫 + + OrderRefund TransType = 301 // 官方充值退款 + OrderRefundVip TransType = 302 // 官方充值VIP退款 + RefundVipCardGive TransType = 303 // 官方充值VIP赠送金币退款 + + StoreBuyGoods TransType = 10001 // 商城购买普通商品 + StoreBuyNudeChat TransType = 10002 // 商城购买裸聊 + StorePublishWish TransType = 10003 // 商城发布许愿单 + StoreGoodsOrderRefund TransType = 10004 // 商城普通商品订单退款 + StoreNudeChatOrderRefund TransType = 10005 // 商城裸聊订单退款 + StoreWishRefund TransType = 10006 // 商城许愿单退款 + StoreWishEdit TransType = 10007 // 商城许愿单修改价格扣款 + + JoinGroup TransType = 10101 // 加入群组 + BuyNakedChat TransType = 10201 // 购买裸聊 + + ActivityRewardGold TransType = 20001 // 活动发放金币 + ActivityRewardVIP TransType = 20002 // 活动发放VIP + ActivityRewardGoldBonusCoupon TransType = 20003 // 活动发放金币加赠券 + ActivityRewardGoldVideoCoupon TransType = 20004 // 活动发放金币观影券 + ActivityRewardAiChangeFaceFree TransType = 20005 // 活动发放AI换脸免费次数 + ActivityRewardAiUndressFree TransType = 20006 // 活动发放AI脱衣免费次数 + ActivityRewardIntegral TransType = 20007 // 活动发放积分 + ActivityRewardPhysical TransType = 20008 // 活动发放实物奖品 + ActivityDeductGold TransType = 20101 // 活动扣除金币 + ActivityDeductIntegral TransType = 20102 // 活动扣除积分 + ActivityDeductLotteryTimes TransType = 20103 // 活动扣除抽奖免费次数 +) + +const ( + CurrencyTypeGold = iota //金币 + CurrencyTypeCash //现金 +) + +const ( + AiGirlfriendTransferIn TransType = 145 + AiGirlfriendTransferOut TransType = 146 +) + +type TransType int64 + +func (t TransType) Key() string { + if key, ok := tranMap[t]; ok { + return key + } + return "" +} + +type M = bson.M + +type DiscDoc = commod.DiscDoc + +// TransactionLog 账户交易流水表, +type TransactionLog struct { + //流水id + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + //出金方 UID + UID uint64 `json:"uid" bson:"uid"` + //交易订单号 + TransNo primitive.ObjectID `json:"purchaseOrder" bson:"purchaseOrder"` + //产品id 传购买某些物品或者卖出某些物品的id + ProductID *string `json:"productID" bson:"productID,omitempty"` + //金币 + Amount int64 `json:"amount" bson:"amount"` + //积分 + Integral int64 `json:"integral" bson:"integral"` + //用户实时余额 + RealIntegral decimal.Decimal `json:"realIntegral" bson:"realIntegral"` + //实际收入/支出的积分 + ActualIntegral float64 `json:"actualIntegral" bson:"actualIntegral"` + //实际收入/支出的金币 + ActualAmount float64 `json:"actualAmount" bson:"actualAmount"` + //税率 + Tax int64 `json:"tax" bson:"tax"` + //系统收取的税额 税率*定价 + TaxAmount float64 `json:"taxAmount" bson:"taxAmount"` + //充值/提现渠道类型 + ChannelType string `json:"channelType" bson:"channelType"` + //交易类型 + TranType string `json:"tranType" bson:"tranType"` + //交易类型的数字值 + TranTypeInt int64 `json:"tranTypeInt" bson:"tranTypeInt"` + //推广绩效 + Performance int64 `form:"performance" json:"performance" bson:"performance"` + //充值用户 + RechargeId uint64 `form:"rechargeId" json:"rechargeId" bson:"rechargeId"` + RechargeUser RechargeUserInfo `json:"rechargeUser" bson:"rechargeUser"` // 购买用户信息 + //记录描述 + Desc string `json:"desc" bson:"desc"` + //创建时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + //设备系统类型 ios pc android + SysType string `json:"sysType" bson:"sysType"` + //代理等级 + AgentLevel int `form:"agentLevel" json:"agentLevel" bson:"agentLevel"` + //会员等级 + VipLevel int `json:"vipLevel" bson:"vipLevel"` + //用户实时余额 + RealAmount decimal.Decimal `json:"realAmount" bson:"realAmount"` + //棋牌充值/提现的金额 单位元 + Money decimal.Decimal `json:"money" bson:"money"` + //用户瓦力棋牌游戏实时余额 + WaLIRealAmount decimal.Decimal `json:"wlRealAmount" bson:"wlRealAmount"` + //唯一订单号 + UniqueOrder string `json:"uniqueOrder,omitempty" bson:"uniqueOrder,omitempty"` + //交易类型(0 金币 1 现金) + CurrencyType int `json:"currencyType,omitempty" bson:"currencyType,omitempty"` + //果币 + FruitCoin int64 `json:"fruitCoin" bson:"fruitCoin,omitempty"` + //下载次数 + DownloadCount int64 `json:"downloadCount" bson:"downloadCount,omitempty"` + //抽奖次数 + LotteryTimes int64 `json:"lotteryTimes" bson:"lotteryTimes,omitempty"` + //果币余额 + FruitCoinBalance int64 `json:"fruitCoinBalance" bson:"fruitCoinBalance,omitempty"` + AiMatePoint float64 `json:"aiMatePoint" bson:"aiMatePoint,omitempty"` // AI女友积分 + RealAiMatePoint float64 `json:"realAiMatePoint" bson:"realAiMatePoint,omitempty"` // 剩余AI女友积分 + IsRepurchase string `json:"isRepurchase" bson:"isRepurchase"` // 是否复购,yes、no + DiscDoc `bson:",inline"` +} + +type RechargeUserInfo struct { + UID uint64 `json:"uid" bson:"uid"` // 用户ID + Name string `json:"name" bson:"name"` // 用户姓名 + Portrait string `json:"portrait" bson:"portrait"` // 用户头像 +} + +type AgentIncomeRes struct { + AgentLevel int `bson:"_id"` + TotalMoney float64 `bson:"totalAmount"` + TotalPerformance int64 `bson:"totalPerformance"` +} + +type LoufengTemp struct { + ProductID string `json:"productID" bson:"productID,omitempty"` //产品id 传购买某些物品或者卖出某些物品的id + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 + +} + +type TransactionR struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //流水id + ActualAmount float64 `json:"actualAmount" bson:"actualAmount"` //实际收入/支出的金币 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 +} + +var mdb *db.MongoDB + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/txnmod/web.go b/models/v/txnmod/web.go new file mode 100644 index 0000000..22077d5 --- /dev/null +++ b/models/v/txnmod/web.go @@ -0,0 +1,59 @@ +package txnmod + +import ( + "time" + + "91porn-server/models/commod" +) + +// CoinLogReq 金币流水请求 +type CoinLogReq struct { + UID uint64 `form:"uid" json:"uid"` + Balance int `form:"balance" json:"balance"` //0所有 1收入 2支出 + TranType string `form:"tranType" json:"tranType"` + DistrictCode string `form:"districtCode" json:"districtCode"` + Start time.Time `form:"start" json:"start"` + End time.Time `form:"end" json:"end"` + commod.Page +} + +// CoinLogResp 金币流水返回 +type CoinLogResp struct { + Logs []*TransactionLog `json:"logs"` + Total int64 `json:"total"` +} + +// TransactionQueryReq 账户交易流水表, +type TransactionQueryReq struct { + ID *string `json:"id,omitempty" bson:"_id,omitempty"` //流水id + UID *uint64 ` json:"uid,omitempty" bson:"uid,omitempty"` //出金方 UID + TransNo *string ` json:"transNo,omitempty" bson:"transNo,omitempty"` //交易订单号 + Amount *int64 ` json:"amount,omitempty" bson:"amount,omitempty"` //金币 + TransType *string ` json:"transType,omitempty" bson:"transType,omitempty"` //交易类型 + Desc *string ` json:"desc,omitempty" bson:"desc,omitempty"` //记录描述 +} + +type BillsRes struct { + Month string `json:"month"` + Income int64 `json:"income"` + Withdraw int64 `json:"withdraw"` + IncomeStr string `json:"incomeStr"` //收益 + WithdrawStr string `json:"withdrawStr"` //提现 + List []*TransactionLog `json:"list"` +} + +type Bills1Res struct { + //收益 + Income string `json:"income"` + //支出 + Expenditure string `json:"expenditure"` + HasNext bool `json:"hasNext"` + //列表 + List []TransactionLog `json:"list"` + //当前焦点月之前是否有数据 + HasNextMonth bool `json:"haxNextMonth"` + //当前焦点月有数据的最迟月份 + Month int `json:"month"` + //当前焦点月有数据的最迟月份的年 + Year int `json:"year"` +} diff --git a/models/v/userResourcemod/type.go b/models/v/userResourcemod/type.go new file mode 100644 index 0000000..117a642 --- /dev/null +++ b/models/v/userResourcemod/type.go @@ -0,0 +1,21 @@ +package userResourcemod + +import ( + "91porn-server/common/db" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// UserResource 用户资源配置 +type UserResource struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Resource string `json:"resource" bson:"resource"` //资源 + Type string `json:"type" bson:"type"` //类型,bg:背景, avatar:头像 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/userResourcemod/userResource.go b/models/v/userResourcemod/userResource.go new file mode 100644 index 0000000..6d4614d --- /dev/null +++ b/models/v/userResourcemod/userResource.go @@ -0,0 +1,175 @@ +package userResourcemod + +import ( + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/localcache" + "91porn-server/common/log" + "91porn-server/models" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.UserResource + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{"type", 1}}, + }, + { + Keys: bson.D{{"updatedAt", -1}}, + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// getListCnt 获取总数 +func getListCnt(cond bson.M) (int64, error) { + total, err := coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getListCnt", table, "Count", err), + log.Any("cond", cond), + ) + return 0, err + } + return total, nil +} + +// GetSkipSize 计算跳转 +func getSkipSize(page int, size int, cond bson.M) (int, int64, error) { + total, err := getListCnt(cond) + if err != nil { + return 0, 0, err + } + return (page - 1) * size, total, nil +} + +// todo app +// GetUserResourceByType 获取用户资源配置 +func GetUserResourceByType(rType string) ([]string, error) { + cacheKey := fmt.Sprintf("GetUserResourceByType:%s", rType) + cfg, ok := localcache.C.Get(cacheKey) + if ok { + return cfg.([]string), nil + } + var datas []UserResource + cond := bson.M{} + if rType != "" { + cond = bson.M{"type": rType} + } + + err := coll(nil).Find(&datas, cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetUserResource", table, "Find", err), + log.Any("rType", rType)) + } + res := make([]string, len(datas)) + if len(datas) > 0 { + for i := range datas { + res[i] = datas[i].Resource + } + } + common.Go(func() { + localcache.C.Set(cacheKey, res, 30*time.Minute) + }) + return res, err +} + +// GetUserResource 获取用户资源配置 +func GetUserResource(page int, size int, rType string) ([]UserResource, int64, error) { + var datas []UserResource + cond := bson.M{} + if rType != "" { + cond = bson.M{"type": rType} + } + sort := bson.D{{"updatedAt", -1}} + skip, total, err := getSkipSize(page, size, cond) + if err != nil { + return datas, total, err + } + opts := options.FindOptions{} + opts.SetSort(sort).SetSkip(int64(skip)).SetLimit(int64(size)) + err = coll(nil).Find(&datas, cond, &opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetUserResource", table, "Find", err), + log.Any("page", page), + log.Any("size", size), + ) + } + return datas, total, err +} + +// Add 添加 +func Add(ur UserResource) error { + _, err := coll(nil).InsertOne(&ur) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Add", table, "InsertOne", err)) + } + return err +} + +func getRecoUpdate(rType, resource *string) bson.M { + update := bson.M{"updatedAt": time.Now()} + if rType != nil { + update["type"] = *rType + } + if resource != nil { + update["resource"] = *resource + } + return update +} + +// ModifyUserResource 修改推荐用户信息 +func ModifyUserResource(id primitive.ObjectID, rType, resource *string) error { + cond := bson.M{"_id": id} + update := getRecoUpdate(rType, resource) + _, err := coll(nil).UpdateOne(cond, bson.M{"$set": update}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ModifyUserResource", table, "UpdateOne", err), + log.Any("id", id), + log.Any("rType", rType), + log.Any("resource", resource), + ) + } + return err +} + +// RemoveUserResource 删除推荐用户信息 +func RemoveUserResource(id primitive.ObjectID) error { + cond := bson.M{"_id": id} + _, err := coll(nil).DeleteOne(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RemoveUserResource", table, "DeleteOne", err), + log.Any("id", id), + ) + } + return err +} + +// GetResourceIsExist 资源是否存在 +func GetResourceIsExist(resource string) bool { + b, err := coll(nil).Exists(bson.M{"resource": resource}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetResourceIsExist", table, "Exists", err)) + return false + } + return b +} diff --git a/models/v/userResourcemod/web.go b/models/v/userResourcemod/web.go new file mode 100644 index 0000000..89ae10f --- /dev/null +++ b/models/v/userResourcemod/web.go @@ -0,0 +1,25 @@ +package userResourcemod + +// ResourceListReq 用户资源配置查询 +type ResourceListReq struct { + PageNumber int `form:"pageNumber" json:"pageNumber"` + PageSize int `form:"pageSize" json:"pageSize"` +} + +// ResourceModifyReq 用户资源配置修改 +type ResourceModifyReq struct { + ID string `form:"id" json:"id"` + Resource *string `json:"resource" form:"resource"` //资源 + Type *string `json:"type" form:"type"` //类型,bg:背景, avatar:头像 +} + +// ResourceAddReq 用户资源配置新增 +type ResourceAddReq struct { + Resource string `json:"resource" form:"resource"` //资源 + Type string `json:"type" form:"type"` //类型,bg:背景, avatar:头像 +} + +// ResourceDelReq 用户资源配置删除 +type ResourceDelReq struct { + ID string `form:"id" json:"id"` +} diff --git a/models/v/useractmod/type.go b/models/v/useractmod/type.go new file mode 100644 index 0000000..47d684a --- /dev/null +++ b/models/v/useractmod/type.go @@ -0,0 +1,31 @@ +package useractmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + IsNoVip uint64 = 0 + Begin string = "BEGIN" +) + +// 用户行为记录 +type UserAct struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + VID primitive.ObjectID `json:"vid" bson:"vid"` //视频ID + PlayWay uint64 `json:"playWay" bson:"playWay"` //观看方式 0:非vip方式观看 1:vip观看 + UID uint64 `json:"uid" bson:"uid"` //用户id + AddPoint string `json:"addPoint" bson:"addPoint"` //开始计算恢复观看次数的标志 标识:Begin + DailyDate time.Time `json:"dailyDate" bson:"dailyDate"` //日期 整点日期 + CreatedAt time.Time `json:"createAt" bson:"createdAt"` + ConsumeKey string `json:"-" bson:"consumeKey,omitempty"` // 每日免费观看视频消费幂等键 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/useractmod/useract.go b/models/v/useractmod/useract.go new file mode 100644 index 0000000..dffc30c --- /dev/null +++ b/models/v/useractmod/useract.go @@ -0,0 +1,135 @@ +package useractmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.UserAct + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "vid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "dailyDate", Value: 1}}, + }, + { + Keys: bson.D{{Key: "vid", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "addPoint", Value: 1}}, + }, + { + Keys: bson.D{{Key: "consumeKey", Value: 1}}, + Options: options.Index(). + SetName("uniq_daily_free_watch_consume"). + SetUnique(true). + SetPartialFilterExpression(bson.M{"consumeKey": bson.M{"$gt": ""}}), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("user model set index err ==>[%+v]", err)) + } +} + +// UserActUpsert 更新用户观看次数 +func UserActUpsert(uid uint64, duration time.Time, watchCount int64) error { + filter := bson.M{"uid": uid, "dailyDate": duration} + update := bson.M{"$inc": bson.M{"addWatchCount": watchCount}, "$set": bson.M{"addUpdatedAt": time.Now()}} + if _, err := coll(nil).UpsertOne(filter, update); err != nil { + log.ZapLog.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UserActUpsert", table, "UpsertOne", err)) + return err + } + return nil +} + +// UserActInsert 生成用户观看记录 +func UserActInsert(act UserAct) error { + return UserActInsertTrans(nil, act) +} + +// UserActInsertTrans 生成用户观看记录,支持复用Mongo事务。 +func UserActInsertTrans(t *db.MongoTool, act UserAct) error { + if _, err := coll(t).InsertOne(act); err != nil { + log.ZapLog.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UserActInsert", table, "InsertOne", err)) + return err + } + return nil +} + +// ActCount 查看用户观看次数 只统计通过非vip观看的次数 +func ActCount(uid uint64, duration time.Time) int64 { + filter := bson.M{"uid": uid, "dailyDate": duration, "playWay": IsNoVip} + cnt, err := coll(nil).Count(filter) + if err != nil { + log.ZapLog.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ActCount", table, "Count", err)) + return 0 + } + return cnt +} + +// IsViewTodayByNoVip +func IsViewTodayByNoVip(uid uint64, vid primitive.ObjectID, duration time.Time) bool { + result, err := IsViewTodayByNoVipTrans(nil, uid, vid, duration) + if err != nil { + log.ZapLog.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsViewTodayByNoVip", table, "Exists", err)) + return false + } + return result +} + +// IsViewTodayByNoVipTrans 查询当天是否已通过非VIP次数观看,支持复用Mongo事务。 +func IsViewTodayByNoVipTrans(t *db.MongoTool, uid uint64, vid primitive.ObjectID, duration time.Time) (bool, error) { + filter := bson.M{"uid": uid, "dailyDate": duration, "vid": vid, "playWay": IsNoVip} + return coll(t).Exists(filter) +} + +// UserActFindByUid +func UserActFindByUid(uid uint64, duration time.Time) (u *UserAct, err error) { + filter := bson.M{"uid": uid, "dailyDate": duration} + if err = coll(nil).FindOne(&u, filter); err != nil { + log.ZapLog.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UserActUpsert", table, "UpsertOne", err)) + return + } + return +} + +// UserActFindByUidDesc +func UserActFindByUidDesc(uid uint64, duration time.Time) (u []*UserAct, err error) { + filter := bson.M{"uid": uid, "dailyDate": duration, "addPoint": Begin} + opt := options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}}).SetLimit(1) + if err = coll(nil).Find(&u, filter, opt); err != nil { + log.ZapLog.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UserActFindByUidDesc", table, "FindOne", err)) + return + } + return +} + +func DeleteBeforeDailyDate(t *db.MongoTool, tm time.Time) error { + _, err := coll(t).DeleteMany(bson.M{"dailyDate": bson.M{"$lt": tm}}) + return err +} diff --git a/models/v/usercheckinmod/type.go b/models/v/usercheckinmod/type.go new file mode 100644 index 0000000..9549741 --- /dev/null +++ b/models/v/usercheckinmod/type.go @@ -0,0 +1,26 @@ +package usercheckinmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type UserCheckin struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + Date time.Time `json:"date" bson:"date"` // 签到日期 + UserId uint64 `json:"userId" bson:"userId"` // 用户ID + Prizes []primitive.ObjectID `json:"prizes" bson:"prizes"` // 领取的奖励id + Gave bool `json:"gave" bson:"gave"` // 是否发放基础奖品 + VipPrizeGave bool `json:"vipPrizeGave" bson:"vipPrizeGave"` // 是否领取VIP奖品 + ContinuouslyDays int64 `json:"continuouslyDays" bson:"continuouslyDays"` // 连续签到天数(本月) + CumulativeDays int64 `json:"cumulativeDays" bson:"cumulativeDays"` // 累计签到天数(本月) + IsReset bool `json:"isReset" bson:"isReset"` // 是否已重置 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间: 签到准确时间 +} + +type CheckinList []*UserCheckin + +var mdb *db.MongoDB diff --git a/models/v/usercheckinmod/usercheckin.go b/models/v/usercheckinmod/usercheckin.go new file mode 100644 index 0000000..ce2a8b5 --- /dev/null +++ b/models/v/usercheckinmod/usercheckin.go @@ -0,0 +1,115 @@ +package usercheckinmod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.UserCheckin + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "userId", Value: 1}, {Key: "date", Value: -1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "userId", Value: 1}}, + }, + { + Keys: bson.D{{Key: "date", Value: -1}}, + Options: options.Index().SetExpireAfterSeconds(100 * 24 * 60 * 60), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +// InsertOne 插入一个签到记录 +func InsertOne(data *UserCheckin) error { + if _, err := coll(nil).InsertOne(data); err != nil { + log.Error(fmt.Sprintf("[usercheckinmod:InsertOne] failed: %+v", err)) + return err + } + return nil +} + +// FindOne 查找一个签到记录 +func FindOne(filter primitive.M) (*UserCheckin, error) { + var data UserCheckin + if err := coll(nil).FindOne(&data, filter); err != nil { + log.Error(fmt.Sprintf("[usercheckinmod:FindOne] failed: %+v", err)) + return nil, err + } + if data.ID.IsZero() { + return nil, nil + } + return &data, nil +} + +// FindMany 查找一批签到记录 +func FindMany(filter primitive.M, opts ...*options.FindOptions) (CheckinList, error) { + data := make(CheckinList, 0) + if err := coll(nil).Find(&data, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[usercheckinmod:FindMany] failed: %+v", err)) + return nil, err + } + return data, nil +} + +// UpdateOne 更新一个签到记录 +func UpdateOne(filter primitive.M, update primitive.M) error { + result, err := coll(nil).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[usercheckinmod:UpdateOne] failed: %+v", err)) + return err + } + if result.MatchedCount != 1 { + return mongo.ErrNoDocuments + } + return nil +} + +// Count 统计数量 +func Count(filter primitive.M) (int64, error) { + count, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[usercheckinmod:Count] failed: %+v", err)) + return 0, err + } + return count, nil +} + +// DeleteOne 删除一个签到记录 +func DeleteOne(filter primitive.M) (int64, error) { + result, err := coll(nil).DeleteOne(filter) + if err != nil { + log.Error(fmt.Sprintf("[usercheckinmod:DeleteOne] failed: %+v", err)) + return 0, err + } + return result.DeletedCount, nil +} diff --git a/models/v/usermod/app.go b/models/v/usermod/app.go new file mode 100644 index 0000000..6b971ee --- /dev/null +++ b/models/v/usermod/app.go @@ -0,0 +1,319 @@ +package usermod + +import ( + "reflect" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + + "91porn-server/models/v/videocoupon" +) + +const ( + DevIDLogin = 0 + TokenLogin = 1 + MobileLogin = 2 + QrCodeLogin = 3 + + CouponPositionUnUsed = "unused" // 未使用 + CouponPositionUsed = "used" // 已使用 +) + +// UserSelector 用户修改参数 +type UserSelector struct { + Gender *string `json:"gender,omitempty" bson:"gender,omitempty"` //性别 + Name *string `json:"name,omitempty" bson:"name,omitempty"` + Portrait *string `json:"portrait,omitempty" bson:"portrait,omitempty"` //头像 + Background *[]string `json:"background,omitempty" bson:"background,omitempty"` //背景图 + Summary *string `json:"summary,omitempty" bson:"summary,omitempty"` //简介 + HasPrivateZone *bool `json:"hasPrivateZone,omitempty" bson:"hasPrivateZone,omitempty"` // 已关闭私密圈权限 + Region *string `json:"region,omitempty" bson:"region,omitempty"` //地区 + Birthday *string `json:"birthday,omitempty" bson:"birthday,omitempty"` //生日 + VipLevel *int `json:"vipLevel,omitempty" bson:"vipLevel,omitempty"` //vip等级 + VipExpireDate *time.Time `json:"vipExpireDate,omitempty" bson:"vipExpireDate,omitempty"` //vip过期时间 + CoinMouthExpireDate *time.Time `json:"coinMouthExpireDate" bson:"coinMouthExpireDate"` //金币月卡过期时间 + HasLocked *bool `json:"hasLocked,omitempty" bson:"hasLocked,omitempty"` //已禁止登陆 + HasBanned *bool `json:"hasBanned,omitempty" bson:"hasBanned,omitempty"` //已禁言 + LockReason *string `json:"lockReason,omitempty" bson:"lockReason,omitempty"` //禁止登录的原因 + LockImg *string `json:"lockImg,omitempty" bson:"lockImg,omitempty"` //禁止登录的原因截图 + LockAt *time.Time `json:"lockAt,omitempty" bson:"lockAt,omitempty"` //封禁开始时间 + ForbidUpload *bool `json:"forbidUpload,omitempty" bson:"forbidUpload,omitempty"` //是否禁止上传文件 + Token *string `json:"token,omitempty" bson:"token,omitempty"` //用户令牌 + WatchCount *uint64 `json:"watchCount,omitempty" bson:"watchCount,omitempty"` //观看次数 + UpdatedAt *time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` //更新时间 + LastVisitAt *time.Time `json:"lastVisitAt,omitempty" bson:"lastVisitAt,omitempty"` //最后登陆时间 + AppLock *string `json:"appLock,omitempty" bson:"appLock,omitempty"` //设备锁 + IsDAgent *bool `json:"isDAgent,omitempty" bson:"isDAgent,omitempty"` //是商圈代理 + LastVer *string `json:"lastVer,omitempty" bson:"lastVer,omitempty"` //最后登陆版本 + TrueScore *int64 `json:"trueScore,omitempty" bson:"trueScore,omitempty"` //用户真实性得分 + IsDirect *bool `json:"isDirect,omitempty" bson:"isDirect,omitempty"` //是直推用户 + DiscCode *string `json:"districtCode,omitempty" bson:"districtCode,omitempty"` //商区码 + PromSeqe *string `json:"promSeqe,omitempty" bson:"promSeqe,omitempty"` //推广序列 + SuperUser *bool `json:"superUser" bson:"superUser,omitempty"` //大v + OfficialCert *bool `json:"officialCert" bson:"officialCert,omitempty"` //是否官方认证 + TaxLevel *int64 `json:"taxLevel" bson:"taxLevel,omitempty"` //扣税等级,默认0,3.7; + ChatPrice *int64 `json:"chatPrice" bson:"chatPrice,omitempty"` //私信价格 + LastSysType *string `json:"lastSysType,omitempty" bson:"lastSysType,omitempty"` //最后登陆系统类型 + SnapVip *int `json:"snapVip,omitempty" bson:"snapVip,omitempty"` //临时vip + OriginVipExpire *time.Time `json:"originVipExpire,omitempty" bson:"originVipExpire,omitempty"` //临时vip记录的原始vip时间 + OriginVipLevel *int `json:"originVipLevel,omitempty" bson:"originVipLevel,omitempty"` //临时vip记录的原始vip等级 + AutoFollow *bool `json:"autoFollow" bson:"autoFollow,omitempty"` //是否配置被自动关注 + HasEnteredGame *bool `json:"hasEnteredGame" bson:"hasEnteredGame,omitempty"` //是否进入过游戏 + BankActName *string `json:"bankActName,omitempty" bson:"bankActName,omitempty"` //银行卡绑定名字 + GoldVideoFreeExpire *time.Time `json:"goldVideoFreeExpire" bson:"goldVideoFreeExpire,omitempty"` //金币视频免费日期 + GoldVideoFreeLimit *int64 `json:"goldVideoFreeLimit" bson:"goldVideoFreeLimit,omitempty"` // 金币视频免费限制门槛(包含acg) + WeeklyActiveValue *int64 `json:"weeklyActiveValue" bson:"weeklyActiveValue,omitempty"` //每周活跃度 + OriginalSort *int `json:"originalSort" bson:"originalSort"` // 新版原创排序 + VideoDeduction *float64 `json:"videoDeduction,omitempty" bson:"videoDeduction,omitempty"` //视频扣量,1:博主10单扣1单, 以此类推 + VideoDeductionPayCount *int `json:"videoDeductionPayCount,omitempty" bson:"videoDeductionPayCount,omitempty"` //视频扣量购买次数 + VideoDeductionCount *int `json:"videoDeductionCount,omitempty" bson:"videoDeductionCount,omitempty"` //视频扣量次数 + MerchantAccount *string `json:"merchantAccount" bson:"merchantAccount,omitempty"` //商家账号 + HasFirstChargeWaLi *bool `json:"hasFirstChargeWaLi,omitempty" bson:"hasFirstChargeWaLi,omitempty"` //是否首充充值瓦力 + IsAdvertiser *bool `json:"isAdvertiser" bson:"isAdvertiser"` // 是否是打广告用户 + HasWhoringCard *bool `json:"hasWhoringCard,omitempty" bson:"hasWhoringCard,omitempty"` // 是否有白嫖卡 + PayVidDiscount *int `json:"payVidDiscount,omitempty" bson:"payVidDiscount,omitempty"` //支付视频折扣 + GoldVideoCoupon *[]UserGoldVideoCoupon `json:"goldVideoCoupon,omitempty" bson:"goldVideoCoupon,omitempty"` + VideoFreeExpiration *time.Time `json:"videoFreeExpiration,omitempty" bson:"videoFreeExpiration,omitempty"` //视频免费过期时间 + PassWord *string `json:"passWord,omitempty" bson:"passWord,omitempty"` //密码 + VipName *string `json:"vipName,omitempty" bson:"vipName,omitempty"` //会员卡名称 + LoufengBookDiscount *int `json:"loufengBookDiscount" bson:"loufengBookDiscount,omitempty"` + LoufengBookDiscountExpiration *time.Time `json:"loufengBookDiscountExpiration" bson:"loufengBookDiscountExpiration,omitempty"` + BannedTime *time.Time `json:"bannedTime" bson:"bannedTime,omitempty"` // 禁言时间 + AllGoldVideoFree *bool `json:"allGoldVideoFree" bson:"allGoldVideoFree,omitempty"` // 是否所有金币视频免费 + AiMateUid *string `json:"aiMateUid" bson:"aiMateUid,omitempty"` // ai伴侣uid + BroadcastExpire *time.Time `json:"broadcastExpire" bson:"broadcastExpire,omitempty"` //直播有效期 + DramaExpire *time.Time `json:"dramaExpire" bson:"dramaExpire,omitempty"` //短剧权益有效期 + +} + +// RenewDramaExpire 从当前有效期顺延短剧权益;已过期时从本次发放时间起算。 +func RenewDramaExpire(current, now time.Time, days int) time.Time { + if current.After(now) { + return current.AddDate(0, 0, days) + } + return now.AddDate(0, 0, days) +} + +func (u UserSelector) IsEmpty() bool { + return reflect.DeepEqual(u, UserSelector{}) +} + +// UserSelector 用户修改参数 +type UserModifyReq struct { + Gender *string `json:"gender,omitempty" bson:"gender,omitempty"` //性别 + Name *string `json:"name,omitempty" bson:"name,omitempty"` + Portrait *string `json:"portrait,omitempty" bson:"portrait,omitempty"` //头像 + Background *[]string `json:"background,omitempty" bson:"background,omitempty"` //背景图 + Summary *string `json:"summary,omitempty" bson:"summary,omitempty"` //简介 + Region *string `json:"region,omitempty" bson:"region,omitempty"` //地区 + Birthday *string `json:"birthday,omitempty" bson:"birthday,omitempty"` //生日 + AppLock *string `json:"appLock,omitempty" bson:"appLock,omitempty"` //设备锁 + UpdatedAt *time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` +} + +type UserOpt struct { + //显示全民代理 + ShowProxy bool `json:"showProxy"` +} + +// RechargeLevel 充值等级 +type RechargeLevel struct { + //等级 + Level int `json:"level"` + //当前位置 + Current int `json:"current"` + //当前等级最大值 + Limit int `json:"limit"` +} + +// AwardsExpireResp 奖章返回数据 +type AwardsExpireResp struct { + Number int `json:"number" bson:"number,omitempty"` // 奖章价值 + IsExpire bool `json:"isExpire" bson:"isExpire,omitempty"` // 奖章是否过期 true有效 false无效 +} + +type UserRes struct { + //年龄 + Age int `form:"age" json:"age" bson:"age"` + //作品总数 + CollectionCount int64 `form:"collectionCount" json:"collectionCount"` + //是否是vip + IsVIP bool `json:"isVip" bson:"isVip"` + //推广落地页 + PromoteURL string `json:"promoteURL" bson:"promoteURL"` + // 邀请者推广码 + InviterCode string `json:"inviterCode" bson:"inviterCode"` + //用户当前定位城市 + City string `json:"city"` + //用户购买视频总数 + BuyVidCount int64 `json:"buyVidCount"` + //用户访问web携带的Token + WebToken string `json:"webToken"` + //用户登陆方式 + LoginType int `json:"loginType"` + // 邀请者推广码 + AppStoreCode string `json:"appStoreCode" bson:"appStoreCode"` + // 视频折扣卡到期时间 + VideoDiscountExpiration time.Time `json:"videoDiscountExpiration"` + // 新版金币视频观影券 + NewGoldVideoCoupon []GoldVideoCoupon `json:"newGoldVideoCoupon"` + //我的图片作品总数 + HappinessPlazaCount int64 `form:"happinessPlazaCount" json:"happinessPlazaCount"` + TotalInvite int64 `form:"totalInvite" json:"totalInvite"` + IsFollow bool `json:"isFollow"` + //是否弹领取框 + IsPopUps bool `form:"isPopUps" json:"isPopUps"` //是否弹出领取框 + WhoringCard WhoringCardInfo `json:"whoringCard" bson:"whoringCard"` // 白嫖卡信息 + AdverAbTestShowType int `json:"adverAbTestShowType"` // 广告ab测展示类型 -1-首次免广告 0-所有广告都可以观看 其他数字N-新人注册N分钟内免广告 + ImEnabled bool `json:"imEnabled"` // IM 功能是否启用 + ImToken string `json:"imToken"` // IM 客户端登录 token + ImUserID int64 `json:"imUserId"` // IM 平台用户 ID + ImDynamicDomain string `json:"dynamicConfigDomain"` // IM SDK 动态配置域名 + ImSocketURL string `json:"socketURL"` // IM SDK WebSocket 地址 + User + UserOpt + RechargeLevel + IsUpgrade bool `json:"isUpgrade" bson:"isUpgrade"` // 是否vip升级 +} +type WhoringCardInfo struct { + HasWhoringCard bool `json:"hasWhoringCard" bson:"hasWhoringCard"` // 是否有白嫖卡 + DetailInfo []DetailInfo `json:"detailInfo" bson:"detailInfo"` // 详细信息 +} + +type DetailInfo struct { + ID primitive.ObjectID `json:"id" bson:"id"` // 白嫖卡ID + HasReturn bool `json:"hasReturn" bson:"hasReturn"` // 是否返现白嫖卡 + TotalDays int64 `json:"totalDays" bson:"totalDays"` // 需要打卡天数 + CurrentSignDays int64 `json:"currentSignDays" bson:"currentSignDays"` // 连续打卡天数 + ForgetSignDays int64 `json:"forgetSignDays" bson:"forgetSignDays"` // 忘记打卡天数 + RenewalSignDays int64 `json:"renewalSignDays" bson:"renewalSignDays"` // 补签打卡天数 + RenewalSignPrice int64 `json:"renewalSignPrice" bson:"renewalSignPrice"` // 补签打卡价格 + StartTime time.Time `json:"startTime" bson:"startTime"` // 开始时间 + EndTime time.Time `json:"endTime" bson:"endTime"` // 结束时间 + RecordTime []Record `json:"recordTime" bson:"recordTime"` // 打卡时间 +} + +type Record struct { + SignTime string `json:"signTime" bson:"signTime"` // 打卡时间 + IsSign bool `json:"isSign" bson:"isSign"` // 是否打卡 +} + +// DevLoginP 块设备登陆参数 +type DevLoginP struct { + DevID string `json:"devID"` + DevType string `json:"devType" ` //设备型号 + SysType string `json:"sysType"` //操作系统类型 安卓 IOS + Ver string `json:"ver" ` //APP版本号 + DevToken string `json:"devToken" bson:"devToken"` //设备token ios 推送用 + CutInfos string `json:"cutInfos"` //剪切板信息 + Mobile string `json:"mobile"` //手机号 + QrCnt string `json:"qrCnt"` //二维码内容 +} + +// BaseInfo 返回的用户信息 +type BaseInfo struct { + UID uint64 `json:"uid" bson:"uid"` + Name string `json:"name" bson:"name"` + Gender string `json:"gender" bson:"gender"` + Portrait string `json:"portrait" bson:"portrait"` + HasLocked bool `json:"hasLocked" bson:"hasLocked"` //已禁止登陆 + HasBanned bool `json:"hasBanned" bson:"hasBanned"` //已禁言 +} + +// BaseInfoVip 返回的用户信息 +type BaseInfoVip struct { + //用户id + UID uint64 `json:"uid"` + //姓名 + Name string `json:"name"` + //性别 + Gender string `json:"gender"` + //头像 + Portrait string `json:"portrait"` + //已禁止登陆 + HasLocked bool `json:"hasLocked"` + //已禁言 + HasBanned bool `json:"hasBanned"` + //vip登记 + VipLevel int `json:"vipLevel"` + //是否是vip + IsVip bool `json:"isVip"` + //充值等级 + RechargeLevel int `json:"rechargeLevel" ` //头像显示的vip等级,累计充值决定 + //是否是大v + SuperUser bool `json:"superUser"` //大v + //活跃值 + ActiveValue int `json:"activeValue"` //活跃度 + //是否官方认证 + OfficialCert bool `json:"officialCert"` //是否官方认证 + //年龄 + Age int `json:"age"` + //关注数 + Follows int64 `json:"follows"` + //粉丝数 + Fans int64 `json:"fans"` + TotalWorks int64 `json:"totalWorks" bson:"totalWorks"` // 总作品数 + Summary *string `json:"summary,omitempty" bson:"summary,omitempty"` // 简介 + Awards []int `json:"awards"` // 用户奖章 + UpTag string `json:"upTag" bson:"upTag"` // 用户标签 + VipName string `json:"vipName" bson:"vipName"` // 用户VIP名称 + VipExpireDate time.Time `json:"vipExpireDate" bson:"vipExpireDate"` // VIP过期时间 +} + +// UserInfo 用户基本信息和状态信息 +type UserInfo struct { + BaseInfo +} + +// LoginWithH5 h5登陆参数 +type LoginWithH5 struct { + Mobile string `json:"mobile" ` // 手机号 + Email string `json:"email"` // email + Account string `json:"account"` // 账号 + Code string `json:"code" ` // 短信码 + PassWord string `json:"passWord"` // 密码 + DevID string `json:"devID" binding:"required"` // 设备id + SysType string `json:"sysType" binding:"required"` // 操作系统类型 安卓 IOS + CutInfos string `json:"cutInfos"` // 剪切板信息 + IsAppStore bool `json:"isAppStore"` // 是否商店包用户 +} + +// LoginH5ByQRCode H5用户通过二维码登录 +type LoginH5ByQRCode struct { + Content string `json:"content" binding:"required"` // 二维码内容 +} + +// GoldVideoCouponDetail 新版观影券详情 +type GoldVideoCouponDetail struct { + Num int `json:"num"` // 观影券面值 + Count int `json:"count"` // 观影券数量 + Source videocoupon.GoldVideoCouponSource `json:"source"` // 观影券获得方式 +} + +// GoldVideoCoupon 观影券 +type GoldVideoCoupon struct { + Position string `json:"position"` // 观影券位置,如“已获得”、“已使用” + Coupons []GoldVideoCouponDetail `json:"coupons"` // 观影券 +} + +type UserRights struct { + UID uint64 `json:"uid"` // 用户ID + GoldVideoCoupons []GoldVideoCoupon `json:"goldVideoCoupons"` // 观影券 + Cards []Card `json:"cards"` // 各种会员卡特权卡信息 + GoldVideoDiscount int `json:"videoDiscount"` // 视频折扣 + VipExpireDate time.Time `json:"vipExpireDate"` // 会员到期时间 + GoldVideoFreeExpire time.Time `json:"goldVideoFreeExpire"` // 金币视频免费到期时间 +} + +type Card struct { + Name string `json:"name"` // 卡片名称Name + Expiration time.Time `json:"expiration"` // 到期时间 +} + +type ResetPasswordRequest struct { + Email string `json:"email"` // email + Mobile string `json:"mobile"` // 电话号码 + UID uint64 `json:"uid"` // 用户ID + TimeStamp int64 `json:"timestamp"` // 时间戳 + RandNum int64 `json:"randNum"` // 随机数 +} diff --git a/models/v/usermod/discStat.go b/models/v/usermod/discStat.go new file mode 100644 index 0000000..adeeb79 --- /dev/null +++ b/models/v/usermod/discStat.go @@ -0,0 +1,163 @@ +package usermod + +import ( + "fmt" + "strings" + "time" + + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/common/ysinterface/disc" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// DistrictStatKey +type DistrictStatKey struct { + DiscSeqe `bson:",inline"` //商区码 + SysType string `bson:"sysType"` //系统类型 iOS Android +} + +func (d DistrictStatKey) GetDiscCode() string { + return d.DistrictCode +} + +func (d DistrictStatKey) GetPromSeqe() string { + return d.PromSeqe +} + +func (d DistrictStatKey) GetSysType() string { + return d.SysType +} + +func (d DistrictStatKey) String() string { + if d.DiscSeqe.String() == "" { + return "" + } + return strings.ToUpper(strings.Join([]string{d.DiscSeqe.String(), d.SysType}, "-")) +} + +// DistrictStater +type DistrictStater = disc.DistrictStater + +var emptyString string + +// DiscSeqeUserCountMap 时间内商区/序列注册数 +func DiscSeqeUserCountMap(mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, (&DistrictCodeGtMatch{&emptyString}).New()) + filter := pageopt.MergeM(mats) + if len(filter) == 0 { + log.Warn("DiscSeqeUserCountMap not have filter") + return make(map[DistrictStater]int64), nil + } + opt := (&options.FindOptions{}).SetProjection(bson.M{ + "districtCode": 1, + "promSeqe": 1, + "sysType": 1, + }) + list := []DistrictStatKey{} + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DiscNewUserCountByTime", table, "Find", err)) + return nil, err + } + m := make(map[DistrictStater]int64) + for _, v := range list { + if v.String() != "" { + m[v] += 1 + } + } + return m, nil +} + +// DiscSeqeNewUserCountByTime 时间内商区/序列注册数 +func DiscSeqeNewUserCountByTime(start time.Time, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, (&commod.DiscBindAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + return DiscSeqeUserCountMap(mats...) +} + +// DiscSeqeNewUIDSMap 时间内商区注册用户 +func DiscSeqeNewUIDSMap(mats ...Matcher) (map[DistrictStater][]uint64, error) { + filter := pageopt.MergeM(mats) + opt := (&options.FindOptions{}).SetProjection(bson.M{ + "districtCode": 1, + "promSeqe": 1, + "sysType": 1, + "uid": 1, + }) + list := []struct { + DistrictStatKey `bson:",inline"` //商区码 + UID uint64 `bson:"uid"` + }{} + if err := coll(nil).Find(&list, filter, opt); err != nil { + return nil, err + } + m := make(map[DistrictStater][]uint64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v.DistrictStatKey] = append(m[v.DistrictStatKey], v.UID) + } + } + return m, nil +} + +// DiscSeqeNewUIDSMapByTime 时间内商区/序列注册用户 +func DiscSeqeNewUIDSMapByTime(start time.Time, end time.Time, mats ...Matcher) (map[DistrictStater][]uint64, error) { + mats = append(mats, (&commod.DiscBindAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + return DiscSeqeNewUIDSMap(mats...) +} + +// DiscNewUIDSMap 商区注册用户 +func DiscNewUIDSMap(mats ...Matcher) (map[DistrictStater][]uint64, error) { + filter := pageopt.MergeM(mats) + opt := (&options.FindOptions{}).SetProjection(bson.M{ + "districtCode": 1, + "uid": 1, + }) + list := []struct { + DistrictCode string `bson:"districtCode"` //商区码 + UID uint64 `bson:"uid"` + }{} + if err := coll(nil).Find(&list, filter, opt); err != nil { + return nil, err + } + m := make(map[DistrictStater][]uint64, len(list)) + for _, v := range list { + key := DistrictStatKey{DiscSeqe: DiscSeqe{DistrictCode: v.DistrictCode}} + if key.String() != "" { + m[key] = append(m[key], v.UID) + } + } + return m, nil +} + +// DiscNewUIDSMapByTime 时间内商区注册用户 +func DiscNewUIDSMapByTime(start time.Time, end time.Time, mats ...Matcher) (map[DistrictStater][]uint64, error) { + mats = append(mats, (&commod.DiscBindAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + return DiscNewUIDSMap(mats...) +} + +// DiscNewCountMapByTime 时间内商区/序列注册数 +func DiscNewCountMapByTime(start time.Time, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, (&commod.DiscBindAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + filter := pageopt.MergeM(mats) + opt := (&options.FindOptions{}).SetProjection(bson.M{ + "districtCode": 1, + "uid": 1, + }) + list := []struct { + DistrictCode string `bson:"districtCode"` //商区码 + }{} + if err := coll(nil).Find(&list, filter, opt); err != nil { + return nil, err + } + m := make(map[DistrictStater]int64, len(list)) + for _, v := range list { + key := DistrictStatKey{DiscSeqe: DiscSeqe{DistrictCode: v.DistrictCode}} + if key.String() != "" { + m[key] += 1 + } + } + return m, nil +} diff --git a/models/v/usermod/drama_expire_test.go b/models/v/usermod/drama_expire_test.go new file mode 100644 index 0000000..a764bbd --- /dev/null +++ b/models/v/usermod/drama_expire_test.go @@ -0,0 +1,27 @@ +package usermod + +import ( + "testing" + "time" +) + +func TestRenewDramaExpire(t *testing.T) { + now := time.Date(2026, 8, 26, 12, 0, 0, 0, time.UTC) + tests := []struct { + name string + current time.Time + days int + want time.Time + }{ + {name: "starts from now", days: 30, want: now.AddDate(0, 0, 30)}, + {name: "extends active entitlement", current: now.AddDate(0, 0, 10), days: 30, want: now.AddDate(0, 0, 40)}, + {name: "refunds active entitlement", current: now.AddDate(0, 0, 30), days: -7, want: now.AddDate(0, 0, 23)}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := RenewDramaExpire(tt.current, now, tt.days); !got.Equal(tt.want) { + t.Fatalf("RenewDramaExpire() = %s, want %s", got, tt.want) + } + }) + } +} diff --git a/models/v/usermod/error.go b/models/v/usermod/error.go new file mode 100644 index 0000000..550e7f0 --- /dev/null +++ b/models/v/usermod/error.go @@ -0,0 +1,13 @@ +package usermod + +import "fmt" + +// MobileHasBindError 手机已被绑定 +type MobileHasBindError struct { + UID uint64 + DuplicateMobile string +} + +func (m MobileHasBindError) Error() string { + return fmt.Sprintf("UID:%d Don't use Duplicate Mobile:%s \n", m.UID, m.DuplicateMobile) +} diff --git a/models/v/usermod/es.go b/models/v/usermod/es.go new file mode 100644 index 0000000..ade0b7b --- /dev/null +++ b/models/v/usermod/es.go @@ -0,0 +1,63 @@ +package usermod + +import ( + "fmt" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" +) + +var es *elastic.Client + +const ESTable = models.ESInfoUserTable + +func InitESIndex() { + es = elastic.Init() + var setting = elastic.M{ + "settings": elastic.M{ + "number_of_shards": elastic.NumberOfShards, + "number_of_replicas": elastic.NumberOfReplicas, + "analysis": elastic.M{ + "analyzer": elastic.M{ + "ik": elastic.M{ + "tokenizer": elastic.AnalyzerIkSmart, + }, + }, + }, + }, + "mappings": elastic.M{ + "properties": elastic.M{ + "name": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkSmart, + "search_analyzer": elastic.AnalyzerIkSmart, + }, + }, + }, + } + if err := es.CreateIndices(ESTable, setting); err != nil { + panic(fmt.Sprintf("%s index indeices err ==>[%+v]", ESTable, err)) + } +} + +func Search(keywords string, from int64, size int64) (data []*ESUserSource, err error) { + query := elastic.M{ + "query": elastic.M{ + "match": elastic.M{ + "name": keywords, + }, + }, + "from": from, + "size": size, + } + if err = es.Search(ESTable, &data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Search", table, "CommonSearch", err), + log.Any("keywords", keywords), + log.Any("from", from), + log.Any("size", size), + ) + return nil, err + } + return +} diff --git a/models/v/usermod/list.go b/models/v/usermod/list.go new file mode 100644 index 0000000..4b7d8b0 --- /dev/null +++ b/models/v/usermod/list.go @@ -0,0 +1,218 @@ +package usermod + +import ( + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Matcher = pageopt.Matcher + +const districtCode = "districtCode" + +// DistrictCodeGtMatch +type DistrictCodeGtMatch struct { + GT *string +} + +func (s *DistrictCodeGtMatch) New() Matcher { + return pageopt.NewGTMatch(districtCode, s.GT) +} + +// DistrictCodeMatch +type DistrictCodeMatch struct { + DistrictCode *string +} + +func (s *DistrictCodeMatch) New() Matcher { + return pageopt.NewAssignMatch(districtCode, s.DistrictCode) +} + +// DistrictCodeINMatch +type DistrictCodeINMatch struct { + DistrictCodeList []string +} + +func (d *DistrictCodeINMatch) New() Matcher { + return pageopt.NewInMatch(districtCode, d.DistrictCodeList) +} + +// PromSeqeMatch +type PromSeqeMatch struct { + Seqe *string +} + +func (d *PromSeqeMatch) New() Matcher { + return pageopt.NewAssignMatch("promSeqe", d.Seqe) +} + +// SysTypeMatch +type SysTypeMatch struct { + SysType *string +} + +func (s *SysTypeMatch) New() Matcher { + return pageopt.NewAssignMatch("sysType", s.SysType) +} + +// IsDirectMatch +type IsDirectMatch struct { + IsDirect *bool +} + +func (b *IsDirectMatch) New() Matcher { + return pageopt.NewAssignMatch("isDirect", b.IsDirect) +} + +// DevIDRegexMatch +type DevIDRegexMatch struct { + DevID *string +} + +func (d *DevIDRegexMatch) New() Matcher { + return pageopt.NewRegexMatch("devID", d.DevID, "i") +} + +// VipLevelMatch +type VipLevelMatch struct { + VipLevel *int +} + +func (v *VipLevelMatch) New() Matcher { + return pageopt.NewAssignMatch("vipLevel", v.VipLevel) +} + +// NameRegexMatch +type NameRegexMatch struct { + Name *string +} + +func (n *NameRegexMatch) New() Matcher { + return pageopt.NewRegexMatch("name", n.Name, "i") +} + +// MobileMatch +type MobileMatch struct { + Mobile *string +} + +func (m *MobileMatch) New() Matcher { + return pageopt.NewAssignMatch("mobile", m.Mobile) +} + +// PromotionCodeMatch +type PromotionCodeMatch struct { + PromotionCode *string +} + +func (p *PromotionCodeMatch) New() Matcher { + return pageopt.NewAssignMatch("promotionCode", p.PromotionCode) +} + +// HasPromotionCodeMatch +type HasPromotionCodeMatch struct { + IsExisted *bool +} + +func (h *HasPromotionCodeMatch) New() Matcher { + return pageopt.NewExistsMatch("promotionCode", h.IsExisted) +} + +// AutoFollowMatch +type AutoFollowMatch struct { + AutoFollow *bool +} + +func (l *AutoFollowMatch) New() Matcher { + return pageopt.NewAssignMatch("autoFollow", l.AutoFollow) +} + +// LockedMatch +type LockedMatch struct { + Locked *bool +} + +func (l *LockedMatch) New() Matcher { + return pageopt.NewAssignMatch("hasLocked", l.Locked) +} + +// BannedMatch +type BannedMatch struct { + Banned *bool +} + +func (b *BannedMatch) New() Matcher { + return pageopt.NewAssignMatch("hasBanned", b.Banned) +} + +// CanUploadMatch +type ForbidUploadMatch struct { + ForbidUpload *bool +} + +func (c *ForbidUploadMatch) New() Matcher { + return pageopt.NewAssignMatch("forbidUpload", c.ForbidUpload) +} + +// TrueScoreGTEMatch +type TrueScoreGTEMatch struct { + GTE *int64 +} + +func (b *TrueScoreGTEMatch) New() Matcher { + return pageopt.NewGTEMatch("trueScore", b.GTE) +} + +// TrueScoreMatch +type TrueScoreMatch struct { + TrueScore *int64 +} + +func (b *TrueScoreMatch) New() Matcher { + return pageopt.NewAssignMatch("trueScore", b.TrueScore) +} + +type RegisterIpMatch struct { + RegisterIp *string +} + +func (b *RegisterIpMatch) New() Matcher { + return pageopt.NewAssignMatch("registerIP", b.RegisterIp) +} + +// CreatedAtGTEAndLTMatch +type CreatedAtGTEAndLTMatch = pageopt.CreatedAtGTEAndLTMatch + +// UIDInMatch +type UIDInMatch = pageopt.UIDInMatch + +// UIDMatch +type UIDMatch = pageopt.UIDMatch + +type Sort = bson.D + +var Sort_CreatedAt_n1 = Sort{{Key: "createdAt", Value: -1}} + +type UserSlice []User + +func List(sort bson.D, skip, limit *int64, matchs ...Matcher) (UserSlice, error) { + opt := (&options.FindOptions{}) + if len(sort) != 0 { + opt.SetSort(sort) + } + if skip != nil { + opt.SetSkip(*skip) + } + if limit != nil { + opt.SetLimit(*limit) + } + filter := pageopt.MergeM(matchs) + userList := UserSlice{} + return userList, coll(nil).Find(&userList, filter, opt) +} + +func Count(matchs ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchs) + return coll(nil).Count(filter) +} diff --git a/models/v/usermod/payment_status_popup_test.go b/models/v/usermod/payment_status_popup_test.go new file mode 100644 index 0000000..1187397 --- /dev/null +++ b/models/v/usermod/payment_status_popup_test.go @@ -0,0 +1,39 @@ +package usermod + +import ( + "testing" + "time" +) + +func TestGetPaymentStatusPopupNewUserIgnoresFreeWatchCount(t *testing.T) { + now := time.Now() + + tests := []struct { + name string + watchCount uint64 + want string + }{ + { + name: "remaining free views stays new unpaid", + watchCount: 3, + want: UserPaymentStatusPopupNewUnpay, + }, + { + name: "exhausted free views stays new unpaid", + watchCount: 0, + want: UserPaymentStatusPopupNewUnpay, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + user := &User{ + CreatedAt: now.Add(-time.Hour), + WatchCount: tt.watchCount, + } + if got := user.GetPaymentStatusPopup(); got != tt.want { + t.Fatalf("GetPaymentStatusPopup() = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/models/v/usermod/type.go b/models/v/usermod/type.go new file mode 100644 index 0000000..616702e --- /dev/null +++ b/models/v/usermod/type.go @@ -0,0 +1,396 @@ +package usermod + +import ( + "encoding/json" + "time" + + "91porn-server/common/db" + "91porn-server/common/timeutil" + "91porn-server/models/commod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +const ( + _ = iota + ThreeDayVIP_SnapVip //3天临时永久vip +) + +type UserType uint8 + +const ( + UserUnkown UserType = iota + UserLoufengAgent //楼风代理 1 + UserTourists //游客 2 +) + +// 用户分层弹窗-付费状态分层 +const ( + UserPaymentStatusPopupUnkown = "unkown" // 未知 + UserPaymentStatusPopupNewUnpay = "new_unpay" // 新用户 + UserPaymentStatusPopupUnder7DayUnpay = "under_7_day_unpay" // 7天内未付费用户 + UserPaymentStatusPopupUnder7DayUnpayNoCountdown = "under_7_day_unpay_no_countdown" // 7天内未付费用户无倒计时 + UserPaymentStatusPopupOver7DayUnpay = "over_7_day_unpay" // 7天后未付费用户 + UserPaymentStatusPopupOver7DayNeedUpgrade = "over_7_day_need_upgrade" // 已付费但未达最高等级,需升级 + UserPaymentStatusPopupMaxVIPLevel = "max_vip_level" // 达到最大vip等级 + UserPaymentStatusPopupUnregistered = "unregistered" // 未注册用户 + UserPaymentStatusPopupNormal = "normal" // 正常用户 +) + +type DiscSeqe = commod.DiscSeqe + +type DiscDoc = commod.DiscDoc + +// User 用户信息 +type User struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + //用户id + UID uint64 `json:"uid" bson:"uid"` + //设备id + DevID string `json:"devID" bson:"devID"` + //设备类型 + DevType string `json:"devType" bson:"devType"` + //设备token ios 推送用 + DevToken string `json:"devToken" bson:"devToken"` + //注册IP + RegisterIP string `json:"registerIP" bson:"registerIP"` + //手机号 + Mobile string `json:"mobile" bson:"mobile"` + //性别 + Gender string `json:"gender" bson:"gender"` + //姓名 + Name string `json:"name" bson:"name"` + //头像 + Portrait string `json:"portrait" bson:"portrait"` + //背景图 + Background []string `json:"background,omitempty" bson:"background,omitempty"` + //简介 + Summary string `json:"summary" bson:"summary"` + //地区 + Region string `json:"region" bson:"region"` + //生日 + Birthday string `json:"birthday" bson:"birthday"` + VipName string `json:"vipName" bson:"vipName"` + // vip等级 0-非vip会员 1-月卡用户 2-季卡用户 3-年卡用户 4-永久会员 + VipLevel int `json:"vipLevel" bson:"vipLevel"` + //已禁止登陆 + HasLocked bool `json:"hasLocked" bson:"hasLocked"` + //禁止登录的原因 + LockReason string `json:"lockReason" bson:"lockReason"` + //封禁开始时间 + LockAt time.Time `json:"lockAt" bson:"lockAt"` + //禁止登录的原因截图 + LockImg *string `json:"lockImg,omitempty" bson:"lockImg,omitempty"` + //已禁言 + HasBanned bool `json:"hasBanned" bson:"hasBanned"` + //是否禁止上传帖子 + ForbidUpload bool `json:"forbidUpload" bson:"forbidUpload"` + //用户令牌 + Token string `json:"token" bson:"token"` + //观看次数 + WatchCount uint64 `json:"watchCount" bson:"watchCount"` + //应用锁 + AppLock string `json:"appLock" bson:"appLock"` + //用户真实性得分 + TrueScore int64 `json:"trueScore" bson:"trueScore"` + //注册时登陆版本 + Ver string `json:"ver" bson:"ver"` + //最后登陆版本 + LastVer string `json:"lastVer" bson:"lastVer"` + //默认入口已处理版本,仅供后端判断当前版本是否首次进入 + DefaultEntryHandledVer string `json:"-" bson:"defaultEntryHandledVer,omitempty"` + //系统类型 + SysType string `json:"sysType" bson:"sysType"` + //最后登陆时间 + LastVisitAt time.Time `json:"lastVisitAt" bson:"lastVisitAt"` + //最后登陆系统类型 + LastSysType string `json:"lastSysType" bson:"lastSysType"` + //vip过期时间 + VipExpireDate time.Time `json:"vipExpireDate" bson:"vipExpireDate"` + //金币月卡过期时间 + CoinMouthExpireDate time.Time `json:"coinMouthExpireDate" bson:"coinMouthExpireDate"` //金币月卡过期时间 + //创建时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + //修改时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + //手机绑定时间,缺省参数 不能删除omitempty + MobileBindAt *time.Time `json:"mobileBindAt" bson:"mobileBindAt,omitempty"` + //手机解绑时间,缺省参数 不能删除omitempty + MobileUnBindAt *time.Time `json:"mobileUnBindAt" bson:"mobileUnBindAt,omitempty"` + //是商区代理 + IsDAgent bool `json:"isDAgent" bson:"isDAgent"` + //推广码 + PromCode string `json:"promotionCode" bson:"promotionCode"` + //商区 + DiscDoc `bson:",inline"` + //大v + SuperUser bool `json:"superUser" bson:"superUser"` // 0不是大v 1大v + SuperUserExpire time.Time `json:"superUserExpire" bson:"superUserExpire"` // 大V 持续时间 + MerchantUser int `json:"merchantUser" bson:"merchantUser"` // 商家认证,0:用户,1商家 + MerchantUserExpire time.Time `json:"merchantUserExpire" bson:"merchantUserExpire"` // 商家认证持续时间 + //活跃度 + ActiveValue int `json:"activeValue" bson:"activeValue"` + //是否官方认证 + OfficialCert bool `json:"officialCert" bson:"officialCert"` + //扣税等级,默认0,3.7; + TaxLevel int64 `json:"taxLevel" bson:"taxLevel"` + //登陆天数 + LoginDays int64 `json:"loginDays" bson:"loginDays"` + //获得打赏的金额 + Rewarded decimal.Decimal `json:"rewarded" bson:"rewarded"` + //临时vip + SnapVip bool `json:"snapVip" bson:"snapVip"` + //临时vip记录的原始vip时间 + OriginVipExpire *time.Time `json:"-" bson:"originVipExpire,omitempty"` + //临时vip记录的原始vip等级 + OriginVipLevel *int `json:"-" bson:"originVipLevel,omitempty"` + // 是否有白嫖卡 + HasWhoringCard bool `json:"hasWhoringCard" bson:"hasWhoringCard"` + //楼凤全免过期时间 + LouFengQmExpiration *time.Time `json:"louFengQmExpiration,omitempty" bson:"louFengQmExpiration,omitempty"` + //是否配置被自动关注 + AutoFollow bool `json:"autoFollow" bson:"autoFollow"` + //是否进入过游戏 + HasEnteredGame bool `json:"hasEnteredGame" bson:"hasEnteredGame"` + //赠送楼凤解锁次数 + LouFengUnlockTimes int `json:"louFengUnlockTimes" bson:"louFengUnlockTimes"` + //金币视频免费日期 + GoldVideoFreeExpire time.Time `json:"goldVideoFreeExpire" bson:"goldVideoFreeExpire"` + //每周活跃度 + WeeklyActiveValue int64 `json:"weeklyActiveValue" bson:"weeklyActiveValue"` + //银行卡绑定名字 + BankActName string `json:"bankActName" bson:"bankActName"` + // 新版原创排序 + OriginalSort int `json:"originalSort" bson:"originalSort"` + //应用中心下载信息 + PUC string `json:"puc" bson:"puc"` + //视频扣量,1:博主10单扣1单, 以此类推 + VideoDeduction float64 `json:"videoDeduction" bson:"videoDeduction"` + //视频扣量购买次数 + VideoDeductionPayCount int `json:"videoDeductionPayCount" bson:"videoDeductionPayCount"` + //视频扣量次数 + VideoDeductionCount int `json:"videoDeductionCount" bson:"videoDeductionCount"` + //图片被购买数量(作为作者) 此项目暂不对外暴露 因此设置`json:"-"` + CoverPayCount int `json:"-" bson:"coverPayCount"` + //是否首充充值瓦力 + HasFirstChargeWaLi bool `json:"hasFirstChargeWaLi" bson:"hasFirstChargeWaLi"` + //是否充值棋牌 + HasFirstChargeVID bool `json:"hasFirstChargeVID" bson:"hasFirstChargeVID"` + //商家账号 + MerchantAccount string `json:"merchantAccount" bson:"merchantAccount"` + //支付视频折扣 + PayVidDiscount int `json:"payVidDiscount" bson:"payVidDiscount"` + //金币视频免费券 + GoldVideoCoupon []UserGoldVideoCoupon `json:"goldVideoCoupon,omitempty" bson:"goldVideoCoupon,omitempty"` + //视频免费过期时间--仅对金币数少于特定数目的视频有效 + VideoFreeExpiration *time.Time `json:"videoFreeExpiration" bson:"videoFreeExpiration,omitempty"` + //密码 + PassWord string `json:"passWord" bson:"passWord"` + //邮箱 + Email string `json:"email" bson:"email"` + //通过邮箱验证时间 + EmailCheckedAt *time.Time `json:"emailCheckedAt" bson:"emailCheckedAt,omitempty"` + //用户自己设定的账号 用于找回账号重设密码 + Account string `json:"account" bson:"account"` + //账号绑定时间 + AccountBindAt *time.Time `json:"accountBindAt" bson:"accountBindAt"` + //用户类型 + Type UserType `json:"userType" bson:"userType"` + //天数 + LoufengBookExpiration time.Time `json:"loufengBookDiscountExpiration" bson:"loufengBookDiscountExpiration"` + TotalWorks int64 `json:"totalWorks" bson:"totalWorks"` // 总作品数 + VidUploadCount int64 `json:"vidUploadCount" bson:"vidUploadCount"` // 视频上传次数 + CoverUploadCount int64 `json:"coverUploadCount" bson:"coverUploadCount"` // 图片帖子上传次数 + AiUndressCount int64 `json:"aiUndressCount" bson:"aiUndressCount"` // ai脱衣剩余次数 + Awards []int `json:"awards" bson:"awards,omitempty"` // 奖励 + AwardsExpire []AwardsExpire `json:"awardsExpire" bson:"awardsExpire,omitempty"` // 奖章过期时间 + Follows int64 `json:"follows" bson:"follows"` // 关注数 + Fans int64 `json:"fans" bson:"fans"` // 粉丝数 + UpTag string `json:"upTag" bson:"upTag"` // 博主认证 + LikeCount int `json:"likeCount" bson:"likeCount"` // 点赞数 + ChatPrice int64 `json:"chatPrice" bson:"chatPrice,omitempty"` // 私信价格 + IsAdvertiser bool `json:"isAdvertiser" bson:"isAdvertiser"` // 是否是打广告用户 + AllGoldVideoFree bool `json:"allGoldVideoFree" bson:"allGoldVideoFree"` // 是否所有金币视频免费 + BannedTime time.Time `json:"bannedTime" bson:"bannedTime"` // 禁言时间 + AiMateUid string `json:"aiMateUid" bson:"aiMateUid"` // ai伴侣uid,对应第三方服务 + BroadcastExpire time.Time `json:"broadcastExpire" bson:"broadcastExpire,omitempty"` //直播有效期 + DramaExpire time.Time `json:"dramaExpire" bson:"dramaExpire,omitempty"` //短剧权益有效期 + GoldVideoFreeLimit int64 `json:"goldVideoFreeLimit" bson:"goldVideoFreeLimit"` + SendMsgPrice int64 `json:"sendMsgPrice"` // 发送私信价格 + IsRegister bool `json:"isRegister"` // 是否注册 + Tid string `json:"tid" bson:"tid"` // 粘贴板传过来的tid(traceId) + //已开启私密圈 + HasPrivateZone bool `json:"hasPrivateZone" bson:"hasPrivateZone"` + OriVer string `json:"oriVer" bson:"oriVer"` // 原始版本 + AdGroup commod.AdGroup `json:"adGroup" bson:"adGroup"` // 广告组 +} + +// AwardsExpire 奖章过期时间 +type AwardsExpire struct { + AwardID primitive.ObjectID `json:"awardID" bson:"awardID,omitempty"` // 奖章 + Number int `json:"number" bson:"number,omitempty"` // 奖章价值 + AwardExpire time.Time `json:"awardExpire" bson:"awardExpire,omitempty"` // 奖章过期时间 +} + +type UploadCountInc struct { + TotalWorks *int64 `bson:"totalWorks,omitempty"` //视频上传次数 + VidUploadCount *int64 `bson:"vidUploadCount,omitempty"` //视频上传次数 + CoverUploadCount *int64 `bson:"coverUploadCount,omitempty"` //图片帖子上传次数 +} + +// LoufengAgent 楼凤经纪人 +type LoufengAgent struct { + //楼凤经纪人账号,由统计中心同步过来 + Account string `json:"agentAccount" bson:"agentAccount"` + //是否可赔付 + Payable bool `json:"payable" bson:"payable"` + //保证金 + Deposit int `json:"deposit" bson:"deposit"` + //简介 + Introduce string `json:"introduce" bson:"introduce"` +} + +// 用户金币视频抵用券 +type UserGoldVideoCoupon struct { + //金币 + Gold int `json:"gold" bson:"gold"` + //券数量 + Count int `json:"count" bson:"count"` +} + +type ESUser struct { + ID primitive.ObjectID `json:"id"` + UID uint64 `json:"uid"` //用户id + Gender string `json:"gender"` //性别 + Name string `json:"name"` + Summary string `json:"summary"` //简介 + Region string `json:"region"` //地区 + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + HasLocked bool `json:"hasLocked"` //已禁止登陆 + HasBanned bool `json:"hasBanned"` //已禁言 +} + +type ESUserSource struct { + ID string `json:"_id"` + Source ESUser `json:"_source"` +} + +// UserStat 用户信息统计 +type UserStat struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +func (u *User) Age() int { + bir := u.Birthday + bir = bir + " 00:00:00" + btm, _ := time.ParseInLocation("2006-01-02 15:04:05", bir, time.Local) + if btm.IsZero() { + return 0 + } + return timeutil.Age(btm, time.Now()) +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +// IsNewUser reports whether the user registered less than 24 hours ago. +func (u *User) IsNewUser(now time.Time) bool { + return u != nil && !u.CreatedAt.IsZero() && now.Before(u.CreatedAt.Add(24*time.Hour)) +} + +func (u User) IsPaidVIP() bool { + return u.VipExpireDate.After(time.Now()) && (u.VipLevel >= 1) +} + +func (u User) IsVIP(now time.Time) bool { + return u.VipExpireDate.After(now) +} + +func (u *User) GetSuperUser(now time.Time) bool { + if u.SuperUserExpire.Before(now) { + return false + } else { + return u.SuperUser + } +} + +// VisitValue +type VisitValue struct { + UID uint64 `json:"uid"` + SysType string `json:"sysType"` +} + +// RegistValue +type RegistValue = VisitValue + +func (v VisitValue) JsonString() string { + ct, _ := json.Marshal(v) + return string(ct) +} + +func GetGoldVideoFreeLimit(u *User) int64 { + if u == nil { + return 0 + } + // 已经过了金币免费,则只有0 + if !u.GoldVideoFreeExpire.After(time.Now()) { + return 0 + } + var goldVideoFreeLimit int64 = 50 + if u == nil { + return goldVideoFreeLimit + } + // 按照用户自身的限制门槛来判断 + if u.GoldVideoFreeLimit > 0 { + goldVideoFreeLimit = u.GoldVideoFreeLimit + } + return goldVideoFreeLimit +} + +// GetPaymentStatusPopup 计算用户付费状态分层,用于下发不同的分层弹窗配置。 +// 注:91porn 的 VipLevel 语义为 0-非会员 1-月卡 2-季卡 3-年卡 4-永久会员,故最高等级为 4。 +func (u *User) GetPaymentStatusPopup() string { + if u == nil { + return UserPaymentStatusPopupUnregistered + } + + now := time.Now() + // 新用户:注册时间在一天以内,且不是付费会员。 + // 旧版分层只按注册时间和付费状态判断,免费观看次数由新版付费引导单独处理。 + if u.IsNewUser(now) && !u.IsPaidVIP() { + return UserPaymentStatusPopupNewUnpay + } + + // 7天内未付费用户 + if u.CreatedAt.AddDate(0, 0, 7).After(now) && !u.IsPaidVIP() { + if u.CreatedAt.AddDate(0, 0, 2).After(now) && !u.IsPaidVIP() { // 两天内未付费用户使用带倒计时图片源 + return UserPaymentStatusPopupUnder7DayUnpay + } + return UserPaymentStatusPopupUnder7DayUnpayNoCountdown + } + + // 7天后仍未付费用户 + if u.CreatedAt.AddDate(0, 0, 7).Before(now) && !u.IsPaidVIP() { + return UserPaymentStatusPopupOver7DayUnpay + } + + if u.IsPaidVIP() { + // 已付费,但未达最高等级(永久会员),引导续费升级 + if !u.VipExpireDate.IsZero() && u.VipLevel < 4 { + return UserPaymentStatusPopupOver7DayNeedUpgrade + } + // 已达最高等级(永久会员) + if !u.VipExpireDate.IsZero() && u.VipLevel == 4 { + return UserPaymentStatusPopupMaxVIPLevel + } + } + + return UserPaymentStatusPopupNormal +} diff --git a/models/v/usermod/user.go b/models/v/usermod/user.go new file mode 100644 index 0000000..e2eb8a8 --- /dev/null +++ b/models/v/usermod/user.go @@ -0,0 +1,1512 @@ +package usermod + +import ( + "encoding/json" + "fmt" + "strconv" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/common/redis" + "91porn-server/common/timeutil/timerange" + "91porn-server/models" + "91porn-server/models/commod" + "91porn-server/skd/skdg" + "91porn-server/web/webg" + + "github.com/pkg/errors" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// SystemDevIDPrex 马甲账号devID前缀 +const SystemDevIDPrex = "system-5rG1eq35Y0F102Qt3BwN2y" + +const table = models.UserTable + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "devID", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "channel", Value: 1}}, + }, + { + Keys: bson.D{{Key: "token", Value: 1}}, + Options: options.Index().SetUnique(true).SetPartialFilterExpression(bson.M{"token": bson.M{"$gt": ""}}), + }, + { + Keys: bson.D{{Key: "promotionCode", Value: 1}}, + Options: options.Index().SetUnique(true).SetPartialFilterExpression(bson.M{"promotionCode": bson.M{"$gt": ""}}), + }, + { + Keys: bson.D{{Key: "vipExpireDate", Value: 1}}, + Options: options.Index().SetPartialFilterExpression(bson.M{"vipExpireDate": bson.M{"$gt": time.Time{}}}), + }, + { + Keys: bson.D{{Key: "mobile", Value: 1}}, + Options: options.Index().SetUnique(true).SetPartialFilterExpression(bson.M{"mobile": bson.M{"$gt": ""}}), + }, + { + Keys: bson.D{{Key: "mobileBindAt", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{{Key: "mobileUnBindAt", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "discBindAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "name", Value: -1}}, + }, + { + Keys: bson.D{{Key: "hasBanned", Value: 1}, {Key: "createdAt", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{{Key: "hasLocked", Value: 1}, {Key: "createdAt", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{{Key: "districtCode", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "districtCode", Value: 1}, {Key: "discBindAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "trueScore", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "isDirect", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "isDirect", Value: 1}, {Key: "discBindAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "districtCode", Value: 1}, {Key: "promotionSeqe", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "districtCode", Value: 1}, {Key: "promotionSeqe", Value: 1}, {Key: "discBindAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "districtCode", Value: 1}, {Key: "isDirect", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "districtCode", Value: 1}, {Key: "isDirect", Value: 1}, {Key: "discBindAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "autoFollow", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "registerIP", Value: 1}}, + }, + { + Keys: bson.D{{Key: "officialCert", Value: 1}, {Key: "originalSort", Value: -1}}, + }, + { + Keys: bson.D{{Key: "account", Value: 1}}, + }, + { + Keys: bson.D{{Key: "imUserId", Value: 1}}, + Options: options.Index().SetSparse(true), + }, + { + // 用于筛选 + Keys: bson.D{{Key: "userType", Value: 1}, {Key: "createdAt", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("user model set index err ==>[%+v]", err)) + } +} + +func findUser(cond bson.M) (*User, error) { + var u User + if err := coll(nil).FindOne(&u, cond); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "findUser", table, "FindOne", err), + log.Any("cond", cond), + ) + return nil, err + } + if u.UID == 0 { + return nil, nil + } + return &u, nil +} + +func findUsers(cond bson.M, opts ...*options.FindOptions) ([]*User, error) { + var u []*User + if err := coll(nil).Find(&u, cond, opts...); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "findUsers", table, "Find", err), + log.Any("cond", cond), + ) + return u, err + } + return u, nil +} + +func findUsersMap(cond bson.M) (map[uint64]*User, error) { + us, err := findUsers(cond) + if err != nil { + return nil, err + } + m := make(map[uint64]*User) + for _, u := range us { + if u == nil { + continue + } + m[u.UID] = u + } + return m, nil +} + +func UserMap(uidList []uint64) (map[uint64]*User, error) { + if len(uidList) == 0 { + return make(map[uint64]*User), nil + } + filter := bson.M{"uid": bson.M{"$in": uidList}} + return findUsersMap(filter) +} + +func userCount(cond bson.M) (int64, error) { + return coll(nil).Count(cond) +} + +func FindUIDByCreateTime(start time.Time, end time.Time) ([]uint64, error) { + filter := bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + users, err := findUsers(filter) + if err != nil { + return nil, err + } + uidList := make([]uint64, len(users)) + for i, user := range users { + uidList[i] = user.UID + } + return uidList, nil +} + +// FindUsersByUID uids查找用户 +func FindUsersByUID(uids []uint64) ([]*User, error) { + return findUsers(bson.M{"uid": bson.M{"$in": uids}}) +} + +// FindUsersByUID uids查找用户 +func FindUsersMapByUID(uids []uint64) (map[uint64]*User, error) { + users, err := findUsers(bson.M{"uid": bson.M{"$in": uids}}) + if err != nil { + return nil, err + } + var uMap = make(map[uint64]*User) + for _, v := range users { + _v := v + uMap[v.UID] = _v + } + return uMap, nil +} + +// FindUsersByKeyword 通过关键字获取用户列表 +func FindUsersByKeyword(keyword string, skip int64, limit int64) ([]*User, error) { + opt := (&options.FindOptions{}).SetSkip(skip).SetLimit(limit) + filter := bson.M{ + "name": bson.M{ + "$regex": fmt.Sprintf("^%s", keyword), + }, + } + return findUsers(filter, opt) +} + +// FindUsersByCreateTime 新用户列表 +func NewUserList(skip int64, limit int64) ([]*User, error) { + sort := bson.D{{Key: "updatedAt", Value: -1}} + opt := (&options.FindOptions{}).SetSort(sort).SetSkip(skip).SetLimit(limit) + return findUsers(bson.M{}, opt) +} + +// InsertUser 插入用户 +func InsertUser(u *User) error { + now := time.Now() + u.CreatedAt = now + u.UpdatedAt = now + if u.Mobile != "" { + u.MobileBindAt = &now + } + if _, err := coll(nil).InsertOne(u); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertUser", table, "InsertOne", err), + log.Any("u", u), + ) + return err + } + return nil +} + +// FindUserByDevID 设备id查找用户 +func FindUserByDevID(devID string) (*User, error) { + return findUser(bson.M{"devID": devID}) +} + +// FindUserByToken token查找用户 +func FindUserByToken(token string) (*User, error) { + return findUser(bson.M{"token": token}) +} + +func FindUserByUIDForNoCache(uid uint64) (*User, error) { + return findUser(bson.M{"uid": uid}) +} + +func RefreshCache(uid uint64) { + redisCachDel(uid) +} + +func RefreshCacheAndGetUser(uid uint64) (*User, error) { + redisCachDel(uid) + return FindUserByUID(uid) +} + +func FindUserByUIDTrans(t *db.MongoTool, uid uint64) (*User, error) { + var u User + if err := coll(t).FindOne(&u, bson.M{"uid": uid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "findUser", table, "FindUserByUIDTrans", err)) + return nil, err + } + if u.UID == 0 { + return nil, nil + } + return &u, nil +} + +// FindUserByUID 根据uid获取用户信息。首先尝试缓存,若从缓存中获取失败,再去DB获取用户信息。 +func FindUserByUID(uid uint64) (*User, error) { + redisKey := redisconst.DataCachKey(table, strconv.FormatUint(uid, 10)) + redisc := getRedis() + str, err := redisc.Get(redisKey) + if err != nil { // redis 错误不向上报告 + log.Error("FindUserByUID redisc.Get", log.Any("uid", uid), log.Any("redisKey", redisKey), log.E(err)) + } + user := &User{} + if str != nil { + if err = json.Unmarshal([]byte(*str), user); err == nil { + return user, nil + } + // json.Unmarshal的错误不向上报告,而是尝试去DB获取用户 + log.Error("FindUserByUID json.Unmarshal", log.Any("uid", uid), log.Any("redisKey", redisKey), log.E(err)) + } + user, err = findUser(bson.M{"uid": uid}) + if err != nil { + return nil, err + } + if user == nil { + return nil, errors.New("empty user") + } + common.Go(func() { + jsonBytes, err := json.Marshal(user) + if err != nil { + log.Error("FindUserByUID json.Marshal", log.Any("uid", uid), log.E(err)) + return + } + if err := redisc.Set(redisKey, string(jsonBytes), redisconst.DataCachExpire); err != nil { + log.Error("FindUserByUID redisc.Set", log.Any("uid", uid), log.Any("redisKey", redisKey), log.E(err)) + } + }) + return user, err +} + +// FindUserPromotionCode uid查找用户 +func FindUserPromotionCode(promotionCode string) (*User, error) { + return findUser(bson.M{"promotionCode": promotionCode}) +} + +// FindUserByMobile手机号查找用户 +func FindUserByMobile(mobile string) (*User, error) { + return findUser(bson.M{"mobile": mobile}) +} + +// FindUserByAccount 根据账号查找用户 +func FindUserByAccount(account string) (*User, error) { + return findUser(bson.M{"account": account}) +} + +func findOneAndUpdateUser(t *db.MongoTool, filter bson.M, up bson.M) (*User, error) { + up["updatedAt"] = time.Now() + u := &User{} + if err := coll(t).FindOneAndUpdate(u, filter, bson.M{"$set": up}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "findOneAndUpdateUser", table, "FindOneAndUpdate", err)) + return u, err + } + return u, nil +} + +func updateUsers(t *db.MongoTool, filter bson.M, up bson.M) (*mongo.UpdateResult, error) { + up["updatedAt"] = time.Now() + return coll(t).UpdateMany(filter, bson.M{"$set": up}) +} + +// UpdateTrans 修改用户信息(开启事务) +func UpdateTrans(t *db.MongoTool, uid uint64, set UserSelector) (*User, error) { + defer redisCachDel(uid) + setM, err := common.ToBsonM(&set) + if err != nil { + log.ZapLog.Warn("user UpdateTrans ToBsonM fail", log.E(err)) + return nil, err + } + return findOneAndUpdateUser(t, bson.M{"uid": uid}, setM) +} + +// UpdateTrans 修改用户信息(开启事务) +func UpdateVIP(t *db.MongoTool, uid uint64, vipExpireDate time.Time, set UserSelector) error { + defer redisCachDel(uid) + now := time.Now() + set.UpdatedAt = &now + res, err := coll(t).UpdateOne(bson.M{"uid": uid, "vipExpireDate": vipExpireDate}, bson.M{"$set": set}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateVIP", table, "UpdateOne", err), + log.Any("uid", uid), + log.Any("vipExpireDate", vipExpireDate.String()), + log.Any("set", set), + ) + return err + } + if res.ModifiedCount == 0 { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateVIP", table, "res.ModifiedCount == 0", err), + log.Any("uid", uid), + log.Any("set", set), + ) + return errors.New("user UpdateVIP ModifiedCount err") + } + return nil +} + +func UpdateManyTrans(t *db.MongoTool, uid []uint64, set UserSelector) (*mongo.UpdateResult, error) { + defer redisCachDelMany(uid) + setM, err := common.ToBsonM(&set) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateManyTrans", table, "ToBsonM", err), + log.Any("uid", uid), + log.Any("set", set), + ) + return nil, err + } + return updateUsers(t, bson.M{"uid": bson.M{"$in": uid}}, setM) +} + +func UpdateMany(uids []uint64, set UserSelector) (*mongo.UpdateResult, error) { + return UpdateManyTrans(nil, uids, set) +} + +// Update ()修改用户信息 +func Update(uid uint64, set UserSelector) (*User, error) { + t := time.Now() + set.UpdatedAt = &t + return UpdateTrans(nil, uid, set) +} + +// UpdateSnapVip ()修改用户信息 +func UpdateSnapVip(uid uint64, set UserSelector) (*User, error) { + t := time.Now() + set.UpdatedAt = &t + s, _ := common.ToBsonM(set) + return findOneAndUpdateUser(nil, bson.M{"uid": uid, "snapVip": 0}, s) +} + +// UpdateUserCountWorks 更新用户作品数 +func UpdateUserCountWorks(m map[uint64]int64) error { + var writes []mongo.WriteModel + for uid, n := range m { + filter := bson.M{ + "uid": uid, + } + update := bson.M{ + "$set": bson.M{ + "totalWorks": n, + }, + } // 这里不去更新 updatedAt(更新时间),以免引起连锁更新,导致服务器高负载 + writes = append(writes, mongo.NewUpdateOneModel(). + SetFilter(filter). + SetUpdate(update), + ) + if len(writes) > 100 { + opt := options.BulkWrite().SetOrdered(false) + _, err := coll(nil).Bulk(writes, opt) + if err != nil { + return err + } + writes = writes[:0] + } + } + if len(writes) == 0 { + return nil + } + opt := options.BulkWrite().SetOrdered(false) + _, err := coll(nil).Bulk(writes, opt) + return err +} + +func ChangeVisit(uid uint64, lastVer, lastSysType string, lastVisitAt time.Time) error { + update := bson.M{ + "$set": bson.M{"lastVisitAt": lastVisitAt, "lastVer": lastVer, "lastSysType": lastSysType}, + "$inc": bson.M{"loginDays": 1}, + } + if _, err := coll(nil).UpdateOne(bson.M{"uid": uid}, update); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ChangeVisit", table, "UpdateOne", err)) + return err + } + return nil +} + +// ClaimDefaultEntryVersion 将当前 App 版本标记为已处理默认入口。 +// 同一用户同一版本只有第一次调用会返回 true,用于避免重复触发默认入口。 +func ClaimDefaultEntryVersion(uid uint64, ver string) (bool, error) { + if uid == 0 || ver == "" { + return false, nil + } + cacheKey := redisconst.DataCachKey("default_entry_version", strconv.FormatUint(uid, 10)) + redisc := getRedis() + if redisc != nil { + cachedVer, err := redisc.Get(cacheKey) + if err != nil { + log.Warn("ClaimDefaultEntryVersion cache get failed", log.Any("uid", uid), log.E(err)) + } else if cachedVer != nil && *cachedVer == ver { + return false, nil + } + } + res, err := coll(nil).UpdateOne( + bson.M{"uid": uid, "defaultEntryHandledVer": bson.M{"$ne": ver}}, + bson.M{"$set": bson.M{"defaultEntryHandledVer": ver}}, + ) + if err != nil { + log.Warn("ClaimDefaultEntryVersion failed", log.Any("uid", uid), log.E(err)) + return false, err + } + if res.ModifiedCount == 0 { + if redisc != nil { + _ = redisc.Set(cacheKey, ver, 30*24*time.Hour) + } + return false, nil + } + redisCachDel(uid) + if redisc != nil { + if err := redisc.Set(cacheKey, ver, 30*24*time.Hour); err != nil { + log.Warn("ClaimDefaultEntryVersion cache set failed", log.Any("uid", uid), log.E(err)) + } + } + return true, nil +} + +func BulkWrite(models []mongo.WriteModel) error { + _, err := coll(nil).Bulk(models, options.BulkWrite().SetOrdered(false)) + return err +} + +func ChangeEmail(uid uint64, email string) (bool, error) { + defer redisCachDel(uid) + filter := bson.M{ + "uid": uid, + "emailCheckedAt": bson.M{"$exists": true}, + } + update := bson.M{ + "$set": bson.M{ + "email": email, + "updatedAt": time.Now(), + }, + } + res, err := coll(nil).UpdateOne(filter, update) + if err != nil { + return false, errors.Wrap(err, fmt.Sprintf("change user[%d] email[%s] falied", uid, email)) + } + return res.ModifiedCount > 0, nil +} + +func ChangeMobile(uid uint64, mobile string) (bool, error) { + defer redisCachDel(uid) + //mobileBindAt存在,更换手机号 + filter := bson.M{ + "uid": uid, + "mobileBindAt": bson.M{"$exists": true}, + } + update := bson.M{ + "$set": bson.M{ + "mobile": mobile, + "updatedAt": time.Now(), + }, + } + ret, err := coll(nil).UpdateOne(filter, update) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ChangeMobile", table, "UpdateOne", err), + log.Any("uid", uid), + log.Any("mobile", mobile), + ) + switch exception := err.(type) { + case mongo.WriteException: + for _, e := range exception.WriteErrors { + if e.Code == 11000 { //https://docs.mongodb.com/manual/reference/method/db.collection.insert/index.html + return false, MobileHasBindError{uid, mobile} + } + } + } + return false, err + } + return ret.MatchedCount+ret.ModifiedCount > 0, nil +} + +func BindAccount(uid, newUid uint64, account, password, devID string) (bool, error) { + defer redisCachDel(uid) + filter := bson.M{ + "uid": uid, + "$or": []bson.M{ + {"accountBindAt": bson.M{"$exists": false}}, + {"accountBindAt": nil}, + }, + } + now := time.Now() + set := bson.M{ + "account": account, + "passWord": password, + "uid": newUid, + "userType": UserUnkown, + "devID": devID, + "accountBindAt": now, + "updatedAt": now, + "createdAt": now, + } + res, err := coll(nil).UpdateOne(filter, bson.M{"$set": set}) + if err != nil { + return false, errors.Wrap(err, fmt.Sprintf("bind user[%d] account[%s] failed", uid, account)) + } + return res.ModifiedCount > 0, nil +} + +func BindEmail(uid uint64, email, password string) (bool, error) { + defer redisCachDel(uid) + filter := bson.M{ + "uid": uid, + "$or": []bson.M{ + {"emailCheckedAt": bson.M{"$exists": false}}, + {"emailCheckedAt": nil}, + }, + } + now := time.Now() + set := bson.M{ + "email": email, + "emailCheckedAt": now, + "updatedAt": now, + } + if password != "" { + set["passWord"] = password + } + res, err := coll(nil).UpdateOne(filter, bson.M{"$set": set}) + if err != nil { + return false, errors.Wrap(err, fmt.Sprintf("bind user[%d] email[%s] failed", uid, email)) + } + return res.ModifiedCount > 0, nil +} + +func BindMobile(t *db.MongoTool, uid uint64, mobile, passWord string) (bool, error) { + defer redisCachDel(uid) + now := time.Now() + //mobileBindAt不存在,绑定手机号 + filter := bson.M{ + "uid": uid, + "$or": bson.A{ + bson.M{"mobileBindAt": bson.M{"$exists": false}}, + bson.M{"mobileBindAt": nil}, + }, + } + set := bson.M{ + "mobile": mobile, + "mobileBindAt": now, + "updatedAt": now, + } + if passWord != "" { + set["passWord"] = passWord + } + update := bson.M{ + "$set": set, + } + if _, err := coll(t).UpdateOne(filter, update); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "BindMobile", table, "UpdateOne", err), + log.Any("uid", uid), + log.Any("mobile", mobile), + ) + return false, err + } + return true, nil +} + +func UnBindMobile(uid uint64) (bool, error) { + defer redisCachDel(uid) + //mobileBindAt存在,解绑手机号 + now := time.Now() + filter := bson.M{ + "uid": uid, + "mobileBindAt": bson.M{"$exists": true}, + } + update := bson.M{ + "$set": bson.M{ + "mobile": "", + "mobileUnBindAt": now, + "updatedAt": now, + }, + } + ret, err := coll(nil).UpdateOne(filter, update) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UnBindMobile", table, "UpdateOne", err), + log.Any("uid", uid), + ) + return false, err + } + return ret.MatchedCount+ret.ModifiedCount > 0, nil +} + +func UpdateUserToken(uid uint64, token string) (*User, error) { + return Update(uid, UserSelector{Token: &token}) +} + +func UpdateUserVipLevel(uid uint64, lvl int) (*User, error) { + return Update(uid, UserSelector{VipLevel: &lvl}) +} + +func UpdateSelf(uid uint64, set UserModifyReq) (*User, error) { + defer redisCachDel(uid) + return Update(uid, UserSelector{ + Gender: set.Gender, + Name: set.Name, + Portrait: set.Portrait, + Background: set.Background, + Summary: set.Summary, + Region: set.Region, + Birthday: set.Birthday, + AppLock: set.AppLock, + UpdatedAt: set.UpdatedAt, + }) +} + +// 扣减次数 +func DincUserWatchCount(uid uint64) (*User, error) { + defer redisCachDel(uid) + filter := bson.M{"uid": uid, "watchCount": bson.M{"$gte": 1}} + u := &User{} + if err := coll(nil).FindOneAndUpdate(u, filter, bson.M{"$inc": bson.M{"watchCount": -1}}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DincUserWatchCount", table, "FindOneAndUpdate", err), + log.Any("uid", uid), + ) + return u, err + } + return u, nil +} + +func UpdateUserWatchCount(uid uint64, count int64) (*User, error) { + defer redisCachDel(uid) + filter := bson.M{"uid": uid, "watchCount": bson.M{"$gte": 0}} + u := &User{} + if err := coll(nil).FindOneAndUpdate(u, filter, bson.M{"$set": bson.M{"watchCount": count}}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateUserWatchCount", table, "FindOneAndUpdate", err), + log.Any("uid", uid), + ) + return u, err + } + return u, nil +} + +// 后台devID重置 +func ResetDevID(uid uint64, devID string) error { + defer redisCachDel(uid) + if _, err := coll(nil).UpdateOne(bson.M{"uid": uid}, bson.M{"$set": bson.M{"devID": devID}}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ResetDevID", table, "UpdateOne", err), + log.Any("uid", uid), + log.Any("devID", devID), + ) + return err + } + return nil +} + +func GetUsersBaseInfo(uids []uint64) ([]*BaseInfo, error) { + var data []*BaseInfo + if len(uids) == 0 { + return data, nil + } + us, err := findUsers(bson.M{"uid": bson.M{"$in": uids}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetUsersBaseInfo", table, "findUsers", err), + log.Any("uids", uids), + ) + return data, err + } + data = make([]*BaseInfo, len(us)) + for i, u := range us { + data[i] = &BaseInfo{ + UID: u.UID, + Name: u.Name, + Gender: u.Gender, + Portrait: u.Portrait, + HasBanned: u.HasBanned, + HasLocked: u.HasLocked, + } + } + return data, nil +} + +func GetUsersBaseInfoWithVip(uids []uint64) ([]*BaseInfoVip, error) { + var data []*BaseInfoVip + if len(uids) == 0 { + return data, nil + } + us, err := findUsers(bson.M{"uid": bson.M{"$in": uids}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetUsersBaseInfoWithVip", table, "findUsers", err), + log.Any("uids", uids), + ) + return data, err + } + data = make([]*BaseInfoVip, len(us)) + for i, u := range us { + isVip := false + if u.VipExpireDate.After(time.Now()) { + isVip = true + } + data[i] = &BaseInfoVip{ + UID: u.UID, + Name: u.Name, + Gender: u.Gender, + Portrait: u.Portrait, + HasLocked: u.HasLocked, + HasBanned: u.HasBanned, + VipLevel: u.VipLevel, + IsVip: isVip, + //RechargeLevel: , + SuperUser: u.SuperUser, + ActiveValue: u.ActiveValue, + OfficialCert: u.OfficialCert, + Age: u.Age(), + Follows: u.Follows, + Fans: u.Fans, + TotalWorks: u.TotalWorks, + Summary: &u.Summary, + Awards: u.Awards, + UpTag: u.UpTag, + VipName: u.VipName, + VipExpireDate: u.VipExpireDate, + } + } + return data, nil +} + +func GetUsersBaseInfoVIPMap(uids []uint64) (map[uint64]*BaseInfoVip, error) { + m := make(map[uint64]*BaseInfoVip) + infos, err := GetUsersBaseInfoWithVip(uids) + if err != nil { + return m, err + } + for _, info := range infos { + m[info.UID] = info + } + return m, nil +} + +func GetUsersBaseInfoMap(uids []uint64) (map[uint64]*BaseInfo, error) { + m := make(map[uint64]*BaseInfo) + infos, err := GetUsersBaseInfo(uids) + if err != nil { + return m, err + } + for _, info := range infos { + m[info.UID] = info + } + return m, nil +} + +// CountByCreatedAt 时间内注册数 +func CountByCreatedAt(start, end time.Time, mats ...Matcher) (int64, error) { + mats = append(mats, (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + return Count(mats...) +} + +// NewAndBindInfoCountByTime +func BindCountByTime(start time.Time, end time.Time) (int64, error) { + filter := bson.M{ + "mobileBindAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + return userCount(filter) +} + +func UnBindCountByTime(start time.Time, end time.Time) (int64, error) { + filter := bson.M{ + "mobileUnBindAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + return userCount(filter) +} + +// NewAndBindCountByTime +func NewAndBindCountByTime(createStart, createEnd, start, end time.Time) (int64, error) { + filter := bson.M{ + "mobile": bson.M{ + "$ne": "", + }, + "createdAt": bson.M{ + "$gte": createStart, + "$lt": createEnd, + }, + "mobileBindAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + return userCount(filter) +} + +// ChannelNewUserCountByTime 时间内渠道注册数 +func ChannelNewUserCountByTime(start time.Time, end time.Time) (map[string]int64, error) { + pipeLine := []bson.M{ + {"$match": bson.M{"createdAt": bson.M{"$gte": start, "$lt": end}}}, + {"$group": bson.M{"_id": "$channel", "count": bson.M{"$sum": 1}}}, + } + docList := []struct { + Channel string `bson:"_id"` + Count int64 `bson:"count"` + }{} + if err := coll(nil).Aggregate(&docList, pipeLine); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ChannelNewUserCountByTime", table, "Aggregate", err), + log.Any("start", start), + log.Any("end", end), + ) + return nil, err + } + ret := make(map[string]int64, len(docList)) + for _, doc := range docList { + ret[doc.Channel] = doc.Count + } + return ret, nil +} + +// ChannelBindUserCountByTime 渠道绑定用户数 +func ChannelBindUserCountByTime(start time.Time, end time.Time) (map[string]int64, error) { + pipeLine := []bson.M{ + { + "$match": bson.M{ + "mobile": bson.M{ + "$ne": "", + }, + "mobileBindAt": bson.M{ + "$gte": start, + "$lt": end, + }, + }, + }, + { + "$group": bson.M{ + "_id": "$channel", "count": bson.M{"$sum": 1}, + }, + }, + } + docList := []struct { + Channel string `bson:"_id"` + Count int64 `bson:"count"` + }{} + if err := coll(nil).Aggregate(&docList, pipeLine); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ChannelBindUserCountByTime", table, "Aggregate", err), + log.Any("start", start), + log.Any("end", end), + ) + return nil, err + } + ret := make(map[string]int64, len(docList)) + for _, doc := range docList { + ret[doc.Channel] = doc.Count + } + return ret, nil +} + +// ChannelNewAndBindUserCountByTime 渠道新增并绑定用户数 +func ChannelNewAndBindUserCountByTime(start time.Time, end time.Time) (map[string]int64, error) { + dayRange := timerange.LocDayRange(start) + pipeLine := []bson.M{ + { + "$match": bson.M{ + "mobile": bson.M{ + "$ne": "", + }, + "createdAt": bson.M{ + "$gte": dayRange.Head, + "$lt": dayRange.Tail, + }, + "mobileBindAt": bson.M{ + "$gte": start, + "$lt": end, + }, + }, + }, + { + "$group": bson.M{ + "_id": "$channel", "count": bson.M{"$sum": 1}, + }, + }, + } + docList := []struct { + Channel string `bson:"_id"` + Count int64 `bson:"count"` + }{} + if err := coll(nil).Aggregate(&docList, pipeLine); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ChannelNewAndBindUserCountByTime", table, "Aggregate", err), + log.Any("start", start), + log.Any("end", end), + ) + return nil, err + } + ret := make(map[string]int64, len(docList)) + for _, doc := range docList { + ret[doc.Channel] = doc.Count + } + return ret, nil +} + +func UIDList(mats ...Matcher) ([]uint64, error) { + filter := pageopt.MergeM(mats) + list := []struct { + UID uint64 `bson:"uid"` + }{} + opt := (&options.FindOptions{}).SetProjection(bson.M{ + "uid": 1, + }) + if err := coll(nil).Find(&list, filter, opt); err != nil { + return nil, err + } + uidList := make([]uint64, len(list)) + for i, v := range list { + uidList[i] = v.UID + } + return uidList, nil +} + +func UIDListByCreateTime(start, end time.Time, mats ...Matcher) ([]uint64, error) { + mats = append(mats, (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New()) + return UIDList(mats...) +} + +func UIDListByCreateTimeAndUIDS(start, end time.Time, uids []uint64, mats ...Matcher) ([]uint64, error) { + mats = append(mats, (&UIDInMatch{UIDS: uids}).New()) + return UIDListByCreateTime(start, end, mats...) +} + +func UserListByCreateTimeAndUIDS(start, end time.Time, uids []uint64, mats ...Matcher) ([]User, error) { + mats = append(mats, + (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New(), + (&UIDInMatch{UIDS: uids}).New()) + filter := pageopt.MergeM(mats) + list := make([]User, 0, len(uids)) + return list, coll(nil).Find(&list, filter) +} + +func ChannelMapByCreateTime(start, end time.Time) (map[uint64]string, error) { + filter := bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + list := make([]struct { + Channel string `bson:"channel"` + UID uint64 `bson:"uid"` + }, 0) + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[uint64]string, len(list)) + for _, v := range list { + m[v.UID] = v.Channel + } + return m, nil +} + +// 获取ES同步数据 +func GetUserListByUpdateTimeRange(start time.Time, end time.Time, page int, size int) (data []User, hasNext bool, err error) { + var query = bson.M{ + "updatedAt": bson.M{"$gte": start, "$lt": end}, + } + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "_id", Value: 1}}) + opts.SetSkip(int64((page - 1) * size)).SetLimit(int64(size) + 1) + if err = coll(nil).Find(&data, query, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetUserListByUpdateTimeRange", table, "Find", err), + log.Any("start", start), + log.Any("end", end), + ) + return + } + if len(data) > size { + hasNext = true + data = data[:size] + } + + return +} + +func redisCachDel(uid uint64) { + redisKey := redisconst.DataCachKey(table, strconv.FormatUint(uid, 10)) + if _, err := getRedis().Del(redisKey); err != nil { + log.Error("redis del user, ", log.Any("err:", err.Error()), log.Any("uid:", uid)) + } +} + +func redisCachDelMany(uids []uint64) { + keys := make([]string, len(uids)) + for i, v := range uids { + keys[i] = redisconst.DataCachKey(table, strconv.FormatUint(v, 10)) + } + _, _ = getRedis().Del(keys...) +} + +func getRedis() *redis.Client { + if appg.Redis != nil { + return appg.Redis + } + if webg.Redis != nil { + return webg.Redis + } + if skdg.Redis != nil { + return skdg.Redis + } + return nil +} + +func GetLockedUIDMap() (map[uint64]byte, error) { + filter := bson.M{ + "hasLocked": true, + } + list := make([]struct { + UID uint64 `bson:"uid"` + }, 0) + opt := (&options.FindOptions{}).SetProjection(bson.M{ + "uid": 1, + }) + if err := coll(nil).Find(&list, filter, opt); err != nil { + return nil, err + } + uidMap := make(map[uint64]byte, len(list)) + for _, v := range list { + uidMap[v.UID] = 1 + } + return uidMap, nil +} + +func DistrictCodeMap(uidList []uint64) (map[uint64]string, error) { + if len(uidList) == 0 { + return make(map[uint64]string), nil + } + filter := bson.M{"uid": bson.M{"$in": uidList}} + userList, err := findUsers(filter) + if err != nil { + return nil, err + } + m := make(map[uint64]string, len(userList)) + for _, v := range userList { + m[v.UID] = v.DistrictCode + } + return m, nil +} + +func GetSystemUsers() (data []User, err error) { + if err = coll(nil).Find(&data, bson.M{"uid": bson.M{"$lte": constant.RobotUIDLimit}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetSystemUsers", table, "Find", err)) + return + } + return +} + +// SetDeductType 设置扣量类型 +func SetDeductType(t *db.MongoTool, uid uint64, deductType commod.DeductType) error { + filter := bson.M{"uid": uid} + update := bson.M{"$unset": bson.M{"deductType": deductType}} + if deductType != commod.NotDed { + update = bson.M{"$set": bson.M{"deductType": deductType}} + } + _, err := coll(t).UpdateOne(filter, update) + return err +} + +func GetCountMapByHour(discCode string, start, end time.Time, mats ...Matcher) ([]int, map[int]int64, int64, error) { + mats = append(mats, + (&DistrictCodeMatch{&discCode}).New(), + (&CreatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New(), + ) + filter := pageopt.MergeM(mats) + opt := (&options.FindOptions{}).SetProjection(bson.M{ + "createdAt": 1, + }) + var list []struct { + CreatedAt time.Time `bson:"createdAt"` //创建时间 + } + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.Error("usermod GetBuyVipCountMap error", log.E(err)) + return nil, nil, 0, err + } + m := make(map[int]int64) + startH := start.Hour() + lastH := end.Hour() + (end.Day()-start.Day())*24 + hs := make([]int, lastH-startH+1) + for i, j := startH, 0; i <= lastH; i++ { + m[i] = 0 + hs[j] = i + j++ + } + var total int64 + for _, v := range list { + hour := v.CreatedAt.Hour() + (v.CreatedAt.Day()-start.Day())*24 + m[hour] += 1 + total++ + } + return hs, m, total, nil +} + +// IncFollow 增加关注数 +func IncFollow(uid uint64) error { + cond := bson.M{"uid": uid} + update := bson.M{"$inc": bson.M{"follows": 1}} + _, err := coll(nil).UpdateOne(cond, update) + return err +} + +// IncFollowByNum 增加关注数 +func IncFollowByNum(uid uint64, num int64) error { + cond := bson.M{"uid": uid} + update := bson.M{"$inc": bson.M{"follows": num}} + _, err := coll(nil).UpdateOne(cond, update) + return err +} + +// DecFollow 减去关注数 +func DecFollow(uid uint64) error { + cond := bson.M{"uid": uid, "follows": bson.M{"$gt": 0}} + update := bson.M{"$inc": bson.M{"follows": -1}} + _, err := coll(nil).UpdateOne(cond, update) + return err +} + +// IncFans 增加粉丝数 +func IncFans(uid uint64) error { + cond := bson.M{"uid": uid} + update := bson.M{"$inc": bson.M{"fans": 1}} + _, err := coll(nil).UpdateOne(cond, update) + return err +} + +// IncLikeCount 增加点赞数 +func IncLikeCount(uid uint64) error { + cond := bson.M{"uid": uid} + update := bson.M{"$inc": bson.M{"likeCount": 1}} + _, err := coll(nil).UpdateOne(cond, update) + return err +} + +// DecFans 减去粉丝数 +func DecFans(uid uint64) error { + cond := bson.M{"uid": uid, "fans": bson.M{"$gt": 0}} + update := bson.M{"$inc": bson.M{"fans": -1}} + _, err := coll(nil).UpdateOne(cond, update) + return err +} + +// InitFansFollows 初始化关注数和粉丝数 +func InitFansFollows(uid uint64, followCnt int64, fansCnt int64) error { + cond := bson.M{"uid": uid} + update := bson.M{"$set": bson.M{"follows": followCnt, "fans": fansCnt, "followsSetFlag": true}} + _, err := coll(nil).UpdateOne(cond, update) + return err +} + +// 添加ai脱衣次数 +func IncUserAiUndress(t *db.MongoTool, uid uint64, count uint64) error { + _, err := coll(t).UpdateOne(bson.M{"uid": uid}, bson.M{"$inc": bson.M{"aiUndressCount": count}}) + return err +} + +// UpdateById 修改用户信息 +func UpdateById(t *db.MongoTool, uid uint64, bannedTime time.Time) error { + defer redisCachDel(uid) + _, err := coll(t).UpdateOne(bson.M{"uid": uid}, bson.M{"$set": bson.M{"bannedTime": bannedTime}}) + return err +} + +func DecUserAiUndress(t *db.MongoTool, uid uint64, count uint64) error { + _, err := coll(t).UpdateOne(bson.M{"uid": uid}, bson.M{"$inc": bson.M{"aiUndressCount": -int64(count)}}) + return err +} + +// IncRewarded 增加打赏金额 +func IncRewarded(uid uint64, decimal decimal.Decimal) error { + cond := bson.M{"uid": uid} + update := bson.M{"$inc": bson.M{"rewarded": decimal}} + _, err := coll(nil).UpdateOne(cond, update) + return err +} + +func StatcenterSyncList(uid uint64, size int64) (UserSlice, error) { + opt := (&options.FindOptions{}) + opt.SetLimit(size) + opt.SetSort(bson.M{"uid": 1}) + filter := bson.M{"uid": bson.M{"$gt": uid}, "userType": bson.M{"$in": []UserType{UserUnkown, UserLoufengAgent}}} + userList := UserSlice{} + return userList, coll(nil).Find(&userList, filter, opt) +} + +func GetRandomSystemUsers(size uint64) (data []*User, err error) { + data = make([]*User, 0) + var pipeline = []bson.M{ + {"$match": bson.M{"uid": bson.M{"$lte": constant.RobotUIDLimit}}}, + {"$sample": bson.M{"size": size}}, + } + if err = coll(nil).Aggregate(&data, pipeline); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetRandomSystemUsers", table, "Aggregate", err), log.Any("pipeline", pipeline)) + return + } + return +} + +// GetOriginalBlogger 获取原创博主 +func GetOriginalBlogger(skip, limit int64) (data []*User, hasNext bool, err error) { + opts := options.Find().SetSort(bson.D{{Key: "originalSort", Value: -1}}).SetSkip(skip).SetLimit(limit + 1) + cond := bson.M{"officialCert": true} + if err = coll(nil).Find(&data, cond, opts); err != nil { + return + } + if len(data) == int(limit)+1 { + hasNext = true + data = data[:limit] + } + return +} + +// 同步数据查询绑定手机用户 +func StatcenterSyncBindUserList(time time.Time, size int64) (UserSlice, error) { + opt := (&options.FindOptions{}) + opt.SetLimit(size) + opt.SetSort(bson.M{"mobileBindAt": 1}) + filter := bson.M{"mobileBindAt": bson.M{"$gt": time}} + userList := UserSlice{} + return userList, coll(nil).Find(&userList, filter, opt) +} + +func OfficialCertList(page, size uint64, subType int) ([]*User, bool, error) { + cond := bson.M{} + // 原创---工作室 + cond["officialCert"] = true + opts := options.Find().SetLimit(int64(size + 1)).SetSkip(int64((page - 1) * size)).SetSort(bson.D{{Key: "originalSort", Value: -1}}) + var users []*User + if err := coll(nil).Find(&users, cond, opts); err != nil { + return nil, false, err + } + var hasNext bool + if uint64(len(users)) > size { + hasNext = true + users = users[:size] + } + return users, hasNext, nil +} + +// PretendList 获取马甲号列表 +func PretendList(skip int64, limit int64) ([]*User, error) { + filter := bson.M{ + "uid": bson.M{ + "$gt": 100001, + "$lt": 100200, + }, + } + return findUsers(filter, options.Find().SetLimit(limit).SetSkip(skip)) +} + +// 获取马甲号总数 +func PretendCount() (int64, error) { + return coll(nil).Count(bson.M{ + "uid": bson.M{ + "$gt": 100001, + "$lt": 100200, + }, + }) +} + +// IncVideoDeduction 视频播放量更新 +func IncVideoDeduction(uid uint64, check bool) error { + defer redisCachDel(uid) + cond := bson.M{"uid": uid} + inc := bson.M{"videoDeductionPayCount": 1} + if check { + inc["videoDeductionCount"] = 1 + } + update := bson.M{"$inc": inc} + _, err := coll(nil).UpdateOne(cond, update) + return err +} + +// IncCoverCount 出售图片量更新 +func IncCoverCount(uid uint64) error { + _, err := coll(nil).UpdateOne(bson.M{"uid": uid}, bson.M{"$inc": bson.M{"coverPayCount": 1}}) + return err +} + +// IncUploadCount 上传次数更新 +func IncUploadCount(uid uint64, inc UploadCountInc) error { + defer redisCachDel(uid) + _, err := coll(nil).UpdateOne(bson.M{"uid": uid}, bson.M{"$inc": inc}) + return err +} + +// LouFengUnlockTimesIncr 增加楼凤解锁次数 +func LouFengUnlockTimesIncr(t *db.MongoTool, uid uint64, times int) error { + result, err := coll(t).UpdateOne(bson.M{"uid": uid}, bson.M{"$inc": bson.M{"louFengUnlockTimes": times}}) + if err != nil { + return err + } + if result.ModifiedCount == 0 { + return errors.New("result.ModifiedCount is 0") + } + return nil +} + +// VipInfoChange 会员信息变更 +func VipInfoChange(t *db.MongoTool, uid uint64, levle int, expire time.Time) error { + result, err := coll(t).UpdateOne(bson.M{"uid": uid}, bson.M{ + "$set": bson.M{ + "vipExpireDate": expire, + "vipLevel": levle, + }}) + if err != nil { + return err + } + if result.ModifiedCount == 0 { + return errors.New("result.ModifiedCount is 0") + } + return nil +} + +func FindByCreateTime(start time.Time, end time.Time) (data []UserStat, err error) { + filter := bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + if err = coll(nil).Find(&data, filter); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "findUsers", table, "Find", err), + log.Any("cond", filter), + ) + } + return +} + +// IncDynamic 增加动态数 +func IncDynamic(uid uint64) error { + defer redisCachDel(uid) + cond := bson.M{"uid": uid} + update := bson.M{"$inc": bson.M{"dynamics": 1}} + _, err := coll(nil).UpdateOne(cond, update) + return err +} + +// UpdateUserById 修改用户信息(开启事务) +func UpdateUserById(t *db.MongoTool, uid uint64, set UserSelector) error { + defer redisCachDel(uid) + now := time.Now() + set.UpdatedAt = &now + res, err := coll(t).UpdateOne(bson.M{"uid": uid}, bson.M{"$set": set}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateVIP", table, "UpdateOne", err), + log.Any("uid", uid), + log.Any("set", set), + ) + return err + } + if res.ModifiedCount == 0 { + err = errors.New("user UpdateVIP ModifiedCount err") + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateVIP", table, "res.ModifiedCount == 0", err), + log.Any("uid", uid), + log.Any("set", set), + ) + return err + } + return nil +} + +// DeleteGuestBeforeDate 删除游客数据 +func DeleteGuestBeforeDate(t *db.MongoTool, tm time.Time) error { + _, err := coll(t).DeleteMany(bson.M{"updatedAt": bson.M{"$lt": tm}, "userType": UserTourists}) + return err +} + +// FetchList 条件获取列表 +func FetchList(filter primitive.M, opts *options.FindOptions, count ...*int64) (out []*User, hasNext bool, err error) { + if opts == nil { + opts = options.Find() + } + + if opts.Limit == nil { + opts.SetLimit(1000) + } + + if opts.Sort == nil { + opts.SetSort(bson.D{{Key: "_id", Value: -1}}) + } + + // 不需要统计总条数 就不要创建count,避免无用的查询 + if len(count) == 1 && count[0] != nil { + *count[0], err = coll(nil).Count(filter) + if err != nil { + return nil, false, err + } + } + + limit := int(*opts.Limit) + opts.SetLimit(int64(limit + 1)) + + err = coll(nil).Find(&out, filter, opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-FetchList]==> Model %s Find fail error:%+v:", table, err), log.Any("filter", filter)) + return out, false, err + } + + hasNext = len(out) > limit + if hasNext { + out = out[:limit] + } + + return out, hasNext, nil +} diff --git a/models/v/usermod/vip_renew.go b/models/v/usermod/vip_renew.go new file mode 100644 index 0000000..5f44c66 --- /dev/null +++ b/models/v/usermod/vip_renew.go @@ -0,0 +1,12 @@ +package usermod + +import "time" + +// RenewVIPLevel returns the level that should remain after extending VIP. +// An active higher level must not be downgraded by a lower-level renewal. +func RenewVIPLevel(currentExpire, now time.Time, currentLevel, renewedLevel int) int { + if currentExpire.After(now) && currentLevel > renewedLevel { + return currentLevel + } + return renewedLevel +} diff --git a/models/v/usermod/vip_renew_test.go b/models/v/usermod/vip_renew_test.go new file mode 100644 index 0000000..c0f5d33 --- /dev/null +++ b/models/v/usermod/vip_renew_test.go @@ -0,0 +1,47 @@ +package usermod + +import ( + "testing" + "time" +) + +func TestRenewVIPLevel(t *testing.T) { + now := time.Date(2026, 8, 29, 12, 0, 0, 0, time.UTC) + tests := []struct { + name string + currentExpire time.Time + currentLevel int + renewedLevel int + want int + }{ + { + name: "preserves active higher level", + currentExpire: now.Add(24 * time.Hour), + currentLevel: 3, + renewedLevel: 1, + want: 3, + }, + { + name: "upgrades active lower level", + currentExpire: now.Add(24 * time.Hour), + currentLevel: 1, + renewedLevel: 3, + want: 3, + }, + { + name: "uses renewed level after expiration", + currentExpire: now.Add(-time.Second), + currentLevel: 3, + renewedLevel: 1, + want: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := RenewVIPLevel(tt.currentExpire, now, tt.currentLevel, tt.renewedLevel); got != tt.want { + t.Fatalf("RenewVIPLevel() = %d, want %d", got, tt.want) + } + }) + } +} diff --git a/models/v/usermod/web.go b/models/v/usermod/web.go new file mode 100644 index 0000000..411892f --- /dev/null +++ b/models/v/usermod/web.go @@ -0,0 +1,278 @@ +package usermod + +import ( + "time" + + "91porn-server/common/log" + "91porn-server/models/l/payvidlgmod" + "91porn-server/models/v/prdcthsomod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type UserListSelector struct { + UID *uint64 `form:"uid,omitempty" json:"uid,omitempty" bson:"uid,omitempty"` //用户id + DevID *string `form:"devID,omitempty" json:"devID,omitempty" bson:"devID,omitempty"` //设备id + DevType *string `form:"devType,omitempty" json:"devType,omitempty" bson:"devType,omitempty"` //设备类型 + DevToken *string `form:"devToken,omitempty" json:"devToken,omitempty" bson:"devToken,omitempty"` //设备toke`n ios 推送用 + RegisterIP *string `form:"registerIP,omitempty" json:"registerIP,omitempty" bson:"registerIP,omitempty"` //注册IP + Mobile *string `form:"mobile,omitempty" json:"mobile,omitempty" bson:"mobile,omitempty"` //手机号 + Gender *string `form:"gender,omitempty" json:"gender,omitempty" bson:"gender,omitempty"` //性别 + Channel *string `form:"channel,omitempty" json:"channel,omitempty" bson:"channel,omitempty"` + Name *string `form:"name,omitempty" json:"name,omitempty" bson:"name,omitempty"` + PromotionCode *string `form:"promotionCode,omitempty" json:"promotionCode,omitempty" bson:"promotionCode,omitempty"` //推广码 + Portrait *string `form:"portrait,omitempty" json:"portrait,omitempty" bson:"portrait,omitempty"` //头像 + Summary *string `form:"summary,omitempty" json:"summary,omitempty" bson:"summary,omitempty"` //简介 + Region *string `form:"region,omitempty" json:"region,omitempty" bson:"region,omitempty"` //地区 + Birthday *string `form:"birthday,omitempty" json:"birthday,omitempty" bson:"birthday,omitempty"` //生日 + VipLevel *int `form:"vipLevel,omitempty" json:"vipLevel,omitempty" bson:"vipLevel,omitempty"` //vip等级 + HasLocked *bool `form:"hasLocked,omitempty" json:"hasLocked,omitempty" bson:"hasLocked,omitempty"` //已禁止登陆 + HasBanned *bool `form:"hasBanned,omitempty" json:"hasBanned,omitempty" bson:"hasBanned,omitempty"` //已禁言 + Token *string `form:"token,omitempty" json:"token,omitempty" bson:"token,omitempty"` //用户令牌 + WatchCount *uint64 `from:"watchCount,omitempty" json:"watchCount,omitempty" bson:"watchCount,omitempty"` //观看次数 + BankActName *string `json:"bankActName,omitempty" bson:"bankActName,omitempty"` //银行卡绑定名字 + UpTag *string `form:"upTag" json:"upTag,omitempty" bson:"upTag,omitempty"` // 博主认证 +} + +// ExportUser 导出的用户信息 +type ExportUser struct { + UID uint64 `xlsx:"用户id"` + //DevID string `xlsx:"设备id"` + DevType string `xlsx:"设备类型"` + RegisterIP string `xlsx:"注册IP"` + Mobile string `xlsx:"手机号"` + //Gender string `xlsx:"性别"` + Channel string `xlsx:"渠道号"` + //Name string `xlsx:"用户名"` + PromotionCode string `xlsx:"推广码"` + //Summary string `xlsx:"简介"` + //Region string `xlsx:"地区"` + //Birthday string `xlsx:"生日"` + //VipLevel int `xlsx:"vip等级"` + VipExpireDate time.Time `xlsx:"vip过期时间"` + CreatedAt time.Time `xlsx:"用户创建时间"` + LastVisitAt time.Time `xlsx:"最后活跃时间"` + //MobileBindAt *time.Time `xlsx:"手机绑定时间"` + //HasLocked bool `xlsx:"已禁止登陆"` + //HasBanned bool `xlsx:"已禁言"` + WatchCount int `xlsx:"七日观看次数"` //七日观看次数,和数据库存的不同 + //VisitCount int `xlsx:"24小时内访问数"` + //TotalVideoCnt int `xlsx:"视频总数"` + //UndealVideoCnt int `xlsx:"未审核视频数"` +} + +// 用户VIP返回数据 +type UserVipInfoRes struct { + VideoDiscount int `json:"videoDiscount"` //视频折扣率 + VideoDiscountExpiration time.Time `json:"videoDiscountExpiration"` // 视频折扣卡到期时间 + VipExpireDate time.Time `json:"vipExpireDate"` //vip过期时间 + GoldVideoFreeExpire time.Time `json:"goldVideoFreeExpire"` //金币视频免费日期 + PayVidDiscount int `json:"payVidDiscount"` //支付视频折扣 + VideoFreeExpiration *time.Time `json:"videoFreeExpiration"` //视频免费过期时间 + GoldVideoCoupon []UserGoldVideoCoupon `json:"goldVideoCoupon"` +} + +func FindAgentByAccount(acc string) (user *User, err error) { + if acc == "" { + return + } + err = coll(nil).FindOne(&user, bson.M{"agentAccount": acc}) + return +} + +func FindAllPayableAgents() ([]string, error) { + var p []struct { + AgentAcc string `bson:"agentAccount"` + } + if err := coll(nil).Find(&p, bson.M{ + "agentAccount": bson.M{"$gt": ""}, + "deposit": bson.M{"$gt": 0}, + "payable": true, + "userType": UserLoufengAgent, + }, options.Find().SetProjection(bson.M{"agentAccount": 1})); err != nil { + return []string{}, err + } + agents := make([]string, len(p)) + for i, agent := range p { + agents[i] = agent.AgentAcc + } + return agents, nil +} + +// FindMany 查询所有 +func FindMany(filter bson.M, _ *options.FindOptions, skip int64, size int64) ([]*User, error) { + sort := bson.D{{Key: "createdAt", Value: -1}} + opts := options.FindOptions{} + opts.SetSort(sort).SetSkip(skip).SetLimit(size) + data := make([]*User, 0) + if err := coll(nil).Find(&data, filter, &opts); err != nil { + log.ZapLog.Warn("proxy FindMany Find fail", log.E(err)) + return data, err + } + return data, nil +} + +// ExportFindMany 导出的时候查询所有 +func ExportFindMany(filter bson.M, skip int64, size int64) ([]*User, error) { + sort := bson.D{{Key: "createdAt", Value: 1}} + filter["userType"] = bson.M{"$in": []UserType{UserUnkown, UserLoufengAgent}} + opts := options.FindOptions{} + opts.SetSort(sort).SetSkip(skip).SetLimit(size) + data := make([]*User, 0) + if err := coll(nil).Find(&data, filter, &opts); err != nil { + log.ZapLog.Warn(" ExportFindMany Find fail", log.E(err)) + return data, err + } + return data, nil +} + +// GetTotalCnt 查询数量 +func GetTotalCnt(filter bson.M, _ *options.FindOptions) (total int64, err error) { + total, err = coll(nil).Count(filter) + if err != nil { + log.ZapLog.Warn("GetTotalCnt fail", log.E(err)) + } + return +} + +// PayUIDCountByCreatedTime 消费包括 购买VIP、付费视屏 +func PayUIDCountByCreatedTime(start time.Time, end time.Time) ([]uint64, error) { + //购买VIP /分 + vUIDList, err := prdcthsomod.PayVipUIDSByCreatedTime(start, end) + if err != nil { + return nil, err + } + //付费视屏 /金币 + pUIDList, err := payvidlgmod.GetUIDListByPayTime(start, end) + if err != nil { + return nil, err + } + payUIDMap := make(map[uint64]byte, len(vUIDList)+len(pUIDList)) + //转载payUIDMap + for _, uid := range vUIDList { + payUIDMap[uid] = 1 + } + for _, uid := range pUIDList { + payUIDMap[uid] = 1 + } + //取出payUID + payUIDList := make([]uint64, len(payUIDMap)) + i := 0 + for k := range payUIDMap { + payUIDList[i] = k + i++ + } + return payUIDList, nil +} + +// NewAndPayUIDListByCreatedTime 消费包括 购买VIP、付费视屏 +func NewAndPayUIDListByCreatedTime(start time.Time, end time.Time) ([]uint64, error) { + uidList, err := FindUIDByCreateTime(start, end) + if err != nil { + return nil, err + } + vUIDList, err := prdcthsomod.PayVipUIDSByCreatedTime(start, end) + if err != nil { + return nil, err + } + pUIDList, err := payvidlgmod.GetUIDListByPayTime(start, end) + if err != nil { + return nil, err + } + payUIDMap := make(map[uint64]bool, len(uidList)) + //转载payUIDMap + for _, uid := range pUIDList { + payUIDMap[uid] = false + } + //标记消费的的UID + for _, uid := range vUIDList { + if _, ok := payUIDMap[uid]; ok { + payUIDMap[uid] = true + } + } + for _, uid := range pUIDList { + if _, ok := payUIDMap[uid]; ok { + payUIDMap[uid] = true + } + } + //取出payUID + payUIDList := make([]uint64, 0, len(payUIDMap)) + for k, v := range payUIDMap { + if v { + payUIDList = append(payUIDList, k) + } + } + return payUIDList, nil +} + +type UpdateFreeTimesCond struct { + UID uint64 `json:"uid" bson:"uid" binding:"required"` // 用户ID + FreeTimes *int64 `json:"freeTimes" bson:"freeTimes"` // 免费次数 + Mark *string `json:"mark" bson:"mark"` // 备注 +} + +func (receiver *UpdateFreeTimesCond) Filter() bson.M { + return bson.M{"uid": receiver.UID, "aiUndressFreeTimes": bson.M{"$gte": receiver.FreeTimes}} +} + +func (receiver *UpdateFreeTimesCond) Update() bson.M { + var update = bson.M{} + if receiver.FreeTimes != nil { + update["aiUndressFreeTimes"] = receiver.FreeTimes + } + return bson.M{"$inc": update} +} + +type UpdateDownloadCond struct { + UID uint64 `json:"uid" bson:"uid" binding:"required"` // 用户ID + Download *int64 `json:"download" bson:"download"` // 下载次数 + Mark *string `json:"mark" bson:"mark"` // 备注 +} + +func (receiver *UpdateDownloadCond) Filter() bson.M { + return bson.M{"uid": receiver.UID} +} + +func (receiver *UpdateDownloadCond) Update() bson.M { + var update = bson.M{} + if receiver.Download != nil { + update["downloadCount"] = receiver.Download + } + return bson.M{"$inc": update} +} + +type SetAdvertiseCond struct { + UId []uint64 `form:"uid" json:"uid" binding:"required"` // 用户uid + Enable *bool `form:"enable" json:"enable" binding:"required"` // 是否开启 +} + +type UpdateLotteryTimesCond struct { + UID uint64 `json:"uid" bson:"uid" binding:"required"` // 用户ID + LotteryTimes *int64 `json:"lotteryTimes" bson:"lotteryTimes"` // 抽空次数 + Mark *string `json:"mark" bson:"mark"` // 备注 +} + +func (receiver *UpdateLotteryTimesCond) Filter() bson.M { + return bson.M{"uid": receiver.UID} +} + +func (receiver *UpdateLotteryTimesCond) Update() bson.M { + var update = bson.M{} + if receiver.LotteryTimes != nil { + update["lotteryTimes"] = receiver.LotteryTimes + } + return bson.M{"$inc": update} +} + +type DeleteCommentCond struct { + UID uint64 `json:"uid" bson:"uid" binding:"required"` // 用户ID +} + +type QueryGameCodeCond struct { + UID uint64 `form:"uid" json:"uid" bson:"uid,omitempty"` //用户ID +} + +func (receiver *DeleteCommentCond) Filter() bson.M { + return bson.M{"uid": receiver.UID} +} diff --git a/models/v/userrecomod/reco.go b/models/v/userrecomod/reco.go new file mode 100644 index 0000000..3e71668 --- /dev/null +++ b/models/v/userrecomod/reco.go @@ -0,0 +1,154 @@ +package userrecomod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.UserReco + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "sortKey", Value: -1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "active", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// getListCnt 获取总数 +func getListCnt(cond bson.M) (int64, error) { + total, err := coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getListCnt", table, "Count", err), + log.Any("cond", cond), + ) + return 0, err + } + return total, nil +} + +// GetSkipSize 计算跳转 +func getSkipSize(page, size uint64, cond bson.M) (uint64, int64, error) { + total, err := getListCnt(cond) + if err != nil { + return 0, 0, err + } + return (page - 1) * size, total, nil +} + +// GetUserReco 获取推荐用户 +func GetUserReco(page, size uint64, isWeb bool) ([]UserReco, int64, error) { + var datas []UserReco + cond := bson.M{} + if !isWeb { + cond = bson.M{"active": true} + } + sort := bson.D{{Key: "sortKey", Value: -1}, {Key: "createdAt", Value: -1}} + skip, total, err := getSkipSize(page, size, cond) + if err != nil { + return datas, total, err + } + opts := options.FindOptions{} + opts.SetSort(sort).SetSkip(int64(skip)).SetLimit(int64(size)) + if err = coll(nil).Find(&datas, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetUserReco", table, "Find", err), + log.Any("page", page), + log.Any("size", size), + log.Any("isWeb", isWeb), + ) + } + return datas, total, err +} + +// AddUserReco 添加一个推荐用户 +func AddUserReco(uid uint64, sortKey int, active bool) error { + now := time.Now() + reco := UserReco{ + UID: uid, + SortKey: sortKey, + Active: active, + UpdatedAt: now, + CreatedAt: now, + } + if _, err := coll(nil).InsertOne(&reco); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "AddUserReco", table, "InsertOne", err), + log.Any("uid", uid), + log.Any("sortKey", sortKey), + log.Any("active", active), + ) + return err + } + return nil +} + +func getRecoUpdate(uid *uint64, sortKey *int, active *bool) bson.M { + update := bson.M{"updatedAt": time.Now()} + if uid != nil { + update["uid"] = *uid + } + if sortKey != nil { + update["sortKey"] = *sortKey + } + if active != nil { + update["active"] = *active + } + return update +} + +// ModifyUserReco 修改推荐用户信息 +func ModifyUserReco(id primitive.ObjectID, uid *uint64, sortKey *int, active *bool) error { + cond := bson.M{"_id": id} + update := getRecoUpdate(uid, sortKey, active) + if _, err := coll(nil).UpdateOne(cond, bson.M{"$set": update}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ModifyUserReco", table, "UpdateOne", err), + log.Any("id", id), + log.Any("uid", uid), + log.Any("sortKey", sortKey), + log.Any("active", active), + ) + return err + } + return nil +} + +// RemoveUserReco 删除推荐用户信息 +func RemoveUserReco(id primitive.ObjectID) error { + cond := bson.M{"_id": id} + if _, err := coll(nil).DeleteOne(cond); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RemoveUserReco", table, "DeleteOne", err), + log.Any("id", id), + ) + return err + } + return nil +} diff --git a/models/v/userrecomod/type.go b/models/v/userrecomod/type.go new file mode 100644 index 0000000..d8c76d2 --- /dev/null +++ b/models/v/userrecomod/type.go @@ -0,0 +1,24 @@ +package userrecomod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// UserReco 用户推送配置 +type UserReco struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + UID uint64 `json:"uid" bson:"uid"` //推荐用户id + SortKey int `json:"sortKey" bson:"sortKey"` //排序 + Active bool `json:"active" bson:"active"` //激活状态 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/userrecomod/web.go b/models/v/userrecomod/web.go new file mode 100644 index 0000000..501052b --- /dev/null +++ b/models/v/userrecomod/web.go @@ -0,0 +1,34 @@ +package userrecomod + +import "91porn-server/models/commod" + +// RecoListReq 推荐用户查询 +type RecoListReq struct { + commod.Page +} + +// RecoListResp 推荐用户应答 +type RecoListResp struct { + Infos []UserReco `json:"infos"` + Total int64 `json:"total"` +} + +// RecoModifyReq 推荐用户修改 +type RecoModifyReq struct { + ID string `form:"id" json:"id"` + UID *uint64 `form:"uid" json:"uid"` + SortKey *int `form:"sortKey" json:"sortKey"` + Active *bool `form:"active" json:"active"` +} + +// RecoAddReq 推荐用户新增 +type RecoAddReq struct { + UID uint64 `form:"uid" json:"uid"` //推荐用户id + SortKey int `form:"sortKey" json:"sortKey"` //排序 + Active bool `form:"active" json:"active"` //激活状态 +} + +// RecoDelReq 推荐用户删除 +type RecoDelReq struct { + ID string `form:"id" json:"id"` +} diff --git a/models/v/usertagmod/app.go b/models/v/usertagmod/app.go new file mode 100644 index 0000000..894e027 --- /dev/null +++ b/models/v/usertagmod/app.go @@ -0,0 +1,12 @@ +package usertagmod + +import "go.mongodb.org/mongo-driver/bson/primitive" + +type UTag struct { + TagID primitive.ObjectID `json:"tagID"` //标签id + TagName string `json:"tagName"` //标签名字 + CoverImg string `json:"coverImg"` // 封面图片 + Description string `json:"description"` // 文字说明 + PlayCount int64 `json:"playCount"` //总历史点击数 + Count int64 `json:"count"` //总记录数 +} diff --git a/models/v/usertagmod/type.go b/models/v/usertagmod/type.go new file mode 100644 index 0000000..8d89179 --- /dev/null +++ b/models/v/usertagmod/type.go @@ -0,0 +1,22 @@ +package usertagmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func Init() { + mdb = db.Init(table) + initIndex() +} + +// UserTag 用户标签表 +type UserTag struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + UID uint64 `bson:"uid"` // 用户id + TagID primitive.ObjectID `bson:"tagID"` // 用户喜欢的标签id + CreatedAt time.Time `bson:"createdAt"` // 创建时间 +} diff --git a/models/v/usertagmod/userTag.go b/models/v/usertagmod/userTag.go new file mode 100644 index 0000000..0aae736 --- /dev/null +++ b/models/v/usertagmod/userTag.go @@ -0,0 +1,127 @@ +package usertagmod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/v/tagmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.UserTag + +// Coll 获取表名 +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initUserTagIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "tagID", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "sortCode", Value: 1}}, + }, + { + Keys: bson.D{{Key: "tagID", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// Insert 保存一条客户端标签信息 +func UTagInsertOne(uTag *UserTag) error { + if _, err := coll(nil).InsertOne(*uTag); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UTagInsertOne", table, "InsertOne", err), + log.Any("uTag", uTag), + ) + return err + } + return nil +} + +// DeleteUserTag 删除用户标签--标签库依旧保留 +func UTagDeleteOne(uid uint64, tagID primitive.ObjectID) error { + if _, err := coll(nil).DeleteOne(bson.M{"uid": uid, "tagID": tagID}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UTagDeleteOne", table, "DeleteOne", err), + log.Any("uid", uid), + log.Any("tagID", tagID), + ) + return err + } + return nil +} + +// TIDListByUID 通过UID获取TID List +func TIDListByUID(uid uint64, skip, limit int64) ([]primitive.ObjectID, error) { + opt := (&options.FindOptions{}). + SetSkip(skip). + SetLimit(limit) + filter := bson.M{ + "uid": uid, + } + utagList := make([]UserTag, 0) + if err := coll(nil).Find(&utagList, filter, opt); err != nil { + return nil, err + } + idList := make([]primitive.ObjectID, len(utagList)) + for i, v := range utagList { + idList[i] = v.ID + } + return idList, nil +} + +// CountByUID 通过UID获取标签数 +func CountByUID(uid uint64) (int64, error) { + filter := bson.M{ + "uid": uid, + } + return coll(nil).Count(filter) +} + +func UserTagList(uid uint64, pageNum uint) ([]UTag, error) { + skip := int64((pageNum - 1) * 6) + limit := int64(6) + tidList, err := TIDListByUID(uid, skip, limit) + if err != nil { + return nil, err + } + count, err := CountByUID(uid) + if err != nil { + return nil, err + } + tagMap, err := tagmod.TagMap(tidList) + if err != nil { + return nil, err + } + utagList := make([]UTag, len(tagMap)) + i := 0 + for _, tag := range tagMap { + utagList[i] = UTag{ + TagID: tag.ID, + TagName: tag.TagName, + CoverImg: tag.CoverImg, + Description: tag.Description, + PlayCount: tag.TPlayCount, + Count: count, + } + i++ + } + return utagList, nil +} diff --git a/models/v/userwatchrecordmod/app.go b/models/v/userwatchrecordmod/app.go new file mode 100644 index 0000000..dde161b --- /dev/null +++ b/models/v/userwatchrecordmod/app.go @@ -0,0 +1,27 @@ +package userwatchrecordmod + +import ( + "91porn-server/models/commod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type ListRequest struct { + commod.Page +} + +func (receiver *ListRequest) Filter(uid uint64) primitive.M { + return bson.M{"uid": uid, "type": bson.M{"$ne": "drama"}} +} + +func (receiver *ListRequest) Options() *options.FindOptions { + return options.Find().SetSkip(int64((receiver.PageNumber - 1) * receiver.PageSize)).SetLimit(int64(receiver.PageSize + 1)).SetSort(bson.M{"recordTime": -1}) +} + +type ListResponse struct { + WorkList []*vidmod.VideoInfoResp `json:"workList"` // 作品列表 + HasNext bool `json:"hasNext"` // 是否有下一页 +} diff --git a/models/v/userwatchrecordmod/app_test.go b/models/v/userwatchrecordmod/app_test.go new file mode 100644 index 0000000..ec536a9 --- /dev/null +++ b/models/v/userwatchrecordmod/app_test.go @@ -0,0 +1,18 @@ +package userwatchrecordmod + +import ( + "testing" + + "go.mongodb.org/mongo-driver/bson" +) + +func TestListRequestFilterExcludesDramaHistory(t *testing.T) { + filter := (&ListRequest{}).Filter(42) + if filter["uid"] != uint64(42) { + t.Fatalf("uid filter = %v, want 42", filter["uid"]) + } + typeFilter, ok := filter["type"].(bson.M) + if !ok || typeFilter["$ne"] != "drama" { + t.Fatalf("type filter = %#v, want drama exclusion", filter["type"]) + } +} diff --git a/models/v/userwatchrecordmod/type.go b/models/v/userwatchrecordmod/type.go new file mode 100644 index 0000000..1eb6385 --- /dev/null +++ b/models/v/userwatchrecordmod/type.go @@ -0,0 +1,15 @@ +package userwatchrecordmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type UserWatchRecord struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + UID uint64 `json:"uid" bson:"uid"` // 用户ID + VideoID primitive.ObjectID `json:"videoId" bson:"videoId"` // 视频ID + RecordTime time.Time `json:"recordTime" bson:"recordTime"` // 记录时间 + CreateTime time.Time `json:"createTime" bson:"createTime"` // 创建时间 +} diff --git a/models/v/userwatchrecordmod/user_watch_record.go b/models/v/userwatchrecordmod/user_watch_record.go new file mode 100644 index 0000000..abe75d0 --- /dev/null +++ b/models/v/userwatchrecordmod/user_watch_record.go @@ -0,0 +1,101 @@ +package userwatchrecordmod + +import ( + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.UserWatchRecord + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// Index 索引设置 +func Index() { + mdb = db.Init(table) + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "recordTime", Value: -1}}, + }, + { + Keys: bson.D{{Key: "videoId", Value: 1}, {Key: "uid", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("UserWatchRecord model set index err ==>[%+v]", err)) + } +} + +func Insert(uid uint64, videoId primitive.ObjectID) error { + now := time.Now() + result, err := coll(nil).InsertOne(&UserWatchRecord{ + UID: uid, + VideoID: videoId, + RecordTime: now, + CreateTime: now, + }) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err), + log.Any("videoId", videoId), + ) + return err + } + if result.InsertedID == nil { + return errors.New("UserWatchRecord Insert is null") + } + return nil +} + +func Refresh(uid uint64, videoId primitive.ObjectID) error { + result, err := coll(nil).UpdateOne(bson.M{"uid": uid, "videoId": videoId}, bson.M{"$set": bson.M{"recordTime": time.Now()}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Refresh", table, "UpdateOne", err), + log.Any("uid", uid), + log.Any("videoId", videoId), + ) + return err + } + if result.ModifiedCount == 0 { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Refresh", table, "UpdateOne", "refresh Fail"), + log.Any("uid", uid), + log.Any("videoId", videoId), + ) + return errors.New("refresh Fail") + } + return nil +} + +func IsExist(uid uint64, videoId primitive.ObjectID) (bool, error) { + count, err := coll(nil).Count(bson.M{"uid": uid, "videoId": videoId}) + if err != nil { + return false, err + } + return count != 0, nil +} + +func List(filter primitive.M, opts ...*options.FindOptions) (out []*UserWatchRecord, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "List", table, "InsertOne", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return + } + return +} diff --git a/models/v/verifyreportmod/type.go b/models/v/verifyreportmod/type.go new file mode 100644 index 0000000..6ddce56 --- /dev/null +++ b/models/v/verifyreportmod/type.go @@ -0,0 +1,96 @@ +package verifyreportmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +const ( + ProcessingStatusDefault = 0 //等待处理 + ProcessingStatusComplete = 1 //审核通过 + ProcessingStatusIgnore = 2 //审核不通过 + + ObjectTypeLoufeng = "loufeng" + ObjectTypeNenMo = "nenmo" + ObjectTypeYyLottery = "yylottery" +) + +type Video struct { +} + +type VerifyReport struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + UID uint64 `json:"uid" bson:"uid"` //用户id + ObjectType string `json:"objectType" bson:"objectType"` //对象类型 + ProductID primitive.ObjectID `json:"productID" bson:"productID"` //对象id + ServiceDetails string `json:"serviceDetails" bson:"serviceDetails"` //内容 + Imgs []string `json:"imgs" bson:"imgs"` //图片 + //Videos []string `json:"videos" bson:"videos"` //录像 + ProcessingStatus int `json:"processingStatus" bson:"processingStatus"` //处理状态 + ProcessedReceipt string `json:"processedReceipt" bson:"processedReceipt"` //处理回执 + BrokerUID *uint64 `json:"brokerUID,omitempty" bson:"brokerUID,omitempty"` //经纪人ID + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 +} + +type VerifyReportUpsert struct { + UID uint64 `json:"uid" bson:"uid"` //用户id + ObjectType string `json:"objectType" bson:"objectType"` //对象类型 + ProductID primitive.ObjectID `json:"productID" bson:"productID"` //对象id + ServiceDetails string `json:"serviceDetails" bson:"serviceDetails"` //内容 + Imgs []string `json:"imgs" bson:"imgs"` //图片 + //Videos []string `json:"videos" bson:"videos"` //录像 + ProcessingStatus int `json:"processingStatus" bson:"processingStatus"` //处理状态 + ProcessedReceipt string `json:"processedReceipt" bson:"processedReceipt"` //处理回执 + BrokerUID *uint64 `json:"brokerUID,omitempty" bson:"brokerUID,omitempty"` //经纪人ID + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type QuerySelector struct { + ID *primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + UID *uint64 `json:"uid,omitempty" bson:"uid,omitempty"` + ObjectType *int `json:"objectType,omitempty" bson:"objectType,omitempty"` //举报对象类型 + ProductID *primitive.ObjectID `json:"productID,omitempty" bson:"productID,omitempty"` //楼凤id + FeedbackType *int `json:"feedbackType,omitempty" bson:"feedbackType,omitempty"` //举报类型 + ProcessingStatus *int `json:"processingStatus,omitempty" bson:"processingStatus,omitempty"` //处理状态 + StartTime *time.Time ` json:"startTime,omitempty" bson:"-"` // 开始时间 + EndTime *time.Time ` json:"endTime,omitempty" bson:"-"` // 结束时间 + BrokerUID *uint64 `json:"brokerUID,omitempty" bson:"brokerUID,omitempty"` //经纪人ID + TimeString *string `json:"timeString,omitempty" bson:"-"` //时间 + +} + +type EditSelector struct { + ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + ProcessingStatus *int `json:"processingStatus,omitempty" bson:"processingStatus,omitempty"` //处理状态 + ProcessedReceipt *string `json:"processedReceipt,omitempty" bson:"processedReceipt,omitempty"` //处理回执 + ServiceDetails *string `json:"serviceDetails,omitempty" bson:"serviceDetails,omitempty"` //内容 + BrokerUID *uint64 `json:"brokerUID,omitempty" bson:"brokerUID,omitempty"` //经纪人ID + Imgs *[]string `json:"imgs,omitempty" bson:"imgs,omitempty"` //图片 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 +} + +type VerifyReportAppRes struct { + Avatar string `json:"avatar"` + Name string `json:"name"` + IsVIP bool `json:"is_vip"` + VipLevel int `json:"vipLevel"` + Age int `json:"age"` + Gender string `json:"gender"` + SuperUser bool `json:"superUser"` + VerifyReport `json:",inline" bson:",inline"` + Title string `json:"title,omitempty"` + City string `json:"city,omitempty"` + Price string `json:"price,omitempty"` + Cover string `json:"cover,omitempty"` +} diff --git a/models/v/verifyreportmod/verifyreport.go b/models/v/verifyreportmod/verifyreport.go new file mode 100644 index 0000000..c200a79 --- /dev/null +++ b/models/v/verifyreportmod/verifyreport.go @@ -0,0 +1,265 @@ +package verifyreportmod + +import ( + "errors" + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.VerifyReport + +// InitIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "processingStatus", Value: 1}}, + }, + { + Keys: bson.D{{Key: "productID", Value: 1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// todo app +func Insert(f VerifyReport) error { + v, err := validInsert(f.UID, f.ProductID) + if err != nil { + return err + } + if !v.ID.IsZero() { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error: Repeat upload:", "Insert", table, "validInsert"), + log.Any("LouFengFeedback", f)) + return errors.New("Repeat upload") + } + now := time.Now() + f.UpdatedAt = now + f.CreatedAt = now + if _, err = coll(nil).InsertOne(f); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err), log.Any("LouFengFeedback", f)) + return err + } + return nil +} + +// validInsert +func validInsert(uid uint64, id primitive.ObjectID) (vf VerifyReport, err error) { + if err = coll(nil).FindOne(&vf, bson.M{"uid": uid, "productID": id, "processingStatus": ProcessingStatusDefault}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "validInsert", table, "FindOne", err), + log.Any("uid", uid), + log.Any("id", id)) + + return + } + return +} + +// GetByPID 根据id获取验证报告 +func GetByPID(pid, objectType string, page commod.Page) (v []VerifyReport, hasNext bool, err error) { + skip := int64(page.Skip()) + limit := int64(page.Limit()) + 1 + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + productID, err := primitive.ObjectIDFromHex(pid) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByPID", table, "ObjectIDFromHex", err), + log.Any("objectType", objectType), + log.Any("productID", pid)) + return + } + if err = coll(nil).Find(&v, bson.M{"objectType": objectType, "productID": productID, "processingStatus": ProcessingStatusComplete}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByPID", table, "FindOne", err), + log.Any("objectType", objectType), + log.Any("productID", pid)) + return + } + if uint64(len(v)) > page.PageSize { + v = v[:page.PageSize] + hasNext = true + } + return +} + +// GetVeRport 获取验证报告 +func GetVeRport(objectType string, page commod.Page) (v []VerifyReport, hasNext bool, err error) { + skip := int64(page.Skip()) + limit := int64(page.Limit()) + 1 + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + if err = coll(nil).Find(&v, bson.M{"objectType": objectType, "processingStatus": ProcessingStatusComplete}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByPID", table, "FindOne", err), + log.Any("objectType", objectType)) + return + } + if uint64(len(v)) > page.PageSize { + v = v[:page.PageSize] + hasNext = true + } + return +} + +// GetByBrokerUID 根据经纪人id获取 +func GetByBrokerUID(brokerUID uint64, objectType string, page commod.Page) (v []VerifyReport, hasNext bool, err error) { + skip := int64(page.Skip()) + limit := int64(page.Limit()) + 1 + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + if err = coll(nil).Find(&v, bson.M{"objectType": objectType, "brokerUID": brokerUID, "processingStatus": ProcessingStatusComplete}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByBrokerUID", table, "Find", err), + log.Any("objectType", objectType), + log.Any("brokerUID", brokerUID)) + return + } + if uint64(len(v)) > page.PageSize { + v = v[:page.PageSize] + hasNext = true + } + return +} + +// GetByBrokerUID 根据经纪人id获取总量 +func CountByBrokerUID(brokerUID uint64, objectType string) (int64, error) { + return coll(nil).Count(bson.M{"objectType": objectType, "brokerUID": brokerUID, "processingStatus": ProcessingStatusComplete}) +} + +// todo web +// StdFind 通用查询 +func StdFind(q QuerySelector, page commod.Page) (data []VerifyReport, total int64, err error) { + data = make([]VerifyReport, 0) + skip := int64(page.Skip()) + limit := int64(page.Limit()) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + f, err := common.ToBsonM(q) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "StdFind", table, "ToBsonM", err), log.Any("cond", f)) + return + } + createdTime := bson.M{} + if q.StartTime != nil { + createdTime["$gte"] = q.StartTime + } + if q.EndTime != nil { + createdTime["$lt"] = q.EndTime + } + if q.TimeString != nil { + f[*q.TimeString] = createdTime + } else { + if len(createdTime) > 0 { + f["createdAt"] = createdTime + } + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "StdFind", table, "Find", err), log.Any("cond", f)) + return + } + total, err = coll(nil).Count(f) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "StdFind", table, "Count", err), log.Any("cond", f)) + return + } + return +} + +// Update 修改LouFeng类型 +func Update(set *EditSelector) error { + set.UpdatedAt = time.Now() + if _, err := coll(nil).UpdateOne(bson.M{"_id": set.ID}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("set", set), + ) + return err + } + return nil +} + +// Update 修改LouFeng类型 +func FindUpdate(set *EditSelector) (vf VerifyReport, err error) { + set.UpdatedAt = time.Now() + if err = coll(nil).FindOneAndUpdate(&vf, bson.M{"_id": set.ID}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("set", set), + ) + return + } + return +} + +// Remove 删除LouFeng类型 +func Remove(id string) (err error) { + OID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Remove", table, "ObjectIDFromHex", err), + log.Any("id", id), + ) + return + } + if _, err = coll(nil).DeleteOne(bson.M{"_id": OID}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Remove", table, "DeleteOne", err), + log.Any("OID", OID), + ) + return + } + return +} + +// InsertBulket 插入一条数据 +func InsertBulket(vr []VerifyReportUpsert) (err error) { + wm := make([]mongo.WriteModel, len(vr)) + for i := range vr { + filter := bson.M{"productID": vr[i].ProductID} + insert, _ := common.ToBsonM(vr[i]) + update := bson.M{"$set": bson.M{"updatedAt": vr[i].CreatedAt}, "$setOnInsert": insert} + wm[i] = mongo.NewUpdateOneModel(). + SetFilter(filter). + SetUpdate(update). + SetUpsert(true) + } + ordered := false + opts := options.BulkWriteOptions{ + Ordered: &ordered, + } + res, err := coll(nil).Bulk(wm, &opts) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertMany", table, "InsertMany", err)) + return + } + fmt.Println("=======res", res.InsertedCount) + return +} diff --git a/models/v/versionmod/version.go b/models/v/versionmod/version.go new file mode 100644 index 0000000..09ef826 --- /dev/null +++ b/models/v/versionmod/version.go @@ -0,0 +1,363 @@ +/* + * @Description: In User Settings Edit + * @Author: your name + * @Date: 2019-08-28 17:35:29 + * @LastEditTime: 2019-08-30 11:25:44 + * @LastEditors: Please set LastEditors + */ +package versionmod + +import ( + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const ( + Normal = 0 + TF = 1 //ios 测试包 + VESTBAG = 2 //马甲包 + + Ios = "ios" + Adr = "android" + H5 = "h5" +) + +// VersionBody request body +type VersionBody struct { + Code int64 `json:"code" bson:"code"` //排序字段 + VersionName string `json:"verName" bson:"verName"` //版本名字 + Platform string `json:"platform" bson:"platform"` //平台 ios + Description string `json:"description" bson:"description"` //描述 + ForcedUpdate bool `json:"forcedUpdate" bson:"ForcedUpdate"` //是否强制更新 + URL string `json:"url" bson:"url"` //下载链接 + IosUrl string `json:"iosUrl" bson:"iosUrl"` //商店包下载链接 +} + +// Version 版本检测表 +type Version struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //id + Code int64 `json:"code" bson:"code"` //排序字段 + VersionName string `json:"verName" bson:"verName"` //版本名字 + Platform string `json:"platform" bson:"platform" ` //平台 ios + Description string `json:"description" bson:"description"` //描述 + URL string `json:"url" bson:"url"` //下载链接 + ForcedUpdate bool `json:"forcedUpdate" bson:"forcedUpdate"` //是否强制更新 + IsActive bool `json:"isActive" bson:"isActive"` //是否启用 + BuildID string `json:"buildId" bson:"buildId"` //包安装ID + PassCheck bool `json:"passCheck" bson:"passCheck"` //是否提审通过 + Origin int64 `json:"origin" bson:"origin"` //表示来源 0 表示 正常 1 TF版 + IsUpgrade bool `json:"isUpgrade" bson:"isUpgrade"` //是否是当前版本升级到新版 + SpecVersion string `json:"specVersion" bson:"specVersion"` //指定升级的版本 + CreatedAt time.Time `form:"createdAt" json:"createdAt" bson:"createdAt"` //记录创建时间 + VersionRange VersionRange `json:"versionRange" bson:"versionRange"` //版本范围 适用于版本版本升级的范围 如果全部为空 则表示全部版本都可升级 +} + +type VersionRange struct { + Major string `json:"major" bson:"major"` + Minor string `json:"minor" bson:"minor"` +} + +// VersionUpdateReq 版本检测表 +type VersionUpdateReq struct { + Platform *string `json:"platform" bson:"platform,omitempty" ` //平台 ios + Description *string `json:"description" bson:"description,omitempty"` //描述 + URL *string `json:"url" form:"url" bson:"url,omitempty"` //下载链接 + ForcedUpdate *bool `json:"forcedUpdate" bson:"forcedUpdate,omitempty"` //是否强制更新 + IsActive *bool `json:"isActive" bson:"isActive,omitempty"` //是否启用 + BuildID *string `json:"buildId" bson:"buildId,omitempty"` //包安装ID + PassCheck *bool `json:"passCheck" bson:"passCheck,omitempty"` //是否提审通过 + Origin *int64 `json:"origin,omitempty" bson:"origin,omitempty"` //表示来源 0 表示 正常 1 TF版 + IsUpgrade *bool `json:"isUpgrade" bson:"isUpgrade"` //是否是当前版本升级到新版 + SpecVersion *string `json:"specVersion" bson:"specVersion,omitempty"` //指定升级的版本 + VersionRange VersionRange `json:"versionRange" bson:"versionRange"` +} + +// VersionQueryReq 查询参数 +type VersionQueryReq struct { + ID *string `form:"id" json:"_id,omitempty" bson:"_id"` //id + VersionName *string `form:"verName" json:"verName,omitempty" bson:"verName"` //版本名字 + Platform *string `form:"platform" json:"platform,omitempty" bson:"platform" ` //平台 ios + Description *string `form:"description" json:"description,omitempty" bson:"description"` //描述 + URL *string `form:"url" json:"url,omitempty" bson:"url"` //下载链接 + ForcedUpdate *bool `form:"forcedUpdate" json:"forcedUpdate,omitempty" bson:"forcedUpdate"` //是否强制更新 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +const table = models.Version + +// InitIndex 设置index +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "verName", Value: 1}, {Key: "buildId", Value: 1}, {Key: "platform", Value: 1}}, + Options: options.Index().SetUnique(true).SetPartialFilterExpression(bson.M{"buildId": bson.M{"$gt": ""}}), + }, + { + Keys: bson.D{{Key: "buildId", Value: 1}}, + }, + { + Keys: bson.D{{Key: "origin", Value: 1}}, + }, + { + Keys: bson.D{{Key: "platform", Value: 1}}, + }, + { + Keys: bson.D{{Key: "isActive", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + } + if _, err := coll().CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll() *db.MongoTool { + return mdb.Coll(table) +} + +func insertCheck(nv *Version) error { + ov, err := FindVersion(nv.Platform) + if err != nil { + return err + } + if nv.Code >= ov.Code { + return nil + } + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "insertCheck", table, "errors.New", err), + log.Any("nv", nv), + ) + return errors.New("version is too low") +} + +// Insert 插入一条记录 +func Insert(v *Version) error { + if err := insertCheck(v); err != nil { + return err + } + v.CreatedAt = time.Now() + if _, err := coll().InsertOne(v); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err), + log.Any("v", v), + ) + return err + } + return nil +} + +// Delete 删除 +func Delete(id primitive.ObjectID) error { + if _, err := coll().DeleteById(id); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Delete", table, "DeleteById", err), + log.Any("id", id), + ) + return err + } + return nil +} + +// Update 修改 +func Update(id primitive.ObjectID, set VersionUpdateReq) error { + if _, err := coll().UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("id", id), + log.Any("set", set), + ) + return err + } + return nil +} + +// FindMany 查找所有版本 +func FindMany(filter bson.M, opts *options.FindOptions) (total int64, data []*Version, err error) { + if err = coll().Find(&data, filter, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindMany", table, "Find", err), + log.Any("filter", filter), + ) + return + } + total, err = coll().Count(filter) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindMany", table, "Count", err), + log.Any("filter", filter), + ) + return + } + if total == 0 { + data = make([]*Version, 0) + return + } + return +} + +// Verlist 查找所有版本 +func Verlist() (data []*Version) { + var infos []*Version + filter := bson.M{"isActive": true, "origin": 0} //排除掉 ios tf 版的 + if err := coll().Find(&infos, filter); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Verlist", table, "Find", err), + log.Any("filter", filter), + ) + return + } + var maxIOS, maxAn, maxH5 Version + data = make([]*Version, 0, len(infos)*3) + for _, v := range infos { + if v == nil { + continue + } + if v.Platform == Ios && v.Code > maxIOS.Code { + maxIOS = *v + } + if v.Platform == Adr && v.Code > maxAn.Code { + maxAn = *v + } + if v.Platform == H5 && v.Code > maxH5.Code { + maxH5 = *v + } + } + data = append(data, &maxIOS) + data = append(data, &maxAn) + data = append(data, &maxH5) + return +} + +/** + * @description: FindVersion 查找最新版本信息 + * @param {type} + * @return: + */ +func FindVersion(pName string) (data Version, err error) { + opts := options.FindOneOptions{ + Sort: bson.D{{Key: "code", Value: -1}}, + } + if err = coll().FindOne(&data, bson.M{"platform": pName, "origin": 0, "isActive": true}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindVersion", table, "FindOne", err), + log.Any("pName", pName), + ) + return + } + return +} + +func FindVersionExcluedBuildId(pName string) (data Version, err error) { + opts := options.FindOneOptions{ + Sort: bson.D{{Key: "code", Value: -1}}, + } + if err = coll().FindOne(&data, bson.M{"platform": pName, "origin": 0, "isActive": true}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindVersionExcluedBuildId", table, "FindOne", err), + log.Any("pName", pName), + ) + return + } + return +} + +// 是否通过提包审核 +func CheckPass(ver, buildId string) (pass bool, err error) { + data := Version{} + if err = coll().FindOne(&data, bson.M{"verName": ver, "buildId": buildId}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CheckPass", table, "FindOne", err), + log.Any("ver", ver), + log.Any("buildId", buildId), + ) + return + } + return data.PassCheck, nil +} + +func FindVersionBaseOnBuildId(buildId string) (data *Version, err error) { + opts := options.FindOneOptions{ + Sort: bson.D{{Key: "code", Value: -1}}, + } + if err = coll().FindOne(&data, bson.M{"buildId": buildId, "isActive": true, "passCheck": true}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindVersionBaseOnBuildId", table, "FindOne", err), + log.Any("buildId", buildId), + ) + return + } + return +} + +// 根据平台buildId 查找版本信息 目的是确认当前buildId 对应的包是否是IOS TF包 +func FindVersionBaseOnBuildIdAndPlatForm(buildId, platform string) (data *Version, err error) { + opts := options.FindOneOptions{ + Sort: bson.D{{Key: "code", Value: -1}}, + } + if err = coll().FindOne(&data, bson.M{"buildId": buildId, "platform": platform, "isActive": true}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindVersionBaseOnBuildIdAndPlatForm", table, "FindOne", err), + log.Any("buildId", buildId), + log.Any("platform", platform), + ) + return + } + return +} + +// 根据平台 版本 还有 build 查看当前版本呢信息 +func FindOneVersionByPlatVerBuild(platform, ver, buildId string) (data *Version, err error) { + if err = coll().FindOne(&data, bson.M{"platform": platform, "verName": ver, "buildId": buildId}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneVersionByPlatVerBuild", table, "FindOne", err), + log.Any("platform", platform), + log.Any("verName", ver), + log.Any("buildId", buildId), + ) + return + } + return +} + +// 根据平台和版本 查找版本信息 限于正常版 +func FindOneVersionByPlatVer(platform, ver string) (data *Version, err error) { + if err = coll().FindOne(&data, bson.M{"platform": platform, "verName": ver, "origin": 0}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneVersionByPlatVer", table, "FindOne", err), + log.Any("platform", platform), + ) + return + } + return +} + +func FindOneByID(id primitive.ObjectID) (data *Version, err error) { + if err = coll().FindOne(&data, bson.M{"_id": id}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneByID", table, "FindOne", err), + log.Any("id", id), + ) + return + } + return +} + +// FindTF 查找所有版本 +func FindTF() (data []*Version) { + var limit int64 = 1 + opt := options.FindOptions{ + Limit: &limit, + Sort: bson.D{{Key: "code", Value: -1}}, + } + filter := bson.M{"isActive": true, "origin": 1} //排除掉 ios tf 版的 + if err := coll().Find(&data, filter, &opt); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Verlist", table, "Find", err), + log.Any("filter", filter), + ) + return + } + return +} diff --git a/models/v/videoactivitymod/app.go b/models/v/videoactivitymod/app.go new file mode 100644 index 0000000..1831f9c --- /dev/null +++ b/models/v/videoactivitymod/app.go @@ -0,0 +1,71 @@ +package videoactivitymod + +import ( + "time" + + "91porn-server/models/commod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type LeaderboardType int + +const ( + Original LeaderboardType = iota // 原创榜 + UP // UP主榜 +) + +type ListRequest struct { + ActivityID string `form:"activityId" json:"activityId"` // 当前活动ID + Type LeaderboardType `form:"type" json:"type"` // 榜单类型 + commod.Page +} + +type ListResponse struct { + ActivityID primitive.ObjectID `json:"activityId"` // 当前活动ID + ActivityBackgroundImage string `json:"activityBackgroundImage"` // 活动背景图 + ActivityEndTime time.Time `json:"activityEndTime"` // 活动结束时间 + ActivityDesc string `json:"activityDesc"` // 活动详情描述 + WorkList []*vidmod.VideoInfo `json:"workList"` // 作品列表 + HasNext bool `json:"hasNext"` // 是否有下一页 +} + +func (receiver *ListRequest) Filter() (primitive.M, error) { + activityId, err := primitive.ObjectIDFromHex(receiver.ActivityID) + if err != nil { + return nil, err + } + filter := bson.M{ + "activityId": activityId, + "status": 1, + "newsType": vidmod.SP, + } + if receiver.Type == UP { + filter["isMadou"] = true + } + return filter, nil +} + +func (receiver *ListRequest) Options() *options.FindOptions { + return options.Find().SetSkip(int64((receiver.PageNumber - 1) * receiver.PageSize)).SetLimit(int64(receiver.PageSize + 1)).SetSort(bson.M{"fakeLikeCount": -1}) +} + +type HistoryRecordRequest struct { + commod.Page +} + +func (receiver *HistoryRecordRequest) Filter() primitive.M { + return bson.M{} +} + +func (receiver *HistoryRecordRequest) Options() *options.FindOptions { + return options.Find().SetSkip(int64((receiver.PageNumber - 1) * receiver.PageSize)).SetLimit(int64(receiver.PageSize + 1)).SetSort(bson.M{"endTime": -1}) +} + +type HistoryRecordResponse struct { + HasNext bool `json:"hasNext"` // 是否有下一页 + ActivityList []*VideoActivity `json:"activityList"` // 列表 +} diff --git a/models/v/videoactivitymod/type.go b/models/v/videoactivitymod/type.go new file mode 100644 index 0000000..5cd0407 --- /dev/null +++ b/models/v/videoactivitymod/type.go @@ -0,0 +1,24 @@ +package videoactivitymod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type StatusType int + +const ( + Enable StatusType = iota + 1 // 使用中 + Unused // 未使用 + Deprecated // 弃用 +) + +type VideoActivity struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + BackgroundImage string `json:"backgroundImage" bson:"backgroundImage"` // 背景图 + EndTime time.Time `json:"endTime" bson:"endTime"` // 结束时间 + Desc string `json:"desc" bson:"desc"` // 详情描述 + Status StatusType `json:"status" bson:"status"` // 状态 + CreateTime time.Time `json:"createTime" bson:"createTime"` // 创建时间 +} diff --git a/models/v/videoactivitymod/video_activity.go b/models/v/videoactivitymod/video_activity.go new file mode 100644 index 0000000..85d0251 --- /dev/null +++ b/models/v/videoactivitymod/video_activity.go @@ -0,0 +1,133 @@ +package videoactivitymod + +import ( + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +const table = models.VideoActivity + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func Init() { + mdb = db.Init(table) + indexModels := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "status", Value: 1}, {Key: "createTime", Value: -1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}, {Key: "endTime", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(indexModels); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// SubmitDocument 提交文档 +func SubmitDocument(document VideoActivity) error { + result, err := coll(nil).InsertOne(&document) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "SubmitDocument", table, "InsertOne", err), + log.Any("document", document), + ) + return err + } + if result.InsertedID.(primitive.ObjectID).IsZero() { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "SubmitDocument", table, "result.InsertedID", "InsertedID is null"), + log.Any("document", document), + ) + return errors.New("result is null") + } + return nil +} + +// EditDocument 修改文档 +func EditDocument(filter, update primitive.M) error { + result, err := coll(nil).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "EditDocument", table, "UpdateOne", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + if result.ModifiedCount == 0 { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "EditDocument", table, "result.ModifiedCount", "ModifiedCount is zero"), + log.Any("filter", filter), + log.Any("update", update), + ) + return errors.New("result is null") + } + return nil +} + +// QueryAllDocument 分页查询文档 +func QueryAllDocument(filter primitive.M, opts ...*options.FindOptions) (out []*VideoActivity, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllDocument", table, "Find", err), + log.Any("filter", filter), + ) + return + } + return +} + +// CountDocument 查询文档条目数 +func CountDocument(filter primitive.M) (int64, error) { + count, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountDocument", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } + return count, nil +} + +// QueryDocumentByStatus 根据状态获取文档 +func QueryDocumentByStatus(status StatusType) (out VideoActivity, err error) { + if err = coll(nil).FindOne(&out, bson.M{"status": status, "endTime": bson.M{"$gte": time.Now()}}, options.FindOne().SetSort(bson.M{"endTime": -1})); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryDocumentByStatus", table, "FindOne", err), + log.Any("status", status), + ) + return + } + // 未查询到开启中的活动、则查询结束时间最晚的一条记录 + if out.ID.IsZero() { + if err = coll(nil).FindOne(&out, bson.M{}, options.FindOne().SetSort(bson.M{"endTime": -1})); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryDocumentByStatus", table, "FindOne", err), + log.Any("status", status), + ) + return + } + } + return +} + +func QueryDocumentByID(id primitive.ObjectID) (out VideoActivity, err error) { + if err = coll(nil).FindOne(&out, bson.M{"_id": id}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryDocumentByStatus", table, "FindOne", err), + log.Any("id", id), + ) + return + } + return +} diff --git a/models/v/videocoupon/init.go b/models/v/videocoupon/init.go new file mode 100644 index 0000000..d10caf3 --- /dev/null +++ b/models/v/videocoupon/init.go @@ -0,0 +1,39 @@ +package videocoupon + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +const table = models.VideoCoupon + +var mdb *db.MongoDB + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "num", Value: 1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} diff --git a/models/v/videocoupon/type.go b/models/v/videocoupon/type.go new file mode 100644 index 0000000..734532a --- /dev/null +++ b/models/v/videocoupon/type.go @@ -0,0 +1,31 @@ +package videocoupon + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 观影券获得渠道 +type GoldVideoCouponSource string + +const GoldVideoCouponSourceVIP GoldVideoCouponSource = "VIP" // 购买VIP卡获得赠送观影券 +const GoldVideoCouponSourceSign GoldVideoCouponSource = "SIGN" // 签到赠送观影券 +const GoldVideoCouponSourceExchange GoldVideoCouponSource = "EXCHANGE" // 兑换码获得观影券 + +// UserGoldVideoCoupon 用户购买观影券 +type UserGoldVideoCoupon struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //ID + UID uint64 `json:"uid" bson:"uid"` //用户ID + Num int `json:"num" bson:"num"` //赠送观影券金币数量 + Source GoldVideoCouponSource `json:"source" bson:"source"` //观影券获得渠道 + Used bool `json:"used" bson:"used"` //观影券是否已使用:false-未使用;true-已使用 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //刷新时间 +} + +type EditSelector struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //ID + Used bool `json:"used" bson:"used"` //观影券是否已使用:false-未使用;true-已使用 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //刷新时间 +} diff --git a/models/v/videocoupon/videocoupon.go b/models/v/videocoupon/videocoupon.go new file mode 100644 index 0000000..7d413f6 --- /dev/null +++ b/models/v/videocoupon/videocoupon.go @@ -0,0 +1,80 @@ +package videocoupon + +import ( + "errors" + "sort" + "time" + + "91porn-server/common/db" + "91porn-server/common/stderr" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// InsertOne ... +func InsertOne(coupon UserGoldVideoCoupon) error { + _, err := coll(nil).InsertOne(coupon) + return err +} + +// InsertMany batch insert +func InsertMany(coupons []UserGoldVideoCoupon) error { + _, err := coll(nil).InsertMany(coupons) + return err +} + +func InsertManyTrans(t *db.MongoTool, coupons []UserGoldVideoCoupon) error { + _, err := coll(t).InsertMany(coupons) + return err +} + +// GetUserUnusedCouponCount 获取用户未使用的观影券总数 +func GetUserUnusedCouponCount(uid uint64) (int64, error) { + filter := bson.M{"uid": uid, "used": false} + return coll(nil).Count(filter) +} + +// GetCouponsByUID 根据uid获取观影券 +func GetCouponsByUID(uid uint64) (list []UserGoldVideoCoupon, err error) { + filter := bson.M{"uid": uid} + err = coll(nil).Find(&list, filter) + return +} + +// GetByUIDAndCouponNum 根据uid和观影券面值查询观影券 +func GetByUIDAndCouponNum(uid uint64, num int) (list []UserGoldVideoCoupon, err error) { + filter := bson.M{"uid": uid, "num": num, "used": false} + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "createdAt", Value: -1}}) + err = coll(nil).Find(&list, filter) + return +} + +// UpdateCoupon 更新用户观影券 +func UpdateCoupon(edit EditSelector) error { + res, err := coll(nil).UpdateOne(bson.M{"_id": edit.ID}, bson.M{"$set": edit}) + if err != nil { + return err + } + if res.ModifiedCount+res.UpsertedCount < 1 { + return errors.New("update failed") + } + return nil +} + +// UseOneCoupon 使用一张观影券 +func UseOneCoupon(coupons []UserGoldVideoCoupon) error { + if len(coupons) == 0 { + return stderr.GoldVideoCoupleNotExist + } + sort.Slice(coupons, func(i, j int) bool { // 优先使用最早创建的观影券 + return coupons[i].CreatedAt.After(coupons[j].CreatedAt) + }) + edit := EditSelector{ + ID: coupons[0].ID, + Used: true, + UpdatedAt: time.Now(), + } + return UpdateCoupon(edit) +} diff --git a/models/v/videodiscountmod/type.go b/models/v/videodiscountmod/type.go new file mode 100644 index 0000000..2c0e85a --- /dev/null +++ b/models/v/videodiscountmod/type.go @@ -0,0 +1,32 @@ +package videodiscountmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +type VideoDiscountLog struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + UID uint64 `json:"uid" bson:"uid"` //用户id + Expiration time.Time `json:"expiration" bson:"expiration"` //过期时间 + VideoDiscount int `json:"videoDiscount" bson:"videoDiscount"` //视频折扣率 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 +} + +type EditSelector struct { + UID *uint64 `json:"uid" bson:"uid,omitempty"` //用户id + Expiration *time.Time `json:"expiration" bson:"expiration,omitempty"` //过期时间 + VideoDiscount *int `json:"videoDiscount" bson:"videoDiscount,omitempty"` //楼凤折扣率 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/videodiscountmod/videodiscount.go b/models/v/videodiscountmod/videodiscount.go new file mode 100644 index 0000000..3a407cf --- /dev/null +++ b/models/v/videodiscountmod/videodiscount.go @@ -0,0 +1,142 @@ +package videodiscountmod + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "time" + + "91porn-server/app/appg" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/models" + "91porn-server/skd/skdg" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.VideoDiscount + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InitIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "expiration", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func RefreshDiscountCache(uid uint64) { + redisCachDel(uid) + if _, err := GetByUID(uid); err != nil { + log.Error("RefreshDiscountCache", log.Any("uid", uid), log.E(err)) + } +} + +func redisCachDel(uid uint64) { + redisKey := redisconst.DataCachKey(table, strconv.FormatUint(uid, 10)) + if _, err := getRedis().Del(redisKey); err != nil { + log.Error("redis del user, ", log.Any("err:", err.Error()), log.Any("uid:", uid)) + } +} + +func getRedis() *redis.Client { + if appg.Redis != nil { + return appg.Redis + } + if webg.Redis != nil { + return webg.Redis + } + if skdg.Redis != nil { + return skdg.Redis + } + return nil +} + +// GetByUID 获取用户购买视频折扣卡记录(设置了缓存!) +func GetByUID(uid uint64) (discount VideoDiscountLog, err error) { + redisKey := redisconst.DataCachKey(table, strconv.FormatUint(uid, 10)) + redisc := appg.Redis + if redisc == nil || !redisc.Exists(redisKey) { + f := bson.M{"uid": uid} + if err = coll(nil).FindOne(&discount, f); err != nil { + log.Error("GetByUID", log.Any("uid", uid), log.E(err)) + return + } + if discount.UID == 0 { + return + } + jsonBytes, err := json.Marshal(discount) + if err != nil { + return discount, err + } + _ = redisc.Set(redisKey, string(jsonBytes), redisconst.DataCachExpire) + return discount, err + } + str, err := redisc.Get(redisKey) + if err != nil { + return discount, err + } + if str == nil { + return discount, errors.New("redis key is null") + } + err = json.Unmarshal([]byte(*str), &discount) + return +} + +// GetByUIDAndWeb 获取用户购买视频折扣卡记录(web后台使用, 不用redis) +func GetByUIDAndWeb(uid uint64) (discount VideoDiscountLog, err error) { + f := bson.M{"uid": uid} + if err = coll(nil).FindOne(&discount, f); err != nil { + log.Error("GetByUIDAndWeb", log.Any("uid", uid), log.E(err)) + return + } + if discount.UID == 0 { + return + } + return discount, err +} + +func Upsert(t *db.MongoTool, set *EditSelector) (err error) { + defer RefreshDiscountCache(*set.UID) + set.UpdatedAt = time.Now() + res, err := coll(t).UpsertOne( + bson.M{"uid": set.UID}, + bson.M{ + "$set": set, + "$setOnInsert": bson.M{ + "createdAt": set.UpdatedAt, + }, + }, + ) + if err != nil { + log.Error("Upsert", log.Any("set", set), log.E(err)) + return + } + if res.ModifiedCount+res.UpsertedCount < 1 { + err = errors.New("upsert fail") + log.Warn("Upsert", log.Any("set", set), log.Any("res", res)) + } + return +} diff --git a/models/v/videogoldcoinmod/app.go b/models/v/videogoldcoinmod/app.go new file mode 100644 index 0000000..60a037a --- /dev/null +++ b/models/v/videogoldcoinmod/app.go @@ -0,0 +1,53 @@ +package videogoldcoinmod + +import ( + "91porn-server/models/v/vidmod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +type QueryVideoGoldCoinCond struct { + Page int64 `form:"pageNumber" binding:"required"` // 当前页码 + Limit int64 `form:"pageSize" binding:"required"` // 每页条数 +} + +type QueryVideoGoldCoinRes struct { + List []*APPVideoGoldCoin `json:"list" bson:"list"` // 视频列表 + ActivityDetail ActivityDetail `json:"activityDetail" bson:"activityDetail"` // 活动详情 + HasNext bool `json:"hasNext" bson:"hasNext"` // 是否还有下一页 + Total int64 `json:"total" bson:"total"` // 视频总数 +} + +type ActivityDetail struct { + StartTime time.Time `json:"startTime" bson:"startTime,omitempty"` // 支付尾款开始时间 + EndTime time.Time `json:"endTime" bson:"endTime,omitempty"` // 支付尾款/活动 结束时间 + BalancePayment bool `json:"balancePayment" bson:"balancePayment"` // 是否有尾款支付 + ActivityTime time.Time `json:"activityTime" bson:"activityTime,omitempty"` // 活动时间 + AdvanceAmount int64 `bson:"advanceAmount" json:"advanceAmount"` // 预付金额 + BalanceAmount int64 `bson:"balanceAmount" json:"balanceAmount"` // 尾款金额 + Status int `json:"status" bson:"status"` // 预售订单状态 + TotalCount int64 `json:"totalCount" bson:"totalCount,omitempty"` // 会员卡总数 +} + +type APPVideoGoldCoin struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // id + VidId primitive.ObjectID `json:"vidId" bson:"vidId"` // 帖子id + VidTitle string `json:"vidTitle" bson:"vidTitle"` // 视频标题 + SourceURL string `json:"sourceURL" bson:"sourceURL"` // 视频资源地址Path + Cover string `json:"cover" bson:"cover"` // 封面图 + SeriesCover []string `json:"seriesCover" bson:"seriesCover"` // 帖子套图 + Coins int64 `json:"coins" bson:"coins"` // 定价 + Detail *vidmod.VideoInfo `json:"detail" bson:"detail"` // 详细数据 +} + +func (receiver *QueryVideoGoldCoinCond) Filter() bson.M { + var query = bson.M{} + query["status"] = 1 + return query +} + +func (receiver QueryVideoGoldCoinCond) Options() *options.FindOptions { + return options.Find().SetSkip((receiver.Page - 1) * receiver.Limit).SetLimit(receiver.Limit + 1).SetSort(bson.M{"sortCode": -1}) +} diff --git a/models/v/videogoldcoinmod/gold_coin_video.go b/models/v/videogoldcoinmod/gold_coin_video.go new file mode 100644 index 0000000..232cd22 --- /dev/null +++ b/models/v/videogoldcoinmod/gold_coin_video.go @@ -0,0 +1,153 @@ +package videogoldcoinmod + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "errors" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.VideoGoldCoin + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 索引设置 +func initIndex() { + many := []mongo.IndexModel{ //batch set indexes //value is the type 1 or -1 + { + Keys: bson.D{{"createdAt", -1}}, + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("user model set index err ==>[%+v]", err)) + } +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +func InsertOne(mt *db.MongoTool, cfg VideoGoldCoin) error { + if _, err := coll(mt).InsertOne(cfg); err != nil { + return err + } + return nil +} + +// QueryAllList 分页查询文档 +func QueryAllList(filter primitive.M, opts ...*options.FindOptions) (out []*VideoGoldCoin, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return nil, err + } + return +} + +// QueryAllCount 查询文档条目数 +func QueryAllCount(filter primitive.M) (int64, error) { + if count, err := coll(nil).Count(filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err), + log.Any("filter", filter), + ) + return 0, err + } else { + return count, nil + } +} + +// Edit 修改文档 +func Edit(filter, update primitive.M) error { + result, err := coll(nil).UpdateOne(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Edit", table, "UpdateOne", err), + log.Any("filter", filter), + log.Any("update", update), + ) + return err + } + if result.ModifiedCount == 0 { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Edit", table, "result.ModifiedCount", "ModifiedCount is zero"), + log.Any("filter", filter), + log.Any("update", update), + ) + return errors.New("result is null") + } + return nil +} + +// GetVideoList 获取金币视频列表 +func GetVideoList(skip, limit int64, cond bson.M, opts ...*options.FindOptions) (back []*APPVideoGoldCoin, total int64, hasNext bool, err error) { + // 获取总条数 + total, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond)) + return + } + if total == 0 { + return + } + err = coll(nil).Find(&back, cond, opts...) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + ) + return + } + if len(back) > int(limit) { + hasNext = true + back = back[:limit] + } + return +} + +func InsertMany(records []VideoGoldCoin) error { + opts := options.InsertMany().SetOrdered(false) + _, err := coll(nil).InsertMany(records, opts) + if !db.IsMongoDupKey(err) { + return err + } + return nil +} + +func IsExistByVideoID(videoID primitive.ObjectID) (bool, error) { + count, err := coll(nil).Count(bson.M{"_id": videoID}) + if err != nil { + return false, err + } + if count > 0 { + return true, nil + } + return false, nil +} + +// Remove 删除 +func Remove(filter primitive.M) (err error) { + _, err = coll(nil).DeleteOne(filter) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Remove", table, "DeleteOne", err), + log.Any("filter", filter), + ) + return + } + return +} diff --git a/models/v/videogoldcoinmod/type.go b/models/v/videogoldcoinmod/type.go new file mode 100644 index 0000000..5eeeb0f --- /dev/null +++ b/models/v/videogoldcoinmod/type.go @@ -0,0 +1,31 @@ +package videogoldcoinmod + +import ( + "91porn-server/common/db" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +var ( + mdb *db.MongoDB +) + +type VideoGoldCoin struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // id + PublisherID uint64 `json:"publisherID" bson:"publisherID"` // 上传者ID + Portrait string `json:"portrait" bson:"portrait"` // 上传者头像 + Name string `json:"name" bson:"name"` // 上传者姓名 + VidId primitive.ObjectID `json:"vidId" bson:"vidId"` // 帖子id + Status int `json:"status" bson:"status"` // 状态,1 上架 2 下架 默认为 1 + Reason string `json:"reason" bson:"reason,omitempty"` // 下架理由 + VidType string `json:"vidType" bson:"vidType"` // 帖子类型 + VidTitle string `json:"vidTitle" bson:"vidTitle"` // 视频标题 + Coins int64 `json:"coins" bson:"coins"` // 定价 + SourceURL string `json:"sourceURL" bson:"sourceURL"` // 视频资源地址Path + Cover string `json:"cover" bson:"cover"` // 封面图 + SeriesCover []string `json:"seriesCover" bson:"seriesCover"` // 帖子套图 + SortCode int `json:"sortCode" bson:"sortCode"` // 排序号 + OperateAccount string `json:"operateAccount" bson:"operateAccount,omitempty"` // 操作者 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` // 创建时间 + UpdateTime time.Time `json:"updateTime" bson:"updateTime,omitempty"` // 更新时间 +} diff --git a/models/v/videogoldcoinmod/web.go b/models/v/videogoldcoinmod/web.go new file mode 100644 index 0000000..54a8fd0 --- /dev/null +++ b/models/v/videogoldcoinmod/web.go @@ -0,0 +1,92 @@ +package videogoldcoinmod + +import ( + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +type AddCond struct { + VideoId string `json:"videoId" bson:"videoId" binding:"required"` // 视频ID +} + +func (receiver *AddCond) Generate(addAct string) VideoGoldCoin { + now := time.Now() + return VideoGoldCoin{ + Status: 1, + OperateAccount: addAct, + CreatedAt: now, + } +} + +type QueryAllCond struct { + Page int64 `form:"pageNumber" binding:"required"` // 查询页码 + Limit int64 `form:"pageSize" binding:"required"` // 页码大小 + Uid *int `form:"uid"` // 用户ID + VidTitle *string `form:"vidTitle"` // 视频标题 + VidType *string `form:"vidType"` // 视频类型 + Status *int `form:"status"` // 上架状态 1 上架 2 下架 +} + +type QueryAllRes struct { + List []*VideoGoldCoin `json:"list"` + Total int64 `json:"total"` +} + +func (receiver *QueryAllCond) Filter() bson.M { + var query = bson.M{} + if receiver.Uid != nil { + query["publisherID"] = receiver.Uid + } + if receiver.VidTitle != nil { + query["vidTitle"] = bson.M{"$regex": receiver.VidTitle} + } + if receiver.Status != nil { + query["status"] = receiver.Status + } + if receiver.VidType != nil { + query["vidType"] = receiver.VidType + } + return query +} + +func (receiver QueryAllCond) Options() *options.FindOptions { + return options.Find().SetSkip((receiver.Page - 1) * receiver.Limit).SetLimit(receiver.Limit).SetSort(bson.M{"createdAt": -1}) +} + +type EditCond struct { + ID primitive.ObjectID `json:"id" binding:"required"` // 文档 + Status *int `json:"status" bson:"status"` // 状态 1 上架 2 下架 + SortCode *int `json:"sortCode" bson:"sortCode"` // 排序号 + Reason *string `json:"reason" bson:"reason"` // 拒绝原因 +} + +func (receiver *EditCond) Filter() bson.M { + return bson.M{"_id": receiver.ID} +} + +func (receiver *EditCond) Update(updateAct string) bson.M { + var update = bson.M{} + if receiver.Reason != nil { + update["reason"] = receiver.Reason + } + if receiver.SortCode != nil { + update["sortCode"] = receiver.SortCode + } + if receiver.Status != nil { + update["status"] = receiver.Status + } + + update["operateAccount"] = updateAct + update["updateTime"] = time.Now() + return bson.M{"$set": update} +} + +type DeleteCond struct { + ID primitive.ObjectID `json:"id" binding:"required"` // 文档 +} + +func (receiver *DeleteCond) Filter() bson.M { + return bson.M{"_id": receiver.ID} +} diff --git a/models/v/vidmod/app.go b/models/v/vidmod/app.go new file mode 100644 index 0000000..c424d7b --- /dev/null +++ b/models/v/vidmod/app.go @@ -0,0 +1,524 @@ +package vidmod + +import ( + "time" + + "91porn-server/models/commod" + "91porn-server/models/v/locmod" + "91porn-server/models/v/usermod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var TotalWatchCount uint64 = 5 //非vip用户每日总观看次数 + +const ( + SubmitVideoPlayTimeLimit = 10 //上传视频最短播放时间为10秒 + TodayRank = 0 + WeekRank = 1 + MonthRank = 2 + + TypeNewst = 0 + TypeHotest = 1 + TypeSameCity = 2 + TypePay = 3 + TypeVip = 4 + + CommonUp = 0 + MadouUp = 1 + + Version string = "1.0.0" // 帖子兼容老版本 +) + +// BaseVid4Aws 上传到AWS的基础信息 +type BaseVid4Aws struct { + ID string `json:"id"` + CheckSum string `json:"checkSum"` + Title string `json:"title"` + Actors []string `json:"actors"` + AddedTime string `json:"addedTime"` + PlayTime uint64 `json:"playTime"` + Tags []string `json:"tags"` + Size int `json:"size"` + Filename string `json:"filename"` + Desc string `json:"desc"` + Type string `json:"type"` + Director string `json:"director"` + Studio string `json:"studio"` + Bango string `json:"bango"` + Via string `json:"via"` +} + +// CoverInfo4Aws 上传到AWS的基础信息 +type CoverInfo4Aws struct { + Type string `json:"type"` + Title string `json:"title"` + Tags []string `json:"tags"` + CoverImg string `json:"coverImg"` + SeriesCover []string `json:"seriesCover"` + Status string `json:"status"` + MimeType string `json:"mimeType"` + Via string `json:"via"` + Topic []string `json:"topic"` + NewUpdateAt string `json:"newUpdateAt"` + Desc string `json:"desc"` +} + +// LocVideoReq 同城视频请求 +type LocVideoReq struct { + City string `form:"city" json:"city"` + commod.Page +} + +// VideoReq 视频详情请求 +type VideoReq struct { + VideoID string `form:"videoID" json:"videoID"` + SearchAccessToken string `form:"searchAccessToken" json:"searchAccessToken"` +} + +// SubmitReq 视频发布请求 +type SubmitReq struct { + UID uint64 `form:"uid" json:"uid"` + NewsType string `form:"newsType" json:"newsType"` + Title string `form:"title" json:"title"` + Content string `form:"content" json:"content"` + Tags []string `form:"tags" json:"tags" binding:"required"` + PlayTime uint `form:"playTime" json:"playTime"` + Cover string `form:"cover" json:"cover"` + CoverThumb string `form:"coverThumb" json:"coverThumb"` + SeriesCover []string `form:"seriesCover" json:"seriesCover"` + Via string `form:"via" json:"via"` + Coins int64 `form:"coins" json:"coins"` + Size int `form:"size" json:"size"` + Resolution string `form:"resolution" json:"resolution"` + Ratio float64 `json:"ratio" bson:"ratio"` //宽高比 + MimeType string `form:"mimeType" json:"mimeType"` + Location locmod.Location `form:"location" json:"location"` + Actor string `form:"actor" json:"actor"` + SourceID string `form:"sourceID" json:"sourceID"` + SourceURL string `form:"sourceURL" json:"sourceURL"` + MD5 string `form:"md5" json:"md5"` + Filename string `form:"filename" json:"filename"` + FreeTime int `form:"freeTime" json:"freeTime"` + IsActivity bool `form:"isActivity" json:"isActivity"` // 是否是参赛作品 +} + +// PlayReq 播放请求 +type PlayReq struct { + VideoID string `form:"videoID" json:"videoID"` + Longer int `form:"longer" json:"longer"` + Progress int `form:"progress" json:"progress"` + PlayWay int `form:"playWay" json:"playWay"` + Via int `form:"via" json:"via"` + TagID string `form:"tagID" json:"tagID"` + //作者 + Publisher uint64 `form:"publisher" json:"publisher"` +} + +// LocReq 地址位置请求参数 +type LocReq struct { + ID string `form:"id" json:"id"` +} + +// LocInfo 城市信息 +type LocInfo struct { + ID primitive.ObjectID `json:"id"` + //城市 + City string `json:"city"` + //封面 + Cover string `json:"cover"` + //访问人数 + Visit int `json:"visit"` + //创建时间 + CreatedAt time.Time `json:"createdAt"` +} + +// LocResp 地址位置应答参数 +type LocResp struct { + LocInfo +} + +// LocVideoResp 同城视频应答 +type LocVideoResp struct { + List []*VideoInfo `json:"list"` + HasNext bool `json:"hasNext"` +} + +// LocationResp 接受地理位置信息的结构 +type LocationResp struct { + Code int `json:"code"` + CountryName string `json:"country_name"` + RegionName string `json:"region_name"` + CityName string `json:"city_name"` + IP string `json:"ip"` +} + +// TagInfo 标签信息 +type TagInfo struct { + ID primitive.ObjectID `json:"id" bson:"_id"` //标签id + Name string `json:"name" bson:"tagName"` //标签名字 + CoverImg string `json:"coverImg" bson:"coverImg"` //封面图片 + Description string `json:"description" bson:"description"` //文字说明 + PlayCount int64 `json:"playCount" bson:"tPlayCount"` //播放量 + HasCollected bool `json:"hasCollected" bson:"hasCollected"` //已收藏 +} + +// VideoStatus 视频状态 +type VideoStatus struct { + //已支付 + HasPaid bool `json:"hasPaid" bson:"hasPaid"` + //每日视频状态信息 + TodayRank int `json:"todayRank" bson:"todayRank"` + //每日播放次数 + TodayPlayCnt int `json:"todayPlayCnt" bson:"todayPlayCnt"` + //已点赞 + HasLiked bool `json:"hasLiked" bson:"hasLiked"` + //已收藏 + HasCollected bool `json:"hasCollected" bson:"hasCollected"` +} + +// UInfo 别名 +type UInfo = usermod.BaseInfoVip + +// Publisher 发布者的信息 +type Publisher struct { + UInfo + //是否关注 + HasFollowed bool `json:"hasFollowed" bson:"hasFollowed"` +} + +// VideoBase 返回视频的基本信息 +type VideoBase struct { + //id + ID primitive.ObjectID `json:"id" bson:"_id"` + //帖子类型, SP,视频帖子,COVER + NewsType string `json:"newsType" bson:"newsType"` + //视频标题 + Title string `json:"title" bson:"title"` + Content string `json:"content" bson:"content"` // 视频内容 + //视频标签 + Tags []TagInfo `json:"tags" bson:"tags"` + //视频资源地址Path + SourceURL string `json:"sourceURL" bson:"sourceURL"` + // H.265 视频资源地址 + H265Url string `json:"h265Url" bson:"h265Url"` + // 预览视频资源地址(并非所有视频都有预览) + PreviewURL string `json:"previewURL" bson:"previewURL"` + //广告跳转连接 目前只有广告帖子有用 + LinkUrl string `json:"linkUrl,omitempty" bson:"linkUrl,omitempty"` + //影片长度 + PlayTime uint `json:"playTime" bson:"playTime"` + //封面大图 + Cover string `json:"cover" bson:"cover"` + //封⾯小图 + CoverThumb string `json:"coverThumb" bson:"coverThumb"` + //帖子套图 + SeriesCover []string `json:"seriesCover" bson:"seriesCover"` + //总播放量 + PlayCount int `json:"playCount" bson:"playCount"` + //视频购买人数 + PurchaseCount int `json:"purchaseCount" bson:"purchaseCount"` + //点赞数 + LikeCount int `json:"likeCount" bson:"likeCount"` + //评论数 + CommentCount int `json:"commentCount" bson:"commentCount"` + //分享数 + ShareCount int `json:"shareCount" bson:"shareCount"` + //折扣后视频金币数(如果有的话) + Coins int64 `json:"coins" bson:"coins"` + //文件大小 byte + Size int `json:"size" bson:"size"` + //分辨率 + Resolution string `json:"resolution" bson:"resolution"` + //宽高比 + Ratio float64 `json:"ratio" bson:"ratio"` + //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + Status int `json:"status" bson:"status"` + //视频未通过审核时的理由 + Reason string `json:"reason" bson:"reason"` + //免费时长 + FreeTime int `json:"freeTime" bson:"freeTime"` + //是否隐藏地址 + IsHideLocation bool `json:"isHideLocation" bson:"isHideLocation"` + //免费专区 + FreeArea bool `json:"freeArea" bson:"freeArea"` + //创建时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + //审核时间 + ReviewAt time.Time `json:"reviewAt" bson:"reviewAt,omitempty"` + //是否置顶 + IsTopping bool `json:"isTopping" bson:"isTopping"` + //力荐 + IsRecommend bool `json:"isRecommend" bson:"isRecommend"` + //置精 + IsChoosen bool `json:"isChoosen" bson:"isChoosen"` + //打赏金额 + Rewarded decimal.Decimal `json:"rewarded" bson:"rewarded"` + //视频原金币数 + OriginCoins int64 `json:"originCoins" bson:"originCoins"` + CollectCount int `json:"collectCount" bson:"collectCount"` // 收藏数 + PageViewCount int64 `json:"pageViewCount" bson:"pageViewCount"` // 视频页面展示次数 + SeedLinkUrl string `json:"seedLinkUrl" bson:"seedLinkUrl,omitempty"` //种子链接 + SeedSize uint64 `json:"seedSize" bson:"seedSize,omitempty"` // 种子影片大小 byte + SeedPlayTime uint64 `json:"seedPlayTime" bson:"seedPlayTime,omitempty"` // 种子影片时长 + PreviewStart int `json:"previewStart" bson:"previewStart"` // 预览时间起始点 + RichText string `json:"richText" bson:"richText"` // 富文本内容 + TimeNodeList []TimeNode `json:"timeNodeList" bson:"timeNodeList"` // 时间节点 + DownloadAllow int `json:"downloadAllow" son:"downloadAllow"` // 允许下载的VIP级别,0表示不允许下载 1表示VIP 2表示免费 + ShowType int `json:"showType" bson:"showType"` // 0-所有的人都可以看 1-奇数可看 2-偶数可看 + LsjId string `json:"lsjId" bson:"lsjId"` // 老司机ID + SearchAccessToken string `json:"searchAccessToken,omitempty" bson:"-"` + ShowFreeTrialBadge bool `json:"showFreeTrialBadge" bson:"-"` // 是否展示免费试看角标 + FreeTrialRemaining uint64 `json:"freeTrialRemaining" bson:"-"` // 用户剩余免费观看次数 + CanUseFreeTrial bool `json:"canUseFreeTrial" bson:"-"` // 当前视频是否可使用免费观看次数 +} + +// CommentInfo 评论信息 +type CommentInfo struct { + //用户id + UID uint64 `json:"uid"` + //姓名 + Name string `json:"name"` + //头像 + Portrait string `json:"portrait"` + //评论id + Cid string `json:"cid"` + //评论内容 + Content string `json:"content"` + ///喜欢次数 + LikeCount int `json:"likeCount"` + //是否作者 + IsAuthor bool `json:"isAuthor"` + //创建时间 + CreatedAt time.Time `json:"createdAt"` +} + +// WatchModel 观看模型 +type WatchModel struct { + ///观看次数 + WatchCount uint64 `json:"watchCount"` + //可观看 + IsWatch bool `json:"isWatch"` + //是否免费 + IsFreeWatch bool `json:"isFreeWatch"` +} + +// WatchCountResp 用户免费观看次数响应。 +type WatchCountResp struct { + IsCan bool `json:"isCan"` + WatchCount uint64 `json:"watchCount"` + TotalWatchCount uint64 `json:"totalWatchCount"` +} + +// WatchConsumeReq 消费免费观看视频次数请求。Vid用于兼容旧字段名。 +type WatchConsumeReq struct { + VideoID string `json:"videoId"` + Vid string `json:"vid"` +} + +func (r WatchConsumeReq) GetVideoID() string { + if r.VideoID != "" { + return r.VideoID + } + return r.Vid +} + +// WatchConsumeResp 消费免费观看视频次数响应。 +type WatchConsumeResp struct { + IsCan bool `json:"isCan"` + WatchCount uint64 `json:"watchCount"` + TotalWatchCount uint64 `json:"totalWatchCount"` + Consumed bool `json:"consumed"` +} + +// VideoInfo 返回的视频列表 +type VideoInfo struct { + VideoBase + UInfo Publisher `json:"publisher,omitempty"` + Location LocInfo `json:"location,omitempty"` + VidStatus VideoStatus `json:"vidStatus,omitempty"` + Comment CommentInfo `json:"comment,omitempty"` + Watch WatchModel `json:"watch,omitempty"` + SortCode int `json:"-"` // 对专题内视频排序 + HappinessPlazaTop int32 `json:"happinessPlazaTop"` //是否是幸福广场置顶 + DiscountAreaPrice int64 `json:"discountAreaPrice"` //折扣专区展示价格 + VideoTypeId string `json:"videoTypeId"` //视频分类ID + VideoTypeName string `json:"videoTypeName"` //视频分类名称 +} + +// VideoInfoResp 返回的视频列表 +type VideoInfoResp struct { + ID primitive.ObjectID `json:"id" bson:"_id"` // ID + NewsType string `json:"newsType" bson:"newsType"` // 帖子类型, SP,视频帖子,COVER + Title string `json:"title" bson:"title"` // 视频标题 + SourceURL string `json:"sourceURL" bson:"sourceURL"` // 视频资源地址Path + H265Url string `json:"h265Url" bson:"h265Url"` // H.265 视频资源地址 + LinkUrl string `json:"linkUrl,omitempty" bson:"linkUrl,omitempty"` // 广告跳转连接 目前只有广告帖子有用 + PlayTime uint `json:"playTime" bson:"playTime"` // 影片长度 + Cover string `json:"cover" bson:"cover"` // 封面大图 + SeriesCover []string `json:"seriesCover" bson:"seriesCover"` // 帖子套图 + PlayCount int `json:"playCount" bson:"playCount"` // 总播放量 + LikeCount int `json:"likeCount" bson:"likeCount"` // 点赞数 + PageViewCount int64 `json:"pageViewCount" bson:"pageViewCount"` // 视频页面展示次数 + CommentCount int `json:"commentCount" bson:"commentCount"` // 评论数 + Coins int64 `json:"coins" bson:"coins"` // 折扣后视频金币数(如果有的话) + Size int `json:"size" bson:"size"` // 文件大小 byte + Resolution string `json:"resolution" bson:"resolution"` // 分辨率 + Ratio float64 `json:"ratio" bson:"ratio"` // 宽高比 + Status int `json:"status" bson:"status"` // 状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + Reason string `json:"reason" bson:"reason"` // 视频未通过审核时的理由 + FreeTime int `json:"freeTime" bson:"freeTime"` // 免费时长 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + ReviewAt time.Time `json:"reviewAt" bson:"reviewAt,omitempty"` // 审核时间 + IsTopping bool `json:"isTopping" bson:"isTopping"` // 是否置顶 + IsPopping bool `json:"isPopping" bson:"isPopping"` // 是否推广 + IsRecommend bool `json:"isRecommend" bson:"isRecommend"` // 力荐 + IsChoosen bool `json:"isChoosen" bson:"isChoosen"` // 置精 + Rewarded decimal.Decimal `json:"rewarded" bson:"rewarded"` // 打赏金额 + OriginCoins int64 `json:"originCoins" bson:"originCoins"` // 视频原金币数 + TotalWorks int64 `json:"totalWorks" bson:"totalWorks"` // 总作品数 + Chosen bool `json:"chosen" bson:"chosen"` // 是否精选 + UInfo AppPublisherResp `json:"publisher,omitempty"` // 用户信息 + VidStatus VideoStatus `json:"vidStatus,omitempty"` // 视频状态 + Watch WatchModel `json:"watch,omitempty"` // 是否观看 + DownloadAllow int `json:"downloadAllow"` // 允许下载的VIP级别,0表示不允许下载 1表示VIP 2表示免费 + ShowType int `json:"showType"` // 0-所有的人都可以看 1-奇数可看 2-偶数可看 + FreeArea bool `json:"freeArea" bson:"freeArea"` // 免费专区 + SearchAccessToken string `json:"searchAccessToken,omitempty" bson:"-"` + ShowFreeTrialBadge bool `json:"showFreeTrialBadge" bson:"-"` // 是否展示免费试看角标 + FreeTrialRemaining uint64 `json:"freeTrialRemaining" bson:"-"` // 用户剩余免费观看次数 + CanUseFreeTrial bool `json:"canUseFreeTrial" bson:"-"` // 当前视频是否可使用免费观看次数 + //视频标签 + Tags []TagInfo `json:"tags" bson:"tags"` +} + +// AppPublisherResp 发布者的信息 +type AppPublisherResp struct { + UID uint64 `json:"uid" bson:"uid"` // 用户id + Name string `json:"name" bson:"name"` // 姓名 + Portrait string `json:"portrait" bson:"portrait"` // 头像 + HasFollowed bool `json:"hasFollowed" bson:"hasFollowed"` // 是否关 + VipLevel int `json:"vipLevel" bson:"vipLevel"` // VIP等级 + VipExpireDate time.Time `json:"vipExpireDate,omitempty" bson:"vipExpireDate"` // VIP过期时间 + VipName string `json:"vipName" bson:"vipName"` // 用户VIP名称 +} + +// RemoveVideoReq 删除自己的视频 +type RemoveVideoReq struct { + IDs []string `json:"ids"` +} + +// NewsListReq 帖子请求 +type NewsListReq struct { + Type int `form:"type" json:"type"` //请求类型,默认0,最新帖子;1,最热帖子;2,同城; 3,付费视频 + SubType int `form:"subType" json:"subType"` // type为0时, 0 默认; 1 只返回短视频 type为1时 1只返回麻豆 + Version string `form:"version" json:"version"` // 原创新版识别标志 + City string `form:"city" json:"city"` //城市 + ReqTime string `form:"reqDate" json:"reqDate"` //请求时间 + commod.Page +} + +// NewsListResp 帖子应答 +type NewsListResp struct { + List []*VideoInfo `json:"list"` + HasNext bool `json:"hasNext"` +} +type OriginalInfo struct { + List [][]*VideoInfo `json:"list"` + HasNext bool `json:"hasNext"` + Version string `json:"version"` +} + +// UnlikeReq 不感兴趣 +type UnlikeReq struct { + VideoID string `json:"videoID"` +} + +// ListType 帖子类型 +type ListType int + +const ( + Video ListType = iota // 视频帖子 默认为全部视频 + LongVideo // 长视频 + ShortVideo // 短视频 + Image // 图片帖子 +) + +// ModelEnum 模块枚举 +type ModelEnum int + +const ( + LatestZone ModelEnum = iota + 1 // 最新专区/最新上架 + HottestZone // 最火专区/热门推荐 + OriginalZone // 原创专区 + GoldZone // 金币专区 + HappinessPlaza // 幸福广场 + MostLiked // 最多点赞 + MostPlay // 最多播放 + MYLickImage // 点赞图片作品 + MyImage // 我的---图片作品 +) + +type PaymentEnum int + +const ( + PaymentDefault PaymentEnum = iota // 默认为全部帖子 + PaymentVIP // 会员帖子 + PaymentGold // 金币帖子 +) + +// AppListReq 帖子列表请求 +type AppListReq struct { + Type ListType `json:"type" form:"type"` // 帖子类型 + Model ModelEnum `json:"model" form:"model"` // 模块 + Time time.Time `json:"time" form:"time"` // [最新专区、幸福广场]必传 值为第一次请求时间 + Tag string `json:"tag" form:"tag"` // 标签 + PaymentType PaymentEnum `json:"paymentType" form:"paymentType"` // 付费类型 + UID uint64 `json:"uid" form:"uid"` // 用户ID + City string `form:"city" json:"city"` + FilterType uint `form:"filterType" json:"filterType"` //过滤类型 1-推荐 2-最新 + commod.Page +} + +// WorkCount 用户视频次数 +type WorkCount struct { + Uid uint64 `json:"_id" bson:"_id"` // 用户ID + Count int64 `json:"count" bson:"count"` // 作品次数 +} + +type LibraryReq struct { + Canvas SortKey `json:"canvas" bson:"canvas"` // 视频分类 + OrderBy SortKey `json:"orderBy" bson:"orderBy"` // 视频排序 + Tags Tag `json:"tags" bson:"tags"` // 全部标签 + PaymentType SortKey `json:"paymentType" bson:"paymentType"` // 付费分类 + TimeType SortKey `json:"timeType" bson:"timeType"` // 时间排序 +} + +type LibraryData struct { + Canvas []SortKey `json:"canvas" bson:"canvas"` // 视频分类 + OrderBy []SortKey `json:"orderBy" bson:"orderBy"` // 视频排序 + VidTags []Tag `json:"vidTags" bson:"vidTags"` // 全部视频标签 + ACGTags []Tag `json:"acgTags" bson:"acgTags"` // 全部ACG标签 + PaymentType []SortKey `json:"paymentType" bson:"paymentType"` // 付费分类 + TimeType []SortKey `json:"timeType" bson:"timeType"` // 时间排序 +} + +type SortKey struct { + Key string `json:"key" bson:"key"` // 健值 + Name string `json:"name" bson:"name"` // 健名称 +} + +type Tag struct { + ID string `json:"id" bson:"id"` // 标签ID + Name string `json:"name" bson:"name"` // 标签名称 +} + +type ChangeVideoSectionReq struct { + SectionID string `uri:"sectionID" binding:"required"` // 专题ID +} + +type HomeMostNewModuleVideoListReq struct { + commod.Page + SortType int `form:"sortType"` // 列表排序 1:最新 2:本周热门 4:本月最热 5:年度最热 +} diff --git a/models/v/vidmod/app_video.go b/models/v/vidmod/app_video.go new file mode 100644 index 0000000..18f07a3 --- /dev/null +++ b/models/v/vidmod/app_video.go @@ -0,0 +1,262 @@ +package vidmod + +import ( + "strconv" + + "91porn-server/common/timeutil/timerange" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func (receiver *AppListReq) LatestFilter() (primitive.M, error) { + // 时间对齐5分钟 + recentMinute := timerange.RecentMinute(receiver.Time, 5) + // 视频帖子并且已通过审核 + filter := bson.M{ + "newsType": "SP", + "status": 1, + "reviewAt": bson.M{"$lte": recentMinute}, + } + if receiver.Tag != "" { + tagID, err := primitive.ObjectIDFromHex(receiver.Tag) + if err != nil { + return nil, err + } + filter["tags"] = bson.M{"$elemMatch": bson.M{"$eq": tagID}} + } + if receiver.PaymentType == PaymentVIP { + filter["coins"] = 0 + } else if receiver.PaymentType == PaymentGold { + filter["coins"] = bson.M{"$gt": 0} + } + if receiver.Type == LongVideo { + filter["playTime"] = bson.M{"$gte": 600} + } else if receiver.Type == ShortVideo { + filter["playTime"] = bson.M{"$lt": 600} + } + return filter, nil +} + +// MostLikedFilter 点赞最多搜索条件 +func (receiver AppListReq) MostLikedFilter() (primitive.M, error) { + // 视频帖子并且已通过审核 + filter := bson.M{ + "newsType": "SP", + "status": 1, + } + if receiver.Tag != "" { + tagID, err := primitive.ObjectIDFromHex(receiver.Tag) + if err != nil { + return nil, err + } + filter["tags"] = bson.M{"$elemMatch": bson.M{"$eq": tagID}} + } + if receiver.PaymentType == PaymentVIP { + filter["coins"] = 0 + } else if receiver.PaymentType == PaymentGold { + filter["coins"] = bson.M{"$gt": 0} + } + if receiver.Type == LongVideo { + filter["playTime"] = bson.M{"$gte": 600} + } else if receiver.Type == ShortVideo { + filter["playTime"] = bson.M{"$lt": 600} + } + return filter, nil +} + +// MostPlayFilter 播放最多搜索条件 +func (receiver *AppListReq) MostPlayFilter() (primitive.M, error) { + // 视频帖子并且已通过审核 + filter := bson.M{ + "newsType": "SP", + "status": 1, + } + if receiver.Tag != "" { + tagID, err := primitive.ObjectIDFromHex(receiver.Tag) + if err != nil { + return nil, err + } + filter["tags"] = bson.M{"$elemMatch": bson.M{"$eq": tagID}} + } + if receiver.PaymentType == PaymentVIP { + filter["coins"] = 0 + } else if receiver.PaymentType == PaymentGold { + filter["coins"] = bson.M{"$gt": 0} + } + if receiver.Type == LongVideo { + filter["playTime"] = bson.M{"$gte": 600} + } else if receiver.Type == ShortVideo { + filter["playTime"] = bson.M{"$lt": 600} + } + return filter, nil +} + +func (receiver *AppListReq) HappinessPlazaFilter() (primitive.M, error) { + // 时间对齐5分钟 + recentMinute := timerange.RecentMinute(receiver.Time, 5) + // 视频帖子并且已通过审核 + filter := bson.M{ + "newsType": "COVER", + "status": 1, + "reviewAt": bson.M{"$lte": recentMinute}, + } + return filter, nil +} + +func (receiver *AppListReq) Filter(uid uint64) (primitive.M, error) { + // 视频帖子并且已通过审核 + filter := bson.M{ + "newsType": SP, + "status": 1, + } + if receiver.Tag != "" { + tagID, err := primitive.ObjectIDFromHex(receiver.Tag) + if err != nil { + return nil, err + } + filter["tags"] = tagID // bson.M{"$elemMatch": bson.M{"$eq": tagID}} + } + if receiver.PaymentType == PaymentVIP { + filter["coins"] = 0 + } else if receiver.PaymentType == PaymentGold { + filter["coins"] = bson.M{"$gt": 0} + } + switch receiver.Type { + case LongVideo: + filter["playTime"] = bson.M{"$gte": 600} + case ShortVideo: + filter["playTime"] = bson.M{"$lt": 600} + case Image: + filter["newsType"] = COVER + } + // 时间对齐5分钟 + recentMinute := timerange.RecentMinute(receiver.Time, 5) + switch receiver.Model { + case LatestZone: + filter["reviewAt"] = bson.M{"$lte": recentMinute} + case HappinessPlaza: + filter["reviewAt"] = bson.M{"$lte": recentMinute} + case MyImage: + if receiver.UID != 0 { + filter["publisherID"] = receiver.UID + } else { + filter["publisherID"] = uid + delete(filter, "status") + } + } + return filter, nil +} + +// Options 分页及排序处理 +func (receiver *AppListReq) Options() *options.FindOptions { + opts := options.Find() //如果是幸福广场,不进行分页 + if receiver.Model != HappinessPlaza { //否则进行分页 + opts.SetSkip(int64((receiver.PageNumber - 1) * receiver.PageSize)).SetLimit(int64(receiver.PageSize + 1)) + } + var sort bson.D + if receiver.FilterType == 1 { + sort = bson.D{{Key: "recoWeigh", Value: -1}} + opts.SetSort(sort) + } else if receiver.FilterType == 2 { + sort = bson.D{{Key: "createdAt", Value: -1}} + opts.SetSort(sort) + } + switch receiver.Model { + case LatestZone: + opts.SetSort(mergeSort(sort, bson.D{{Key: "reviewAt", Value: -1}})) + case HottestZone: + opts.SetSort(mergeSort(sort, bson.D{{Key: "hot", Value: -1}, {Key: "reviewAt", Value: -1}})) + case MostLiked: + opts.SetSort(mergeSort(sort, bson.D{{Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}})) + case MostPlay: + opts.SetSort(mergeSort(sort, bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}})) + case MyImage: + opts.SetSort(mergeSort(sort, bson.D{{Key: "createdAt", Value: -1}})) + case HappinessPlaza: + //opts.SetSort(bson.D{{"happinessPlazaTop", -1}, {"reviewAt", -1}}) + opts.SetSort(mergeSort(sort, bson.D{{Key: "reviewAt", Value: -1}})) //统一默认按照审核时间排序 + } + return opts +} + +func mergeSort(m bson.D, mm bson.D) bson.D { + if len(m) == 0 { + return mm + } + if len(mm) == 0 { + return m + } + for _, vv := range mm { + for _, v := range m { + if v.Key == vv.Key { + continue + } + } + m = append(m, vv) + } + return m +} + +// RedisKey 获取redis缓存key +func (receiver *AppListReq) RedisKey(uid uint64) string { + redisKey := "appListReqCache:status:1" + if receiver.Type == Image { + redisKey += ":newsType:" + COVER + } else { + redisKey += ":newsType:" + SP + } + if receiver.Tag != "" { + tagID, err := primitive.ObjectIDFromHex(receiver.Tag) + if err != nil { + return "" + } + redisKey += ":tags:" + tagID.Hex() + } + if receiver.PaymentType == PaymentVIP { + redisKey += ":coins:0" + } else if receiver.PaymentType == PaymentGold { + redisKey += ":coins:gt0" + } + if receiver.Type == LongVideo { + redisKey += ":playTime:gte600" + } + if receiver.Type == ShortVideo { + redisKey += ":playTime:lt600" + } + // 时间对齐5分钟 + recentMinute := timerange.RecentMinute(receiver.Time, 5) + switch receiver.Model { + case LatestZone: + redisKey += ":reviewAt:lte" + recentMinute.Format("200601021504") + case HappinessPlaza: + redisKey += ":reviewAt:lte" + recentMinute.Format("200601021504") + case MyImage: + if receiver.UID != 0 { + redisKey += ":publisherID:" + strconv.FormatUint(receiver.UID, 10) + } else { + redisKey += ":publisherID:" + strconv.FormatUint(receiver.UID, 10) + redisKey += ":status:delete" + } + } + if receiver.Model != HappinessPlaza { //分页信息 + redisKey += ":skip:" + strconv.FormatUint((receiver.PageNumber-1)*receiver.PageSize, 10) + ":limit:" + strconv.FormatUint((receiver.PageSize+1), 10) + } + switch receiver.Model { + case LatestZone: + redisKey += ":reviewAt:-1" + case HottestZone: + redisKey += ":hot:-1:reviewAt:-1" + case MostLiked: + redisKey += ":likeCount:-1:reviewAt:-1" + case MostPlay: + redisKey += ":playCount:-1:reviewAt:-1" + case MyImage: + redisKey += ":createdAt:-1" + case HappinessPlaza: + redisKey += ":reviewAt:-1" + } + redisKey += ":filterType:" + strconv.FormatInt(int64(receiver.FilterType), 10) + return redisKey +} diff --git a/models/v/vidmod/cache.go b/models/v/vidmod/cache.go new file mode 100644 index 0000000..bf64eda --- /dev/null +++ b/models/v/vidmod/cache.go @@ -0,0 +1,85 @@ +package vidmod + +import ( + "91porn-server/app/appg" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/redis" + "errors" + "fmt" + + "github.com/vmihailenco/msgpack/v5" +) + +func getRedis() *redis.Client { + return appg.Redis +} + +// GetByIDFromRedis 根据id获取一条记录 +func GetByIDFromRedis(id string) (vid VideoModel, err error) { + redisKey := redisconst.DataCachKey(table, id) + redisc := getRedis() + if redisc == nil || !redisc.Exists(redisKey) { + vid, err = GetVideoInfo(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByID", table, "FindOne", err), log.Any("id", id)) + } + if err != nil { + return + } + if redisc == nil { + return + } + var jsonBytes []byte + jsonBytes, err = msgpack.Marshal(&vid) + if err != nil { + return + } + err = redisc.Set(redisKey, string(jsonBytes), redisconst.DataCachExpire) + return + } + str, err := redisc.Get(redisKey) + if err != nil { + return + } + if str == nil { + err = errors.New("redis key is null") + return + } + err = msgpack.Unmarshal([]byte(*str), &vid) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByID", table, "FindOne", err), log.Any("id", id)) + } + return +} + +func GetShareListFromRedis() ([]ShareInfo, error) { + redisKey := redisconst.GetVideoShareListKey() + redisc := getRedis() + if redisc == nil || !redisc.Exists(redisKey) { + status := CheckPass + sis, err := getShareList(&status) + if err != nil { + return nil, err + } + if redisc == nil { + return sis, err + } + var jsonBytes []byte + jsonBytes, err = msgpack.Marshal(sis) + if err != nil { + return sis, nil + } + err = redisc.Set(redisKey, jsonBytes, redisconst.GetVideoShareListExpired()) + return sis, nil + } + str, err := redisc.Get(redisKey) + if err != nil { + return nil, err + } + if str == nil { + return nil, errors.New("redis key is null") + } + var sis []ShareInfo + return sis, msgpack.Unmarshal([]byte(*str), &sis) +} diff --git a/models/v/vidmod/es.go b/models/v/vidmod/es.go new file mode 100644 index 0000000..b6adf28 --- /dev/null +++ b/models/v/vidmod/es.go @@ -0,0 +1,161 @@ +package vidmod + +import ( + "fmt" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" +) + +var es *elastic.Client + +const ESTable = models.ESInfoVideoTable + +func InitESIndex() { + es = elastic.Init() + var setting = elastic.M{ + "settings": elastic.M{ + "number_of_shards": elastic.NumberOfShards, + "number_of_replicas": elastic.NumberOfReplicas, + "analysis": elastic.M{ + "analyzer": elastic.M{ + "ik": elastic.M{ + "tokenizer": elastic.AnalyzerIkSmart, + }, + }, + }, + }, + "mappings": elastic.M{ + "properties": elastic.M{ + "title": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkSmart, + "search_analyzer": elastic.AnalyzerIkSmart, + }, + "tagsName": elastic.M{ + "type": "text", + "analyzer": elastic.AnalyzerIkSmart, + "search_analyzer": elastic.AnalyzerIkSmart, + }, + }, + }, + } + if err := es.CreateIndices(ESTable, setting); err != nil { + panic(fmt.Sprintf("%s index indeices err ==>[%+v]", ESTable, err)) + } +} + +// 根据关键字搜索 +func Search(keywords string, from int64, size int64) (data []ESVideoSource, err error) { + es = elastic.Init() + query := elastic.M{ + "query": elastic.M{ + "bool": elastic.M{ + "must": elastic.A{ + {"multi_match": elastic.M{ + "query": keywords, + "fields": []string{"title"}}, + }, + {"terms": elastic.M{"status": []int{1, 3}}}, + {"range": elastic.M{"playTime": elastic.M{"lt": 600}}}, + }, + }, + }, + "sort": elastic.A{{"hot": elastic.M{"order": "desc"}}}, + "from": from, + "size": size, + } + if err = es.Search(ESTable, &data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Search", table, "CommonSearch", err), + log.Any("keywords", keywords), + log.Any("from", from), + log.Any("size", size)) + return + } + return +} +func SearchByCondWithTotal(filter elastic.M) (data ESVideoSourceWithTotal, err error) { + es = elastic.Init() + if err = es.SearchWithTotal(ESTable, &data, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Search", table, "CommonSearch", err), log.Any("filter", filter)) + return + } + return +} + +func SearchWithTotal(keywords string, from int64, size int64) (data ESVideoSourceWithTotal, err error) { + es = elastic.Init() + query := elastic.M{ + "query": elastic.M{ + "bool": elastic.M{ + "must": elastic.A{ + {"multi_match": elastic.M{ + "query": keywords, + "fields": []string{"title"}}, + }, + {"terms": elastic.M{"status": []int{1, 3}}}, + }, + }, + }, + "sort": elastic.A{{"hot": elastic.M{"order": "desc"}}}, + "from": from, + "size": size, + } + if err = es.SearchWithTotal(ESTable, &data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Search", table, "CommonSearch", err), + log.Any("keywords", keywords), + log.Any("from", from), + log.Any("size", size)) + return + } + return +} + +// DirectSearch 用户根据关键字搜索 +func DirectSearch(keywords string, vidType string, sortType int, from int64, size int64) (data []ESVideoSource, err error) { + es = elastic.Init() + query := elastic.M{ + "query": elastic.M{ + "bool": elastic.M{ + "must": elastic.A{ + {"multi_match": elastic.M{ + "query": keywords, + "fields": []string{"title"}}, + }, + {"terms": elastic.M{"status": []int{1, 3}}}, + {"term": elastic.M{"newsType.keyword": vidType}}, + }, + }, + }, + // "sort": elastic.A{{"hot": elastic.M{"order": "desc"}}}, + "from": from, + "size": size, + } + // sortType 1最多观看 2最新上架 3最多收藏 + switch sortType { + case 1: + query["sort"] = elastic.A{{"playCount": elastic.M{"order": "desc"}}} + case 2: + query["sort"] = elastic.A{{"createdAt": elastic.M{"order": "desc"}}} + case 3: + query["sort"] = elastic.A{{"collectCount": elastic.M{"order": "desc"}}} + } + if err = es.Search(ESTable, &data, query); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Search", table, "CommonSearch", err), + log.Any("keywords", keywords), + log.Any("from", from), + log.Any("size", size)) + return + } + return +} + +func DeleteByCond(filter elastic.M) (err error) { + es = elastic.Init() + if err = es.BulkDelete(ESTable, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Search", table, "CommonSearch", err), log.Any("filter", filter)) + return + } + return +} diff --git a/models/v/vidmod/free_trial_response_test.go b/models/v/vidmod/free_trial_response_test.go new file mode 100644 index 0000000..9de6dc2 --- /dev/null +++ b/models/v/vidmod/free_trial_response_test.go @@ -0,0 +1,54 @@ +package vidmod + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestWatchCountRespJSONIncludesTotal(t *testing.T) { + data, err := json.Marshal(WatchCountResp{ + IsCan: true, + WatchCount: 2, + TotalWatchCount: 3, + }) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(data), `"totalWatchCount":3`) { + t.Fatalf("totalWatchCount missing from response: %s", data) + } +} + +func TestVideoInfoIncludesFreeTrialFieldsWhenFalse(t *testing.T) { + for name, value := range map[string]interface{}{ + "VideoInfo": VideoInfo{}, + "VideoInfoResp": VideoInfoResp{}, + } { + t.Run(name, func(t *testing.T) { + data, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + for _, field := range []string{ + `"showFreeTrialBadge":false`, + `"freeTrialRemaining":0`, + `"canUseFreeTrial":false`, + } { + if !strings.Contains(string(data), field) { + t.Fatalf("%s missing from response: %s", field, data) + } + } + }) + } +} + +func TestVideoInfoRespIncludesFreeArea(t *testing.T) { + data, err := json.Marshal(VideoInfoResp{FreeArea: true}) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(data), `"freeArea":true`) { + t.Fatalf("freeArea missing from response: %s", data) + } +} diff --git a/models/v/vidmod/hevc.go b/models/v/vidmod/hevc.go new file mode 100644 index 0000000..cb70421 --- /dev/null +++ b/models/v/vidmod/hevc.go @@ -0,0 +1,276 @@ +package vidmod + +import ( + "fmt" + "strings" + "time" + + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func h265URLMissingFilter() bson.A { + return bson.A{ + bson.M{"h265Url": ""}, + bson.M{"h265Url": nil}, + bson.M{"h265Url": bson.M{"$exists": false}}, + } +} + +func h265FailCountRetryableFilter() bson.A { + return bson.A{ + bson.M{"h265FailCount": bson.M{"$exists": false}}, + bson.M{"h265FailCount": bson.M{"$lt": H265MaxFailCount}}, + } +} + +func h265QueueableStatusFilter() bson.A { + return bson.A{ + bson.M{"h265Status": bson.M{"$exists": false}}, + bson.M{"h265Status": H265StatusNone}, + bson.M{"h265Status": H265StatusFailed, "$or": h265FailCountRetryableFilter()}, + } +} + +// QueueH265Transcode 将审核通过且尚无 H.265 地址的长视频加入等待队列。 +func QueueH265Transcode(ids []primitive.ObjectID) (int64, error) { + if len(ids) == 0 { + return 0, nil + } + now := time.Now() + filter := bson.M{ + "_id": bson.M{"$in": ids}, + "status": CheckPass, + "newsType": SP, + "sourceURL": bson.M{"$exists": true, "$ne": ""}, + "$and": bson.A{ + bson.M{"$or": h265URLMissingFilter()}, + bson.M{"$or": h265QueueableStatusFilter()}, + }, + } + update := bson.M{ + "$set": bson.M{ + "h265Status": H265StatusQueued, + "h265QueuedAt": now, + "updatedAt": now, + }, + "$unset": bson.M{"h265PendingAt": ""}, + } + result, err := coll(nil).UpdateMany(filter, update) + if err != nil { + log.Error("QueueH265Transcode UpdateMany failed", log.Any("ids", ids), log.E(err)) + return 0, err + } + return result.ModifiedCount, nil +} + +// CountH265Pending 统计已经提交云端、尚未取得结果的有效长视频。 +func CountH265Pending() (int64, error) { + filter := bson.M{ + "h265Status": H265StatusPending, + "status": CheckPass, + "newsType": SP, + "sourceURL": bson.M{"$exists": true, "$ne": ""}, + "$or": h265URLMissingFilter(), + } + count, err := coll(nil).Count(filter) + if err != nil { + log.Error("CountH265Pending Count failed", log.E(err)) + } + return count, err +} + +// GetQueuedH265Videos 获取等待提交或允许重试的 H.265 任务。 +func GetQueuedH265Videos(limit int64) ([]*VideoModel, error) { + if limit <= 0 { + return []*VideoModel{}, nil + } + filter := bson.M{ + "h265Status": bson.M{"$in": bson.A{H265StatusQueued, H265StatusFailed}}, + "status": CheckPass, + "newsType": SP, + "sourceURL": bson.M{"$exists": true, "$ne": ""}, + "$and": bson.A{ + bson.M{"$or": h265URLMissingFilter()}, + bson.M{"$or": h265FailCountRetryableFilter()}, + }, + } + opts := options.Find(). + SetLimit(limit). + SetSort(bson.D{{Key: "h265QueuedAt", Value: 1}, {Key: "reviewAt", Value: -1}}) + var out []*VideoModel + if err := coll(nil).Find(&out, filter, opts); err != nil { + log.Error("GetQueuedH265Videos Find failed", log.Any("filter", filter), log.E(err)) + return nil, err + } + return out, nil +} + +// GetPendingH265Videos 获取已经提交云端、等待轮询结果的 H.265 任务。 +func GetPendingH265Videos(limit int64) ([]*VideoModel, error) { + if limit <= 0 { + return []*VideoModel{}, nil + } + filter := bson.M{ + "h265Status": H265StatusPending, + "status": CheckPass, + "newsType": SP, + "sourceURL": bson.M{"$exists": true, "$ne": ""}, + "$or": h265URLMissingFilter(), + } + opts := options.Find().SetLimit(limit).SetSort(bson.D{{Key: "h265PendingAt", Value: 1}}) + var out []*VideoModel + if err := coll(nil).Find(&out, filter, opts); err != nil { + log.Error("GetPendingH265Videos Find failed", log.Any("filter", filter), log.E(err)) + return nil, err + } + return out, nil +} + +// ClaimH265Pending 原子地将一个等待任务标记为云端处理中,并返回本次 +// claim 的整秒时间。SKD 用该时间生成稳定的限时拉流 URL,重启后仍能算出 +// 与提交时相同的云端 file_id。 +func ClaimH265Pending(id primitive.ObjectID) (time.Time, bool, error) { + now := time.Now().UTC().Truncate(time.Second) + filter := bson.M{ + "_id": id, + "h265Status": bson.M{"$in": bson.A{H265StatusQueued, H265StatusFailed}}, + "status": CheckPass, + "newsType": SP, + "sourceURL": bson.M{"$exists": true, "$ne": ""}, + "$and": bson.A{ + bson.M{"$or": h265URLMissingFilter()}, + bson.M{"$or": h265FailCountRetryableFilter()}, + }, + } + result, err := coll(nil).UpdateOne(filter, bson.M{"$set": bson.M{ + "h265Status": H265StatusPending, + "h265PendingAt": now, + "updatedAt": now, + }}) + if err != nil { + log.Error("ClaimH265Pending UpdateOne failed", log.Any("id", id), log.E(err)) + return time.Time{}, false, err + } + if result.ModifiedCount == 0 { + return time.Time{}, false, nil + } + return now, true, nil +} + +// MarkH265Queued 将当前 pending 尝试重新放回等待队列。 +// sourceURL 与 pendingAt 共同标识一次云端尝试,避免旧实例把同视频的 +// 新尝试或已经成功回填的状态回退成 queued。 +func MarkH265Queued(id primitive.ObjectID, sourceURL string, pendingAt time.Time) (bool, error) { + now := time.Now() + filter := pendingH265AttemptFilter(id, sourceURL, pendingAt) + result, err := coll(nil).UpdateOne(filter, bson.M{ + "$set": bson.M{ + "h265Status": H265StatusQueued, + "h265QueuedAt": now, + "updatedAt": now, + }, + "$unset": bson.M{"h265PendingAt": ""}, + }) + if err != nil { + log.Error("MarkH265Queued UpdateOne failed", log.Any("id", id), log.E(err)) + return false, err + } + return result.ModifiedCount > 0, nil +} + +// MarkH265Success 保存上游明确返回的可播放地址并将任务标记为成功。 +// 老司机重新导入属于权威回填,不受旧云转码任务状态限制。 +func MarkH265Success(id primitive.ObjectID, h265URL string) error { + h265URL = strings.TrimSpace(h265URL) + if h265URL == "" { + return fmt.Errorf("empty h265 url") + } + now := time.Now() + _, err := coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{ + "$set": bson.M{ + "h265Url": h265URL, + "h265Status": H265StatusSuccess, + "h265FailCount": 0, + "updatedAt": now, + }, + "$unset": bson.M{ + "h265QueuedAt": "", + "h265PendingAt": "", + }, + }) + if err != nil { + log.Error("MarkH265Success UpdateOne failed", log.Any("id", id), log.E(err)) + } + return err +} + +// MarkPendingH265Success 仅完成仍对应同一源地址的 pending 任务。 +// 如果视频源已变更,或其他流程已经回填 H.265,旧云任务结果必须被忽略。 +func MarkPendingH265Success(id primitive.ObjectID, sourceURL string, pendingAt time.Time, h265URL string) (bool, error) { + h265URL = strings.TrimSpace(h265URL) + if strings.TrimSpace(sourceURL) == "" { + return false, fmt.Errorf("empty source url") + } + if pendingAt.IsZero() { + return false, fmt.Errorf("empty H265 pending time") + } + if h265URL == "" { + return false, fmt.Errorf("empty h265 url") + } + now := time.Now() + filter := pendingH265AttemptFilter(id, sourceURL, pendingAt) + result, err := coll(nil).UpdateOne(filter, bson.M{ + "$set": bson.M{ + "h265Url": h265URL, + "h265Status": H265StatusSuccess, + "h265FailCount": 0, + "updatedAt": now, + }, + "$unset": bson.M{ + "h265QueuedAt": "", + "h265PendingAt": "", + }, + }) + if err != nil { + log.Error("MarkPendingH265Success UpdateOne failed", log.Any("id", id), log.E(err)) + return false, err + } + return result.ModifiedCount > 0, nil +} + +// MarkH265Failed 记录当前 pending 尝试失败;达到 H265MaxFailCount 后将不再自动重试。 +// 条件更新会跳过新尝试或已经成功回填的视频,避免旧轮询结果回退状态。 +func MarkH265Failed(id primitive.ObjectID, sourceURL string, pendingAt time.Time) (bool, error) { + now := time.Now() + filter := pendingH265AttemptFilter(id, sourceURL, pendingAt) + result, err := coll(nil).UpdateOne(filter, bson.M{ + "$set": bson.M{ + "h265Status": H265StatusFailed, + "updatedAt": now, + }, + "$inc": bson.M{"h265FailCount": 1}, + "$unset": bson.M{ + "h265QueuedAt": "", + "h265PendingAt": "", + }, + }) + if err != nil { + log.Error("MarkH265Failed UpdateOne failed", log.Any("id", id), log.E(err)) + return false, err + } + return result.ModifiedCount > 0, nil +} + +func pendingH265AttemptFilter(id primitive.ObjectID, sourceURL string, pendingAt time.Time) bson.M { + return bson.M{ + "_id": id, + "sourceURL": sourceURL, + "h265Status": H265StatusPending, + "h265PendingAt": pendingAt, + "$or": h265URLMissingFilter(), + } +} diff --git a/models/v/vidmod/hevc_test.go b/models/v/vidmod/hevc_test.go new file mode 100644 index 0000000..f4fda64 --- /dev/null +++ b/models/v/vidmod/hevc_test.go @@ -0,0 +1,33 @@ +package vidmod + +import ( + "testing" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestPendingH265SuccessFilterBindsAttemptState(t *testing.T) { + id := primitive.NewObjectID() + source := " /sp/movie/index.m3u8 " + pendingAt := time.Date(2026, 7, 25, 12, 30, 0, 0, time.UTC) + filter := pendingH265AttemptFilter(id, source, pendingAt) + + if filter["_id"] != id { + t.Fatalf("filter id = %v, want %v", filter["_id"], id) + } + if filter["sourceURL"] != source { + t.Fatalf("filter sourceURL = %q, want exact stored source %q", filter["sourceURL"], source) + } + if filter["h265Status"] != H265StatusPending { + t.Fatalf("filter status = %v, want pending", filter["h265Status"]) + } + if filter["h265PendingAt"] != pendingAt { + t.Fatalf("filter pendingAt = %v, want %v", filter["h265PendingAt"], pendingAt) + } + missing, ok := filter["$or"].(bson.A) + if !ok || len(missing) != 3 { + t.Fatalf("filter must require a missing H265 URL: %#v", filter["$or"]) + } +} diff --git a/models/v/vidmod/liaoba_top_sort_test.go b/models/v/vidmod/liaoba_top_sort_test.go new file mode 100644 index 0000000..cf5eb48 --- /dev/null +++ b/models/v/vidmod/liaoba_top_sort_test.go @@ -0,0 +1,35 @@ +package vidmod + +import ( + "sort" + "testing" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestVideoModelSortUsesStableTieBreakers(t *testing.T) { + reviewAt := time.Date(2026, 7, 28, 10, 0, 0, 0, time.UTC) + olderReviewAt := reviewAt.Add(-time.Hour) + lowerID := primitive.ObjectID{11: 1} + higherID := primitive.ObjectID{11: 2} + + videos := VideoModelSort{ + {ID: lowerID, LiaoBaTopSort: 100, ReviewAt: reviewAt}, + {ID: primitive.NewObjectID(), LiaoBaTopSort: 200, ReviewAt: olderReviewAt}, + {ID: primitive.NewObjectID(), LiaoBaTopSort: 100, ReviewAt: olderReviewAt}, + {ID: higherID, LiaoBaTopSort: 100, ReviewAt: reviewAt}, + } + + sort.Sort(videos) + + if got := videos[0].LiaoBaTopSort; got != 200 { + t.Fatalf("first liaoBaTopSort = %d, want 200", got) + } + if got := videos[1].ID; got != higherID { + t.Fatalf("same sort/reviewAt should use descending _id, got %s want %s", got.Hex(), higherID.Hex()) + } + if got := videos[2].ID; got != lowerID { + t.Fatalf("same sort should prefer newer reviewAt, got %s want %s", got.Hex(), lowerID.Hex()) + } +} diff --git a/models/v/vidmod/list.go b/models/v/vidmod/list.go new file mode 100644 index 0000000..dce8751 --- /dev/null +++ b/models/v/vidmod/list.go @@ -0,0 +1,182 @@ +package vidmod + +import ( + "fmt" + "strconv" + "time" + + "91porn-server/common/log" + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type VIDSlice []VideoModel + +func (v VIDSlice) ToMap() map[ObjectID]*VideoModel { + m := make(map[ObjectID]*VideoModel, len(v)) + for i := 0; i < len(v); i++ { + m[v[i].ID] = &v[i] + } + return m +} + +func (v VIDSlice) IDs() []ObjectID { + ids := make([]ObjectID, len(v)) + for i, v := range v { + ids[i] = v.ID + } + return ids +} + +type Matcher = pageopt.Matcher + +// IDInMatch +type IDInMatch = pageopt.IDInMatch + +// PublisherIDMatch +type PublisherIDMatch struct { + PublisherID *uint64 +} + +func (c *PublisherIDMatch) New() Matcher { + return pageopt.NewAssignMatch("publisherID", c.PublisherID) +} + +// PublisherIDInMatch +type PublisherIDInMatch struct { + PublisherIDList []uint64 +} + +func (c *PublisherIDInMatch) New() Matcher { + return pageopt.NewInMatch("publisherID", c.PublisherIDList) +} + +// StatusInMatch +type StatusInMatch struct { + StatusList []int +} + +func (c *StatusInMatch) New() Matcher { + return pageopt.NewInMatch("status", c.StatusList) +} + +// StatusInMatch +type StatusMatch struct { + Status int +} + +func (c *StatusMatch) New() Matcher { + return pageopt.NewAssignMatch("status", c.Status) +} + +// ReviewAtGTEAndLTMatch +type ReviewAtGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (c *ReviewAtGTEAndLTMatch) New() Matcher { + return pageopt.NewGTEAndLTMatch("reviewAt", c.GTE, c.LT) +} + +// UpdatedAtGTEAndLTMatch +type UpdatedAtGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (c *UpdatedAtGTEAndLTMatch) New() Matcher { + return pageopt.NewGTEAndLTMatch("updatedAt", c.GTE, c.LT) +} + +type CreatedAtGTEAndLTMatch = pageopt.CreatedAtGTEAndLTMatch + +type Sort = bson.D + +func List(sort Sort, skip, limit *int64, matchers ...Matcher) (VIDSlice, error) { + filter := pageopt.MergeM(matchers) + opt := &options.FindOptions{} + if len(sort) != 0 { + opt.SetSort(sort) + } + if skip != nil { + opt.SetSkip(*skip) + } + if limit != nil { + opt.SetLimit(*limit) + } + list := VIDSlice{} + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.Error("vidmod List error", log.E(err)) + return nil, err + } + return list, nil +} + +func Count(matchers ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchers) + count, err := coll(nil).Count(filter) + if err != nil { + log.Error("vidmod Count error", log.E(err)) + return 0, err + } + return count, nil +} + +func CountMapByPublisherID(matchers ...Matcher) (map[uint64]int64, error) { + filter := pageopt.MergeM(matchers) + pipeline := []M{ + {"$match": filter}, + {"$group": M{"_id": "$publisherID", "count": M{"$sum": 1}}}, + } + var list []struct { + PublisherID uint64 `bson:"_id"` + Count int64 `bson:"count"` + } + if err := coll(nil).Aggregate(&list, pipeline); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountTotalLikeByUID", table, "Aggregate", err), + log.Any("filter", filter), + ) + return nil, err + } + m := make(map[uint64]int64, len(list)) + for _, v := range list { + m[v.PublisherID] = v.Count + } + return m, nil +} + +func PublisherIDList(matchers ...Matcher) (uids []uint64, err error) { + filter := pageopt.MergeM(matchers) + list, err := coll(nil).Distinct("publisherID", filter) + if err != nil { + return nil, err + } + for _, i := range list { + id, err := strconv.Atoi(fmt.Sprintf("%v", i)) + if err != nil { + continue + } + uids = append(uids, uint64(id)) + } + return uids, nil +} + +func CountNum(filter interface{}) (int64, error) { + count, err := coll(nil).Count(filter) + if err != nil { + log.Error("vidmod Count error", log.E(err)) + return 0, err + } + return count, nil +} + +func GetListByCond(cond bson.M, opts *options.FindOptions) ([]VideoModel, error) { + var data []VideoModel + if err := coll(nil).Find(&data, cond, opts); err != nil { + return nil, err + } + return data, nil +} diff --git a/models/v/vidmod/m3u8sign.go b/models/v/vidmod/m3u8sign.go new file mode 100644 index 0000000..cd71046 --- /dev/null +++ b/models/v/vidmod/m3u8sign.go @@ -0,0 +1,52 @@ +package vidmod + +// M3u8Signer 就地改写单个 m3u8 播放地址字段,由 app/service/m3u8ticket 实现。 +// playable 表示正片、preview 表示试看,用于票据里区分播放语义。 +type M3u8Signer interface { + SignM3u8URL(field *string, playable, preview bool) +} + +// M3u8Signable 由携带 m3u8 播放地址的响应体实现:把自己(含嵌套列表)所有需要签票的地址字段 +// 逐个交给 signer 就地改写,避免 m3u8ticket 反向依赖上层包,也避免每次请求反射遍历响应体。 +type M3u8Signable interface { + SignM3u8(s M3u8Signer) +} + +// SignM3u8Infos 依次为列表里每个 *VideoInfo 签票,供各外层响应体的 SignM3u8 复用(nil 元素安全跳过)。 +func SignM3u8Infos(s M3u8Signer, list []*VideoInfo) { + for _, v := range list { + v.SignM3u8(s) + } +} + +// SignM3u8Resps 依次为列表里每个 *VideoInfoResp 签票,供各外层响应体的 SignM3u8 复用(nil 元素安全跳过)。 +func SignM3u8Resps(s M3u8Signer, list []*VideoInfoResp) { + for _, v := range list { + v.SignM3u8(s) + } +} + +// SignM3u8 对详情/列表项的 sourceURL、h265Url 按正片,previewURL 按试看交给 signer 签票。 +func (v *VideoInfo) SignM3u8(s M3u8Signer) { + if v == nil { + return + } + s.SignM3u8URL(&v.SourceURL, true, false) + s.SignM3u8URL(&v.H265Url, true, false) + s.SignM3u8URL(&v.PreviewURL, false, true) +} + +// SignM3u8 对列表返回体(无 previewURL 字段)的 sourceURL、h265Url 按正片签票。 +func (v *VideoInfoResp) SignM3u8(s M3u8Signer) { + if v == nil { + return + } + s.SignM3u8URL(&v.SourceURL, true, false) + s.SignM3u8URL(&v.H265Url, true, false) +} + +// 编译期断言:叶子视频类型实现 M3u8Signable,供 m3u8ticket 零反射签票分支使用。 +var ( + _ M3u8Signable = (*VideoInfo)(nil) + _ M3u8Signable = (*VideoInfoResp)(nil) +) diff --git a/models/v/vidmod/m3u8sign_test.go b/models/v/vidmod/m3u8sign_test.go new file mode 100644 index 0000000..c36d28d --- /dev/null +++ b/models/v/vidmod/m3u8sign_test.go @@ -0,0 +1,35 @@ +package vidmod + +import "testing" + +// countingSigner 记录被交出的地址字段个数,用于验证各响应体 SignM3u8 的字段枚举是否完整。 +type countingSigner struct{ n int } + +func (c *countingSigner) SignM3u8URL(field *string, playable, preview bool) { c.n++ } + +func TestVideoInfoOffersThreeFields(t *testing.T) { + c := &countingSigner{} + (&VideoInfo{}).SignM3u8(c) + if c.n != 3 { // sourceURL + h265Url + previewURL + t.Fatalf("VideoInfo should offer 3 url fields, got %d", c.n) + } +} + +func TestVideoInfoRespOffersTwoFields(t *testing.T) { + c := &countingSigner{} + (&VideoInfoResp{}).SignM3u8(c) + if c.n != 2 { // sourceURL + h265Url(无 previewURL) + t.Fatalf("VideoInfoResp should offer 2 url fields, got %d", c.n) + } +} + +func TestNilVideosAreSafe(t *testing.T) { + c := &countingSigner{} + var v *VideoInfo + v.SignM3u8(c) // nil 接收者不应 panic + SignM3u8Infos(c, []*VideoInfo{nil}) + SignM3u8Resps(c, []*VideoInfoResp{nil}) + if c.n != 0 { + t.Fatalf("nil videos must not offer any field, got %d", c.n) + } +} diff --git a/models/v/vidmod/payment_guide.go b/models/v/vidmod/payment_guide.go new file mode 100644 index 0000000..83fc54b --- /dev/null +++ b/models/v/vidmod/payment_guide.go @@ -0,0 +1,72 @@ +package vidmod + +import ( + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type PaymentGuideVideo struct { + ID string `json:"id"` + Title string `json:"title"` + Cover string `json:"cover"` + CoverThumb string `json:"coverThumb"` + PlayTime uint `json:"playTime"` + PlayCount int `json:"playCount"` + FakePlayCount int `json:"-"` +} + +// LatestVIPContent returns the newest approved VIP videos for the +// VIP-content-update guide. Free-area, paid-coin and non-recommendable videos +// are not VIP content for this scene. +func LatestVIPContent(limit int64, excludedModuleIDs []string) ([]PaymentGuideVideo, error) { + if limit <= 0 { + return []PaymentGuideVideo{}, nil + } + filter := latestVIPContentFilter(excludedModuleIDs) + opts := options.Find(). + SetLimit(limit). + SetSort(bson.D{{Key: "reviewAt", Value: -1}, {Key: "_id", Value: -1}}). + SetProjection(bson.M{ + "_id": 1, + "title": 1, + "cover": 1, + "coverThumb": 1, + "playTime": 1, + "playCount": 1, + "fakePlayCount": 1, + }) + var videos []VideoModel + if err := coll(nil).Find(&videos, filter, opts); err != nil { + return nil, err + } + result := make([]PaymentGuideVideo, 0, len(videos)) + for _, video := range videos { + if video.ID.IsZero() { + continue + } + result = append(result, PaymentGuideVideo{ + ID: video.ID.Hex(), + Title: video.Title, + Cover: video.Cover, + CoverThumb: video.CoverThumb, + PlayTime: video.PlayTime, + PlayCount: video.PlayCount, + FakePlayCount: video.FakePlayCount, + }) + } + return result, nil +} + +func latestVIPContentFilter(excludedModuleIDs []string) bson.M { + filter := bson.M{ + "status": CheckPass, + "newsType": bson.M{"$in": []string{SP, SHORT}}, + "coins": 0, + "freeArea": bson.M{"$ne": true}, + "recoWeight": bson.M{"$ne": -1}, + } + if len(excludedModuleIDs) > 0 { + filter["mId"] = bson.M{"$nin": excludedModuleIDs} + } + return filter +} diff --git a/models/v/vidmod/payment_guide_test.go b/models/v/vidmod/payment_guide_test.go new file mode 100644 index 0000000..723c8d1 --- /dev/null +++ b/models/v/vidmod/payment_guide_test.go @@ -0,0 +1,31 @@ +package vidmod + +import ( + "reflect" + "testing" + + "go.mongodb.org/mongo-driver/bson" +) + +func TestLatestVIPContentFilter(t *testing.T) { + excluded := []string{"module-a", "module-b"} + got := latestVIPContentFilter(excluded) + want := bson.M{ + "status": CheckPass, + "newsType": bson.M{"$in": []string{SP, SHORT}}, + "coins": 0, + "freeArea": bson.M{"$ne": true}, + "recoWeight": bson.M{"$ne": -1}, + "mId": bson.M{"$nin": excluded}, + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("latestVIPContentFilter() = %#v, want %#v", got, want) + } +} + +func TestLatestVIPContentFilterWithoutExcludedModules(t *testing.T) { + got := latestVIPContentFilter(nil) + if _, exists := got["mId"]; exists { + t.Fatalf("mId filter must be absent when no modules are excluded: %#v", got) + } +} diff --git a/models/v/vidmod/ranking.go b/models/v/vidmod/ranking.go new file mode 100644 index 0000000..7c722a0 --- /dev/null +++ b/models/v/vidmod/ranking.go @@ -0,0 +1,36 @@ +package vidmod + +import ( + "time" + + "91porn-server/common/stderr" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const rankingQueryMaxTime = 3 * time.Second + +// FindCumulativeRanking keeps the ranking usable when the Redis period rank is +// empty. It reuses persisted popularity counters and the existing compound +// index for a bounded fallback query. +func FindCumulativeRanking(newsType string, page commod.Page) ([]*VideoModel, bool, error) { + if newsType == "" || page.PageNumber == 0 || page.PageSize == 0 || page.PageSize > 100 { + return nil, false, stderr.ErrParamError + } + filter, sorts := cumulativeRankingQuery(newsType) + opts := options.Find(). + SetSkip(page.Skip64()). + SetLimit(page.Limit64()). + SetSort(sorts). + SetMaxTime(rankingQueryMaxTime) + return FindList(filter, opts) +} + +func cumulativeRankingQuery(newsType string) (bson.M, bson.D) { + return bson.M{ + "newsType": newsType, + "status": bson.M{"$in": []int{CheckPass, Free}}, + }, bson.D{{Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}} +} diff --git a/models/v/vidmod/recommend_batch_test.go b/models/v/vidmod/recommend_batch_test.go new file mode 100644 index 0000000..e9dfcef --- /dev/null +++ b/models/v/vidmod/recommend_batch_test.go @@ -0,0 +1,1491 @@ +package vidmod + +import ( + "context" + "errors" + "fmt" + "reflect" + "sort" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +type fakeRecommendScoreStore struct { + maxEligibleID func(context.Context) (ObjectID, error) + countUninitialized func(context.Context, ObjectID, int64) (int64, error) + openCursor func(context.Context, ObjectID, int32) (recommendScoreCursor, error) + findBatch func(context.Context, ObjectID, ObjectID, int) ([]recommendScoreDocument, error) + bulkWrite func(context.Context, []mongo.WriteModel) error +} + +func (f fakeRecommendScoreStore) MaxEligibleID(ctx context.Context) (ObjectID, error) { + return f.maxEligibleID(ctx) +} + +func (f fakeRecommendScoreStore) CountUninitialized( + ctx context.Context, + maxID ObjectID, + limit int64, +) (int64, error) { + if f.countUninitialized == nil { + return 0, nil + } + return f.countUninitialized(ctx, maxID, limit) +} + +func (f fakeRecommendScoreStore) OpenCursor( + ctx context.Context, + maxID ObjectID, + batchSize int32, +) (recommendScoreCursor, error) { + if f.openCursor != nil { + return f.openCursor(ctx, maxID, batchSize) + } + if f.findBatch == nil { + return nil, errors.New("fake recommend score cursor is not configured") + } + documents := make([]recommendScoreDocument, 0) + afterID := ObjectID{} + limit := int(batchSize) + for { + batch, err := f.findBatch(ctx, afterID, maxID, limit) + if err != nil { + return nil, err + } + if len(batch) == 0 { + break + } + documents = append(documents, batch...) + afterID = batch[len(batch)-1].ID + if len(batch) < limit || afterID == maxID { + break + } + } + return &fakeRecommendScoreCursor{documents: documents, current: -1}, nil +} + +func (f fakeRecommendScoreStore) BulkWrite(ctx context.Context, writes []mongo.WriteModel) error { + return f.bulkWrite(ctx, writes) +} + +type fakeRecommendScoreCursor struct { + documents []recommendScoreDocument + index int + current int + cursorErr error + decodeErrAt int + decodeErr error + closeErr error + closeCalls atomic.Int32 + closeContextCanceled atomic.Bool +} + +func (f *fakeRecommendScoreCursor) Next(ctx context.Context) bool { + if err := ctx.Err(); err != nil { + f.cursorErr = err + return false + } + if f.index >= len(f.documents) { + return false + } + f.current = f.index + f.index++ + return true +} + +func (f *fakeRecommendScoreCursor) Decode(value interface{}) error { + if f.decodeErr != nil && f.current == f.decodeErrAt { + return f.decodeErr + } + document, ok := value.(*recommendScoreDocument) + if !ok { + return fmt.Errorf("unexpected decode target %T", value) + } + if f.current < 0 || f.current >= len(f.documents) { + return errors.New("decode called without current document") + } + *document = f.documents[f.current] + return nil +} + +func (f *fakeRecommendScoreCursor) Err() error { + return f.cursorErr +} + +func (f *fakeRecommendScoreCursor) Close(ctx context.Context) error { + f.closeCalls.Add(1) + f.closeContextCanceled.Store(ctx.Err() != nil) + return f.closeErr +} + +type refreshRecommendScoresResult struct { + candidates []RecommendCandidate + err error +} + +func recommendBatchTestID(value byte) ObjectID { + var id ObjectID + id[len(id)-1] = value + return id +} + +func recommendBatchTestScore(value int64) *int64 { + return &value +} + +func recommendBatchTestDocuments(count int) []recommendScoreDocument { + documents := make([]recommendScoreDocument, count) + for i := range documents { + documents[i] = recommendScoreDocument{ + ID: recommendBatchTestID(byte(i + 1)), + RecommendLikeCount: int64(i + 1), + RecommendInitialized: true, + } + } + return documents +} + +func recommendBatchTestFind( + documents []recommendScoreDocument, + afterID, maxID ObjectID, + limit int, +) []recommendScoreDocument { + batch := make([]recommendScoreDocument, 0, limit) + for _, document := range documents { + if compareObjectID(document.ID, afterID) <= 0 || + compareObjectID(document.ID, maxID) > 0 { + continue + } + batch = append(batch, document) + if len(batch) == limit { + break + } + } + return batch +} + +func TestRefreshRecommendScoresBatchesAtFixedHighWater(t *testing.T) { + const ( + batchSize = 2 + cursorBatchSize = 4 + ) + initialDocuments := recommendBatchTestDocuments(5) + highWater := initialDocuments[len(initialDocuments)-1].ID + documents := append([]recommendScoreDocument(nil), initialDocuments...) + + var stateMu sync.Mutex + maxEligibleIDCalls := 0 + openCursorCalls := 0 + var openedMaxID ObjectID + var openedBatchSize int32 + var cursor *fakeRecommendScoreCursor + bulkSizes := make([]int, 0, 3) + + store := fakeRecommendScoreStore{ + maxEligibleID: func(context.Context) (ObjectID, error) { + stateMu.Lock() + defer stateMu.Unlock() + maxEligibleIDCalls++ + return highWater, nil + }, + openCursor: func( + _ context.Context, + maxID ObjectID, + serverBatchSize int32, + ) (recommendScoreCursor, error) { + stateMu.Lock() + defer stateMu.Unlock() + openCursorCalls++ + openedMaxID = maxID + openedBatchSize = serverBatchSize + // 模拟扫描开始后插入的数据;固定高水位必须把它排除在本轮之外。 + documents = append(documents, recommendScoreDocument{ + ID: recommendBatchTestID(6), + RecommendLikeCount: 6, + RecommendInitialized: true, + }) + filtered := make([]recommendScoreDocument, 0, len(documents)) + for _, document := range documents { + if compareObjectID(document.ID, maxID) <= 0 { + filtered = append(filtered, document) + } + } + cursor = &fakeRecommendScoreCursor{documents: filtered, current: -1} + return cursor, nil + }, + bulkWrite: func(_ context.Context, writes []mongo.WriteModel) error { + stateMu.Lock() + defer stateMu.Unlock() + bulkSizes = append(bulkSizes, len(writes)) + return nil + }, + } + + candidates, err := refreshRecommendScores( + context.Background(), + time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC), + batchSize, + cursorBatchSize, + 2, + store, + ) + if err != nil { + t.Fatalf("refreshRecommendScores() error = %v", err) + } + + stateMu.Lock() + gotMaxEligibleIDCalls := maxEligibleIDCalls + gotOpenCursorCalls := openCursorCalls + gotOpenedMaxID := openedMaxID + gotOpenedBatchSize := openedBatchSize + gotBulkSizes := append([]int(nil), bulkSizes...) + stateMu.Unlock() + + if gotMaxEligibleIDCalls != 1 { + t.Fatalf("MaxEligibleID calls = %d, want 1", gotMaxEligibleIDCalls) + } + if gotOpenCursorCalls != 1 { + t.Fatalf("OpenCursor calls = %d, want 1", gotOpenCursorCalls) + } + if gotOpenedMaxID != highWater { + t.Fatalf("OpenCursor maxID = %s, want fixed high water %s", gotOpenedMaxID.Hex(), highWater.Hex()) + } + if gotOpenedBatchSize != cursorBatchSize { + t.Fatalf("OpenCursor batch size = %d, want %d", gotOpenedBatchSize, cursorBatchSize) + } + if cursor == nil { + t.Fatal("OpenCursor returned a nil cursor") + } + if cursor.closeCalls.Load() != 1 { + t.Fatalf("Cursor close calls = %d, want 1", cursor.closeCalls.Load()) + } + sort.Ints(gotBulkSizes) + if got, want := gotBulkSizes, []int{1, 2, 2}; len(got) != len(want) || + got[0] != want[0] || got[1] != want[1] || got[2] != want[2] { + t.Fatalf("BulkWrite batch sizes = %v, want %v", got, want) + } + + if len(candidates) != len(initialDocuments) { + t.Fatalf("candidate count = %d, want %d", len(candidates), len(initialDocuments)) + } + seen := make(map[ObjectID]bool, len(candidates)) + for _, candidate := range candidates { + seen[candidate.ID] = true + } + for _, document := range initialDocuments { + if !seen[document.ID] { + t.Errorf("candidate %s is missing", document.ID.Hex()) + } + } + if seen[recommendBatchTestID(6)] { + t.Error("candidate inserted above the fixed high water was included") + } +} + +func TestPrepareRecommendScoreBatchWritesOnlyChangedOrUninitialized(t *testing.T) { + now := time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC) + documents := []recommendScoreDocument{ + { + ID: recommendBatchTestID(1), + LikeCount: 1, + RecommendLikeCount: 10, RecommendCollectCount: 2, + RecommendCommentCount: 3, RecommendShareCount: 4, + RecommendScore: recommendBatchTestScore(CalculateRecommendScore(10, 2, 3, 4)), + RecommendInitialized: true, + }, + { + ID: recommendBatchTestID(2), + LikeCount: 8, + RecommendLikeCount: 5, RecommendCollectCount: 1, + RecommendScore: recommendBatchTestScore(0), + RecommendInitialized: true, + }, + { + ID: recommendBatchTestID(3), + LikeCount: 7, + RecommendScore: recommendBatchTestScore(7), + RecommendInitialized: false, + }, + { + ID: recommendBatchTestID(4), + RecommendScore: recommendBatchTestScore(0), + RecommendInitialized: true, + }, + { + ID: recommendBatchTestID(5), + RecommendScore: nil, + RecommendInitialized: true, + }, + } + + candidates, writes := prepareRecommendScoreBatch(documents, now) + if len(candidates) != len(documents) { + t.Fatalf("candidate count = %d, want %d", len(candidates), len(documents)) + } + if len(writes) != 3 { + t.Fatalf("write count = %d, want 3 changed/uninitialized/missing-score documents", len(writes)) + } + wantWriteIDs := []ObjectID{documents[1].ID, documents[2].ID, documents[4].ID} + for i, write := range writes { + update, ok := write.(*mongo.UpdateOneModel) + if !ok { + t.Fatalf("write %d type = %T, want *mongo.UpdateOneModel", i, write) + } + filter, ok := update.Filter.(bson.M) + if !ok || filter["_id"] != wantWriteIDs[i] { + t.Fatalf("write %d filter = %#v, want _id %s", i, update.Filter, wantWriteIDs[i].Hex()) + } + pipeline, ok := update.Update.(mongo.Pipeline) + if !ok { + t.Fatalf("write %d update type = %T, want mongo.Pipeline", i, update.Update) + } + pipelineText := fmt.Sprint(pipeline) + for _, field := range []string{ + "$recommendLikeCount", + "$recommendCollectCount", + "$recommendCommentCount", + "$recommendShareCount", + } { + if !strings.Contains(pipelineText, field) { + t.Fatalf("write %d pipeline does not reference %s: %s", i, field, pipelineText) + } + } + } + wantScores := []int64{ + CalculateRecommendScore(10, 2, 3, 4), + CalculateRecommendScore(8, 1, 0, 0), + 7, + 0, + 0, + } + for i, candidate := range candidates { + if candidate.RecommendScore != wantScores[i] { + t.Errorf("candidate %d score = %d, want %d", i, candidate.RecommendScore, wantScores[i]) + } + } +} + +func TestRefreshRecommendScoresSkipsBulkWriteForUnchangedBatches(t *testing.T) { + documents := recommendBatchTestDocuments(5) + for i := range documents { + documents[i].RecommendScore = recommendBatchTestScore(CalculateRecommendScore( + documents[i].RecommendLikeCount, + documents[i].RecommendCollectCount, + documents[i].RecommendCommentCount, + documents[i].RecommendShareCount, + )) + } + highWater := documents[len(documents)-1].ID + var bulkCalls atomic.Int32 + store := fakeRecommendScoreStore{ + maxEligibleID: func(context.Context) (ObjectID, error) { + return highWater, nil + }, + findBatch: func( + _ context.Context, + afterID, maxID ObjectID, + limit int, + ) ([]recommendScoreDocument, error) { + return recommendBatchTestFind(documents, afterID, maxID, limit), nil + }, + bulkWrite: func(context.Context, []mongo.WriteModel) error { + bulkCalls.Add(1) + return errors.New("unchanged batch must not be written") + }, + } + + candidates, err := refreshRecommendScores( + context.Background(), + time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC), + 2, + 2, + 2, + store, + ) + if err != nil { + t.Fatalf("refreshRecommendScores() error = %v", err) + } + if len(candidates) != len(documents) { + t.Fatalf("candidate count = %d, want %d", len(candidates), len(documents)) + } + if got := bulkCalls.Load(); got != 0 { + t.Fatalf("BulkWrite calls = %d, want 0 for unchanged documents", got) + } +} + +func TestRefreshRecommendScoresInitializationGuardStopsBeforeWrites(t *testing.T) { + documents := recommendBatchTestDocuments(2) + highWater := documents[len(documents)-1].ID + var cursorCalls, bulkCalls atomic.Int32 + store := fakeRecommendScoreStore{ + maxEligibleID: func(context.Context) (ObjectID, error) { + return highWater, nil + }, + countUninitialized: func( + _ context.Context, + gotMaxID ObjectID, + limit int64, + ) (int64, error) { + if gotMaxID != highWater { + t.Fatalf("CountUninitialized maxID = %s, want %s", gotMaxID.Hex(), highWater.Hex()) + } + if limit != 11 { + t.Fatalf("CountUninitialized limit = %d, want 11", limit) + } + return 11, nil + }, + openCursor: func(context.Context, ObjectID, int32) (recommendScoreCursor, error) { + cursorCalls.Add(1) + return nil, errors.New("cursor must not open after guard rejection") + }, + bulkWrite: func(context.Context, []mongo.WriteModel) error { + bulkCalls.Add(1) + return errors.New("bulk write must not run after guard rejection") + }, + } + + candidates, err := refreshRecommendScores( + context.Background(), + time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC), + 2, + 2, + 2, + store, + 10, + ) + var limitErr RecommendInitializationLimitError + if !errors.As(err, &limitErr) { + t.Fatalf("error = %v, want RecommendInitializationLimitError", err) + } + if limitErr.Limit != 10 || limitErr.ObservedAtLeast != 11 { + t.Fatalf("limit error = %+v", limitErr) + } + if candidates != nil { + t.Fatalf("candidates = %#v, want nil", candidates) + } + if cursorCalls.Load() != 0 || bulkCalls.Load() != 0 { + t.Fatalf("cursorCalls=%d bulkCalls=%d, want zero", cursorCalls.Load(), bulkCalls.Load()) + } +} + +func TestRefreshRecommendScoresInitializationGuardAllowsExactLimit(t *testing.T) { + documents := recommendBatchTestDocuments(2) + for i := range documents { + documents[i].RecommendScore = recommendBatchTestScore(documents[i].RecommendLikeCount) + } + highWater := documents[len(documents)-1].ID + var countCalls atomic.Int32 + store := fakeRecommendScoreStore{ + maxEligibleID: func(context.Context) (ObjectID, error) { + return highWater, nil + }, + countUninitialized: func(context.Context, ObjectID, int64) (int64, error) { + countCalls.Add(1) + return 10, nil + }, + findBatch: func( + _ context.Context, + afterID, maxID ObjectID, + limit int, + ) ([]recommendScoreDocument, error) { + return recommendBatchTestFind(documents, afterID, maxID, limit), nil + }, + bulkWrite: func(context.Context, []mongo.WriteModel) error { + return errors.New("unchanged documents must not be written") + }, + } + + candidates, err := refreshRecommendScores( + context.Background(), + time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC), + 2, + 2, + 2, + store, + 10, + ) + if err != nil { + t.Fatalf("refreshRecommendScores() error = %v", err) + } + if countCalls.Load() != 1 || len(candidates) != len(documents) { + t.Fatalf("countCalls=%d candidateCount=%d", countCalls.Load(), len(candidates)) + } +} + +func TestRefreshRecommendScoresWritesOnlyChangedMixedBatch(t *testing.T) { + documents := recommendBatchTestDocuments(4) + for i := range documents { + documents[i].RecommendScore = recommendBatchTestScore(documents[i].RecommendLikeCount) + } + documents[2].RecommendScore = recommendBatchTestScore(0) + highWater := documents[len(documents)-1].ID + var stateMu sync.Mutex + var bulkSizes []int + store := fakeRecommendScoreStore{ + maxEligibleID: func(context.Context) (ObjectID, error) { + return highWater, nil + }, + findBatch: func( + _ context.Context, + afterID, maxID ObjectID, + limit int, + ) ([]recommendScoreDocument, error) { + return recommendBatchTestFind(documents, afterID, maxID, limit), nil + }, + bulkWrite: func(_ context.Context, writes []mongo.WriteModel) error { + stateMu.Lock() + defer stateMu.Unlock() + bulkSizes = append(bulkSizes, len(writes)) + return nil + }, + } + + candidates, err := refreshRecommendScores( + context.Background(), + time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC), + 2, + 2, + 2, + store, + ) + if err != nil { + t.Fatalf("refreshRecommendScores() error = %v", err) + } + if len(candidates) != len(documents) { + t.Fatalf("candidate count = %d, want %d", len(candidates), len(documents)) + } + stateMu.Lock() + gotBulkSizes := append([]int(nil), bulkSizes...) + stateMu.Unlock() + if len(gotBulkSizes) != 1 || gotBulkSizes[0] != 1 { + t.Fatalf("BulkWrite batch sizes = %v, want [1]", gotBulkSizes) + } +} + +func TestRefreshRecommendScoresBoundsAndUsesConcurrency(t *testing.T) { + const workerCount = 3 + documents := recommendBatchTestDocuments(8) + highWater := documents[len(documents)-1].ID + release := make(chan struct{}) + started := make(chan struct{}, len(documents)) + + var stateMu sync.Mutex + active := 0 + maxActive := 0 + bulkCalls := 0 + store := fakeRecommendScoreStore{ + maxEligibleID: func(context.Context) (ObjectID, error) { + return highWater, nil + }, + findBatch: func( + _ context.Context, + afterID, maxID ObjectID, + limit int, + ) ([]recommendScoreDocument, error) { + return recommendBatchTestFind(documents, afterID, maxID, limit), nil + }, + bulkWrite: func(ctx context.Context, _ []mongo.WriteModel) error { + stateMu.Lock() + active++ + bulkCalls++ + if active > maxActive { + maxActive = active + } + stateMu.Unlock() + defer func() { + stateMu.Lock() + active-- + stateMu.Unlock() + }() + + started <- struct{}{} + select { + case <-release: + return nil + case <-ctx.Done(): + return ctx.Err() + } + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + result := make(chan refreshRecommendScoresResult, 1) + go func() { + candidates, err := refreshRecommendScores( + ctx, + time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC), + 1, + 1, + workerCount, + store, + ) + result <- refreshRecommendScoresResult{candidates: candidates, err: err} + }() + + for i := 0; i < workerCount; i++ { + select { + case <-started: + case <-ctx.Done(): + t.Fatalf("only %d/%d workers reached the barrier: %v", i, workerCount, ctx.Err()) + } + } + stateMu.Lock() + gotMaxActiveAtBarrier := maxActive + stateMu.Unlock() + if gotMaxActiveAtBarrier != workerCount { + t.Fatalf("active BulkWrite calls at barrier = %d, want %d", gotMaxActiveAtBarrier, workerCount) + } + close(release) + + var got refreshRecommendScoresResult + select { + case got = <-result: + case <-ctx.Done(): + t.Fatalf("refreshRecommendScores() did not finish: %v", ctx.Err()) + } + if got.err != nil { + t.Fatalf("refreshRecommendScores() error = %v", got.err) + } + if len(got.candidates) != len(documents) { + t.Fatalf("candidate count = %d, want %d", len(got.candidates), len(documents)) + } + + stateMu.Lock() + gotActive := active + gotMaxActive := maxActive + gotBulkCalls := bulkCalls + stateMu.Unlock() + if gotActive != 0 { + t.Errorf("active BulkWrite calls after return = %d, want 0", gotActive) + } + if gotMaxActive != workerCount { + t.Errorf("maximum concurrent BulkWrite calls = %d, want %d", gotMaxActive, workerCount) + } + if gotBulkCalls != len(documents) { + t.Errorf("BulkWrite calls = %d, want %d", gotBulkCalls, len(documents)) + } +} + +func TestRefreshRecommendScoresFirstErrorCancelsWorkers(t *testing.T) { + const workerCount = 2 + writeErr := errors.New("bulk write failed") + documents := recommendBatchTestDocuments(8) + highWater := documents[len(documents)-1].ID + started := make(chan int32, workerCount) + releaseFailure := make(chan struct{}) + cancelObserved := make(chan struct{}) + var cancelObservedOnce sync.Once + var releaseOnce sync.Once + release := func() { + releaseOnce.Do(func() { + close(releaseFailure) + }) + } + defer release() + + var bulkCalls atomic.Int32 + store := fakeRecommendScoreStore{ + maxEligibleID: func(context.Context) (ObjectID, error) { + return highWater, nil + }, + findBatch: func( + _ context.Context, + afterID, maxID ObjectID, + limit int, + ) ([]recommendScoreDocument, error) { + return recommendBatchTestFind(documents, afterID, maxID, limit), nil + }, + bulkWrite: func(ctx context.Context, _ []mongo.WriteModel) error { + call := bulkCalls.Add(1) + started <- call + if call == 1 { + select { + case <-releaseFailure: + return writeErr + case <-ctx.Done(): + return ctx.Err() + } + } + <-ctx.Done() + cancelObservedOnce.Do(func() { + close(cancelObserved) + }) + return ctx.Err() + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + result := make(chan refreshRecommendScoresResult, 1) + go func() { + candidates, err := refreshRecommendScores( + ctx, + time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC), + 1, + 1, + workerCount, + store, + ) + result <- refreshRecommendScoresResult{candidates: candidates, err: err} + }() + + for i := 0; i < workerCount; i++ { + select { + case <-started: + case <-ctx.Done(): + t.Fatalf("only %d/%d writes reached the barrier: %v", i, workerCount, ctx.Err()) + } + } + release() + + var got refreshRecommendScoresResult + select { + case got = <-result: + case <-ctx.Done(): + t.Fatalf("refreshRecommendScores() did not finish after first error: %v", ctx.Err()) + } + if !errors.Is(got.err, writeErr) { + t.Fatalf("refreshRecommendScores() error = %v, want %v", got.err, writeErr) + } + if got.candidates != nil { + t.Fatalf("candidates = %v, want nil on write error", got.candidates) + } + select { + case <-cancelObserved: + default: + t.Fatal("peer worker did not observe cancellation from the first error") + } + if gotCalls := bulkCalls.Load(); gotCalls != workerCount { + t.Fatalf("BulkWrite calls = %d, want %d blocked workers only", gotCalls, workerCount) + } +} + +func TestRefreshRecommendScoresEmptyHighWaterSkipsWork(t *testing.T) { + var findCalls atomic.Int32 + var bulkCalls atomic.Int32 + store := fakeRecommendScoreStore{ + maxEligibleID: func(context.Context) (ObjectID, error) { + return ObjectID{}, nil + }, + findBatch: func( + context.Context, + ObjectID, ObjectID, + int, + ) ([]recommendScoreDocument, error) { + findCalls.Add(1) + return nil, nil + }, + bulkWrite: func(context.Context, []mongo.WriteModel) error { + bulkCalls.Add(1) + return nil + }, + } + + candidates, err := refreshRecommendScores( + context.Background(), + time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC), + 2, + 2, + 2, + store, + ) + if err != nil { + t.Fatalf("refreshRecommendScores() error = %v", err) + } + if candidates == nil || len(candidates) != 0 { + t.Fatalf("candidates = %#v, want non-nil empty slice", candidates) + } + if got := findCalls.Load(); got != 0 { + t.Errorf("FindBatch calls = %d, want 0", got) + } + if got := bulkCalls.Load(); got != 0 { + t.Errorf("BulkWrite calls = %d, want 0", got) + } +} + +func TestRefreshRecommendScoresPropagatesContextCancellation(t *testing.T) { + documents := recommendBatchTestDocuments(2) + highWater := documents[len(documents)-1].ID + writeStarted := make(chan struct{}, 1) + cancelObserved := make(chan struct{}) + store := fakeRecommendScoreStore{ + maxEligibleID: func(context.Context) (ObjectID, error) { + return highWater, nil + }, + findBatch: func( + _ context.Context, + afterID, maxID ObjectID, + limit int, + ) ([]recommendScoreDocument, error) { + return recommendBatchTestFind(documents, afterID, maxID, limit), nil + }, + bulkWrite: func(ctx context.Context, _ []mongo.WriteModel) error { + writeStarted <- struct{}{} + <-ctx.Done() + close(cancelObserved) + return ctx.Err() + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + guard, stopGuard := context.WithTimeout(context.Background(), 5*time.Second) + defer stopGuard() + result := make(chan refreshRecommendScoresResult, 1) + go func() { + candidates, err := refreshRecommendScores( + ctx, + time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC), + 1, + 1, + 1, + store, + ) + result <- refreshRecommendScoresResult{candidates: candidates, err: err} + }() + + select { + case <-writeStarted: + case <-guard.Done(): + t.Fatalf("BulkWrite did not start: %v", guard.Err()) + } + cancel() + + var got refreshRecommendScoresResult + select { + case got = <-result: + case <-guard.Done(): + t.Fatalf("refreshRecommendScores() did not stop after cancellation: %v", guard.Err()) + } + if !errors.Is(got.err, context.Canceled) { + t.Fatalf("refreshRecommendScores() error = %v, want %v", got.err, context.Canceled) + } + if got.candidates != nil { + t.Fatalf("candidates = %v, want nil after cancellation", got.candidates) + } + select { + case <-cancelObserved: + default: + t.Fatal("BulkWrite did not observe context cancellation") + } +} + +func TestRecommendScoreCursorQueryUsesBoundedIndexedScan(t *testing.T) { + maxID := recommendBatchTestID(9) + generatedAt := time.Date(2026, 7, 31, 8, 0, 0, 0, time.UTC) + scope := newRecommendScoreScope(generatedAt, []string{" blocked ", "", "blocked", "other"}) + filter, opts := recommendScoreCursorQuery(maxID, 4000, scope) + + if filter["status"] != CheckPass || filter["newsType"] != SHORT || filter["deleteAt"] != nil { + t.Fatalf("filter = %#v, want eligible short-video filter", filter) + } + if !reflect.DeepEqual(filter["recoWeight"], bson.M{"$ne": -1}) { + t.Fatalf("recoWeight filter = %#v, want explicit non-recommend exclusion", filter["recoWeight"]) + } + reviewAt, ok := filter["reviewAt"].(bson.M) + if !ok || !reflect.DeepEqual(reviewAt, bson.M{"$lte": generatedAt}) { + t.Fatalf("reviewAt filter = %#v, want <= %s", filter["reviewAt"], generatedAt) + } + moduleIDs, ok := filter["mId"].(bson.M) + if !ok || !reflect.DeepEqual(moduleIDs["$nin"], []string{"blocked", "other"}) { + t.Fatalf("mId filter = %#v, want normalized excluded modules", filter["mId"]) + } + idRange, ok := filter["_id"].(bson.M) + if !ok || idRange["$lte"] != maxID || len(idRange) != 1 { + t.Fatalf("_id filter = %#v, want $lte %s", filter["_id"], maxID.Hex()) + } + if opts.BatchSize == nil || *opts.BatchSize != 4000 { + t.Fatalf("cursor batch size = %v, want 4000", opts.BatchSize) + } + if opts.Limit != nil { + t.Fatalf("cursor limit = %v, want nil for one streaming cursor", *opts.Limit) + } + if opts.Hint != shortRecommendRefreshIndexName { + t.Fatalf("cursor hint = %#v, want %s", opts.Hint, shortRecommendRefreshIndexName) + } + wantSort := bson.D{{Key: "_id", Value: 1}} + if !reflect.DeepEqual(opts.Sort, wantSort) { + t.Fatalf("cursor sort = %#v, want %#v", opts.Sort, wantSort) + } + projection, ok := opts.Projection.(bson.M) + if !ok { + t.Fatalf("cursor projection type = %T, want bson.M", opts.Projection) + } + for _, field := range []string{ + "_id", "mId", "reviewAt", "likeCount", "collectCount", "commentCount", "shareCount", + "recommendLikeCount", "recommendCollectCount", "recommendCommentCount", + "recommendShareCount", "recommendScore", "recommendInitialized", + } { + if projection[field] != 1 { + t.Fatalf("projection[%q] = %#v, want 1", field, projection[field]) + } + } +} + +func TestRecommendScoreMaxEligibleIDQueryUsesSameEligibilityScope(t *testing.T) { + generatedAt := time.Date(2026, 7, 31, 8, 0, 0, 0, time.UTC) + scope := newRecommendScoreScope(generatedAt, []string{"blocked"}) + filter, opts := recommendScoreMaxEligibleIDQuery(scope) + if filter["status"] != CheckPass || filter["newsType"] != SHORT || filter["deleteAt"] != nil { + t.Fatalf("filter = %#v, want eligible short-video filter", filter) + } + if !reflect.DeepEqual(filter["recoWeight"], bson.M{"$ne": -1}) { + t.Fatalf("recoWeight filter = %#v, want explicit non-recommend exclusion", filter["recoWeight"]) + } + if !reflect.DeepEqual(filter["reviewAt"], bson.M{"$lte": generatedAt}) { + t.Fatalf("reviewAt filter = %#v, want <= %s", filter["reviewAt"], generatedAt) + } + if !reflect.DeepEqual(filter["mId"], bson.M{"$nin": []string{"blocked"}}) { + t.Fatalf("mId filter = %#v, want blocked module", filter["mId"]) + } + if opts.Hint != shortRecommendRefreshIndexName { + t.Fatalf("hint = %#v, want %s", opts.Hint, shortRecommendRefreshIndexName) + } + if !reflect.DeepEqual(opts.Sort, bson.D{{Key: "_id", Value: -1}}) { + t.Fatalf("sort = %#v, want descending _id", opts.Sort) + } +} + +func TestRecommendScoreMaxEligibleIDResultTreatsEmptyCollectionAsEmptyQueue(t *testing.T) { + id, err := recommendScoreMaxEligibleIDResult( + recommendBatchTestID(9), + fmt.Errorf("wrapped: %w", mongo.ErrNoDocuments), + ) + if err != nil { + t.Fatalf("recommendScoreMaxEligibleIDResult() error = %v", err) + } + if !id.IsZero() { + t.Fatalf("id = %s, want zero ObjectID", id.Hex()) + } + + wantErr := errors.New("find failed") + id, err = recommendScoreMaxEligibleIDResult(recommendBatchTestID(9), wantErr) + if !errors.Is(err, wantErr) || !id.IsZero() { + t.Fatalf("id = %s, error = %v, want zero id and %v", id.Hex(), err, wantErr) + } +} + +func TestRecommendInitializationCountQueryUsesSameBoundedIndex(t *testing.T) { + maxID := recommendBatchTestID(9) + generatedAt := time.Date(2026, 7, 31, 8, 0, 0, 0, time.UTC) + scope := newRecommendScoreScope(generatedAt, []string{"blocked"}) + filter, opts := recommendInitializationCountQuery(maxID, 50_001, scope) + if filter["status"] != CheckPass || filter["newsType"] != SHORT || filter["deleteAt"] != nil { + t.Fatalf("filter = %#v, want eligible short-video filter", filter) + } + if !reflect.DeepEqual(filter["recoWeight"], bson.M{"$ne": -1}) { + t.Fatalf("recoWeight filter = %#v, want explicit non-recommend exclusion", filter["recoWeight"]) + } + reviewAt, ok := filter["reviewAt"].(bson.M) + if !ok || !reflect.DeepEqual(reviewAt, bson.M{"$lte": generatedAt}) { + t.Fatalf("reviewAt filter = %#v, want <= %s", filter["reviewAt"], generatedAt) + } + moduleIDs, ok := filter["mId"].(bson.M) + if !ok || !reflect.DeepEqual(moduleIDs["$nin"], []string{"blocked"}) { + t.Fatalf("mId filter = %#v, want blocked module", filter["mId"]) + } + idRange, ok := filter["_id"].(bson.M) + if !ok || idRange["$lte"] != maxID { + t.Fatalf("_id filter = %#v", filter["_id"]) + } + initialized, ok := filter["recommendInitialized"].(bson.M) + if !ok || initialized["$ne"] != true { + t.Fatalf("recommendInitialized filter = %#v", filter["recommendInitialized"]) + } + if opts.Limit == nil || *opts.Limit != 50_001 { + t.Fatalf("limit = %v, want 50001", opts.Limit) + } + if opts.Hint != shortRecommendRefreshIndexName { + t.Fatalf("hint = %#v, want %s", opts.Hint, shortRecommendRefreshIndexName) + } +} + +func TestRecommendVideosByIDsFilterRevalidatesEligibility(t *testing.T) { + ids := []ObjectID{recommendBatchTestID(1), recommendBatchTestID(2)} + filter := recommendVideosByIDsFilter(ids) + idMatch, ok := filter["_id"].(bson.M) + if !ok || !reflect.DeepEqual(idMatch["$in"], ids) { + t.Fatalf("_id filter = %#v", filter["_id"]) + } + if filter["status"] != CheckPass || + filter["newsType"] != SHORT || + filter["deleteAt"] != nil || + !reflect.DeepEqual(filter["recoWeight"], bson.M{"$ne": -1}) { + t.Fatalf("filter = %#v, want approved non-deleted short video", filter) + } +} + +func TestGetRecommendVideosByIDsContextEmptyInputDoesNotAccessMongo(t *testing.T) { + originalDB := mdb + mdb = nil + t.Cleanup(func() { mdb = originalDB }) + videos, err := GetRecommendVideosByIDsContext(context.Background(), nil) + if err != nil { + t.Fatalf("GetRecommendVideosByIDsContext() error = %v", err) + } + if len(videos) != 0 { + t.Fatalf("videos = %#v, want empty", videos) + } +} + +func TestStreamRecommendScoreBatchesOwnsSlicesAndClosesCursor(t *testing.T) { + documents := recommendBatchTestDocuments(5) + maxID := documents[len(documents)-1].ID + cursor := &fakeRecommendScoreCursor{documents: documents, current: -1} + openCalls := 0 + store := fakeRecommendScoreStore{ + maxEligibleID: func(context.Context) (ObjectID, error) { + return maxID, nil + }, + openCursor: func(_ context.Context, gotMaxID ObjectID, gotBatchSize int32) (recommendScoreCursor, error) { + openCalls++ + if gotMaxID != maxID { + t.Fatalf("OpenCursor maxID = %s, want %s", gotMaxID.Hex(), maxID.Hex()) + } + if gotBatchSize != 4 { + t.Fatalf("OpenCursor batch size = %d, want 4", gotBatchSize) + } + return cursor, nil + }, + bulkWrite: func(context.Context, []mongo.WriteModel) error { + return nil + }, + } + + var batches [][]recommendScoreDocument + err := streamRecommendScoreBatches( + context.Background(), + maxID, + 2, + 4, + store, + func(batch []recommendScoreDocument) error { + batches = append(batches, batch) + return nil + }, + ) + if err != nil { + t.Fatalf("streamRecommendScoreBatches() error = %v", err) + } + if openCalls != 1 { + t.Fatalf("OpenCursor calls = %d, want 1", openCalls) + } + if got, want := cursor.closeCalls.Load(), int32(1); got != want { + t.Fatalf("Cursor close calls = %d, want %d", got, want) + } + if cursor.closeContextCanceled.Load() { + t.Fatal("Cursor Close received an already-canceled cleanup context") + } + wantBatchSizes := []int{2, 2, 1} + if len(batches) != len(wantBatchSizes) { + t.Fatalf("batch count = %d, want %d", len(batches), len(wantBatchSizes)) + } + gotIDs := make([]ObjectID, 0, len(documents)) + for i, batch := range batches { + if len(batch) != wantBatchSizes[i] { + t.Errorf("batch %d size = %d, want %d", i, len(batch), wantBatchSizes[i]) + } + for _, document := range batch { + gotIDs = append(gotIDs, document.ID) + } + } + for i := range documents { + if gotIDs[i] != documents[i].ID { + t.Fatalf("streamed ID %d = %s, want %s", i, gotIDs[i].Hex(), documents[i].ID.Hex()) + } + } +} + +func TestStreamRecommendScoreBatchesErrorLifecycle(t *testing.T) { + openErr := errors.New("open cursor failed") + decodeErr := errors.New("decode failed") + cursorErr := errors.New("cursor failed") + closeErr := errors.New("close failed") + + tests := []struct { + name string + open func() (recommendScoreCursor, error) + wantErr error + wantClose int32 + wantYieldCall int + }{ + { + name: "open error", + open: func() (recommendScoreCursor, error) { + return nil, openErr + }, + wantErr: openErr, + }, + { + name: "nil cursor", + open: func() (recommendScoreCursor, error) { + return nil, nil + }, + wantErr: errors.New("recommend score cursor is nil"), + }, + { + name: "decode error wins over close error", + open: func() (recommendScoreCursor, error) { + return &fakeRecommendScoreCursor{ + documents: recommendBatchTestDocuments(2), + current: -1, + decodeErrAt: 0, + decodeErr: decodeErr, + closeErr: closeErr, + }, nil + }, + wantErr: decodeErr, + wantClose: 1, + }, + { + name: "cursor error prevents partial tail", + open: func() (recommendScoreCursor, error) { + return &fakeRecommendScoreCursor{ + documents: recommendBatchTestDocuments(1), + current: -1, + cursorErr: cursorErr, + }, nil + }, + wantErr: cursorErr, + wantClose: 1, + }, + { + name: "close error", + open: func() (recommendScoreCursor, error) { + return &fakeRecommendScoreCursor{ + current: -1, + closeErr: closeErr, + }, nil + }, + wantErr: closeErr, + wantClose: 1, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var opened recommendScoreCursor + store := fakeRecommendScoreStore{ + maxEligibleID: func(context.Context) (ObjectID, error) { + return recommendBatchTestID(2), nil + }, + openCursor: func(context.Context, ObjectID, int32) (recommendScoreCursor, error) { + cursor, err := test.open() + opened = cursor + return cursor, err + }, + bulkWrite: func(context.Context, []mongo.WriteModel) error { + return nil + }, + } + yieldCalls := 0 + err := streamRecommendScoreBatches( + context.Background(), + recommendBatchTestID(2), + 2, + 4, + store, + func([]recommendScoreDocument) error { + yieldCalls++ + return nil + }, + ) + if test.name == "nil cursor" { + if err == nil || !strings.Contains(err.Error(), test.wantErr.Error()) { + t.Fatalf("error = %v, want containing %q", err, test.wantErr) + } + } else if !errors.Is(err, test.wantErr) { + t.Fatalf("error = %v, want %v", err, test.wantErr) + } + if yieldCalls != test.wantYieldCall { + t.Fatalf("yield calls = %d, want %d", yieldCalls, test.wantYieldCall) + } + if cursor, ok := opened.(*fakeRecommendScoreCursor); ok { + if got := cursor.closeCalls.Load(); got != test.wantClose { + t.Fatalf("Cursor close calls = %d, want %d", got, test.wantClose) + } + } + }) + } +} + +func TestStreamRecommendScoreBatchesCancellationUsesCleanupContext(t *testing.T) { + documents := recommendBatchTestDocuments(2) + cursor := &fakeRecommendScoreCursor{documents: documents, current: -1} + store := fakeRecommendScoreStore{ + maxEligibleID: func(context.Context) (ObjectID, error) { + return documents[len(documents)-1].ID, nil + }, + openCursor: func(context.Context, ObjectID, int32) (recommendScoreCursor, error) { + return cursor, nil + }, + bulkWrite: func(context.Context, []mongo.WriteModel) error { + return nil + }, + } + ctx, cancel := context.WithCancel(context.Background()) + err := streamRecommendScoreBatches( + ctx, + documents[len(documents)-1].ID, + 1, + 2, + store, + func([]recommendScoreDocument) error { + cancel() + return context.Canceled + }, + ) + if !errors.Is(err, context.Canceled) { + t.Fatalf("error = %v, want %v", err, context.Canceled) + } + if got := cursor.closeCalls.Load(); got != 1 { + t.Fatalf("Cursor close calls = %d, want 1", got) + } + if cursor.closeContextCanceled.Load() { + t.Fatal("Cursor Close received the canceled work context") + } +} + +func TestStreamRecommendScoreBatchesRejectsInvalidIDSequence(t *testing.T) { + id1 := recommendBatchTestID(1) + id2 := recommendBatchTestID(2) + id3 := recommendBatchTestID(3) + tests := []struct { + name string + documents []recommendScoreDocument + maxID ObjectID + }{ + { + name: "zero ID", + documents: []recommendScoreDocument{{}}, + maxID: id2, + }, + { + name: "duplicate across batch boundary", + documents: []recommendScoreDocument{{ID: id1}, {ID: id1}}, + maxID: id2, + }, + { + name: "descending across batch boundary", + documents: []recommendScoreDocument{{ID: id2}, {ID: id1}}, + maxID: id2, + }, + { + name: "above fixed high water", + documents: []recommendScoreDocument{{ID: id1}, {ID: id3}}, + maxID: id2, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cursor := &fakeRecommendScoreCursor{documents: test.documents, current: -1} + store := fakeRecommendScoreStore{ + maxEligibleID: func(context.Context) (ObjectID, error) { + return test.maxID, nil + }, + openCursor: func(context.Context, ObjectID, int32) (recommendScoreCursor, error) { + return cursor, nil + }, + bulkWrite: func(context.Context, []mongo.WriteModel) error { + return nil + }, + } + err := streamRecommendScoreBatches( + context.Background(), + test.maxID, + 1, + 2, + store, + func([]recommendScoreDocument) error { return nil }, + ) + if err == nil || !strings.Contains(err.Error(), "invalid _id sequence") { + t.Fatalf("error = %v, want invalid _id sequence", err) + } + if got := cursor.closeCalls.Load(); got != 1 { + t.Fatalf("Cursor close calls = %d, want 1", got) + } + }) + } +} + +func TestRefreshRecommendScoresNonZeroHighWaterEmptyCursor(t *testing.T) { + highWater := recommendBatchTestID(1) + cursor := &fakeRecommendScoreCursor{current: -1} + var bulkCalls atomic.Int32 + store := fakeRecommendScoreStore{ + maxEligibleID: func(context.Context) (ObjectID, error) { + return highWater, nil + }, + openCursor: func(context.Context, ObjectID, int32) (recommendScoreCursor, error) { + return cursor, nil + }, + bulkWrite: func(context.Context, []mongo.WriteModel) error { + bulkCalls.Add(1) + return nil + }, + } + + candidates, err := refreshRecommendScores( + context.Background(), + time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC), + 2, + 4, + 2, + store, + ) + if err != nil { + t.Fatalf("refreshRecommendScores() error = %v", err) + } + if candidates == nil || len(candidates) != 0 { + t.Fatalf("candidates = %#v, want non-nil empty slice", candidates) + } + if got := cursor.closeCalls.Load(); got != 1 { + t.Fatalf("Cursor close calls = %d, want 1", got) + } + if got := bulkCalls.Load(); got != 0 { + t.Fatalf("BulkWrite calls = %d, want 0", got) + } +} + +func TestRefreshRecommendScoresRejectsInvalidLimitsBeforeDatabaseAccess(t *testing.T) { + tests := []struct { + name string + batchSize int + cursorBatchSize int32 + workerCount int + wantMessage string + }{ + { + name: "process batch", + batchSize: 0, + cursorBatchSize: 4, + workerCount: 2, + wantMessage: "batch size must be positive", + }, + { + name: "cursor batch", + batchSize: 2, + cursorBatchSize: 0, + workerCount: 2, + wantMessage: "cursor batch size must be positive", + }, + { + name: "worker count", + batchSize: 2, + cursorBatchSize: 4, + workerCount: 0, + wantMessage: "worker count must be positive", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var databaseCalls atomic.Int32 + store := fakeRecommendScoreStore{ + maxEligibleID: func(context.Context) (ObjectID, error) { + databaseCalls.Add(1) + return recommendBatchTestID(1), nil + }, + openCursor: func(context.Context, ObjectID, int32) (recommendScoreCursor, error) { + databaseCalls.Add(1) + return nil, errors.New("must not open cursor") + }, + bulkWrite: func(context.Context, []mongo.WriteModel) error { + databaseCalls.Add(1) + return nil + }, + } + candidates, err := refreshRecommendScores( + context.Background(), + time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC), + test.batchSize, + test.cursorBatchSize, + test.workerCount, + store, + ) + if err == nil || !strings.Contains(err.Error(), test.wantMessage) { + t.Fatalf("error = %v, want containing %q", err, test.wantMessage) + } + if candidates != nil { + t.Fatalf("candidates = %#v, want nil", candidates) + } + if got := databaseCalls.Load(); got != 0 { + t.Fatalf("database calls = %d, want 0", got) + } + }) + } +} + +func TestRefreshRecommendScoresPropagatesSourceErrors(t *testing.T) { + maxErr := errors.New("max ID failed") + openErr := errors.New("open cursor failed") + tests := []struct { + name string + maxID func(context.Context) (ObjectID, error) + openCursor func(context.Context, ObjectID, int32) (recommendScoreCursor, error) + wantErr error + wantOpens int32 + }{ + { + name: "max ID", + maxID: func(context.Context) (ObjectID, error) { + return ObjectID{}, maxErr + }, + openCursor: func(context.Context, ObjectID, int32) (recommendScoreCursor, error) { + return nil, errors.New("must not open cursor") + }, + wantErr: maxErr, + }, + { + name: "open cursor", + maxID: func(context.Context) (ObjectID, error) { + return recommendBatchTestID(1), nil + }, + openCursor: func(context.Context, ObjectID, int32) (recommendScoreCursor, error) { + return nil, openErr + }, + wantErr: openErr, + wantOpens: 1, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var openCalls atomic.Int32 + store := fakeRecommendScoreStore{ + maxEligibleID: test.maxID, + openCursor: func(ctx context.Context, maxID ObjectID, batchSize int32) (recommendScoreCursor, error) { + openCalls.Add(1) + return test.openCursor(ctx, maxID, batchSize) + }, + bulkWrite: func(context.Context, []mongo.WriteModel) error { + return errors.New("must not write") + }, + } + candidates, err := refreshRecommendScores( + context.Background(), + time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC), + 2, + 4, + 2, + store, + ) + if !errors.Is(err, test.wantErr) { + t.Fatalf("error = %v, want %v", err, test.wantErr) + } + if candidates != nil { + t.Fatalf("candidates = %#v, want nil", candidates) + } + if got := openCalls.Load(); got != test.wantOpens { + t.Fatalf("OpenCursor calls = %d, want %d", got, test.wantOpens) + } + }) + } +} diff --git a/models/v/vidmod/recommend_test.go b/models/v/vidmod/recommend_test.go new file mode 100644 index 0000000..9e94d51 --- /dev/null +++ b/models/v/vidmod/recommend_test.go @@ -0,0 +1,24 @@ +package vidmod + +import "testing" + +func TestCalculateRecommendScore(t *testing.T) { + if got, want := CalculateRecommendScore(1, 2, 3, 4), int64(34); got != want { + t.Fatalf("score=%d want=%d", got, want) + } + if got := CalculateRecommendScore(0, 0, 0, 0); got != 0 { + t.Fatalf("zero score=%d", got) + } +} + +func TestNonNegativeHistoryValue(t *testing.T) { + if got := nonNegativeInt(-1); got != 0 { + t.Fatalf("negative history=%d want=0", got) + } + if got := maxInt64(8, nonNegativeInt(3)); got != 8 { + t.Fatalf("existing monotonic count overwritten: %d", got) + } + if got := maxInt64(2, nonNegativeInt(3)); got != 3 { + t.Fatalf("history initialization=%d want=3", got) + } +} diff --git a/models/v/vidmod/setDoc.go b/models/v/vidmod/setDoc.go new file mode 100644 index 0000000..f4f4bc1 --- /dev/null +++ b/models/v/vidmod/setDoc.go @@ -0,0 +1,24 @@ +package vidmod + +import "time" + +type SetDoc struct { + PublisherID *uint64 `json:"publisherID" bson:"publisherID,omitempty"` //上传者ID + Status *int `json:"status" bson:"status,omitempty"` //状态,0 未审核 1通过 2审核失败 3已删除-逻辑删除 默认为0 + DeleteAt *time.Time `json:"deleteAt" bson:"deleteAt,omitempty"` //删除时间,不能去除omitempty +} + +func (u *SetDoc) SetPublisherID(publisherID uint64) *SetDoc { + u.PublisherID = &publisherID + return u +} + +func (u *SetDoc) SetStatus(status int) *SetDoc { + u.Status = &status + return u +} + +func (u *SetDoc) SetDeleteAt(deleteAt time.Time) *SetDoc { + u.DeleteAt = &deleteAt + return u +} diff --git a/models/v/vidmod/type.go b/models/v/vidmod/type.go new file mode 100644 index 0000000..d678c3a --- /dev/null +++ b/models/v/vidmod/type.go @@ -0,0 +1,307 @@ +package vidmod + +import ( + "91porn-server/common/constant" + "time" + + "91porn-server/common/db" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +// PretendAccInitCoins 金币初始为1000时为马甲账号 +var PretendAccInitCoins int64 = 1000 + +// PretendAccDefaultCoins 马甲账号默认金币数 +var PretendAccDefaultCoins int64 = 10 + +const ( +// SP string = "SP" // 短视频帖子类型 +// COVER string = "COVER" // 图文帖子 +// PIC string = "PIC" // 图集帖子 +// AD_COVER string = "AD_COVER" // 图片广告 +// AD_SP string = "AD_SP" // 视频广告 +// SEED_LINK string = "SEED_LINK" // 种子链接帖子 +) + +const ( + SP = constant.SP // 长视频帖子 + SHORT = constant.SHORT // 短视频帖子 + COVER = constant.COVER // 图文帖子 + PIC = constant.PIC // 图集帖子 + AD_COVER = constant.AD_COVER // 图片广告 + AD_SP = constant.AD_SP // 视频广告 + SEED_LINK = constant.SEED_LINK // 种子链接帖子 +) + +const ( + // 影片质量 + High string = "high" //影片质量高 720P以上 + Middle string = "middle" //影片质量高 480P-720P以上 + Low string = "low" //影片质量高 480P以下 + + // 影片板式 + Vertical string = "vertical" //竖屏 + Horizontal string = "horizontal" //横屏 + Square string = "square" //方形屏 +) + +// H265Status 表示视频 H.265 异步转码状态。 +// 状态流转:None/Failed -> Queued -> Pending -> Success/Failed。 +type H265Status int + +const ( + H265StatusFailed H265Status = -1 // 转码失败,未超过失败次数上限时可重新入队 + H265StatusNone H265Status = 0 // 未加入转码队列 + H265StatusQueued H265Status = 1 // 已进入本地等待队列 + H265StatusPending H265Status = 2 // 已提交云端,等待轮询结果 + H265StatusSuccess H265Status = 3 // 转码成功,h265Url 可用 + + H265MaxFailCount = 3 +) + +// MinFreeTime 付费视频最低免费时长 +const MinFreeTime = 3 + +type M = bson.M + +type A = bson.A + +type D = bson.D + +const ( + WaitingCheck = 0 // 帖子未审核 + CheckPass = 1 // 帖子通过 + CheckFailure = 2 // 帖子审核失败 + Free = 3 // 帖子视为免费 老版本才有 该状态已经废弃 + IsDeleted = 4 // 帖子逻辑删除 + OffShelf = 5 // 帖子下架 + OnlinePass = 6 // 帖子定时上架 + + // 免费观看时长 + free10 = 10 + free60 = 60 + free120 = 120 + + second30 = 30 + minite10 = 10 * 60 + minite30 = 30 * 60 + minite60 = 60 * 60 +) + +// VideoModel 视频 +type VideoModel struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + NewsType string `json:"newsType" bson:"newsType"` // 帖子类型, SP,视频帖子,COVER + PublisherID uint64 `json:"publisherID" bson:"publisherID"` // 上传者ID + Title string `json:"title" bson:"title"` // 视频标题 + Content string `json:"content" bson:"content"` // 视频内容 + Tags []primitive.ObjectID `json:"tags" bson:"tags"` // 视频标签 + Activity []primitive.ObjectID `json:"activity" bson:"activity"` // 活动ID + SourceID string `json:"sourceID" bson:"sourceID"` // 视频在仓库中的资源ID + SourceURL string `json:"sourceURL" bson:"sourceURL"` // 视频资源地址Path + H265Url string `json:"h265Url" bson:"h265Url"` // H.265 视频资源地址 + H265Status H265Status `json:"h265Status" bson:"h265Status,omitempty"` // H.265 异步转码状态 + H265FailCount int `json:"h265FailCount" bson:"h265FailCount,omitempty"` // H.265 转码失败次数 + H265QueuedAt time.Time `json:"h265QueuedAt" bson:"h265QueuedAt,omitempty"` // H.265 入队时间 + H265PendingAt time.Time `json:"h265PendingAt" bson:"h265PendingAt,omitempty"` // H.265 提交云端时间 + PreviewURL string `json:"previewURL" bson:"previewURL"` // 预览视频资源地址(并非所有视频都有预览) + MimeType string `json:"mimeType" bson:"mimeType"` // 视频格式类型 + Filename string `json:"fileName" bson:"fileName,omitempty"` // 文件名称 + PlayTime uint `json:"playTime" bson:"playTime"` // 影片长度 + Cover string `json:"cover" bson:"cover"` // 封面大图 + CoverThumb string `json:"coverThumb" bson:"coverThumb"` // 封⾯小图 + SeriesCover []string `json:"seriesCover" bson:"seriesCover"` // 帖子套图 + VideoCover []string `json:"videoCover" bson:"videoCover"` // 视频截图 + SeriesNum int `json:"seriesNum" bson:"seriesNum"` // 图集数量 + Via string `json:"via" bson:"via"` // 来源 自拍,上传 + Rating int `json:"rating" bson:"rating"` // 总历史点击数 + PlayCount int `json:"playCount" bson:"playCount"` // 总播放量 + EffectivePlayCount int `json:"effectivePlayCount" bson:"effectivePlayCount"` // 有效播放量 + PurchaseCount int `json:"purchaseCount" bson:"purchaseCount"` // 视频购买人数 + LikeCount int `json:"likeCount" bson:"likeCount"` // 点赞数 + CollectCount int `json:"collectCount" bson:"collectCount"` // 收藏数 + CommentCount int `json:"commentCount" bson:"commentCount"` // 评论数 + ShareCount int `json:"shareCount" bson:"shareCount"` // 分享数 + RecommendLikeCount int64 `json:"-" bson:"recommendLikeCount"` // 推荐累计点赞,只增不减 + RecommendCollectCount int64 `json:"-" bson:"recommendCollectCount"` // 推荐累计收藏,只增不减 + RecommendCommentCount int64 `json:"-" bson:"recommendCommentCount"` // 推荐累计评论,只增不减 + RecommendShareCount int64 `json:"-" bson:"recommendShareCount"` // 推荐累计分享,只增不减 + RecommendScore int64 `json:"-" bson:"recommendScore"` // 每日全量计算的推荐分,仅变化时落库 + RecommendScoreAt time.Time `json:"-" bson:"recommendScoreAt,omitempty"` // 推荐分最后变化或初始化时间 + RecommendInitialized bool `json:"-" bson:"recommendInitialized"` // 历史累计是否初始化 + FakeLikeCount int `json:"fakeLikeCount" bson:"fakeLikeCount"` // 点赞假数据 总点赞量 = 真点赞量+假点赞量 + FakeCommentCount int `json:"fakeCommentCount" bson:"fakeCommentCount"` // 评论假数据 总评论量 = 真评论量+假评论量 + FakeShareCount int `json:"fakeShareCount" bson:"fakeShareCount"` // 分享假数据 总分享量 = 真分享量+假分享量 + FakePlayCount int `json:"fakePlayCount" bson:"fakePlayCount"` // 播放假数据 总播放量 = 真播放量+假播放量 + Status int `json:"status" bson:"status"` // 状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + Reason string `json:"reason" bson:"reason,omitempty"` // 视频未通过审核时的理由 + Location primitive.ObjectID `json:"location" bson:"location"` // 位置 + FreeTime int `json:"freeTime" bson:"freeTime,omitempty"` // 免费观影时长 + Coins int64 `json:"coins" bson:"coins"` // 定价 + Size int `json:"size" bson:"size"` // 文件大小 byte + Resolution string `json:"resolution" bson:"resolution"` // 分辨率 + Width int `json:"width" bson:"width"` // 视频宽度 + Height int `json:"height" bson:"height"` // 视频高度 + Ratio float64 `json:"ratio" bson:"ratio"` // 宽高比 + Quality string `json:"quality" bson:"quality,omitempty"` // 视频质量 720P以上高质量-high 480-720P 中等质量-middle 其他为low + Direction string `json:"direction" bson:"direction,omitempty"` // 视频版式 vertical-竖屏 horizontal-横屏 square-方屏 + MD5 string `json:"md5" bson:"md5,omitempty"` // 文件摘要 + Actor string `json:"actor" bson:"actor,omitempty"` // 演员 + Chosen bool `json:"chosen" bson:"chosen"` // 是否精选 + ChosenDate time.Time `json:"chosenDate" bson:"chosenDate,omitempty"` // 精选刷新时间 + FreeArea bool `json:"freeArea" bson:"freeArea"` // 免费专区 + FreeAreaDate time.Time `json:"freeAreaDate" bson:"freeAreaDate,omitempty"` // 加入免费专区刷新时间 + IsHideLocation bool `json:"isHideLocation" bson:"isHideLocation"` // 是否隐藏地址 + RecoWeight int `json:"recoWeight" bson:"recoWeight"` // 推荐权重 -1,不可推荐 + NewUpdatedAt string `json:"newUpdatedAt" bson:"newUpdatedAt"` // 从源站同步信息 使用的标识符 + ReviewAt time.Time `json:"reviewAt" bson:"reviewAt,omitempty"` // 审核时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` // 刷新时间 + DeleteAt *time.Time `json:"deleteAt" bson:"deleteAt,omitempty"` // 删除时间,不能去除omitempty + IsTopping bool `json:"isTopping" bson:"isTopping"` // IsTopping + IsRecommend bool `json:"isRecommend" bson:"isRecommend"` // 力荐 + IsChoosen bool `json:"isChoosen" bson:"isChoosen"` // 置精(本项目的社区精华在使用) + ReviewAccount string `json:"reviewAccount" bson:"reviewAccount"` // 审核人 + LinkUrl string `json:"linkUrl" bson:"linkUrl"` // 广告跳转连接 目前只有广告帖子有用 + Rewarded decimal.Decimal `json:"rewarded" bson:"rewarded"` // 打赏金额 + FakeRewarded decimal.Decimal `json:"fakeRewarded" bson:"fakeRewarded"` // (假)获得打赏金额 + SortCode int `json:"sortCode" bson:"sortCode"` // 排序号 目前只有广告帖子有用 + TagSort interface{} `json:"tagSort" bson:"tagSort"` // 标签排序 + LiaoBaTop bool `json:"liaoBaTop" bson:"liaoBaTop"` // "撩吧"页面置顶 + LiaoBaTopSort int `json:"liaoBaTopSort" bson:"liaoBaTopSort"` // 置顶排序号 + WorksSort int `json:"worksSort" bson:"worksSort"` // 作品排序-最多5个 + PageViewCount int64 `json:"pageViewCount" bson:"pageViewCount"` // 视频页面展示次数 + ActivityID primitive.ObjectID `json:"activityId" bson:"activityId"` // 参赛视频活动ID + Hot float64 `json:"hot" bson:"hot"` // 热度值 + HappinessPlazaTop int32 `json:"happinessPlazaTop" bson:"happinessPlazaTop"` // 幸福广场置顶 + MDSID string `json:"mdsID" bson:"mdsID"` // 媒体资源库id + VerticalCover string `json:"verticalCover" bson:"verticalCover"` // 竖版封面 + MID string `json:"mId" bson:"mId"` // 模块ID + UpTag string `json:"upTag" bson:"upTag"` // 博主认证 + ShareSort int `json:"-" bson:"shareSort,omitempty"` // 分享视频列表排序. 出现在当用户分享视频时的推荐列表里. <=0时表示不推荐 + SeedLinkUrl string `json:"seedLinkUrl" bson:"seedLinkUrl,omitempty"` // 种子链接 + SeedSize uint64 `json:"seedSize" bson:"seedSize,omitempty"` // 种子影片大小 byte + SeedPlayTime uint64 `json:"seedPlayTime" bson:"seedPlayTime,omitempty"` // 种子影片时长 + DiscountAreaId primitive.ObjectID `json:"discountAreaId" bson:"discountAreaId,omitempty"` // 折扣专区 + RichText string `json:"richText" bson:"richText"` // 富文本内容 + PreviewStart int `json:"previewStart" bson:"previewStart"` // 预览时间起始点 + TimeNodeList []TimeNode `json:"timeNodeList" bson:"timeNodeList"` // 时间节点 + DownloadAllow int `json:"downloadAllow" bson:"downloadAllow"` // 允许下载的VIP级别,0表示不允许下载 1表示VIP 2表示免费 + ShowType int `json:"showType" bson:"showType"` // 0-所有的人都可以看 1-奇数可看 2-偶数可看 +} + +type TimeNode struct { + Time int `json:"time" bson:"time"` // 时间节点 单位秒 + Name string `json:"name" bson:"name"` // 节点名称 + Img string `json:"img" bson:"img"` // 图片 +} + +type ESVideo struct { + ID primitive.ObjectID `json:"id"` // ID + PublisherID uint64 `json:"publisherID"` // 上传者ID + NewsType string `json:"newsType"` // 帖子类型, SP,视频帖子,COVER + Title string `json:"title"` // 视频标题 + Tags []primitive.ObjectID `json:"tags"` // 视频标签 + TagsName []string `json:"tagsName"` // 标签名称 + Filename string `json:"fileName"` // 文件名称 + PlayCount int `json:"playCount"` // 总播放量 + PurchaseCount int `json:"purchaseCount"` // 视频购买人数 + LikeCount int `json:"likeCount"` // 点赞数 + CollectCount int `json:"collectCount" ` // 收藏数 + CommentCount int `json:"commentCount"` // 评论数 + ShareCount int `json:"shareCount"` // 分享数 + FakeLikeCount int `json:"fakeLikeCount"` // 点赞假数据 总点赞量 = 真点赞量+假点赞量 常威确认 + FakeShareCount int `json:"fakeShareCount"` // 分享假数据 总分享量 = 真分享量+假分享量 常威确认 + FakePlayCount int `json:"fakePlayCount"` // 播放假数据 总播放量 = 真播放量+假播放量 常威确认 + EffectivePlayCount int `json:"effectivePlayCount"` // 有效播放量 + Hot float64 `json:"hot"` // 视频热度值 + PlayTime uint `json:"playTime"` // 视频时长 + Status int `json:"status"` // 状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + Coins int64 `json:"coins"` // 定价 + Location primitive.ObjectID `json:"location"` // 位置 + CreatedAt time.Time `json:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt"` // 刷新时间 + ReviewedAt time.Time `json:"reviewAt"` // 审核时间 +} + +type ShareInfo struct { + ID primitive.ObjectID `json:"id" bson:"_id"` + Title string `json:"title" bson:"title"` + PlayTime uint `json:"playTime" bson:"playTime"` + Cover string `json:"cover" bson:"cover"` + SourceUrl string `json:"sourceUrl" bson:"sourceURL"` +} + +// 推荐model +type RecoVideoModel struct { + ID primitive.ObjectID `json:"id" bson:"_id"` + ChosenDate time.Time `json:"chosenDate" bson:"chosenDate,omitempty"` //精选刷新时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 +} + +type ESVideoSource struct { + ID primitive.ObjectID `json:"_id"` + Source ESVideo `json:"_source"` +} + +type ESVideoSourceWithTotal struct { + Hits []ESVideoSource `json:"hits"` + Total Total `json:"total"` +} + +type Total struct { + Relation string `json:"relation"` + Value int `json:"value"` +} + +// CityCount 统计播放量/访问量 +type CityCount struct { + ID primitive.ObjectID `bson:"_id"` + Count int `bson:"count"` +} + +// PulisherVideoID 推荐发布者 +type PulisherVideoID struct { + ID uint64 `bson:"_id"` + VideoID primitive.ObjectID `bson:"videoID"` +} + +// UserVideoCount 我的视频数 +type UserVideoCount struct { + ID uint64 `bson:"_id"` + Count int `bson:"count"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type PrefetchVideoModel struct { + M3u8Url string `bson:"sourceURL"` + Images []string `bson:"seriesCover"` +} +type VideoModelSort []*VideoModel + +func (a VideoModelSort) Len() int { return len(a) } + +func (a VideoModelSort) Less(i, j int) bool { + if a[i].LiaoBaTopSort != a[j].LiaoBaTopSort { + return a[i].LiaoBaTopSort > a[j].LiaoBaTopSort + } + if !a[i].ReviewAt.Equal(a[j].ReviewAt) { + return a[i].ReviewAt.After(a[j].ReviewAt) + } + return a[i].ID.Hex() > a[j].ID.Hex() +} + +func (a VideoModelSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +func (info *VideoModel) IsCover() bool { + return info.NewsType == COVER || info.NewsType == SEED_LINK || info.NewsType == PIC || info.MDSID != "" +} diff --git a/models/v/vidmod/update_marker.go b/models/v/vidmod/update_marker.go new file mode 100644 index 0000000..123225a --- /dev/null +++ b/models/v/vidmod/update_marker.go @@ -0,0 +1,59 @@ +package vidmod + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type ModuleLatestAt struct { + ModuleID string `bson:"_id"` + LatestAt time.Time `bson:"latestAt"` +} + +// LatestReviewAt 返回过滤条件下最近一次审核通过时间。 +func LatestReviewAt(filter bson.M) (*time.Time, error) { + var list []VideoModel + opts := options.Find(). + SetSort(bson.D{{Key: "reviewAt", Value: -1}, {Key: "_id", Value: -1}}). + SetLimit(1). + SetProjection(bson.M{"reviewAt": 1}) + if err := coll(nil).Find(&list, filter, opts); err != nil { + return nil, err + } + if len(list) == 0 || list[0].ReviewAt.IsZero() { + return nil, nil + } + latest := list[0].ReviewAt + return &latest, nil +} + +// LatestReviewAtByModules 批量返回各亚模块最近一次内容审核通过时间。 +func LatestReviewAtByModules(moduleIDs []string) (map[string]time.Time, error) { + result := make(map[string]time.Time, len(moduleIDs)) + if len(moduleIDs) == 0 { + return result, nil + } + pipeline := []bson.M{ + {"$match": bson.M{ + "status": CheckPass, + "mId": bson.M{"$in": moduleIDs}, + "reviewAt": bson.M{"$gt": time.Time{}}, + }}, + {"$group": bson.M{ + "_id": "$mId", + "latestAt": bson.M{"$max": "$reviewAt"}, + }}, + } + var rows []ModuleLatestAt + if err := coll(nil).Aggregate(&rows, pipeline); err != nil { + return nil, err + } + for _, row := range rows { + if row.ModuleID != "" && !row.LatestAt.IsZero() { + result[row.ModuleID] = row.LatestAt + } + } + return result, nil +} diff --git a/models/v/vidmod/vid.go b/models/v/vidmod/vid.go new file mode 100644 index 0000000..f2b179d --- /dev/null +++ b/models/v/vidmod/vid.go @@ -0,0 +1,3520 @@ +package vidmod + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "math" + "sort" + "strings" + "sync" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/localcache" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/common/stderr" + "91porn-server/models" + "91porn-server/models/commod" + "91porn-server/models/v/vidpopmod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +// 推荐将选比例 +var chosonRate = 0.8 + +const ( + ToppingLimit = 5 // ToppingLimit 置顶、力荐、置精数量限制 + table = models.VideoInfo + vidPopCfgKey = "vidPopCfgKey" + allVidWithPop = "allVidWithPop" +) + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "sourceID", Value: 1}}, + Options: options.Index().SetUnique(true).SetPartialFilterExpression(bson.M{"sourceID": bson.M{"$gt": ""}}), + }, + // skd 同步数据到es的时候需要按照更新时间筛选 + { + Keys: bson.D{{"updatedAt", -1}}, + }, + { + Keys: bson.D{{Key: "commentCount", Value: -1}}, + }, + { + Keys: bson.D{{Key: "playCount", Value: -1}}, + }, + { + // 针对视频热度排序查询 + Keys: bson.D{{Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "hot", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + { + // 针对视频播放最多排序查询 + Keys: bson.D{{Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + { + // 针对视频点赞最多排序查询 2抖音-推荐排序 + Keys: bson.D{{Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "isTopping", Value: -1}, {Key: "status", Value: -1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "publisherID", Value: 1}, {Key: "status", Value: 1}, {Key: "worksSort", Value: -1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}, {Key: "playCount", Value: -1}, {Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "coins", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: 1}}, + }, + // 由于废除了isTopping字段置顶,所以上线后这个索引要删掉 + //{ + // Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {Key: "isTopping", Value: 1}, {Key: "reviewAt", Value: 1}}, + //}, + // 置顶改为有数值的置顶,废除isTopping字段 + // 首页最新展示样式模块 + { + Keys: bson.D{{Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: -1}}, + }, + // 首页-最新 + { + Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: -1}}, + }, + // 首页-最多观看 + { + Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + // 首页-最多观看 + { + Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {Key: "hot", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + // 首页-热门推荐 + { + Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {Key: "liaoBaTopSort", Value: -1}, {Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + // 首页-最多收藏 + { + Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {Key: "collectCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + // 黄游-畅销(最多购买) + { + Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {Key: "purchaseCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "isTopping", Value: 1}, {Key: "reviewAt", Value: 1}}, + }, + // 由于废除了isTopping字段置顶,所以上线后这些个索引要删掉 + //{ + // Keys: bson.D{{Key: "tags", Value: 1}, {Key: "status", Value: 1}, {Key: "isTopping", Value: 1}, {Key: "reviewAt", Value: 1}}, + //}, + // 置顶改为有数值的置顶,废除isTopping字段 + // 社区-最新/标签新 + { + Keys: bson.D{{Key: "tags", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: -1}}, + }, + // 社区-推荐/标签最多收藏 + { + Keys: bson.D{{Key: "tags", Value: 1}, {Key: "status", Value: 1}, {Key: "liaoBaTopSort", Value: -1}, {Key: "collectCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + // 社区-最热/标签最多观看 + { + Keys: bson.D{{Key: "tags", Value: 1}, {Key: "status", Value: 1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + // 社区-精选 + { + Keys: bson.D{{Key: "tags", Value: 1}, {Key: "status", Value: 1}, {Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + // 社区-视频/标签-最多观看--按照类型查询 + { + Keys: bson.D{{Key: "tags", Value: 1}, {Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + // 标签-最多收藏 + { + Keys: bson.D{{Key: "tags", Value: 1}, {Key: "status", Value: 1}, {Key: "collectCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + // 标签-最新-按照类型查询 + { + Keys: bson.D{{Key: "tags", Value: 1}, {Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: -1}}, + }, + // /标签-最多收藏-按照类型查询 + { + Keys: bson.D{{Key: "tags", Value: 1}, {Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "liaoBaTopSort", Value: -1}, {Key: "collectCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + // /标签-最多收藏-按照类型查询 + { + Keys: bson.D{{Key: "tags", Value: 1}, {Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "liaoBaTopSort", Value: -1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + { // 热评 + Keys: bson.D{{Key: "tags", Value: 1}, {Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "hot", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}, {Key: "isTopping", Value: -1}, {Key: "reviewAt", Value: 1}}, + }, + { // 各类视频热度排序查询 + Keys: bson.D{{Key: "status", Value: 1}, {Key: "hot", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + { // 每日短视频推荐分数按 _id 稳定分批扫描 + Keys: bson.D{ + {Key: "newsType", Value: 1}, + {Key: "status", Value: 1}, + {Key: "deleteAt", Value: 1}, + {Key: "_id", Value: 1}, + }, + Options: options.Index().SetName(shortRecommendRefreshIndexName), + }, + // 折扣专区使用的索引 + { + Keys: bson.D{{Key: "discountAreaId", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "discountAreaId", Value: 1}, {Key: "status", Value: 1}, {Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + + // 2025--- 新版本 --------------------- + { + Keys: bson.D{{Key: "publisherID", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: -1}}, + }, { // 排行榜总榜查询 + Keys: bson.D{{Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {Key: "reviewAt", Value: -1}, {Key: "collectCount", Value: -1}}, + }, { // 短视频推荐(精选靠前) + Keys: bson.D{{Key: "newsType", Value: 1}, {Key: "status", Value: 1}, {"chosen", -1}, {Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "showType", Value: 1}}, + }, + { + Keys: bson.D{{Key: "liaoBaTopSort", Value: 1}, {Key: "reviewAt", Value: -1}}, + }, + { + // H.265 等待队列:按状态和入队时间读取待提交任务。 + Keys: bson.D{{Key: "h265Status", Value: 1}, {Key: "h265QueuedAt", Value: 1}}, + }, + { + // H.265 云端处理中队列:使用专用时间,避免普通业务更新影响超时判断。 + Keys: bson.D{{Key: "h265Status", Value: 1}, {Key: "h265PendingAt", Value: 1}}, + }, + { + // H.265 失败重试:按状态、失败次数和审核时间读取可重试任务。 + Keys: bson.D{{Key: "h265Status", Value: 1}, {Key: "h265FailCount", Value: 1}, {Key: "reviewAt", Value: -1}}, + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +type RecommendInteraction string + +const ( + RecommendInteractionLike RecommendInteraction = "like" + RecommendInteractionCollect RecommendInteraction = "collect" + RecommendInteractionComment RecommendInteraction = "comment" + RecommendInteractionShare RecommendInteraction = "share" +) + +// IncrementRecommendInteraction 累加真实互动推荐值。取消、删除和运营假数据不得调用。 +func IncrementRecommendInteraction(id ObjectID, interaction RecommendInteraction) error { + return IncrementRecommendInteractionContext(context.Background(), id, interaction) +} + +// IncrementRecommendInteractionContext 累加真实互动推荐值,并继承调用方的取消和超时。 +func IncrementRecommendInteractionContext( + ctx context.Context, + id ObjectID, + interaction RecommendInteraction, +) error { + field := "" + switch interaction { + case RecommendInteractionLike: + field = "recommendLikeCount" + case RecommendInteractionCollect: + field = "recommendCollectCount" + case RecommendInteractionComment: + field = "recommendCommentCount" + case RecommendInteractionShare: + field = "recommendShareCount" + default: + return fmt.Errorf("unknown recommend interaction: %s", interaction) + } + initialized := bson.M{ + "_id": id, "status": CheckPass, "newsType": SHORT, + "recommendInitialized": true, + } + collection := coll(mdb.ToolCtx(ctx)) + result, err := collection.UpdateOne(initialized, bson.M{"$inc": bson.M{field: 1}}) + if err != nil || result.MatchedCount > 0 { + return err + } + + // 初始化只会由一个并发请求成功;失败者随后重试普通 $inc,避免丢互动。 + initializing := bson.M{ + "_id": id, "status": CheckPass, "newsType": SHORT, + "recommendInitialized": bson.M{"$ne": true}, + } + result, err = collection.UpdateOne(initializing, recommendInteractionInitializationPipeline(field)) + if err != nil || result.MatchedCount > 0 { + return err + } + _, err = collection.UpdateOne(initialized, bson.M{"$inc": bson.M{field: 1}}) + return err +} + +func recommendInteractionInitializationPipeline(incrementField string) mongo.Pipeline { + countFields := []struct { + recommend string + legacy string + }{ + {recommend: "recommendLikeCount", legacy: "likeCount"}, + {recommend: "recommendCollectCount", legacy: "collectCount"}, + {recommend: "recommendCommentCount", legacy: "commentCount"}, + {recommend: "recommendShareCount", legacy: "shareCount"}, + } + set := bson.M{"recommendInitialized": true} + for _, countField := range countFields { + current := bson.M{"$ifNull": bson.A{"$" + countField.recommend, int64(0)}} + baseline := bson.M{"$max": bson.A{ + current, + bson.M{"$max": bson.A{ + bson.M{"$ifNull": bson.A{"$" + countField.legacy, int64(0)}}, + int64(0), + }}, + }} + if countField.recommend == incrementField { + baseline = bson.M{"$add": bson.A{baseline, int64(1)}} + } + set[countField.recommend] = baseline + } + return mongo.Pipeline{bson.D{{Key: "$set", Value: set}}} +} + +const ( + shortRecommendRefreshIndexName = "idx_short_recommend_scan" + recommendScoreBatchSize = 1000 + recommendScoreCursorBatchSize = 4000 + recommendScoreWorkerCount = 2 + recommendScoreCursorCloseTimeout = 5 * time.Second +) + +// RecommendCandidate 是生成全局队列所需的最小视频快照。 +type RecommendCandidate struct { + ID ObjectID + MID string + ReviewAt time.Time + RecommendScore int64 +} + +// recommendScoreDocument 仅在单个扫描批次内存活,避免全局队列长期持有互动明细。 +type recommendScoreDocument struct { + ID ObjectID `bson:"_id"` + MID string `bson:"mId"` + ReviewAt time.Time `bson:"reviewAt"` + LikeCount int `bson:"likeCount"` + CollectCount int `bson:"collectCount"` + CommentCount int `bson:"commentCount"` + ShareCount int `bson:"shareCount"` + RecommendLikeCount int64 `bson:"recommendLikeCount"` + RecommendCollectCount int64 `bson:"recommendCollectCount"` + RecommendCommentCount int64 `bson:"recommendCommentCount"` + RecommendShareCount int64 `bson:"recommendShareCount"` + RecommendScore *int64 `bson:"recommendScore"` + RecommendInitialized bool `bson:"recommendInitialized"` +} + +type recommendScoreStore interface { + MaxEligibleID(ctx context.Context) (ObjectID, error) + OpenCursor(ctx context.Context, maxID ObjectID, batchSize int32) (recommendScoreCursor, error) + BulkWrite(ctx context.Context, writes []mongo.WriteModel) error +} + +type recommendInitializationCounter interface { + CountUninitialized(ctx context.Context, maxID ObjectID, limit int64) (int64, error) +} + +type recommendScoreCursor interface { + Next(context.Context) bool + Decode(interface{}) error + Err() error + Close(context.Context) error +} + +type recommendScoreScope struct { + generatedAt time.Time + excludedModuleIDs []string +} + +func newRecommendScoreScope(generatedAt time.Time, excludedModuleIDs []string) recommendScoreScope { + seen := make(map[string]struct{}, len(excludedModuleIDs)) + normalized := make([]string, 0, len(excludedModuleIDs)) + for _, moduleID := range excludedModuleIDs { + moduleID = strings.TrimSpace(moduleID) + if moduleID == "" { + continue + } + if _, ok := seen[moduleID]; ok { + continue + } + seen[moduleID] = struct{}{} + normalized = append(normalized, moduleID) + } + return recommendScoreScope{ + generatedAt: generatedAt, + excludedModuleIDs: normalized, + } +} + +func (s recommendScoreScope) filter() bson.M { + filter := bson.M{ + "status": CheckPass, + "newsType": SHORT, + "deleteAt": nil, + "recoWeight": bson.M{"$ne": -1}, + // 审核通过时间晚于本次构建快照的视频不能进入新视频池或高分池。 + "reviewAt": bson.M{"$lte": s.generatedAt}, + } + if len(s.excludedModuleIDs) > 0 { + // mId $nin 的选择性通常较低,不为它单独增加索引;Mongo 继续使用 + // newsType/status/deleteAt/_id 稳定扫描索引,并在服务端尽早做残余过滤。 + filter["mId"] = bson.M{"$nin": append([]string(nil), s.excludedModuleIDs...)} + } + return filter +} + +type mongoRecommendScoreStore struct { + scope recommendScoreScope +} + +func (s mongoRecommendScoreStore) MaxEligibleID(ctx context.Context) (ObjectID, error) { + var latest struct { + ID ObjectID `bson:"_id"` + } + filter, opts := recommendScoreMaxEligibleIDQuery(s.scope) + err := coll(mdb.ToolCtx(ctx)).FindOne(&latest, filter, opts) + return recommendScoreMaxEligibleIDResult(latest.ID, err) +} + +func recommendScoreMaxEligibleIDResult(id ObjectID, err error) (ObjectID, error) { + if errors.Is(err, mongo.ErrNoDocuments) { + return primitive.NilObjectID, nil + } + if err != nil { + return primitive.NilObjectID, err + } + return id, nil +} + +func recommendScoreMaxEligibleIDQuery( + scope recommendScoreScope, +) (bson.M, *options.FindOneOptions) { + opts := options.FindOne(). + SetProjection(bson.M{"_id": 1}). + SetSort(bson.D{{Key: "_id", Value: -1}}). + SetHint(shortRecommendRefreshIndexName) + return scope.filter(), opts +} + +func recommendScoreCursorQuery( + maxID ObjectID, + batchSize int32, + scope recommendScoreScope, +) (bson.M, *options.FindOptions) { + filter := scope.filter() + filter["_id"] = bson.M{"$lte": maxID} + opts := options.Find(). + SetProjection(bson.M{ + "_id": 1, "mId": 1, "reviewAt": 1, "likeCount": 1, "collectCount": 1, "commentCount": 1, "shareCount": 1, + "recommendLikeCount": 1, "recommendCollectCount": 1, "recommendCommentCount": 1, + "recommendShareCount": 1, "recommendScore": 1, "recommendInitialized": 1, + }). + SetSort(bson.D{{Key: "_id", Value: 1}}). + SetBatchSize(batchSize). + SetHint(shortRecommendRefreshIndexName) + return filter, opts +} + +func (s mongoRecommendScoreStore) OpenCursor( + ctx context.Context, + maxID ObjectID, + batchSize int32, +) (recommendScoreCursor, error) { + filter, opts := recommendScoreCursorQuery(maxID, batchSize, s.scope) + return coll(mdb.ToolCtx(ctx)).FindCursor(filter, opts) +} + +func (mongoRecommendScoreStore) BulkWrite(ctx context.Context, writes []mongo.WriteModel) error { + _, err := coll(mdb.ToolCtx(ctx)).Bulk(writes, options.BulkWrite().SetOrdered(false)) + return err +} + +func (s mongoRecommendScoreStore) CountUninitialized( + ctx context.Context, + maxID ObjectID, + limit int64, +) (int64, error) { + if limit <= 0 { + return 0, nil + } + filter, opts := recommendInitializationCountQuery(maxID, limit, s.scope) + return coll(mdb.ToolCtx(ctx)).Count(filter, opts) +} + +func recommendInitializationCountQuery( + maxID ObjectID, + limit int64, + scope recommendScoreScope, +) (bson.M, *options.CountOptions) { + filter := scope.filter() + filter["_id"] = bson.M{"$lte": maxID} + filter["recommendInitialized"] = bson.M{"$ne": true} + opts := options.Count(). + SetHint(shortRecommendRefreshIndexName). + SetLimit(limit) + return filter, opts +} + +func nonNegativeInt(v int) int64 { + if v < 0 { + return 0 + } + return int64(v) +} + +func maxInt64(a, b int64) int64 { + if a > b { + return a + } + return b +} + +func CalculateRecommendScore(like, collect, comment, share int64) int64 { + return like + collect*2 + comment*3 + share*5 +} + +func compareObjectID(a, b ObjectID) int { + return bytes.Compare(a[:], b[:]) +} + +func prepareRecommendScoreBatch( + documents []recommendScoreDocument, + now time.Time, +) ([]RecommendCandidate, []mongo.WriteModel) { + candidates := make([]RecommendCandidate, 0, len(documents)) + var writes []mongo.WriteModel + var updatePipeline mongo.Pipeline + for i := range documents { + document := &documents[i] + like := document.RecommendLikeCount + collect := document.RecommendCollectCount + comment := document.RecommendCommentCount + share := document.RecommendShareCount + // 传统真实计数是增量链路的每日补偿源;取max既能补偿瞬时漏写, + // 又保证取消点赞/收藏或删除评论时推荐累计分不回退。 + like = maxInt64(like, nonNegativeInt(document.LikeCount)) + collect = maxInt64(collect, nonNegativeInt(document.CollectCount)) + comment = maxInt64(comment, nonNegativeInt(document.CommentCount)) + share = maxInt64(share, nonNegativeInt(document.ShareCount)) + score := CalculateRecommendScore(like, collect, comment, share) + candidates = append(candidates, RecommendCandidate{ + ID: document.ID, MID: document.MID, ReviewAt: document.ReviewAt, RecommendScore: score, + }) + if document.RecommendInitialized && + document.RecommendScore != nil && + *document.RecommendScore == score { + continue + } + if updatePipeline == nil { + updatePipeline = recommendScoreUpdatePipeline(now) + } + writes = append(writes, mongo.NewUpdateOneModel(). + SetFilter(bson.M{"_id": document.ID}). + SetUpdate(updatePipeline)) + } + return candidates, writes +} + +func recommendScoreUpdatePipeline(now time.Time) mongo.Pipeline { + counts := []struct { + recommend string + legacy string + }{ + {recommend: "recommendLikeCount", legacy: "likeCount"}, + {recommend: "recommendCollectCount", legacy: "collectCount"}, + {recommend: "recommendCommentCount", legacy: "commentCount"}, + {recommend: "recommendShareCount", legacy: "shareCount"}, + } + initializeSet := bson.M{"recommendInitialized": true} + for _, count := range counts { + current := bson.M{"$ifNull": bson.A{"$" + count.recommend, int64(0)}} + baseline := bson.M{"$max": bson.A{ + current, + bson.M{"$max": bson.A{ + bson.M{"$ifNull": bson.A{"$" + count.legacy, int64(0)}}, + int64(0), + }}, + }} + initializeSet[count.recommend] = baseline + } + score := bson.M{"$add": bson.A{ + "$recommendLikeCount", + bson.M{"$multiply": bson.A{"$recommendCollectCount", int64(2)}}, + bson.M{"$multiply": bson.A{"$recommendCommentCount", int64(3)}}, + bson.M{"$multiply": bson.A{"$recommendShareCount", int64(5)}}, + }} + return mongo.Pipeline{ + bson.D{{Key: "$set", Value: initializeSet}}, + bson.D{{Key: "$set", Value: bson.M{ + "recommendScore": score, "recommendScoreAt": now, + }}}, + } +} + +func streamRecommendScoreBatches( + ctx context.Context, + maxID ObjectID, + batchSize int, + cursorBatchSize int32, + store recommendScoreStore, + yield func([]recommendScoreDocument) error, +) (err error) { + cursor, err := store.OpenCursor(ctx, maxID, cursorBatchSize) + if err != nil { + return err + } + if cursor == nil { + return errors.New("recommend score cursor is nil") + } + defer func() { + closeCtx, cancel := context.WithTimeout(context.Background(), recommendScoreCursorCloseTimeout) + defer cancel() + if closeErr := cursor.Close(closeCtx); err == nil && closeErr != nil { + err = closeErr + } + }() + + batch := make([]recommendScoreDocument, 0, batchSize) + previousID := primitive.NilObjectID + for cursor.Next(ctx) { + var document recommendScoreDocument + if err := cursor.Decode(&document); err != nil { + return err + } + if document.ID.IsZero() || + compareObjectID(document.ID, previousID) <= 0 || + compareObjectID(document.ID, maxID) > 0 { + return fmt.Errorf("recommend score cursor returned an invalid _id sequence") + } + previousID = document.ID + batch = append(batch, document) + if len(batch) < batchSize { + continue + } + if err := yield(batch); err != nil { + return err + } + // Worker 异步持有已提交批次,必须换一块底层数组,不能 batch[:0] 复用。 + batch = make([]recommendScoreDocument, 0, batchSize) + } + if err := cursor.Err(); err != nil { + return err + } + if len(batch) > 0 { + if err := yield(batch); err != nil { + return err + } + } + return nil +} + +// refreshRecommendScores 固定扫描高水位,并使用有界 worker 分批刷新推荐分。 +func refreshRecommendScores( + ctx context.Context, + now time.Time, + batchSize int, + cursorBatchSize int32, + workerCount int, + store recommendScoreStore, + initializationLimits ...int64, +) ([]RecommendCandidate, error) { + startedAt := time.Now() + if batchSize <= 0 { + return nil, fmt.Errorf("recommend score batch size must be positive") + } + if cursorBatchSize <= 0 { + return nil, fmt.Errorf("recommend score cursor batch size must be positive") + } + if workerCount <= 0 { + return nil, fmt.Errorf("recommend score worker count must be positive") + } + maxID, err := store.MaxEligibleID(ctx) + if err != nil { + return nil, err + } + if maxID.IsZero() { + return []RecommendCandidate{}, nil + } + initializationLimit := int64(0) + if len(initializationLimits) > 0 { + initializationLimit = initializationLimits[0] + } + if initializationLimit > 0 { + counter, ok := store.(recommendInitializationCounter) + if !ok { + return nil, fmt.Errorf("recommend score store does not support initialization guard") + } + count, countErr := counter.CountUninitialized(ctx, maxID, initializationLimit+1) + if countErr != nil { + return nil, countErr + } + if count > initializationLimit { + return nil, RecommendInitializationLimitError{ + ObservedAtLeast: count, + Limit: initializationLimit, + } + } + } + + workCtx, cancel := context.WithCancel(ctx) + defer cancel() + jobs := make(chan []recommendScoreDocument, workerCount) + candidates := make([]RecommendCandidate, 0, batchSize*workerCount) + scoreWriteCount := 0 + bulkWriteCount := 0 + var candidatesMu sync.Mutex + var firstErr error + var errOnce sync.Once + fail := func(err error) { + if err == nil { + return + } + errOnce.Do(func() { + firstErr = err + cancel() + }) + } + + var workers sync.WaitGroup + workers.Add(workerCount) + for i := 0; i < workerCount; i++ { + go func() { + defer workers.Done() + for { + select { + case <-workCtx.Done(): + return + case batch, ok := <-jobs: + if !ok { + return + } + batchCandidates, writes := prepareRecommendScoreBatch(batch, now) + if len(writes) > 0 { + if err := store.BulkWrite(workCtx, writes); err != nil { + fail(err) + return + } + } + candidatesMu.Lock() + candidates = append(candidates, batchCandidates...) + scoreWriteCount += len(writes) + if len(writes) > 0 { + bulkWriteCount++ + } + candidatesMu.Unlock() + } + } + }() + } + + var producer sync.WaitGroup + producer.Add(1) + go func() { + defer producer.Done() + defer close(jobs) + err := streamRecommendScoreBatches( + workCtx, + maxID, + batchSize, + cursorBatchSize, + store, + func(batch []recommendScoreDocument) error { + select { + case jobs <- batch: + return nil + case <-workCtx.Done(): + return workCtx.Err() + } + }, + ) + if err != nil { + fail(err) + } + }() + + producer.Wait() + workers.Wait() + if firstErr != nil { + return nil, firstErr + } + if err := ctx.Err(); err != nil { + return nil, err + } + log.Info("short recommend scores refreshed", + log.Any("candidateCount", len(candidates)), + log.Any("scoreWriteCount", scoreWriteCount), + log.Any("bulkWriteCount", bulkWriteCount), + log.Any("batchSize", batchSize), + log.Any("cursorBatchSize", cursorBatchSize), + log.Any("workerCount", workerCount), + log.Any("durationMs", time.Since(startedAt).Milliseconds())) + return candidates, nil +} + +// RefreshRecommendScores 全量计算短视频推荐分,仅回写首次初始化或分数变化的视频。 +func RefreshRecommendScores(ctx context.Context, now time.Time) ([]RecommendCandidate, error) { + scope := newRecommendScoreScope(now, nil) + return refreshRecommendScores( + ctx, + now, + recommendScoreBatchSize, + recommendScoreCursorBatchSize, + recommendScoreWorkerCount, + mongoRecommendScoreStore{scope: scope}, + ) +} + +// RecommendInitializationLimitError 表示首次推荐字段初始化量超过发布保护阈值。 +// 该错误在任何BulkWrite之前返回,避免生产首发产生不可控的海量写入。 +type RecommendInitializationLimitError struct { + ObservedAtLeast int64 + Limit int64 +} + +func (e RecommendInitializationLimitError) Error() string { + return fmt.Sprintf( + "short recommend initialization requires at least %d writes, limit is %d", + e.ObservedAtLeast, + e.Limit, + ) +} + +// RefreshRecommendScoresWithInitializationLimit 在刷新前限制首次初始化写入量。 +// excludedModuleIDs 会进入初始化计数与扫描共用的Mongo过滤器,确保被排除模块 +// 不占用保护额度、不生成候选,也不会触发推荐分BulkWrite。 +// limit<=0时关闭保护,行为与RefreshRecommendScores一致。 +func RefreshRecommendScoresWithInitializationLimit( + ctx context.Context, + now time.Time, + limit int64, + excludedModuleIDs ...string, +) ([]RecommendCandidate, error) { + scope := newRecommendScoreScope(now, excludedModuleIDs) + return refreshRecommendScores( + ctx, + now, + recommendScoreBatchSize, + recommendScoreCursorBatchSize, + recommendScoreWorkerCount, + mongoRecommendScoreStore{scope: scope}, + limit, + ) +} + +// GetTotalCnt 获取标签视频总数 +func GetTotalCnt(cond bson.M) (int64, error) { + total, err := coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getTotalCnt", table, "Count", err), + log.Any("cond", cond), + ) + return 0, err + } + return total, nil +} + +// getPublishedVideoList 条件获取视频列表 +func getPublishedVideoList(page, size uint64, cond bson.M, sort bson.D) ([]*VideoModel, int, int64, bool, error) { + hasNext := false + cond["status"] = 1 + opts := options.FindOptions{} + if sort != nil { + opts.SetSort(sort) + } + opts.SetSkip(int64((page - 1) * size)).SetLimit(int64(size + 1)) + var back []*VideoModel + if err := coll(nil).Find(&back, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getPublishedVideoList", table, "Find", err), + log.Any("page", page), + log.Any("size", size), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return nil, 0, 0, false, err + } + if uint64(len(back)) > size { + hasNext = true + back = back[:size] + } + return back, 1, 1, hasNext, nil +} + +// GetVideoListByCond 条件获取视频列表 +func GetVideoListByCond(filter primitive.M, opts ...*options.FindOptions) ([]*VideoModel, error) { + var out []*VideoModel + if err := coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-QueryAllDocument]==> Model %s Find fail error:%+v:", table, err), + log.Any("filter", filter), + ) + return nil, err + } + return out, nil +} + +// getPublishedVideoList 条件获取视频列表,供推荐使用 +func getPublishedVideoList4Reco(size uint64, cond bson.M, sort bson.D) ([]*RecoVideoModel, error) { + var back []*RecoVideoModel + if size == 0 { + return back, nil + } + cond["status"] = 1 + cond["playTime"] = bson.M{"$lt": 600} + opts := options.FindOptions{} + if sort != nil { + opts.SetSort(sort) + } + opts.SetProjection(bson.M{"_id": 1, "createdAt": 1, "chosenDate": 1}) + opts.SetLimit(int64(size)) + if err := coll(nil).Find(&back, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getPublishedVideoList4Reco", table, "Find", err), + log.Any("size", size), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return nil, err + } + return back, nil +} + +// getDistinctVideoID 获取推荐的视频id,发布者去重 +func getDistinctVideoID(page, size uint64, cond bson.M, sort bson.D) ([]primitive.ObjectID, time.Time, time.Time, error) { + var maxChosenLine time.Time + var maxCreateLine time.Time + cond["recoWeight"] = bson.M{"$gte": 0} + videoInfos, err := getPublishedVideoList4Reco(size, cond, sort) + if err != nil { + return []primitive.ObjectID{}, maxChosenLine, maxCreateLine, err + } + videoInfosLen := len(videoInfos) + if videoInfosLen == 0 { + return []primitive.ObjectID{}, maxChosenLine, maxCreateLine, nil + } + vids := make([]primitive.ObjectID, 0, videoInfosLen) + for _, i := range videoInfos { + if i == nil { + continue + } + if i.ChosenDate.After(maxChosenLine) { + maxChosenLine = i.ChosenDate + } + if i.CreatedAt.After(maxCreateLine) { + maxCreateLine = i.CreatedAt + } + vids = append(vids, i.ID) + } + return vids, maxChosenLine, maxCreateLine, nil +} + +func getChosenVideoID(line time.Time, page, size uint64, cond bson.M, sort bson.D) ([]primitive.ObjectID, time.Time, error) { + cond["chosenDate"] = bson.M{"$gt": line} + cond["chosen"] = true + ids, chonseLine, _, err := getDistinctVideoID(page, size, cond, sort) + return ids, chonseLine, err +} + +func getUnChosenVideoID(line time.Time, page, size uint64, cond bson.M, sort bson.D) ([]primitive.ObjectID, time.Time, error) { + cond["createdAt"] = bson.M{"$gt": line} + cond["chosen"] = false + ids, _, createLine, err := getDistinctVideoID(page, size, cond, sort) + return ids, createLine, err +} + +func getRecommendVideoIDs(chosenLine time.Time, unpopLine time.Time, cond bson.M, page, size uint64, sort bson.D) ([]primitive.ObjectID, time.Time, time.Time, error) { + chosenSize := uint64(float64(size) * chosonRate) + if chosenSize == 0 { + chosenSize = 1 + } + var maxChoseLine time.Time + var maxCreateLine time.Time + idsChosen, maxChoseLine, err := getChosenVideoID(chosenLine, page, chosenSize, cond, sort) + if err != nil { + return nil, maxChoseLine, maxCreateLine, err + } + unChosenSize := size - uint64(len(idsChosen)) + if unChosenSize == 0 { + return idsChosen, maxChoseLine, maxCreateLine, nil + } + delete(cond, "chosenDate") + idsUnchosen, maxCreateLine, err := getUnChosenVideoID(unpopLine, page, unChosenSize, cond, sort) + if err != nil { + return nil, maxChoseLine, maxCreateLine, err + } + idsChosen = append(idsChosen, idsUnchosen...) + return idsChosen, maxChoseLine, maxCreateLine, nil +} + +// getVideoList 条件获取视频列表 +func getVideoList(page, size uint64, cond bson.M, s bson.D) ([]*VideoModel, int64, bool, error) { + total, err := coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Count", err), + log.Any("page", page), + log.Any("size", size), + log.Any("cond", cond), + log.Any("sort", s), + ) + return nil, 0, false, err + } + var ( + back []*VideoModel + opts = options.Find().SetSkip(int64(page-1) * int64(size)).SetLimit(int64(size)) + ) + if s != nil { + opts.SetSort(s) + } + if err = coll(nil).Find(&back, cond, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), + log.Any("page", page), + log.Any("size", size), + log.Any("cond", cond), + log.Any("sort", s), + ) + return nil, 0, false, err + } + if uint64(len(back)) > size { + return back[:size], total, true, nil + } + return back, total, false, nil +} + +func GetHotList(skip, limit int64, cond bson.M) (back []*VideoModel, total int64, hasNext bool, err error) { + // 获取总条数 + total, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + ) + return + } + if total == 0 { + return + } + res := make([]*VideoModel, 0) + if err = coll(nil).Find(&res, cond); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + ) + return + } + cfg, err := getVideoPopularityConfig() + if err != nil { + return + } + for _, v := range res { + v.Hot = calcHot(v, cfg) + } + sort.Slice(res, func(i, j int) bool { + return res[i].Hot > res[j].Hot + }) + if len(res) <= int(skip) { + hasNext = false + return + } + if len(res) <= int(skip+limit) { + back = res[skip:] + } else { + back = res[skip : skip+limit] + hasNext = true + } + return +} + +// 主动设置缓存 +func SetAllVidWithPopCache() { + res := make([]*VideoModel, 0) + filter := bson.M{ + "newsType": SP, + "status": 1, + } + if err := coll(nil).Find(&res, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "setAllVidWithPopCache", table, "Find", err), + log.Any("cond", filter), + ) + return + } + cfg, err := getVideoPopularityConfig() + if err != nil { + return + } + for _, v := range res { + v.Hot = calcHot(v, cfg) + } + sort.Slice(res, func(i, j int) bool { + return res[i].Hot > res[j].Hot + }) + localcache.C.Set(allVidWithPop, res, 30*time.Minute) +} + +func GetHotListFromCache(skip, limit int64, listType ListType, tag string, paymentEnum PaymentEnum) (back []*VideoModel, hasNext bool, err error) { + res := make([]*VideoModel, 0) + if cfg, ok := localcache.C.Get(allVidWithPop); ok { + res = cfg.([]*VideoModel) + } else { + filter := bson.M{ + "newsType": SP, + "status": 1, + } + if err = coll(nil).Find(&res, filter); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", filter), + ) + return + } + cfg, err := getVideoPopularityConfig() + if err != nil { + return nil, false, err + } + for _, v := range res { + v.Hot = calcHot(v, cfg) + } + sort.Slice(res, func(i, j int) bool { + return res[i].Hot > res[j].Hot + }) + localcache.C.Set(allVidWithPop, res, 30*time.Minute) + } + if listType != Video || tag != "" || paymentEnum != PaymentDefault { + outIndexSli := make([]int, 0) + for i, v := range res { + if listType == LongVideo && v.PlayTime < 600 { + outIndexSli = append([]int{i}, outIndexSli...) + continue + } + if listType == ShortVideo && v.PlayTime >= 600 { + outIndexSli = append([]int{i}, outIndexSli...) + continue + } + if tag != "" { + hasTag := false + for _, t := range v.Tags { + if t.Hex() == tag { + hasTag = true + break + } + } + if !hasTag { + outIndexSli = append([]int{i}, outIndexSli...) + continue + } + } + if paymentEnum == PaymentVIP && v.Coins > 0 { + outIndexSli = append([]int{i}, outIndexSli...) + continue + } + if paymentEnum == PaymentGold && v.Coins == 0 { + outIndexSli = append([]int{i}, outIndexSli...) + continue + } + } + if len(outIndexSli) > 0 { + for oi := range outIndexSli { + res = append(res[:outIndexSli[oi]], res[outIndexSli[oi]+1:]...) + } + } + } + if len(res) <= int(skip) { + hasNext = false + return + } + if len(res) <= int(skip+limit) { + back = res[skip:] + } else { + back = res[skip : skip+limit] + hasNext = true + } + return +} + +func calcHot(video *VideoModel, cfg *vidpopmod.VideoPopularityConfig) float64 { + if video.Status == CheckPass || video.Status == Free { + qualityScore := float64(video.PlayCount + cfg.EffectivePlayCountMultiplier*video.EffectivePlayCount + + cfg.LikeCountMultiplier*video.LikeCount) + hot := (qualityScore + float64(cfg.InitialPopularity)) / math.Pow(1+time.Since(video.ReviewAt).Hours(), + cfg.ReviewTimePower) + return hot + } + return 0 +} + +func getVideoPopularityConfig() (*vidpopmod.VideoPopularityConfig, error) { + cfg, ok := localcache.C.Get(vidPopCfgKey) + if ok { + return cfg.(*vidpopmod.VideoPopularityConfig), nil + } + res, err := vidpopmod.FindOne() + if err != nil { + return nil, err + } + localcache.C.Set(vidPopCfgKey, &res, 15*time.Minute) + return &res, nil +} + +// 获取作品列表 +func getWorksList(skip, limit int64, cond bson.M, sort bson.D) (back []*VideoModel, total int64, hasNext bool, err error) { + opts := options.FindOptions{} + if sort != nil { + opts.SetSort(sort) + } + opts.SetSkip(skip).SetLimit(limit + 1) + // 获取总条数 + total, err = coll(nil).Count(cond) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + if total == 0 { + return + } + if err = coll(nil).Find(&back, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return + } + if len(back) > int(limit) { + hasNext = true + back = back[:limit] + } + return +} + +// ModifyMany 批量修改视屏信息 +func ModifyMany(ids []primitive.ObjectID, doc SetDoc) (int64, error) { + if len(ids) == 0 { + return 0, nil + } + filter := M{ + "_id": bson.M{"$in": ids}, + } + docM, err := common.ToBsonM(doc) + if err != nil { + return 0, err + } + update := M{ + "$set": docM, + } + ret, err := coll(nil).UpdateMany(filter, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RemoveVideoFromMy", table, "UpdateMany", err), + log.Any("updateDoc", doc), + ) + return 0, err + } + return ret.ModifiedCount, nil +} + +// CountByCreatedTime CountByCreatedTime +func CountByCreatedTime(start time.Time, end time.Time) (int64, error) { + count, err := coll(nil).Count(bson.M{"createdAt": bson.M{"$gte": start, "$lt": end}}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountByCreatedTime", table, "Count", err)) + return count, err + } + return count, nil +} + +// CountTotalLikeByUID 查询用户被赞总数 +func CountTotalLikeByUID(uid uint64) (int64, error) { + pipeline := []bson.M{ + {"$match": bson.M{"publisherID": uid}}, + {"$group": bson.M{"_id": nil, "total": bson.M{"$sum": "$likeCount"}}}, + } + type res struct { + Total int64 `json:"total" bson:"total"` + } + resA := make([]*res, 0) + if err := coll(nil).Aggregate(&resA, pipeline); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountTotalLikeByUID", table, "Aggregate", err), + log.Any("uid", uid), + ) + return 0, err + } + if len(resA) > 0 { + return resA[0].Total, nil + } + return 0, nil +} + +// 获取广告帖子 +func GetAdVideoIdsList(size uint64) []string { + cond := bson.M{"status": CheckPass, "newsType": bson.M{"$in": []string{AD_COVER, AD_SP}}, "recoWeight": bson.M{"$gte": 0}} + sort := bson.D{{Key: "sortCode", Value: -1}, {Key: "createdAt", Value: -1}} + videoInfos, err := getPublishedVideoList4Reco(size, cond, sort) + if err != nil { + return []string{} + } + videoInfosLen := len(videoInfos) + if videoInfosLen == 0 { + return []string{} + } + ids := make([]string, videoInfosLen) + for i := range videoInfos { + ids[i] = videoInfos[i].ID.Hex() + } + return ids +} + +// GetVideoListByPublishers 通过发布者获取视频列表 +func GetVideoListByPublishers(uids []uint64, page, size uint64) ([]*VideoModel, int, bool, error) { + if len(uids) == 0 { + return []*VideoModel{}, 0, false, nil + } + cond := bson.M{"publisherID": bson.M{"$in": uids}} // 下层函数已经设置过滤status + sort := bson.D{{Key: "reviewAt", Value: -1}} + infos, totalPages, _, hasNext, err := getPublishedVideoList(page, size, cond, sort) + return infos, totalPages, hasNext, err +} + +// GetVideoListRecommend 获取推荐视频列表 +func GetVideoListRecommend(chosenLine time.Time, unpopLine time.Time, newsType, quality, direction string, page, size uint64) ([]*VideoModel, []primitive.ObjectID, int, time.Time, time.Time, error) { + cond := bson.M{"newsType": newsType, "coins": 0} + if quality != "" { + cond["quality"] = quality + } + if direction != "" { + cond["direction"] = direction + } + sort := bson.D{{Key: "createdAt", Value: 1}} + ids, maxChoseLine, maxCreateLine, err := getRecommendVideoIDs(chosenLine, unpopLine, cond, page, size, sort) + if err != nil { + return nil, nil, 0, maxChoseLine, maxCreateLine, err + } + newIDs := common.Deduplication(ids) + infos, err := GetVideoListByIDs(newIDs) + if err != nil { + return nil, nil, 0, maxChoseLine, maxCreateLine, err + } + return infos, newIDs, 1, maxChoseLine, maxCreateLine, nil +} + +func FindOneByUid(cond bson.M) (vidInfo *VideoModel, err error) { + sort := D{ + {"reviewAt", -1}, + } + opt := (&options.FindOneOptions{}).SetSort(sort) + err = coll(nil).FindOne(&vidInfo, cond, opt) + return +} + +// GetUnpopularVideos 获取冷门视频列表 +func GetUnpopularVideos(line time.Time, newsType, quality, direction string, page, size uint64) ([]string, time.Time, error) { + cond := bson.M{"createdAt": bson.M{"$gt": line}, "coins": 0, "newsType": newsType} + if quality != "" { + cond["quality"] = quality + } + if direction != "" { + cond["direction"] = direction + } + sort := bson.D{{Key: "createdAt", Value: 1}} + ids, maxLine, err := getUnChosenVideoID(line, page, size, cond, sort) + if err != nil { + return nil, maxLine, err + } + if len(ids) == 0 { //如果没有视频拉取了,重置时间游标 + var newRound time.Time + ids, maxLine, err = getUnChosenVideoID(newRound, page, size, cond, sort) + } + arr := common.ObjectIDs2String(ids) + return arr, maxLine, err +} + +// GetChargeVideos 获取收费视频 +func GetChargeVideos(line time.Time, newsType, quality, direction string, page, size uint64) ([]string, time.Time, error) { + cond := bson.M{"coins": bson.M{"$gt": 0}, "newsType": newsType} + if quality != "" { + cond["quality"] = quality + } + if direction != "" { + cond["direction"] = direction + } + sort := bson.D{{Key: "createdAt", Value: 1}} + ids, maxLine, err := getUnChosenVideoID(line, page, size, cond, sort) + if err != nil { + return nil, maxLine, err + } + if len(ids) == 0 { //如果没有视频拉取了,重置时间游标 + var newRound time.Time + ids, maxLine, err = getUnChosenVideoID(newRound, page, size, cond, sort) + } + arr := common.ObjectIDs2String(ids) + return arr, maxLine, err +} + +// GetLatestUploadForRecommend 获取最新上传视频列表 +func GetLatestUploadForRecommend(line time.Time, newsType, quality, direction string, page, size uint64) ([]string, time.Time, error) { + sort := bson.D{{Key: "createdAt", Value: 1}} + filter := bson.M{"createdAt": bson.M{"$gt": line}, "coins": 0, "newsType": newsType} + if quality != "" { + filter["quality"] = quality + } + if direction != "" { + filter["direction"] = direction + } + oids, _, maxLine, err := getDistinctVideoID(page, size, filter, sort) + ids := common.ObjectIDs2String(oids) + return ids, maxLine, err +} + +// GetBloggerVideos 获取原创博主视频 +func GetBloggerVideos(bloggerUID uint64, skip, limit int64) (videos []*VideoModel, hasNext bool, err error) { + opts := options.Find().SetSkip(skip).SetLimit(limit + 1).SetSort(bson.D{{Key: "createdAt", Value: -1}}) + cond := bson.M{"publisherID": bloggerUID, "status": 1, "newsType": SP} + if err = coll(nil).Find(&videos, cond, opts); err != nil { + return + } + if len(videos) == int(limit)+1 { + hasNext = true + videos = videos[:limit] + } + return +} + +func GetVideoListByIDsSortByPlayCnt(vids []ObjectID, limit int64) (videos []*VideoModel, err error) { + cond := M{"_id": bson.M{"$in": vids}} + sort := D{{Key: "playCount", Value: -1}} + opts := options.FindOptions{} + opts.SetSort(sort).SetLimit(limit) + err = coll(nil).Find(&videos, cond, &opts) + return +} + +// GetVideoListByIDs 通过视频id获取视频列表 +func GetVideoListByIDs(vids []ObjectID) ([]*VideoModel, error) { + if vids == nil { + vids = []ObjectID{} + } + cond := M{"_id": bson.M{"$in": vids}} + infos, _, _, _, err := getPublishedVideoList(1, uint64(len(vids)), cond, nil) + return infos, err +} + +// GetVideoListByIDs 通过视频id获取视频列表 +func GetVideoListByIDsAndPlayTime(vids []ObjectID, page, size uint64, playTimeType int) ([]*VideoModel, error) { + if vids == nil { + vids = []ObjectID{} + } + cond := M{"_id": bson.M{"$in": vids}} + switch playTimeType { + case 1: + cond["playTime"] = bson.M{"$gte": 60 * 10} + case 2: + cond["playTime"] = bson.M{"$lt": 60 * 10} + } + infos, _, _, _, err := getPublishedVideoList(page, size+1, cond, nil) + return infos, err +} + +// GetVideoByID 根据id获取一条视频记录 +func GetVideoByID(id primitive.ObjectID) (videoInfo *VideoModel, err error) { + err = coll(nil).FindOne(&videoInfo, bson.M{"_id": id}) + return +} + +func GetVideoListByIDsNew(vids []ObjectID) ([]*VideoModel, bool, error) { + if vids == nil { + vids = []ObjectID{} + } + cond := M{"_id": bson.M{"$in": vids}} + sort := bson.D{{Key: "reviewAt", Value: -1}, {Key: "createdAt", Value: -1}} + infos, _, _, hasNext, err := getPublishedVideoList(1, uint64(len(vids)), cond, sort) + return infos, hasNext, err +} + +// GetVideoListByIDs 通过视频id获取视频列表 底层不判断视频状态, 由上次业务保证ID的正确性 +func GetVideoListByIDsNoStatus(vids []ObjectID) ([]*VideoModel, error) { + if vids == nil { + vids = []ObjectID{} + } + cond := M{"_id": bson.M{"$in": vids}} + sort := D{{Key: "reviewAt", Value: -1}} + infos, _, _, err := getVideoList(1, uint64(len(vids)), cond, sort) + return infos, err +} + +// GetVideoListByIDsUnPublish 通过视频id获取状态不为审核通过的视频 +func GetVideoListByIDsUnPublish(vids []ObjectID) (back []*RecoVideoModel, err error) { + opts := options.Find() + opts.SetProjection(bson.M{"_id": 1}) + cond := M{"_id": bson.M{"$in": vids}, "status": bson.M{"$ne": CheckPass}} + if err = coll(nil).Find(&back, cond, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoListByIDsUnPublish", table, "Find", err)) + } + return +} + +// VideoMap VideoMap +func VideoMap(vids []ObjectID) (map[ObjectID]*VideoModel, error) { + filter := M{ + "_id": bson.M{"$in": vids}, + "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + } + list := make([]VideoModel, 0, len(vids)) + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[ObjectID]*VideoModel) + for _, v := range list { + vid := v + m[vid.ID] = &vid + } + return m, nil +} + +// SearchVideoMap 搜索 +func SearchVideoMap(vids []ObjectID) (map[ObjectID]*VideoModel, []VideoModel, error) { + filter := M{ + "_id": bson.M{"$in": vids}, + "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + } + list := make([]VideoModel, 0, len(vids)) + if err := coll(nil).Find(&list, filter); err != nil { + return nil, nil, err + } + m := make(map[ObjectID]*VideoModel) + for _, v := range list { + vid := v + m[vid.ID] = &vid + } + return m, list, nil +} + +// GetHisWorkList 获取作品列表 +func GetHisWorkList(uid uint64, sikp, limit int64, status *int, sortType string, playTimeType int) ([]*VideoModel, int64, bool, error) { + cond := bson.M{"publisherID": uid} + switch playTimeType { + case 1: + cond["newsType"] = SP + case 2: + cond["newsType"] = SHORT + case 3: + cond["newsType"] = COVER + case 4: + cond["newsType"] = PIC + default: + cond["newsType"] = bson.M{"$in": []string{SP, SHORT, COVER, PIC}} + } + if status != nil { + cond["status"] = status + } + var sort bson.D + switch sortType { + case "hot": + return GetHotList(sikp, limit, cond) + case "watch": + sort = bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}} + case "like": + sort = bson.D{{Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}} + case "new", "": + sort = bson.D{{Key: "reviewAt", Value: -1}} + default: //兼容旧版,传入参数为其他值 + sort = bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}} + } + // infos, _, total, hasNext, err := getPublishedVideoList(page, size, cond, sort) + return getWorksList(sikp, limit, cond, sort) +} + +// GetMyWorkList 获取我自己作品列表 +func GetMyWorkList(uid uint64, sortType string, sikp, limit uint64, playTimeType int) ([]*VideoModel, int64, bool, error) { + cond := bson.M{"publisherID": uid} + switch playTimeType { + case 1: + cond["newsType"] = SP + case 2: + cond["newsType"] = SHORT + case 3: + cond["newsType"] = COVER + case 4: + cond["newsType"] = PIC + default: + cond["newsType"] = bson.M{"$in": []string{SP, SHORT, COVER, PIC}} + } + sort := bson.D{{Key: "createdAt", Value: -1}} + switch sortType { + case "hot": + sort = bson.D{{"likeCount", -1}, {"reviewAt", -1}} + case "new", "": + sort = bson.D{{Key: "reviewAt", Value: -1}} + default: + } + // infos, _, total, hasNext, err := getPublishedVideoList(page, size, cond, sort) + return getWorksList(int64(sikp), int64(limit), cond, sort) +} + +// GetWorksSortList 获取排序作品---默认最多存在5个作品 +func GetWorksSortList(uid uint64, status *int) ([]*VideoModel, int64, bool, error) { + cond := bson.M{"publisherID": uid, "worksSort": bson.M{"$gt": 0}} + if status != nil { + cond["status"] = status + } + sort := bson.D{{Key: "worksSort", Value: 1}} + return getWorksList(0, 5, cond, sort) +} + +func GetWorksSortCount(uid uint64, status *int) (int64, error) { + cond := bson.M{"publisherID": uid, "worksSort": bson.M{"$gt": 0}} + if status != nil { + cond["status"] = status + } + // 获取总条数 + return coll(nil).Count(cond) +} + +func UpdateWorksSort(uid uint64, new, old int) error { + cond := bson.M{"publisherID": uid, "worksSort": old} + if _, err := coll(nil).UpdateOne(cond, bson.M{"$set": bson.M{"worksSort": new}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", + "UpdateWorksSort", table, "UpdateOne", err), log.Any("cond", cond), log.Any("new", new)) + return err + } + return nil +} + +// GetVideoListByLocation 根据位置ID获取视频帖子列表,按照置顶,力荐,置精,播放量,审核通过时间排序 +func GetVideoListByLocation(locationID primitive.ObjectID, page, limit int64) ([]*VideoModel, error) { + var out []*VideoModel + filter := bson.M{"location": locationID, "status": 1, "newsType": SP} + opts := options.Find().SetSkip((page - 1) * limit).SetLimit(limit). + SetSort(bson.D{{Key: "isChoosen", Value: -1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}) + return out, coll(nil).Find(&out, filter, opts) +} + +// GetImageListByLocation 根据位置ID获取图片帖子列表,按照置顶,力荐,置精,播放量,审核通过时间排序 +func GetImageListByLocation(locationID primitive.ObjectID, page, limit int64) ([]*VideoModel, error) { + var out []*VideoModel + filter := bson.M{"location": locationID, "status": 1, "newsType": COVER} + opts := options.Find().SetSkip((page - 1) * limit).SetLimit(limit). + SetSort(bson.D{{Key: "isChoosen", Value: -1}, {Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}) + return out, coll(nil).Find(&out, filter, opts) +} + +// GetAllVideoListByMapCond 根据条件获取所有视频,不分页! +func GetAllVideoListByMapCond(cond map[string]interface{}, sort primitive.D) ([]*VideoModel, stderr.Code) { + opts := options.FindOptions{} + if sort != nil { + opts.SetSort(sort) + } + var back []*VideoModel + if err := coll(nil).Find(&back, bson.M(cond), &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoList", table, "Find", err), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return nil, stderr.ErrDbQueryError + } + return back, stderr.Success +} + +// GetvideoListByMapCond web端通过组合条件获取视频列表 +func GetvideoListByMapCond(cond map[string]interface{}, sort primitive.D, page, size uint64) ([]*VideoModel, int64, error) { + infos, total, _, err := getVideoList(page, size, bson.M(cond), sort) + return infos, total, err +} + +// IncForwardCount 转发次数增加 +func IncForwardCount(inc int, ids ...ObjectID) error { + var query = bson.M{} + if len(ids) == 1 { + query["_id"] = ids[0] + } else { + query["_id"] = bson.M{"$in": ids} + } + update := bson.M{"$set": bson.M{"updatedAt": time.Now()}, "$inc": bson.M{"forwardCount": inc, "collectCount": inc}} + _, err := coll(nil).UpdateOne(query, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncLikeCount", table, "UpdateOne", err), + log.Any("ids", ids), + ) + return err + } + return nil +} + +// TotalWorks 用户总作品数 +func TotalWorks(filter bson.M) ([]WorkCount, error) { + pip := []bson.M{ + {"$match": filter}, // 过滤条件 由外部决定 + {"$group": bson.M{"_id": "$publisherID", "count": bson.M{"$sum": 1}}}, // 统计用户作品总数 + {"$sort": bson.M{"count": -1}}, // 按照作品数排序 + //{"$project": bson.M{"uid": "$_id.publisherID", "count": 1, "_id": 0}}, // 输出结果不包含_id + } + data := make([]WorkCount, 0) + if err := coll(nil).Aggregate(&data, pip); err != nil { + return nil, fmt.Errorf("TotalWorks err: %s", err.Error()) + } + return data, nil +} + +// IncBatchForwardCount 转发次数增加 +func IncBatchForwardCount(ids []ObjectID) error { + query := bson.M{"_id": bson.M{"$in": ids}} + update := bson.M{"$set": bson.M{"updatedAt": time.Now()}, "$inc": bson.M{"forwardCount": 1}} + _, err := coll(nil).UpdateMany(query, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncBatchForwardCount", table, "UpdateMany", err), + log.Any("ids", ids), + ) + return err + } + return nil +} + +// GetPayVidList GetPayVidList +func GetPayVidList(sort bson.D, skip int64, limit int64) ([]ObjectID, error) { + filter := bson.M{ + "status": CheckPass, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + "newsType": SP, + "coins": bson.M{"$gt": 0}, + } + opt := &options.FindOptions{} + if len(sort) != 0 { + opt.SetSort(sort) + } + opt.SetSkip(skip) + opt.SetLimit(limit) + payVideoList := make([]VideoModel, 0, limit) + if err := coll(nil).Find(&payVideoList, filter, opt); err != nil { + return nil, err + } + vidList := make([]ObjectID, len(payVideoList)) + for i, v := range payVideoList { + vidList[i] = v.ID + } + return vidList, nil +} + +// GetFreeVidList 获取免费视频 +func GetFreeVidList(sort bson.D, skip int64, limit int64) ([]ObjectID, error) { + filter := bson.M{ + "freeArea": true, + "coins": 0, + "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + } + opt := &options.FindOptions{} + opt.SetSort(bson.D{{Key: "fakePlayCount", Value: -1}, {Key: "createdAt", Value: -1}}) + opt.SetSkip(skip) + opt.SetLimit(limit) + list := make([]VideoModel, 0, limit) + if err := coll(nil).Find(&list, filter, opt); err != nil { + return nil, err + } + listLen := len(list) + if listLen == 0 { + return []ObjectID{}, nil + } + vidList := make([]ObjectID, listLen) + for i := range list { + vidList[i] = list[i].ID + } + return vidList, nil +} + +// GetVideoInfo 通过视频id获取视频详细信息 +func GetVideoInfo(videoID string) (VideoModel, error) { + v := VideoModel{} + oid, err := primitive.ObjectIDFromHex(videoID) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideoInfo", table, "ObjectIDFromHex", err), + log.Any("videoID", videoID), + ) + return v, err + } + if err := coll(nil).FindOne(&v, bson.M{"_id": oid}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideoInfo", table, "FindOne", err), + log.Any("videoID", videoID), + ) + return v, err + } + return v, nil +} + +// IsPublisher 判断是否是视频发布者 +func IsPublisher(uid uint64, vid ObjectID) (bool, error) { + info, err := GetVideoInfo(vid.Hex()) + if err != nil { + return false, err + } + return info.PublisherID == uid, nil +} + +// GetShareCount 获取视频分享数 +func GetShareCount(vid string) (int, error) { + info, err := GetVideoInfo(vid) + if err != nil { + return 0, err + } + return info.ShareCount, nil +} + +// GetShareList 获取分享视频列表 +func getShareList(status *int) ([]ShareInfo, error) { + filter := bson.M{"shareSort": bson.M{"$gt": 0}} + if status != nil { + filter["status"] = *status + } + opts := options.Find().SetSort(bson.D{{Key: "shareSort", Value: -1}}).SetLimit(20) + var sis []ShareInfo + return sis, coll(nil).Find(&sis, filter, opts) +} + +// GetVideoTag 获取视频标签 +func GetVideoTag(vid string) ([]ObjectID, error) { + info, err := GetVideoInfo(vid) + if err != nil { + return nil, err + } + return info.Tags, nil +} + +// GetVideoCover 获取视频封面 +func GetVideoCover(vid string) (string, error) { + info, err := GetVideoInfo(vid) + if err != nil { + return "", err + } + return info.Cover, nil +} + +// FakeLikeGteLike 是否假点赞数大于真点赞数 +func FakeLikeGteLike(vid string, fakeLike int) (bool, error) { + info, err := GetVideoInfo(vid) + if err != nil { + return false, err + } + return fakeLike >= info.LikeCount, nil +} + +// GetVidStatus 获取视频状态 +func GetVidStatus(vid string) (int, error) { + info, err := GetVideoInfo(vid) + if err != nil { + return 0, err + } + return info.Status, nil +} + +// GetVideoCntByPublisher 获取某人发布的视频数 +func GetVideoCntByPublisher(uid uint64, t string) (int64, error) { + total, err := coll(nil).Count(bson.M{"publisherID": uid, "newsType": t}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideoCntByPublisher", table, "Count", err), + log.Any("uid", uid), + ) + return 0, err + } + return total, nil +} + +// GetPublishedVideoCntByPublisher 获取某人过审发布的视频数 +func GetPublishedVideoCntByPublisher(uid uint64) (int64, error) { + total, err := coll(nil).Count(bson.M{"publisherID": uid, "status": 1}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetPublishedVideoCntByPublisher", table, "Count", err), + log.Any("uid", uid), + ) + return 0, err + } + return total, nil +} + +// VideoListByTagID VideoListByTagID +func VideoListByTagID(tid ObjectID, sort bson.D, vidSkipPer int64, vidLimitPer int64) ([]VideoModel, error) { + filter := M{ + "tags": M{ + "$in": A{tid}, + }, + "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + } + opt := &options.FindOptions{} + if len(sort) != 0 { + opt.SetSort(sort) + } + opt.SetSkip(vidSkipPer) + opt.SetLimit(vidLimitPer) + list := make([]VideoModel, 0, vidLimitPer) + return list, coll(nil).Find(&list, filter, opt) +} + +// VideoListByTagIDSort 排序 +func VideoListByTagIDSort(tid ObjectID, newsType string, sort int, skip, limit uint64) (list []RecoVideoModel, err error) { + filter := M{ + "tags": tid, + "newsType": newsType, + "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + } + sortCond := bson.D{{Key: "tagSort." + tid.Hex(), Value: -1}, {Key: "reviewAt", Value: -1}} + switch sort { + case 1: + sortCond = bson.D{{Key: "liaoBaTopSort", Value: -1}, {Key: "playCount", Value: -1}, {Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}} + case 2: + sortCond = bson.D{{Key: "reviewAt", Value: -1}} + case 3: + sortCond = bson.D{{Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}} + case 4: + sortCond = bson.D{{Key: "isChoosen", Value: -1}, {Key: "chosen", Value: -1}, {Key: "commentCount", Value: -1}, {Key: "reviewAt", Value: -1}} + case 5: + sortCond = bson.D{{Key: "reviewAt", Value: -1}} + filter["playTime"] = bson.M{"$gte": 600} + filter["newsType"] = SP + default: + } + opt := options.Find().SetSkip(int64(skip)).SetLimit(int64(limit)) + opt.SetProjection(bson.M{"chosenDate": 1, "createdAt": 1}) // 默认展示_id字段 + opt.SetSort(sortCond) //内嵌动态字段无法设置索引,建议不要这么设计 + return list, coll(nil).Find(&list, filter, opt) +} + +// VideoListByTagIDForRecommend 根据标签推荐 +func VideoListByTagIDForRecommend(tid, NewsType, quality, direction string, line time.Time, vidPage, vidLimitPer uint64) ([]string, time.Time, error) { + oid, _ := primitive.ObjectIDFromHex(tid) + filter := M{"tags": oid, "newsType": NewsType, "createdAt": bson.M{"$gt": line}, "coins": 0, "status": 1} //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + if quality != "" { + filter["quality"] = quality + } + if direction != "" { + filter["direction"] = direction + } + sort := bson.D{{Key: "createdAt", Value: 1}} + ids, _, maxCreateLine, err := getDistinctVideoID(vidPage, vidLimitPer, filter, sort) + if err != nil { + return nil, maxCreateLine, err + } + vidIDs := common.ObjectIDs2String(ids) + return vidIDs, maxCreateLine, nil +} + +// InsertBase 提交视频基本信息 +func InsertBase(info VideoModel) (primitive.ObjectID, error) { + vid, err := coll(nil).InsertOne(&info) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertBase", table, "InsertOne", err), + log.Any("info", info), + ) + return primitive.NilObjectID, err + } + return vid.InsertedID.(primitive.ObjectID), nil +} + +// 根据md5判断该视频是否重复 +func IsExist(md5 string, size int) bool { + suc, err := coll(nil).Exists(bson.M{"md5": md5, "size": size}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsExist", table, "Exists", err), + log.Any("md5", md5), + log.Any("size", size), + ) + return suc + } + return suc +} + +// 根据sourceID 判断文件视频文件是否存在 +func IsExistBySocID(sourceID string) bool { + suc, err := coll(nil).Exists(bson.M{"sourceID": sourceID}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsExistBySocID", table, "Exists", err), + log.Any("sourceID", sourceID), + ) + return suc + } + return suc +} + +// GetFreeTime 获取免费时长 +func GetFreeTime(tm uint) int { + if tm < second30 { + return 0 + } + if tm < minite10 { + return free10 + } + if tm < minite30 { + return free60 + } + if tm < minite60 { + return free120 + } + return free120 +} + +// IncCommentCount 评论统计次数加加 +func IncCommentCount(id ObjectID) error { + query := bson.M{"_id": id} + update := bson.M{"$inc": bson.M{"commentCount": 1, "fakeCommentCount": 1}} + if _, err := coll(nil).UpdateOne(query, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncCommentCount", table, "UpdateOne", err), + log.Any("id", id), + ) + return err + } + return nil +} + +// IncCommentCount 评论统计次数减少 +func DecCommentCount(id ObjectID, n int64) error { + query := bson.M{"_id": id} + update := bson.M{"$inc": bson.M{"commentCount": n, "fakeCommentCount": n}} + if _, err := coll(nil).UpdateOne(query, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DecCommentCount", table, "UpdateOne", err), + log.Any("id", id), + ) + return err + } + return nil +} + +// IncLikeCount 点赞统计次数加加 +func IncLikeCount(id ObjectID) error { + query := bson.M{"_id": id} + update := bson.M{"$set": bson.M{"updatedAt": time.Now()}, "$inc": bson.M{"likeCount": 1, "fakeLikeCount": 1}} + if _, err := coll(nil).UpdateOne(query, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncLikeCount", table, "UpdateOne", err), + log.Any("id", id), + ) + return err + } + return nil +} + +// DecLikeCount 点赞统计次数减减 +func DecLikeCount(ids ...ObjectID) error { + var query = bson.M{} + if len(ids) == 1 { + query["_id"] = ids[0] + } else { + query["_id"] = bson.M{"$in": ids} + } + update := bson.M{"$set": bson.M{"updatedAt": time.Now()}, "$inc": bson.M{"likeCount": -1, "fakeLikeCount": -1}} + if _, err := coll(nil).UpdateOne(query, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DecLikeCount", table, "UpdateOne", err), + log.Any("ids", ids), + ) + return err + } + return nil +} + +// IncPlayCount 播发统计次数加加 +func IncPlayCount(id ObjectID, req PlayReq, popCfg vidpopmod.VideoPopularityConfig) error { + query := bson.M{"_id": id} + vid, err := GetVideoInfo(id.Hex()) + if err != nil { + log.Error("IncPlayCount GetVideoInfo", log.Any("video id", id), log.E(err)) + return err + } + now := time.Now() + update := bson.M{"$set": bson.M{"updatedAt": now}, "$inc": bson.M{"playCount": 1, "fakePlayCount": 1}} + if req.Longer > vid.FreeTime { + vid.EffectivePlayCount += calcVideoEffectivePlayCount(req.Longer, int(vid.PlayTime), popCfg) + vid.PlayCount += 1 + vid.FakePlayCount += 1 + update = bson.M{ + "$set": bson.M{ + "updatedAt": now, + //"hot": calcHot(&vid, &popCfg), + }, + "$inc": bson.M{ + "playCount": 1, + "fakePlayCount": 1, + "effectivePlayCount": calcVideoEffectivePlayCount(req.Longer, int(vid.PlayTime), popCfg), + }} + } + if _, err = coll(nil).UpdateOne(query, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncPlayCount", table, "UpdateOne", err), + log.Any("id", id), + ) + return err + } + return nil +} + +func calcVideoEffectivePlayCount(longer, total int, popCfg vidpopmod.VideoPopularityConfig) int { + for _, v := range popCfg.PlayTimePercentage { + if total >= v.Min && total < v.Max { + eff := float64(total) * float64(v.Percentage) / 100 + if float64(longer) > eff { + return 1 + } + } + } + return 0 +} + +// IncPurchaseCount 购买次数增加 +func IncPurchaseCount(id ObjectID) error { + query := bson.M{"_id": id} + update := bson.M{"$inc": bson.M{"purchaseCount": 1}} + if _, err := coll(nil).UpdateOne(query, update); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncPurchaseCount", table, "UpdateOne", err), + log.Any("id", id), + ) + return err + } + return nil +} + +// UpdateVideoByMapCond web通过map视频更新 +func UpdateVideoByMapCond(id ObjectID, set bson.M) (VideoModel, error) { + set["updatedAt"] = time.Now() + var v VideoModel + if err := coll(nil).FindOneAndUpdate(&v, bson.M{"_id": id}, bson.M{"$set": set}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateVideoByMapCond", table, "UpdateMany", err), + log.Any("id", id), + log.Any("update", set), + ) + return v, err + } + return v, nil +} + +func UpdateOneByID(id ObjectID, set bson.M) (res *mongo.UpdateResult, err error) { + return coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}) +} + +func BatchUpdateReco(ids []ObjectID, reco bool) error { + if len(ids) == 0 { + return nil + } + query := bson.M{"_id": bson.M{"$in": ids}} + var update = bson.M{} + if reco { + update["recoWeight"] = 0 + } else { + update["recoWeight"] = -1 + } + _, err := coll(nil).UpdateMany(query, bson.M{"$set": update}) + return err +} + +// UpdateVideoBatch web批量更新视频状态 +func UpdateVideoBatch(ids []ObjectID, field string, status bool) (int64, error) { + if len(ids) == 0 { + return 0, nil + } + query := bson.M{"_id": bson.M{"$in": ids}} + update := bson.M{field: status} + now := time.Now() + if field == "chosen" { + update["chosenDate"] = now + } + if field == "freeArea" { + update["freeAreaDate"] = now + update["recoWeight"] = -1 + } + update["updatedAt"] = now + //if field == "liaoBaTop" && status { + // n, err := CountNum(bson.M{"liaoBaTop": true}) + // if err != nil { + // return 0, err + // } + //if n >= 20 { + // return 0, errors.New("overLiaoBaLimit") + //} + //} + result, err := coll(nil).UpdateMany(query, bson.M{"$set": update}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateVideoBatch", table, "UpdateMany", err), + log.Any("ids", ids), + log.Any("field", field), + log.Any("status", status), + ) + return 0, err + } + return result.ModifiedCount, nil +} + +// UpdateManyVideo web批量更新视频状态 +func UpdateManyVideo(ids []ObjectID, update bson.M) (int64, error) { + if len(ids) == 0 { + return 0, nil + } + query := bson.M{"_id": bson.M{"$in": ids}} + result, err := coll(nil).UpdateMany(query, bson.M{"$set": update}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateManyVideo", table, "UpdateMany", err), + log.Any("ids", ids), + ) + return 0, err + } + return result.ModifiedCount, nil +} + +// UpdateVideosByMapCond web通过map视频更新 +func UpdateVideosByMapCond(ids []ObjectID, set bson.M) (err error) { + set["updatedAt"] = time.Now() + + if _, err = coll(nil).UpdateMany(bson.M{"_id": bson.M{"$in": ids}}, bson.M{"$set": set}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateVideoByMapCond", table, "UpdateMany", err), + log.Any("ids", ids), + log.Any("update", set), + ) + return err + } + return nil +} + +// PassVids web批量更新视频状态 +func PassVidsOnlineTime(id primitive.ObjectID, pass int, reason string, manager string, onlineTime time.Time, tagSort primitive.M) (VideoModel, error) { + var vid VideoModel + query := bson.M{"_id": id} + var update bson.M + now := time.Now() + if pass == Free { + update = bson.M{"status": CheckPass, "coins": 0, "updatedAt": now, "reviewAt": onlineTime, "reviewAccount": manager} + } else if pass == CheckPass { + update = bson.M{"status": CheckPass, "updatedAt": now, "reviewAt": onlineTime, "reviewAccount": manager, "tagSort": tagSort} + } else if pass == CheckFailure { + update = bson.M{"status": CheckFailure, "reason": reason, "updatedAt": now, "reviewAt": onlineTime, "reviewAccount": manager} + } + if err := coll(nil).FindOneAndUpdate(&vid, query, bson.M{"$set": update}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "PassVids", table, "FindOneAndUpdate", err), + log.Any("id", id), + log.Any("pass", pass), + log.Any("reason", reason), + ) + return vid, err + } + return vid, nil +} + +// PassVids web批量更新视频状态 +func PassVids(id primitive.ObjectID, pass int, reason string, manager string, tagSort primitive.M) (VideoModel, error) { + var vid VideoModel + query := bson.M{"_id": id} + var update bson.M + now := time.Now() + if pass == Free { + update = bson.M{"status": CheckPass, "coins": 0, "updatedAt": now, "reviewAt": now, "reviewAccount": manager} + } else if pass == CheckPass { + update = bson.M{"status": CheckPass, "updatedAt": now, "reviewAt": now, "reviewAccount": manager, "tagSort": tagSort} + } else if pass == CheckFailure { + update = bson.M{"status": CheckFailure, "reason": reason, "updatedAt": now, "reviewAt": now, "reviewAccount": manager} + } + if err := coll(nil).FindOneAndUpdate(&vid, query, bson.M{"$set": update}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "PassVids", table, "FindOneAndUpdate", err), + log.Any("id", id), + log.Any("pass", pass), + log.Any("reason", reason), + ) + return vid, err + } + return vid, nil +} + +// UpdateVideoResolutionPlayTime 更新视频 +func UpdateVideoResolutionPlayTime(id ObjectID, update WebVideoUpdateDoc) (int64, error) { + query := bson.M{"_id": id} + doc, _ := common.ToBsonM(update) + result, err := coll(nil).UpdateOne(query, bson.M{"$set": doc}) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateVideoResolutionPlayTime", table, "UpdateOne", err), + log.Any("id", id), + log.Any("update", update), + ) + return 0, err + } + return result.ModifiedCount, nil +} + +// sumFakePlayCount sumFakePlayCount +func sumFakePlayCount(sort D, skip, limit int64, mats ...pageopt.Matcher) (int64, error) { + videoList := make([]VideoModel, 0, limit) + opt := (&options.FindOptions{}). + SetSort(sort). + SetSkip(skip). + SetLimit(limit) + filter := pageopt.MergeM(mats) + filter["status"] = 1 //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + if err := coll(nil).Find(&videoList, filter, opt); err != nil { + return 0, err + } + total := int64(0) + for _, v := range videoList { + total += int64(v.FakePlayCount) + } + return total, nil +} + +func vidList(sort D, skip, limit int64) ([]ObjectID, error) { + videoList := make([]VideoModel, 0, limit) + opt := (&options.FindOptions{}). + SetSort(sort). + SetSkip(skip). + SetLimit(limit) + filter := M{ + "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + } + if err := coll(nil).Find(&videoList, filter, opt); err != nil { + return nil, err + } + vidList := make([]ObjectID, 0, len(videoList)) + for _, video := range videoList { + vidList = append(vidList, video.ID) + } + return vidList, nil +} + +// VIDListByCreatedTime VIDListByCreatedTime +func VIDListByCreatedAt(skip, limit int64) ([]ObjectID, error) { + sort := D{ + {Key: "createdAt", Value: -1}, + } + return vidList(sort, skip, limit) +} + +func SumFakePlayCountSortByCreatedAt(limit int64) (int64, error) { + sort := D{ + {Key: "createdAt", Value: -1}, + } + return sumFakePlayCount(sort, 0, limit) +} + +func SumFakePlayCountSortByFakeLikeCount(limit int64) (int64, error) { + sort := D{ + {Key: "fakeLikeCount", Value: -1}, + } + return sumFakePlayCount(sort, 0, limit) +} + +// VIDListByFakePlayCount VIDListByFakePlayCount +func VIDListByFakePlayCount(skip, limit int64) ([]ObjectID, error) { + sort := D{ + {Key: "fakePlayCount", Value: -1}, + } + return vidList(sort, skip, limit) +} + +func SumFakePlayCountSortByFakePlayCount(limit int64) (int64, error) { + sort := D{ + {Key: "fakePlayCount", Value: -1}, + } + return sumFakePlayCount(sort, 0, limit) +} + +// VIDListByFakeLikeCount VIDListByFakeLikeCount +func VIDListByFakeLikeCount(skip, limit int64) ([]ObjectID, error) { + sort := D{ + {Key: "fakeLikeCount", Value: -1}, + } + return vidList(sort, skip, limit) +} + +func SumFakePlayCountByFakeLikeCount(limit int64) (int64, error) { + sort := D{ + {Key: "fakeLikeCount", Value: -1}, + } + return sumFakePlayCount(sort, 0, limit) +} + +// VIDListSortByFakeCommentCount VIDListSortByFakeCommentCount +func VIDListSortByFakeCommentCount(skip, limit int64) ([]ObjectID, error) { + sort := D{ + {Key: "fakeCommentCount", Value: -1}, + } + return vidList(sort, skip, limit) +} + +func SumFakePlayCountSortByFakeCommentCount(limit int64) (int64, error) { + sort := D{ + {Key: "fakeCommentCount", Value: -1}, + } + return sumFakePlayCount(sort, 0, limit) +} + +func SumFakePlayCountSortByFreeArea(limit int64) (int64, error) { + sort := D{{Key: "fakeCommentCount", Value: -1}} + freeAreaMatch := &pageopt.AssignMatch{Key: "freeArea", Val: true} + coinsMatch := &pageopt.AssignMatch{Key: "coins", Val: 0} + return sumFakePlayCount(sort, 0, limit, freeAreaMatch, coinsMatch) +} + +func VIDListToneOfficialRecom(skip, limit int64) ([]primitive.ObjectID, error) { + sort := D{ + {Key: "fakeCommentCount", Value: -1}, + } + opt := (&options.FindOptions{}). + SetSort(sort). + SetSkip(skip). + SetLimit(limit) + filter := M{ + "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + "chosen": true, + } + var videoList []VideoModel + if err := coll(nil).Find(&videoList, filter, opt); err != nil { + return nil, err + } + vidList := make([]ObjectID, len(videoList)) + for i := range videoList { + vidList[i] = videoList[i].ID + } + return vidList, nil +} + +func SumFakePlayCountSortByToneOfficialRecom(limit int64) (int64, error) { + sort := D{ + {Key: "fakeCommentCount", Value: -1}, + } + opt := (&options.FindOptions{}). + SetSort(sort). + SetLimit(limit) + filter := M{ + "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + "chosen": true, + } + videoList := make([]VideoModel, 0, limit) + if err := coll(nil).Find(&videoList, filter, opt); err != nil { + return 0, err + } + total := int64(0) + for _, v := range videoList { + total += int64(v.FakePlayCount) + } + return total, nil +} + +// 通过关键字或tid匹配title获取VID List +func VIDListByKeywordOrTags(keyword string, tids []ObjectID, skip int64, limit int64) ([]ObjectID, error) { + opt := (&options.FindOptions{}). + SetSkip(skip). + SetLimit(limit). + SetSort(D{{Key: "fakePlayCount", Value: -1}, {Key: "createdAt", Value: -1}}) //索引 因为业务没有明确要求顺序 排序放最后提升效率 + filter := M{ + "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + "title": M{ + "$regex": fmt.Sprintf("^%s", keyword), + }, + } + if len(tids) != 0 { + filter = M{ + "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + "$or": A{ + M{"title": M{ + "$regex": fmt.Sprintf("^%s", keyword), + }}, + M{"tags": M{"$in": tids}}, + }, + } + } + docList := make([]struct { + ID ObjectID `bson:"_id"` + }, 0, limit) + if err := coll(nil).Find(&docList, filter, opt); err != nil { + return nil, err + } + idList := make([]ObjectID, len(docList)) + for i := range docList { + idList[i] = docList[i].ID + } + return idList, nil +} + +// GetCityPlayCount2Map 获取城市的访问量和播放量(热度) +func GetCityPlayCount2Map(locIDs []ObjectID) (map[ObjectID]int, error) { + if locIDs == nil { + locIDs = []ObjectID{} + } + m := make(map[ObjectID]int) + var data []CityCount + p := []bson.M{ + {"$match": bson.M{"location": bson.M{"$in": locIDs}, "status": 1}}, + {"$group": bson.M{"_id": "$location", "count": bson.M{"$sum": "$playCount"}}}, + } + if err := coll(nil).Aggregate(&data, p); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCityPlayCount2Map", table, "Aggregate", err), + log.Any("locIDs", locIDs), + ) + return nil, err + } + for _, d := range data { + m[d.ID] = d.Count + } + return m, nil +} + +// GetCityPublishedVideoCount2Map 获取城市的视频数 +func GetCityPublishedVideoCount2Map(locIDs []ObjectID) (map[ObjectID]int, error) { + if locIDs == nil { + locIDs = []ObjectID{} + } + m := make(map[ObjectID]int) + var data []CityCount + p := []bson.M{ + {"$match": bson.M{"location": bson.M{"$in": locIDs}, "status": 1}}, + {"$group": bson.M{"_id": "$location", "count": bson.M{"$sum": 1}}}, + } + if err := coll(nil).Aggregate(&data, p); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetCityPublishedVideoCount2Map", table, "Aggregate", err), + log.Any("locIDs", locIDs), + ) + return nil, err + } + for _, d := range data { + m[d.ID] = d.Count + } + return m, nil +} + +// GetTitleTimeByIDs 获取视频标题 +func GetTitleTimeByIDs(videoIDs []primitive.ObjectID) (map[primitive.ObjectID]TitleTime, error) { + m := make(map[primitive.ObjectID]TitleTime) + if videoIDs == nil { + videoIDs = []primitive.ObjectID{} + } + infos, err := GetVideoListByIDs(videoIDs) + if err != nil { + return m, err + } + for _, i := range infos { + info := TitleTime{ + Title: i.Title, + PlayTime: i.PlayTime, + } + m[i.ID] = info + } + return m, nil +} + +// uid获取视频并按播放量降序 +func GetVideosByPublisherID(publisherID uint64, stdQuery commod.StdQuery) (data []VideoModel, err error) { + if err = coll(nil).Find(&data, bson.M{"publisherID": publisherID}, commod.ConvertToListQuery(stdQuery)); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideosByPublisherID", table, "Find", err), + log.Any("publisherID", publisherID), + ) + return + } + return +} + +func CountPayVidsByUIDS(ids []primitive.ObjectID) (total int64, err error) { + total, err = coll(nil).Count(bson.M{"_id": bson.M{"$in": ids}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CountPayVidsByUIDS", table, "Count", err), + log.Any("ids", ids), + ) + return + } + return +} + +// RecommendVidoeByCity 通过城市推荐视频 +func RecommendVidoeByCity(locID primitive.ObjectID, line time.Time, newsType, quality, direction string, page, size uint64) ([]string, time.Time, error) { + cond := bson.M{"location": locID, "coins": 0, "createdAt": bson.M{"$gt": line}, "newsType": newsType} + if quality != "" { + cond["quality"] = quality + } + if direction != "" { + cond["direction"] = direction + } + sort := bson.D{{Key: "createdAt", Value: 1}} + ids, _, maxLine, err := getDistinctVideoID(page, size, cond, sort) + if err != nil { + return nil, maxLine, err + } + arr := common.ObjectIDs2String(ids) + return arr, maxLine, err +} + +// RecommendVidoeByHotWords 通过热词推荐视频 +func RecommendVidoeByHotWords(words []string, page, size uint64) []string { + pt := make([]string, len(words)) + for _, w := range words { + es, err := Search(w, int64((page-1)*size), int64(size)) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "RecommendVidoeByHotWords", table, "Search", err), + log.Any("words", words), + log.Any("page", page), + log.Any("size", size), + log.E(err)) + continue + } + for _, v := range es { + pt = append(pt, v.ID.Hex()) + } + } + return pt +} + +// GetChosenVideoID 获取精选视频 +func GetChosenVideoID(line time.Time, newsType string, page, size uint64) ([]string, time.Time, error) { + cond := bson.M{"coins": 0, "newsType": newsType} + sort := bson.D{{Key: "createdAt", Value: 1}} + ids, maxLine, err := getChosenVideoID(line, page, size, cond, sort) + if err != nil { + return []string{}, maxLine, err + } + arr := common.ObjectIDs2String(ids) + return arr, maxLine, err +} + +// GetPublishedVideoListForSyncCdn 条件获取视频列表 分页获取 +func GetPublishedVideoListForSyncCdn(page int, size int, lastId string) ([]*VideoModel, error) { + cond := bson.M{} + cond["status"] = 1 + if lastId != "" { + oid, _ := primitive.ObjectIDFromHex(lastId) + cond["_id"] = bson.M{"$gt": oid} + } + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "createdAt", Value: 1}}) + opts.SetSkip(int64((page - 1) * size)).SetLimit(int64(size)) + var back []*VideoModel + if err := coll(nil).Find(&back, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetPublishedVideoListForSyncCdn", table, "Find", err), + log.Any("page", page), + log.Any("size", size), + log.Any("lastId", lastId), + ) + return nil, err + } + return back, nil +} + +func GetVideoListByUpdateTimeRange(start time.Time, end time.Time, page int, size int) (data []*VideoModel, hasNext bool, err error) { + var query = bson.M{ + "updatedAt": bson.M{"$gte": start, "$lt": end}, + } + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "_id", Value: 1}}) + opts.SetSkip(int64((page - 1) * size)).SetLimit(int64(size) + 1) + if err = coll(nil).Find(&data, query, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideoListByUpdateTimeRange", table, "Find", err), + log.Any("start", start), + log.Any("end", end), + ) + return + } + if len(data) > size { + hasNext = true + data = data[:size] + } + + return +} + +func GetVideoListLtMinPlayLikeCount(minFakeLikesCnt, minFakePlayCnt, skip, limit int64) (data []*VideoModel, err error) { + filter := M{ + "$or": A{ + M{"fakeLikeCount": M{"$lt": minFakeLikesCnt}}, + M{"fakePlayCount": M{"$lt": minFakePlayCnt}}, + }, + } + opts := options.Find().SetSkip(skip).SetLimit(limit) + err = coll(nil).Find(&data, filter, opts) + return +} + +func GetVideoListByCreateTimeRange(start time.Time, end time.Time, maxCommentNum int) (data []*VideoModel, err error) { + var query = bson.M{ + "createdAt": bson.M{"$gte": start, "$lt": end}, + "commentCount": bson.M{"$lt": maxCommentNum}, + "status": 1, + } + opts := options.Find().SetSkip(0).SetLimit(5000) + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideoListByCreateTimeRange", table, "Find", err), + log.Any("start", start), + log.Any("end", end), + ) + return + } + return +} + +func FindMany(mats ...Matcher) (VIDSlice, error) { + return List(nil, nil, nil, mats...) +} + +// GetHisVideoCount 获取多个用户视频总数 +func GetHisVideoCount(uids []uint64) (map[uint64]int, error) { + if uids == nil { + uids = []uint64{} + } + var data []UserVideoCount + p := []bson.M{ + {"$match": bson.M{"publisherID": bson.M{"$in": uids}, "status": 1}}, + {"$group": bson.M{"_id": "$publisherID", "count": bson.M{"$sum": 1}}}, + } + if err := coll(nil).Aggregate(&data, p); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetHisVideoCount", table, "Aggregate", err), + log.Any("uids", uids), + ) + return nil, err + } + m := make(map[uint64]int) + for _, d := range data { + m[d.ID] = d.Count + } + return m, nil +} + +// 获取新导入的视频 +func GetNewImportVideo(newUpdatedAt string, startTime, endTime time.Time, page, pageSize int64) (data []*VideoModel, err error) { + opts := options.Find().SetSkip((page - 1) * pageSize).SetLimit(pageSize).SetSort(bson.D{{Key: "createdAt", Value: 1}}) + query := bson.M{} + if newUpdatedAt != "" { + query["newUpdatedAt"] = newUpdatedAt + } + if !startTime.IsZero() { + query["createdAt"] = bson.M{"$gte": startTime} + } + if !endTime.IsZero() { + query["createdAt"] = bson.M{"$lt": endTime} + } + if err = coll(nil).Find(&data, query, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetNewImportVideo", table, "Find", err), + log.Any("newUpdatedAt", newUpdatedAt), + log.Any("startTime", startTime), + log.Any("endTime", endTime), + ) + return + } + return +} + +func UpdateManyForContentCount(m map[primitive.ObjectID]int) (updateCount int64, err error) { + models := make([]mongo.WriteModel, len(m)) + i := 0 + for k, v := range m { + models[i] = mongo.NewUpdateOneModel().SetFilter(bson.M{"_id": k}).SetUpdate(bson.M{"$inc": bson.M{"commentCount": v}}) + i++ + } + res, err := coll(nil).Bulk(models) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateMany", table, "Bulk", err), + log.Any("map", m)) + return + } + updateCount = res.ModifiedCount + return +} + +// DelVideo 删除视频 +func DelVideo(ids []primitive.ObjectID) error { + if ids == nil { + return nil + } + if _, err := coll(nil).DeleteMany(bson.M{"_id": bson.M{"$in": ids}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteMany", table, "err", err), + log.Any("ids", ids)) + return err + } + return nil +} + +// GetVideosByUIDs 通过uids获取视频 +func GetVideosByUIDs(ids []uint64) (map[uint64]int, map[uint64]int, error) { + mTotal := make(map[uint64]int) + mUndeal := make(map[uint64]int) + if ids == nil { + return mTotal, mUndeal, nil + } + var vInfos []VideoModel + if err := coll(nil).Find(&vInfos, bson.M{"publisherID": bson.M{"$in": ids}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideosByUIDs", table, "err", err), + log.Any("ids", ids)) + return mTotal, mUndeal, err + } + for _, v := range vInfos { + if v.Status == 0 { + mUndeal[v.PublisherID]++ + } + mTotal[v.PublisherID]++ + } + return mTotal, mUndeal, nil +} + +// getPublishedNewsList 条件获取帖子列表 +func getPublishedNewsList(page, size uint64, cond bson.M, sort bson.D) ([]*VideoModel, bool, error) { + hasNext := false + cond["status"] = 1 + skip := (page - 1) * size + opts := options.FindOptions{} + if sort != nil { + opts.SetSort(sort) + } + opts.SetSkip(int64(skip)).SetLimit(int64(size + 1)) + var back []*VideoModel + if err := coll(nil).Find(&back, cond, &opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getPublishedVideoList", table, "Find", err), + log.Any("page", page), + log.Any("size", size), + log.Any("cond", cond), + log.Any("sort", sort), + ) + return nil, hasNext, err + } + if uint64(len(back)) > size { + hasNext = true + back = back[:size] + } + return back, hasNext, nil +} + +// GetNewestNews 获取最新帖子列表 +func GetNewestNews(page, size uint64, recentMinute time.Time) ([]*VideoModel, bool, error) { + cond := bson.M{"reviewAt": bson.M{"$lte": recentMinute}} + sort := bson.D{{Key: "reviewAt", Value: -1}} + return getPublishedNewsList(page, size, cond, sort) +} + +// GetNewestNews 获取最新帖子列表 +func GetNewestNews_old(page, size uint64) ([]*VideoModel, bool, error) { + cond := bson.M{} + sort := bson.D{{Key: "reviewAt", Value: -1}} + return getPublishedNewsList(page, size, cond, sort) +} + +// GetNewestNews 获取最新帖子列表 +func GetNewestShortVideo(page, size uint64) ([]*VideoModel, bool, error) { + cond := bson.M{"newsType": SP, "playTime": bson.M{"$lte": 600}} + sort := bson.D{{Key: "reviewAt", Value: -1}} + return getPublishedNewsList(page, size, cond, sort) +} + +// GetList 获取帖子列表 +func GetList(filter primitive.M, opts ...*options.FindOptions) (out []*VideoModel, err error) { + if err = coll(nil).Find(&out, filter, opts...); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetList", table, "Find", err), + log.Any("filter", filter), + log.Any("opts", opts), + ) + return + } + return +} + +// GetNewsByIDs2M 通过id获取视频信息到map +func GetNewsByIDs2M(ids []primitive.ObjectID) (map[primitive.ObjectID]*VideoModel, error) { + mInfo := make(map[primitive.ObjectID]*VideoModel) + cond := bson.M{"_id": bson.M{"$in": ids}} + infos, _, err := getPublishedNewsList(1, uint64(len(ids)), cond, nil) + if err != nil { + return mInfo, err + } + for _, v := range infos { + mInfo[v.ID] = v + } + return mInfo, err +} + +// GetNewsListLocation 获取附近的帖子列表 +func GetNewsListLocation(lid ObjectID, page, size uint64) ([]*VideoModel, bool, error) { + cond := bson.M{"location": lid} + sort := bson.D{{Key: "playCount", Value: -1}, {Key: "createdAt", Value: -1}} + return getPublishedNewsList(page, size, cond, sort) +} + +// GetCoinsNews 获取金币专区帖子列表 +func GetCoinsNews(page, size uint64, typ int) ([]*VideoModel, bool, error) { + cond := bson.M{"coins": bson.M{"$gt": 0}} + if typ == CommonUp { + cond["$or"] = bson.A{bson.M{"isMadou": bson.M{"$exists": false}}, bson.M{"isMadou": false}} + } + if typ == MadouUp { + cond["isMadou"] = true + } + sort := bson.D{{Key: "createdAt", Value: -1}} + return getPublishedNewsList(page, size, cond, sort) +} + +// 获取原创新版帖子列表 +func GetOriginals(uid uint64) ([]*VideoModel, error) { + cond := bson.M{"publisherID": uid, "newsType": SP} + sort := bson.D{{Key: "worksSort", Value: -1}} + infos, _, err := getPublishedNewsList(1, 3, cond, sort) + return infos, err +} + +// GetTotalNewsList 统计获取帖子 +func GetTotalNewsList() ([]*VideoModel, error) { + cond := bson.M{"status": 1} + var back []*VideoModel + if err := coll(nil).Find(&back, cond); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getPublishedVideoList", table, "Find", err)) + return back, err + } + return back, nil +} + +// IsNewsCover 获取帖子类型 +func IsNewsCover(id string) (bool, error) { + info, err := GetVideoInfo(id) + if err != nil { + return false, err + } + return info.NewsType == COVER || info.NewsType == SEED_LINK || info.NewsType == PIC || info.MDSID != "", nil +} + +// GetTopping 获取置顶 +func GetTopping(typ int, lid primitive.ObjectID, isMadou bool) ([]*VideoModel, error) { + cond := bson.M{"isTopping": true} + sort := bson.D{{Key: "reviewAt", Value: -1}} + if typ == TypeSameCity { + cond["location"] = lid + } + if typ == TypePay { + cond["coins"] = bson.M{"$gt": 0} + cond["isMadou"] = isMadou + } + infos, _, err := getPublishedNewsList(1, ToppingLimit, cond, sort) + return infos, err +} + +// GetReco 获取力荐 +func GetReco(typ int, lid primitive.ObjectID, isMadou bool) ([]*VideoModel, error) { + cond := bson.M{"isRecommend": true} + sort := bson.D{{Key: "reviewAt", Value: -1}} + if typ == TypeSameCity { + cond["location"] = lid + } + if typ == TypePay { + cond["coins"] = bson.M{"$gt": 0} + cond["isMadou"] = isMadou + } + infos, _, err := getPublishedNewsList(1, ToppingLimit, cond, sort) + return infos, err +} + +// GetChosen 获取置精 +func GetChosen(typ int, lid primitive.ObjectID, isMadou bool) ([]*VideoModel, error) { + cond := bson.M{"isChoosen": true} + sort := bson.D{{Key: "reviewAt", Value: -1}} + if typ == TypeSameCity { + cond["location"] = lid + } + if typ == TypePay { + cond["coins"] = bson.M{"$gt": 0} + cond["isMadou"] = isMadou + } + infos, _, err := getPublishedNewsList(1, ToppingLimit, cond, sort) + return infos, err +} + +// IsSP 是否是短视频 +func IsSP(newsType string) bool { + return newsType == SP +} + +// IsCover 是否是图片帖子 +func IsCover(newsType string) bool { + return newsType == COVER || newsType == PIC +} + +// IncRewarded 增加打赏金额 +func IncRewarded(vid primitive.ObjectID, decimal decimal.Decimal) error { + cond := bson.M{"_id": vid} + update := bson.M{"$inc": bson.M{"rewarded": decimal}} + _, err := coll(nil).UpdateOne(cond, update) + return err +} + +// IncSpecFakeRewarded 增加指定的(假)打赏金额 +func IncSpecFakeRewarded(vid primitive.ObjectID, decimal decimal.Decimal) error { + cond := bson.M{"_id": vid} + update := bson.M{"$inc": bson.M{"fakeRewarded": decimal}} + _, err := coll(nil).UpdateOne(cond, update) + return err +} + +func GetManyPrefetchVideos(page, pageSize int64) ([]*PrefetchVideoModel, error) { + opt := options.Find(). + SetLimit(pageSize). + SetSkip((page - 1) * pageSize). + SetSort(bson.M{"createdAt": 1}) + var data []*PrefetchVideoModel + return data, coll(nil).Find(&data, bson.M{"status": 1}, opt) +} + +func GetLiaoBaTop() ([]*VideoModel, error) { + var data []*VideoModel + opts := options.Find().SetSort(bson.D{ + {Key: "liaoBaTopSort", Value: -1}, + {Key: "reviewAt", Value: -1}, + {Key: "_id", Value: -1}, + }) + return data, coll(nil).Find(&data, bson.M{"liaoBaTop": true, "status": bson.M{"$nin": []int{0, 2}}}, opts) +} + +func GetVipList(page, pageSize uint64) ([]*VideoModel, bool, error) { + cond := bson.M{"coins": 0} + sort := bson.D{{Key: "liaoBaTopSort", Value: -1}, {Key: "reviewAt", Value: -1}} + return getPublishedNewsList(page, pageSize, cond, sort) +} + +// FindShortVidByIDs 获取长视频 +func FindShortVidByIDs(vids []primitive.ObjectID) ([]primitive.ObjectID, error) { + var data struct { + IDS []primitive.ObjectID `bson:"ids"` + } + p := []bson.M{ + {"$match": bson.M{"_id": bson.M{"$in": vids}, "playTime": bson.M{"$gt": 600}}}, + {"$group": bson.M{"_id": nil, "ids": bson.M{"$push": "$_id"}}}, + } + if err := coll(nil).AggregateDecode(&data, p); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindShortVidByIDs", table, "Aggregate", err), + log.Any("vids", vids), + ) + return nil, err + } + return data.IDS, nil +} + +// GetRecommIDs 获取推荐视频 +func GetRecommIDs(limit int64) ([]*VideoModel, error) { + cond := bson.M{"newsType": SP, "status": 1, "quality": bson.M{"$in": []string{Middle, High}}} + var back []*VideoModel + opt := options.Find().SetLimit(limit) + if err := coll(nil).Find(&back, cond, opt); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "getVideoIDsList", table, "AggregateDecode", err), + log.Any("cond", cond), + ) + return nil, err + } + return back, nil +} + +// GetHotVideoByOnlineTimeRange 获取时间区间上架的最热视频 +func GetHotVideoByOnlineTimeRange(start time.Time, end time.Time, page, size int64) (back []*VideoModel, hasNext bool, err error) { + var query = bson.M{ + "newsType": SP, + "reviewAt": bson.M{"$gte": start, "$lt": end}, + "status": CheckPass, + } + opt := &options.FindOptions{} + opt.SetSort(bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}}).SetSkip((page - 1) * size).SetLimit(size + 1) + + if err = coll(nil).Find(&back, query, opt); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetHotVideoByOnlineTimeRange", table, "Find", err), + log.Any("start", start), + log.Any("end", end), + ) + return + } + + if int64(len(back)) > size { + hasNext = true + back = back[:size] + } + + return +} + +func IncVideoPageView(vids []primitive.ObjectID) error { + if len(vids) == 0 { + return nil + } + query := bson.M{"_id": bson.M{"$in": vids}} + update := bson.M{"$inc": bson.M{"pageViewCount": 1}} + + _, err := coll(nil).UpdateMany(query, update) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncVideoPageView", table, "UpdateMany", err), log.Any("vids", vids)) + return err + } + return nil +} + +func Bulk(models []mongo.WriteModel) error { + _, err := coll(nil).Bulk(models, options.BulkWrite().SetOrdered(false)) + return err +} + +// GetCountByPublish 获取用户视频数量 status 0 未审核 1通过 2审核失败 3视为免费 默认为0 +func GetCountByPublish(publisherID uint64, status *int) (int64, error) { + var filter = bson.M{"publisherID": publisherID} + if status != nil { + filter["status"] = status + } + return coll(nil).Count(filter) +} + +// WorkLeaderboard 作品榜单 +func WorkLeaderboard(bind interface{}, filter bson.M, limit int) error { + filter["publisherID"] = bson.M{"$nin": []uint64{100007, 100008}} // 排除官方上传 + pip := []bson.M{ + {"$match": filter}, // 过滤条件 由外部决定 + {"$group": bson.M{"_id": "$publisherID", "count": bson.M{"$sum": 1}}}, // 统计用户作品总数 + {"$sort": bson.M{"count": -1}}, // 按照作品数排序 + {"$limit": limit}, // 限制返回条数 + } + return coll(nil).Aggregate(bind, pip) +} + +// GetCreatorNumber 获取创作者数量(查询非常慢,需要配合redis使用) +func GetCreatorNumber() (int64, error) { + var result []struct { + Count int64 `json:"count" bson:"count"` + } + pip := []bson.M{ + {"$group": bson.M{"_id": "$publisherID"}}, // 按照创作者ID分组 + {"$group": bson.M{"_id": "null", "count": bson.M{"$sum": 1}}}, // 统计创作者总数 + {"$project": bson.M{"_id": 0}}, // 输出结果不包含_id + } + if err := coll(nil).Aggregate(&result, pip); err != nil { + return 0, err + } + // 容错判定 + if len(result) > 0 { + return result[0].Count, nil + } + return 0, nil +} + +func IsSubmitByPublisherID(publisherID uint64) (bool, error) { + return coll(nil).Exists(bson.M{"publisherID": publisherID}) +} + +// GetRecommendation 获取推荐帖子==>幸福广场使用 +func GetRecommendation() (out []*VideoModel, err error) { + filter := bson.M{ + "newsType": COVER, + "status": 1, + //"$or": bson.A{ + // bson.M{"isTopping": true}, + // bson.M{"isRecommend": true}, + // bson.M{"isChoosen": true}, + //}, + } + if err = coll(nil).Find(&out, filter, options.Find().SetLimit(10)); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetRecommendation", table, "Find", err), + log.Any("filter", filter), + ) + return nil, err + } + return +} + +// VideoListByTagID +func VideoListByTagIDAndPlayTime(tid ObjectID, playDuration int, outVids []ObjectID) ([]*VideoModel, error) { + opt := &options.FindOptions{} + opt.SetSort(bson.D{{Key: "playCount", Value: -1}}) + filter := M{ + "tags": tid, + "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + "coins": 0, + } + if playDuration == 1 { + filter["playTime"] = bson.M{"$gte": 600} + opt.SetLimit(1) + } else if playDuration == 2 { + filter["playTime"] = bson.M{"$lt": 600} + opt.SetLimit(2) + } + if len(outVids) > 0 { + filter["_id"] = bson.M{"$nin": outVids} + } + list := make([]*VideoModel, 0) + return list, coll(nil).Find(&list, filter, opt) +} + +// HotVideo 热门视频 +func HotVideo() ([]*VideoModel, error) { + opt := &options.FindOptions{} + opt.SetSort(bson.D{{Key: "playCount", Value: -1}}).SetLimit(20) + filter := M{ + "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + "coins": 0, + } + list := make([]*VideoModel, 0) + return list, coll(nil).Find(&list, filter, opt) +} + +func GetVideosByIDs(ids []primitive.ObjectID) ([]*VideoModel, error) { + var out []*VideoModel + return out, coll(nil).Find(&out, bson.M{"_id": bson.M{"$in": ids}, "status": 1}) +} + +// GetRecommendVideosByIDsContext 获取仍满足短视频推荐条件的视频完整快照。 +// 队列生成后视频可能被下架、删除或改变类型,因此请求侧必须再次校验当前状态。 +func GetRecommendVideosByIDsContext(ctx context.Context, ids []primitive.ObjectID) ([]*VideoModel, error) { + if len(ids) == 0 { + return []*VideoModel{}, nil + } + var out []*VideoModel + return out, coll(mdb.ToolCtx(ctx)).Find(&out, recommendVideosByIDsFilter(ids)) +} + +func recommendVideosByIDsFilter(ids []primitive.ObjectID) bson.M { + return bson.M{ + "_id": bson.M{"$in": ids}, + "status": CheckPass, + "newsType": SHORT, + "deleteAt": nil, + "recoWeight": bson.M{"$ne": -1}, + } +} + +func DeleteBeforeDeleteAt(t *db.MongoTool, tm time.Time) error { + _, err := coll(t).DeleteMany(bson.M{"deletedAt": bson.M{"$lt": tm}}) + return err +} + +func GetVideosRecommandInVids(vids []primitive.ObjectID) ([]*VideoModel, error) { + var vms []*VideoModel + opts := options.Find().SetSort(bson.D{{Key: "effectivePlayCount", Value: -1}}).SetLimit(20) + return vms, coll(nil).Find(&vms, bson.M{"_id": bson.M{"$in": vids}, "status": 1, "reviewAt": bson.M{"$gte": time.Now().Add(time.Hour * 24 * -200)}}, opts) +} + +func GetSearchRecommand() (data []*VideoModel, err error) { + //加一个搜索缓存 + key := fmt.Sprintf("searchRecommend_search") + expaire := time.Minute * 2 + s, err := appg.Redis.Get(key) + if err != nil { + return + } + if s != nil && *s != "" { + err = json.Unmarshal([]byte(*s), &data) + return data, err + } + if data == nil { + var rechargeVms []*VideoModel + var freeVms []*VideoModel + opts := options.Find().SetSort(bson.D{{Key: "effectivePlayCount", Value: -1}}).SetLimit(10) + tm := time.Now().Add(time.Hour * 24 * -200) + var wg sync.WaitGroup + wg.Add(2) + common.Go(func() { + defer wg.Done() + if err := coll(nil).Find(&rechargeVms, bson.M{"coins": bson.M{"$gt": 0}, "status": 1, "reviewAt": bson.M{"$gte": tm}}, opts); err != nil { + return + } + }) + common.Go(func() { + defer wg.Done() + if err := coll(nil).Find(&freeVms, bson.M{"coins": 0, "status": 1, "reviewAt": bson.M{"$gte": tm}}, opts); err != nil { + return + } + }) + wg.Wait() + data = append(data, rechargeVms...) + data = append(data, freeVms...) + sort.Slice(data, func(i, j int) bool { + return data[i].FakePlayCount > data[j].FakePlayCount + }) + + common.Go(func() { + b, _ := json.Marshal(data) + _ = appg.Redis.Set(key, string(b), expaire) + }) + + } + return data, nil +} + +func FindOneByFilter(cond bson.M) (vidInfo *VideoModel, err error) { + err = coll(nil).FindOne(&vidInfo, cond) + return +} + +// ExportFindMany 导出的时候查询所有 +func ExportFindMany(filter bson.M, skip int64, size int64) ([]*VideoModel, error) { + sort := bson.D{{Key: "createdAt", Value: 1}} + opts := options.FindOptions{} + opts.SetSort(sort).SetSkip(skip).SetLimit(size) + data := make([]*VideoModel, 0) + if err := coll(nil).Find(&data, filter, &opts); err != nil { + log.ZapLog.Warn(" ExportFindMany Find fail", log.E(err)) + return data, err + } + return data, nil +} + +func GetShortVideoListByUpdateTimeRange(start time.Time, end time.Time, page int, size int) (data []*VideoModel, hasNext bool, err error) { + var query = bson.M{ + //"newsType": SHORT, 防止改类型 + "playTime": bson.M{"$lte": 300}, + "updatedAt": bson.M{"$gte": start, "$lt": end}, + } + opts := options.Find().SetSort(bson.D{{Key: "_id", Value: 1}}).SetSkip(int64((page - 1) * size)).SetLimit(int64(size)) + data, hasNext, err = FindList(query, opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideoListByUpdateTimeRange", table, "Find", err), + log.Any("start", start), + log.Any("end", end), + ) + return + } + return +} + +//==================================================================================================== +//==================================================================================================== +//==================================================================================================== +//==================================================================================================== +//==================================================================================================== + +type FilterType interface { + string | primitive.ObjectID | bson.M +} + +func makeFilter[T FilterType](cond T) (bson.M, error) { + var ( + filter bson.M + c any = cond + ) + + switch vidStr := c.(type) { + case string: + oid, err := primitive.ObjectIDFromHex(vidStr) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-Delete]==> Model %s %s fail error:%+v", table, "ObjectIDFromHex", err), log.Any("oid", oid)) + return nil, err + } + filter = bson.M{"_id": oid} + + case primitive.ObjectID: + filter = bson.M{"_id": c} + + default: // bson.M + filter = c.(bson.M) + } + + return filter, nil +} + +// Create 创建数据 +func Create(t *db.MongoTool, data ...*VideoModel) error { + r, err := coll(t).InsertMany(data) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-Create]==> Model %s %s fail error:%+v", table, "InsertMany", err), log.Any("data", data)) + return err + } + if len(r.InsertedIDs) == len(data) { + for i, d := range r.InsertedIDs { + data[i].ID = d.(primitive.ObjectID) + } + } + + return nil +} + +// DeleteOne 根据条件删除一条 +func DeleteOne[T FilterType](cond T, opt ...*options.DeleteOptions) error { + filter, err := makeFilter(cond) + if err != nil { + return err + } + _, err = coll(nil).DeleteOne(filter, opt...) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-Delete]==> Model %s %s fail error:%+v", table, "DeleteOne", err), log.Any("filter", filter)) + } + return err +} + +// DeleteMany 根据条件删除一条或者多条 +func DeleteMany[T FilterType](cond T, opt ...*options.DeleteOptions) error { + filter, err := makeFilter(cond) + if err != nil { + return err + } + _, err = coll(nil).DeleteMany(filter, opt...) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-Delete]==> Model %s %s fail error:%+v", table, "DeleteMany", err), log.Any("filter", filter)) + } + return err +} + +// FindList 条件获取列表 +func FindList(filter bson.M, opts *options.FindOptions, count ...*int64) (out []*VideoModel, hasNext bool, err error) { + if opts == nil { + opts = options.Find() + } + + if opts.Limit == nil { + opts.SetLimit(1000) + } + + if opts.Sort == nil { + opts.SetSort(bson.D{{Key: "_id", Value: -1}}) + } + + // 不需要统计总条数 就不要创建count,避免无用的查询 + if len(count) == 1 { + *count[0], err = coll(nil).Count(filter) + if err != nil { + + return nil, false, err + } + } + + limit := int(*opts.Limit) + opts.SetLimit(int64(limit + 1)) + + err = coll(nil).Find(&out, filter, opts) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-FetchList]==> Model %s Find fail error:%+v:", table, err), log.Any("filter", filter)) + return out, false, err + } + + hasNext = len(out) > limit + if hasNext { + out = out[:limit] + } + + return out, hasNext, nil +} + +// FindOne 通过id获取详细信息 +func FindOne[T FilterType](cond T, opt ...*options.FindOneOptions) (v *VideoModel, err error) { + filter, err := makeFilter(cond) + if err != nil { + return nil, err + } + err = coll(nil).FindOne(&v, filter, opt...) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-FetchInfoById]==> Model %s %s fail error:%+v:", table, "FindOne", err), log.Any("filter", filter)) + return v, err + } + return v, nil +} + +// UpdateOne 单条更新 +func UpdateOne[T FilterType](cond T, data bson.M) (err error) { + filter, err := makeFilter(cond) + if err != nil { + return err + } + + _, err = coll(nil).UpdateOne(filter, data) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-Update]==> Model %s %s fail error:%+v:", table, "UpdateOne", err), log.Any("filter", filter)) + return err + } + return nil +} + +// UpdateMany 批量更新 +func UpdateMany[T FilterType](cond T, data bson.M) (err error) { + filter, err := makeFilter(cond) + if err != nil { + return err + } + _, err = coll(nil).UpdateMany(filter, data) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-Update]==> Model %s %s fail error:%+v:", table, "UpdateOne", err), log.Any("filter", filter)) + return err + } + return nil +} + +// GetVideoListByIDsPublish 通过视频id获取状态为审核通过的视频 +func GetVideoListByIDsPublish(vIds []ObjectID) (back []*VideoModel, err error) { + opts := options.Find().SetSort(bson.D{{Key: "reviewAt", Value: -1}, {Key: "createdAt", Value: -1}}) + cond := M{"_id": bson.M{"$in": vIds}, "status": CheckPass} + if err = coll(nil).Find(&back, cond, opts); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetVideoListByIDsPublish", table, "Find", err)) + } + return +} + +// FindForReviewBatch 按 _id 游标分页扫描上架视频,用于内容审查任务 +// 仅扫描 status ∈ {CheckPass(1), Free(3)}(与"已上架"语义一致) +// lastID 传 primitive.NilObjectID 表示从头开始;limit 控制每批数量 +func FindForReviewBatch(lastID primitive.ObjectID, limit int64) ([]*VideoModel, error) { + cond := bson.M{"status": bson.M{"$in": []int{CheckPass, Free}}} + if !lastID.IsZero() { + cond["_id"] = bson.M{"$gt": lastID} + } + opts := options.Find(). + SetSort(bson.D{{Key: "_id", Value: 1}}). + SetLimit(limit) + var list []*VideoModel + if err := coll(nil).Find(&list, cond, opts); err != nil { + return nil, err + } + return list, nil +} + +// CountForReview 上架视频总数(用于任务初始化);条件与 FindForReviewBatch 保持一致 +func CountForReview() (int64, error) { + return coll(nil).Count(bson.M{"status": bson.M{"$in": []int{CheckPass, Free}}}) +} + +// UpdateForReview 内容审查通过后回写文本字段 +// title/content/richText 任一非空则更新对应字段 +func UpdateForReview(id primitive.ObjectID, title, content, richText string) error { + set := bson.M{} + if title != "" { + set["title"] = title + } + if content != "" { + set["content"] = content + } + if richText != "" { + set["richText"] = richText + } + if len(set) == 0 { + return nil + } + set["updatedAt"] = time.Now() + _, err := coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set}) + return err +} + +// OffShelfManyForReview 内容审查命中后批量下架视频(status -> OffShelf) +// 仅当前状态为 CheckPass/Free 的会被改动,已删除/已下架等状态保持不变 +func OffShelfManyForReview(ids []primitive.ObjectID) error { + if len(ids) == 0 { + return nil + } + _, err := coll(nil).UpdateMany( + bson.M{"_id": bson.M{"$in": ids}, "status": bson.M{"$in": []int{CheckPass, Free}}}, + bson.M{"$set": bson.M{"status": OffShelf, "updatedAt": time.Now()}}, + ) + return err +} diff --git a/models/v/vidmod/watch_consume_test.go b/models/v/vidmod/watch_consume_test.go new file mode 100644 index 0000000..c3a00fe --- /dev/null +++ b/models/v/vidmod/watch_consume_test.go @@ -0,0 +1,49 @@ +package vidmod + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestWatchConsumeReqVideoIDCompatibility(t *testing.T) { + tests := []struct { + body string + want string + }{ + {body: `{"videoId":"new-field"}`, want: "new-field"}, + {body: `{"vid":"legacy-field"}`, want: "legacy-field"}, + {body: `{"videoId":"new-field","vid":"legacy-field"}`, want: "new-field"}, + } + for _, tt := range tests { + var req WatchConsumeReq + if err := json.Unmarshal([]byte(tt.body), &req); err != nil { + t.Fatal(err) + } + if got := req.GetVideoID(); got != tt.want { + t.Fatalf("GetVideoID() = %q, want %q", got, tt.want) + } + } +} + +func TestWatchConsumeRespJSON(t *testing.T) { + data, err := json.Marshal(WatchConsumeResp{ + IsCan: true, + WatchCount: 2, + TotalWatchCount: 3, + Consumed: false, + }) + if err != nil { + t.Fatal(err) + } + for _, field := range []string{ + `"isCan":true`, + `"watchCount":2`, + `"totalWatchCount":3`, + `"consumed":false`, + } { + if !strings.Contains(string(data), field) { + t.Fatalf("%s missing from response: %s", field, data) + } + } +} diff --git a/models/v/vidmod/web.go b/models/v/vidmod/web.go new file mode 100644 index 0000000..680589d --- /dev/null +++ b/models/v/vidmod/web.go @@ -0,0 +1,555 @@ +package vidmod + +import ( + "time" + + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/locmod" + "91porn-server/models/v/usermod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + Merging = "Merging" // 合并中 + MergeCompleted = "MergeCompleted" //合并完成 + MergeError = "MergeError" // 合并失败 + Converting = "Converting" //格式转换中 + ConvertCompleted = "ConvertCompleted" //转换完成 + ConvertError = "ConvertError" // 转换失败 + UploadLoadingToFs = "UpLoading" // 正在上传中 + UploadCompleted = "UploadCompleted" // 上传完成 + Completed = "Completed" // 完成 + + FileUploadError = "FileUploadError" //文件上传失败 +) + +// WebSubmitReq web端视频上传请求 +type WebSubmitReq struct { + UID uint64 `form:"uid" json:"uid" binding:"required"` + NewsType string `form:"newsType" json:"newsType"` + Title string `form:"title" json:"title"` + Content string `form:"content" json:"content" bson:"content"` + Tags []string `form:"tags" json:"tags" binding:"required"` + PlayTime uint `form:"playTime" json:"playTime"` + Cover string `form:"cover" json:"cover"` + CoverThumb string `form:"coverThumb" json:"coverThumb"` + VerticalCover string `form:"verticalCover" json:"verticalCover" ` //竖版封 + SeriesCover []string `form:"seriesCover" json:"seriesCover"` + Via string `form:"via" json:"via"` + Coins int64 `form:"coins" json:"coins"` + Size int `form:"size" json:"size"` + Resolution string `form:"resolution" json:"resolution"` + Ratio float64 `json:"ratio" bson:"ratio"` //宽高比 + MimeType string `form:"mimeType" json:"mimeType"` + Location locmod.Location `form:"location" json:"location"` + Actor string `form:"actor" json:"actor"` + SourceID string `form:"sourceID" json:"sourceID"` + SourceURL string `form:"sourceURL" json:"sourceURL"` + MD5 string `form:"md5" json:"md5"` + Filename string `form:"filename" json:"filename"` + FreeTime int `form:"freeTime" json:"freeTime"` + Width int `form:"width" json:"width" ` // 视频宽度 + Height int `form:"height" json:"height" ` // 视频高度 + MDSID string `json:"mdsID" ` + Status int `json:"status"` // 状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + Account string `json:"account"` //审核帐号 + SeedLinkUrl string `form:"seedLinkUrl" json:"seedLinkUrl" bson:"seedLinkUrl"` // 种子链接内容 + SeedSize uint64 `json:"seedSize" bson:"seedSize,omitempty"` // 种子影片大小 byte + SeedPlayTime uint64 `json:"seedPlayTime" bson:"seedPlayTime,omitempty"` // 种子影片时长 + RichText string `form:"richText" json:"richText"` // 富文本内容 + PreviewStart int `form:"previewStart" json:"previewStart"` // 预览时间起始点 +} + +// ListReq 视频列表请求 +type ListReq struct { + Status int `form:"status" json:"status"` + Key string `form:"key" json:"key"` + Value int `form:"value" json:"value"` + IsFree int `form:"isFree" json:"isFree"` + IsUserUp int `form:"isUserUp" json:"isUserUp"` + Title string `form:"title" json:"title"` + UID uint64 `form:"uid" json:"uid"` + Start time.Time `form:"start" json:"start"` + End time.Time `form:"end" json:"end"` + Chosen int `form:"chosen" json:"chosen"` + FreeArea int `form:"freeArea" json:"freeArea"` + Tag string `form:"tag" json:"tag"` + ID string `form:"id" json:"id"` + IsPretendAcc int `form:"isPretendAcc" json:"isPretendAcc"` //是否马甲账号,0,所有,1,是,2,否 + NewsType string `form:"newsType" json:"newsType"` //帖子类型,0,所以;1,视频,2,图集 + LiaoBaTop *bool `form:"liaoBaTop" json:"liaoBaTop"` //"撩吧"页面置顶 + SectionID string `form:"sectionID" json:"sectionID"` //专题ID + IsSortedUnderModule bool `form:"isSortedUnderModule" json:"isSortedUnderModule"` //视频在专题下是否设置了排序码 + IsPush bool `form:"isPush" bson:"isPush"` //是否强推 + IsRecommended *bool `form:"isRecommended" bson:"isRecommended"` + IsHappinessPlazaTop *bool `form:"isHappinessPlazaTop"` // 是否幸福广场置顶 + ReviewAccount string `form:"reviewAccount" json:"reviewAccount"` // 审核管理员账号 + ShowType *int `json:"showType" form:"showType"` // 0-所有的人都可以看 1-奇数可看 2-偶数可看 + commod.Page +} + +// AwsPullReq 从AWS拉取视频信息请求接口 +type AwsPullReq struct { + ID string `json:"id"` + Type string `json:"type"` + Tag string `json:"tag"` + MaxPlayTime int `json:"maxPlayTime"` + MinPlayTime int `json:"minPlayTime"` + PageSize int `json:"pageSize"` + Page int `json:"page"` + Status string `json:"status"` + NewUpdateAt string `json:"newUpdateAt"` +} + +// PullReq 前端参数传递 +type PullReq struct { + Token string `json:"token" binding:"required"` + SyncType string `json:"syncType" binding:"required"` //同步类型 SP-同步视频 SERIES-同步套图 + UID []uint64 `json:"uids" binding:"required"` + Cityes []string `json:"cityes" binding:"required"` + RStatus string `json:"rStatus" binding:"required"` //审核状态, + Coins string `json:"coins" binding:"required"` + Retry bool `json:"retry"` //表示从某一批次 从头开始导入数据 + AwsPullReq +} + +// EditInfo 修改内容 +type EditInfo struct { + NewsType *string `form:"newsType" json:"newsType" bson:"newsType,omitempty"` // 帖子类型, SP,COVER,SEED_LINK + PublisherID *uint64 `form:"publisherID" json:"publisherID" bson:"publisherID,omitempty"` // 上传者ID + Cover *string `form:"cover" json:"cover,omitempty" bson:"cover,omitempty"` //封面大图 + CoverThumb *string `form:"coverThumb" json:"coverThumb,omitempty" bson:"coverThumb,omitempty"` + FakeLikeCount *int `form:"fakeLikeCount" json:"fakeLikeCount,omitempty" bson:"fakeLikeCount,omitempty"` //点赞假数据 + FakeCommentCount *int `form:"fakeCommentCount" json:"fakeCommentCount,omitempty" bson:"fakeCommentCount,omitempty"` //评论假数据 + FakeShareCount *int `form:"fakeShareCount" json:"fakeShareCount,omitempty" bson:"fakeShareCount,omitempty"` //分享假数据 + FakePlayCount *int `form:"fakePlayCount" json:"fakePlayCount,omitempty" bson:"fakePlayCount,omitempty"` //播发假数据 + FreeTime *int `form:"freeTime" json:"freeTime,omitempty" bson:"freeTime,omitempty"` //免费观影时长 + Coins *uint `form:"coins" json:"coins,omitempty" bson:"coins,omitempty"` //定价 + Title *string `form:"title" json:"title,omitempty" bson:"title,omitempty"` + Content *string `form:"content" json:"content,omitempty" bson:"content,omitempty"` // 帖子内容 + UpTag *string `form:"upTag" json:"upTag,omitempty" bson:"upTag,omitempty"` // 博主认证 + Status *int `form:"status" json:"status" bson:"status,omitempty"` // 状态 + CreatedAt *string `form:"createdAt" json:"createdAt,omitempty" bson:"createdAt,omitempty"` //视频上传时间 + SeriesCover *[]string `form:"seriesCover" json:"seriesCover" bson:"seriesCover,omitempty"` //帖子套图 + SeriesNum *int `form:"seriesNum" json:"seriesNum" bson:"seriesNum,omitempty"` //图集数量 + RecoWeight *int `form:"recoWeight" json:"recoWeight" bson:"recoWeight,omitempty"` //推荐权重 -1,不可推荐 + LinkUrl *string `form:"linkUrl" json:"linkUrl" bson:"linkUrl,omitempty"` + PreviewURL *string `json:"previewURL" bson:"previewURL,omitempty"` // 预览视频资源地址(并非所有视频都有预览) + SortCode *int `form:"sortCode" json:"sortCode" bson:"sortCode"` //排序号 目前只有广告帖子有用 + FakeRewarded *int `form:"fakeRewarded" json:"fakeRewarded" bson:"fakeRewarded,omitempty"` //(假)获得打赏次数 + LiaoBaTop *bool `form:"liaoBaTop" json:"liaoBaTop" bson:"liaoBaTop,omitempty"` //"撩吧"页面置顶 + LiaoBaTopSort *int `form:"liaoBaTopSort" json:"liaoBaTopSort" bson:"liaoBaTopSort,omitempty"` //"撩吧"页面置顶 排序 + WorksSort *int `form:"worksSort" json:"worksSort" bson:"worksSort,omitempty"` //作品排序 + ActivityID *string `form:"activityId" json:"activityId" bson:"activityId,omitempty"` //参赛作品活动ID + HappinessPlazaTop *int32 `json:"happinessPlazaTop" bson:"happinessPlazaTop,omitempty"` // 幸福广场置顶 + ShareSort *int `json:"shareSort" bson:"shareSort,omitempty"` // 分享视频列表排序. 出现在当用户分享视频时的推荐列表里. <=0时表示不推荐 + SeedLinkUrl *string `json:"seedLinkUrl" bson:"seedLinkUrl,omitempty"` // 种子下载链接 + SeedSize *int `json:"seedSize" bson:"seedSize,omitempty"` // 种子影片大小 byte + SeedPlayTime *uint `json:"seedPlayTime" bson:"seedPlayTime,omitempty"` // 种子影片时长 + SeedDownloadDesc *string `json:"seedDownloadDesc" bson:"seedDownloadDesc,omitempty"` // 种子下载说明 + SeedTips *string `json:"seedTips" bson:"seedTips,omitempty"` // 种子温馨提示 + PreviewStart *int `json:"previewStart" form:"previewStart" bson:"previewStart,omitempty"` // 预览时间起点 + RichText *string `json:"richText" bson:"-"` // 富文本内容(单独更新,不需要bson) + TimeNodeList *[]TimeNode `json:"timeNodeList" form:"timeNodeList"` // 时间节点 + ShowType *int `json:"showType" bson:"showType,omitempty"` // 0-所有的人都可以看 1-奇数可看 2-偶数可看 + DownloadAllow *int `json:"downloadAllow" bson:"downloadAllow,omitempty"` // 允许下载的VIP级别,0表示不允许下载 1表示VIP 2表示免费 +} + +// EditReq 视频更新参数 +type EditReq struct { + ID string `form:"id" json:"id"` + Type int `form:"type" json:"type"` //0 所有 1只标签 2只更新富文本 + Tags []string `form:"tags" json:"tags" bson:"tags,omitempty"` + TagSort []struct { + TagId string `form:"tagId" json:"tagId"` + SortCode int `form:"sortCode" json:"sortCode"` + } `form:"tagSort" json:"tagSort"` + EditInfo +} + +// DeleteReq 视频批量操作参数 +type DeleteReq struct { + IDs []string `json:"ids"` +} + +// BatchReq 视频批量操作参数 +type BatchReq struct { + IDs []string `json:"ids"` + Pass int `json:"pass"` //1通过 2不通过 3通过并修改成免费视屏 + Reason string `json:"reason"` //不通过时的理由 +} + +// BatchEditInfo 批量更新的结构 +type BatchEditInfo struct { + ID string `json:"id"` + Reason string `json:"reason"` + Coins int `json:"coins"` + FreeTime int `json:"freeTime"` +} + +// EditManyReq 视频批量更新参数 +type EditManyReq struct { + IDs []string `json:"ids"` + Field string `json:"field"` //批量更新字段 + Status bool `json:"status"` //更新视频状态,比如免费专区/取消免费专区,收费/取消收费,精选/取消精选 + TagIds []primitive.ObjectID `json:"tagIds"` // tag更新 + Coins *int `json:"coins"` // 价格 + PublisherID *uint64 `form:"publisherID" json:"publisherID"` // 上传者ID + OriginPublisherID *uint64 `form:"originPublisherID" json:"originPublisherID"` // 原上传者ID(用于转移功能) + UpStatus *int `json:"upStatus"` // 上架状态 1-上架 5-下架 + Tags []string `form:"tags" json:"tags" bson:"tags,omitempty"` + FreeTime *int `json:"freeTime"` // 免费观影时长 + DownloadAllow *int `json:"downloadAllow" bson:"downloadAllow"` // 允许下载的VIP级别,0表示不允许下载 1表示VIP 2表示免费 +} + +// PassManyReq 视频批量审核参数 +type PassManyReq struct { + Infos []BatchEditInfo `json:"infos"` + Pass int `json:"pass"` +} + +// WebVideo 返回视频的基本信息 +type WebVideo struct { + ID primitive.ObjectID `json:"id" xlsx:"帖子ID"` // 帖子ID + NewsType string `json:"newsType" xlsx:"-"` // 帖子类型, VID,视频帖子,COVER + Publisher usermod.BaseInfo `json:"publisher" xlsx:"-"` // 帖子发布者ID + PubliserID uint64 `json:"-" xlsx:"用户ID"` // 用户ID + PublisherName string `json:"-" xlsx:"用户昵称"` // 用户昵称 + Title string `json:"title" xlsx:"视频标题"` // 帖子标题 + Content string `json:"content" bson:"content"` // 视频内容 + Tags []TagInfo `json:"tags" xlsx:"-"` // 帖子标签列表 + SourceID string `json:"sourceID" xlsx:"-"` // 帖子源站ID + SourceURL string `json:"sourceURL" xlsx:"-"` // 帖子源站URL + PreviewURL string `json:"previewURL" bson:"previewURL"` // 预览视频资源地址(并非所有视频都有预览) + FileName string `json:"fileName" xlsx:"-"` // 文件名 + PlayTime uint `json:"playTime" xlsx:"总时长"` // 视频播放时长 + Status int `json:"status" xlsx:"-"` // 状态,0 未审核 1通过 2审核失败 3通过并认为免费 默认为0 + Cover string `json:"cover" xlsx:"-"` // 视频封面 + CoverThumb string `json:"coverThumb" xlsx:"-"` // 视频封面缩略图 + SeriesCover []string `json:"seriesCover" xlsx:"-"` // 封面套图 + IsUser bool `json:"isUser" xlsx:"-"` // TBD + PlayCount int `json:"playCount" xlsx:"播放量(真)"` // 视频播放量(真) + LikeCount int `json:"likeCount" xlsx:"点赞量(真)"` // 视频点赞量(真) + CommentCount int `json:"commentCount" xlsx:"评论数(真)"` // 视频评论量(真) + ShareCount int `json:"shareCount" xlsx:"-"` // 视频分享量(真) + FakeLikeCount int `json:"fakeLikeCount" xlsx:"-"` // 点赞假数据 + FakeCommentCount int `json:"fakeCommentCount" xlsx:"-"` // 评论假数据 + FakeShareCount int `json:"fakeShareCount" xlsx:"-"` // 分享假数据 + FakePlayCount int `json:"fakePlayCount" xlsx:"-"` // 播发假数据 + LikeRate float64 `json:"likeRate" xlsx:"点赞率"` // 点赞率 + Coins int64 `json:"coins" xlsx:"价格"` // 视频定价金币数 + Resolution string `json:"resolution" xlsx:"-"` // 视频分辨率 + FreeTime int `json:"freeTime" xlsx:"免费时长"` // 视频免费观看时长 + Chosen bool `json:"chosen" xlsx:"-"` // 是否精选 + FreeArea bool `json:"freeArea" xlsx:"-"` // 是否免费专区 + Pushing bool `json:"pushing" xlsx:"-"` // 是否推送 + Size int `json:"size" xlsx:"-"` // 文件大小 byte + CreatedAt time.Time `json:"createdAt" xlsx:"上传时间"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" xlsx:"更新时间"` // 更新时间 + Reason string `json:"reason" xlsx:"-"` // 审核失败的理由 + IsTopping bool `json:"isTopping" xlsx:"-"` // TBD + IsRecommend bool `json:"isRecommend" xlsx:"-"` // 力荐 + IsChoosen bool `json:"isChoosen" xlsx:"是否精选"` // 置精 + IsMadou bool `json:"isMadou" xlsx:"-"` // 是否麻豆上传 + RecoWeight int `json:"recoWeight" xlsx:"-"` // 推荐权重 -1,不可推荐 + ReviewAccount string `json:"reviewAccount" xlsx:"-"` // 审核人 + LinkUrl string `json:"linkUrl" xlsx:"-"` // TBD + SortCode int `json:"sortCode" xlsx:"-"` // 排序号 目前只有广告帖子有用 + TagSort []TagSort `json:"tagSort" xlsx:"-"` // 标签排序 + Rewarded decimal.Decimal `json:"rewarded" xlsx:"-"` // 打赏 + FakeRewarded decimal.Decimal `json:"fakeRewarded" xlsx:"-"` // (假)获得打赏 + LiaoBaTop bool `json:"liaoBaTop" xlsx:"-"` // 是否撩吧置顶 + LiaoBaTopSort int `json:"liaoBaTopSort" xlsx:"-"` // 撩吧置顶排序码 + WorksSort int `json:"worksSort" xlsx:"-"` // TBD + TotalSellDays float64 `json:"totalSellDays" xlsx:"售卖天数"` // 视频总的售卖天数: 从审核通过后开始计算 + TotalSellCount int64 `json:"totalSellCount" xlsx:"售卖次数"` // 视频总的售卖次数 + TotalSellAmount int64 `json:"totalSellAmount" xlsx:"售卖金币"` // 视频总的售卖金币数 + PurchaseRate float64 `json:"purchaseRate" xlsx:"成交率"` // 视频成交率: 视频总的售卖次数 / 视频播放量 + SectionID primitive.ObjectID `json:"sectionID" xlsx:"-"` // 视频所属专题ID + SortCodeUnderSection int `json:"sortCodeUnderSection" xlsx:"-"` // 视频在某个专题下的排序 + SectionName string `json:"sectionName" bson:"sectionName"` // 专题名称 + ModuleName string `json:"moduleName" bson:"moduleName"` // 父专题名称 + PageViewCount int64 `json:"pageViewCount" xlsx:"展现量"` // 视频页面展示次数 + HitRate float64 `json:"hitRate" xlsx:"点击率"` // 视频点击率 = 页面展示次数 / 视频播放量 + TagsExport string `json:"-" xlsx:"标签"` // 标签,仅用于导出 + ActivityID primitive.ObjectID `json:"activityId" xlsx:"-"` // 参赛视频活动ID + HappinessPlazaTop int32 `json:"happinessPlazaTop" xlsx:"-"` // 幸福广场置顶 + ReviewAt time.Time `json:"reviewAt" xlsx:"-"` // 审核时间 + ShareSort int `json:"shareSort" bson:"shareSort,omitempty"` // 分享视频列表排序. 出现在当用户分享视频时的推荐列表里. <=0时表示不推荐 + SeedLinkUrl string `json:"seedLinkUrl" bson:"seedLinkUrl,omitempty"` // 种子链接 + SeedSize uint64 `json:"seedSize" bson:"seedSize,omitempty"` // 种子影片大小 byte + SeedPlayTime uint64 `json:"seedPlayTime" bson:"seedPlayTime,omitempty"` // 种子影片时长 + RichText string `json:"richText" bson:"richText"` // 富文本内容 + PreviewStart int `json:"previewStart" bson:"previewStart"` // 预览时间起始点 + TimeNodeList []TimeNode `json:"timeNodeList" bson:"timeNodeList"` // 时间节点 + DownloadAllow int `json:"downloadAllow" bson:"downloadAllow"` // 允许下载的VIP级别,0表示不允许下载 1表示VIP 2表示免费 + ShowType int `json:"showType" bson:"showType"` // 0-所有的人都可以看 1-奇数可看 2-偶数可看 +} + +type TagSort struct { + TagId string `json:"tagId"` + TagName string `json:"tagName"` + SortCode int `json:"sortCode"` +} + +// ListResp web 视频列表应答 +type ListResp struct { + VInfos []*WebVideo `json:"vInfos"` // 帖子列表 + Total int64 `json:"total"` // 帖子总量 +} + +// AwsPullResp 从AWS拉取视频响应体 +type AwsPullResp struct { + ID string `json:"id"` + PublishID string `json:"publishID"` //视频上传ID + CheckSum string `json:"checkSum"` + Title string `json:"title"` + Actors []string `json:"actors"` + PlayTime uint `json:"playTime"` + Tags []string `json:"tags"` + Size int `json:"size"` + Filename string `json:"filename"` + Desc string `json:"desc"` + FieldNameFs string `json:"fieldNameFs"` + CoverImg []string `json:"coverImg"` + Via string `json:"via"` + Width int `json:"width"` + Height int `json:"height"` + Status string `json:"status"` + Ratio float64 `json:"ratio"` +} + +// AwsPullSeriesResp 从AWS拉取套图响应体 +type AwsPullSeriesResp struct { + ID string `json:"id"` + Type string `json:"type"` //套图类型 SP 泡芙短视频套图 AV 泡芙长视频套图 + Title string `json:"title"` // 标题 + Tags []string `json:"tags"` // 标签 + Desc string `json:"desc"` // 详细描述 + CoverImg string `json:"coverImg"` // 封面 + SeriesCover []string `json:"seriesCover"` // 套图 + Number int64 `json:"number"` // 图片数量 + Status string `json:"status"` // 当前状态 + MimeType string `json:"mimeType"` // mime文件类型 + Via string `json:"via"` // 来源 + NewUpdateAt string `json:"newUpdateAt"` // q1需要要加 +} + +// 从server-file 返回的发布者信息 +type Publiser struct { + UID string `json:"uid" bson:"uid" binding:"required"` //用户id + UserName string `json:"name" bson:"name"` //昵称 + Portrait string `json:"portrait" bson:"portrait"` //头像 + Summary string `json:"summary" bson:"summary"` //简介 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +// AwsResport 上传 +type PublishResport struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data Publiser `json:"data"` +} + +type Resport struct { + Code stderr.Code `json:"code"` + Msg string `json:"msg"` + Data []AwsPullResp `json:"data"` +} + +type PuFinfoResp struct { + Code stderr.Code `json:"code"` + Msg string `json:"msg"` + Data AwsPullResp `json:"data"` +} + +// 套图响应结果 +type SeriesResport struct { + Code stderr.Code `json:"code"` + Msg string `json:"msg"` + Data []AwsPullSeriesResp `json:"data"` +} + +// OperateResult 更新或者删除的操作返回结果 +type OperateResult struct { + Count int64 `json:"count"` + MergingCnt int64 `json:"mergingCnt"` //合并中的视频数 + ConvertingCnt int64 `json:"convertingCnt"` //转码中的视频数 + MerErrCnt int64 `json:"merErrCnt"` //合并失败视频 + ConvertErrCnt int64 `json:"convertErrCnt"` //转码失败视频数 +} + +// WebSubmitReq 视频发布请求 +type WenUploadRes struct { + PlayTime uint `json:"playTime" binding:"required"` + Size int `json:"size"` + Resolution string `json:"resolution"` + MimeType string `json:"mimeType"` + SourceID string `json:"sourceID" binding:"required"` + SourceURL string `json:"sourceURL" binding:"required"` + MD5 string `json:"md5"` + Filename string `json:"filename"` +} + +// AwsResport 上传 +type AwsResport struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data AwsUploadResp `json:"data"` +} + +// AwsUploadResp AwsUploadResp +type AwsUploadResp struct { + VID string `json:"id"` + VideoURI string `json:"videoUri"` +} + +type FsSendSingleResp struct { + Code stderr.Code `json:"code"` + Msg string `json:"msg"` + Data FsSendSingleData `json:"data"` +} + +// FsSendSingleData FsSendSingleData +type FsSendSingleData struct { + Domain string `json:"domain"` + FileName string `json:"fileName"` +} + +// FsSendSingleBatchResp FsSendSingleBatchResp +type FsSendBatchResp struct { + Code stderr.Code `json:"code"` + Msg string `json:"msg"` + Data FsSendBatchData `json:"data"` +} + +// FsSendSingleData FsSendSingleData +type FsSendBatchData struct { + Batch []*FsSendSingleData `json:"batch"` +} + +func (this *FsSendBatchData) GetFileNames() []string { + names := []string{} + for _, v := range this.Batch { + names = append(names, v.FileName) + } + return names +} + +func (this *FsSendBatchData) GetDomains() []string { + domains := []string{} + for _, v := range this.Batch { + domains = append(domains, v.Domain) + } + return domains +} + +func (this *FsSendBatchData) Count() int { + return len(this.Batch) +} + +// UpserVideoInfo 用于upsert video的 结构体 +type UpserVideoInfo struct { + Title string `json:"title" bson:"title,omitempty"` //视频标题 + Tags []primitive.ObjectID `json:"tags" bson:"tags,omitempty"` //视频标签 + SourceID string `json:"sourceID" bson:"sourceID,omitempty"` //视频在仓库中的资源ID + SourceURL string `json:"sourceURL" bson:"sourceURL,omitempty"` //视频资源地址Path + MimeType string `json:"mimeType" bson:"mimeType,omitempty"` //视频格式类型 + Filename string `json:"fileName" bson:"fileName,omitempty"` //文件名称 + PlayTime uint `json:"playTime" bson:"playTime,omitempty"` //影片长度 + Cover string `json:"cover" bson:"cover,omitempty"` //封面大图 + CoverThumb string `json:"coverThumb" bson:"coverThumb,omitempty"` //封⾯小图 + SeriesCover []string `json:"seriesCover" bson:"seriesCover,omitempty"` //封面套图 + Via string `json:"via" bson:"via,omitempty"` //来源 自拍,上传 + Size int `json:"size" bson:"size,omitempty"` //文件大小 byte + Resolution string `json:"resolution" bson:"resolution,omitempty"` //分辨率 + Ratio float64 `json:"ratio" bson:"ratio,omitempty"` //宽高比 + MD5 string `json:"md5" bson:"md5,omitempty"` //文件摘要 + Actor string `json:"actor" bson:"actor,omitempty"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //刷新时间 +} + +// TitleTime 视频标题时长数据 +type TitleTime struct { + Title string `json:"title"` + PlayTime uint `json:"playTime"` +} + +// WebVideoUpdateDoc 用于审核视频 同步信息 +type WebVideoUpdateDoc struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Cover *string `json:"cover,omitempty" bson:"cover,omitempty"` + CoverThumb *string `json:"coverThumb,omitempty" bson:"coverThumb,omitempty"` + SourceID *string `json:"sourceID,omitempty" bson:"sourceID,omitempty"` + PlayTime *uint `json:"playTime,omitempty" bson:"playTime,omitempty"` + FreeTime *int `json:"freeTime,omitempty" bson:"freeTime,omitempty"` + SeriesCover *[]string `json:"seriesCover,omitempty" bson:"seriesCover,omitempty"` // 封面套图 + VideoCover *[]string `json:"videoCover,omitempty" bson:"videoCover,omitempty"` // 视频截图 + Resolution *string `json:"resolution,omitempty" bson:"resolution,omitempty"` + Width *int `json:"width,omitempty" bson:"width,omitempty"` //视频宽度 + Height *int `json:"height,omitempty" bson:"height,omitempty"` //视频高度 + MD5 *string `json:"md5,omitempty" bson:"md5,omitempty" ` //文件摘要 + Actor *string `json:"actor,omitempty" bson:"actor,omitempty"` + Size *int `json:"size,omitempty" bson:"size,omitempty"` + Filename *string `json:"fileName,omitempty" bson:"fileName,omitempty"` + Via *string `json:"via,omitempty" bson:"via,omitempty"` + Ratio *float64 `json:"ratio,omitempty" bson:"ratio,omitempty"` + Quality *string `json:"quality,omitempty" bson:"quality,omitempty"` //视频质量 720P以上高质量-high 480-720P 中等质量-middle 其他为low + Direction *string `json:"direction,omitempty" bson:"direction,omitempty"` //视频版式 vertical-竖屏 horizontal-横屏 square-方屏 +} + +// SyncServerFileEdit 用户同步从server-file获取的基本信息 强制更新 +type WebSyncServerFileEdit struct { + PlayTime uint `json:"playTime" bson:"playTime"` //播放时长 + Size int `json:"size" bson:"size"` //文件大小 byte + Resolution string `json:"resolution" bson:"resolution"` //分辨率 + Ratio float64 `json:"ratio" bson:"ratio"` //宽高比 + MD5 string `json:"md5" bson:"md5"` //文件摘要 +} + +type BatchUpdateRecoRequest struct { + IDs []primitive.ObjectID `json:"ids"` // 帖子ID列表 + Reco bool `json:"reco"` // 是否推荐 +} + +type WebElasticSearchRequest struct { + Keyword string `json:"keyword"` // 关键词 + commod.Page +} + +type WebElasticSearchResponse struct { + List []ESVideo `json:"list"` // list + HasNext bool `json:"hasNext"` // hasNext + Total int `json:"total"` // total +} + +// UploadMediaVideo 上传到媒资库的基础信息请求体 +type UploadMediaVideo struct { + FsResourceId string `json:"fs_resource_id"` // 文件资源id + HashId string `json:"hash_id"` // 视频唯一id + Title string `json:"title"` // 视频标题 + CoverImage string `json:"cover_image"` // 封面图 + M3u8Src string `json:"m3u8_src"` // m3u8地址 + FileSize int `json:"file_size"` // 视频大小 + Length int `json:"length"` // 视频时长 秒 + Width int `json:"width"` // 宽度 + Height int `json:"height"` // 高度 + TagsText string `json:"tags_text"` // 视频标签,多个以逗号分割 +} + +// BatchUpdateVidNewsTypeReq 批量更帖子类型参数 +type BatchUpdateVidNewsTypeReq struct { + Ids []primitive.ObjectID `form:"ids" json:"ids" bson:"ids"` // 帖子Id + NewsType string `form:"newsType" json:"newsType" bson:"newsType,omitempty"` // 帖子类型 +} + +// EditVideTagsReq 批量更视频参数 +type EditVideTagsReq struct { + Ids []string `form:"ids" json:"ids" bson:"ids"` // 视频ID + Tags []string `form:"tags" json:"tags" bson:"tags,omitempty"` // 标签 + Overwrite bool `form:"overwrite" json:"overwrite"` // 是否覆盖 +} diff --git a/models/v/vidpopmod/type.go b/models/v/vidpopmod/type.go new file mode 100644 index 0000000..9abfa0c --- /dev/null +++ b/models/v/vidpopmod/type.go @@ -0,0 +1,42 @@ +package vidpopmod + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const table = models.VideoPopularityConfig + +var mdb *db.MongoDB + +type VideoPopularityConfig struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID + InitialPopularity int `json:"initalPop" bson:"initalPop"` // 初始热度值 + PlayTimePercentage []PlayTimePercentage `json:"playTimePercentage" bson:"playTimePercentage"` // 会员视频有效播放量的百分比 + ReviewTimePower float64 `json:"reviewTimePower" bson:"reviewTimePower"` // 过审时长幂 + EffectivePlayCountMultiplier int `json:"effectivePlayCountMultiplier" bson:"effectivePlayCountMultiplier"` // 有效播放量放大因子 + LikeCountMultiplier int `json:"LikeCountMultiplier" bson:"LikeCountMultiplier"` // 点赞量放大因子 + IsActive bool `json:"isActive" bson:"isActive"` // 是否开启IsActive + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 +} + +type PlayTimePercentage struct { + Min int `json:"min" bson:"min"` // 视频时长下限 + Max int `json:"max" bson:"max"` // 视频时长上限 + Percentage int `json:"percentage" bson:"percentage"` // 有效播放时长比例 +} + +func Init() { + mdb = db.Init(table) +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} diff --git a/models/v/vidpopmod/vidpop.go b/models/v/vidpopmod/vidpop.go new file mode 100644 index 0000000..23f215f --- /dev/null +++ b/models/v/vidpopmod/vidpop.go @@ -0,0 +1,65 @@ +package vidpopmod + +import ( + "encoding/json" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + + "go.mongodb.org/mongo-driver/bson" +) + +func InsertOne(cfg VideoPopularityConfig) error { + if _, err := coll(nil).UpdateMany(bson.M{"isActive": true}, bson.M{"$set": bson.M{"isActive": false}}); err != nil { + return err + } + cfg.CreatedAt = time.Now() + cfg.IsActive = true + _, err := coll(nil).InsertOne(cfg) + return err +} + +func FindOne() (VideoPopularityConfig, error) { + cfg := VideoPopularityConfig{} + return cfg, coll(nil).FindOne(&cfg, bson.M{"isActive": true}) +} + +func FindVidPopCfg() (VideoPopularityConfig, error) { + cfg := VideoPopularityConfig{} + //先从redis拿数据 + redisKey := table + str, err := appg.Redis.Get(redisKey) + if err != nil { // redis 错误不向上报告 + log.Error("FindVidPopCfg redisc.Get", log.Any("redisKey", redisKey), log.E(err)) + } + if str != nil { + err = json.Unmarshal([]byte(*str), &cfg) + if err == nil { + return cfg, nil + } else { // json.Unmarshal的错误不向上报告,而是尝试去DB获取用户 + log.Error("FindVidPopCfg json.Unmarshal", log.Any("redisKey", redisKey), log.E(err)) + } + } + //没有数据, 查数据库 + if err = coll(nil).FindOne(&cfg, bson.M{"isActive": true}); err != nil { + return cfg, err + } + if cfg.ID.IsZero() { + return cfg, nil + } + //结果存redis + common.Go(func() { + jsonBytes, err := json.Marshal(cfg) + if err != nil { + log.Error("FindVidPopCfg json.Marshal", log.E(err)) + return + } + if err := appg.Redis.Set(redisKey, string(jsonBytes), redisconst.DataCachExpire); err != nil { + log.Error("FindVidPopCfg redisc.Set", log.Any("redisKey", redisKey), log.E(err)) + } + }) + return cfg, nil +} diff --git a/models/v/vidtimeonlinemod/type.go b/models/v/vidtimeonlinemod/type.go new file mode 100644 index 0000000..e33bca3 --- /dev/null +++ b/models/v/vidtimeonlinemod/type.go @@ -0,0 +1,32 @@ +package vidtimeonlinemod + +import ( + "91porn-server/common/db" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// VidTimeOnlineModel 视频 +type VidTimeOnlineModel struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // id + PublisherID uint64 `json:"publisherID" bson:"publisherID"` // 上传者ID + Portrait string `json:"portrait" bson:"portrait"` // 上传者头像 + VidId primitive.ObjectID `json:"vidId" bson:"vidId"` // 帖子id + SectionId primitive.ObjectID `json:"sectionId" bson:"sectionId"` // 专题id + VidType string `json:"vidType" bson:"vidType"` // 帖子类型 + VidTitle string `json:"vidTitle" bson:"vidTitle"` // 视频标题 + OnlineSort int `json:"onlineSort" bson:"onlineSort"` // 上线顺序 + OnlineTime time.Time `json:"onlineTime" bson:"onlineTime"` // 定时上线时间 + SourceURL string `json:"sourceURL" bson:"sourceURL"` // 视频资源地址Path + Cover string `json:"cover" bson:"cover"` // 封面图 + Tags []primitive.ObjectID `json:"tags" bson:"tags"` // 视频标签 + SeriesCover []string `json:"seriesCover" bson:"seriesCover"` // 帖子套图 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` // 创建时间 + ReviewAccount string `json:"reviewAccount" bson:"reviewAccount,omitempty"` // 审核人 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/vidtimeonlinemod/vidtimeonline.go b/models/v/vidtimeonlinemod/vidtimeonline.go new file mode 100644 index 0000000..196189c --- /dev/null +++ b/models/v/vidtimeonlinemod/vidtimeonline.go @@ -0,0 +1,147 @@ +package vidtimeonlinemod + +import ( + "91porn-server/common/db" + "91porn-server/models" + "91porn-server/models/commod" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "time" + + "go.mongodb.org/mongo-driver/mongo" +) + +var mdb *db.MongoDB + +const table = models.VidTimeOnline + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// initIndex 初始化索引 +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{"onlineTime", -1}}, + }, + } + _, err := coll(nil).CreateIndex(many) + if err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +// GetAllByPage 分页获取列表 +func GetAllByPage(cond bson.M, page commod.Page) ([]VidTimeOnlineModel, int64, error) { + skip := int64(page.Skip()) + limit := int64(page.Limit()) + opt := &options.FindOptions{} + opt.SetSkip(skip) + opt.SetLimit(limit) + vidTimeOnlineList := make([]VidTimeOnlineModel, 0) + err := coll(nil).Find(&vidTimeOnlineList, cond, opt) + if err != nil { + return nil, 0, err + } + total, err := coll(nil).Count(bson.M{}) + if err != nil { + return nil, 0, err + } + return vidTimeOnlineList, total, nil +} + +// GetByPage 获取可以执行的列表 +func GetByPage(page commod.Page) ([]VidTimeOnlineModel, error) { + skip := int64(page.Skip()) + limit := int64(page.Limit()) + sort := bson.D{bson.E{"onlineTime", 1}} + opt := &options.FindOptions{} + opt.SetSkip(skip).SetLimit(limit).SetSort(sort) + vidTimeOnlineList := make([]VidTimeOnlineModel, 0) + err := coll(nil).Find(&vidTimeOnlineList, bson.M{ + "onlineTime": bson.M{ + "$lte": time.Now(), + }, + }, opt) + if err != nil { + return nil, err + } + return vidTimeOnlineList, nil +} + +// GetByVidIds 根据vid获取列表 +func GetByVidIds(vidIds []primitive.ObjectID) ([]VidTimeOnlineModel, error) { + vidTimeOnlineList := make([]VidTimeOnlineModel, 0) + err := coll(nil).Find(&vidTimeOnlineList, bson.M{"vidId": bson.M{"$in": vidIds}}) + if err != nil { + return nil, err + } + return vidTimeOnlineList, nil +} + +// GetByVidId 根据id获取 +func GetByVidId(vid primitive.ObjectID) (VidTimeOnlineModel, error) { + var res VidTimeOnlineModel + err := coll(nil).FindOne(&res, bson.M{"vidId": vid}) + return res, err +} + +// DeleteByIds 删除 +func DeleteByIds(ids []primitive.ObjectID) error { + _, err := coll(nil).DeleteMany(bson.M{"_id": bson.M{"$in": ids}}) + if err != nil { + return err + } + return nil +} + +// DeleteByVids 根据帖子id删除数据 +func DeleteByVids(vids []primitive.ObjectID) error { + _, err := coll(nil).DeleteMany(bson.M{"vidId": bson.M{"$in": vids}}) + if err != nil { + return err + } + return nil +} + +// Add 新增 +func Add(prams VidTimeOnlineModel) error { + _, err := coll(nil).InsertOne(prams) + if err != nil { + return err + } + return nil +} + +// InsertMany 批量新增 +func InsertMany(records []VidTimeOnlineModel) error { + opts := options.InsertMany().SetOrdered(false) + _, err := coll(nil).InsertMany(records, opts) + if !db.IsMongoDupKey(err) { + return err + } + return nil +} + +// UpdateMany 通过帖子id修改定时上架信息 +func UpdateMany(vids []primitive.ObjectID, update bson.M) (err error) { + if len(vids) == 0 { + return + } + _, err = coll(nil).UpdateMany( + bson.M{"vidId": bson.M{"$in": vids}}, + bson.M{"$set": update}, + ) + return +} + +func UpdateById(id primitive.ObjectID, update bson.M) (err error) { + _, err = coll(nil).UpdateOne(bson.M{"_id": id}, update) + return +} diff --git a/models/v/vidtimeonlinemod/web.go b/models/v/vidtimeonlinemod/web.go new file mode 100644 index 0000000..3058b88 --- /dev/null +++ b/models/v/vidtimeonlinemod/web.go @@ -0,0 +1,55 @@ +package vidtimeonlinemod + +import ( + "91porn-server/models/commod" + "91porn-server/models/v/vidmod" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +// ListReq 视频列表请求 +type ListReq struct { + commod.Page + UID int `form:"uid" json:"uid"` // 用户ID + VidId string `form:"vidId" bson:"vidId"` // 帖子ID + Tag *string `form:"tag" json:"tag" bson:"tag"` // 标题 +} + +// DeleteReq 视频批量操作参数 +type DeleteReq struct { + IDs []string `json:"ids"` +} + +// BatchAuditReq 批量自动上架参数 +type BatchAuditReq struct { + Vids []primitive.ObjectID `json:"vids" form:"vids" binding:"required"` // 自上架数据 + SectionID primitive.ObjectID `json:"sectionID" form:"sectionID"` // 专题id + OnlineTime time.Time `json:"onlineTime,omitempty" form:"onlineTime"` // 定时上线的时间 +} + +// VidTimeOnlineModelResp 视频 +type VidTimeOnlineModelResp struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // id + PublisherID uint64 `json:"publisherID" bson:"publisherID"` // 上传者ID + Portrait string `json:"portrait" bson:"portrait"` // 上传者头像 + VidId primitive.ObjectID `json:"vidId" bson:"vidId"` // 帖子id + VidType string `json:"vidType" bson:"vidType"` // 帖子类型 + VidTitle string `json:"vidTitle" bson:"vidTitle"` // 视频标题 + OnlineSort int `json:"onlineSort" bson:"onlineSort"` // 上线顺序 + SourceURL string `json:"sourceURL" bson:"sourceURL"` // 视频资源地址Path + Cover string `json:"cover" bson:"cover"` // 封面图 + Tags []vidmod.TagInfo `json:"tags" bson:"tags"` // 视频标签 + SeriesCover []string `json:"seriesCover" bson:"seriesCover"` // 帖子套图 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` // 创建时间 + OnlineTime time.Time `json:"onlineTime" bson:"onlineTime,omitempty"` // 上架时间 + ReviewAccount string `json:"reviewAccount" bson:"reviewAccount,omitempty"` // 审核人 + SectionId primitive.ObjectID `json:"sectionID"` +} + +// EditReq 更新参数 +type EditReq struct { + VidId primitive.ObjectID `json:"vidId" form:"vidId" binding:"required"` // 帖子id + Price int `json:"price" form:"price"` // 价格 + SectionID primitive.ObjectID `json:"sectionID" form:"sectionID"` // 专题id + OnlineTime time.Time `json:"onlineTime,omitempty" form:"onlineTime"` // 定时上线的时间 +} diff --git a/models/v/vipcardexperimentmod/model.go b/models/v/vipcardexperimentmod/model.go new file mode 100644 index 0000000..1ccb8fb --- /dev/null +++ b/models/v/vipcardexperimentmod/model.go @@ -0,0 +1,283 @@ +package vipcardexperimentmod + +import ( + "fmt" + "strings" + "time" + + "91porn-server/common/db" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mdb *db.MongoDB + +func Init() { + mdb = db.Init(models.VIPCardExperiment) + initIndexes() +} + +func experimentColl() *db.MongoTool { + return mdb.Coll(models.VIPCardExperiment) +} + +func eventColl() *db.MongoTool { + return mdb.Coll(models.VIPCardAnalyticsEvent) +} + +func initIndexes() { + if _, err := experimentColl().CreateIndex([]mongo.IndexModel{ + { + Keys: bson.D{{Key: "experimentId", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "activeSlot", Value: 1}}, + Options: options.Index(). + SetName("uniq_vip_card_active_slot"). + SetUnique(true). + SetPartialFilterExpression(bson.M{"activeSlot": bson.M{"$gt": ""}}), + }, + { + Keys: bson.D{ + {Key: "status", Value: 1}, + {Key: "startAt", Value: 1}, + {Key: "endAt", Value: 1}, + {Key: "publishedAt", Value: -1}, + }, + }, + }); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", models.VIPCardExperiment, err)) + } + if _, err := eventColl().CreateIndex([]mongo.IndexModel{ + { + Keys: bson.D{{Key: "eventId", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{ + {Key: "experimentId", Value: 1}, + {Key: "variant", Value: 1}, + {Key: "eventName", Value: 1}, + {Key: "occurredAt", Value: 1}, + }, + }, + { + Keys: bson.D{ + {Key: "experimentId", Value: 1}, + {Key: "productId", Value: 1}, + {Key: "eventName", Value: 1}, + }, + }, + }); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", models.VIPCardAnalyticsEvent, err)) + } +} + +func Publish(experiment *Experiment, operator string) error { + experiment.Normalize() + if err := experiment.Validate(); err != nil { + return err + } + now := time.Now() + experiment.ID = primitive.NewObjectID() + experiment.Status = StatusActive + experiment.PublishedAt = now + experiment.CreatedAt = now + experiment.UpdatedAt = now + experiment.CreatedBy = operator + experiment.DisabledAt = nil + experiment.DisabledBy = "" + experiment.ActiveSlot = activeSlot + + return mdb.Trans(func(t *db.MongoTool) error { + coll := t.Coll(models.VIPCardExperiment) + if _, err := coll.UpdateMany( + bson.M{"status": StatusActive}, + bson.M{ + "$set": bson.M{ + "status": StatusDisabled, + "disabledAt": now, + "disabledBy": operator, + "updatedAt": now, + }, + "$unset": bson.M{"activeSlot": ""}, + }, + ); err != nil { + return err + } + _, err := coll.InsertOne(experiment) + return err + }) +} + +func Current(now time.Time) (*Experiment, error) { + filter := bson.M{ + "status": StatusActive, + "$and": bson.A{ + bson.M{"$or": bson.A{ + bson.M{"startAt": bson.M{"$exists": false}}, + bson.M{"startAt": nil}, + bson.M{"startAt": bson.M{"$lte": now}}, + }}, + bson.M{"$or": bson.A{ + bson.M{"endAt": bson.M{"$exists": false}}, + bson.M{"endAt": nil}, + bson.M{"endAt": bson.M{"$gt": now}}, + }}, + }, + } + var out Experiment + if err := experimentColl().FindOne( + &out, + filter, + options.FindOne().SetSort(bson.D{{Key: "publishedAt", Value: -1}, {Key: "_id", Value: -1}}), + ); err != nil { + return nil, err + } + if out.ID.IsZero() { + return nil, nil + } + return &out, nil +} + +func FindByExperimentID(experimentID string) (*Experiment, error) { + var out Experiment + if err := experimentColl().FindOne(&out, bson.M{"experimentId": experimentID}); err != nil { + return nil, err + } + if out.ID.IsZero() { + return nil, nil + } + return &out, nil +} + +func Disable(experimentID, operator string) (bool, error) { + now := time.Now() + result, err := experimentColl().UpdateMany( + bson.M{"experimentId": experimentID, "status": StatusActive}, + bson.M{ + "$set": bson.M{ + "status": StatusDisabled, + "disabledAt": now, + "disabledBy": operator, + "updatedAt": now, + }, + "$unset": bson.M{"activeSlot": ""}, + }, + ) + if err != nil { + return false, err + } + return result.ModifiedCount > 0, nil +} + +func InsertEvent(event *AnalyticsEvent) (bool, error) { + event.EventID = strings.TrimSpace(event.EventID) + event.EventName = strings.ToUpper(strings.TrimSpace(event.EventName)) + event.SessionID = strings.TrimSpace(event.SessionID) + event.ExperimentID = strings.TrimSpace(event.ExperimentID) + event.Variant = strings.ToUpper(strings.TrimSpace(event.Variant)) + event.ID = primitive.NewObjectID() + event.ReceivedAt = time.Now() + result, err := eventColl().UpsertOne( + bson.M{"eventId": event.EventID}, + bson.M{"$setOnInsert": event}, + ) + if err != nil { + return false, err + } + return result.UpsertedCount > 0, nil +} + +func EventStatistics(experimentID string) ([]EventStat, []ProductImpressionStat, error) { + var eventRaw []struct { + ID struct { + Variant string `bson:"variant"` + EventName string `bson:"eventName"` + } `bson:"_id"` + People int `bson:"people"` + Times int `bson:"times"` + } + err := eventColl().Aggregate(&eventRaw, []bson.M{ + {"$match": bson.M{"experimentId": experimentID}}, + {"$group": bson.M{ + "_id": bson.M{ + "variant": "$variant", + "eventName": "$eventName", + "uid": "$uid", + }, + "userTimes": bson.M{"$sum": 1}, + }}, + {"$group": bson.M{ + "_id": bson.M{ + "variant": "$_id.variant", + "eventName": "$_id.eventName", + }, + "people": bson.M{"$sum": 1}, + "times": bson.M{"$sum": "$userTimes"}, + }}, + {"$sort": bson.D{{Key: "_id.variant", Value: 1}, {Key: "_id.eventName", Value: 1}}}, + }) + if err != nil { + return nil, nil, err + } + eventStats := make([]EventStat, 0, len(eventRaw)) + for _, item := range eventRaw { + eventStats = append(eventStats, EventStat{ + Variant: item.ID.Variant, + EventName: item.ID.EventName, + People: item.People, + Times: item.Times, + }) + } + + var productRaw []struct { + ID struct { + Variant string `bson:"variant"` + ProductID primitive.ObjectID `bson:"productId"` + } `bson:"_id"` + People int `bson:"people"` + Times int `bson:"times"` + } + err = eventColl().Aggregate(&productRaw, []bson.M{ + {"$match": bson.M{ + "experimentId": experimentID, + "eventName": EventProductImpression, + }}, + {"$group": bson.M{ + "_id": bson.M{ + "variant": "$variant", + "productId": "$productId", + "uid": "$uid", + }, + "userTimes": bson.M{"$sum": 1}, + }}, + {"$group": bson.M{ + "_id": bson.M{ + "variant": "$_id.variant", + "productId": "$_id.productId", + }, + "people": bson.M{"$sum": 1}, + "times": bson.M{"$sum": "$userTimes"}, + }}, + {"$sort": bson.D{{Key: "_id.variant", Value: 1}, {Key: "times", Value: -1}}}, + }) + if err != nil { + return nil, nil, err + } + productStats := make([]ProductImpressionStat, 0, len(productRaw)) + for _, item := range productRaw { + productStats = append(productStats, ProductImpressionStat{ + Variant: item.ID.Variant, + ProductID: item.ID.ProductID, + People: item.People, + Times: item.Times, + }) + } + return eventStats, productStats, nil +} diff --git a/models/v/vipcardexperimentmod/product_ids.go b/models/v/vipcardexperimentmod/product_ids.go new file mode 100644 index 0000000..d321944 --- /dev/null +++ b/models/v/vipcardexperimentmod/product_ids.go @@ -0,0 +1,163 @@ +package vipcardexperimentmod + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/bsontype" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// OrderedProductIDs exposes productIds as an order-number-to-product-ID map +// while keeping a slice internally so every backend consumer has deterministic +// numeric order. BSON is stored as an embedded document with keys 1..n. +// Legacy JSON/BSON arrays remain readable for existing experiment records. +type OrderedProductIDs []primitive.ObjectID + +func (ids OrderedProductIDs) MarshalJSON() ([]byte, error) { + var buffer bytes.Buffer + buffer.WriteByte('{') + for index, productID := range ids { + if index > 0 { + buffer.WriteByte(',') + } + key, _ := json.Marshal(strconv.Itoa(index + 1)) + value, err := json.Marshal(productID) + if err != nil { + return nil, err + } + buffer.Write(key) + buffer.WriteByte(':') + buffer.Write(value) + } + buffer.WriteByte('}') + return buffer.Bytes(), nil +} + +func (ids *OrderedProductIDs) UnmarshalJSON(data []byte) error { + data = bytes.TrimSpace(data) + if len(data) == 0 { + return fmt.Errorf("productIds is required") + } + if bytes.Equal(data, []byte("null")) { + *ids = nil + return nil + } + switch data[0] { + case '[': + var legacy []primitive.ObjectID + if err := json.Unmarshal(data, &legacy); err != nil { + return fmt.Errorf("invalid legacy productIds: %w", err) + } + *ids = legacy + return nil + case '{': + var values map[string]json.RawMessage + if err := json.Unmarshal(data, &values); err != nil { + return fmt.Errorf("invalid productIds map: %w", err) + } + ordered := make(OrderedProductIDs, len(values)) + occupied := make([]bool, len(values)) + seenProducts := make(map[primitive.ObjectID]struct{}, len(values)) + for key, rawProductID := range values { + index, err := parseProductOrder(key, len(values)) + if err != nil { + return err + } + var productID primitive.ObjectID + if err = json.Unmarshal(rawProductID, &productID); err != nil || productID.IsZero() { + return fmt.Errorf("productIds.%s must be a valid product ID", key) + } + if occupied[index] { + return fmt.Errorf("productIds order %s is duplicated", key) + } + if _, exists := seenProducts[productID]; exists { + return fmt.Errorf("productIds contains duplicate product ID: %s", productID.Hex()) + } + occupied[index] = true + seenProducts[productID] = struct{}{} + ordered[index] = productID + } + *ids = ordered + return nil + default: + return fmt.Errorf("productIds must be an order map") + } +} + +func (ids OrderedProductIDs) MarshalBSONValue() (bsontype.Type, []byte, error) { + document := make(bson.D, 0, len(ids)) + for index, productID := range ids { + document = append(document, bson.E{ + Key: strconv.Itoa(index + 1), + Value: productID, + }) + } + return bson.MarshalValue(document) +} + +func (ids *OrderedProductIDs) UnmarshalBSONValue(valueType bsontype.Type, data []byte) error { + rawValue := bson.RawValue{Type: valueType, Value: data} + switch valueType { + case bsontype.Array: + var legacy []primitive.ObjectID + if err := rawValue.Unmarshal(&legacy); err != nil { + return fmt.Errorf("invalid legacy productIds: %w", err) + } + *ids = legacy + return nil + case bsontype.EmbeddedDocument: + var document bson.Raw + if err := rawValue.Unmarshal(&document); err != nil { + return fmt.Errorf("invalid productIds document: %w", err) + } + elements, err := document.Elements() + if err != nil { + return fmt.Errorf("invalid productIds document: %w", err) + } + ordered := make(OrderedProductIDs, len(elements)) + occupied := make([]bool, len(elements)) + seenProducts := make(map[primitive.ObjectID]struct{}, len(elements)) + for _, element := range elements { + key := element.Key() + index, parseErr := parseProductOrder(key, len(elements)) + if parseErr != nil { + return parseErr + } + productID, ok := element.Value().ObjectIDOK() + if !ok || productID.IsZero() { + return fmt.Errorf("productIds.%s must be a valid product ID", key) + } + if occupied[index] { + return fmt.Errorf("productIds order %s is duplicated", key) + } + if _, exists := seenProducts[productID]; exists { + return fmt.Errorf("productIds contains duplicate product ID: %s", productID.Hex()) + } + occupied[index] = true + seenProducts[productID] = struct{}{} + ordered[index] = productID + } + *ids = ordered + return nil + case bsontype.Null, bsontype.Undefined: + *ids = nil + return nil + default: + return fmt.Errorf("productIds must be an ordered document") + } +} + +func parseProductOrder(key string, size int) (int, error) { + order, err := strconv.Atoi(key) + if err != nil || order <= 0 || strconv.Itoa(order) != key { + return 0, fmt.Errorf("productIds order must be a positive integer: %s", key) + } + if order > size { + return 0, fmt.Errorf("productIds order must be continuous from 1") + } + return order - 1, nil +} diff --git a/models/v/vipcardexperimentmod/type.go b/models/v/vipcardexperimentmod/type.go new file mode 100644 index 0000000..17ec596 --- /dev/null +++ b/models/v/vipcardexperimentmod/type.go @@ -0,0 +1,294 @@ +package vipcardexperimentmod + +import ( + "fmt" + "hash/fnv" + "regexp" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + StatusActive = "ACTIVE" + StatusDisabled = "DISABLED" + activeSlot = "VIP_CARD_CURRENT" + + VariantA = "A" + VariantB = "B" + + BadgeMostPopular = "MOST_POPULAR" + BadgeNewUser = "NEW_USER_OFFER" + + EventCardPageView = "VIP_CARD_PAGE_VIEW" + EventProductImpression = "VIP_PRODUCT_IMPRESSION" + EventCloseWithoutPay = "VIP_CARD_CLOSE_WITHOUT_PURCHASE" +) + +var validBadgeTypes = map[string]struct{}{ + BadgeMostPopular: {}, + BadgeNewUser: {}, +} + +var validEventNames = map[string]struct{}{ + EventCardPageView: {}, + EventProductImpression: {}, + EventCloseWithoutPay: {}, +} + +var hexColorPattern = regexp.MustCompile(`^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$`) + +type ProductBadge struct { + ProductID primitive.ObjectID `json:"productId" bson:"productId"` + BadgeType string `json:"badgeType" bson:"badgeType"` + BadgeText string `json:"badgeText" bson:"badgeText"` +} + +type BadgeStyle struct { + BadgeType string `json:"badgeType" bson:"badgeType"` + BackgroundColor string `json:"backgroundColor" bson:"backgroundColor"` + TextColor string `json:"textColor" bson:"textColor"` +} + +type UIConfig struct { + BackgroundImage string `json:"backgroundImage" bson:"backgroundImage"` + BadgeStyles []BadgeStyle `json:"badgeStyles" bson:"badgeStyles"` +} + +type VariantConfig struct { + Name string `json:"name" bson:"name"` + SkinKey string `json:"skinKey" bson:"skinKey"` + DefaultProductID primitive.ObjectID `json:"defaultProductId,omitempty" bson:"defaultProductId,omitempty"` + ProductIDs OrderedProductIDs `json:"productIds" bson:"productIds"` + UIConfig *UIConfig `json:"uiConfig,omitempty" bson:"uiConfig,omitempty"` + ProductBadges []ProductBadge `json:"productBadges" bson:"productBadges"` +} + +type Experiment struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + ExperimentID string `json:"experimentId" bson:"experimentId"` + Name string `json:"name" bson:"name"` + Status string `json:"status" bson:"status"` + TrafficA int `json:"trafficA" bson:"trafficA"` + TrafficB int `json:"trafficB" bson:"trafficB"` + VariantA VariantConfig `json:"variantA" bson:"variantA"` + VariantB VariantConfig `json:"variantB" bson:"variantB"` + StartAt *time.Time `json:"startAt,omitempty" bson:"startAt,omitempty"` + EndAt *time.Time `json:"endAt,omitempty" bson:"endAt,omitempty"` + PublishedAt time.Time `json:"publishedAt" bson:"publishedAt"` + DisabledAt *time.Time `json:"disabledAt,omitempty" bson:"disabledAt,omitempty"` + CreatedBy string `json:"createdBy" bson:"createdBy"` + DisabledBy string `json:"disabledBy,omitempty" bson:"disabledBy,omitempty"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` + ActiveSlot string `json:"-" bson:"activeSlot,omitempty"` +} + +type AnalyticsEvent struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + EventID string `json:"eventId" bson:"eventId"` + EventName string `json:"eventName" bson:"eventName"` + UID uint64 `json:"uid" bson:"uid"` + SessionID string `json:"sessionId" bson:"sessionId"` + OccurredAt time.Time `json:"occurredAt" bson:"occurredAt"` + ExperimentID string `json:"experimentId" bson:"experimentId"` + Variant string `json:"variant" bson:"variant"` + ProductID primitive.ObjectID `json:"productId,omitempty" bson:"productId,omitempty"` + ReceivedAt time.Time `json:"receivedAt" bson:"receivedAt"` +} + +type EventStat struct { + Variant string `json:"variant" bson:"variant"` + EventName string `json:"eventName" bson:"eventName"` + People int `json:"people" bson:"people"` + Times int `json:"times" bson:"times"` +} + +type ProductImpressionStat struct { + Variant string `json:"variant" bson:"variant"` + ProductID primitive.ObjectID `json:"productId" bson:"productId"` + People int `json:"people" bson:"people"` + Times int `json:"times" bson:"times"` +} + +func (c *UIConfig) Normalize() { + if c == nil { + return + } + c.BackgroundImage = strings.TrimSpace(c.BackgroundImage) + if c.BadgeStyles == nil { + c.BadgeStyles = make([]BadgeStyle, 0) + return + } + for i := range c.BadgeStyles { + c.BadgeStyles[i].BadgeType = strings.ToUpper(strings.TrimSpace(c.BadgeStyles[i].BadgeType)) + c.BadgeStyles[i].BackgroundColor = strings.ToUpper(strings.TrimSpace(c.BadgeStyles[i].BackgroundColor)) + c.BadgeStyles[i].TextColor = strings.ToUpper(strings.TrimSpace(c.BadgeStyles[i].TextColor)) + } +} + +func (c UIConfig) Validate(name string) error { + if len(c.BackgroundImage) > 2048 { + return fmt.Errorf("%s.backgroundImage is too long", name) + } + seenTypes := make(map[string]struct{}, len(c.BadgeStyles)) + for _, style := range c.BadgeStyles { + if _, ok := validBadgeTypes[style.BadgeType]; !ok { + return fmt.Errorf("%s unsupported badgeType: %s", name, style.BadgeType) + } + if _, exists := seenTypes[style.BadgeType]; exists { + return fmt.Errorf("%s allows only one style per badgeType", name) + } + seenTypes[style.BadgeType] = struct{}{} + if !hexColorPattern.MatchString(style.BackgroundColor) { + return fmt.Errorf("%s invalid backgroundColor for %s", name, style.BadgeType) + } + if !hexColorPattern.MatchString(style.TextColor) { + return fmt.Errorf("%s invalid textColor for %s", name, style.BadgeType) + } + } + return nil +} + +func (v *VariantConfig) Normalize() { + v.Name = strings.TrimSpace(v.Name) + v.SkinKey = strings.TrimSpace(v.SkinKey) + if v.UIConfig != nil { + v.UIConfig.Normalize() + if v.UIConfig.BackgroundImage == "" && len(v.UIConfig.BadgeStyles) == 0 { + v.UIConfig = nil + } + } + seenProducts := make(map[primitive.ObjectID]struct{}, len(v.ProductIDs)) + productIDs := make(OrderedProductIDs, 0, len(v.ProductIDs)) + for _, id := range v.ProductIDs { + if id.IsZero() { + continue + } + if _, exists := seenProducts[id]; exists { + continue + } + seenProducts[id] = struct{}{} + productIDs = append(productIDs, id) + } + v.ProductIDs = productIDs + + badges := make([]ProductBadge, 0, len(v.ProductBadges)) + for _, badge := range v.ProductBadges { + badge.BadgeType = strings.ToUpper(strings.TrimSpace(badge.BadgeType)) + badge.BadgeText = strings.TrimSpace(badge.BadgeText) + if badge.BadgeText == "" { + switch badge.BadgeType { + case BadgeMostPopular: + badge.BadgeText = "最受欢迎" + case BadgeNewUser: + badge.BadgeText = "新人特惠" + } + } + badges = append(badges, badge) + } + v.ProductBadges = badges +} + +func (v VariantConfig) Validate(name string) error { + if len(v.ProductIDs) == 0 { + return fmt.Errorf("%s.productIds is required", name) + } + productSet := make(map[primitive.ObjectID]struct{}, len(v.ProductIDs)) + for _, id := range v.ProductIDs { + productSet[id] = struct{}{} + } + if !v.DefaultProductID.IsZero() { + if _, ok := productSet[v.DefaultProductID]; !ok { + return fmt.Errorf("%s.defaultProductId must belong to productIds", name) + } + } + if v.UIConfig != nil { + if err := v.UIConfig.Validate(name + ".uiConfig"); err != nil { + return err + } + } + badgeProducts := make(map[primitive.ObjectID]struct{}, len(v.ProductBadges)) + for _, badge := range v.ProductBadges { + if badge.ProductID.IsZero() { + return fmt.Errorf("%s badge productId is required", name) + } + if _, exists := badgeProducts[badge.ProductID]; exists { + return fmt.Errorf("%s allows only one badge per product", name) + } + badgeProducts[badge.ProductID] = struct{}{} + if _, ok := productSet[badge.ProductID]; !ok { + return fmt.Errorf("%s badge productId must belong to productIds", name) + } + if _, ok := validBadgeTypes[badge.BadgeType]; !ok { + return fmt.Errorf("%s unsupported badgeType: %s", name, badge.BadgeType) + } + } + return nil +} + +func (e *Experiment) Normalize() { + e.ExperimentID = strings.TrimSpace(e.ExperimentID) + e.Name = strings.TrimSpace(e.Name) + e.Status = strings.ToUpper(strings.TrimSpace(e.Status)) + e.VariantA.Normalize() + e.VariantB.Normalize() +} + +func (e Experiment) Validate() error { + if e.ExperimentID == "" { + return fmt.Errorf("experimentId is required") + } + if len(e.ExperimentID) > 128 { + return fmt.Errorf("experimentId is too long") + } + if e.TrafficA < 0 || e.TrafficB < 0 || e.TrafficA+e.TrafficB != 100 { + return fmt.Errorf("trafficA and trafficB must add up to 100") + } + if e.StartAt != nil && e.EndAt != nil && !e.EndAt.After(*e.StartAt) { + return fmt.Errorf("endAt must be later than startAt") + } + if err := e.VariantA.Validate("variantA"); err != nil { + return err + } + if err := e.VariantB.Validate("variantB"); err != nil { + return err + } + return nil +} + +func (e Experiment) ActiveAt(now time.Time) bool { + if e.Status != StatusActive { + return false + } + if e.StartAt != nil && e.StartAt.After(now) { + return false + } + return e.EndAt == nil || e.EndAt.After(now) +} + +func (e Experiment) Assign(uid uint64) string { + hasher := fnv.New32a() + _, _ = hasher.Write([]byte(fmt.Sprintf("%s:%d", e.ExperimentID, uid))) + if int(hasher.Sum32()%100) < e.TrafficA { + return VariantA + } + return VariantB +} + +func (e Experiment) ConfigFor(variant string) (VariantConfig, bool) { + switch strings.ToUpper(strings.TrimSpace(variant)) { + case VariantA: + return e.VariantA, true + case VariantB: + return e.VariantB, true + default: + return VariantConfig{}, false + } +} + +func ValidEventName(name string) bool { + _, ok := validEventNames[strings.ToUpper(strings.TrimSpace(name))] + return ok +} diff --git a/models/v/vipcardexperimentmod/type_test.go b/models/v/vipcardexperimentmod/type_test.go new file mode 100644 index 0000000..8603c73 --- /dev/null +++ b/models/v/vipcardexperimentmod/type_test.go @@ -0,0 +1,429 @@ +package vipcardexperimentmod + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestExperimentNormalizeValidateAndAssign(t *testing.T) { + first := primitive.NewObjectID() + second := primitive.NewObjectID() + experiment := Experiment{ + ExperimentID: " vip-card-test ", + TrafficA: 50, + TrafficB: 50, + VariantA: VariantConfig{ + DefaultProductID: first, + ProductIDs: []primitive.ObjectID{first, first, second}, + UIConfig: &UIConfig{ + BackgroundImage: " card-skin-a.png ", + BadgeStyles: []BadgeStyle{{ + BadgeType: " most_popular ", + BackgroundColor: " #f04432 ", + TextColor: " #ffffff ", + }}, + }, + ProductBadges: []ProductBadge{{ + ProductID: first, + BadgeType: " most_popular ", + }}, + }, + VariantB: VariantConfig{ + DefaultProductID: second, + ProductIDs: []primitive.ObjectID{second, first}, + }, + } + experiment.Normalize() + if err := experiment.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } + if experiment.ExperimentID != "vip-card-test" { + t.Fatalf("ExperimentID = %q", experiment.ExperimentID) + } + if len(experiment.VariantA.ProductIDs) != 2 { + t.Fatalf("duplicate product IDs were not removed: %#v", experiment.VariantA.ProductIDs) + } + if got := experiment.VariantA.ProductBadges[0].BadgeText; got != "最受欢迎" { + t.Fatalf("default badge text = %q", got) + } + if got := experiment.VariantA.UIConfig.BackgroundImage; got != "card-skin-a.png" { + t.Fatalf("background image = %q", got) + } + style := experiment.VariantA.UIConfig.BadgeStyles[0] + if style.BadgeType != BadgeMostPopular || + style.BackgroundColor != "#F04432" || + style.TextColor != "#FFFFFF" { + t.Fatalf("normalized badge style = %#v", style) + } + firstAssignment := experiment.Assign(12345) + for i := 0; i < 10; i++ { + if got := experiment.Assign(12345); got != firstAssignment { + t.Fatalf("assignment changed: first=%s got=%s", firstAssignment, got) + } + } +} + +func TestExperimentValidateRejectsInvalidConfigurations(t *testing.T) { + productID := primitive.NewObjectID() + base := Experiment{ + ExperimentID: "experiment", + TrafficA: 50, + TrafficB: 50, + VariantA: VariantConfig{ + DefaultProductID: productID, + ProductIDs: []primitive.ObjectID{productID}, + }, + VariantB: VariantConfig{ + DefaultProductID: productID, + ProductIDs: []primitive.ObjectID{productID}, + }, + } + + invalidTraffic := base + invalidTraffic.TrafficA = 90 + if err := invalidTraffic.Validate(); err == nil { + t.Fatal("expected invalid traffic to fail") + } + + invalidDefault := base + invalidDefault.VariantA.DefaultProductID = primitive.NewObjectID() + if err := invalidDefault.Validate(); err == nil { + t.Fatal("expected a default product outside productIds to fail") + } + + duplicateMostPopular := base + duplicateMostPopular.VariantA.ProductIDs = append( + duplicateMostPopular.VariantA.ProductIDs, + primitive.NewObjectID(), + ) + duplicateMostPopular.VariantA.ProductBadges = []ProductBadge{ + {ProductID: duplicateMostPopular.VariantA.ProductIDs[0], BadgeType: BadgeMostPopular}, + {ProductID: duplicateMostPopular.VariantA.ProductIDs[1], BadgeType: BadgeMostPopular}, + } + if err := duplicateMostPopular.Validate(); err == nil { + t.Fatal("expected multiple MOST_POPULAR badges to fail") + } +} + +func TestExperimentValidateAllowsMissingDefaultProduct(t *testing.T) { + productID := primitive.NewObjectID() + experiment := Experiment{ + ExperimentID: "optional-default-product", + TrafficA: 50, + TrafficB: 50, + VariantA: VariantConfig{ + ProductIDs: []primitive.ObjectID{productID}, + }, + VariantB: VariantConfig{ + ProductIDs: []primitive.ObjectID{productID}, + }, + } + if err := experiment.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } +} + +func TestUIConfigValidate(t *testing.T) { + valid := UIConfig{ + BackgroundImage: "vip-card-skin.png", + BadgeStyles: []BadgeStyle{ + { + BadgeType: BadgeNewUser, + BackgroundColor: "#F04432", + TextColor: "#FFFFFFFF", + }, + { + BadgeType: BadgeMostPopular, + BackgroundColor: "#2B251A", + TextColor: "#F7D98C", + }, + }, + } + if err := valid.Validate("uiConfig"); err != nil { + t.Fatalf("valid UI config failed: %v", err) + } + + tests := []struct { + name string + styles []BadgeStyle + want string + }{ + { + name: "unsupported badge type", + styles: []BadgeStyle{{ + BadgeType: "UNKNOWN", + BackgroundColor: "#F04432", + TextColor: "#FFFFFF", + }}, + want: "unsupported badgeType", + }, + { + name: "invalid background color", + styles: []BadgeStyle{{ + BadgeType: BadgeNewUser, + BackgroundColor: "#FFF", + TextColor: "#FFFFFF", + }}, + want: "invalid backgroundColor", + }, + { + name: "invalid text color", + styles: []BadgeStyle{{ + BadgeType: BadgeNewUser, + BackgroundColor: "#F04432", + TextColor: "white", + }}, + want: "invalid textColor", + }, + { + name: "duplicate badge type", + styles: []BadgeStyle{ + { + BadgeType: BadgeNewUser, + BackgroundColor: "#F04432", + TextColor: "#FFFFFF", + }, + { + BadgeType: BadgeNewUser, + BackgroundColor: "#2B251A", + TextColor: "#F7D98C", + }, + }, + want: "only one style per badgeType", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := UIConfig{BadgeStyles: tt.styles} + err := config.Validate("uiConfig") + if err == nil || !strings.Contains(err.Error(), tt.want) { + t.Fatalf("Validate() error = %v, want %q", err, tt.want) + } + }) + } +} + +func TestUIConfigNormalizeDropsEmptyConfig(t *testing.T) { + productID := primitive.NewObjectID() + variant := VariantConfig{ + DefaultProductID: productID, + ProductIDs: []primitive.ObjectID{productID}, + UIConfig: &UIConfig{}, + } + variant.Normalize() + if variant.UIConfig != nil { + t.Fatalf("empty UI config = %#v, want nil", variant.UIConfig) + } +} + +func TestExperimentValidateChecksVariantBUIConfig(t *testing.T) { + productID := primitive.NewObjectID() + experiment := Experiment{ + ExperimentID: "variant-b-ui-check", + TrafficA: 50, + TrafficB: 50, + VariantA: VariantConfig{ + DefaultProductID: productID, + ProductIDs: []primitive.ObjectID{productID}, + }, + VariantB: VariantConfig{ + DefaultProductID: productID, + ProductIDs: []primitive.ObjectID{productID}, + UIConfig: &UIConfig{BadgeStyles: []BadgeStyle{{ + BadgeType: BadgeNewUser, + BackgroundColor: "#FFF", + TextColor: "#FFFFFF", + }}}, + }, + } + err := experiment.Validate() + if err == nil || !strings.Contains(err.Error(), "variantB.uiConfig") { + t.Fatalf("Validate() error = %v, want variantB.uiConfig error", err) + } +} + +func TestUIConfigValidateBackgroundImageLength(t *testing.T) { + valid := UIConfig{BackgroundImage: strings.Repeat("a", 2048)} + if err := valid.Validate("uiConfig"); err != nil { + t.Fatalf("2048-character background image failed: %v", err) + } + invalid := UIConfig{BackgroundImage: strings.Repeat("a", 2049)} + if err := invalid.Validate("uiConfig"); err == nil { + t.Fatal("expected overlong background image to fail") + } +} + +func TestExperimentUIConfigJSONAndBSONRoundTrip(t *testing.T) { + productID := primitive.NewObjectID() + experiment := Experiment{ + ExperimentID: "vip-card-contract", + TrafficA: 100, + TrafficB: 0, + VariantA: VariantConfig{ + DefaultProductID: productID, + ProductIDs: []primitive.ObjectID{productID}, + UIConfig: &UIConfig{ + BackgroundImage: "vip-card-skin-a.png", + BadgeStyles: []BadgeStyle{{ + BadgeType: BadgeMostPopular, + BackgroundColor: "#2B251A", + TextColor: "#F7D98C", + }}, + }, + }, + VariantB: VariantConfig{ + DefaultProductID: productID, + ProductIDs: []primitive.ObjectID{productID}, + }, + } + + jsonData, err := json.Marshal(experiment) + if err != nil { + t.Fatalf("json.Marshal() error = %v", err) + } + if !bytes.Contains(jsonData, []byte(`"uiConfig"`)) || + !bytes.Contains(jsonData, []byte(`"backgroundColor":"#2B251A"`)) { + t.Fatalf("JSON contract missing UI config: %s", jsonData) + } + var fromJSON Experiment + if err = json.Unmarshal(jsonData, &fromJSON); err != nil { + t.Fatalf("json.Unmarshal() error = %v", err) + } + if fromJSON.VariantA.UIConfig == nil || + fromJSON.VariantA.UIConfig.BackgroundImage != "vip-card-skin-a.png" || + fromJSON.VariantB.UIConfig != nil { + t.Fatalf("JSON round trip = %#v", fromJSON) + } + + bsonData, err := bson.Marshal(experiment) + if err != nil { + t.Fatalf("bson.Marshal() error = %v", err) + } + var fromBSON Experiment + if err = bson.Unmarshal(bsonData, &fromBSON); err != nil { + t.Fatalf("bson.Unmarshal() error = %v", err) + } + if fromBSON.VariantA.UIConfig == nil || + fromBSON.VariantA.UIConfig.BadgeStyles[0].TextColor != "#F7D98C" || + fromBSON.VariantB.UIConfig != nil { + t.Fatalf("BSON round trip = %#v", fromBSON) + } +} + +func TestOrderedProductIDsJSONMapAndLegacyArray(t *testing.T) { + first := primitive.NewObjectID() + second := primitive.NewObjectID() + ids := OrderedProductIDs{first, second} + + encoded, err := json.Marshal(ids) + if err != nil { + t.Fatalf("json.Marshal() error = %v", err) + } + want := `{"1":"` + first.Hex() + `","2":"` + second.Hex() + `"}` + if string(encoded) != want { + t.Fatalf("JSON = %s, want %s", encoded, want) + } + + var fromMap OrderedProductIDs + input := []byte(`{"2":"` + second.Hex() + `","1":"` + first.Hex() + `"}`) + if err = json.Unmarshal(input, &fromMap); err != nil { + t.Fatalf("map json.Unmarshal() error = %v", err) + } + if len(fromMap) != 2 || fromMap[0] != first || fromMap[1] != second { + t.Fatalf("map JSON order = %#v", fromMap) + } + + var fromLegacy OrderedProductIDs + legacy := []byte(`["` + second.Hex() + `","` + first.Hex() + `"]`) + if err = json.Unmarshal(legacy, &fromLegacy); err != nil { + t.Fatalf("legacy json.Unmarshal() error = %v", err) + } + if len(fromLegacy) != 2 || fromLegacy[0] != second || fromLegacy[1] != first { + t.Fatalf("legacy JSON order = %#v", fromLegacy) + } +} + +func TestOrderedProductIDsRejectsInvalidMap(t *testing.T) { + productID := primitive.NewObjectID().Hex() + tests := []string{ + `{"2":"` + productID + `"}`, + `{"01":"` + productID + `"}`, + `{"1":"` + productID + `","2":"` + productID + `"}`, + } + for _, input := range tests { + var ids OrderedProductIDs + if err := json.Unmarshal([]byte(input), &ids); err == nil { + t.Fatalf("json.Unmarshal(%s) succeeded, want error", input) + } + } +} + +func TestOrderedProductIDsBSONDocumentAndLegacyArray(t *testing.T) { + first := primitive.NewObjectID() + second := primitive.NewObjectID() + type orderedDocument struct { + ProductIDs OrderedProductIDs `bson:"productIds"` + } + + encoded, err := bson.Marshal(orderedDocument{ProductIDs: OrderedProductIDs{first, second}}) + if err != nil { + t.Fatalf("bson.Marshal() error = %v", err) + } + rawValue := bson.Raw(encoded).Lookup("productIds") + if rawValue.Type != bson.TypeEmbeddedDocument { + t.Fatalf("productIds BSON type = %s, want document", rawValue.Type) + } + elements, err := rawValue.Document().Elements() + if err != nil { + t.Fatalf("productIds document error = %v", err) + } + if len(elements) != 2 || elements[0].Key() != "1" || elements[1].Key() != "2" || + elements[0].Value().ObjectID() != first || elements[1].Value().ObjectID() != second { + t.Fatalf("productIds BSON document = %#v", elements) + } + + var fromDocument orderedDocument + if err = bson.Unmarshal(encoded, &fromDocument); err != nil { + t.Fatalf("document bson.Unmarshal() error = %v", err) + } + if len(fromDocument.ProductIDs) != 2 || + fromDocument.ProductIDs[0] != first || fromDocument.ProductIDs[1] != second { + t.Fatalf("document BSON order = %#v", fromDocument.ProductIDs) + } + + legacy, err := bson.Marshal(struct { + ProductIDs []primitive.ObjectID `bson:"productIds"` + }{ProductIDs: []primitive.ObjectID{second, first}}) + if err != nil { + t.Fatalf("legacy bson.Marshal() error = %v", err) + } + var fromLegacy orderedDocument + if err = bson.Unmarshal(legacy, &fromLegacy); err != nil { + t.Fatalf("legacy bson.Unmarshal() error = %v", err) + } + if len(fromLegacy.ProductIDs) != 2 || + fromLegacy.ProductIDs[0] != second || fromLegacy.ProductIDs[1] != first { + t.Fatalf("legacy BSON order = %#v", fromLegacy.ProductIDs) + } +} + +func TestExperimentActiveAt(t *testing.T) { + now := time.Now() + start := now.Add(-time.Minute) + end := now.Add(time.Minute) + experiment := Experiment{Status: StatusActive, StartAt: &start, EndAt: &end} + if !experiment.ActiveAt(now) { + t.Fatal("expected active experiment") + } + past := now.Add(-2 * time.Minute) + experiment.EndAt = &past + if experiment.ActiveAt(now) { + t.Fatal("expired experiment must not be active") + } +} diff --git a/models/v/vipconfigmod/type.go b/models/v/vipconfigmod/type.go new file mode 100644 index 0000000..e27dd51 --- /dev/null +++ b/models/v/vipconfigmod/type.go @@ -0,0 +1,33 @@ +package vipconfigmod + +import ( + "time" + + "91porn-server/common/db" +) + +var mdb *db.MongoDB + +type VIPConfig struct { + ID int `json:"-" bson:"_id,omitempty"` + VipUpPrice int64 `json:"vipUpPrice" bson:"vipUpPrice"` //vip升级价格 + SVipUpPrice int64 `json:"sVipUpPrice" bson:"sVipUpPrice"` //svip升级价格 + VipUpCheapPrice int64 `json:"vipUpCheapPrice" bson:"vipUpCheapPrice"` //vip升级节省价格 + SVipUpCheapPrice int64 `json:"sVipUpCheapPrice" bson:"sVipUpCheapPrice"` //svip升级节省价格 + CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 + TimeConfig int64 `json:"timeConfig" bson:"timeConfig"` //时间配置 +} + +func Init() { + mdb = db.Init(table) +} + +type EditSelector struct { + VipUpPrice *int64 `json:"vipUpPrice,omitempty" bson:"vipUpPrice,omitempty"` //vip升级价格 + SVipUpPrice *int64 `json:"sVipUpPrice,omitempty" bson:"sVipUpPrice,omitempty"` //svip升级价格 + VipUpCheapPrice *int64 `json:"vipUpCheapPrice,omitempty" bson:"vipUpCheapPrice,omitempty"` //vip升级节省价格 + SVipUpCheapPrice *int64 `json:"sVipUpCheapPrice,omitempty" bson:"sVipUpCheapPrice,omitempty"` //svip升级节省价格 + TimeConfig *int64 `json:"timeConfig,omitempty" bson:"timeConfig,omitempty"` //时间配置 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` //修改时间 +} diff --git a/models/v/vipconfigmod/vipconfig.go b/models/v/vipconfigmod/vipconfig.go new file mode 100644 index 0000000..7a5d2a1 --- /dev/null +++ b/models/v/vipconfigmod/vipconfig.go @@ -0,0 +1,43 @@ +package vipconfigmod + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" +) + +const table = models.VipConfig + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func FindOne() (c VIPConfig, err error) { + err = coll(nil).FindOne(&c, bson.M{"_id": 1}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetByID", table, "FindOne", err)) + } + return +} + +//todo app + +// todo web +// InsertVIP 插入一条数据 +func Update(set EditSelector) error { + now := time.Now() + set.UpdatedAt = now + _, err := coll(nil).UpsertOne(bson.M{"_id": 1}, bson.M{"$set": set, "$setOnInsert": bson.M{"createdAt": set.UpdatedAt}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpsertOne", err)) + } + return err +} diff --git a/models/v/walletmod/app.go b/models/v/walletmod/app.go new file mode 100644 index 0000000..784d44f --- /dev/null +++ b/models/v/walletmod/app.go @@ -0,0 +1,7 @@ +package walletmod + +type WalletResp struct { + Wallet + // 当日剩余ai免费次数 + TodayAiFreeTimes int64 `json:"todayAiFreeTimes" bson:"-"` +} diff --git a/models/v/walletmod/type.go b/models/v/walletmod/type.go new file mode 100644 index 0000000..78b96bb --- /dev/null +++ b/models/v/walletmod/type.go @@ -0,0 +1,113 @@ +/* + * @Description: + * @Author: your name + * @Date: 2019-08-29 16:20:49 + * @LastEditTime: 2019-08-30 18:53:18 + * @LastEditors: Please set LastEditors + */ +package walletmod + +import ( + "91porn-server/common/db" + "time" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var mdb *db.MongoDB + +// Wallet 钱包 +type Wallet struct { + //钱包id 索引 + ID primitive.ObjectID `form:"id" json:"id" bson:"_id,omitempty"` + //关联用户 + UID uint64 `form:"uid" json:"uid" bson:"uid"` + //蝴蝶币数量 + Amount int64 `form:"amount" json:"amount" bson:"amount"` + //冻结蝴蝶币数量(抽奖消耗) + FreezeAmount int64 `form:"freezeAmount" json:"freezeAmount" bson:"freezeAmount"` + //作品收益 (单位 个) 整数位 + Income int64 `form:"income" json:"income" bson:"income"` + //作品收益 (单位 个) 例如:0.7 小数位 + IncomePot float64 `form:"incomePot" json:"incomePot" bson:"incomePot"` + //累积充值金额 + Consumption int64 `form:"consumption" json:"consumption" bson:"consumption"` + //收益金额(推广收益 单位 个) + Money int64 `form:"money" json:"money" bson:"money"` + //累积提取金额 + TakeOut int64 `form:"takeOut" json:"takeOut" bson:"takeOut"` + //推广绩效 + Performance int64 `form:"performance" json:"performance" bson:"performance"` + //视频收益 + VidIncome float64 `json:"vidIncome" bson:"vidIncome"` + //推广收益 + ProxyIncome float64 `json:"proxyIncome" bson:"proxyIncome"` + //推广用户累积充值金额,下级充值的,用来判断用户对应的分成区间,不做交易使用,单位分 + ProxyConsumption int64 `form:"proxyConsumption" json:"proxyConsumption" bson:"proxyConsumption"` + //打赏收益 + RewardIncome decimal.Decimal `json:"rewardIncome" bson:"rewardIncome"` + //累计扣除的收益金币数量 + DedIncome decimal.Decimal `json:"dedIncome" bson:"dedIncome"` + //usdt可提现余额,防止用usdt充值,其它方式洗钱(usdt充多少提多少) + UsdtAmount int64 `form:"usdtAmount" json:"usdtAmount" bson:"usdtAmount"` + //瓦力推广收益 + WaLiProxyIncome float64 `json:"waliProxyIncome" bson:"waliProxyIncome"` + //裸聊收益 + NudeChatIncome decimal.Decimal `json:"nudeChatIncome" bson:"nudeChatIncome"` + //果币:裸聊专用币 + FruitCoin int64 `json:"fruitCoin" bson:"fruitCoin"` + Integral int64 `json:"integral" bson:"integral"` // 积分 + DownloadCount int64 `json:"downloadCount" bson:"downloadCount"` // 下载次数 + AiUndressFreeTimes int64 `json:"aiUndressFreeTimes" bson:"aiUndressFreeTimes"` // 用户AI脱衣免费次数 + TotalAiUndressFreeTimes int64 `json:"totalAiUndressFreeTimes" bson:"totalAiUndressFreeTimes"` // 用户累计AI脱衣免费次数 + LotteryTimes int64 `json:"lotteryTimes" bson:"lotteryTimes"` // 用户抽奖次数 + TotalLotteryTimes int64 `json:"totalLotteryTimes" bson:"totalLotteryTimes"` // 用户累计抽奖次数 + AiMateBalance float64 `json:"aiMateBalance" bson:"aiMateBalance"` //ai伴侣余额 + LastAiMateRecordId string `json:"lastAiMateRecordId" bson:"lastAiMateRecordId"` // 最后一条ai伴侣扣费记录id + DownloadRequests []DownloadRequest `json:"-" bson:"downloadRequests,omitempty"` // 最近下载授权幂等记录 +} + +type DownloadRequest struct { + RequestKey string `bson:"requestKey"` + Fingerprint string `bson:"fingerprint"` + CreatedAt time.Time `bson:"createdAt"` +} + +func Init() { + mdb = db.Init(table) + initIndex() +} + +type DebitPlan struct { + Amount int64 + Income int64 + AiUndressFreeTimes int64 + Consumption int64 `bson:"consumption,omitempty"` // 累积充值金额 + DownloadCount int64 `bson:"downloadCount,omitempty"` // 下载次数 + LotteryTimes int64 `bson:"lotteryTimes,omitempty"` // 累积抽奖次数 +} + +type CreditPlan struct { + Amount *int64 `bson:"amount,omitempty"` // 金币 + Income *int64 `bson:"income,omitempty"` // 收益 + DownloadCount *int64 `bson:"downloadCount,omitempty"` // 下载次数 + Performance *int64 `bson:"performance,omitempty"` // 推广绩效 + VidIncome *float64 `bson:"vidIncome,omitempty"` // 视频收益 + ProxyIncome *int64 `bson:"proxyIncome,omitempty"` // 推广收益 + ProxyConsumption *int64 `bson:"proxyConsumption,omitempty"` // 推广用户累积充值金额 + Consumption *int64 `bson:"consumption,omitempty"` // 累积充值金额 + WaLiProxyIncome *float64 `bson:"waliProxyIncome,omitempty"` // 瓦力推广收益 + FruitCoin *int64 `bson:"fruitCoin,omitempty"` // 果币 + NudeChatIncome *decimal.Decimal `bson:"nudeChatIncome,omitempty"` // 裸聊收益 + AiUndressFreeTimes *int64 `bson:"aiUndressFreeTimes,omitempty"` // 赠送AI脱衣次数 + TotalAiUndressFreeTimes *int64 `bson:"totalAiUndressFreeTimes,omitempty"` // 累积AI脱衣次数 + Integral *int64 `bson:"integral,omitempty"` // 积分 + LotteryTimes *int64 `bson:"lotteryTimes,omitempty"` // 累积抽奖次数 + AiMateBalance *float64 `bson:"aiMateBalance,omitempty"` // ai伴侣余额 +} + +type CreditPlanWaLi struct { + Income int64 `bson:"income,omitempty"` // 收益 + WaLiProxyIncome float64 ` bson:"waliProxyIncome,omitempty"` // 瓦力推广收益 +} diff --git a/models/v/walletmod/wallet.go b/models/v/walletmod/wallet.go new file mode 100644 index 0000000..35ecab9 --- /dev/null +++ b/models/v/walletmod/wallet.go @@ -0,0 +1,943 @@ +/* + * @Description: In User Settings Edit + * @Author: your name + * @Date: 2019-08-29 19:55:45 + * @LastEditTime: 2019-08-30 19:39:56 + * @LastEditors: Please set LastEditors + */ +package walletmod + +import ( + "errors" + "fmt" + "sync" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var ( + ErrDownloadRequestConflict = errors.New("download request id belongs to another resource") + ErrDownloadCountNotEnough = errors.New("download count is not enough") + ErrDownloadAuthorizationRace = errors.New("download authorization changed concurrently") +) + +const maxDownloadRequestHistory = 1000 + +var mutex sync.Mutex + +const table = models.Wallet + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +func initIndex() { + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "uid", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + } + if _, err := coll(nil).CreateIndex(many); err != nil { + panic(fmt.Sprintf("wallet model set index err ==>[%+v]", err)) + } +} + +func InsertWallet(t *db.MongoTool, w *Wallet) error { + if _, err := coll(t).InsertOne(w); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertWallet", table, "InsertOne", err), + log.Any("w", w), + ) + return err + } + return nil +} + +// GetWallet 通过uid,查找用户余额 +func GetWallet(uid uint64) (w *Wallet, err error) { + w = &Wallet{} + if err = coll(nil).FindOne(w, bson.M{"uid": uid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetWallet", table, "FindOne", err), + log.Any("uid", uid), + ) + } + return +} + +func GetWalletMap(uidList []uint64) (map[uint64]Wallet, error) { + if len(uidList) == 0 { + return make(map[uint64]Wallet), nil + } + filter := bson.M{ + "uid": bson.M{ + "$in": uidList, + }, + } + walletList := make([]Wallet, 0, len(uidList)) + if err := coll(nil).Find(&walletList, filter); err != nil { + return nil, err + } + walletMap := make(map[uint64]Wallet, len(walletList)) + for _, wallet := range walletList { + walletMap[wallet.UID] = wallet + } + return walletMap, nil +} + +// CreditAmount 货币增加(虚拟货币) +func CreditAmount(t *db.MongoTool, amt int64, uid uint64) (*Wallet, error) { + w := Wallet{} + if err := coll(t).FindOneAndUpsert(&w, bson.M{"uid": uid}, bson.M{ + "$inc": bson.M{"amount": amt}, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditAmount", table, "FindOneAndUpsert", err), + log.Any("amt", amt), + log.Any("uid", uid), + ) + return nil, err + } + return &w, nil +} + +// DebitAmount 货币扣除(虚拟货币) +func DebitAmount(t *db.MongoTool, amt int64, uid uint64) (*Wallet, error) { + mutex.Lock() + defer mutex.Unlock() + w := Wallet{} + if err := coll(t).FindOneAndUpdate(&w, bson.M{"uid": uid, "amount": bson.M{"$gte": amt}}, bson.M{ + "$inc": bson.M{"amount": -amt}}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DebitAmount", table, "FindOneAndUpdate", err), + log.Any("amt", amt), + log.Any("uid", uid), + ) + return nil, err + } + if w.ID.IsZero() { + return nil, errors.New("not enough balance") + } + return &w, nil +} + +// CreditIntegral 新增积分 +func CreditIntegral(t *db.MongoTool, integral int64, uid uint64) (*Wallet, error) { + w := Wallet{} + if err := coll(t).FindOneAndUpsert(&w, bson.M{"uid": uid}, bson.M{ + "$inc": bson.M{"integral": integral}, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditIntegral", table, "FindOneAndUpsert", err), + log.Any("integral", integral), + log.Any("uid", uid), + ) + return nil, err + } + return &w, nil +} + +// DebitIntegral 扣除积分 +func DebitIntegral(t *db.MongoTool, integral int64, uid uint64) (*Wallet, error) { + mutex.Lock() + defer mutex.Unlock() + w := Wallet{} + if err := coll(t).FindOneAndUpdate(&w, bson.M{"uid": uid, "integral": bson.M{"$gte": integral}}, bson.M{ + "$inc": bson.M{"integral": -integral}}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DebitIntegral", table, "FindOneAndUpdate", err), + log.Any("integral", integral), + log.Any("uid", uid), + ) + return nil, err + } + if w.ID.IsZero() { + return nil, errors.New("not enough balance") + } + return &w, nil +} + +// 扣除金币 +// 金币不足则报错 +func DebitAmountAndCheck(t *db.MongoTool, uid int64, amt int64) error { + mutex.Lock() + defer mutex.Unlock() + result, err := coll(t).UpdateOne(bson.M{"uid": uid, "amount": bson.M{"$gte": amt}}, bson.M{ + "$inc": bson.M{"amount": -amt}}) + if err != nil { + return err + } + if result.ModifiedCount != 1 { + return errors.New("金币余额不足") + } + return nil +} + +// 扣除金币 +// 金币不足则报错 +// 包括普通金币和可提现金币 +// 先扣除普通金币, 再扣除可提现金币 +// t 必须已经开启事务!!! +func DebitAmountDefault(t *db.MongoTool, uid int64, amt int64) error { + mutex.Lock() + defer mutex.Unlock() + var wallet Wallet + if err := coll(t).FindOne(&wallet, bson.M{"uid": uid}); err != nil { + return err + } + if wallet.Amount+wallet.Income < amt { + return errors.New("金币余额不足") + } + if wallet.Amount >= amt { + result, err := coll(t).UpdateOne(bson.M{"uid": uid, "amount": bson.M{"$gte": amt}}, bson.M{ + "$inc": bson.M{"amount": -amt}}) + if err != nil { + return err + } + if result.ModifiedCount != 1 { + return errors.New("金币余额不足") + } + return nil + } + useIncome := amt - wallet.Amount + result, err := coll(t).UpdateOne(bson.M{"uid": uid, "amount": wallet.Amount, "income": bson.M{"$gte": useIncome}}, bson.M{ + "$inc": bson.M{"amount": -wallet.Amount, "income": -useIncome}}) + if err != nil { + return err + } + if result.ModifiedCount != 1 { + return errors.New("金币余额不足") + } + return nil +} + +// GetAmount 获取金币 +func GetAmount(t *db.MongoTool, uid uint64) (int64, error) { + w := &Wallet{} + if err := coll(t).FindOne(w, bson.M{"uid": uid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAmount", table, "FindOne", err), + log.Any("uid", uid), + ) + return 0, err + } + return w.Amount, nil +} + +// GetAmount 获取果币 +func GetFruitAmount(t *db.MongoTool, uid uint64) (int64, error) { + w := &Wallet{} + if err := coll(t).FindOne(w, bson.M{"uid": uid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAmount", table, "FindOne", err), + log.Any("uid", uid), + ) + return 0, err + } + return w.FruitCoin, nil +} + +// GetAiMateBalance 获取ai伴侣币余额 +func GetAiMateBalance(t *db.MongoTool, uid uint64) (float64, error) { + w := &Wallet{} + if err := coll(t).FindOne(w, bson.M{"uid": uid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAmount", table, "FindOne", err), + log.Any("uid", uid), + ) + return 0, err + } + return w.AiMateBalance, nil +} + +// SetAmount 设置货币(虚拟货币) +func SetAmount(t *db.MongoTool, amt int64, uid uint64) error { + //写入数据库,待完成 + if _, err := coll(t).UpdateOne(bson.M{"uid": uid}, bson.M{ + "$set": bson.M{"amount": amt}, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "SetAmount", table, "UpdateOne", err), + log.Any("amt", amt), + log.Any("uid", uid), + ) + return err + } + return nil +} + +// 返回更新前的值 被删除的商人无法更新 +func FindUpdate(tool *db.MongoTool, uid uint64, amt int64) (*Wallet, error) { + w := Wallet{} + opt := &options.FindOneAndUpdateOptions{} + opt.SetReturnDocument(options.Before) + if err := coll(tool).FindOneAndUpdate(&w, bson.M{"uid": uid}, bson.M{"$set": bson.M{"amount": amt}}, opt); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindUpdate", table, "FindOneAndUpdate", err)) + return nil, err + } + if w.ID.IsZero() { + return nil, errors.New("wallet not found") + } + return &w, nil +} + +func Debit(t *db.MongoTool, p *DebitPlan, uid uint64) (*Wallet, error) { + if p == nil { + return nil, errors.New("wallet nil DebitPlan") + } + w := &Wallet{} + var err error + if p.Amount != 0 { + w, err = DebitAmount(t, p.Amount, uid) + if err != nil { + return nil, err + } + } + if p.Income != 0 { + w, err = DebitIncome(t, p.Income, uid) + if err != nil { + return nil, err + } + } + if p.Consumption != 0 { + err = CreditConsumption(t, p.Consumption, uid) + if err != nil { + return nil, err + } + } + if p.DownloadCount != 0 { + err = CreditDownloadCount(t, p.DownloadCount, uid) + if err != nil { + return nil, err + } + } + if p.AiUndressFreeTimes != 0 { + w, err = DebitAiFreeTimes(t, p.AiUndressFreeTimes, uid) + if err != nil { + return nil, err + } + } + if p.LotteryTimes != 0 { + w, err = DebitLotteryTimes(t, p.LotteryTimes, uid) + if err != nil { + return nil, err + } + } + if !w.ID.IsZero() { + return w, nil + } + if err = coll(t).FindOne(w, bson.M{"uid": uid}); err != nil { + return nil, err + } + if w.ID.IsZero() { + return nil, errors.New("wallet not found") + } + return w, nil +} + +// CreditIncomeBasePot +func CreditIncomeBasePot(t *db.MongoTool, incomeInt int64, income float64, pot float64, uid uint64) (*Wallet, error) { + mutex.Lock() + defer mutex.Unlock() + var wallet Wallet + if err := coll(t).FindOneAndUpsert(&wallet, bson.M{"uid": uid}, bson.M{ + "$set": bson.M{"incomePot": pot, "vidIncome": income}, + "$inc": bson.M{"income": incomeInt}, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditIncome", table, "FindOneAndUpdate", err), + log.Any("income", income), + log.Any("incomePot", pot), + log.Any("uid", uid), + ) + return nil, err + } + return &wallet, nil +} + +// CreditIncomeBasePotWithReward +func CreditIncomeBasePotWithReward(t *db.MongoTool, incomeInt int64, rewardIncome decimal.Decimal, pot float64, uid uint64) (*Wallet, error) { + var w Wallet + if err := coll(t).FindOneAndUpsert(&w, bson.M{"uid": uid}, bson.M{ + "$set": bson.M{"incomePot": pot}, + "$inc": bson.M{"income": incomeInt, "rewardIncome": rewardIncome}, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditIncome", table, "FindOneAndUpsert", err), + log.Any("income", incomeInt), + log.Any("incomePot", pot), + log.Any("uid", uid), + ) + return nil, err + } + return &w, nil +} + +// CreditDebIncome 累计扣量金币 增加 +func CreditDebIncome(t *db.MongoTool, income string, uid uint64) error { + debIncr, _ := primitive.ParseDecimal128(income) + //写入数据库,待完成 + res, err := coll(t).UpsertOne(bson.M{"uid": uid}, bson.M{ + "$inc": bson.M{"dedIncome": debIncr}, + }) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditDebIncome", table, "UpsertOne", err), + log.Any("income", income), + log.Any("uid", uid), + ) + return err + } + if res.UpsertedCount != 1 && res.ModifiedCount != 1 { + return errors.New("Invalid account") + } + return nil +} + +// CreditIncome 货币增加 +func CreditIncome(t *db.MongoTool, income int64, uid uint64) (*Wallet, error) { + var w Wallet + if err := coll(t).FindOneAndUpsert(&w, bson.M{"uid": uid}, bson.M{ + "$inc": bson.M{"income": income}, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditIncome", table, "FindOneAndUpsert", err), + log.Any("income", income), + log.Any("uid", uid), + ) + return nil, err + } + return &w, nil +} + +// DebitIncome 货币扣除 +func DebitIncome(t *db.MongoTool, income int64, uid uint64) (*Wallet, error) { + var w Wallet + if err := coll(t).FindOneAndUpdate(&w, bson.M{"uid": uid, "income": bson.M{"$gte": income}}, bson.M{ + "$inc": bson.M{"income": -income}, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DebitIncome", table, "FindOneAndUpdate", err), + log.Any("income", income), + log.Any("uid", uid), + ) + return nil, err + } + if w.ID.IsZero() { + return nil, errors.New("wallet not found") + } + return &w, nil +} + +// CreditFruitCoin 果币增加 +func CreditFruitCoin(t *db.MongoTool, uid uint64, fruitCoin int64) (*Wallet, error) { + var item *Wallet + if err := coll(t).FindOneAndUpsert(&item, bson.M{"uid": uid}, bson.M{"$inc": bson.M{"fruitCoin": fruitCoin}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditFruitCoin", table, "FindOneAndUpsert", err), + log.Any("fruitCoin", fruitCoin), + log.Any("uid", uid), + ) + return nil, err + } + return item, nil +} + +// UpdateAiMateBalance ai伴侣币修改 +func UpdateAiMateBalance(t *db.MongoTool, uid uint64, aiMateBalance float64) (*Wallet, error) { + var item *Wallet + if err := coll(t).FindOneAndUpsert(&item, bson.M{"uid": uid}, bson.M{"$set": bson.M{"aiMateBalance": aiMateBalance}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateAiMateBalance", table, "FindOneAndUpsert", err), + log.Any("aiMateBalance", aiMateBalance), + log.Any("uid", uid), + ) + return nil, err + } + return item, nil +} + +// DebitFruitCoin 果币减少 +func DebitFruitCoin(t *db.MongoTool, uid uint64, fruitCoin int64) (*Wallet, error) { + var item *Wallet + if err := coll(t).FindOneAndUpsert(&item, bson.M{"uid": uid}, bson.M{"$inc": bson.M{"fruitCoin": -fruitCoin}}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DebitFruitCoin", table, "FindOneAndUpsert", err), + log.Any("fruitCoin", -fruitCoin), + log.Any("uid", uid), + ) + return nil, err + } + return item, nil +} + +// CreditNudeChatIncome 裸聊收益增加 +func CreditNudeChatIncome(t *db.MongoTool, uid uint64, nudeChatIncome int64) (*Wallet, error) { + var item *Wallet + if err := coll(t).FindOneAndUpsert(&item, bson.M{"uid": uid}, bson.M{"$inc": bson.M{ + "nudeChatIncome": decimal.NewFromInt(nudeChatIncome), + "income": nudeChatIncome, + }}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditNudeChatIncome", table, "FindOneAndUpsert", err), + log.Any("nudeChatIncome", nudeChatIncome), + log.Any("uid", uid), + ) + return nil, err + } + return item, nil +} + +// DebitNudeChatIncome 裸聊收益减少 +func DebitNudeChatIncome(t *db.MongoTool, uid uint64, nudeChatIncome int64) (*Wallet, error) { + var item *Wallet + if err := coll(t).FindOneAndUpsert(&item, bson.M{"uid": uid}, bson.M{"$inc": bson.M{ + "nudeChatIncome": decimal.NewFromInt(-nudeChatIncome), + "income": -nudeChatIncome, + }}); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DebitNudeChatIncome", table, "FindOneAndUpsert", err), + log.Any("nudeChatIncome", nudeChatIncome), + log.Any("uid", uid), + ) + return nil, err + } + return item, nil +} + +// CreditMoney 货币增加 +func CreditMoney(t *db.MongoTool, money int64, uid uint64) error { + //写入数据库,待完成 + res, err := coll(t).UpsertOne(bson.M{"uid": uid}, bson.M{ + "$inc": bson.M{"money": money}, + }) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditMoney", table, "UpsertOne", err), + log.Any("money", money), + log.Any("uid", uid), + ) + return err + } + if res.UpsertedCount != 1 && res.ModifiedCount != 1 { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditMoney", table, "errors.New", "Invalid account"), + log.Any("money", money), + log.Any("uid", uid), + ) + return errors.New("Invalid account") + } + return nil +} + +// DebitMoney 货币扣除 +func DebitMoney(t *db.MongoTool, money int64, uid uint64) (*Wallet, error) { + w := Wallet{} + if err := coll(t).FindOneAndUpdate(&w, bson.M{"uid": uid, "money": bson.M{"$gte": money}}, bson.M{ + "$inc": bson.M{"money": -money}, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DebitMoney", table, "FindOneAndUpdate", err), + log.Any("money", money), + log.Any("uid", uid), + ) + return nil, err + } + if w.ID.IsZero() { + return nil, errors.New("not enough balance") + } + return &w, nil +} + +// CreditConsumption 总充值增加 +func CreditConsumption(t *db.MongoTool, money int64, uid uint64) error { + res, err := coll(t).UpsertOne(bson.M{ + "uid": uid, + }, bson.M{ + "$inc": bson.M{ + "consumption": money, + }, + }) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditConsumption", table, "UpsertOne", err), + log.Any("money", money), + log.Any("uid", uid), + ) + return err + } + if res.UpsertedCount != 1 && res.ModifiedCount != 1 { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditConsumption", table, "errors.New", "Invalid account"), + log.Any("money", money), + log.Any("uid", uid), + ) + return errors.New("Invalid account") + } + return nil +} + +// CreditDownloadCount 总下载次数加 +func CreditDownloadCount(t *db.MongoTool, downloadCount int64, uid uint64) error { + res, err := coll(t).UpsertOne(bson.M{"uid": uid}, bson.M{"$inc": bson.M{"downloadCount": -downloadCount}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditDownloadCount", table, "UpsertOne", err), + log.Any("downloadCount", downloadCount), + log.Any("uid", uid), + ) + return err + } + if res.UpsertedCount != 1 && res.ModifiedCount != 1 { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditDownloadCount", table, "errors.New", "Invalid account"), + log.Any("downloadCount", downloadCount), + log.Any("uid", uid), + ) + return errors.New("update DownloadCount err") + } + return nil +} + +func Find(filter bson.M) ([]*Wallet, error) { + ws := make([]*Wallet, 0) + if err := coll(nil).Find(&ws, filter); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Find", table, "Find", err), + log.Any("filter", filter), + ) + return nil, err + } + return ws, nil +} + +// Credit 货币增加(虚拟货币) +func Credit(t *db.MongoTool, creditPlan CreditPlan, uid uint64) (*Wallet, error) { + w := Wallet{} + if err := coll(t).FindOneAndUpsert(&w, bson.M{"uid": uid}, bson.M{ + "$inc": creditPlan, + }); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CreditAmount", table, "FindOneAndUpsert", err), + log.Any("creditPlan", creditPlan), + log.Any("uid", uid), + ) + return nil, err + } + return &w, nil +} + +// DescCredit 货币扣除(虚拟货币) +func DescCredit(t *db.MongoTool, creditPlan CreditPlan, uid uint64) (wallet *Wallet, err error) { + filter := bson.M{"uid": uid} + if creditPlan.Income != nil { + filter["income"] = bson.M{"$gte": creditPlan.Income} + } + if creditPlan.Amount != nil { + filter["amount"] = bson.M{"$gte": creditPlan.Amount} + } + if creditPlan.LotteryTimes != nil { + filter["lotteryTimes"] = bson.M{"$gte": creditPlan.LotteryTimes} + } + err = coll(t).FindOneAndUpsert(&wallet, filter, bson.M{ + "$inc": creditPlan, + }) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DescCredit", table, "FindOneAndUpsert", err), + log.Any("creditPlan", creditPlan), + log.Any("uid", uid), + ) + } + return +} + +// 抽奖预扣 +func AddFreeze(userId uint64, amt int64) (*Wallet, error) { + filter := bson.M{ + "uid": userId, + "amount": bson.M{"$gte": amt}, + } + inc := bson.M{ + "amount": -amt, + "freezeAmount": amt, + } + w := Wallet{} + if err := coll(nil).FindOneAndUpdate(&w, filter, bson.M{"$inc": inc}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DrawFreeze", table, "FindOneAndUpdate", err), + log.Any("uid", userId), + log.Any("amt", amt), + ) + return nil, err + } + if w.ID.IsZero() { + return nil, errors.New("not enough balance") + } + return &w, nil +} + +// 扣除预扣 +func CutFreeze(t *db.MongoTool, userId uint64, amt, amount int64) (*Wallet, error) { + filter := bson.M{ + "uid": userId, + "freezeAmount": bson.M{"$gte": amt}, + } + inc := bson.M{ + "freezeAmount": -amt, + "amount": amount, + } + w := Wallet{} + if err := coll(nil).FindOneAndUpdate(&w, filter, bson.M{"$inc": inc}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "CutFreeze", table, "FindOneAndUpdate", err), + log.Any("uid", userId), + log.Any("amt", amt), + ) + return nil, err + } + if w.ID.IsZero() { + return nil, errors.New("not enough balance") + } + return &w, nil +} + +func (w *Wallet) RealAmount() decimal.Decimal { + return decimal.NewFromInt(w.Amount).Add(decimal.NewFromInt(w.Income)). + Add(decimal.NewFromFloat(w.IncomePot)).Add(decimal.NewFromInt(w.Money)) +} + +func GetRealAmount(w *Wallet) decimal.Decimal { + if w == nil { + return decimal.Decimal{} + } + return decimal.NewFromInt(w.Amount).Add(decimal.NewFromInt(w.Income)). + Add(decimal.NewFromFloat(w.IncomePot)).Add(decimal.NewFromInt(w.Money)) +} + +func (w *Wallet) RealIntegral() decimal.Decimal { + if w != nil && w.Integral != 0 { + return decimal.NewFromInt(w.Integral) + } else { + return decimal.NewFromInt(0) + } +} + +func UpsertOne(t *db.MongoTool, filter bson.M, update bson.M) error { + if _, err := coll(t).UpsertOne(filter, update); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "update", table, "updateOne", err)) + return err + } + return nil +} + +func BulkWrite(t *db.MongoTool, models []mongo.WriteModel, opts ...*options.BulkWriteOptions) error { + if _, err := coll(t).Bulk(models, opts...); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "update", table, "updateOne", err)) + return err + } + return nil +} + +// IncUsdtAmount usdt可提现额度加减 +func IncUsdtAmount(userId uint64, usdtAmount int64) (*Wallet, error) { + filter := bson.M{"uid": userId} + if usdtAmount < 0 { + filter["usdtAmount"] = bson.M{"$gte": 0} + } + inc := bson.M{"usdtAmount": usdtAmount} + var w Wallet + if err := coll(nil).FindOneAndUpdate(&w, filter, bson.M{"$inc": inc}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncUsdtAmount", table, "FindOneAndUpdate", err), + log.Any("uid", userId), + log.Any("amt", usdtAmount), + ) + return nil, err + } + if w.ID.IsZero() { + return nil, errors.New("wallet not found") + } + return &w, nil +} + +// AddUsdtAmount 增加usdt可提现额度 +func AddUsdtAmount(t *db.MongoTool, userId uint64, usdtAmount int64) error { + result, err := coll(t).UpdateOne(bson.M{"uid": userId}, bson.M{"$inc": bson.M{"usdtAmount": usdtAmount}}) + if err != nil { + return err + } + if result.ModifiedCount == 0 { + return errors.New("result.ModifiedCount is 0") + } + return nil +} + +// IsRechargeUser 是否充值用户 +func IsRechargeUser(t *db.MongoTool, uid uint64) bool { + w := &Wallet{} + if err := coll(t).FindOne(w, bson.M{"uid": uid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsRechargeUser", table, "FindOne", err), + log.Any("uid", uid), + ) + return false + } + if w.ID.IsZero() || w.Consumption <= 0 { + return false + } + return true +} + +// DebitAmountAndIncome 货币扣除(虚拟货币) +func DebitAmountAndIncome(t *db.MongoTool, amt, income int64, uid uint64) (wallet *Wallet, err error) { + mutex.Lock() + defer mutex.Unlock() + filter := bson.M{"uid": uid} + update := bson.M{} + if amt > 0 { + filter["amount"] = bson.M{"$gte": amt} + update["amount"] = -amt + } + if income > 0 { + filter["income"] = bson.M{"$gte": income} + update["income"] = -income + } + + err = coll(t).FindOneAndUpdate(&wallet, filter, bson.M{"$inc": update}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DebitAmountAndIncome", table, "FindOneAndUpdate", err), + log.Any("filter", filter), + log.Any("update", update), + log.Any("uid", uid), + ) + } + return +} + +// ReturnAmountAndIncome 货币返回(虚拟货币) +func ReturnAmountAndIncome(t *db.MongoTool, amt, income int64, uid uint64) (wallet *Wallet, err error) { + mutex.Lock() + defer mutex.Unlock() + filter := bson.M{"uid": uid} + update := bson.M{} + if -amt > 0 { + update["amount"] = -amt + } + if -income > 0 { + update["income"] = -income + } + + err = coll(t).FindOneAndUpdate(&wallet, filter, bson.M{"$inc": update}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "ReturnAmountAndIncome", table, "FindOneAndUpdate", err), + log.Any("filter", filter), + log.Any("update", update), + log.Any("uid", uid), + ) + } + return +} + +// DebitAiFreeTimes 扣出AI脱衣次数 +func DebitAiFreeTimes(t *db.MongoTool, freeTimes int64, uid uint64) (wallet *Wallet, err error) { + filter := bson.M{"uid": uid} + if freeTimes > 0 { + filter["aiUndressFreeTimes"] = bson.M{"$gte": freeTimes} + } + err = coll(t).FindOneAndUpsert(&wallet, filter, bson.M{"$inc": bson.M{"aiUndressFreeTimes": -freeTimes}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DebitAiFreeTimes", table, "FindOneAndUpdate", err), + log.Any("freeTimes", freeTimes), + log.Any("uid", uid), + ) + } + return +} + +// DebitDownloadCounts 扣出下载次数 +func DebitDownloadCounts(t *db.MongoTool, downloadCount int64, uid uint64) (wallet *Wallet, err error) { + filter := bson.M{"uid": uid} + if downloadCount > 0 { + filter["downloadCount"] = bson.M{"$gte": downloadCount} + } + err = coll(t).FindOneAndUpsert(&wallet, filter, bson.M{"$inc": bson.M{"downloadCount": -downloadCount}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DebitAiDownloadCount", table, "FindOneAndUpdate", err), + log.Any("downloadCount", downloadCount), + log.Any("uid", uid), + ) + } + return +} + +// AuthorizeDramaDownload atomically records one idempotency key and debits one +// shared download count. The bounded wallet history covers retries even if an +// App instance or Redis cache restarts. +func AuthorizeDramaDownload(t *db.MongoTool, uid uint64, requestKey, fingerprint string, now time.Time) (*Wallet, bool, error) { + wallet := &Wallet{} + if err := coll(t).FindOne(wallet, bson.M{"uid": uid}); err != nil { + return nil, false, err + } + for _, request := range wallet.DownloadRequests { + if request.RequestKey != requestKey { + continue + } + if request.Fingerprint != fingerprint { + return nil, false, ErrDownloadRequestConflict + } + return wallet, false, nil + } + if wallet.DownloadCount <= 0 { + return nil, false, ErrDownloadCountNotEnough + } + record := DownloadRequest{RequestKey: requestKey, Fingerprint: fingerprint, CreatedAt: now} + filter := bson.M{ + "uid": uid, "downloadCount": bson.M{"$gte": 1}, + "downloadRequests.requestKey": bson.M{"$ne": requestKey}, + } + update := bson.M{ + "$inc": bson.M{"downloadCount": -1}, + "$push": bson.M{"downloadRequests": bson.M{ + "$each": []DownloadRequest{record}, "$slice": -maxDownloadRequestHistory, + }}, + } + updated := &Wallet{} + if err := coll(t).FindOneAndUpdate(updated, filter, update); err != nil { + return nil, false, err + } + if updated.UID == 0 { + return nil, false, ErrDownloadAuthorizationRace + } + return updated, true, nil +} + +// DebitLotteryTimes 扣出抽奖次数 +func DebitLotteryTimes(t *db.MongoTool, lotteryTimes int64, uid uint64) (wallet *Wallet, err error) { + filter := bson.M{"uid": uid} + if lotteryTimes > 0 { + filter["lotteryTimes"] = bson.M{"$gte": lotteryTimes} + } + err = coll(t).FindOneAndUpsert(&wallet, filter, bson.M{"$inc": bson.M{"lotteryTimes": -lotteryTimes}}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DebitAiDownloadCount", table, "FindOneAndUpdate", err), + log.Any("downloadCount", lotteryTimes), + log.Any("uid", uid), + ) + } + return +} + +// Credit ai伴侣货币增加(虚拟货币) +func CreditAiMate(t *db.MongoTool, amt float64, uid uint64, aiMateOrderId string) (wallet *Wallet, err error) { + update := bson.M{"$inc": bson.M{"aiMateBalance": amt}} + if amt < 0 && aiMateOrderId != "" { + update["$set"] = bson.M{"lastAiMateRecordId": aiMateOrderId} + } + if err = coll(t).FindOneAndUpsert(&wallet, bson.M{"uid": uid}, update); err != nil { + log.Warn(fmt.Sprintf("[METHOD-CreditAmount]==> Model %s FindOneAndUpsert fail error:%+v:", table, err), + log.Any("aiMateBalance", amt), + log.Any("uid", uid), + ) + } + return +} + +// 重新设置用户ai积分 +func SetAiMateBalance(t *db.MongoTool, uid uint64, aiMateBalance float64) error { + mutex.Lock() + defer mutex.Unlock() + _, err := coll(t).UpsertOne(bson.M{"uid": uid}, bson.M{"$set": bson.M{"aiMateBalance": aiMateBalance}}) + return err +} + +// GetIntegral 获取积分 +func GetIntegral(t *db.MongoTool, uid uint64) (int64, error) { + w := &Wallet{} + if err := coll(t).FindOne(w, bson.M{"uid": uid}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetAmount", table, "FindOne", err), + log.Any("uid", uid), + ) + return 0, err + } + return w.Integral, nil +} diff --git a/models/v/wdchannmod/app.go b/models/v/wdchannmod/app.go new file mode 100644 index 0000000..5aa391f --- /dev/null +++ b/models/v/wdchannmod/app.go @@ -0,0 +1,17 @@ +package wdchannmod + +import "91porn-server/models/v/wdtaxmod" + +type WithdrawChannelAppRes struct { + Channels []*AppWithdrawChannels `json:"channels"` + wdtaxmod.WithdrawTariff +} + +// WithdrawChannel 提现渠道配置 +type AppWithdrawChannels struct { + ChannelName string `json:"channelName,omitempty" bson:"channelName,omitempty"` //提现渠道名字 + CID string `json:"cid,omitempty" bson:"cid,omitempty"` //渠道id + PayType string `json:"payType" bson:"payType"` //支付方式 + MinMoney int `json:"minMoney" bson:"minMoney"` //支持最小支付金额 + MaxMoney int `json:"maxMoney" bson:"maxMoney"` //支持最大支付金额 +} diff --git a/models/v/wdchannmod/channel.go b/models/v/wdchannmod/channel.go new file mode 100644 index 0000000..de0bcc3 --- /dev/null +++ b/models/v/wdchannmod/channel.go @@ -0,0 +1,134 @@ +package wdchannmod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.WithdrawChannel + +// initIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "payType", Value: 1}, {Key: "cid", Value: 1}}, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InsertWithdrawChannel 插入一条数据 +func InsertWithdrawChannel(c *WithdrawChannel) error { + if _, err := coll(nil).InsertOne(c); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertWithdrawChannel", table, "InsertOne", err), + log.Any("c", c), + ) + return err + } + return nil +} + +// DeleteWithdrawChannel delete +func DeleteWithdrawChannel(cond bson.M) (int64, error) { + result, err := coll(nil).DeleteMany(cond) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteWithdrawChannel", table, "DeleteMany", err), + log.Any("cond", cond), + ) + return 0, err + } + return result.DeletedCount, err +} + +// UpdateWithdrawChannel 金币配置更新 +func UpdateWithdrawChannel(id string, set *WithdrawChannelSelector) (int64, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateWithdrawChannel", table, "ObjectIDFromHex", err), + log.Any("id", id), + log.Any("set", set), + ) + log.ZapLog.Warn("withdraw UpdateWithdrawChannel ObjectIDFromHex fail", log.E(err)) + return 0, err + } + result, err := coll(nil).UpdateOne(bson.M{"_id": oid}, bson.M{"$set": set}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateWithdrawChannel", table, "UpdateOne", err), + log.Any("id", id), + log.Any("set", set), + ) + return 0, err + } + return result.ModifiedCount, nil +} + +// GetWithdrawChannels 条件获取支付渠道列表 +func GetWithdrawChannels() ([]*WithdrawChannel, error) { + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "updatedAt", Value: 1}}) + var back []*WithdrawChannel + if err := coll(nil).Find(&back, bson.M{}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetWithdrawChannels", table, "Find", err)) + return nil, err + } + return back, nil +} + +// GetWithdrawChannel 条件获取支付渠道列表 +func GetWithdrawChannel(paytype string) (w *WithdrawChannel, err error) { + w = &WithdrawChannel{} + if err = coll(nil).FindOne(w, bson.M{"payType": paytype}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetWithdrawChannel", table, "FindOne", err), + log.Any("payType", paytype), + ) + return + } + return +} + +// GetWithdrawChannel 条件获取可用的提现方式 +func GetActiveWithdrawChannels() ([]*AppWithdrawChannels, error) { + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "weight", Value: -1}, {Key: "updatedAt", Value: 1}}) + var back []*AppWithdrawChannels + if err := coll(nil).Find(&back, bson.M{"active": true}, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetActiveWithdrawChannels", table, "Find", err)) + return nil, err + } + return back, nil +} + +// GetWithdrawChannel 条件获取支付渠道列表 +func IsYinse() (bool, error) { + w := WithdrawChannel{} + if err := coll(nil).FindOne(&w, bson.M{"cid": "yinse", "active": true}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IsYinse", table, "FindOne", err)) + return false, err + } + return !w.ID.IsZero(), nil +} diff --git a/models/v/wdchannmod/list.go b/models/v/wdchannmod/list.go new file mode 100644 index 0000000..10cc856 --- /dev/null +++ b/models/v/wdchannmod/list.go @@ -0,0 +1,93 @@ +package wdchannmod + +import ( + "fmt" + "math/rand" + + "91porn-server/common/log" + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type WithdrawChannelSlice []WithdrawChannel + +// 加权随机 +func (w WithdrawChannelSlice) WeightedRandom() WithdrawChannel { + if len(w) == 0 { + return WithdrawChannel{} + } + var indexs []int + for i, v := range w { + for j := 0; j < v.Weight; j++ { + indexs = append(indexs, i) + } + } + i := rand.Intn(len(indexs)) + if i < 0 { + return WithdrawChannel{} + } + return w[indexs[i]] +} + +func (w WithdrawChannelSlice) ToPayTypeList() []string { + payTypeMap := make(map[string]byte) + for _, v := range w { + payTypeMap[v.PayType] = 1 + } + payTypeList := make([]string, len(payTypeMap)) + i := 0 + for p := range payTypeMap { + payTypeList[i] = p + i++ + } + return payTypeList +} + +type Matcher = pageopt.Matcher + +// ActiveMatch +type ActiveMatch struct { + Active *bool +} + +func (d *ActiveMatch) New() Matcher { + return pageopt.NewAssignMatch("active", d.Active) +} + +// PayTypeMatch +type PayTypeMatch struct { + PayType *string +} + +func (d *PayTypeMatch) New() Matcher { + return pageopt.NewAssignMatch("payType", d.PayType) +} + +type Sort = bson.D + +func List(sort Sort, skip, limit *int64, matchers ...Matcher) (WithdrawChannelSlice, error) { + filter := pageopt.MergeM(matchers) + opt := &options.FindOptions{} + if len(sort) != 0 { + opt.SetSort(sort) + } + if skip != nil { + opt.SetSkip(*skip) + } + if limit != nil { + opt.SetLimit(*limit) + } + list := WithdrawChannelSlice{} + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "List", table, "Find", err), + log.Any("sort", sort), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("matchers", matchers), + ) + return nil, err + } + return list, nil +} diff --git a/models/v/wdchannmod/type.go b/models/v/wdchannmod/type.go new file mode 100644 index 0000000..af00d9c --- /dev/null +++ b/models/v/wdchannmod/type.go @@ -0,0 +1,36 @@ +package wdchannmod + +import ( + "time" + + "91porn-server/common/db" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + TypeAlipay = "alipay" //支付宝方式 + TypeBankCard = "bankcard" //银行卡方式 +) + +var mdb *db.MongoDB + +// WithdrawChannel 提现渠道配置 +type WithdrawChannel struct { + ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + ChannelName string `json:"channelName,omitempty" bson:"channelName"` //提现渠道名字 + CID string `json:"cid,omitempty" bson:"cid"` //渠道id + PayType string `json:"payType,omitempty" bson:"payType"` //支付方式 + MinMoney int `json:"minMoney,omitempty" bson:"minMoney"` //支持最小支付金额 + QpMinMoney int `json:"qpMinMoney,omitempty" bson:"qpMinMoney"` //支持棋牌最小支付金额 + MaxMoney int `json:"maxMoney,omitempty" bson:"maxMoney"` //支持最大支付金额 + Weight int `json:"weight,omitempty" bson:"weight"` //权重 + Active bool `json:"active,omitempty" bson:"active"` //是否激活 + CreatedAt time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` //创建时间 + UpdatedAt time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` //刷新时间 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/wdchannmod/web.go b/models/v/wdchannmod/web.go new file mode 100644 index 0000000..4319da4 --- /dev/null +++ b/models/v/wdchannmod/web.go @@ -0,0 +1,42 @@ +package wdchannmod + +import "91porn-server/models/v/wdtaxmod" + +// ChannelReq 新增提现渠道 +type ChannelReq struct { + ChannelName string `form:"channelName" json:"channelName"` //支付渠道名字 + CID string `form:"cid" json:"cid"` //渠道id + PayType string `form:"payType" json:"payType"` //支付方式 + MinMoney int `form:"minMoney" json:"minMoney"` //支持最小支付金额 + QpMinMoney int `form:"qpMinMoney" json:"qpMinMoney"` //支持棋牌最小支付金额 + MaxMoney int `form:"maxMoney" json:"maxMoney"` //支持最大支付金额 + Weight int `form:"weight" json:"weight"` //权重 +} + +// WithdrawChannelSelector 提现配置修改 +type WithdrawChannelSelector struct { + ChannelName *string `json:"channelName,omitempty" bson:"channelName,omitempty"` //提现渠道名字 + CID *string `json:"cid,omitempty" bson:"cid,omitempty"` //渠道id + PayType *string `json:"payType,omitempty" bson:"payType,omitempty"` //支付方式 + MinMoney *int `json:"minMoney,omitempty" bson:"minMoney,omitempty"` //支持最小支付金额 + QpMinMoney *int `json:"qpMinMoney,omitempty" bson:"qpMinMoney,omitempty"` //支持棋牌最小支付金额 + QpMaxMoney *int `json:"qpMaxMoney,omitempty" bson:"qpMaxMoney,omitempty"` //支持棋牌最大支付金额 + MaxMoney *int `json:"maxMoney,omitempty" bson:"maxMoney,omitempty"` //支持最大支付金额 + Weight *int `json:"weight,omitempty" bson:"weight,omitempty"` //权重 + Active *bool `json:"active,omitempty" bson:"active,omitempty"` //是否激活 +} + +type WithdrawChannelRes struct { + WithdrawChannel + wdtaxmod.WithdrawTariff +} + +// DelReq 删除请求 +type DelReq struct { + IDs []string `form:"ids" json:"ids"` +} + +// OpeResp 操作应答 +type OpeResp struct { + Count int64 `json:"count"` +} diff --git a/models/v/wdordmod/app.go b/models/v/wdordmod/app.go new file mode 100644 index 0000000..4579641 --- /dev/null +++ b/models/v/wdordmod/app.go @@ -0,0 +1,46 @@ +package wdordmod + +import "time" + +// WithdrawOrder struct { +type WithdrawQueryReq struct { + ID *string `json:"_id,omitempty" form:"id" bson:"_id"` //流水id + UID *uint64 `json:"uid,omitempty" form:"uid" bson:"uid"` //用户id + Amount *int64 `json:"amount,omitempty" form:"amount" bson:"amount"` //蝴蝶币数量 + Money *int64 `json:"money,omitempty" form:"money" bson:"money"` //提现金额 + Type *int `json:"type,omitempty" form:"type" bson:"type"` //提现类型 + ActName *string `json:"actName,omitempty" form:"actName" bson:"actName,omitempty"` //交易账户持有人 + Act *string `json:"act,omitempty" form:"act" bson:"act,omitempty"` //交易账户 + TaxRate *int `json:"taxRate,omitempty" form:"taxRate" bson:"taxRate"` //税率 + Status *int `json:"status,omitempty" form:"status" bson:"status"` //1审核中 2审核通过,转账中 3 审核失败 4 完成 + StatusDesc *string `json:"statusDesc,omitempty" form:"statusDesc" bson:"statusDesc"` //状态说明 + UpdatedAt *string `json:"updatedAt,omitempty" form:"updatedAt" bson:"updatedAt"` //时间戳 + CreatedAt *string `json:"createdAt,omitempty" form:"createdAt" bson:"createdAt"` //时间错 +} + +type EditSelector struct { + OID *string `json:"oid,omitempty" bson:"oid,omitempty"` + //第三方支付平台id + PayMoney *int64 `json:"payMoney,omitempty" bson:"payMoney,omitempty"` //实际到账 + Status int `json:"status" bson:"status"` //1未审核 2审核通过,转账中 3已拒绝 4未知错误 5提现成功 6提现失败 + StatusDesc string `json:"statusDesc,omitempty" bson:"statusDesc,omitempty"` //状态说明 + CheckedAt *time.Time `json:"checkedAt,omitempty" bson:"checkedAt,omitempty"` //审核时间 + UsdtRate *float64 `json:"usdtRate" bson:"usdtRate,omitempty"` //usdt费率 Todo:仅订单类型为usdt时必传 + ProgressAt *time.Time `json:"progressAt,omitempty" bson:"progressAt,omitempty"` //第三方下单成功时间 + FailureAt *time.Time `json:"failureAt,omitempty" bson:"failureAt,omitempty"` //回调失败时间 + SuccessAt *time.Time `json:"successAt,omitempty" bson:"successAt,omitempty"` //回调成功时间 + ReceivedAt *time.Time `json:"receivedAt,omitempty" bson:"receivedAt,omitempty"` //到账时间 + Operator *string `json:"operator,omitempty" bson:"operator,omitempty"` //操作者 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` +} + +type WithdrawReq struct { + Name *string `json:"name" ` //用户名 + Money int64 `json:"money" binding:"required"` //提现金额 + PayType string `json:"payType" binding:"required"` //提现方式,alipay,bankcard,usdt + WithdrawType *WithdrawTypes `json:"withdrawType"` //提现类型,0,代理提现; 1,金币提现 + ActName string `json:"actName"` //交易账户持有人 + Act string `json:"act" ` //交易账户 + DevID *string `json:"devID" ` //设备id + ProductType int `json:"productType"` //产品类型 0站群 1棋牌 +} diff --git a/models/v/wdordmod/discStat.go b/models/v/wdordmod/discStat.go new file mode 100644 index 0000000..12fd842 --- /dev/null +++ b/models/v/wdordmod/discStat.go @@ -0,0 +1,170 @@ +package wdordmod + +import ( + "strings" + "time" + + "91porn-server/common/pageopt" + "91porn-server/common/ysinterface/disc" +) + +// DistrictStatKey +type DistrictStatKey struct { + DiscSeqe `bson:",inline"` //商区码 + DeviceType string `bson:"deviceType"` //系统类型 iOS Android +} + +var ( + discWithdraw = DiscWithdraw +) + +func (d DistrictStatKey) GetDiscCode() string { + return d.DistrictCode +} + +func (d DistrictStatKey) GetPromSeqe() string { + return d.PromSeqe +} + +func (d DistrictStatKey) GetSysType() string { + return d.DeviceType +} + +func (d DistrictStatKey) String() string { + if d.DiscSeqe.String() == "" { + return "" + } + return strings.ToUpper(strings.Join([]string{d.DiscSeqe.String(), d.DeviceType}, "-")) +} + +// DistrictStater +type DistrictStater = disc.DistrictStater + +// DistrictOrderCountMap districtCode->订单数 map +func DistrictOrderCountMap(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, + (&CreatedAtGTEAndLTMatch{&start, &end}).New(), + (&WithdrawTypeNEMatch{&discWithdraw}).New(), + ) + filter := pageopt.MergeM(mats) + list := []DistrictStatKey{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater]int64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v] += 1 + } + } + return m, nil +} + +// DistrictOrderMoneyMap districtCode->订单金额 map +func DistrictOrderMoneyMap(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, + (&CreatedAtGTEAndLTMatch{&start, &end}).New(), + (&WithdrawTypeNEMatch{&discWithdraw}).New(), + ) + filter := pageopt.MergeM(mats) + list := []struct { + DistrictStatKey `bson:",inline"` //商区码 + Money int64 `bson:"money"` //提现金额 + }{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater]int64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v.DistrictStatKey] += v.Money + } + } + return m, nil +} + +// DistrictCompleteOrderCountMap districtCode->完成订单数 map +func DistrictCompleteOrderCountMap(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, + (&SuccessAtGTEAndLTMatch{&start, &end}).New(), + (&WithdrawTypeNEMatch{&discWithdraw}).New(), + ) + filter := pageopt.MergeM(mats) + list := []DistrictStatKey{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater]int64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v] += 1 + } + } + return m, nil +} + +// DistrictCompleteOrderPayMoneyMap districtCode->完成订单的实际到账金额 map +func DistrictCompleteOrderPayMoneyMap(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, + (&SuccessAtGTEAndLTMatch{&start, &end}).New(), + (&WithdrawTypeNEMatch{&discWithdraw}).New(), + ) + filter := pageopt.MergeM(mats) + list := []struct { + DistrictStatKey `bson:",inline"` //商区码 + PayMoney int64 `bson:"payMoney"` //实际到账金额 李秋山确认 + }{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater]int64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v.DistrictStatKey] += v.PayMoney + } + } + return m, nil +} + +// DistrictReceivedOrderCountMap districtCode->已出款订单数 map +func DistrictReceivedOrderCountMap(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + mats = append(mats, + (&ReceivedAtGTEAndLTMatch{&start, &end}).New(), + (&WithdrawTypeNEMatch{&discWithdraw}).New(), + ) + list := []DistrictStatKey{} + filter := pageopt.MergeM(mats) + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater]int64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v] += 1 + } + } + return m, nil +} + +// DistrictReceivedMoneyMap districtCode->已出款金额 map +func DistrictReceivedMoneyMap(start, end time.Time, mats ...Matcher) (map[DistrictStater]int64, error) { + list := []struct { + DistrictStatKey `bson:",inline"` //商区码 + PayMoney int64 `bson:"payMoney"` //实际到账金额 李秋山确认 + }{} + mats = append(mats, + (&ReceivedAtGTEAndLTMatch{&start, &end}).New(), + (&WithdrawTypeNEMatch{&discWithdraw}).New(), + ) + filter := pageopt.MergeM(mats) + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[DistrictStater]int64, len(list)) + for _, v := range list { + if v.String() != "" { + m[v.DistrictStatKey] += v.PayMoney + } + } + return m, nil +} diff --git a/models/v/wdordmod/error.go b/models/v/wdordmod/error.go new file mode 100644 index 0000000..e6ae4d9 --- /dev/null +++ b/models/v/wdordmod/error.go @@ -0,0 +1,9 @@ +package wdordmod + +// OrderNotExistError +type OrderNotExistError struct { +} + +func (d OrderNotExistError) Error() string { + return "order not exist error" +} diff --git a/models/v/wdordmod/list.go b/models/v/wdordmod/list.go new file mode 100644 index 0000000..26153bc --- /dev/null +++ b/models/v/wdordmod/list.go @@ -0,0 +1,200 @@ +package wdordmod + +import ( + "fmt" + "time" + + "91porn-server/common/log" + "91porn-server/common/pageopt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Matcher = pageopt.Matcher + +// StatusMatch +type StatusMatch struct { + Status *int +} + +func (s *StatusMatch) New() Matcher { + return pageopt.NewAssignMatch("status", s.Status) +} + +// UIDMatch +type UIDMatch = pageopt.UIDMatch + +// WithdrawTypeMatch +type WithdrawTypeMatch struct { + WithdrawTypes *WithdrawTypes +} + +func (w *WithdrawTypeMatch) New() Matcher { + return pageopt.NewAssignMatch("withdrawType", w.WithdrawTypes) +} + +// WithdrawTypeNEMatch +type WithdrawTypeNEMatch struct { + WithdrawTypes *WithdrawTypes +} + +func (w *WithdrawTypeNEMatch) New() Matcher { + return pageopt.NewNEMatch("withdrawType", w.WithdrawTypes) +} + +// PayTypeMatch +type PayTypeMatch struct { + PayType *string +} + +func (w *PayTypeMatch) New() Matcher { + return pageopt.NewAssignMatch("payType", w.PayType) +} + +// IDMatch +type IDMatch struct { + ID *primitive.ObjectID +} + +func (s *IDMatch) New() Matcher { + return pageopt.NewAssignMatch("_id", s.ID) +} + +// DistrictCodeMatch +type DistrictCodeMatch struct { + DistrictCode *string +} + +func (d *DistrictCodeMatch) New() Matcher { + return pageopt.NewAssignMatch("districtCode", d.DistrictCode) +} + +// PromSeqeMatch +type PromSeqeMatch struct { + PromSeqe *string +} + +func (s *PromSeqeMatch) New() Matcher { + return pageopt.NewAssignMatch("promSeqe", s.PromSeqe) +} + +// CheckedAtGTEAndLTMatch 审核时间 +type CheckedAtGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (c *CheckedAtGTEAndLTMatch) New() Matcher { + return pageopt.NewGTEAndLTMatch("checkedAt", c.GTE, c.LT) +} + +// ProgressAtGTEAndLTMatch 第三方下单成功时间 +type ProgressAtGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (p *ProgressAtGTEAndLTMatch) New() Matcher { + return pageopt.NewGTEAndLTMatch("progressAt", p.GTE, p.LT) +} + +// FailureAtGTEAndLTMatch 回调失败时间 +type FailureAtGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (f *FailureAtGTEAndLTMatch) New() Matcher { + return pageopt.NewGTEAndLTMatch("failureAt", f.GTE, f.LT) +} + +// SuccessAtGTEAndLTMatch 回调成功时间 +type SuccessAtGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (s *SuccessAtGTEAndLTMatch) New() Matcher { + return pageopt.NewGTEAndLTMatch("successAt", s.GTE, s.LT) +} + +// CreatedAtGTEAndLTMatch 提现申请时间 +type CreatedAtGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (c *CreatedAtGTEAndLTMatch) New() Matcher { + return pageopt.NewGTEAndLTMatch("createdAt", c.GTE, c.LT) +} + +// ReceivedAtGTEAndLTMatch 到账时间 李秋山确认 三方出款时间 +type ReceivedAtGTEAndLTMatch struct { + GTE *time.Time + LT *time.Time +} + +func (r *ReceivedAtGTEAndLTMatch) New() Matcher { + return pageopt.NewGTEAndLTMatch("receivedAt", r.GTE, r.LT) +} + +// IsDirectMatch +type IsDirectMatch struct { + IsDirect *bool +} + +func (b *IsDirectMatch) New() Matcher { + return pageopt.NewAssignMatch("isDirect", b.IsDirect) +} + +// DeviceTypeMatch +type DeviceTypeMatch struct { + DeviceType *string +} + +func (d *DeviceTypeMatch) New() Matcher { + return pageopt.NewAssignMatch("deviceType", d.DeviceType) +} + +type Sort = bson.D + +var Sort_CreatedAt_n1 = Sort{{Key: "createdAt", Value: -1}} + +func List(sort Sort, skip, limit *int64, matchers ...Matcher) ([]WithdrawOrder, error) { + filter := pageopt.MergeM(matchers) + opt := (&options.FindOptions{}) + if len(sort) != 0 { + opt.SetSort(sort) + } + if skip != nil { + opt.SetSkip(*skip) + } + if limit != nil { + opt.SetLimit(*limit) + } + list := []WithdrawOrder{} + if err := coll(nil).Find(&list, filter, opt); err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "List", table, "Find", err), + log.Any("sort", sort), + log.Any("skip", skip), + log.Any("limit", limit), + log.Any("matchers", matchers), + ) + return nil, err + } + return list, nil +} + +func Count(matchers ...Matcher) (int64, error) { + filter := pageopt.MergeM(matchers) + count, err := coll(nil).Count(filter) + if err != nil { + log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Count", table, "Count", err), + log.Any("matchers", matchers), + ) + return 0, err + } + return count, nil +} diff --git a/models/v/wdordmod/type.go b/models/v/wdordmod/type.go new file mode 100644 index 0000000..e07918c --- /dev/null +++ b/models/v/wdordmod/type.go @@ -0,0 +1,84 @@ +package wdordmod + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models/commod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + _ = iota + DEFAULT //1 用户发起提现 + PROCESSING //2 进行中,审核已通过,等待转账 + REFUSE //3 已拒绝 + UNKNOWN //4 未知错误 + SUCCESS //5 提现成功,提现到账 + FAILURE //6 失败,已经退款 +) +const ( + UNKNOWNError = "网络错误," + ServerError = "系统错误" + PROCESSINGError = "订单进行中" + SUCCESSStatus = " 提现已到账" + + TypeAlipay = "alipay" //支付宝方式 + TypeBankCard = "bankcard" //银行卡方式 + TypeUsdt = "usdt" //USDT方式 +) + +type WithdrawTypes = int + +const ( + AgentWithdraw WithdrawTypes = iota //代理提现 + GoldWithdraw //金币提现 + DiscWithdraw //商区提现 + FinanWithdraw //财务转账提现 + WlGameWithdraw //瓦力游戏提现 +) + +var mdb *db.MongoDB + +type DiscSeqe = commod.DiscSeqe + +type DiscDoc = commod.DiscDoc + +// WithdrawOrder 提现订单 +type WithdrawOrder struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //流水id + CID string `json:"cid" bson:"cid"` + UID uint64 `json:"uid" bson:"uid"` //用户id + Name string `json:"name" bson:"name"` //用户名 + Amount int64 `json:"amount" bson:"amount"` //蝴蝶币数量 + OID string `json:"oid" bson:"oid"` //第三方支付平台id + Money int64 `json:"money" bson:"money" binding:"required"` //提现金额 + PayMoney int64 `json:"payMoney" bson:"payMoney"` //实际到账 + PayType string `json:"payType" bson:"payType" binding:"required"` //提现方式,alipay,bankcard + WithdrawType WithdrawTypes `json:"withdrawType" bson:"withdrawType"` //提现类型,0,代理提现; 1,金币提现 + ActName string `json:"actName" bson:"actName"` //交易账户持有人 + Act string `json:"act" bson:"act"` //交易账户 + UserIP string `json:"userIp" bson:"userIp"` //用户ip + DeviceType string `json:"deviceType" bson:"deviceType"` //设备类型 + DevID string `json:"devID" bson:"devID"` //设备id + Status int `json:"status" bson:"status"` //1未审核 2审核通过,转账中 3已拒绝 4未知错误 5提现成功 6提现失败 + StatusDesc string `json:"statusDesc" bson:"statusDesc"` //状态说明 + CheckedAt time.Time `json:"checkedAt" bson:"checkedAt"` //审核时间 + UsdtRate float64 `json:"usdtRate" bson:"usdtRate"` //usdt费率 Todo:仅订单类型为usdt时必传 + ProgressAt time.Time `json:"progressAt" bson:"progressAt"` //第三方下单成功时间 + FailureAt time.Time `json:"failureAt" bson:"failureAt"` //回调失败时间 + SuccessAt time.Time `json:"successAt" bson:"successAt"` //回调成功时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //刷新时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //提现申请时间 + ReceivedAt time.Time `json:"receivedAt" bson:"receivedAt"` //到账时间 李秋山确认 三方出款时间 + DistrictRemark string `json:"districtRemark" bson:"districtRemark"` //备注 + ProductType int `json:"productType" bson:"productType"` //支付产品类型 0 站群 1 棋牌 + BankCode string `json:"bankCode" bson:"bankCode"` //银行编码 + DiscDoc `bson:",inline"` //商区 +} + +func Init() { + mdb = db.Init(table) + initIndex() +} diff --git a/models/v/wdordmod/web.go b/models/v/wdordmod/web.go new file mode 100644 index 0000000..acd6ef9 --- /dev/null +++ b/models/v/wdordmod/web.go @@ -0,0 +1,25 @@ +package wdordmod + +// WithdrawUpdate struct { +type WithdrawUpdate struct { + ID string `json:"id" form:"id" bson:"_id"` //流水id + StatusDesc string `json:"statusDesc" form:"statusDesc" bson:"statusDesc"` //状态说明 + //时间错 +} + +// WithdrawOrder struct { +type WithdrawWebQueryReq struct { + ID *string `json:"_id,omitempty" form:"id" bson:"_id"` //流水id + OID *string `json:"oid,omitempty" form:"oid,omitempty" bson:"oid"` + UID *uint64 `json:"uid,omitempty" form:"uid" bson:"uid"` //用户id + Amount *int64 `json:"amount,omitempty" form:"amount" bson:"amount"` //蝴蝶币数量 + Money *int64 `json:"money,omitempty" form:"money" bson:"money" ` //提现金额 + PayType *string `json:"payType,omitempty" form:"payType" bson:"payType" ` //提现类型 + TaxRate *int `json:"taxRate,omitempty" form:"taxRate" bson:"taxRate"` //税率 + Status *int `json:"status,omitempty" form:"status" bson:"status" ` //1审核中 2审核通过,转账中 3 审核失败 4 完成 + StatusDesc *string `json:"statusDesc,omitempty" form:"statusDesc" bson:"statusDesc"` //状态说明 + DistrictCode *string `json:"districtCode,omitempty" form:"districtCode" bson:"districtCode"` //商区码 + DistrictRemark *string `json:"districtRemark,omitempty" form:"districtRemark" bson:"districtRemark"` //备注 + ProductType *int `form:"productType" json:"productType,omitempty" bson:"productType"` //产品类型 0站群 1棋牌 + FilterChecked bool `form:"filterChecked" json:"filterChecked" bson:"-"` //是否过滤15分钟内未审核过的订单 +} diff --git a/models/v/wdordmod/withdraw.go b/models/v/wdordmod/withdraw.go new file mode 100644 index 0000000..9df96aa --- /dev/null +++ b/models/v/wdordmod/withdraw.go @@ -0,0 +1,470 @@ +/* + * @Description: 账户充值流水 + * @Author: your name + * @Date: 2019-08-29 20:45:27 + * @LastEditTime: 2019-08-30 13:54:21 + * @LastEditors: Please set LastEditors + */ +package wdordmod + +import ( + "errors" + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const table = models.WithdrawOrder + +// InitIndex 设置index +func initIndex() { + coll := coll(nil) + many := []mongo.IndexModel{ + { + Keys: bson.D{{Key: "name", Value: 1}}, + }, + { + Keys: bson.D{{Key: "amount", Value: 1}}, + }, + { + Keys: bson.D{{Key: "oid", Value: 1}}, + // Options: options.Index().SetUnique(true).SetSparse(true), + }, + { + Keys: bson.D{{Key: "money", Value: 1}}, + }, + { + Keys: bson.D{{Key: "payMoney", Value: 1}}, + }, + { + Keys: bson.D{{Key: "createdAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "updatedAt", Value: 1}}, + }, + { + Keys: bson.D{{Key: "districtCode", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "isDirect", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "uid", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "withdrawType", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "status", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "payType", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + { + Keys: bson.D{{Key: "deductType", Value: 1}, {Key: "createdAt", Value: -1}}, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{{Key: "districtRemark", Value: 1}, {Key: "createdAt", Value: -1}}, + }, + } + if _, err := coll.CreateIndex(many); err != nil { + panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err)) + } +} + +func coll(t *db.MongoTool) *db.MongoTool { + if t == nil { + return mdb.Coll(table) + } + return t.Coll(table) +} + +// InsertWithdrawOrder 订单写入 +func InsertWithdrawOrder(t *db.MongoTool, w *WithdrawOrder) (id primitive.ObjectID, err error) { + if w.CreatedAt.IsZero() { + w.CreatedAt = time.Now() + } + ret, err := coll(t).InsertOne(w) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "InsertWithdrawOrder", table, "InsertOne", err), + log.Any("w", w), + ) + return primitive.NilObjectID, err + } + return ret.InsertedID.(primitive.ObjectID), err +} + +// FindOneAndUpate 查询 +func FindOneAndUpate(id string, status int, set EditSelector) (w WithdrawOrder, err error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneAndUpate", table, "ObjectIDFromHex", err), + log.Any("id", id), + log.Any("status", status), + log.Any("set", set), + ) + return + } + if err = coll(nil).FindOneAndUpdate(&w, bson.M{"_id": oid, "status": status}, bson.M{"$set": set}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOneAndUpate", table, "FindOneAndUpdate", err), + log.Any("id", id), + log.Any("status", status), + log.Any("set", set), + ) + } + return +} + +// FindWithdrawOrderByID 查询 +func FindWithdrawOrderByID(id string) (w WithdrawOrder, err error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindWithdrawOrderByID", table, "ObjectIDFromHex", err), + log.Any("id", id), + ) + return + } + if err = coll(nil).FindOneByID(&w, oid); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindWithdrawOrderByID", table, "FindOneByID", err), + log.Any("id", id), + ) + } + if w.ID.IsZero() { //coll(nil).FindOne过滤了mongo.ErrNoDocuments + err = OrderNotExistError{} + } + return +} + +// FindWithdrawOrderByIDAndStatus 查询 +func FindWithdrawOrderByIDAndStatus(id string, status int) (w WithdrawOrder, err error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindWithdrawOrderByIDAndStatus", table, "ObjectIDFromHex", err), + log.Any("status", status), + log.Any("id", id), + ) + return + } + if err = coll(nil).FindOne(&w, bson.M{"_id": oid, "status": status}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindWithdrawOrderByIDAndStatus", table, "FindOne", err), + log.Any("status", status), + log.Any("id", id), + ) + } + if w.ID.IsZero() { //coll(nil).FindOne过滤了mongo.ErrNoDocuments + err = OrderNotExistError{} + } + return +} + +// FindOrders 查询充值订单 +func FindOrdersByUID(uid uint64, pageNumber, pageSize uint64) (total int64, data []*WithdrawOrder, hasNext bool, err error) { + data = make([]*WithdrawOrder, 0) + f := bson.M{"uid": uid} + skip := int64(pageSize * (pageNumber - 1)) + limit := int64(pageSize + 1) + opts := options.FindOptions{ + Skip: &skip, + Limit: &limit, + Sort: bson.D{{Key: "createdAt", Value: -1}}, + } + if err = coll(nil).Find(&data, f, &opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOrders", table, "Find", err), + log.Any("cond", f), + ) + return + } + total, err = coll(nil).Count(f) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOrders", table, "Count", err), + log.Any("cond", f), + ) + } + if len(data) > int(pageSize) { + hasNext = true + data = data[:pageSize] + } + return +} + +// FindOrders 查询充值订单 +func FindOrders(cond bson.M, opts *options.FindOptions) (total int64, data []*WithdrawOrder, err error) { + if v, ok := cond["districtRemark"]; ok { + cond["districtRemark"] = bson.M{"$regex": v, "$options": "i"} + } + data = make([]*WithdrawOrder, 0) + if err = coll(nil).Find(&data, cond, opts); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOrders", table, "Find", err), + log.Any("cond", cond), + ) + return + } + total, err = coll(nil).Count(cond) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "FindOrders", table, "Count", err), + log.Any("cond", cond), + ) + } + return +} + +// UpdateWithPROCESSINGAndUNKNOWN 回调更改订单状态 +func UpdateWithPROCESSINGAndUNKNOWN(t *db.MongoTool, id string, set EditSelector) error { + set.UpdatedAt = time.Now() + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "ObjectIDFromHex", err), + log.Any("id", id), + log.Any("set", set), + ) + return err + } + res, err := coll(t).UpdateOne(primitive.M{"_id": objID, "status": bson.M{"$in": []int{PROCESSING, UNKNOWN}}}, bson.M{"$set": set}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("id", id), + log.Any("set", set), + ) + return err + } + if res.ModifiedCount <= 0 { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "errors.New", "Invalid account"), + log.Any("id", id), + log.Any("set", set), + ) + return errors.New("Invalid account") + } + return nil +} + +// Update 更新充值订单状态,OId +func Update(t *db.MongoTool, id string, set EditSelector) error { + set.UpdatedAt = time.Now() + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "ObjectIDFromHex", err), + log.Any("id", id), + log.Any("set", set), + ) + return err + } + res, err := coll(t).UpdateOne(primitive.M{"_id": objID}, bson.M{"$set": set}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateOne", err), + log.Any("id", id), + log.Any("set", set), + ) + return err + } + if res.ModifiedCount <= 0 { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "errors.New", "Invalid account"), + log.Any("id", id), + log.Any("set", set), + ) + return errors.New("Invalid account") + } + return nil +} + +// TotalMoney 统计总提现金额 +func TotalMoney(matchers ...Matcher) (int64, error) { + var ret struct { + Money int64 `bson:"money"` //实际到账金额 + } + filter := []bson.M{ + { + "$match": pageopt.MergeM(matchers), + }, + { + "$group": bson.M{ + "_id": nil, + "money": bson.M{"$sum": "$money"}, + }, + }, + } + if err := coll(nil).AggregateDecode(&ret, filter); err != nil { + log.ZapLog.Warn("withdraw TotalMoney AggregateDecode failed", log.E(err)) + return 0, err + } + return ret.Money, nil +} + +// WithdrawAmontByTime 金币提现金额 提现类型,0:代理提现; 1:金币提现 李秋山确认 +func WithdrawAmontByTime(start time.Time, end time.Time, types WithdrawTypes) (int64, error) { + pipeline := []bson.M{ + { + "$match": bson.M{ + // createdAt ∈ [startTime, endTime) + "successAt": bson.M{"$gte": start, "$lt": end}, + "withdrawType": types, + }, + }, + { + "$group": bson.M{ + "_id": nil, + "totalWithdrawAmount": bson.M{"$sum": "$money"}, //Money:提现金额 + }, + }, + } + var ret struct { + TotalWithdrawAmount int64 `bson:"totalWithdrawAmount"` + } + if err := coll(nil).AggregateDecode(&ret, pipeline); err != nil { + return 0, err + } + return ret.TotalWithdrawAmount, nil +} + +// ChannelOrderCountMap channel->订单数 map +func ChannelOrderCountMap(start, end time.Time) (map[string]int64, error) { + filter := bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + list := []struct { + ID primitive.ObjectID `bson:"_id,omitempty"` //流水id + Channel string `bson:"channel"` //渠道类型 金鱼 + }{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[string]int64, len(list)) + for _, v := range list { + m[v.Channel] += 1 + } + return m, nil +} + +// ChannelOrderMoneyMap channel->订单金额 map +func ChannelOrderMoneyMap(start, end time.Time) (map[string]int64, error) { + filter := bson.M{ + "createdAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + list := []struct { + Channel string `bson:"channel"` //渠道类型 鲨鱼 金鱼 + Money int64 `bson:"money"` //提现金额 + }{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[string]int64, len(list)) + for _, v := range list { + m[v.Channel] += v.Money + } + return m, nil +} + +// ChannelCompleteOrderCountMap channel->完成订单数 map +func ChannelCompleteOrderCountMap(start, end time.Time) (map[string]int64, error) { + filter := bson.M{ + "successAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + list := []struct { + ID primitive.ObjectID `bson:"_id,omitempty"` //流水id + Channel string `bson:"channel"` //渠道类型 鲨鱼 金鱼 + }{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[string]int64, len(list)) + for _, v := range list { + m[v.Channel] += 1 + } + return m, nil +} + +// ChannelCompleteOrderAmountMap channel->完成订单的实际到账金额 map +func ChannelCompleteOrderPayMoneyMap(start, end time.Time) (map[string]int64, error) { + filter := bson.M{ + "successAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + list := []struct { + Channel string `bson:"channel"` //渠道类型 鲨鱼 金鱼 + PayMoney int64 `bson:"payMoney"` //实际到账金额 李秋山确认 + }{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[string]int64, len(list)) + for _, v := range list { + m[v.Channel] += v.PayMoney + } + return m, nil +} + +// ChannelReceivedOrderCountMap channel->已出款订单数 map +func ChannelReceivedOrderCountMap(start, end time.Time) (map[string]int64, error) { + filter := bson.M{ + "receivedAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + list := []struct { + ID primitive.ObjectID `bson:"_id,omitempty"` //流水id + Channel string `bson:"channel"` //渠道类型 鲨鱼 金鱼 + }{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[string]int64, len(list)) + for _, v := range list { + m[v.Channel] += 1 + } + return m, nil +} + +// ChannelReceivedMoneyMap channel->已出款金额 map +func ChannelReceivedMoneyMap(start, end time.Time) (map[string]int64, error) { + filter := bson.M{ + "receivedAt": bson.M{ + "$gte": start, + "$lt": end, + }, + } + list := []struct { + Channel string `bson:"channel"` //渠道类型 鲨鱼 金鱼 + PayMoney int64 `bson:"payMoney"` //实际到账金额 李秋山确认 + }{} + if err := coll(nil).Find(&list, filter); err != nil { + return nil, err + } + m := make(map[string]int64, len(list)) + for _, v := range list { + m[v.Channel] += v.PayMoney + } + return m, nil +} + +func UpdateOne(t *db.MongoTool, filter bson.M, update bson.M) (rr error) { + n, err := coll(t).UpdateOne(filter, bson.M{"$set": update}) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "update", table, "updateOne", err)) + return err + } + if n.ModifiedCount <= 0 { + return errors.New("not modified") + } + return nil +} diff --git a/models/v/wdtaxmod/type.go b/models/v/wdtaxmod/type.go new file mode 100644 index 0000000..711e723 --- /dev/null +++ b/models/v/wdtaxmod/type.go @@ -0,0 +1,22 @@ +package wdtaxmod + +import "91porn-server/common/db" + +// WithdrawTariff 提现税率配置修改 +type WithdrawTariff struct { + ID int `bson:"_id"` + CashTax int `json:"cashTax,omitempty" bson:"cashTax"` // 代理提现比率 + CoinTax int `json:"coinTax,omitempty" bson:"coinTax"` // 金币提现比率 + DiscCashTax int `json:"discCashTax,omitempty" bson:"discCashTax"` // 商区自提现比率 + DiscFinanceTax int `json:"discFinanceTax,omitempty" bson:"discFinanceTax"` // 商区财务打款比率 + GameTax int `json:"gameTax" bson:"gameTax"` // qp提现比率 + SuperUserCoinTax int `json:"superUserCoinTax,omitempty" bson:"superUserCoinTax"` // 大V金币提现比率 +} + +var ( + mdb *db.MongoDB +) + +func Init() { + mdb = db.Init(table) +} diff --git a/models/v/wdtaxmod/web.go b/models/v/wdtaxmod/web.go new file mode 100644 index 0000000..0637637 --- /dev/null +++ b/models/v/wdtaxmod/web.go @@ -0,0 +1,10 @@ +package wdtaxmod + +// WithdrawTariffSelector 提现税率配置修改 +type WithdrawTariffSelector struct { + CashTax *int `json:"cashTax,omitempty" bson:"cashTax,omitempty"` //代理提现比率 + CoinTax *int `json:"coinTax,omitempty" bson:"coinTax,omitempty"` //金币提现比率 + DiscCashTax *int `json:"discCashTax,omitempty" bson:"discCashTax,omitempty"` //商区提现比率 + DiscFinanceTax *int `json:"discFinanceTax,omitempty" bson:"discFinanceTax,omitempty"` //商区财务打款比率 + GameTax *int `json:"gameTax,omitempty" bson:"gameTax,omitempty"` //qp提现比率 +} diff --git a/models/v/wdtaxmod/withdrawtariff.go b/models/v/wdtaxmod/withdrawtariff.go new file mode 100644 index 0000000..3bc0d06 --- /dev/null +++ b/models/v/wdtaxmod/withdrawtariff.go @@ -0,0 +1,36 @@ +package wdtaxmod + +import ( + "fmt" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + + "go.mongodb.org/mongo-driver/bson" +) + +const table = models.WithdrawTariff + +func coll() *db.MongoTool { + return mdb.Coll(table) +} + +func GetWithDrawTariff() (wt WithdrawTariff, err error) { + if err = coll().FindOne(&wt, bson.M{"_id": 1}); err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetWithDrawTariff", table, "FindOne", err)) + } + return +} + +func UpdateWithDrawTariff(set WithdrawTariffSelector) (modifyCount int64, err error) { + res, err := coll().UpsertOne(bson.M{"_id": 1}, bson.M{ + "$set": set, + }) + if err != nil { + log.Warn(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "UpdateWithDrawTariff", table, "UpsertOne", err)) + return + } + modifyCount = res.ModifiedCount + return +} diff --git a/project.md b/project.md new file mode 100644 index 0000000..c966803 --- /dev/null +++ b/project.md @@ -0,0 +1,124 @@ +--- +项目名称: 91PORN #如果在dmp则使用dmp的名称 如果不在则使用自定义名称 +一级部门: 北斗 #请填写一级部门名称 北斗 +二级部门: 瑞升 #请填写二级部门名称,无则留空, 如无极 + +DMP编码: JHA-204 #无则留空 + +项目类型: app + # 可选值: + # H5 / APP / PC / 服务 / 组件 / 工具 / AI / 中间件 + +开发语言: GO + # 可选值: + # GO / Java / PHP / Node + +项目负责人: DN8276 # 员工编号, 多个用逗号隔开,这里的负责是这套代码的负责人 + +运维负责人: DN9460,DN9651,DN4792 # + +产品负责人: YY1165,YY1145,YY1208 # 张天意,李云霄,邦妮 + + +项目成员: DN8276 + +项目状态: 开发中 + # 可选值: + # 开发中 / 维护中 / 已停止 / 已归档 + +项目级别: 高 + # 可选值: + # 高 / 中 / 低 , 北斗体系 s和a为 高 b为中 c d为低 + +核心分支: main + # 即部署时候使用的分支,可选值: + # master / main / dev / release + +代码仓库: + - ssh://git@bd-git-sh.jsyyds.com:8082/ruisheng/jha-204-91pron/backend/91porn-server.git + +最后审计人: + +最后审计时间: + +告警群: + +备注: + +--- + +# 1. 项目说明 + +91PORN后端服务,基于 Go 1.23 + Gin 框架,采用多服务架构: + +- **APP API (`app/`)**: 面向客户端的接口服务,包含用户、视频、搜索、支付、会员、排行、抽奖、发布等模块 +- **Web API (`web/`)**: 面向管理后台的接口服务,包含运营管理、数据配置、内容审核等模块 +- **SKD (`skd/`)**: 后台定时任务与异步处理服务,包含定时任务 (`job/`)、一次性脚本 (`once/`)、数据导出等 +- **Swagger (`main.go`)**: API 文档服务,端口 20114 + +项目目录结构: + +``` +91porn-server/ +├── app/ # APP 端 API 服务 (api/router/service/middleware/proto/appg) +├── web/ # Web 管理后台 API 服务 (api/router/service/middleware/proto/webg) +├── skd/ # 后台任务服务 (job/once/service/skdg) +├── common/ # 公共模块 (cache/db/redis/elastic/file/email/log/tg/httputil/stderr 等) +├── models/ # 数据模型层 (Init/commod/v/*) +├── middleware/ # 全局中间件 (CORS/日志/IP/RequestID/UA) +├── generate/ # 代码生成工具 +├── scripts/ # 运维/数据脚本 +├── swagger/ # Swagger 文档 +├── tests/ # 测试 +├── Makefile # 构建脚本 +└── Dockerfile # Docker 构建文件 +``` + + +# 2. 基础设施与中间件 + +- MongoDB(主数据库,通过 go.mongodb.org/mongo-driver) +- Redis(缓存、分布式锁 redsync) +- Kafka(消息队列,Sarama 客户端) +- Elasticsearch(全文搜索) +- AWS S3(文件存储) +- Telegram Bot(告警通知) +- 邮件服务 + + +# 3. 运维部署 + +通过 Shell 脚本交叉编译(`GOOS=linux GOARCH=amd64`)并打包上传到远程服务器,每个服务有独立的部署脚本: + +| 脚本 | 服务 | 产物 | 远程目录 | +|------|------|------|----------| +| `app_update_prod.sh` | APP API | `91porn-app-server` | `/home/server/91porn/app` | +| `web_update_prod.sh` | Web API | `91porn-web-server` | `/home/server/91porn/web` | +| `skd_update_prod.sh` | SKD | `91porn-skd-server` | `/home/server/91porn/skd` | +| `skd_update_test.sh` | SKD (测试) | `91porn-skd-server` | `/server/91porn/91porn-skd` | + +发布流程: +1. 本地交叉编译 `GOOS=linux GOARCH=amd64 go build` +2. 生成 MD5 校验文件 +3. 打包为 `.tar.gz` +4. 通过 `scp` 上传到远程服务器 +5. 发送 Telegram 通知到服务器小组群 +6. 测试环境额外通过 SSH 远程解压并 `supervisorctl restart` 重启服务 + + +# 4. 风险说明 + +- Kafka 消息积压可能导致异步任务延迟 +- Redis 热 Key 风险(缓存层使用广泛) +- MongoDB 大查询性能风险 +- 依赖外部服务:AWS S3、Telegram、邮件、Elasticsearch + + +# 5. 历史事故(可选) +- 暂无 + +# 6. 交接说明(可选) + +- 各服务配置文件位于各自目录下的 `config/` 文件夹(JSON 格式) +- 加密密钥文件:`enc.key`、`enc_pms.key`、`enkeymt.key`,注意保密 +- APP 端包含请求加密中间件(RSA),密钥在 `app/middleware/requestEncrypt/` diff --git a/script/createAuthEnum.js b/script/createAuthEnum.js new file mode 100644 index 0000000..ed485b6 --- /dev/null +++ b/script/createAuthEnum.js @@ -0,0 +1,123 @@ +let tree = new Map([ + [ + { + "name": "用户管理", + "path": "/user", + "icon": "icon" + }, + [ + { + "name": "查询登陆日志", + "path": "/user/查询登陆日志", + "icon": "icon" + }, + { + "name": "查询观看日志", + "path": "/user/查询观看日志", + "icon": "icon" + }, + ] + ], + [ + { + "name": "管理员", + "path": "/admin", + "icon": "icon" + }, + [ + { + "name": "查询登陆日志", + "path": "/admin/查询登陆日志", + "icon": "icon" + }, + { + "name": "查询权限", + "path": "/admin/查询权限", + "icon": "icon" + }, + ] + ], + [ + { + "name": "查询权限", + "path": "/admin/查询权限", + "icon": "icon" + }, + [ + { + "name": "修改", + "path": "/admin/查询权限/修改", + "icon": "icon" + }, + { + "name": "插入", + "path": "/admin/查询权限/插入", + "icon": "icon" + }, + ] + ] +]); + +let coll = "auth_menu" +const MongoClient = require('mongodb').MongoClient; +let dburl = "mongodb://192.168.1.249:27017,192.168.1.249:27018,192.168.1.249:27019/video?replicaSet=mongoRepl&maxPoolSize=200&connectTimeout=30"; + +MongoClient.connect(dburl, {useUnifiedTopology: true, useNewUrlParser: true}, (err, db) => { + if (err) { + console.log('数据库连接失败!'); + return; + } + ; + + let videoDB = db.db("video"); + let ps = Array.from(tree.keys()).map(key => { + return new Promise(function (resolve, rejcet) { + videoDB.collection(coll).findOneAndUpdate( + {path: key.path}, + {$set: key}, + { + upsert: true, + new: true + }, + (err, result) => { + if (err) { + rejcet('数据更新失败' + err) + return + } + ; + resolve({id: result.value._id, array: tree.get(key)}) + }); + }) + }) + + Promise.all(ps).then(pairs => { + let pss = pairs.map(pair => { + let ps = pair.array.map(e => { + return new Promise(function (resolve, rejcet) { + videoDB.collection(coll).updateOne({path: e.path}, + {$set: {parent: pair.id, ...e}}, + {upsert: true}, + (err, result) => { + if (err) { + rejcet('数据插入失败!' + err) + return + } + ; + resolve() + }); + }) + }) + return ps + }) + pArry = pss.reduce((a, c) => { + return a.concat(a, c) + }, []) + return Promise.all(pArry) + }).then(() => { + console.log('完成!'); + db.close() + }).catch(err => { + console.log(err); + db.close() + }) +}) diff --git a/script/createAuthority.js b/script/createAuthority.js new file mode 100644 index 0000000..5253e83 --- /dev/null +++ b/script/createAuthority.js @@ -0,0 +1,82 @@ +let tree = new Map([ + [ + { + "role": "admin", + }, + [ + "/user", + "/user/查询登陆日志", + "/user/查询观看日志", + + "/admin", + "/admin/查询登陆日志", + "/admin/查询权限", + ] + ], +]); + +let authMenuColl = "auth_menu" +let authorityColl = "authority" +const MongoClient = require('mongodb').MongoClient; +let dburl = "mongodb://192.168.1.249:27017,192.168.1.249:27018,192.168.1.249:27019/video?replicaSet=mongoRepl&maxPoolSize=200&connectTimeout=30"; + +MongoClient.connect(dburl, {useUnifiedTopology: true, useNewUrlParser: true}, (err, db) => { + if (err) { + console.log('数据库连接失败!'); + return; + } + ; + + let videoDB = db.db("video"); + let pArray = [] + for ([key, value] of tree) { + let ps = value.map(e => { + return new Promise(function (resolve, rejcet) { + videoDB.collection(authMenuColl).findOne({"path": e}, (err, result) => { + if (err) { + rejcet('查询失败!' + err) + return + } + ; + resolve(result._id) + }); + }) + }) + + let p = Promise.all(ps).then(ids => { + return new Promise(function (resolve, rejcet) { + videoDB.collection(authorityColl).updateOne( + { + role: key.role, + }, + { + $set: { + ...key, + auth: ids, + createdAt: (new Date()), + updatedAt: (new Date()), + } + }, + {upsert: true}, + (err) => { + if (err) { + rejcet('inset失败!' + err) + return + } + ; + resolve() + }) + }) + + }) + pArray.push(p) + } + + Promise.all(pArray).then(() => { + db.close() + console.log('完成!'); + }).catch(err => { + db.close() + console.log(err); + }) +}) \ No newline at end of file diff --git a/script/golang/gen_qr_code/main.go b/script/golang/gen_qr_code/main.go new file mode 100644 index 0000000..cebad31 --- /dev/null +++ b/script/golang/gen_qr_code/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "os" + + "91porn-server/app/appg" + "91porn-server/app/middleware/authuser" + "91porn-server/common/conf" + "91porn-server/common/ysqr" +) + +func main() { + content := ysqr.Content{ + UID: 19230828, + T: ysqr.Login, + LoginClaims: ysqr.LoginClaims{ + DevID: "dd380c8973159955f3830a573ad6ba5f", + }, + } + fmt.Println(content.String(authuser.GetTokenSecret())) +} + +func init() { + cfg := appg.GlobalConfig{} + if err := conf.LoadJSON("config/app.json", &cfg); err != nil { + fmt.Printf("startUp get config error: %v\n", err) + os.Exit(1) + } + appg.Conf = &cfg +} diff --git a/script/golang/seed_adver_abtest/main.go b/script/golang/seed_adver_abtest/main.go new file mode 100644 index 0000000..4ecda53 --- /dev/null +++ b/script/golang/seed_adver_abtest/main.go @@ -0,0 +1,410 @@ +package main + +import ( + "91porn-server/common" + "91porn-server/common/conf" + "91porn-server/common/constant" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/Init" + "91porn-server/models/commod" + "91porn-server/skd/job/dailyAdverCalc" + "91porn-server/skd/skdg" + "context" + "flag" + "fmt" + "math" + "os" + "regexp" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const ( + defaultConfigPath = "config/skd.json" + defaultSeedTag = "abtest-seed" + defaultDistrictCode = "ABTEST_SEED" + defaultVersion = "1.0.6" + defaultStartDate = "2030-01-01" + defaultUIDBase = 990000000000000 + batchSize = 1000 +) + +type seedConfig struct { + ConfigPath string + SeedTag string + DistrictCode string + Version string + StartDate string + CohortDays int + ActiveDays int + UsersPerGroup int + UIDBase int64 + Cleanup bool + RebuildRetention bool + DryRun bool +} + +type seedSummary struct { + RegDay time.Time + AdGroup commod.AdGroup + CohortSize int + ActiveCounts []int +} + +func main() { + time.Local, _ = time.LoadLocation("Asia/Shanghai") + + cfg := parseFlags() + if err := validateConfig(cfg); err != nil { + fmt.Fprintf(os.Stderr, "invalid config: %v\n", err) + os.Exit(1) + } + + log.Init(log.Options{Level: "info"}) + + loc, _ := time.LoadLocation("Asia/Shanghai") + startDate, err := time.ParseInLocation(time.DateOnly, cfg.StartDate, loc) + if err != nil { + fmt.Fprintf(os.Stderr, "parse start date failed: %v\n", err) + os.Exit(1) + } + startDate = common.NormalizeDate(startDate) + + if err := initMongo(cfg.ConfigPath); err != nil { + fmt.Fprintf(os.Stderr, "init mongo failed: %v\n", err) + os.Exit(1) + } + defer db.CloseDS() + Init.InitMongo() + + users, stats, summaries := buildSeedDocs(cfg, startDate, loc) + endActiveDate := startDate.AddDate(0, 0, cfg.CohortDays+cfg.ActiveDays-2) + + printPlan(cfg, startDate, endActiveDate, summaries) + if cfg.DryRun { + return + } + + if cfg.Cleanup { + if err := cleanupSeedData(cfg, startDate, endActiveDate); err != nil { + fmt.Fprintf(os.Stderr, "cleanup failed: %v\n", err) + os.Exit(1) + } + } + + if err := insertInBatches(models.UserTable, users); err != nil { + fmt.Fprintf(os.Stderr, "insert users failed: %v\n", err) + os.Exit(1) + } + if err := insertInBatches(models.UserAdverStat, stats); err != nil { + fmt.Fprintf(os.Stderr, "insert user_adver_stat failed: %v\n", err) + os.Exit(1) + } + + if cfg.RebuildRetention { + if err := rebuildRetentionRange(startDate, endActiveDate); err != nil { + fmt.Fprintf(os.Stderr, "rebuild retention failed: %v\n", err) + os.Exit(1) + } + } + + fmt.Printf("\nseed completed: users=%d stats=%d rebuildRetention=%v\n", len(users), len(stats), cfg.RebuildRetention) +} + +func parseFlags() seedConfig { + cfg := seedConfig{} + flag.StringVar(&cfg.ConfigPath, "config", defaultConfigPath, "path to skd json config") + flag.StringVar(&cfg.SeedTag, "seed-tag", defaultSeedTag, "seed tag used in devID/name prefix") + flag.StringVar(&cfg.DistrictCode, "district-code", defaultDistrictCode, "district code written into seeded docs") + flag.StringVar(&cfg.Version, "version", defaultVersion, "android version written into seeded docs") + flag.StringVar(&cfg.StartDate, "start-date", defaultStartDate, "cohort start date in YYYY-MM-DD") + flag.IntVar(&cfg.CohortDays, "cohort-days", 5, "number of registration days to generate") + flag.IntVar(&cfg.ActiveDays, "active-days", 5, "number of active days to generate for each cohort") + flag.IntVar(&cfg.UsersPerGroup, "users-per-group", 30, "base cohort size per ad group per day") + flag.Int64Var(&cfg.UIDBase, "uid-base", defaultUIDBase, "starting uid for seeded users") + flag.BoolVar(&cfg.Cleanup, "cleanup", true, "cleanup previous seeded docs in the target window") + flag.BoolVar(&cfg.RebuildRetention, "rebuild-retention", true, "run CalcRetention for the generated active date range") + flag.BoolVar(&cfg.DryRun, "dry-run", false, "print the plan without writing data") + flag.Parse() + return cfg +} + +func validateConfig(cfg seedConfig) error { + switch { + case cfg.CohortDays <= 0: + return fmt.Errorf("cohort-days must be > 0") + case cfg.ActiveDays <= 0: + return fmt.Errorf("active-days must be > 0") + case cfg.UsersPerGroup <= 0: + return fmt.Errorf("users-per-group must be > 0") + case cfg.UIDBase <= 0: + return fmt.Errorf("uid-base must be > 0") + case strings.TrimSpace(cfg.SeedTag) == "": + return fmt.Errorf("seed-tag must not be empty") + } + return nil +} + +func initMongo(configPath string) error { + cfg := skdg.GlobalConfig{} + if err := conf.LoadJSON(configPath, &cfg); err != nil { + return err + } + + db.InitDS([]db.DBmap{ + {Key: models.VideoDb, URL: cfg.Mongo.VideoDbUrl}, + {Key: models.StatDb, URL: cfg.Mongo.StatDbUrl}, + {Key: models.LogDb, URL: cfg.Mongo.LogDbUrl}, + }, registerPool()) + return nil +} + +func registerPool() []db.Register { + res := make([]db.Register, len(models.RegisterPool)) + for i, v := range models.RegisterPool { + res[i] = db.Register{Key: v.Key, Table: v.Table} + } + return res +} + +func buildSeedDocs(cfg seedConfig, startDate time.Time, loc *time.Location) ([]bson.M, []bson.M, []seedSummary) { + var ( + users []bson.M + stats []bson.M + summaries []seedSummary + nextUID = cfg.UIDBase + ) + + for dayIdx := 0; dayIdx < cfg.CohortDays; dayIdx++ { + regDay := startDate.AddDate(0, 0, dayIdx) + for _, group := range []commod.AdGroup{commod.AdGroupA, commod.AdGroupB, commod.AdGroupC} { + cohortSize := cohortSizeFor(group, dayIdx, cfg.UsersPerGroup) + activeCounts := make([]int, cfg.ActiveDays) + cohortUIDs := make([]int64, 0, cohortSize) + + for userIdx := 0; userIdx < cohortSize; userIdx++ { + uid := nextUID + nextUID++ + cohortUIDs = append(cohortUIDs, uid) + + createdAt := time.Date(regDay.Year(), regDay.Month(), regDay.Day(), 10, userIdx%50, 0, 0, loc) + devID := fmt.Sprintf("%s-%s-%02d-%03d", cfg.SeedTag, group, dayIdx, userIdx) + users = append(users, bson.M{ + "uid": uint64(uid), + "name": fmt.Sprintf("%s_%s_d%02d_u%03d", cfg.SeedTag, group, dayIdx, userIdx), + "devID": devID, + "districtCode": cfg.DistrictCode, + "sysType": constant.SysTypeAndroid, + "oriVer": cfg.Version, + "adGroup": group, + "createdAt": createdAt, + "updatedAt": createdAt, + }) + } + + for activeOffset := 0; activeOffset < cfg.ActiveDays; activeOffset++ { + activeCount := activeUserCount(cohortSize, groupActivityRate(group, activeOffset)) + activeCounts[activeOffset] = activeCount + activeDay := common.NormalizeDate(regDay.AddDate(0, 0, activeOffset)) + + for userIdx := 0; userIdx < activeCount; userIdx++ { + uid := cohortUIDs[userIdx] + adClick := int64(groupWeight(group) + 1 + activeOffset%2) + appClick := int64(1) + payCount, payTotal := payForUser(group, activeOffset, userIdx) + + stats = append(stats, bson.M{ + "userId": uid, + "date": activeDay, + "districtCode": cfg.DistrictCode, + "sysType": constant.SysTypeAndroid, + "adGroup": group, + "adClick": adClick, + "appClick": appClick, + "totalClick": adClick + appClick, + "payCount": payCount, + "payTotal": payTotal, + "regDay": common.NormalizeDate(regDay), + "version": cfg.Version, + "createdAt": activeDay.Add(2 * time.Hour), + "updatedAt": activeDay.Add(2 * time.Hour), + }) + } + } + + summaries = append(summaries, seedSummary{ + RegDay: common.NormalizeDate(regDay), + AdGroup: group, + CohortSize: cohortSize, + ActiveCounts: activeCounts, + }) + } + } + + return users, stats, summaries +} + +func cohortSizeFor(group commod.AdGroup, dayIdx, base int) int { + return base + dayIdx*2 + groupWeight(group)*3 +} + +func groupWeight(group commod.AdGroup) int { + switch group { + case commod.AdGroupA: + return 0 + case commod.AdGroupB: + return 1 + default: + return 2 + } +} + +func groupActivityRate(group commod.AdGroup, dayOffset int) float64 { + rates := map[commod.AdGroup][]float64{ + commod.AdGroupA: {0.85, 0.60, 0.45, 0.30, 0.20, 0.12, 0.08}, + commod.AdGroupB: {0.70, 0.50, 0.35, 0.22, 0.14, 0.08, 0.05}, + commod.AdGroupC: {0.55, 0.35, 0.20, 0.10, 0.06, 0.03, 0.02}, + } + if dayOffset < len(rates[group]) { + return rates[group][dayOffset] + } + last := rates[group][len(rates[group])-1] + decay := math.Pow(0.7, float64(dayOffset-len(rates[group])+1)) + return last * decay +} + +func activeUserCount(cohortSize int, rate float64) int { + if cohortSize <= 0 || rate <= 0 { + return 0 + } + count := int(math.Round(float64(cohortSize) * rate)) + if count < 0 { + return 0 + } + if count > cohortSize { + return cohortSize + } + return count +} + +func payForUser(group commod.AdGroup, dayOffset, userIdx int) (int64, int64) { + switch group { + case commod.AdGroupA: + if dayOffset <= 2 && userIdx%7 == 0 { + return 1, 1999 + } + case commod.AdGroupB: + if dayOffset <= 1 && userIdx%9 == 0 { + return 1, 1299 + } + case commod.AdGroupC: + if dayOffset == 0 && userIdx%12 == 0 { + return 1, 699 + } + } + return 0, 0 +} + +func cleanupSeedData(cfg seedConfig, startDate, endActiveDate time.Time) error { + userFilter := bson.M{ + "devID": bson.M{ + "$regex": fmt.Sprintf("^%s-", regexp.QuoteMeta(cfg.SeedTag)), + }, + } + statFilter := bson.M{ + "districtCode": cfg.DistrictCode, + "version": cfg.Version, + "regDay": bson.M{ + "$gte": startDate, + "$lt": startDate.AddDate(0, 0, cfg.CohortDays), + }, + "date": bson.M{ + "$gte": startDate, + "$lte": endActiveDate, + }, + } + + if _, err := db.BaseDAO(models.UserTable, nil).DeleteMany(userFilter); err != nil { + return err + } + if _, err := db.BaseDAO(models.UserAdverStat, nil).DeleteMany(statFilter); err != nil { + return err + } + + if cfg.RebuildRetention { + retentionFilter := bson.M{ + "date": bson.M{ + "$gte": startDate, + "$lt": startDate.AddDate(0, 0, cfg.CohortDays), + }, + "adGroup": bson.M{ + "$in": []commod.AdGroup{commod.AdGroupA, commod.AdGroupB, commod.AdGroupC}, + }, + } + if _, err := db.BaseDAO(models.DailyRetention, nil).DeleteMany(retentionFilter); err != nil { + return err + } + } + return nil +} + +func insertInBatches(table string, docs []bson.M) error { + if len(docs) == 0 { + return nil + } + dao := db.BaseDAO(table, nil) + for start := 0; start < len(docs); start += batchSize { + end := start + batchSize + if end > len(docs) { + end = len(docs) + } + batch := docs[start:end] + writes := make([]mongo.WriteModel, 0, len(batch)) + for _, doc := range batch { + writes = append(writes, mongo.NewInsertOneModel().SetDocument(doc)) + } + if _, err := dao.Bulk(writes, options.BulkWrite().SetOrdered(false)); err != nil { + return err + } + } + return nil +} + +func rebuildRetentionRange(startDate, endActiveDate time.Time) error { + ctx := context.Background() + for d := common.NormalizeDate(startDate); !d.After(endActiveDate); d = d.AddDate(0, 0, 1) { + if err := dailyAdverCalc.CalcRetention(ctx, d); err != nil { + return fmt.Errorf("calc retention for %s failed: %w", d.Format(time.DateOnly), err) + } + } + return nil +} + +func printPlan(cfg seedConfig, startDate, endActiveDate time.Time, summaries []seedSummary) { + fmt.Printf("seed tag: %s\n", cfg.SeedTag) + fmt.Printf("date range: cohort=%s ~ %s, active=%s ~ %s\n", + startDate.Format(time.DateOnly), + startDate.AddDate(0, 0, cfg.CohortDays-1).Format(time.DateOnly), + startDate.Format(time.DateOnly), + endActiveDate.Format(time.DateOnly), + ) + fmt.Printf("base users/group/day: %d, cleanup=%v, rebuildRetention=%v, dryRun=%v\n\n", + cfg.UsersPerGroup, cfg.Cleanup, cfg.RebuildRetention, cfg.DryRun, + ) + + for _, item := range summaries { + rates := make([]string, 0, len(item.ActiveCounts)) + for _, active := range item.ActiveCounts { + rates = append(rates, fmt.Sprintf("%.4f", float64(active)/float64(item.CohortSize))) + } + fmt.Printf("%s group=%s cohort=%d active=%v rates=%v\n", + item.RegDay.Format(time.DateOnly), item.AdGroup, item.CohortSize, item.ActiveCounts, rates, + ) + } +} diff --git a/scripts/repair_test_drama_tags.py b/scripts/repair_test_drama_tags.py new file mode 100755 index 0000000..84b278e --- /dev/null +++ b/scripts/repair_test_drama_tags.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 + +import argparse +import json +import os +from datetime import datetime, timezone +from pathlib import Path + +THEATER_TAG_TYPE = 5 +THEATER_KIND = 8 +THEATER_DIMENSION = "剧场" + + +def find_video_mongodb_uri(config): + def walk(value, path=""): + if isinstance(value, dict): + for key, child in value.items(): + child_path = f"{path}.{key}" if path else key + yield from walk(child, child_path) + elif isinstance(value, list): + for index, child in enumerate(value): + yield from walk(child, f"{path}[{index}]") + else: + yield path, value + + for path, value in walk(config): + if isinstance(value, str) and value.startswith("mongodb://") and "video" in path.lower(): + return value + raise RuntimeError("video MongoDB URI was not found in the supplied config") + + +def parse_args(): + parser = argparse.ArgumentParser(description="Repair one test short drama that references animation tags") + parser.add_argument("--config", required=True, help="test app.json path") + parser.add_argument("--media-id", required=True, help="target short-drama ObjectID") + parser.add_argument("--backup", required=True, help="non-existing rollback evidence path") + return parser.parse_args() + + +def main(): + args = parse_args() + from bson import ObjectId, json_util + from pymongo import MongoClient + + media_id = ObjectId(args.media_id) + backup_path = Path(args.backup) + if backup_path.exists(): + raise RuntimeError("backup path already exists") + + with open(args.config, "r", encoding="utf-8") as config_file: + uri = find_video_mongodb_uri(json.load(config_file)) + + client = MongoClient(uri, serverSelectionTimeoutMS=5000, connectTimeoutMS=5000) + database = client.get_default_database() + media = database.media.find_one({"_id": media_id}, max_time_ms=5000) + if media is None: + raise RuntimeError("target media does not exist") + if media.get("mediaType") != "drama" or media.get("updatedAct") != "codex-test-seed": + raise RuntimeError("target is not the expected seeded test short drama") + + old_tag_ids = list(media.get("tags") or []) + if len(old_tag_ids) > 20: + raise RuntimeError("target has too many tags") + old_tags = list(database.media_tag.find({"_id": {"$in": old_tag_ids}}, max_time_ms=5000)) + old_tags_by_id = {tag["_id"]: tag for tag in old_tags} + if len(old_tags_by_id) != len(set(old_tag_ids)): + raise RuntimeError("one or more source tags do not exist") + + module = None + module_id = media.get("mId") + if isinstance(module_id, ObjectId) and module_id != ObjectId("000000000000000000000000"): + module = database.module_conf.find_one({"_id": module_id}, {"moduleName": 1, "type": 1}, max_time_ms=5000) + if module is None or module.get("type") != 12: + raise RuntimeError("target is not attached to the expected short-drama module") + + backup = { + "createdAt": datetime.now(timezone.utc), + "media": media, + "sourceTags": old_tags, + "insertedTagIds": [], + } + backup_path.parent.mkdir(parents=True, exist_ok=True) + descriptor = os.open(backup_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(descriptor, "w", encoding="utf-8") as backup_file: + backup_file.write(json_util.dumps(backup, ensure_ascii=False)) + + new_tag_ids = [] + inserted_tag_ids = [] + now = datetime.now(timezone.utc) + for old_tag_id in old_tag_ids: + old_tag = old_tags_by_id[old_tag_id] + name = str(old_tag.get("name") or "").strip() + if not name: + raise RuntimeError("source tag has an empty name") + theater_tag = database.media_tag.find_one( + { + "name": name, + "dimension": THEATER_DIMENSION, + "type": THEATER_TAG_TYPE, + "kind": THEATER_KIND, + "isDelete": False, + }, + {"_id": 1}, + max_time_ms=5000, + ) + if theater_tag is None: + result = database.media_tag.insert_one( + { + "name": name, + "dimension": THEATER_DIMENSION, + "type": THEATER_TAG_TYPE, + "kind": THEATER_KIND, + "active": True, + "isDiscovery": False, + "library": False, + "sort": 0, + "isDelete": False, + "updatedAct": "codex-test-repair", + "createdAt": now, + "updateTime": now, + } + ) + theater_tag = {"_id": result.inserted_id} + inserted_tag_ids.append(result.inserted_id) + new_tag_ids.append(theater_tag["_id"]) + + update = { + "kind": THEATER_KIND, + "tags": new_tag_ids, + "moduleName": module["moduleName"], + "sId": ObjectId("000000000000000000000000"), + "sectionName": "", + "sectionSort": 0, + "updatedAct": "codex-test-repair", + "updateTime": now, + } + result = database.media.update_one( + {"_id": media_id, "mediaType": "drama", "updatedAct": "codex-test-seed"}, + {"$set": update}, + ) + if result.matched_count != 1 or result.modified_count != 1: + raise RuntimeError("target changed before repair completed") + + if inserted_tag_ids: + backup["insertedTagIds"] = inserted_tag_ids + temporary_path = backup_path.with_suffix(backup_path.suffix + ".tmp") + descriptor = os.open(temporary_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(descriptor, "w", encoding="utf-8") as backup_file: + backup_file.write(json_util.dumps(backup, ensure_ascii=False)) + os.replace(temporary_path, backup_path) + + print( + f"repaired=1 media_id={media_id} tags={len(new_tag_ids)} " + f"created_tags={len(inserted_tag_ids)} backup={backup_path}" + ) + client.close() + + +if __name__ == "__main__": + main() diff --git a/scripts/seed_checkin/main.go b/scripts/seed_checkin/main.go new file mode 100644 index 0000000..4008d87 --- /dev/null +++ b/scripts/seed_checkin/main.go @@ -0,0 +1,161 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const mongoURI = "mongodb://admin:TESTwfeZYP0kjBdy8DAUdwU@172.104.181.99:57017,172.104.181.99:57018,172.104.181.99:57019/91porn?replicaSet=yctest&maxPoolSize=200&connectTimeout=30&authSource=admin" + +// SeedCheckin 为指定用户生成N天连续签到数据,使得今天是第N+1天签到 +// userIds: 用户ID列表 (uint64, 对应本项目的 UID) +// n: 已签到天数(生成过去N天的记录,今天为第N+1天) +func SeedCheckin(ctx context.Context, db *mongo.Database, userIds []uint64, n int) error { + prizeColl := db.Collection("checkin_prize") + checkinColl := db.Collection("user_checkin") + + // 东八区 + loc, _ := time.LoadLocation("Asia/Shanghai") + now := time.Now().In(loc) + today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) + + fmt.Printf("Now: %v\n", now) + fmt.Printf("Today (CST 00:00): %v\n", today) + fmt.Printf("生成 %d 天签到记录,今天签到为第 %d 天\n", n, n+1) + + // 1. 查奖励配置(非大奖) + cursor, err := prizeColl.Find(ctx, bson.M{ + "status": true, + "checkinType": int64(1), + }, options.Find().SetSort(bson.M{"checkinDays": 1})) + if err != nil { + return fmt.Errorf("query prizes failed: %w", err) + } + var prizeConfigs []bson.M + if err := cursor.All(ctx, &prizeConfigs); err != nil { + return fmt.Errorf("decode prizes failed: %w", err) + } + + // 按天数映射奖品ID(本项目用 ObjectID) + dayPrizes := make(map[int64][]primitive.ObjectID) + for _, p := range prizeConfigs { + days := toInt64(p["checkinDays"]) + bigPrize, _ := p["bigPrize"].(bool) + if !bigPrize { + if oid, ok := p["_id"].(primitive.ObjectID); ok { + dayPrizes[days] = append(dayPrizes[days], oid) + } + } + } + + // 2. 删除这些用户的所有签到记录 + delResult, err := checkinColl.DeleteMany(ctx, bson.M{ + "userId": bson.M{"$in": userIds}, + }) + if err != nil { + return fmt.Errorf("delete failed: %w", err) + } + fmt.Printf("\nDeleted %d existing records\n", delResult.DeletedCount) + + // 3. 插入过去N天的签到记录(东八区时间) + for _, userId := range userIds { + fmt.Printf("\n--- userId=%d ---\n", userId) + for day := n; day >= 1; day-- { + date := today.AddDate(0, 0, -day) + continuouslyDays := int64(n - day + 1) + cumulativeDays := continuouslyDays + prizes := dayPrizes[continuouslyDays] + + doc := bson.M{ + "_id": primitive.NewObjectID(), + "date": date, + "userId": userId, + "prizes": prizes, + "gave": true, + "vipPrizeGave": false, + "continuouslyDays": continuouslyDays, + "cumulativeDays": cumulativeDays, + "isReset": false, + "createdAt": date.Add(10 * time.Hour), + } + + _, err := checkinColl.InsertOne(ctx, doc) + if err != nil { + log.Printf(" Insert failed: %v", err) + continue + } + fmt.Printf(" date=%s contDays=%d prizes=%v ✓\n", + date.Format("2006-01-02 15:04:05 MST"), + continuouslyDays, prizes) + } + } + + // 4. 验证 + fmt.Println("\n=== Verification ===") + loc2, _ := time.LoadLocation("Asia/Shanghai") + for _, userId := range userIds { + cur, _ := checkinColl.Find(ctx, bson.M{ + "userId": userId, + }, options.Find().SetSort(bson.M{"date": -1})) + var records []bson.M + _ = cur.All(ctx, &records) + fmt.Printf("userId=%d: %d records, 今天签到为第%d天\n", userId, len(records), len(records)+1) + for _, r := range records { + var t time.Time + switch d := r["date"].(type) { + case primitive.DateTime: + t = d.Time().In(loc2) + case time.Time: + t = d.In(loc2) + } + fmt.Printf(" date=%s contDays=%v prizes=%v\n", + t.Format("2006-01-02 15:04:05 MST"), + r["continuouslyDays"], r["prizes"]) + } + } + + fmt.Println("\nDone!") + return nil +} + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI)) + if err != nil { + log.Fatal("connect failed:", err) + } + defer client.Disconnect(ctx) + + db := client.Database("91porn") + + // ====== 在这里修改参数 ====== + userIds := []uint64{303458} + n := 6 // 生成6天签到记录,今天签到为第7天 + // =========================== + + if err := SeedCheckin(ctx, db, userIds, n); err != nil { + log.Fatal(err) + } +} + +func toInt64(v interface{}) int64 { + switch n := v.(type) { + case int64: + return n + case int32: + return int64(n) + case float64: + return int64(n) + default: + return 0 + } +} diff --git a/scripts/seed_checkin_setup/main.go b/scripts/seed_checkin_setup/main.go new file mode 100644 index 0000000..506c3b4 --- /dev/null +++ b/scripts/seed_checkin_setup/main.go @@ -0,0 +1,404 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const mongoURI = "mongodb://admin:TESTwfeZYP0kjBdy8DAUdwU@172.104.181.99:57017,172.104.181.99:57018,172.104.181.99:57019/91porn?replicaSet=yctest&maxPoolSize=200&connectTimeout=30&authSource=admin" + +// PrizeType 与 prizemod.PrizeType 保持一致 +const ( + PrizeTypeCurrentValue = 1 // 活跃值 + PrizeTypeGold = 2 // 金币 + PrizeTypeVIPCard = 5 // 会员卡 + PrizeTypeIntegral = 8 // 积分 + PrizeTypeAIUndress = 9 // AI脱衣 + PrizeTypeAIChangeFace = 10 // AI换脸 + PrizeTypeVideoCoupon = 11 // 观影券 +) + +const CheckinTypeContinuously = 1 + +// activityPrizeDef 定义基础奖品 +type activityPrizeDef struct { + Name string + Type int + Count int32 + Price int64 + Desc string +} + +// checkinPrizeDef 定义签到奖品配置 +type checkinPrizeDef struct { + Title string + CheckinDays int64 + BigPrize bool + PrizeName string + PrizeRef string // 引用 activityPrizeDef 的 key +} + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI)) + if err != nil { + log.Fatal("connect failed:", err) + } + defer client.Disconnect(ctx) + + db := client.Database("91porn") + now := time.Now() + + // ======================== + // Step 0: 清理上次脚本插入的旧数据 + // ======================== + fmt.Println("=== Step 0: 清理旧签到配置数据 ===") + db.Collection("checkin_config").DeleteMany(ctx, bson.M{}) + db.Collection("checkin_prize").DeleteMany(ctx, bson.M{}) + // 清理上次脚本创建的 activity_prize(按名称前缀匹配) + delRes, _ := db.Collection("activity_prize").DeleteMany(ctx, bson.M{ + "desc": bson.M{"$regex": "^签到奖励|^VIP签到"}, + }) + fmt.Printf(" 清理 activity_prize: %d, checkin_prize: all, checkin_config: all\n\n", delRes.DeletedCount) + + // ======================== + // Step 1: 创建 activity_prize 基础奖品 + // ======================== + fmt.Println("=== Step 1: 创建 activity_prize 基础奖品 ===") + + activityPrizeColl := db.Collection("activity_prize") + + // 定义所有需要的奖品规格 + prizeDefs := map[string]activityPrizeDef{ + // 积分类 + "integral_3": {Name: "签到积分x3", Type: PrizeTypeIntegral, Count: 3, Price: 3, Desc: "签到奖励3积分"}, + "integral_5": {Name: "签到积分x5", Type: PrizeTypeIntegral, Count: 5, Price: 5, Desc: "签到奖励5积分"}, + "integral_8": {Name: "签到积分x8", Type: PrizeTypeIntegral, Count: 8, Price: 8, Desc: "签到奖励8积分"}, + "integral_10": {Name: "签到积分x10", Type: PrizeTypeIntegral, Count: 10, Price: 10, Desc: "签到奖励10积分"}, + "integral_12": {Name: "签到积分x12", Type: PrizeTypeIntegral, Count: 12, Price: 12, Desc: "签到奖励12积分"}, + "integral_15": {Name: "签到积分x15", Type: PrizeTypeIntegral, Count: 15, Price: 15, Desc: "签到奖励15积分"}, + "integral_18": {Name: "签到积分x18", Type: PrizeTypeIntegral, Count: 18, Price: 18, Desc: "签到奖励18积分"}, + "integral_20": {Name: "签到积分x20", Type: PrizeTypeIntegral, Count: 20, Price: 20, Desc: "签到奖励20积分"}, + "integral_25": {Name: "签到积分x25", Type: PrizeTypeIntegral, Count: 25, Price: 25, Desc: "签到奖励25积分"}, + "integral_30": {Name: "签到积分x30", Type: PrizeTypeIntegral, Count: 30, Price: 30, Desc: "签到奖励30积分"}, + "integral_50": {Name: "签到积分x50", Type: PrizeTypeIntegral, Count: 50, Price: 50, Desc: "签到奖励50积分"}, + "integral_80": {Name: "签到积分x80", Type: PrizeTypeIntegral, Count: 80, Price: 80, Desc: "签到奖励80积分"}, + "integral_100": {Name: "签到积分x100", Type: PrizeTypeIntegral, Count: 100, Price: 100, Desc: "签到奖励100积分"}, + // 金币类 + "gold_50": {Name: "签到金币x50", Type: PrizeTypeGold, Count: 50, Price: 50, Desc: "签到奖励50金币"}, + "gold_100": {Name: "签到金币x100", Type: PrizeTypeGold, Count: 100, Price: 100, Desc: "签到奖励100金币"}, + "gold_200": {Name: "签到金币x200", Type: PrizeTypeGold, Count: 200, Price: 200, Desc: "签到奖励200金币"}, + "gold_500": {Name: "签到金币x500", Type: PrizeTypeGold, Count: 500, Price: 500, Desc: "签到奖励500金币"}, + // 观影券 + "video_coupon_1": {Name: "观影券x1", Type: PrizeTypeVideoCoupon, Count: 1, Price: 1, Desc: "签到奖励1张观影券"}, + "video_coupon_2": {Name: "观影券x2", Type: PrizeTypeVideoCoupon, Count: 2, Price: 2, Desc: "签到奖励2张观影券"}, + "video_coupon_3": {Name: "观影券x3", Type: PrizeTypeVideoCoupon, Count: 3, Price: 3, Desc: "签到奖励3张观影券"}, + // AI + "ai_undress_1": {Name: "AI脱衣x1", Type: PrizeTypeAIUndress, Count: 1, Price: 1, Desc: "签到奖励AI脱衣1次"}, + "ai_undress_2": {Name: "AI脱衣x2", Type: PrizeTypeAIUndress, Count: 2, Price: 2, Desc: "签到奖励AI脱衣2次"}, + "ai_changeface_1": {Name: "AI换脸x1", Type: PrizeTypeAIChangeFace, Count: 1, Price: 1, Desc: "签到奖励AI换脸1次"}, + "ai_changeface_2": {Name: "AI换脸x2", Type: PrizeTypeAIChangeFace, Count: 2, Price: 2, Desc: "签到奖励AI换脸2次"}, + // VIP专属奖品 + "vip_integral_5": {Name: "VIP积分x5", Type: PrizeTypeIntegral, Count: 5, Price: 5, Desc: "VIP签到额外5积分"}, + "vip_integral_8": {Name: "VIP积分x8", Type: PrizeTypeIntegral, Count: 8, Price: 8, Desc: "VIP签到额外8积分"}, + "vip_integral_10": {Name: "VIP积分x10", Type: PrizeTypeIntegral, Count: 10, Price: 10, Desc: "VIP签到额外10积分"}, + "vip_integral_15": {Name: "VIP积分x15", Type: PrizeTypeIntegral, Count: 15, Price: 15, Desc: "VIP签到额外15积分"}, + "vip_integral_20": {Name: "VIP积分x20", Type: PrizeTypeIntegral, Count: 20, Price: 20, Desc: "VIP签到额外20积分"}, + "vip_integral_30": {Name: "VIP积分x30", Type: PrizeTypeIntegral, Count: 30, Price: 30, Desc: "VIP签到额外30积分"}, + "vip_integral_50": {Name: "VIP积分x50", Type: PrizeTypeIntegral, Count: 50, Price: 50, Desc: "VIP签到额外50积分"}, + "vip_gold_100": {Name: "VIP金币x100", Type: PrizeTypeGold, Count: 100, Price: 100, Desc: "VIP签到额外100金币"}, + "vip_gold_200": {Name: "VIP金币x200", Type: PrizeTypeGold, Count: 200, Price: 200, Desc: "VIP签到额外200金币"}, + "vip_gold_500": {Name: "VIP金币x500", Type: PrizeTypeGold, Count: 500, Price: 500, Desc: "VIP签到额外500金币"}, + "vip_video_coupon_1": {Name: "VIP观影券x1", Type: PrizeTypeVideoCoupon, Count: 1, Price: 1, Desc: "VIP签到额外1张观影券"}, + "vip_video_coupon_2": {Name: "VIP观影券x2", Type: PrizeTypeVideoCoupon, Count: 2, Price: 2, Desc: "VIP签到额外2张观影券"}, + "vip_ai_undress_1": {Name: "VIP AI脱衣x1", Type: PrizeTypeAIUndress, Count: 1, Price: 1, Desc: "VIP签到额外AI脱衣1次"}, + "vip_ai_undress_2": {Name: "VIP AI脱衣x2", Type: PrizeTypeAIUndress, Count: 2, Price: 2, Desc: "VIP签到额外AI脱衣2次"}, + "vip_ai_changeface_1": {Name: "VIP AI换脸x1", Type: PrizeTypeAIChangeFace, Count: 1, Price: 1, Desc: "VIP签到额外AI换脸1次"}, + "vip_ai_changeface_2": {Name: "VIP AI换脸x2", Type: PrizeTypeAIChangeFace, Count: 2, Price: 2, Desc: "VIP签到额外AI换脸2次"}, + } + + prizeIdMap := make(map[string]primitive.ObjectID) + for key, def := range prizeDefs { + doc := bson.M{ + "name": def.Name, "type": def.Type, "count": def.Count, "price": def.Price, + "sort": 0, "desc": def.Desc, "value": 0, "weights": "0", "level": 1, + "status": true, "validityTime": 0, "image": "", + "updateTime": now, "createTimt": now, + } + result, err := activityPrizeColl.InsertOne(ctx, doc) + if err != nil { + log.Printf(" [WARN] 插入 '%s' 失败: %v", key, err) + continue + } + prizeIdMap[key] = result.InsertedID.(primitive.ObjectID) + } + fmt.Printf(" 共创建 %d 个 activity_prize\n\n", len(prizeIdMap)) + + // ======================== + // Step 2: 创建 checkin_config + // ======================== + fmt.Println("=== Step 2: 配置 checkin_config ===") + configColl := db.Collection("checkin_config") + configDoc := bson.M{ + "enable": true, + "description": "每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!", + "backgroundImage": "", + "rewardBgVideos": []bson.M{ + {"prizeType": PrizeTypeIntegral, "bgMediaUrl": ""}, + {"prizeType": PrizeTypeGold, "bgMediaUrl": ""}, + {"prizeType": PrizeTypeVideoCoupon, "bgMediaUrl": ""}, + {"prizeType": PrizeTypeAIUndress, "bgMediaUrl": ""}, + {"prizeType": PrizeTypeAIChangeFace, "bgMediaUrl": ""}, + }, + "integerExchangeList": []bson.M{ + {"name": "月卡VIP", "icon": ""}, + {"name": "观影券x10", "icon": ""}, + {"name": "AI脱衣x5", "icon": ""}, + {"name": "AI换脸x5", "icon": ""}, + }, + } + configColl.InsertOne(ctx, configDoc) + fmt.Println(" checkin_config 已创建(enable=true)\n") + + // ======================== + // Step 3: 创建 checkin_prize 31天连续签到(普通 + VIP) + // ======================== + fmt.Println("=== Step 3: 创建 checkin_prize 31天签到奖品 ===") + checkinPrizeColl := db.Collection("checkin_prize") + + // 31天普通奖品配置(奖励递增,每周有节奏感) + normalPrizes := []checkinPrizeDef{ + // 第1周:基础奖励 + {Title: "第1天", CheckinDays: 1, PrizeName: "积分x3", PrizeRef: "integral_3"}, + {Title: "第2天", CheckinDays: 2, PrizeName: "积分x5", PrizeRef: "integral_5"}, + {Title: "第3天", CheckinDays: 3, PrizeName: "积分x5", PrizeRef: "integral_5"}, + {Title: "第4天", CheckinDays: 4, PrizeName: "积分x8", PrizeRef: "integral_8"}, + {Title: "第5天", CheckinDays: 5, PrizeName: "积分x8", PrizeRef: "integral_8"}, + {Title: "第6天", CheckinDays: 6, PrizeName: "积分x10", PrizeRef: "integral_10"}, + {Title: "第7天", CheckinDays: 7, PrizeName: "观影券x1", PrizeRef: "video_coupon_1"}, + // 第2周:稳步提升 + {Title: "第8天", CheckinDays: 8, PrizeName: "积分x10", PrizeRef: "integral_10"}, + {Title: "第9天", CheckinDays: 9, PrizeName: "积分x10", PrizeRef: "integral_10"}, + {Title: "第10天", CheckinDays: 10, PrizeName: "积分x12", PrizeRef: "integral_12"}, + {Title: "第11天", CheckinDays: 11, PrizeName: "积分x12", PrizeRef: "integral_12"}, + {Title: "第12天", CheckinDays: 12, PrizeName: "积分x15", PrizeRef: "integral_15"}, + {Title: "第13天", CheckinDays: 13, PrizeName: "金币x50", PrizeRef: "gold_50"}, + {Title: "第14天", CheckinDays: 14, PrizeName: "观影券x1", PrizeRef: "video_coupon_1"}, + // 第3周:中期奖励 + {Title: "第15天", CheckinDays: 15, PrizeName: "积分x15", PrizeRef: "integral_15"}, + {Title: "第16天", CheckinDays: 16, PrizeName: "积分x18", PrizeRef: "integral_18"}, + {Title: "第17天", CheckinDays: 17, PrizeName: "积分x18", PrizeRef: "integral_18"}, + {Title: "第18天", CheckinDays: 18, PrizeName: "积分x20", PrizeRef: "integral_20"}, + {Title: "第19天", CheckinDays: 19, PrizeName: "AI脱衣x1", PrizeRef: "ai_undress_1"}, + {Title: "第20天", CheckinDays: 20, PrizeName: "积分x20", PrizeRef: "integral_20"}, + {Title: "第21天", CheckinDays: 21, PrizeName: "金币x100", PrizeRef: "gold_100"}, + // 第4周:高级奖励 + {Title: "第22天", CheckinDays: 22, PrizeName: "积分x25", PrizeRef: "integral_25"}, + {Title: "第23天", CheckinDays: 23, PrizeName: "积分x25", PrizeRef: "integral_25"}, + {Title: "第24天", CheckinDays: 24, PrizeName: "AI换脸x1", PrizeRef: "ai_changeface_1"}, + {Title: "第25天", CheckinDays: 25, PrizeName: "积分x30", PrizeRef: "integral_30"}, + {Title: "第26天", CheckinDays: 26, PrizeName: "观影券x2", PrizeRef: "video_coupon_2"}, + {Title: "第27天", CheckinDays: 27, PrizeName: "积分x30", PrizeRef: "integral_30"}, + {Title: "第28天", CheckinDays: 28, PrizeName: "金币x200", PrizeRef: "gold_200"}, + // 最后冲刺 + {Title: "第29天", CheckinDays: 29, PrizeName: "积分x50", PrizeRef: "integral_50"}, + {Title: "第30天", CheckinDays: 30, PrizeName: "观影券x3", PrizeRef: "video_coupon_3"}, + {Title: "第31天", CheckinDays: 31, PrizeName: "积分x100", PrizeRef: "integral_100"}, + } + + // 31天VIP专属奖品配置 + vipPrizes := []checkinPrizeDef{ + // 第1周 + {Title: "VIP第1天", CheckinDays: 1, BigPrize: true, PrizeName: "VIP积分x5", PrizeRef: "vip_integral_5"}, + {Title: "VIP第2天", CheckinDays: 2, BigPrize: true, PrizeName: "VIP积分x5", PrizeRef: "vip_integral_5"}, + {Title: "VIP第3天", CheckinDays: 3, BigPrize: true, PrizeName: "VIP积分x8", PrizeRef: "vip_integral_8"}, + {Title: "VIP第4天", CheckinDays: 4, BigPrize: true, PrizeName: "VIP积分x8", PrizeRef: "vip_integral_8"}, + {Title: "VIP第5天", CheckinDays: 5, BigPrize: true, PrizeName: "VIP积分x10", PrizeRef: "vip_integral_10"}, + {Title: "VIP第6天", CheckinDays: 6, BigPrize: true, PrizeName: "VIP积分x10", PrizeRef: "vip_integral_10"}, + {Title: "VIP第7天", CheckinDays: 7, BigPrize: true, PrizeName: "VIP观影券x1", PrizeRef: "vip_video_coupon_1"}, + // 第2周 + {Title: "VIP第8天", CheckinDays: 8, BigPrize: true, PrizeName: "VIP积分x10", PrizeRef: "vip_integral_10"}, + {Title: "VIP第9天", CheckinDays: 9, BigPrize: true, PrizeName: "VIP积分x10", PrizeRef: "vip_integral_10"}, + {Title: "VIP第10天", CheckinDays: 10, BigPrize: true, PrizeName: "VIP积分x15", PrizeRef: "vip_integral_15"}, + {Title: "VIP第11天", CheckinDays: 11, BigPrize: true, PrizeName: "VIP积分x15", PrizeRef: "vip_integral_15"}, + {Title: "VIP第12天", CheckinDays: 12, BigPrize: true, PrizeName: "VIP金币x100", PrizeRef: "vip_gold_100"}, + {Title: "VIP第13天", CheckinDays: 13, BigPrize: true, PrizeName: "VIP积分x15", PrizeRef: "vip_integral_15"}, + {Title: "VIP第14天", CheckinDays: 14, BigPrize: true, PrizeName: "VIP AI脱衣x1", PrizeRef: "vip_ai_undress_1"}, + // 第3周 + {Title: "VIP第15天", CheckinDays: 15, BigPrize: true, PrizeName: "VIP积分x15", PrizeRef: "vip_integral_15"}, + {Title: "VIP第16天", CheckinDays: 16, BigPrize: true, PrizeName: "VIP积分x20", PrizeRef: "vip_integral_20"}, + {Title: "VIP第17天", CheckinDays: 17, BigPrize: true, PrizeName: "VIP积分x20", PrizeRef: "vip_integral_20"}, + {Title: "VIP第18天", CheckinDays: 18, BigPrize: true, PrizeName: "VIP观影券x1", PrizeRef: "vip_video_coupon_1"}, + {Title: "VIP第19天", CheckinDays: 19, BigPrize: true, PrizeName: "VIP积分x20", PrizeRef: "vip_integral_20"}, + {Title: "VIP第20天", CheckinDays: 20, BigPrize: true, PrizeName: "VIP AI换脸x1", PrizeRef: "vip_ai_changeface_1"}, + {Title: "VIP第21天", CheckinDays: 21, BigPrize: true, PrizeName: "VIP金币x200", PrizeRef: "vip_gold_200"}, + // 第4周 + {Title: "VIP第22天", CheckinDays: 22, BigPrize: true, PrizeName: "VIP积分x20", PrizeRef: "vip_integral_20"}, + {Title: "VIP第23天", CheckinDays: 23, BigPrize: true, PrizeName: "VIP积分x30", PrizeRef: "vip_integral_30"}, + {Title: "VIP第24天", CheckinDays: 24, BigPrize: true, PrizeName: "VIP AI脱衣x2", PrizeRef: "vip_ai_undress_2"}, + {Title: "VIP第25天", CheckinDays: 25, BigPrize: true, PrizeName: "VIP积分x30", PrizeRef: "vip_integral_30"}, + {Title: "VIP第26天", CheckinDays: 26, BigPrize: true, PrizeName: "VIP观影券x2", PrizeRef: "vip_video_coupon_2"}, + {Title: "VIP第27天", CheckinDays: 27, BigPrize: true, PrizeName: "VIP积分x30", PrizeRef: "vip_integral_30"}, + {Title: "VIP第28天", CheckinDays: 28, BigPrize: true, PrizeName: "VIP金币x500", PrizeRef: "vip_gold_500"}, + // 最后冲刺 + {Title: "VIP第29天", CheckinDays: 29, BigPrize: true, PrizeName: "VIP积分x50", PrizeRef: "vip_integral_50"}, + {Title: "VIP第30天", CheckinDays: 30, BigPrize: true, PrizeName: "VIP AI换脸x2", PrizeRef: "vip_ai_changeface_2"}, + {Title: "VIP第31天", CheckinDays: 31, BigPrize: true, PrizeName: "VIP积分x50", PrizeRef: "vip_integral_50"}, + } + + allCheckinPrizes := append(normalPrizes, vipPrizes...) + insertedCount := 0 + + for _, cp := range allCheckinPrizes { + prizeId, ok := prizeIdMap[cp.PrizeRef] + if !ok { + log.Printf(" [WARN] 找不到奖品引用 '%s',跳过", cp.PrizeRef) + continue + } + doc := bson.M{ + "title": cp.Title, + "image": "", + "checkinDays": cp.CheckinDays, + "checkinType": int64(CheckinTypeContinuously), + "prizeId": prizeId, + "prizeName": cp.PrizeName, + "status": true, + "bigPrize": cp.BigPrize, + "createdAt": now, + "updatedAt": now, + } + _, err := checkinPrizeColl.InsertOne(ctx, doc) + if err != nil { + log.Printf(" [WARN] 插入 '%s' 失败: %v", cp.Title, err) + continue + } + tag := "" + if cp.BigPrize { + tag = " [VIP]" + } + fmt.Printf(" Day%-2d %-16s => activity_prize(%s)%s\n", + cp.CheckinDays, cp.PrizeName, prizeId.Hex(), tag) + insertedCount++ + } + fmt.Printf(" 共创建 %d 个 checkin_prize(普通%d + VIP%d)\n\n", insertedCount, len(normalPrizes), len(vipPrizes)) + + // ======================== + // Step 4: 生成测试用户签到记录 + // ======================== + fmt.Println("=== Step 4: 生成测试用户签到记录 ===") + + // ====== 在这里修改参数 ====== + userIds := []uint64{300001} + n := 6 // 生成6天签到记录,今天签到为第7天(触发双倍奖励) + // =========================== + + SeedCheckin(ctx, db, userIds, n) + + fmt.Println("\n=== 全部配置完成 ===") + fmt.Println("数据关联关系:") + fmt.Println(" checkin_config (enable=true)") + fmt.Println(" checkin_prize (31天普通 + 31天VIP) --[prizeId]--> activity_prize") + fmt.Println(" user_checkin --[prizes]--> checkin_prize._id") + fmt.Printf(" 测试用户: %v (已签到%d天,今天第%d天)\n", userIds, n, n+1) +} + +// SeedCheckin 为指定用户生成N天连续签到数据 +func SeedCheckin(ctx context.Context, db *mongo.Database, userIds []uint64, n int) { + prizeColl := db.Collection("checkin_prize") + checkinColl := db.Collection("user_checkin") + + loc, _ := time.LoadLocation("Asia/Shanghai") + now := time.Now().In(loc) + today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) + + fmt.Printf(" Now: %v\n", now) + fmt.Printf(" Today (CST 00:00): %v\n", today) + fmt.Printf(" 生成 %d 天签到记录,今天签到为第 %d 天\n", n, n+1) + + // 查奖励配置(非大奖) + cursor, err := prizeColl.Find(ctx, bson.M{ + "status": true, + "checkinType": int64(1), + }, options.Find().SetSort(bson.M{"checkinDays": 1})) + if err != nil { + log.Printf(" query prizes failed: %v", err) + return + } + var prizeConfigs []bson.M + if err := cursor.All(ctx, &prizeConfigs); err != nil { + log.Printf(" decode prizes failed: %v", err) + return + } + + dayPrizes := make(map[int64][]primitive.ObjectID) + for _, p := range prizeConfigs { + days := toInt64(p["checkinDays"]) + bigPrize, _ := p["bigPrize"].(bool) + if !bigPrize { + if oid, ok := p["_id"].(primitive.ObjectID); ok { + dayPrizes[days] = append(dayPrizes[days], oid) + } + } + } + + delResult, err := checkinColl.DeleteMany(ctx, bson.M{ + "userId": bson.M{"$in": userIds}, + }) + if err != nil { + log.Printf(" delete failed: %v", err) + return + } + fmt.Printf(" Deleted %d existing records\n", delResult.DeletedCount) + + for _, userId := range userIds { + fmt.Printf("\n --- userId=%d ---\n", userId) + for day := n; day >= 1; day-- { + date := today.AddDate(0, 0, -day) + continuouslyDays := int64(n - day + 1) + cumulativeDays := continuouslyDays + prizes := dayPrizes[continuouslyDays] + + doc := bson.M{ + "_id": primitive.NewObjectID(), + "date": date, + "userId": userId, + "prizes": prizes, + "gave": true, + "vipPrizeGave": false, + "continuouslyDays": continuouslyDays, + "cumulativeDays": cumulativeDays, + "isReset": false, + "createdAt": date.Add(10 * time.Hour), + } + _, err := checkinColl.InsertOne(ctx, doc) + if err != nil { + log.Printf(" Insert failed: %v", err) + continue + } + fmt.Printf(" date=%s contDays=%d prizes=%v\n", + date.Format("2006-01-02"), continuouslyDays, prizes) + } + } +} + +func toInt64(v interface{}) int64 { + switch n := v.(type) { + case int64: + return n + case int32: + return int64(n) + case float64: + return int64(n) + default: + return 0 + } +} diff --git a/services/srv_im/ad_notify.go b/services/srv_im/ad_notify.go new file mode 100644 index 0000000..052ba3d --- /dev/null +++ b/services/srv_im/ad_notify.go @@ -0,0 +1,710 @@ +package srv_im + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "sort" + "strings" + "time" + + "91porn-server/app/service/adser" + "91porn-server/common" + "91porn-server/common/crypt" + "91porn-server/common/enum/imad" + "91porn-server/common/httputil" + "91porn-server/common/imclient" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/redis" + "91porn-server/models/v/imusermod" + "91porn-server/skd/skdg" + "91porn-server/web/webg" +) + +// srvImRedis 自适应当前进程可用的 Redis 客户端: +// skd cron 调用 SendAdNotify 时 webg.Redis 为 nil,必须用 skdg.Redis; +// web 服务调用时反之。两端写的是同一个 Redis 实例。 +func srvImRedis() *redis.Client { + if skdg.Redis != nil { + return skdg.Redis + } + if webg.Redis != nil { + return webg.Redis + } + return nil +} + +// srvImAdCenterCfg 同样的自适应思路:从当前进程的 Conf 里读 AdCenter 配置。 +type srvImAdCenter struct { + ApiDomain string + MerchantCode string + AppCode string + AesKey string +} + +func srvImAdCenterCfg() srvImAdCenter { + if skdg.Conf != nil { + c := skdg.Conf.AdCenter + if c.ApiDomain != "" || c.MerchantCode != "" || c.AppCode != "" || c.AesKey != "" { + return srvImAdCenter{ApiDomain: c.ApiDomain, MerchantCode: c.MerchantCode, AppCode: c.AppCode, AesKey: c.AesKey} + } + } + if webg.Conf != nil { + c := webg.Conf.AdCenter + return srvImAdCenter{ApiDomain: c.ApiDomain, MerchantCode: c.MerchantCode, AppCode: c.AppCode, AesKey: c.AesKey} + } + return srvImAdCenter{} +} + +const ( + adNotifyType = "AD_NOTIFY" + adNotifyTargetSpecified = "specified" + adNotifyTargetOnline = "online" + adNotifyOnlineScanLimit = 1000 + adNotifyPassthroughBatch = 100 + adNotifyVisibleCacheTTL = 24 * time.Hour + maxAdNotifyAdsPerSlot = 10 +) + +type AdNotifyReq struct { + UIDs []uint64 `json:"uids"` + UserIDs []uint64 `json:"userIds"` + Position string `json:"position"` + Positions []string `json:"positions"` + Target string `json:"target"` + TraceID string `json:"traceId"` + MaxUsers int `json:"maxUsers"` + ChannelType string `json:"channelType"` +} + +type AdNotifyResp struct { + TraceID string `json:"traceId"` + AdCount int `json:"adCount"` + Positions []string `json:"positions"` + Target string `json:"target"` + CandidateUserCount int `json:"candidateUserCount"` + TargetUserCount int `json:"targetUserCount"` + OnlineUserCount int `json:"onlineUserCount"` + OnlineStatusCheckedCount int `json:"onlineStatusCheckedCount"` + OnlineStatusReturnedCount int `json:"onlineStatusReturnedCount"` + OnlineStatusMissingCount int `json:"onlineStatusMissingCount"` + OnlineStatusInvisibleCount int `json:"onlineStatusInvisibleCount"` + MessageID string `json:"messageId"` + MessageIDs []string `json:"messageIds"` + MessageType int `json:"messageType"` + ReceiverCount int `json:"receiverCount"` + ImCreatedAt int64 `json:"imCreatedAt"` + Sent bool `json:"sent"` +} + +type adNotifyTarget struct { + UID uint64 + IMUserID int64 +} + +type adNotifyPassthroughSummary struct { + MessageID string + MessageIDs []string + MessageType int + ReceiverCount int + CreatedAt int64 +} + +type adNotifyOnlineStatusSummary struct { + Targets []adNotifyTarget + CheckedCount int + ReturnedCount int + MissingCount int + InvisibleCount int +} + +func SendAdNotify(req AdNotifyReq) (AdNotifyResp, stderr.Code, string) { + now := time.Now() + positionsReq := normalizeAdNotifyPositions(req) + for _, position := range positionsReq { + if !imad.IsPositionCode(position) { + return AdNotifyResp{}, stderr.ErrParamError, "positions 必须为 IM 广告位标识" + } + } + if strings.TrimSpace(req.TraceID) == "" { + req.TraceID = fmt.Sprintf("%s_%d", adNotifyType, now.UnixNano()) + } + targetMode := normalizeAdNotifyTarget(req.Target) + if targetMode == "" { + return AdNotifyResp{}, stderr.ErrParamError, "target 仅支持 online 或 specified" + } + + positions, adCount := resolveAdNotifyPositions(positionsReq) + resp := AdNotifyResp{ + TraceID: req.TraceID, + AdCount: adCount, + Positions: positions, + Target: targetMode, + } + if len(positions) == 0 { + return resp, stderr.Success, "" + } + + payload, err := buildAdNotifyPayload(req, positions, now) + if err != nil { + return resp, stderr.ErrParamError, err.Error() + } + extInfo, err := json.Marshal(payload) + if err != nil { + return resp, stderr.ErrParamError, err.Error() + } + + // online 模式:调 SendAppPassthrough 一次性广播给本商户/租户下所有在线用户, + // 由 IM 平台自己 fan-out,省掉扫表 + 查在线 + 设可见 + 分批的流程 + if targetMode == adNotifyTargetOnline { + return sendAdNotifyBroadcast(resp, req, string(extInfo)) + } + + // specified 模式:仍按 uid 拉取目标、查在线状态、分批走 SendOnlinePassthrough + targets, code, msg := collectAdNotifyTargets(targetMode, req) + if code != stderr.Success { + return resp, code, msg + } + resp.CandidateUserCount = len(targets) + resp.TargetUserCount = len(targets) + if len(targets) == 0 { + return resp, stderr.Success, "" + } + + ensureAdNotifyOnlineStatusVisible(targets) + onlineSummary, code, msg := filterOnlineAdNotifyTargets(targets) + if code != stderr.Success { + return resp, code, msg + } + onlineTargets := onlineSummary.Targets + resp.OnlineUserCount = len(onlineTargets) + resp.OnlineStatusCheckedCount = onlineSummary.CheckedCount + resp.OnlineStatusReturnedCount = onlineSummary.ReturnedCount + resp.OnlineStatusMissingCount = onlineSummary.MissingCount + resp.OnlineStatusInvisibleCount = onlineSummary.InvisibleCount + if len(onlineTargets) == 0 { + return resp, stderr.Success, "" + } + + result, code, msg := sendAdNotifyPassthrough(onlineTargets, imclient.OnlinePassthroughRequest{ + PassthroughType: adNotifyType, + Content: string(extInfo), + ExtInfo: string(extInfo), + ChannelType: req.ChannelType, + }) + if code != stderr.Success { + return resp, code, msg + } + resp.MessageID = result.MessageID + resp.MessageIDs = result.MessageIDs + resp.MessageType = result.MessageType + resp.ReceiverCount = result.ReceiverCount + resp.ImCreatedAt = result.CreatedAt + resp.Sent = true + return resp, stderr.Success, "" +} + +// logAdNotifyBroadcastCurl 把广播请求拼成等价 curl 命令打日志(INFO 级别)。 +// 仅包含 IM SDK 显式 header(X-Merchant-Code / X-App-Key / X-Client-Id / token), +// 签名相关 header 由 imclient 内部按请求计算,curl 复现时需要去掉签名校验或对接时再加。 +func logAdNotifyBroadcastCurl(traceID, token string, body imclient.AppPassthroughRequest) { + cfg := srvImV2Cfg() + base := strings.TrimRight(cfg.BaseURL, "/") + u := fmt.Sprintf("%s/api/endpoint/%s/%s/message/passthrough/send/batch/all", + base, url.PathEscape(cfg.MerchantCode), url.PathEscape(cfg.TenantCode)) + raw, _ := json.Marshal(body) + curl := fmt.Sprintf(`curl -X POST '%s' `+ + `-H 'Content-Type: application/json' `+ + `-H 'Accept: application/json' `+ + `-H 'X-Merchant-Code: %s' `+ + `-H 'X-App-Key: %s' `+ + `-H 'X-Client-Id: %s' `+ + `-H 'token: %s' `+ + `-d '%s'`, + u, cfg.MerchantCode, cfg.AppKey, cfg.ClientID, token, string(raw)) + log.Info("SendAdNotify broadcast curl", + log.Any("traceId", traceID), log.Any("curl", curl)) +} + +// sendAdNotifyBroadcast 走 IM 平台的"全在线广播"接口 +// POST /api/endpoint/{merchantCode}/{tenantCode}/message/passthrough/send/batch/all +// 一次调用就把消息推给本商户/租户下所有在线用户,无需先查用户列表 +func sendAdNotifyBroadcast(resp AdNotifyResp, req AdNotifyReq, extInfo string) (AdNotifyResp, stderr.Code, string) { + c := newWebSDKClient() + body := imclient.AppPassthroughRequest{ + PassthroughType: adNotifyType, + Content: extInfo, + ExtInfo: extInfo, + ChannelType: req.ChannelType, + } + var result *imclient.PassthroughResult + err := withAppToken(c, func(token string) error { + // 打印等价 curl,方便联调 + logAdNotifyBroadcastCurl(req.TraceID, token, body) + var sendErr error + result, sendErr = c.SendAppPassthrough(body, token) + return sendErr + }) + if err != nil { + log.Error("SendAdNotify broadcast failed", log.Any("traceId", req.TraceID), log.E(err)) + return resp, stderr.Failure, err.Error() + } + if result != nil { + resp.MessageID = result.MessageID + resp.MessageType = result.MessageType + resp.ReceiverCount = result.ReceiverCount + resp.OnlineUserCount = result.ReceiverCount // 平台返回的就是实际推送到的在线数 + resp.ImCreatedAt = result.CreatedAt + } + resp.Sent = true + return resp, stderr.Success, "" +} + +func normalizeAdNotifyTarget(target string) string { + switch strings.ToLower(strings.TrimSpace(target)) { + case "": + return adNotifyTargetSpecified + case adNotifyTargetSpecified: + return adNotifyTargetSpecified + case adNotifyTargetOnline: + return adNotifyTargetOnline + default: + return "" + } +} + +func collectAdNotifyTargets(targetMode string, req AdNotifyReq) ([]adNotifyTarget, stderr.Code, string) { + switch targetMode { + case adNotifyTargetOnline: + return scanAdNotifyIMUsers(req.MaxUsers) + case adNotifyTargetSpecified: + uids := normalizeUIDs(append(req.UIDs, req.UserIDs...)) + if len(uids) == 0 { + return nil, stderr.ErrParamError, "userIds不能为空;全量在线推送请传 target=online" + } + return resolveAdNotifyTargetsByUIDs(uids) + default: + return nil, stderr.ErrParamError, "target 仅支持 online 或 specified" + } +} + +func resolveAdNotifyTargetsByUIDs(uids []uint64) ([]adNotifyTarget, stderr.Code, string) { + // uid → imUserId 走 imusermod(唯一真源) + byUID, err := imusermod.IMUserIDMapByUIDs(uids) + if err != nil { + return nil, stderr.Failure, err.Error() + } + targets := make([]adNotifyTarget, 0, len(byUID)) + for _, uid := range uids { + imUserID, ok := byUID[uid] + if !ok { + continue + } + targets = append(targets, adNotifyTarget{UID: uid, IMUserID: imUserID}) + } + return targets, stderr.Success, "" +} + +func scanAdNotifyIMUsers(maxUsers int) ([]adNotifyTarget, stderr.Code, string) { + targets := make([]adNotifyTarget, 0) + // 直接扫 im_user 表(只含已注册 IM 的用户),按 uid 游标分页。 + // 表本身就小,且天然只有"有 imUserId 的用户",无需过滤 imUserId>0。 + var lastUID uint64 + for { + limit := int64(adNotifyOnlineScanLimit) + if maxUsers > 0 { + remaining := int64(maxUsers - len(targets)) + if remaining <= 0 { + break + } + if remaining < limit { + limit = remaining + } + } + list, err := imusermod.ListAfterUID(lastUID, limit) + if err != nil { + return nil, stderr.Failure, err.Error() + } + if len(list) == 0 { + break + } + for i := range list { + u := list[i] + if u.UID == 0 || u.IMUserID <= 0 { + continue + } + targets = append(targets, adNotifyTarget{UID: u.UID, IMUserID: u.IMUserID}) + } + // 本批末条 uid 即下一页游标(按 uid 升序) + lastUID = list[len(list)-1].UID + // 本批不满 limit,说明已扫到末尾 + if int64(len(list)) < limit { + break + } + } + return targets, stderr.Success, "" +} + +func resolveAdNotifyPositions(requested []string) ([]string, int) { + if len(requested) > 0 { + return requested, 0 + } + slots, err := webAdSlots() + if err != nil { + return nil, 0 + } + positions := listAdPositionsWithAds(slots) + adCount := 0 + for _, position := range positions { + ads := selectAdDetailsByPosition(slots, position) + adCount += len(ads) + } + return positions, adCount +} + +func normalizeAdNotifyPositions(req AdNotifyReq) []string { + positions := make([]string, 0, len(req.Positions)+1) + if position := strings.TrimSpace(req.Position); position != "" { + positions = append(positions, position) + } + positions = append(positions, req.Positions...) + seen := make(map[string]struct{}, len(positions)) + normalized := make([]string, 0, len(positions)) + for _, position := range positions { + position = strings.TrimSpace(position) + if position == "" { + continue + } + if _, ok := seen[position]; ok { + continue + } + seen[position] = struct{}{} + normalized = append(normalized, position) + } + return normalized +} + +func buildAdNotifyPayload(req AdNotifyReq, positions []string, now time.Time) (map[string]interface{}, error) { + return map[string]interface{}{ + "type": adNotifyType, + "positions": positions, + "traceId": req.TraceID, + "createdAt": now.UnixMilli(), + }, nil +} + +func ensureAdNotifyOnlineStatusVisible(targets []adNotifyTarget) { + if len(targets) == 0 { + return + } + c := newWebSDKClient() + seen := make(map[int64]struct{}, len(targets)) + successCount := 0 + for _, target := range targets { + if target.IMUserID <= 0 { + continue + } + if _, ok := seen[target.IMUserID]; ok { + continue + } + seen[target.IMUserID] = struct{}{} + if !shouldEnsureAdNotifyOnlineStatusVisible(target.IMUserID) { + continue + } + if err := withAppToken(c, func(token string) error { + return c.SetOnlineStatus(imclient.SetOnlineStatusRequest{ + UserID: target.IMUserID, + ShowOnlineStatus: true, + }, token) + }); err != nil { + log.Warn("SendAdNotify SetOnlineStatusVisible failed", + log.Any("uid", target.UID), log.Any("imUserId", target.IMUserID), log.E(err)) + continue + } + markAdNotifyOnlineStatusVisible(target.IMUserID) + successCount++ + } + log.Info("SendAdNotify SetOnlineStatusVisible summary", + log.Any("candidateCount", len(seen)), log.Any("successCount", successCount)) +} + +func shouldEnsureAdNotifyOnlineStatusVisible(imUserID int64) bool { + r := srvImRedis() + if r == nil { + return true + } + cached, err := r.Get(adNotifyVisibleCacheKey(imUserID)) + if err != nil { + log.Warn("SendAdNotify visible cache get failed", log.Any("imUserId", imUserID), log.E(err)) + return true + } + return cached == nil +} + +func markAdNotifyOnlineStatusVisible(imUserID int64) { + r := srvImRedis() + if r == nil { + return + } + if err := r.Set(adNotifyVisibleCacheKey(imUserID), 1, adNotifyVisibleCacheTTL); err != nil { + log.Warn("SendAdNotify visible cache set failed", log.Any("imUserId", imUserID), log.E(err)) + } +} + +func adNotifyVisibleCacheKey(imUserID int64) string { + return fmt.Sprintf("im:online_status_visible:%d", imUserID) +} + +func filterOnlineAdNotifyTargets(targets []adNotifyTarget) (adNotifyOnlineStatusSummary, stderr.Code, string) { + imUserIDs := make([]int64, 0, len(targets)) + seen := make(map[int64]struct{}, len(targets)) + for _, target := range targets { + if target.IMUserID <= 0 { + continue + } + if _, ok := seen[target.IMUserID]; ok { + continue + } + seen[target.IMUserID] = struct{}{} + imUserIDs = append(imUserIDs, target.IMUserID) + } + summary := adNotifyOnlineStatusSummary{CheckedCount: len(imUserIDs)} + statusByIMUserID := make(map[int64]bool, len(imUserIDs)) + invisibleCount := 0 + c := newWebSDKClient() + for _, batch := range chunkInt64s(imUserIDs, maxIMOnlineStatusBatchSize) { + var statuses []imclient.OnlineStatus + err := withAppToken(c, func(token string) error { + var statusErr error + statuses, statusErr = c.BatchOnlineStatus(imclient.BatchOnlineStatusRequest{UserIDs: batch}, token) + return statusErr + }) + if err != nil { + return summary, stderr.Failure, err.Error() + } + for _, item := range statuses { + if item.UserID <= 0 { + continue + } + statusByIMUserID[item.UserID] = item.Online + if !item.Visible { + invisibleCount++ + } + } + } + summary.ReturnedCount = len(statusByIMUserID) + summary.MissingCount = summary.CheckedCount - summary.ReturnedCount + if summary.MissingCount < 0 { + summary.MissingCount = 0 + } + summary.InvisibleCount = invisibleCount + summary.Targets = onlineAdNotifyTargets(targets, statusByIMUserID) + log.Info("SendAdNotify online status summary", + log.Any("checkedCount", summary.CheckedCount), + log.Any("returnedCount", summary.ReturnedCount), + log.Any("missingCount", summary.MissingCount), + log.Any("onlineCount", len(summary.Targets)), + log.Any("invisibleCount", summary.InvisibleCount), + log.Any("sampleIMUserIds", sampleInt64s(imUserIDs, 10)), + ) + return summary, stderr.Success, "" +} + +func sampleInt64s(values []int64, limit int) []int64 { + if limit <= 0 || len(values) == 0 { + return nil + } + if len(values) < limit { + limit = len(values) + } + return append([]int64(nil), values[:limit]...) +} + +func onlineAdNotifyTargets(targets []adNotifyTarget, statusByIMUserID map[int64]bool) []adNotifyTarget { + online := make([]adNotifyTarget, 0, len(targets)) + seen := make(map[int64]struct{}, len(targets)) + for _, target := range targets { + if target.IMUserID <= 0 || !statusByIMUserID[target.IMUserID] { + continue + } + if _, ok := seen[target.IMUserID]; ok { + continue + } + seen[target.IMUserID] = struct{}{} + online = append(online, target) + } + return online +} + +func sendAdNotifyPassthrough(targets []adNotifyTarget, req imclient.OnlinePassthroughRequest) (adNotifyPassthroughSummary, stderr.Code, string) { + var summary adNotifyPassthroughSummary + c := newWebSDKClient() + for _, batch := range chunkAdNotifyTargets(targets, adNotifyPassthroughBatch) { + receiverIDs := make([]int64, 0, len(batch)) + for _, target := range batch { + receiverIDs = append(receiverIDs, target.IMUserID) + } + req.ReceiverIDSet = receiverIDs + var result *imclient.PassthroughResult + err := withAppToken(c, func(token string) error { + var sendErr error + result, sendErr = c.SendOnlinePassthrough(req, token) + return sendErr + }) + if err != nil { + return summary, stderr.Failure, err.Error() + } + if result == nil { + continue + } + if summary.MessageID == "" { + summary.MessageID = result.MessageID + } + summary.MessageType = result.MessageType + summary.ReceiverCount += result.ReceiverCount + summary.CreatedAt = result.CreatedAt + summary.MessageIDs = append(summary.MessageIDs, result.MessageID) + } + return summary, stderr.Success, "" +} + +func chunkAdNotifyTargets(targets []adNotifyTarget, size int) [][]adNotifyTarget { + if size <= 0 { + size = adNotifyPassthroughBatch + } + chunks := make([][]adNotifyTarget, 0, (len(targets)+size-1)/size) + for start := 0; start < len(targets); start += size { + end := start + size + if end > len(targets) { + end = len(targets) + } + chunks = append(chunks, targets[start:end]) + } + return chunks +} + +type webAdCenterResp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data string `json:"data"` +} + +func webAdSlots() ([]adser.AdSlot, error) { + cfg := srvImAdCenterCfg() + if cfg.ApiDomain == "" || cfg.MerchantCode == "" || cfg.AppCode == "" || cfg.AesKey == "" { + return nil, errors.New("adCenter 未配置") + } + foreverCacheKey := fmt.Sprintf("jtAdForever-%s-%s", cfg.MerchantCode, cfg.AppCode) + redisKey := fmt.Sprintf("jtAd-%s-%s", cfg.MerchantCode, cfg.AppCode) + r := srvImRedis() + if r != nil { + str, err := r.Get(redisKey) + if err != nil { + log.Error(fmt.Sprintf("IM AdNotify 获取广告缓存异常:%v", err)) + } + if str != nil { + var cached []adser.AdSlot + if err = json.Unmarshal([]byte(*str), &cached); err == nil { + return cached, nil + } + } + } + + req := adser.JtAdvertiseReq{ + MerchantCode: cfg.MerchantCode, + AppCode: cfg.AppCode, + AdStatus: 1, + } + var serverResp *webAdCenterResp + body, _ := json.Marshal(req) + code, err := httputil.DefaultClientPostJsonWithResp(&serverResp, cfg.ApiDomain+"/openapi/getAdvertiseList", nil, body) + if err != nil { + return webAdSlotsFromLastTime(foreverCacheKey, err) + } + if code != http.StatusOK { + return webAdSlotsFromLastTime(foreverCacheKey, fmt.Errorf("response status %d", code)) + } + if serverResp == nil || serverResp.Code != 0 { + msg := "" + if serverResp != nil { + msg = serverResp.Msg + } + return webAdSlotsFromLastTime(foreverCacheKey, fmt.Errorf("response code error: %s", msg)) + } + adsData, err := crypt.AdDecrypt(serverResp.Data, cfg.AesKey) + if err != nil { + return nil, err + } + var slots []adser.AdSlot + if err = json.Unmarshal([]byte(adsData), &slots); err != nil { + return nil, err + } + for i := range slots { + sort.Slice(slots[i].AdDetailInfoList, func(j, k int) bool { + return slots[i].AdDetailInfoList[j].Sort < slots[i].AdDetailInfoList[k].Sort + }) + } + if r := srvImRedis(); r != nil { + common.Go(func() { + data, err := json.Marshal(&slots) + if err != nil { + return + } + _ = r.Set(foreverCacheKey, data, time.Hour*72) + _ = r.Set(redisKey, data, time.Minute) + }) + } + return slots, nil +} + +func webAdSlotsFromLastTime(cacheKey string, fallback error) ([]adser.AdSlot, error) { + r := srvImRedis() + if r == nil { + return nil, fallback + } + str, err := r.Get(cacheKey) + if err != nil || str == nil { + return nil, fallback + } + var slots []adser.AdSlot + if err = json.Unmarshal([]byte(*str), &slots); err != nil { + return nil, fallback + } + return slots, nil +} + +func listAdPositionsWithAds(slots []adser.AdSlot) []string { + positions := make([]string, 0) + seen := make(map[string]struct{}) + for _, slot := range slots { + code := strings.TrimSpace(slot.AdvertiseLocationCode) + if !imad.IsPositionCode(code) || len(slot.AdDetailInfoList) == 0 { + continue + } + if _, ok := seen[code]; ok { + continue + } + seen[code] = struct{}{} + positions = append(positions, code) + } + sort.Strings(positions) + return positions +} + +func selectAdDetailsByPosition(slots []adser.AdSlot, position string) []adser.AdDetailInfo { + position = strings.TrimSpace(position) + matches := make([]adser.AdDetailInfo, 0) + for _, slot := range slots { + if strings.TrimSpace(slot.AdvertiseLocationCode) != position { + continue + } + matches = append(matches, slot.AdDetailInfoList...) + } + if len(matches) > maxAdNotifyAdsPerSlot { + matches = matches[:maxAdNotifyAdsPerSlot] + } + return matches +} diff --git a/services/srv_im/im.go b/services/srv_im/im.go new file mode 100644 index 0000000..5066d0f --- /dev/null +++ b/services/srv_im/im.go @@ -0,0 +1,785 @@ +package srv_im + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "sync" + "time" + + "91porn-server/common/imclient" + "91porn-server/common/stderr" + "91porn-server/models/v/imusermod" + "91porn-server/models/v/sessionmod" + "91porn-server/models/v/usermod" + "91porn-server/skd/skdg" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const ( + maxIMOnlineStatusBatchSize = 100 + defaultIMTokenTTL = int64(86400) + imUserSyncPageSize = int64(500) +) + +var webAppTokenCache = struct { + sync.Mutex + token string + expiresAt time.Time +}{} + +type UserListReq struct { + UID *uint64 `json:"uid"` + HasIM *bool `json:"hasIm"` + IsUp *bool `json:"isUp"` + PageNum int64 `json:"pageNum"` + PageSize int64 `json:"pageSize"` +} + +type UserInfo struct { + UID uint64 `json:"uid"` + ImUserID int64 `json:"imUserId"` + NickName string `json:"nickName"` + Avatar string `json:"avatar"` + IsUp bool `json:"isUp"` +} + +type UserListResp struct { + Total int64 `json:"total"` + List []UserInfo `json:"list"` +} + +type SyncUsersReq struct { + UIDs []uint64 `json:"uids" binding:"required"` +} + +type SyncUsersResp struct { + Total int `json:"total"` + Success int `json:"success"` + Skipped int `json:"skipped"` + Failed int `json:"failed"` +} + +type FriendAddDirectReq struct { + UID uint64 `json:"uid" binding:"required"` + PeerUID uint64 `json:"peerUid" binding:"required"` +} + +type OnlineStatusReq struct { + UIDs []uint64 `json:"uids" binding:"required"` +} + +type OnlineStatusInfo struct { + UID uint64 `json:"uid"` + ImUserID int64 `json:"imUserId"` + Online bool `json:"online"` +} + +type PassthroughReq struct { + UIDs []uint64 `json:"uids"` + UserIDs []uint64 `json:"userIds"` + PassthroughType string `json:"passthroughType"` + EventType string `json:"eventType"` + Content string `json:"content"` + ExtInfo string `json:"extInfo"` + Payload map[string]interface{} `json:"payload"` + ChannelType string `json:"channelType"` + SenderUID uint64 `json:"senderUid"` + AllApp bool `json:"allApp"` + OnlineOnly bool `json:"onlineOnly"` +} + +type PassthroughResp struct { + TargetCount int `json:"targetCount"` + Result *imclient.PassthroughResult `json:"result,omitempty"` +} + +type SendMessageReq struct { + UID uint64 `json:"uid" binding:"required"` + PeerUID uint64 `json:"peerUid" binding:"required"` + Content string `json:"content" binding:"required"` +} + +type SendMessageResp struct { + UID uint64 `json:"uid"` + ImUserID int64 `json:"imUserId"` + PeerUID uint64 `json:"peerUid"` + PeerImUserID int64 `json:"peerImUserId"` + Content string `json:"content"` + MessageType int `json:"messageType"` +} + +type SyncDialogUsersStats struct { + Enabled bool `json:"enabled"` + Total int `json:"total"` + Skipped int `json:"skipped"` + Success int `json:"success"` + Failed int `json:"failed"` + FriendSuccess int `json:"friendSuccess"` + FriendFailed int `json:"friendFailed"` +} + +type imFriendPair struct { + UID uint64 + PeerUID uint64 +} + +func ListUsers(req UserListReq) (UserListResp, stderr.Code, string) { + if req.PageNum <= 0 { + req.PageNum = 1 + } + if req.PageSize <= 0 || req.PageSize > 100 { + req.PageSize = 20 + } + filter := buildUserListFilter(req) + skip := (req.PageNum - 1) * req.PageSize + opts := options.Find(). + SetSort(bson.M{"uid": -1}). + SetSkip(skip). + SetLimit(req.PageSize) + var total int64 + users, _, err := usermod.FetchList(filter, opts, &total) + if err != nil { + return UserListResp{}, stderr.Failure, err.Error() + } + resp := UserListResp{Total: total, List: make([]UserInfo, 0, len(users))} + for _, user := range users { + resp.List = append(resp.List, userInfoFromUser(user)) + } + return resp, stderr.Success, "" +} + +func SyncUsers(req SyncUsersReq) (SyncUsersResp, stderr.Code, string) { + uids := normalizeUIDs(req.UIDs) + resp := SyncUsersResp{Total: len(uids)} + if len(uids) == 0 { + return resp, stderr.ErrParamError, "uids不能为空" + } + c := newWebSDKClient() + if !c.Enabled() { + return resp, stderr.Failure, "IM 未配置" + } + for _, uid := range uids { + user, err := usermod.FindUserByUID(uid) + if err != nil || user == nil || user.UID == 0 { + resp.Failed++ + continue + } + if imusermod.IMUserIDByUID(user.UID) > 0 { + resp.Skipped++ + continue + } + if _, err = ensureUserRegistered(c, user); err != nil { + resp.Failed++ + continue + } + resp.Success++ + } + return resp, stderr.Success, "" +} + +func AddFriendDirect(req FriendAddDirectReq) (stderr.Code, string) { + if req.UID == 0 || req.PeerUID == 0 || req.UID == req.PeerUID { + return stderr.ErrParamError, "uid/peerUid错误" + } + c := newWebSDKClient() + userIMID, peerIMID, err := ensurePairRegistered(c, req.UID, req.PeerUID) + if err != nil { + return stderr.Failure, err.Error() + } + if err = ensureFriendDirect(c, userIMID, peerIMID); err != nil { + return stderr.Failure, err.Error() + } + if err = ensureFriendDirect(c, peerIMID, userIMID); err != nil { + return stderr.Failure, err.Error() + } + return stderr.Success, "" +} + +func BatchOnlineStatus(req OnlineStatusReq) ([]OnlineStatusInfo, stderr.Code, string) { + uids := normalizeUIDs(req.UIDs) + if len(uids) == 0 { + return []OnlineStatusInfo{}, stderr.Success, "" + } + mappings, err := imusermod.FindByUIDsQuiet(uids) + if err != nil { + return nil, stderr.Failure, err.Error() + } + imIDs := make([]int64, 0, len(mappings)) + byIMID := make(map[int64]uint64, len(mappings)) + seen := make(map[int64]struct{}, len(mappings)) + for _, m := range mappings { + if m.IMUserID <= 0 { + continue + } + if _, ok := seen[m.IMUserID]; ok { + continue + } + seen[m.IMUserID] = struct{}{} + imIDs = append(imIDs, m.IMUserID) + byIMID[m.IMUserID] = m.UID + } + if len(imIDs) == 0 { + return []OnlineStatusInfo{}, stderr.Success, "" + } + c := newWebSDKClient() + statuses := make([]imclient.OnlineStatus, 0, len(imIDs)) + for _, batch := range chunkInt64s(imIDs, maxIMOnlineStatusBatchSize) { + var items []imclient.OnlineStatus + err := withAppToken(c, func(token string) error { + var batchErr error + items, batchErr = c.BatchOnlineStatus(imclient.BatchOnlineStatusRequest{UserIDs: batch}, token) + return batchErr + }) + if err != nil { + return nil, stderr.Failure, err.Error() + } + statuses = append(statuses, items...) + } + resp := make([]OnlineStatusInfo, 0, len(statuses)) + for _, item := range statuses { + resp = append(resp, OnlineStatusInfo{ + UID: byIMID[item.UserID], + ImUserID: item.UserID, + Online: item.Online, + }) + } + return resp, stderr.Success, "" +} + +func SendPassthrough(req PassthroughReq) (PassthroughResp, stderr.Code, string) { + passthroughType := strings.TrimSpace(req.PassthroughType) + if passthroughType == "" { + passthroughType = strings.TrimSpace(req.EventType) + } + if passthroughType == "" { + return PassthroughResp{}, stderr.ErrParamError, "passthroughType不能为空" + } + c := newWebSDKClient() + content := strings.TrimSpace(req.Content) + extInfo := strings.TrimSpace(req.ExtInfo) + if req.Payload != nil { + payloadBytes, err := json.Marshal(req.Payload) + if err != nil { + return PassthroughResp{}, stderr.ErrParamError, err.Error() + } + if content == "" { + content = string(payloadBytes) + } + if extInfo == "" { + extInfo = string(payloadBytes) + } + } + if extInfo == "" { + extInfo = "{}" + } + senderID, err := resolveSenderIMID(c, req.SenderUID) + if err != nil { + return PassthroughResp{}, stderr.Failure, err.Error() + } + if req.AllApp { + var result *imclient.PassthroughResult + err := withAppToken(c, func(token string) error { + var sendErr error + result, sendErr = c.SendAppPassthrough(imclient.AppPassthroughRequest{ + SenderID: senderID, + PassthroughType: passthroughType, + Content: content, + ExtInfo: extInfo, + ChannelType: req.ChannelType, + }, token) + return sendErr + }) + if err != nil { + return PassthroughResp{}, stderr.Failure, err.Error() + } + return PassthroughResp{Result: result}, stderr.Success, "" + } + uids := normalizeUIDs(append(req.UIDs, req.UserIDs...)) + if len(uids) == 0 { + return PassthroughResp{}, stderr.ErrParamError, "uids不能为空;全应用透传需显式传 allApp=true" + } + imIDs, err := loadRegisteredIMIDs(uids) + if err != nil { + return PassthroughResp{}, stderr.Failure, err.Error() + } + if len(imIDs) == 0 { + return PassthroughResp{}, stderr.ErrParamError, "没有匹配到可发送透传的用户" + } + var result *imclient.PassthroughResult + err = withAppToken(c, func(token string) error { + var sendErr error + result, sendErr = c.SendOnlinePassthrough(imclient.OnlinePassthroughRequest{ + SenderID: senderID, + ReceiverIDSet: imIDs, + PassthroughType: passthroughType, + Content: content, + ExtInfo: extInfo, + ChannelType: req.ChannelType, + }, token) + return sendErr + }) + if err != nil { + return PassthroughResp{}, stderr.Failure, err.Error() + } + return PassthroughResp{TargetCount: len(imIDs), Result: result}, stderr.Success, "" +} + +func SendMessage(req SendMessageReq) (SendMessageResp, stderr.Code, string) { + content := strings.TrimSpace(req.Content) + if content == "" { + return SendMessageResp{}, stderr.ErrParamError, "content不能为空" + } + if req.UID == 0 || req.PeerUID == 0 || req.UID == req.PeerUID { + return SendMessageResp{}, stderr.ErrParamError, "uid/peerUid错误" + } + c := newWebSDKClient() + userIMID, peerIMID, err := ensurePairRegistered(c, req.UID, req.PeerUID) + if err != nil { + return SendMessageResp{}, stderr.Failure, err.Error() + } + if err = ensureFriendDirect(c, userIMID, peerIMID); err != nil { + return SendMessageResp{}, stderr.Failure, err.Error() + } + if err = ensureFriendDirect(c, peerIMID, userIMID); err != nil { + return SendMessageResp{}, stderr.Failure, err.Error() + } + messageType := imclient.MessageTypeText + if err = withAppToken(c, func(token string) error { + return c.SendMessage(imclient.SendMessageRequest{ + SenderID: userIMID, + ReceiverID: peerIMID, + Content: content, + MessageType: messageType, + }, token) + }); err != nil { + return SendMessageResp{}, stderr.Failure, err.Error() + } + return SendMessageResp{ + UID: req.UID, + ImUserID: userIMID, + PeerUID: req.PeerUID, + PeerImUserID: peerIMID, + Content: content, + MessageType: messageType, + }, stderr.Success, "" +} + +func SyncDialogUsers() (SyncDialogUsersStats, error) { + c := newWebSDKClient() + if !c.Enabled() { + return SyncDialogUsersStats{Enabled: false}, nil + } + syncIDs, pairs, err := CollectDialogUserIDs() + if err != nil { + return SyncDialogUsersStats{Enabled: true}, err + } + stats := SyncDialogUsersStats{Enabled: true, Total: len(syncIDs)} + imIDs := make(map[uint64]int64, len(syncIDs)) + for uid := range syncIDs { + user, err := usermod.FindUserByUID(uid) + if err != nil || user == nil || user.UID == 0 { + stats.Failed++ + continue + } + if imid := imusermod.IMUserIDByUID(user.UID); imid > 0 { + imIDs[user.UID] = imid + stats.Skipped++ + continue + } + imUserID, err := ensureUserRegistered(c, user) + if err != nil { + stats.Failed++ + continue + } + imIDs[user.UID] = imUserID + stats.Success++ + time.Sleep(100 * time.Millisecond) + } + for _, pair := range pairs { + userIMID := imIDs[pair.UID] + peerIMID := imIDs[pair.PeerUID] + if userIMID <= 0 || peerIMID <= 0 { + stats.FriendFailed++ + continue + } + if err := ensureFriendDirect(c, userIMID, peerIMID); err != nil { + stats.FriendFailed++ + continue + } + stats.FriendSuccess++ + time.Sleep(100 * time.Millisecond) + } + return stats, nil +} + +func CollectDialogUserIDs() (map[uint64]struct{}, []imFriendPair, error) { + syncIDs := make(map[uint64]struct{}) + pairSet := make(map[string]struct{}) + pairs := make([]imFriendPair, 0) + var lastID primitive.ObjectID + for { + filter := bson.M{} + if !lastID.IsZero() { + filter["_id"] = bson.M{"$gt": lastID} + } + opts := options.Find(). + SetProjection(bson.M{"_id": 1, "sendUid": 1, "takeUid": 1}). + SetSort(bson.M{"_id": 1}). + SetLimit(imUserSyncPageSize) + sessions, err := sessionmod.FindManyByFilter(filter, opts) + if err != nil { + return nil, nil, err + } + if len(sessions) == 0 { + break + } + for _, session := range sessions { + if session.SendUid > 0 { + syncIDs[session.SendUid] = struct{}{} + } + if session.TakeUid > 0 { + syncIDs[session.TakeUid] = struct{}{} + } + addIMFriendPair(pairSet, &pairs, session.SendUid, session.TakeUid) + } + lastID = sessions[len(sessions)-1].ID + if int64(len(sessions)) < imUserSyncPageSize { + break + } + } + return syncIDs, pairs, nil +} + +func buildUserListFilter(req UserListReq) bson.M { + filter := bson.M{} + andFilters := bson.A{} + if req.UID != nil && *req.UID > 0 { + filter["uid"] = *req.UID + } + if req.HasIM != nil { + if *req.HasIM { + filter["imUserId"] = bson.M{"$gt": 0} + } else { + andFilters = append(andFilters, bson.M{"$or": bson.A{ + bson.M{"imUserId": bson.M{"$exists": false}}, + bson.M{"imUserId": bson.M{"$lte": 0}}, + }}) + } + } + if req.IsUp != nil { + if *req.IsUp { + andFilters = append(andFilters, creatorFilter()) + } else { + andFilters = append(andFilters, bson.M{"$nor": bson.A{creatorFilter()}}) + } + } + if len(andFilters) > 0 { + filter["$and"] = andFilters + } + return filter +} + +func creatorFilter() bson.M { + return bson.M{"$or": bson.A{ + bson.M{"originalUp": true}, + bson.M{"officialCert": true}, + bson.M{"superUser": true}, + bson.M{"merchantUser": bson.M{"$gt": 0}}, + bson.M{"vidUploadCount": bson.M{"$gt": 0}}, + bson.M{"coverUploadCount": bson.M{"$gt": 0}}, + bson.M{"upTag": bson.M{"$ne": ""}}, + }} +} + +func userInfoFromUser(user *usermod.User) UserInfo { + if user == nil { + return UserInfo{} + } + return UserInfo{ + UID: user.UID, + ImUserID: imusermod.IMUserIDByUID(user.UID), + NickName: user.Name, + Avatar: user.Portrait, + IsUp: isCreator(user), + } +} + +func isCreator(user *usermod.User) bool { + return user != nil && (user.OfficialCert || user.SuperUser || + user.MerchantUser > 0 || user.VidUploadCount > 0 || user.CoverUploadCount > 0 || user.UpTag != "") +} + +func ensurePairRegistered(c *imclient.Client, uid, peerUID uint64) (int64, int64, error) { + user, err := usermod.FindUserByUID(uid) + if err != nil { + return 0, 0, err + } + peer, err := usermod.FindUserByUID(peerUID) + if err != nil { + return 0, 0, err + } + userIMID, err := ensureUserRegistered(c, user) + if err != nil { + return 0, 0, fmt.Errorf("sync user im id failed: %w", err) + } + peerIMID, err := ensureUserRegistered(c, peer) + if err != nil { + return 0, 0, fmt.Errorf("sync peer im id failed: %w", err) + } + return userIMID, peerIMID, nil +} + +func ensureUserRegistered(c *imclient.Client, user *usermod.User) (int64, error) { + if user == nil || user.UID == 0 { + return 0, fmt.Errorf("user is empty") + } + // 以 imusermod 为准:有映射即已注册 + if mapping, err := imusermod.FindByUIDQuiet(user.UID); err == nil && mapping.IMUserID > 0 { + return mapping.IMUserID, nil + } + thirdPartyID := strconv.FormatUint(user.UID, 10) + var imUserID int64 + err := withAppToken(c, func(token string) error { + var registerErr error + imUserID, registerErr = c.Register(imclient.RegisterRequest{ + ThirdPartyID: thirdPartyID, + Password: sdkPassword(user.UID), + Nickname: user.Name, + Avatar: user.Portrait, + }, token) + return registerErr + }) + if err != nil { + return 0, err + } + if imUserID <= 0 { + return 0, fmt.Errorf("im register returned empty user id") + } + if err = imusermod.UpsertByUID(user.UID, imUserID, thirdPartyID); err != nil { + return 0, err + } + return imUserID, nil +} + +func resolveSenderIMID(c *imclient.Client, senderUID uint64) (int64, error) { + if senderUID == 0 { + return 0, nil + } + user, err := usermod.FindUserByUID(senderUID) + if err != nil { + return 0, err + } + return ensureUserRegistered(c, user) +} + +func loadRegisteredIMIDs(uids []uint64) ([]int64, error) { + mappings, err := imusermod.FindByUIDsQuiet(normalizeUIDs(uids)) + if err != nil { + return nil, err + } + imIDs := make([]int64, 0, len(mappings)) + seen := make(map[int64]struct{}, len(mappings)) + for _, m := range mappings { + if m.IMUserID <= 0 { + continue + } + if _, ok := seen[m.IMUserID]; ok { + continue + } + seen[m.IMUserID] = struct{}{} + imIDs = append(imIDs, m.IMUserID) + } + return imIDs, nil +} + +func ensureFriendDirect(c *imclient.Client, userIMID, friendIMID int64) error { + if userIMID <= 0 || friendIMID <= 0 || userIMID == friendIMID { + return nil + } + err := withAppToken(c, func(token string) error { + return c.DirectAddFriend(imclient.DirectAddFriendRequest{ + UserID: userIMID, + FriendID: friendIMID, + Archive: true, + }, token) + }) + if err == nil || isDuplicateFriendError(err) { + return nil + } + return err +} + +func isDuplicateFriendError(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "already") || + strings.Contains(msg, "exist") || + strings.Contains(msg, "重复") || + strings.Contains(msg, "已是好友") || + strings.Contains(msg, "好友关系") +} + +func addIMFriendPair(pairSet map[string]struct{}, pairs *[]imFriendPair, uid, peerUID uint64) { + addDirectedIMFriendPair(pairSet, pairs, uid, peerUID) + addDirectedIMFriendPair(pairSet, pairs, peerUID, uid) +} + +func addDirectedIMFriendPair(pairSet map[string]struct{}, pairs *[]imFriendPair, uid, peerUID uint64) { + if uid == 0 || peerUID == 0 || uid == peerUID { + return + } + key := fmt.Sprintf("%d:%d", uid, peerUID) + if _, ok := pairSet[key]; ok { + return + } + pairSet[key] = struct{}{} + *pairs = append(*pairs, imFriendPair{UID: uid, PeerUID: peerUID}) +} + +func normalizeUIDs(uids []uint64) []uint64 { + seen := make(map[uint64]struct{}, len(uids)) + normalized := make([]uint64, 0, len(uids)) + for _, uid := range uids { + if uid == 0 { + continue + } + if _, ok := seen[uid]; ok { + continue + } + seen[uid] = struct{}{} + normalized = append(normalized, uid) + } + return normalized +} + +func chunkInt64s(ids []int64, size int) [][]int64 { + if size <= 0 { + size = maxIMOnlineStatusBatchSize + } + if len(ids) == 0 { + return nil + } + chunks := make([][]int64, 0, (len(ids)+size-1)/size) + for start := 0; start < len(ids); start += size { + end := start + size + if end > len(ids) { + end = len(ids) + } + chunks = append(chunks, ids[start:end]) + } + return chunks +} + +// srvImV2Cfg 拿当前进程可用的 ImV2 配置: +// skd cron 调用 SendAdNotify 时 webg.Conf 为 nil,必须用 skdg.Conf; +// web 服务调用反之。两侧 imv2 段必须配同样的内容。 +type srvImV2 struct { + BaseURL string + DynamicConfigDomain string + SocketURL string + MerchantCode string + TenantCode string + AppKey string + ClientID string + ClientSecret string +} + +func srvImV2Cfg() srvImV2 { + if skdg.Conf != nil { + c := skdg.Conf.ImV2 + if c.BaseURL != "" || c.AppKey != "" { + return srvImV2{ + BaseURL: c.BaseURL, DynamicConfigDomain: c.DynamicConfigDomain, SocketURL: c.SocketURL, + MerchantCode: c.MerchantCode, TenantCode: c.TenantCode, AppKey: c.AppKey, + ClientID: c.ClientID, ClientSecret: c.ClientSecret, + } + } + } + if webg.Conf != nil { + c := webg.Conf.ImV2 + return srvImV2{ + BaseURL: c.BaseURL, DynamicConfigDomain: c.DynamicConfigDomain, SocketURL: c.SocketURL, + MerchantCode: c.MerchantCode, TenantCode: c.TenantCode, AppKey: c.AppKey, + ClientID: c.ClientID, ClientSecret: c.ClientSecret, + } + } + return srvImV2{} +} + +func newWebSDKClient() *imclient.Client { + cfg := srvImV2Cfg() + return imclient.New(imclient.Config{ + Enable: true, + BaseURL: cfg.BaseURL, + MerchantCode: cfg.MerchantCode, + TenantCode: cfg.TenantCode, + AppKey: cfg.AppKey, + ClientID: cfg.ClientID, + ClientSecret: cfg.ClientSecret, + SignKey: imclient.DefaultSignKey, + AESKey: imclient.DefaultAESKey, + EnableSign: true, + EncryptTimestamp: true, + TokenTTL: defaultIMTokenTTL, + }) +} + +func appToken(c *imclient.Client) (string, error) { + if !c.Enabled() { + return "", fmt.Errorf("IM 未配置") + } + webAppTokenCache.Lock() + defer webAppTokenCache.Unlock() + if webAppTokenCache.token != "" && time.Now().Before(webAppTokenCache.expiresAt) { + return webAppTokenCache.token, nil + } + token, err := c.AppToken() + if err != nil { + return "", err + } + webAppTokenCache.token = token + cacheSeconds := defaultIMTokenTTL - 60 + if cacheSeconds < 60 { + cacheSeconds = defaultIMTokenTTL + } + webAppTokenCache.expiresAt = time.Now().Add(time.Duration(cacheSeconds) * time.Second) + return token, nil +} + +func invalidateAppToken() { + webAppTokenCache.Lock() + defer webAppTokenCache.Unlock() + webAppTokenCache.token = "" + webAppTokenCache.expiresAt = time.Time{} +} + +func withAppToken(c *imclient.Client, fn func(token string) error) error { + for attempt := 0; attempt < 2; attempt++ { + token, err := appToken(c) + if err != nil { + return err + } + err = fn(token) + if err == nil { + return nil + } + if attempt == 0 && imclient.IsSessionExpired(err) { + invalidateAppToken() + continue + } + return err + } + return fmt.Errorf("im app token retry exhausted") +} + +func sdkPassword(uid uint64) string { + return fmt.Sprintf("hjll%014d", uid%100000000000000) +} diff --git a/skd/job/cache/rankcache/run.go b/skd/job/cache/rankcache/run.go new file mode 100644 index 0000000..a1ba54c --- /dev/null +++ b/skd/job/cache/rankcache/run.go @@ -0,0 +1,51 @@ +package rankcache + +import ( + "91porn-server/app/service/rankser" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/models/v/usermod" + "91porn-server/skd/skdg" +) + +type UserRank = rankser.UserRank + +type User = rankser.User + +type UBaseInfo = usermod.BaseInfo + +const ( + UserInviteRankType = rankser.UserInviteRankType //推广达人 + UserIncomeRankType = rankser.UserIncomeRankType //收益达人 + UserUploadRankType = rankser.UserUploadRankType //上传达人 +) + +func Update() { + saveUserIncomeRank() + saveUserUploadRank() +} + +func saveUserIncomeRank() { + rank, err := rankser.GetUserIncomeRank() + if err != nil { + log.Error("rankcach GetUserIncomeRank error", log.E(err)) + return + } + redisKey := redisconst.RankKey(UserIncomeRankType) + if err = skdg.Redis.Set(redisKey, rank, redisconst.RankExpireMax); err != nil { + log.Warn("rankcach UserIncomeRankType redis set error", log.E(err)) + } +} + +func saveUserUploadRank() { + rank, err := rankser.GetUserUploadRank() + if err != nil { + log.Error("rankcach GetUserUploadRank error", log.E(err)) + return + } + redisKey := redisconst.RankKey(UserUploadRankType) + _ = skdg.Redis.Set(redisKey, rank, redisconst.RankExpireMax) + if err != nil { + log.Warn("rankcach UserUploadRankType redis set error", log.E(err)) + } +} diff --git a/skd/job/cache/tonecache/run.go b/skd/job/cache/tonecache/run.go new file mode 100644 index 0000000..b4af84f --- /dev/null +++ b/skd/job/cache/tonecache/run.go @@ -0,0 +1,51 @@ +package tonecache + +import ( + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/models/v/tonemod" + "91porn-server/models/v/vidmod" + "91porn-server/skd/skdg" +) + +const MaxVideoCount = 500 + +func Update() { + officialRecomPlayCount, err := vidmod.SumFakePlayCountSortByToneOfficialRecom(MaxVideoCount) + if err != nil { + log.Error("tonecach OfficialRecomPlayCount SumFakePlayCountSortByToneOfficialRecom faild", log.E(err)) + } + latestUploadPlayCount, err := vidmod.SumFakePlayCountSortByCreatedAt(MaxVideoCount) + if err != nil { + log.Error("tonecach LatestUploadPlayCount SumFakePlayCountSortByCreatedAt faild", log.E(err)) + } + mostLikesPlayCount, err := vidmod.SumFakePlayCountSortByFakeLikeCount(MaxVideoCount) + if err != nil { + log.Error("tonecach MostLikesPlayCount SumFakePlayCountSortByFakeLikeCount faild", log.E(err)) + } + playAtMostPlayCount, err := vidmod.SumFakePlayCountSortByFakePlayCount(MaxVideoCount) + if err != nil { + log.Error("tonecach PlayAtMostPlayCount SumFakePlayCountSortByFakePlayCount faild", log.E(err)) + } + mostCommentsPlayCount, err := vidmod.SumFakePlayCountSortByFakeCommentCount(MaxVideoCount) + if err != nil { + log.Error("tonecach MostCommentsPlayCount SumFakePlayCountSortByFakeCommentCount faild", log.E(err)) + } + freeAreaPlayCount, err := vidmod.SumFakePlayCountSortByFreeArea(MaxVideoCount) + if err != nil { + log.Error("tonecach MostCommentsPlayCount SumFakePlayCountSortByFakeCommentCount faild", log.E(err)) + } + toneValue := tonemod.ToneValue{ + OfficialRecomPlayCount: officialRecomPlayCount, + GoldCoinAreaPayCount: 0, + LatestUploadPlayCount: latestUploadPlayCount, + MostLikesPlayCount: mostLikesPlayCount, + PlayAtMostPlayCount: playAtMostPlayCount, + MostCommentsPlayCount: mostCommentsPlayCount, + FreeAreaPlayCount: freeAreaPlayCount, + } + redisKey := redisconst.ToneKey() + if err = skdg.Redis.Set(redisKey, toneValue, redisconst.ToneExpireMax); err != nil { + log.Warn("tonecach Update redis set error", log.E(err)) + } +} diff --git a/skd/job/check/daichongcallback.go b/skd/job/check/daichongcallback.go new file mode 100644 index 0000000..90ae474 --- /dev/null +++ b/skd/job/check/daichongcallback.go @@ -0,0 +1,149 @@ +package check + +import ( + "context" + "time" + + "91porn-server/common/daichong" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/rchgordmod" + "91porn-server/skd/skdg" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var pageSize int64 = 20 + +// 通知第三方代充订单结果 +func DaiChongOrderCallBack() { + log.Info("DaiChongOrderCallBack start") + sort := rchgordmod.Sort{{Key: "createdAt", Value: 1}} + daichongType := string(rchgordmod.Rech_DaiChong) + matList := []pageopt.Matcher{ + (&rchgordmod.RechargeTypeMatch{RechargeType: &daichongType}).New(), + (&rchgordmod.NotifyStatusMatch{NotifyStatus: []int{rchgordmod.Notify_DEFAULT, rchgordmod.Notify_FAILURE}}).New(), + (&rchgordmod.StatusInMatch{Status: []int{rchgordmod.SUCCESS, rchgordmod.FAILURE}}).New(), + } + var pageNumber int64 = 1 //从第一页开始 + for { + var skip = (pageNumber - 1) * pageSize + //订单列表 + rchgorderList, err := rchgordmod.List(sort, &skip, &pageSize, matList...) + if err != nil { + return + } + if len(rchgorderList) == 0 { + log.Info("DaiChongOrderCallBack end") + return + } + for _, v := range rchgorderList { + if oid, err := primitive.ObjectIDFromHex(v.OID); err == nil { + if !oid.IsZero() { + continue + } + } + handOrderBack(v) + } + pageNumber++ + } +} + +// 将超时支付的订单设置为失败订单,并通知给对方 +func HandleProcessingOrder() { + log.Info("HandleProcessingOrder start") + //创建时间小于三天前的订单 + threeDayBefore := time.Now().AddDate(0, 0, -3) + processing := rchgordmod.Processing + sort := rchgordmod.Sort{{Key: "createdAt", Value: 1}} + daichongType := string(rchgordmod.Rech_DaiChong) + matList := []pageopt.Matcher{ + (&rchgordmod.RechargeTypeMatch{RechargeType: &daichongType}).New(), + (&rchgordmod.CreatedAtLTMatch{LT: &threeDayBefore}).New(), + (&rchgordmod.StatusMatch{Status: &processing}).New(), + } + var pageNumber int64 = 1 //从第一页开始 + for { + var skip = (pageNumber - 1) * pageSize + //订单列表 + rchgorderList, err := rchgordmod.List(sort, &skip, &pageSize, matList...) + if err != nil { + return + } + if len(rchgorderList) == 0 { + log.Info("HandleProcessingOrder end") + return + } + for _, v := range rchgorderList { + v.Status = rchgordmod.FAILURE + v.StatusDesc = rchgordmod.Status(rchgordmod.FAILURE).Desc() + v.Remark = "该订单超时三天未处理,失败" + v.FailureAt = time.Now() + handOrderBack(v) + } + pageNumber++ + } +} + +func handOrderBack(v *rchgordmod.RechargeOrder) { + var orderStatus int + switch v.Status { + case rchgordmod.SUCCESS: + orderStatus = daichong.Success + case rchgordmod.FAILURE: + orderStatus = daichong.Failed + } + if orderStatus == 0 { + return + } + var timestamp = v.NotifyTime + if v.NotifyTime == 0 { + timestamp = time.Now().UTC().Unix() + } + resp, code := orderCallBack(daichong.OrderCallSign{ + OrderId: v.OID, + Time: timestamp, + Status: orderStatus, + }) + selector := rchgordmod.ReplaceEditSelector{ + Status: &v.Status, + StatusDesc: &v.StatusDesc, + Remark: &v.Remark, + FailureAt: &v.FailureAt, + SuccessAt: &v.SuccessAt, + } + if code == stderr.Success { + notifySuccess := rchgordmod.Notify_SUCCESS + selector.NotifyStatus = ¬ifySuccess + log.Info("daichong order call back send http request successfully", + log.Any("orderStatus", orderStatus), + log.Any("orderId", v.OID), + log.Any("time", time.Now().Format(time.RFC3339))) + } else { + notifyFailure := rchgordmod.FAILURE + selector.NotifyStatus = ¬ifyFailure + log.Warn("daichong order call back send http request failed", log.Any("responseCode", code), log.Any("msg", resp.Msg)) + } + _ = rchgordmod.UpdateBaseOnOid(nil, v.OID, selector) +} + +// 上分通知 将订单的处理结果通知给代充平台 +func orderCallBack(req daichong.OrderCallSign) (commod.Resp, stderr.Code) { + sign, err := daichong.Convert2Sign(req, skdg.Conf.DaiChong.AppSecret) + if err != nil { + log.Warn("create sign data wrong", log.Any("warn", err)) + return commod.Resp{}, stderr.Failure + } + orderReq := daichong.CommonReq{ + AppId: skdg.Conf.DaiChong.AppID, + Data: sign, + } + resp, err := daichong.OrderCallBack(context.TODO(), orderReq, skdg.Conf.DaiChong.Domain) + if err != nil { + log.Warn("GetOrderInfo send http wrong", log.Any("warn", err)) + return commod.Resp{}, stderr.Failure + } + return resp, resp.Code +} diff --git a/skd/job/checkin/checkin.go b/skd/job/checkin/checkin.go new file mode 100644 index 0000000..69bd75d --- /dev/null +++ b/skd/job/checkin/checkin.go @@ -0,0 +1,70 @@ +package checkin + +import ( + "91porn-server/common/log" + "91porn-server/models/v/checkinconfigmod" + "91porn-server/models/v/vidmod" + "91porn-server/skd/service" + + "go.mongodb.org/mongo-driver/bson" +) + +var videoCheckRunning bool + +// VideoCheckRS 定期检查签到奖励视频的切片状态 +func VideoCheckRS() { + if videoCheckRunning { + return + } + videoCheckRunning = true + defer func() { + videoCheckRunning = false + }() + + cfg, err := checkinconfigmod.FindOne(bson.M{}) + if err != nil || cfg == nil { + return + } + if len(cfg.RewardBgVideos) == 0 { + return + } + + updated := false + for i, v := range cfg.RewardBgVideos { + if v.FsResourceId == "" { + continue + } + // 已完成的跳过 + if v.FsResourceStatus == vidmod.Completed { + continue + } + fileInfo, err := service.PullFileInfo(v.FsResourceId) + if err != nil { + log.Error("checkin VideoCheckRS PullFileInfo fail", + log.Any("fsResourceId", v.FsResourceId), log.E(err)) + continue + } + newStatus := fileInfo.Data.Status + if newStatus == "" { + continue + } + if newStatus != v.FsResourceStatus { + cfg.RewardBgVideos[i].FsResourceStatus = newStatus + updated = true + log.Info("checkin VideoCheckRS status changed", + log.Any("fsResourceId", v.FsResourceId), + log.Any("oldStatus", v.FsResourceStatus), + log.Any("newStatus", newStatus)) + } + } + + if updated { + err := checkinconfigmod.UpdateOne( + bson.M{"_id": cfg.ID}, + bson.M{"$set": bson.M{"rewardBgVideos": cfg.RewardBgVideos}}, + ) + if err != nil { + log.Error("checkin VideoCheckRS update config fail", log.E(err)) + } + } +} diff --git a/skd/job/dailyAdverCalc/job.go b/skd/job/dailyAdverCalc/job.go new file mode 100644 index 0000000..e3f5c20 --- /dev/null +++ b/skd/job/dailyAdverCalc/job.go @@ -0,0 +1,205 @@ +package dailyAdverCalc + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/models/commod" + "91porn-server/models/s/dailyretentionmod" + "91porn-server/models/s/useradverstatmod" + "context" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// retentionKey 用于内存中按 (RegDay, AdGroup) 分组 +type retentionKey struct { + RegDay time.Time + AdGroup commod.AdGroup +} + +// retentionAccum 内存中的累计数据 +type retentionAccum struct { + Retained int64 + AdClick int64 + TotalClick int64 + PayCount int64 + PayTotal int64 +} + +// CalcRetention 计算留存数据 +// targetDate: 活跃日期(要统计哪一天的活跃数据) +// 例如:targetDate=15号,会计算所有在15号活跃的用户,按其注册日分组,计算各自的第N天留存 +func CalcRetention(ctx context.Context, targetDate time.Time) error { + targetDate = common.NormalizeDate(targetDate) + startDate := targetDate.AddDate(0, 0, -dailyretentionmod.MaxRetentionDays) + accumMap := make(map[retentionKey]*retentionAccum) + + // 1. 在 Mongo 中按 (regDay, adGroup) 聚合,避免把当天明细全部拉回内存 + stats, err := findUserAdStatSummaryByDate(ctx, targetDate, startDate) + if err != nil { + return fmt.Errorf("query UserAdStat failed: %w", err) + } + for _, stat := range stats { + key := retentionKey{ + RegDay: common.NormalizeDate(stat.ID.RegDay), + AdGroup: stat.ID.AdGroup, + } + if accumMap[key] == nil { + accumMap[key] = &retentionAccum{} + } + accumMap[key].Retained = stat.Retained + accumMap[key].AdClick = stat.AdClick + accumMap[key].TotalClick = stat.TotalClick + accumMap[key].PayCount = stat.PayCount + accumMap[key].PayTotal = stat.PayTotal + } + log.Info("calc retention", log.Any("stats.total", len(stats)), log.Any("stats", stats)) + // 2. 批量写入 DailyRetention + now := time.Now() + writes := make([]mongo.WriteModel, 0, len(accumMap)) + for key, acc := range accumMap { + // 计算是注册后第几天 + dayN := int(targetDate.Sub(key.RegDay).Hours() / 24) + if dayN < 0 || dayN > dailyretentionmod.MaxRetentionDays { + log.Info("calc retention check dayN", log.Any("dayN", dayN)) + continue // 超出范围,跳过 + } + + retained := acc.Retained + + // 获取已有记录(用于计算留存率和累计值) + existingItem, _ := getDailyRetention(ctx, key.RegDay, key.AdGroup) + cohortSize := retained + if dayN > 0 && existingItem != nil { + cohortSize = existingItem.NewUsers + } + rate := calculateRetentionRate(retained, cohortSize) + + // 计算累计值:前一天累计 + 当天值 + prevAdClick, prevTotalClick, prevPayCount, prevPayTotal := getPrevAccum(existingItem, dayN) + adClickAcc := prevAdClick + acc.AdClick + totalClickAcc := prevTotalClick + acc.TotalClick + payCountAcc := prevPayCount + acc.PayCount + payTotalAcc := prevPayTotal + acc.PayTotal + + writes = append(writes, buildDailyRetentionUpsertModel( + key.RegDay, key.AdGroup, dayN, retained, rate, + adClickAcc, totalClickAcc, payCountAcc, payTotalAcc, now, + )) + } + log.Info("calc retention ", log.Any("writes", writes)) + if err := dailyretentionmod.BulkWrite(writes, options.BulkWrite().SetOrdered(false)); err != nil { + return fmt.Errorf("bulk upsert daily retention failed: %w", err) + } + + return nil +} + +// getDailyRetention 获取某个 (RegDay, AdGroup) 的 DailyRetention 记录 +func getDailyRetention(ctx context.Context, regDay time.Time, adGroup commod.AdGroup) (*dailyretentionmod.DailyRetention, error) { + filter := bson.M{ + "date": regDay, + "adGroup": adGroup, + } + return dailyretentionmod.QueryOne(filter) +} + +// buildDailyRetentionUpsertModel 构造留存数据 upsert 模型 +func buildDailyRetentionUpsertModel(regDay time.Time, adGroup commod.AdGroup, dayN int, retained int64, rate float64, adClick, totalClick, payCount, payTotal int64, now time.Time) mongo.WriteModel { + filter, update := buildDailyRetentionUpsertData(regDay, adGroup, dayN, retained, rate, adClick, totalClick, payCount, payTotal, now) + return mongo.NewUpdateOneModel(). + SetFilter(filter). + SetUpdate(update). + SetUpsert(true) +} + +func buildDailyRetentionUpsertData(regDay time.Time, adGroup commod.AdGroup, dayN int, retained int64, rate float64, adClick, totalClick, payCount, payTotal int64, now time.Time) (bson.M, bson.M) { + regDay = common.NormalizeDate(regDay) + + filter := bson.M{ + "date": regDay, + "adGroup": adGroup, + } + + // 使用点号表示法更新 map 中的特定 key + retainedKey := fmt.Sprintf("%s.%d", "userRetained", dayN) + ratesKey := fmt.Sprintf("%s.%d", "userRates", dayN) + adClickKey := fmt.Sprintf("%s.%d", "adClickAcc", dayN) + totalClickKey := fmt.Sprintf("%s.%d", "totalClickAcc", dayN) + payCountKey := fmt.Sprintf("%s.%d", "payCountAcc", dayN) + payTotalKey := fmt.Sprintf("%s.%d", "payTotalAcc", dayN) + + update := bson.M{ + "$setOnInsert": bson.M{ + "date": regDay, + "adGroup": adGroup, + "createdAt": now, + }, + "$set": bson.M{ + "updatedAt": now, + retainedKey: retained, + ratesKey: rate, + adClickKey: adClick, + totalClickKey: totalClick, + payCountKey: payCount, + payTotalKey: payTotal, + }, + } + + // 如果是 day0,同时更新 NewUsers + if dayN == 0 { + update["$set"].(bson.M)["newUsers"] = retained + } + return filter, update +} + +func calculateRetentionRate(retained, cohortSize int64) float64 { + if cohortSize <= 0 { + return 0 + } + return float64(retained) / float64(cohortSize) +} + +// getPrevAccum 获取前一天的累计值 +func getPrevAccum(item *dailyretentionmod.DailyRetention, dayN int) (adClick, totalClick, payCount, payTotal int64) { + if item == nil || dayN <= 0 { + return 0, 0, 0, 0 + } + prevDay := dayN - 1 + if item.AdClickAcc != nil { + adClick = item.AdClickAcc[prevDay] + } + if item.TotalClickAcc != nil { + totalClick = item.TotalClickAcc[prevDay] + } + if item.PayCountAcc != nil { + payCount = item.PayCountAcc[prevDay] + } + if item.PayTotalAcc != nil { + payTotal = item.PayTotalAcc[prevDay] + } + return +} + +// findDailyRetentionByDateRange 查询指定日期范围内的 DailyRetention 记录 +func findDailyRetentionByDateRange(ctx context.Context, startDate, endDate time.Time) ([]*dailyretentionmod.DailyRetention, error) { + filter := bson.M{ + "date": bson.M{ + "$gte": startDate, + "$lte": endDate, + }, + } + return dailyretentionmod.QueryAllList(filter) +} + +// findUserAdStatSummaryByDate 查询指定日期在留存窗口内的聚合数据 +func findUserAdStatSummaryByDate(ctx context.Context, date, startDate time.Time) ([]*useradverstatmod.DailyRetentionSummary, error) { + local, _ := time.LoadLocation("Asia/Shanghai") + date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, local) + startDate = common.NormalizeDate(startDate) + return useradverstatmod.AggregateDailyRetention(date, startDate, date) +} diff --git a/skd/job/dailyAdverCalc/job_test.go b/skd/job/dailyAdverCalc/job_test.go new file mode 100644 index 0000000..3985f07 --- /dev/null +++ b/skd/job/dailyAdverCalc/job_test.go @@ -0,0 +1,105 @@ +package dailyAdverCalc + +import ( + "91porn-server/common" + "91porn-server/models/commod" + "91porn-server/models/s/dailyretentionmod" + "testing" + "time" + + "go.mongodb.org/mongo-driver/bson" +) + +func TestCalculateRetentionRate(t *testing.T) { + t.Run("zero cohort size returns zero", func(t *testing.T) { + if got := calculateRetentionRate(10, 0); got != 0 { + t.Fatalf("expected zero rate, got %v", got) + } + }) + + t.Run("uses retained divided by cohort size", func(t *testing.T) { + got := calculateRetentionRate(25, 100) + want := 0.25 + if got != want { + t.Fatalf("expected %v, got %v", want, got) + } + }) +} + +func TestGetPrevAccum(t *testing.T) { + t.Run("nil item or day zero returns zeros", func(t *testing.T) { + adClick, totalClick, payCount, payTotal := getPrevAccum(nil, 0) + if adClick != 0 || totalClick != 0 || payCount != 0 || payTotal != 0 { + t.Fatalf("expected all zero values, got %d %d %d %d", adClick, totalClick, payCount, payTotal) + } + }) + + t.Run("reads previous day accumulators", func(t *testing.T) { + item := &dailyretentionmod.DailyRetention{ + AdClickAcc: map[int]int64{1: 3, 2: 8}, + TotalClickAcc: map[int]int64{1: 5, 2: 13}, + PayCountAcc: map[int]int64{1: 1, 2: 2}, + PayTotalAcc: map[int]int64{1: 100, 2: 300}, + } + + adClick, totalClick, payCount, payTotal := getPrevAccum(item, 3) + if adClick != 8 || totalClick != 13 || payCount != 2 || payTotal != 300 { + t.Fatalf("unexpected previous accum values: %d %d %d %d", adClick, totalClick, payCount, payTotal) + } + }) +} + +func TestBuildDailyRetentionUpsertData(t *testing.T) { + loc, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + t.Fatalf("load location failed: %v", err) + } + regDay := time.Date(2026, 3, 18, 14, 25, 0, 0, loc) + now := time.Date(2026, 3, 19, 9, 30, 0, 0, loc) + + t.Run("day zero writes new users and normalizes date", func(t *testing.T) { + filter, update := buildDailyRetentionUpsertData(regDay, commod.AdGroupA, 0, 12, 1, 4, 8, 2, 600, now) + + wantDate := common.NormalizeDate(regDay) + if got := filter["date"].(time.Time); !got.Equal(wantDate) { + t.Fatalf("expected normalized date %v, got %v", wantDate, got) + } + if got := filter["adGroup"]; got != commod.AdGroupA { + t.Fatalf("expected ad group %v, got %v", commod.AdGroupA, got) + } + + setOnInsert := update["$setOnInsert"].(bson.M) + if got := setOnInsert["date"].(time.Time); !got.Equal(wantDate) { + t.Fatalf("expected setOnInsert date %v, got %v", wantDate, got) + } + if got := setOnInsert["createdAt"].(time.Time); !got.Equal(now) { + t.Fatalf("expected createdAt %v, got %v", now, got) + } + + setFields := update["$set"].(bson.M) + if got := setFields["userRetained.0"]; got != int64(12) { + t.Fatalf("expected userRetained.0 to be 12, got %v", got) + } + if got := setFields["userRates.0"]; got != float64(1) { + t.Fatalf("expected userRates.0 to be 1, got %v", got) + } + if got := setFields["newUsers"]; got != int64(12) { + t.Fatalf("expected newUsers to be 12, got %v", got) + } + }) + + t.Run("non day zero does not overwrite new users", func(t *testing.T) { + _, update := buildDailyRetentionUpsertData(regDay, commod.AdGroupB, 2, 6, 0.3, 9, 11, 3, 800, now) + + setFields := update["$set"].(bson.M) + if got := setFields["userRetained.2"]; got != int64(6) { + t.Fatalf("expected userRetained.2 to be 6, got %v", got) + } + if got := setFields["userRates.2"]; got != 0.3 { + t.Fatalf("expected userRates.2 to be 0.3, got %v", got) + } + if _, ok := setFields["newUsers"]; ok { + t.Fatalf("did not expect newUsers on non-day-zero update") + } + }) +} diff --git a/skd/job/dataclean/dataclean.go b/skd/job/dataclean/dataclean.go new file mode 100644 index 0000000..557ebcf --- /dev/null +++ b/skd/job/dataclean/dataclean.go @@ -0,0 +1,29 @@ +package dataclean + +import ( + "time" + + "91porn-server/common/timeutil" +) + +func RunSync() { + today := timeutil.BeginningOfDay(time.Now()) + + // log库清理 + go clearVisitLog(today, 7) + go clearPlayLog(today) + go clearSearchLog(today) + go clearLoginTime(today) + go clearUserRegisterLog(today) + + // stat库清理 + go clearKeywordStat(today) + go clearNewsHot(today) + go clearKwRankStat(today) + go clearVideoStat(today) + + // 主库清理 + go clearNoticeFmt(today) + go clearUserAct(today) + go clearGuestUser(today) +} diff --git a/skd/job/dataclean/method.go b/skd/job/dataclean/method.go new file mode 100644 index 0000000..78528c7 --- /dev/null +++ b/skd/job/dataclean/method.go @@ -0,0 +1,94 @@ +package dataclean + +import ( + "91porn-server/models/v/usermod" + "time" + + "91porn-server/common/log" + "91porn-server/models/l/loginlgmod" + "91porn-server/models/l/playlgmod" + "91porn-server/models/l/registermod" + "91porn-server/models/l/searchlogmod" + "91porn-server/models/l/visitlogmod" + "91porn-server/models/s/hotstatmod" + "91porn-server/models/s/kwrankmod" + "91porn-server/models/s/kwstatmod" + "91porn-server/models/s/statvidmod" + "91porn-server/models/v/noticefmtmod" + "91porn-server/models/v/useractmod" +) + +func clearVisitLog(today time.Time, day int) { + count, err := visitlogmod.DataClean(today.AddDate(0, 0, -day)) + if err != nil { + log.Error("skd dataclean clearVisitLog error", log.E(err)) + return + } + log.Info("skd dataclean clearVisitLog cleaned", log.Any("count", count)) +} + +func clearPlayLog(today time.Time) { + if err := playlgmod.DeleteBeforeCreatedAt(nil, today.AddDate(0, 0, -3)); err != nil { + log.Error("skd dataclean clearPlayLog error", log.E(err)) + } +} + +func clearSearchLog(today time.Time) { + if err := searchlogmod.DeleteBeforeCreatedAt(nil, today.AddDate(0, 0, -3)); err != nil { + log.Error("skd dataclean clearSearchLog error", log.E(err)) + } +} + +func clearKeywordStat(today time.Time) { + if err := kwstatmod.DeleteBeforeSumDate(nil, today.AddDate(0, 0, -30)); err != nil { + log.Error("skd dataclean clearKeywordStat error", log.E(err)) + } +} + +func clearNewsHot(today time.Time) { + if err := hotstatmod.DeleteBeforeCreatedAt(nil, today.AddDate(0, 0, -32)); err != nil { + log.Error("skd dataclean clearNewsHot error", log.E(err)) + } +} + +func clearLoginTime(today time.Time) { + if err := loginlgmod.DeleteBeforeLoginTime(nil, today.AddDate(0, 0, -5)); err != nil { + log.Error("skd dataclean clearLoginTime error", log.E(err)) + } +} + +func clearUserRegisterLog(today time.Time) { + if err := registermod.DeleteBeforeCreatedAt(nil, today.AddDate(0, 0, -7)); err != nil { + log.Error("skd dataclean clearUserRegisterLog error", log.E(err)) + } +} + +func clearKwRankStat(today time.Time) { + if err := kwrankmod.DeleteBeforeSumDate(nil, today.AddDate(0, 0, -30)); err != nil { + log.Error("skd dataclean clearKwRankStat error", log.E(err)) + } +} + +func clearVideoStat(today time.Time) { + if err := statvidmod.DeleteBeforeCreatedAt(nil, today.AddDate(0, 0, -7)); err != nil { + log.Error("skd dataclean clearVideoStat error", log.E(err)) + } +} + +func clearNoticeFmt(today time.Time) { + if err := noticefmtmod.DeleteBeforeCreatedAt(nil, today.AddDate(0, 0, -5)); err != nil { + log.Error("skd dataclean clearNoticeFmt error", log.E(err)) + } +} + +func clearUserAct(today time.Time) { + if err := useractmod.DeleteBeforeDailyDate(nil, today.AddDate(0, 0, -2)); err != nil { + log.Error("skd dataclean clearUserAct error", log.E(err)) + } +} + +func clearGuestUser(today time.Time) { + if err := usermod.DeleteGuestBeforeDate(nil, today.AddDate(0, 0, -7)); err != nil { + log.Error("skd dataclean clearGuestUser error", log.E(err)) + } +} diff --git a/skd/job/elastic/esaudiobook/method.go b/skd/job/elastic/esaudiobook/method.go new file mode 100644 index 0000000..72a1426 --- /dev/null +++ b/skd/job/elastic/esaudiobook/method.go @@ -0,0 +1,27 @@ +package esaudiobook + +import ( + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/v/audiobookmod" +) + +// 同步用户数据 +func SyncDataToES(data []audiobookmod.AudioBookBase) error { + if len(data) == 0 { + return nil + } + var source = elastic.M{} + for k := range data { + var tmp = data[k] + source[data[k].ID.Hex()] = tmp + } + log.Info("Elastic sync audiobook data committing...") + if err := es.Bulk(models.ESInfoAudioBookTable, source); err != nil { + log.Info("Elastic sync audiobook data commit failed.") + return err + } + log.Info("Elastic sync audiobook data committed.") + return nil +} diff --git a/skd/job/elastic/esaudiobook/sync.go b/skd/job/elastic/esaudiobook/sync.go new file mode 100644 index 0000000..404dc28 --- /dev/null +++ b/skd/job/elastic/esaudiobook/sync.go @@ -0,0 +1,58 @@ +package esaudiobook + +import ( + "fmt" + "time" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/v/audiobookmod" + "91porn-server/skd/skdg" +) + +var es *elastic.Client + +func RunSync(defaultDate time.Time) { + es = skdg.VideoES + log.Info("Elastic Search audiobook sync start...") + defer log.Info("Elastic Search audiobook sync end.") + // 获取上次数据同步的记录时间 + lastSyncAt, err := statrecordmod.LastRecordTime(statrecordmod.ElasticSyncAudioBookJob, defaultDate) + if err != nil { + log.Error("Elastic sync audiobook data RunSync get last record time failed", log.E(err)) + return + } + var now = time.Now().Add(-time.Minute * 20) + var recentMinute = timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) + for recentMinute.After(lastSyncAt) { + var end = lastSyncAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + if end.After(recentMinute) { + end = recentMinute + } + var subTimeRange = timerange.TimeRange{ + Head: lastSyncAt, + Tail: end, + } + data, err := audiobookmod.GetListByUpdateTimeRange(subTimeRange.Head, subTimeRange.Tail) + if err != nil { + return + } + if err = commit(data, subTimeRange.Tail); err != nil { + log.Error("Elastic sync audiobook data RunSync commit err! ", log.E(err)) + return + } + lastSyncAt = end + } +} + +func commit(data []audiobookmod.AudioBookBase, syncTime time.Time) error { + if err := SyncDataToES(data); err != nil { + return fmt.Errorf("Elastic sync commit audiobook data SyncDataToES failed!, err:%+v\n", err) + } + if err := statrecordmod.UpsertOneTrans(nil, statrecordmod.ElasticSyncAudioBookJob, syncTime); err != nil { + return fmt.Errorf("Elastic sync commit audiobook data UpsertOneTrans failed!, err:%+v\n", err) + } + return nil +} diff --git a/skd/job/elastic/eskeyword/method.go b/skd/job/elastic/eskeyword/method.go new file mode 100644 index 0000000..2e24459 --- /dev/null +++ b/skd/job/elastic/eskeyword/method.go @@ -0,0 +1,26 @@ +package eskeyword + +import ( + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/s/kwstatmod" +) + +// 同步数据到ES +func SyncDataToES(data []kwstatmod.Keyword) error { + if len(data) == 0 { + return nil + } + var source = elastic.M{} + for _, v := range data { + source[v.ID.Hex()] = kwstatmod.ESKeywordSrc(v) + } + log.Info("Elastic sync keyword committing...") + if err := es.Bulk(models.ESStatKeywordTable, source); err != nil { + log.Info("Elastic sync keyword commit failed.") + return err + } + log.Info("Elastic sync keyword committed.") + return nil +} diff --git a/skd/job/elastic/eskeyword/sync.go b/skd/job/elastic/eskeyword/sync.go new file mode 100644 index 0000000..50d8a50 --- /dev/null +++ b/skd/job/elastic/eskeyword/sync.go @@ -0,0 +1,58 @@ +package eskeyword + +import ( + "fmt" + "time" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/kwstatmod" + "91porn-server/models/s/statrecordmod" + "91porn-server/skd/skdg" +) + +var es *elastic.Client + +func RunSync(defaultDate time.Time) { + es = skdg.VideoES + log.Info("Elastic Search sync keyword start...") + defer log.Info("Elastic Search tag sync keyword end.") + // 获取上次数据同步的记录时间 + lastSyncAt, err := statrecordmod.LastRecordTime(statrecordmod.ElasticSyncKeywordJob, defaultDate) + if err != nil { + log.Error("Elastic sync keyword data RunSync get last record time failed", log.E(err)) + return + } + var now = time.Now() + var recentMinute = timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) + for recentMinute.After(lastSyncAt) { + var end = lastSyncAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + if end.After(recentMinute) { + end = recentMinute + } + var subTimeRange = timerange.TimeRange{ + Head: lastSyncAt, + Tail: end, + } + data, err := kwstatmod.GetKeywordsByTimeRange(subTimeRange.Head, subTimeRange.Tail) + if err != nil { + return + } + if err = commit(data, subTimeRange.Tail); err != nil { + log.Error("Elastic sync keyword data RunSync commit err! ", log.E(err)) + return + } + lastSyncAt = end + } +} + +func commit(data []kwstatmod.Keyword, syncTime time.Time) error { + if err := SyncDataToES(data); err != nil { + return fmt.Errorf("Elastic sync commit keyword data SyncDataToES failed!, err:%+v\n", err) + } + if err := statrecordmod.UpsertOneTrans(nil, statrecordmod.ElasticSyncKeywordJob, syncTime); err != nil { + return fmt.Errorf("Elastic sync commit keyword data UpsertOneTrans failed!, err:%+v\n", err) + } + return nil +} diff --git a/skd/job/elastic/esmedia/method.go b/skd/job/elastic/esmedia/method.go new file mode 100644 index 0000000..c3fe9fd --- /dev/null +++ b/skd/job/elastic/esmedia/method.go @@ -0,0 +1,65 @@ +package esmedia + +import ( + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/v/mediamod" +) + +// SyncDataToES 同步数据到ES +func SyncDataToES(data []*mediamod.Media) error { + if len(data) == 0 { + return nil + } + + source := buildSource(data) + log.Info("Elastic sync ACG media data committing...") + if err := es.BulkChecked(models.ESMediaTable, source); err != nil { + log.Info("Elastic sync ACG media data commit failed.") + return err + } + log.Info("Elastic sync ACG media data committed.") + return nil +} + +func buildSource(data []*mediamod.Media) elastic.M { + var source = elastic.M{} + for _, v := range data { + source[v.ID.Hex()] = mediamod.ESMedia{ + ID: v.ID, + Title: v.Title, + HorizontalCover: v.HorizontalCover, + VerticalCover: v.VerticalCover, + Tags: v.Tags, + Summary: v.Summary, + TotalEpisode: v.TotalEpisode, + UpdateStatus: v.UpdateStatus, + MediaType: v.MediaType, + Kind: v.Kind, + PermissionIconHide: v.PermissionIconHide, + Permission: v.Permission, + Price: v.Price, + Direction: v.Direction, + FreeTime: v.FreeTime, + CountLike: v.CountLike, + CountBrowse: v.CountBrowse, + CountCollect: v.CountCollect, + MID: v.MID, + ModuleName: v.ModuleName, + SectionName: v.SectionName, + SID: v.SID, + SectionSort: v.SectionSort, + Choice: v.Choice, + ChoiceSort: v.ChoiceSort, + Number: v.Number, + SortCode: v.SortCode, + Status: v.Status, + IsDelete: v.IsDelete, + UpdatedAct: v.UpdatedAct, + CreatedAt: v.CreatedAt, + UpdateTime: v.UpdateTime, + } + } + return source +} diff --git a/skd/job/elastic/esmedia/method_test.go b/skd/job/elastic/esmedia/method_test.go new file mode 100644 index 0000000..f459423 --- /dev/null +++ b/skd/job/elastic/esmedia/method_test.go @@ -0,0 +1,25 @@ +package esmedia + +import ( + "testing" + + "91porn-server/models/v/mediamod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestBuildSourceIncludesCountCollect(t *testing.T) { + id := primitive.NewObjectID() + source := buildSource([]*mediamod.Media{{ + ID: id, + CountCollect: 12, + }}) + + media, ok := source[id.Hex()].(mediamod.ESMedia) + if !ok { + t.Fatalf("source type = %T, want mediamod.ESMedia", source[id.Hex()]) + } + if media.CountCollect != 12 { + t.Fatalf("countCollect = %d, want 12", media.CountCollect) + } +} diff --git a/skd/job/elastic/esmedia/sync.go b/skd/job/elastic/esmedia/sync.go new file mode 100644 index 0000000..44291b5 --- /dev/null +++ b/skd/job/elastic/esmedia/sync.go @@ -0,0 +1,80 @@ +package esmedia + +import ( + "fmt" + "time" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/v/mediamod" + "91porn-server/skd/skdg" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var es *elastic.Client + +func RunSync(defaultDate time.Time) { + es = skdg.VideoES + log.Info("Elastic Search ACG media sync start...") + defer log.Info("Elastic Search ACG media sync end.") + // MongoDB 日期精度为毫秒;本轮开始后的修改必须留给下一轮再次同步。 + startedAt := time.Now().Truncate(time.Millisecond) + lastSyncAt, err := statrecordmod.LastRecordTime(statrecordmod.ElasticSyncMediaJob, defaultDate) + if err != nil { + log.Error("Elastic sync ACG media get last record time failed", log.E(err)) + return + } + if !startedAt.After(lastSyncAt) { + return + } + // 固定 ID 上界,避免持续新增数据使本轮无法结束。 + maxID, err := mediamod.GetLatestIDForESSync() + if err != nil { + log.Error("Elastic sync ACG media get latest ID failed", log.E(err)) + return + } + sync := mediaSync{ + read: mediamod.GetListForESSync, + write: SyncDataToES, + checkpoint: func(at time.Time) error { + return statrecordmod.UpsertOneTrans(nil, statrecordmod.ElasticSyncMediaJob, at) + }, + } + if err = sync.run(lastSyncAt, startedAt, maxID); err != nil { + log.Error("Elastic sync ACG media failed; checkpoint not confirmed", log.E(err)) + } +} + +type mediaSync struct { + read func(time.Time, primitive.ObjectID, primitive.ObjectID, int) ([]*mediamod.Media, error) + write func([]*mediamod.Media) error + checkpoint func(time.Time) error +} + +func (s mediaSync) run(since, startedAt time.Time, maxID primitive.ObjectID) error { + const size = 1000 + var after primitive.ObjectID + for !maxID.IsZero() { + data, err := s.read(since, after, maxID, size) + if err != nil { + return fmt.Errorf("read media batch: %w", err) + } + if len(data) == 0 { + break + } + if err = s.write(data); err != nil { + return fmt.Errorf("write media batch: %w", err) + } + after = data[len(data)-1].ID + if len(data) < size || after == maxID { + break + } + } + // 所有批次及每条 ES 写入均成功后才推进;失败时下一轮从原进度幂等重试。 + if err := s.checkpoint(startedAt); err != nil { + return fmt.Errorf("save media checkpoint: %w", err) + } + return nil +} diff --git a/skd/job/elastic/essearchlog/method.go b/skd/job/elastic/essearchlog/method.go new file mode 100644 index 0000000..bc5008e --- /dev/null +++ b/skd/job/elastic/essearchlog/method.go @@ -0,0 +1,26 @@ +package essearchlog + +import ( + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/l/searchlogmod" +) + +// 同步数据到ES +func SyncDataToES(data []searchlogmod.SearchLog) error { + if len(data) == 0 { + return nil + } + var source = elastic.M{} + for _, v := range data { + source[v.ID.Hex()] = searchlogmod.ESSearchLogSource(v) + } + log.Info("Elastic sync search log data committing...") + if err := es.Bulk(models.ESLogSearchLogTable, source); err != nil { + log.Info("Elastic sync search log data commit failed.") + return err + } + log.Info("Elastic sync search log data committed.") + return nil +} diff --git a/skd/job/elastic/essearchlog/sync.go b/skd/job/elastic/essearchlog/sync.go new file mode 100644 index 0000000..bb9cb9b --- /dev/null +++ b/skd/job/elastic/essearchlog/sync.go @@ -0,0 +1,58 @@ +package essearchlog + +import ( + "fmt" + "time" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/l/searchlogmod" + "91porn-server/models/s/statrecordmod" + "91porn-server/skd/skdg" +) + +var es *elastic.Client + +func RunSync(defaultDate time.Time) { + es = skdg.VideoES + log.Info("Elastic Search sync search log start...") + defer log.Info("Elastic Search tag sync search log end.") + // 获取上次数据同步的记录时间 + lastSyncAt, err := statrecordmod.LastRecordTime(statrecordmod.ElasticSyncSearchLogJob, defaultDate) + if err != nil { + log.Error("Elastic sync search log data RunSync get last record time failed", log.E(err)) + return + } + var now = time.Now() + var recentMinute = timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) + for recentMinute.After(lastSyncAt) { + var end = lastSyncAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + if end.After(recentMinute) { + end = recentMinute + } + var subTimeRange = timerange.TimeRange{ + Head: lastSyncAt, + Tail: end, + } + data, err := searchlogmod.GetSearchLogByTimeRange(subTimeRange.Head, subTimeRange.Tail) + if err != nil { + return + } + if err = commit(data, subTimeRange.Tail); err != nil { + log.Error("Elastic sync search log data RunSync commit err! ", log.E(err)) + return + } + lastSyncAt = end + } +} + +func commit(data []searchlogmod.SearchLog, syncTime time.Time) error { + if err := SyncDataToES(data); err != nil { + return fmt.Errorf("Elastic sync commit search log data SyncDataToES failed!, err:%+v\n", err) + } + if err := statrecordmod.UpsertOneTrans(nil, statrecordmod.ElasticSyncSearchLogJob, syncTime); err != nil { + return fmt.Errorf("Elastic sync commit search log data UpsertOneTrans failed!, err:%+v\n", err) + } + return nil +} diff --git a/skd/job/elastic/estag/method.go b/skd/job/elastic/estag/method.go new file mode 100644 index 0000000..5585155 --- /dev/null +++ b/skd/job/elastic/estag/method.go @@ -0,0 +1,44 @@ +package estag + +import ( + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/v/tagmod" +) + +// 同步数据到ES +func SyncDataToES(data []tagmod.Tag) error { + if len(data) == 0 { + return nil + } + var source = elastic.M{} + for _, v := range data { + var tmp = tagmod.ESTag{ + ID: v.ID, + TagName: v.TagName, + SortCode: v.SortCode, + VPlayCount: v.VPlayCount, + TPlayCount: v.TPlayCount, + FakePlayCount: v.FakePlayCount, + PlayRating: v.PlayRating, + CollCount: v.CollCount, + LikeCount: v.LikeCount, + FakeCollCount: v.FakeCollCount, + VideoCount: v.VideoCount, + IsActive: v.IsActive, + IsWonder: v.IsWonder, + WonderSortCode: v.WonderSortCode, + CreatedAt: v.CreatedAt, + UpdatedAt: v.UpdatedAt, + } + source[v.ID.Hex()] = tmp + } + log.Info("Elastic sync tag data committing...") + if err := es.Bulk(models.ESInfoTagTable, source); err != nil { + log.Info("Elastic sync tag data commit failed.") + return err + } + log.Info("Elastic sync tag data committed.") + return nil +} diff --git a/skd/job/elastic/estag/sync.go b/skd/job/elastic/estag/sync.go new file mode 100644 index 0000000..9199025 --- /dev/null +++ b/skd/job/elastic/estag/sync.go @@ -0,0 +1,61 @@ +package estag + +import ( + "fmt" + "time" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/v/tagmod" + "91porn-server/skd/skdg" +) + +var es *elastic.Client + +func RunSync(defaultDate time.Time) { + es = skdg.VideoES + log.Info("Elastic Search tag sync start...") + defer log.Info("Elastic Search tag sync end.") + // 获取上次数据同步的记录时间 + lastSyncAt, err := statrecordmod.LastRecordTime(statrecordmod.ElasticSyncTagJob, defaultDate) + if err != nil { + log.Error("Elastic sync tag data RunSync get last record time failed", log.E(err)) + return + } + var now = time.Now() + var recentMinute = timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) + for recentMinute.After(lastSyncAt) { + var end = lastSyncAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + if end.After(recentMinute) { + end = recentMinute + } + var subTimeRange = timerange.TimeRange{ + Head: lastSyncAt, + Tail: end, + } + data, err := tagmod.GetTagListByUpdateTimeRange(subTimeRange.Head, subTimeRange.Tail) + if err != nil { + return + } + if err = commit(data, subTimeRange.Tail); err != nil { + log.Error("Elastic sync tag data RunSync commit err! ", log.E(err)) + return + } + lastSyncAt = end + } +} + +func commit(data []tagmod.Tag, syncTime time.Time) error { + if len(data) <= 0 { + return nil + } + if err := SyncDataToES(data); err != nil { + return fmt.Errorf("Elastic sync commit tag data SyncDataToES failed!, err:%+v\n", err) + } + if err := statrecordmod.UpsertOneTrans(nil, statrecordmod.ElasticSyncTagJob, syncTime); err != nil { + return fmt.Errorf("Elastic sync commit tag data UpsertOneTrans failed!, err:%+v\n", err) + } + return nil +} diff --git a/skd/job/elastic/esuser/method.go b/skd/job/elastic/esuser/method.go new file mode 100644 index 0000000..5c7af30 --- /dev/null +++ b/skd/job/elastic/esuser/method.go @@ -0,0 +1,38 @@ +package esuser + +import ( + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/v/usermod" +) + +// 同步用户数据 +func SyncDataToES(data []usermod.User) error { + if len(data) == 0 { + return nil + } + var source = elastic.M{} + for _, v := range data { + var tmp = usermod.ESUser{ + ID: v.ID, + UID: v.UID, + Gender: v.Gender, + Name: v.Name, + Summary: v.Summary, + Region: v.Region, + CreatedAt: v.CreatedAt, + UpdatedAt: v.UpdatedAt, + HasLocked: v.HasLocked, + HasBanned: v.HasBanned, + } + source[v.ID.Hex()] = tmp + } + log.Info("Elastic sync user data committing...") + if err := es.Bulk(models.ESInfoUserTable, source); err != nil { + log.Info("Elastic sync user data commit failed.") + return err + } + log.Info("Elastic sync user data committed.") + return nil +} diff --git a/skd/job/elastic/esuser/sync.go b/skd/job/elastic/esuser/sync.go new file mode 100644 index 0000000..e175eae --- /dev/null +++ b/skd/job/elastic/esuser/sync.go @@ -0,0 +1,72 @@ +package esuser + +import ( + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/v/usermod" + "91porn-server/skd/skdg" + "fmt" + "time" +) + +var es *elastic.Client + +func RunSync(defaultDate time.Time) { + es = skdg.VideoES + log.Info("Elastic Search user sync start...") + defer log.Info("Elastic Search user sync end.") + // 获取上次数据同步的记录时间 + lastSyncAt, err := statrecordmod.LastRecordTime(statrecordmod.ElasticSyncUserJob, defaultDate) + if err != nil { + log.Error("Elastic sync user data RunSync get last record time failed", log.E(err)) + return + } + var now = time.Now() + var recentMinute = timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) + for recentMinute.After(lastSyncAt) { + var end = lastSyncAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + if end.After(recentMinute) { + end = recentMinute + } + var subTimeRange = timerange.TimeRange{ + Head: lastSyncAt, + Tail: end, + } + var page, size = 1, 1000 + hasNext := true + var data []usermod.User + for hasNext { + data, hasNext, err = usermod.GetUserListByUpdateTimeRange(subTimeRange.Head, subTimeRange.Tail, page, size) + if err != nil { + log.Error("Elastic sync user data GetUserListByUpdateTimeRange err! ", log.E(err)) + return + } + + if hasNext && len(data) > 0 { + _ = SyncDataToES(data) + page++ + } else { + if err = commit(data, subTimeRange.Tail); err != nil { + log.Error("Elastic sync user data RunSync commit err! ", log.E(err)) + return + } + } + } + + lastSyncAt = end + } +} + +func commit(data []usermod.User, syncTime time.Time) error { + err := SyncDataToES(data) + if err != nil { + return fmt.Errorf("Elastic sync commit user data SyncDataToES failed!, err:%+v\n", err) + } + err = statrecordmod.UpsertOneTrans(nil, statrecordmod.ElasticSyncUserJob, syncTime) + if err != nil { + return fmt.Errorf("Elastic sync commit user data UpsertOneTrans failed!, err:%+v\n", err) + } + return err +} diff --git a/skd/job/elastic/esvideo/method.go b/skd/job/elastic/esvideo/method.go new file mode 100644 index 0000000..8b412dc --- /dev/null +++ b/skd/job/elastic/esvideo/method.go @@ -0,0 +1,108 @@ +package esvideo + +import ( + "math" + "time" + + "91porn-server/common/elastic" + "91porn-server/common/localcache" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/vidmod" + "91porn-server/models/v/vidpopmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const vidPopCfgKey = "vidPopCfgKey" + +// SyncDataToES 同步数据到ES +func SyncDataToES(data []*vidmod.VideoModel) error { + if len(data) == 0 { + return nil + } + // 获取标签名字 + var tagsID []primitive.ObjectID + for _, v := range data { + tagsID = append(tagsID, v.Tags...) + } + tags, err := tagmod.FindTagsByIDS(tagsID) + if err != nil { + return err + } + var tagsNameMap = make(map[primitive.ObjectID]string) + for _, v := range tags { + tagsNameMap[v.ID] = v.TagName + } + var source = elastic.M{} + for _, v := range data { + var tagsName []string + for _, v1 := range v.Tags { + tagsName = append(tagsName, tagsNameMap[v1]) + } + source[v.ID.Hex()] = vidmod.ESVideo{ + ID: v.ID, + PublisherID: v.PublisherID, + Title: v.Title, + NewsType: v.NewsType, + Tags: v.Tags, + TagsName: tagsName, + Filename: v.Filename, + PlayCount: v.PlayCount, + EffectivePlayCount: v.EffectivePlayCount, + PurchaseCount: v.PurchaseCount, + LikeCount: v.LikeCount, + CommentCount: v.CommentCount, + CollectCount: v.CollectCount, + ShareCount: v.ShareCount, + FakeLikeCount: v.FakeLikeCount, + FakeShareCount: v.FakeShareCount, + FakePlayCount: v.FakePlayCount, + Status: v.Status, + Location: v.Location, + CreatedAt: v.CreatedAt, + UpdatedAt: v.UpdatedAt, + ReviewedAt: v.ReviewAt, + Hot: calcHot(v), + PlayTime: v.PlayTime, + Coins: v.Coins, + } + } + log.Info("Elastic sync video data committing...") + if err = es.Bulk(models.ESInfoVideoTable, source); err != nil { + log.Info("Elastic sync video data commit failed.") + return err + } + log.Info("Elastic sync video data committed.") + return nil +} + +func calcHot(video *vidmod.VideoModel) float64 { + if video.Status == vidmod.CheckPass || video.Status == vidmod.Free { + cfg, err := getVideoPopularityConfig() + if err != nil { + log.Error("Elastic sync video getVideoPopularityConfig", log.E(err)) + return 0 + } + qualityScore := float64(video.PlayCount + cfg.EffectivePlayCountMultiplier*video.EffectivePlayCount + + cfg.LikeCountMultiplier*video.LikeCount) + hot := (qualityScore + float64(cfg.InitialPopularity)) / math.Pow(1+time.Since(video.ReviewAt).Hours(), + cfg.ReviewTimePower) + return hot + } + return 0 +} + +func getVideoPopularityConfig() (*vidpopmod.VideoPopularityConfig, error) { + cfg, ok := localcache.C.Get(vidPopCfgKey) + if ok { + return cfg.(*vidpopmod.VideoPopularityConfig), nil + } + res, err := vidpopmod.FindOne() + if err != nil { + return nil, err + } + localcache.C.Set(vidPopCfgKey, &res, 5*time.Minute) + return &res, nil +} diff --git a/skd/job/elastic/esvideo/sync.go b/skd/job/elastic/esvideo/sync.go new file mode 100644 index 0000000..13addb0 --- /dev/null +++ b/skd/job/elastic/esvideo/sync.go @@ -0,0 +1,74 @@ +package esvideo + +import ( + "fmt" + "time" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/v/vidmod" + "91porn-server/skd/skdg" +) + +var es *elastic.Client + +func RunSync(defaultDate time.Time) { + es = skdg.VideoES + log.Info("Elastic Search video sync start...") + defer log.Info("Elastic Search video sync end.") + // 获取上次数据同步的记录时间 + lastSyncAt, err := statrecordmod.LastRecordTime(statrecordmod.ElasticSyncVideoJob, defaultDate) + if err != nil { + log.Error("Elastic sync video data RunSync get last record time failed", log.E(err)) + return + } + var now = time.Now() + var recentMinute = timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) + for recentMinute.After(lastSyncAt) { + var end = lastSyncAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + if end.After(recentMinute) { + end = recentMinute + } + var subTimeRange = timerange.TimeRange{ + Head: lastSyncAt, + Tail: end, + } + var page, size = 1, 1000 + hasNext := true + var data []*vidmod.VideoModel + for hasNext { + data, hasNext, err = vidmod.GetVideoListByUpdateTimeRange(subTimeRange.Head, subTimeRange.Tail, page, size) + if err != nil { + log.Error("Elastic sync video data GetVideoListByUpdateTimeRange err! ", log.E(err)) + return + } + if hasNext && len(data) > 0 { + _ = SyncDataToES(data) + page++ + } else { + if err = commit(data, subTimeRange.Tail); err != nil { + log.Error("Elastic sync video data RunSync commit err! ", log.E(err)) + return + } + } + } + + lastSyncAt = end + } +} + +func commit(data []*vidmod.VideoModel, syncTime time.Time) error { + if len(data) <= 0 { + _ = statrecordmod.UpsertOneTrans(nil, statrecordmod.ElasticSyncVideoJob, syncTime) + return nil + } + if err := SyncDataToES(data); err != nil { + return fmt.Errorf("Elastic sync commit video data SyncDataToES failed!, err:%+v\n", err) + } + if err := statrecordmod.UpsertOneTrans(nil, statrecordmod.ElasticSyncVideoJob, syncTime); err != nil { + return fmt.Errorf("Elastic sync commit video data UpsertOneTrans failed!, err:%+v\n", err) + } + return nil +} diff --git a/skd/job/elastic/rdvideo/method.go b/skd/job/elastic/rdvideo/method.go new file mode 100644 index 0000000..cba2ce6 --- /dev/null +++ b/skd/job/elastic/rdvideo/method.go @@ -0,0 +1,27 @@ +package rdvideo + +import ( + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/models/v/vidmod" +) + +// SyncDataToRedis 同步数据到redis +func SyncDataToRedis(data []*vidmod.VideoModel) error { + if len(data) == 0 { + return nil + } + + for _, v := range data { + if v.NewsType != vidmod.SHORT || (v.Status != vidmod.CheckPass && v.Status != vidmod.Free) { + // 去除 + _, _ = redis.Handler.SRem(redisconst.ShortVideosKey, v.ID.Hex()) + } else { + _, _ = redis.Handler.SAdd(redisconst.ShortVideosKey, v.ID.Hex()) + } + } + + log.Info("Redis sync video data completed.") + return nil +} diff --git a/skd/job/elastic/rdvideo/sync.go b/skd/job/elastic/rdvideo/sync.go new file mode 100644 index 0000000..8d3890c --- /dev/null +++ b/skd/job/elastic/rdvideo/sync.go @@ -0,0 +1,77 @@ +package rdvideo + +import ( + "fmt" + "time" + + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/v/vidmod" +) + +func RunSync(defaultDate time.Time) { + log.Info("Redis short video sync start...") + defer log.Info("Redis short video sync end.") + // 如果短视频缓存存在的数据量过低,表示缓存被清理过,需要重新同步 + c, err := redis.Handler.SCard(redisconst.ShortVideosKey) + if err == nil && c < 1000 { + _ = statrecordmod.UpsertOneTrans(nil, statrecordmod.RedisSyncShortVideoJob, defaultDate) + } + + // 获取上次数据同步的记录时间 + lastSyncAt, err := statrecordmod.LastRecordTime(statrecordmod.RedisSyncShortVideoJob, defaultDate) + if err != nil { + log.Error("Redis sync short video data RunSync get last record time failed", log.E(err)) + return + } + var now = time.Now() + var recentMinute = timerange.RecentMinute(now, statrecordmod.OneDayScale) + for recentMinute.After(lastSyncAt) { + var end = lastSyncAt.Add(statrecordmod.OneDayScale * time.Minute) + if end.After(recentMinute) { + end = recentMinute + } + var subTimeRange = timerange.TimeRange{ + Head: lastSyncAt, + Tail: end, + } + var page, size = 1, 1000 + hasNext := true + var data []*vidmod.VideoModel + for hasNext { + data, hasNext, err = vidmod.GetShortVideoListByUpdateTimeRange(subTimeRange.Head, subTimeRange.Tail, page, size) + if err != nil { + log.Error("Redis sync short video data GetVideoListByUpdateTimeRange err! ", log.E(err)) + return + } + if hasNext && len(data) > 0 { + _ = SyncDataToRedis(data) + page++ + } else { + if err = commit(data, subTimeRange.Tail); err != nil { + log.Error("Redis sync short video data RunSync commit err! ", log.E(err)) + return + } + } + } + + lastSyncAt = end + } +} + +func commit(data []*vidmod.VideoModel, syncTime time.Time) error { + if len(data) <= 0 { + _ = statrecordmod.UpsertOneTrans(nil, statrecordmod.RedisSyncShortVideoJob, syncTime) + return nil + } + if err := SyncDataToRedis(data); err != nil { + return fmt.Errorf("Redis sync commit short video data SyncDataToES failed!, err:%+v\n", err) + } + if err := statrecordmod.UpsertOneTrans(nil, statrecordmod.RedisSyncShortVideoJob, syncTime); err != nil { + return fmt.Errorf("Redis sync commit short video data UpsertOneTrans failed!, err:%+v\n", err) + } + return nil +} diff --git a/skd/job/hevc_transcode.go b/skd/job/hevc_transcode.go new file mode 100644 index 0000000..6bec5f3 --- /dev/null +++ b/skd/job/hevc_transcode.go @@ -0,0 +1,555 @@ +package job + +import ( + "context" + "errors" + "fmt" + "net/url" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/hevcpull" + "91porn-server/common/laosiji" + "91porn-server/common/log" + "91porn-server/models/v/hevctaskmod" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/modulesectionmod" + "91porn-server/models/v/modulevidmod" + "91porn-server/models/v/sysconfmod" + "91porn-server/models/v/vidmod" + "91porn-server/skd/skdg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const ( + h265DefaultMaxRunning = 400 // 配置缺失或无效时,最多允许 400 个视频处于云端转码中 + h265CheckBatchSize = 100 // 每轮最多轮询 100 个已提交的 H265 任务 + h265HomeCandidateSize = 400 // 首页每个模块、每种排序最多保障 400 个视频 + h265PendingTimeout = 240 * time.Hour + h265PullURLTTL = 480 * time.Hour +) + +var ( + h265SubmitRunning int32 + h265CheckRunning int32 + h265ScanRunning int32 + + h265MissingCredentialsLogOnce sync.Once + h265MissingAppURLLogOnce sync.Once + h265InvalidPullSecretLogOnce sync.Once +) + +// h265TranscodingJobEnabled 默认只允许生产环境执行真实云转码。 +// 测试环境必须通过 hevc.enableTranscode 显式开启,避免误提交云端任务。 +func h265TranscodingJobEnabled() bool { + if skdg.Conf == nil { + return false + } + log.Info("h265TranscodingJobEnabled ", log.Any("skdg.Conf.Base.Env", skdg.Conf.Base.Env)) + log.Info("h265TranscodingJobEnabled ", log.Any("skdg.Conf.Hevc.EnableTranscode", skdg.Conf.Hevc.EnableTranscode)) + if skdg.Conf.Hevc.EnableTranscode != nil && !*skdg.Conf.Hevc.EnableTranscode { + return false + } + if skdg.Conf.Base.Env != constant.ProdEnv && + (skdg.Conf.Hevc.EnableTranscode == nil || !*skdg.Conf.Hevc.EnableTranscode) { + return false + } + if !laosiji.Configured() { + h265MissingCredentialsLogOnce.Do(func() { + log.Warn("H265 transcoding jobs disabled: laosiji credentials are incomplete") + }) + return false + } + if _, err := hevcpull.SignURL( + "https://configuration-check.invalid/source.m3u8", + skdg.Conf.Hevc.PullSecret, + time.Now().Add(h265PullURLTTL), + ); err != nil { + h265InvalidPullSecretLogOnce.Do(func() { + log.Warn("H265 transcoding jobs disabled: hevc.pullSecret is missing or invalid", log.E(err)) + }) + return false + } + if buildH265AppTranscodeM3u8URL( + skdg.Conf.Url.AppApiUrl, + "health-check.m3u8", + skdg.Conf.Hevc.PullSecret, + time.Now().Add(h265PullURLTTL), + ) == "" { + h265MissingAppURLLogOnce.Do(func() { + log.Warn("H265 transcoding jobs disabled: url.appApiUrl is missing or invalid", log.Any("skdg.Conf.Url.AppApiUrl", skdg.Conf.Url.AppApiUrl)) + }) + return false + } + return true +} + +// SubmitH265TranscodingQueue 把本地等待队列中的任务异步提交到云转码。 +func SubmitH265TranscodingQueue() { + if !h265TranscodingJobEnabled() { + return + } + if !atomic.CompareAndSwapInt32(&h265SubmitRunning, 0, 1) { + log.Info("SubmitH265TranscodingQueue skip: previous run still running") + return + } + defer atomic.StoreInt32(&h265SubmitRunning, 0) + + ctx := context.Background() + maxRunning := h265MaxRunning() + pendingCount, err := vidmod.CountH265Pending() + if err != nil { + return + } + available := int64(maxRunning) - pendingCount + if available <= 0 { + return + } + + domains, err := laosiji.SystemDomains(ctx) + if err != nil { + log.Warn("SubmitH265TranscodingQueue skip: laosiji domains failed", log.E(err)) + return + } + videos, err := vidmod.GetQueuedH265Videos(available) + if err != nil { + return + } + for _, video := range videos { + if err = submitH265Video(ctx, domains, video, maxRunning); err != nil { + log.Warn("SubmitH265TranscodingQueue submit failed", log.Any("videoId", video.ID), log.E(err)) + if errors.Is(err, laosiji.ErrTranscodeQueueFull) { + return + } + } + } +} + +// CheckH265TranscodingResults 轮询云端任务并回写 h265Url。 +func CheckH265TranscodingResults() { + if !h265TranscodingJobEnabled() { + return + } + if !atomic.CompareAndSwapInt32(&h265CheckRunning, 0, 1) { + log.Info("CheckH265TranscodingResults skip: previous run still running") + return + } + defer atomic.StoreInt32(&h265CheckRunning, 0) + + ctx := context.Background() + domains, err := laosiji.SystemDomains(ctx) + if err != nil { + log.Warn("CheckH265TranscodingResults skip: laosiji domains failed", log.E(err)) + return + } + videos, err := vidmod.GetPendingH265Videos(h265CheckBatchSize) + if err != nil || len(videos) == 0 { + return + } + now := time.Now() + for _, video := range videos { + if video == nil { + continue + } + if h265PendingTimedOut(video, now) { + timeoutMsg := fmt.Sprintf("H265 transcode pending timeout after %.0fh", h265PendingTimeout.Hours()) + marked, markErr := vidmod.MarkH265Failed(video.ID, video.SourceURL, video.H265PendingAt) + if markErr != nil { + log.Warn("CheckH265TranscodingResults mark timeout failed", log.Any("videoId", video.ID), log.E(markErr)) + } else if marked { + upsertH265TaskWithError(video, domains, "", hevctaskmod.H265TaskStatusFailed, timeoutMsg) + } else { + log.Info("CheckH265TranscodingResults ignored stale timeout", log.Any("videoId", video.ID)) + } + continue + } + result, found, queryErr := queryH265Transcode(ctx, domains, video) + if queryErr != nil { + log.Warn("CheckH265TranscodingResults query failed", log.Any("videoId", video.ID), log.E(queryErr)) + // 保留原 processing 审计状态。查询返回和日志已经足够诊断, + // 此处不落库可避免并发成功后被旧查询错误覆盖。 + continue + } + if !found { + const notFoundMsg = "laosiji transcode task not found" + log.Warn("CheckH265TranscodingResults task not found, retry as failed", log.Any("videoId", video.ID)) + marked, markErr := vidmod.MarkH265Failed(video.ID, video.SourceURL, video.H265PendingAt) + if markErr != nil { + log.Warn("CheckH265TranscodingResults mark not-found failed", log.Any("videoId", video.ID), log.E(markErr)) + } else if marked { + // Failed 状态仍会进入等待队列,但会累计失败次数,避免云端永久查无任务时无限重提。 + upsertH265TaskWithError(video, domains, "", hevctaskmod.H265TaskStatusFailed, notFoundMsg) + } else { + log.Info("CheckH265TranscodingResults ignored stale not-found result", log.Any("videoId", video.ID)) + } + continue + } + applyH265Result(video, result) + } +} + +func h265PendingTimedOut(video *vidmod.VideoModel, now time.Time) bool { + if video == nil || + video.H265Status != vidmod.H265StatusPending || + video.H265PendingAt.IsZero() || + video.H265PendingAt.After(now) { + return false + } + return now.Sub(video.H265PendingAt) >= h265PendingTimeout +} + +// ScanHomeH265TranscodingQueue 将首页各启用视频模块的重点 SP 视频加入等待队列。 +func ScanHomeH265TranscodingQueue() { + if !h265TranscodingJobEnabled() { + return + } + if !atomic.CompareAndSwapInt32(&h265ScanRunning, 0, 1) { + log.Info("ScanHomeH265TranscodingQueue skip: previous run still running") + return + } + defer atomic.StoreInt32(&h265ScanRunning, 0) + + moduleIDs, moduleIDHexes := h265HomeModuleIDs() + if len(moduleIDs) == 0 { + return + } + + videoIDs := make([]primitive.ObjectID, 0) + seen := make(map[primitive.ObjectID]struct{}) + for _, moduleIDHex := range moduleIDHexes { + for _, sortType := range []int{1, 2, 3, 7, 9} { + videos, err := vidmod.GetVideoListByCond(homeH265Filter(moduleIDHex, sortType), homeH265Options(sortType)) + if err != nil { + log.Warn("ScanHomeH265TranscodingQueue video query failed", + log.Any("moduleId", moduleIDHex), log.Any("sortType", sortType), log.E(err)) + continue + } + appendUniqueH265VideoIDs(&videoIDs, seen, videos) + } + } + + for _, moduleID := range moduleIDs { + sections, err := modulesectionmod.GetAllBySubModuleID(moduleID) + if err != nil { + log.Warn("ScanHomeH265TranscodingQueue section query failed", log.Any("moduleId", moduleID), log.E(err)) + continue + } + for _, section := range sections { + sectionVideos, err := modulevidmod.SectionVideosBySectionID(section.ID, options.Find(). + SetLimit(h265HomeCandidateSize). + SetSort(bson.D{{Key: "sortCode", Value: -1}, {Key: "videoReviewedAt", Value: -1}})) + if err != nil { + log.Warn("ScanHomeH265TranscodingQueue section videos failed", log.Any("sectionId", section.ID), log.E(err)) + continue + } + for _, sectionVideo := range sectionVideos { + if _, exists := seen[sectionVideo.VideoID]; exists { + continue + } + seen[sectionVideo.VideoID] = struct{}{} + videoIDs = append(videoIDs, sectionVideo.VideoID) + } + } + } + + modified, err := vidmod.QueueH265Transcode(videoIDs) + if err != nil { + log.Warn("ScanHomeH265TranscodingQueue enqueue failed", log.E(err)) + return + } + if modified > 0 { + log.Info("ScanHomeH265TranscodingQueue queued videos", log.Any("count", modified)) + } +} + +func submitH265Video(ctx context.Context, domains laosiji.SystemDomainsResp, video *vidmod.VideoModel, maxRunning int) error { + if video == nil || strings.TrimSpace(video.SourceURL) == "" { + return nil + } + claimedAt, claimed, err := vidmod.ClaimH265Pending(video.ID) + if err != nil || !claimed { + return err + } + video.H265PendingAt = claimedAt + result, err := submitH265Transcode(ctx, domains, video, maxRunning) + if err != nil { + if errors.Is(err, laosiji.ErrTranscodeQueueFull) { + marked, markErr := vidmod.MarkH265Queued(video.ID, video.SourceURL, video.H265PendingAt) + if markErr != nil { + log.Warn("submitH265Video restore queue failed", log.Any("videoId", video.ID), log.E(markErr)) + } else if marked { + upsertH265TaskWithError(video, domains, "", hevctaskmod.H265TaskStatusQueued, err.Error()) + } + return err + } + marked, markErr := vidmod.MarkH265Failed(video.ID, video.SourceURL, video.H265PendingAt) + if markErr != nil { + log.Warn("submitH265Video mark failed", log.Any("videoId", video.ID), log.E(markErr)) + } else if marked { + upsertH265TaskWithError(video, domains, "", hevctaskmod.H265TaskStatusFailed, err.Error()) + } + return err + } + applyH265Result(video, result) + return nil +} + +func submitH265Transcode(ctx context.Context, domains laosiji.SystemDomainsResp, video *vidmod.VideoModel, maxRunning int) (laosiji.TranscodeResult, error) { + fileURL := hevcAppTranscodeM3u8URL(video.SourceURL, video.H265PendingAt) + fileID := h265CloudFileID(video) + if fileURL == "" || fileID == "" { + return laosiji.TranscodeResult{}, fmt.Errorf("empty signed app transcode m3u8 url, videoId:%s", video.ID.Hex()) + } + return laosiji.SubmitH264ToH265Task(ctx, fileID, fileURL, domains, maxRunning) +} + +func queryH265Transcode(ctx context.Context, domains laosiji.SystemDomainsResp, video *vidmod.VideoModel) (laosiji.TranscodeResult, bool, error) { + fileURL := hevcAppTranscodeM3u8URL(video.SourceURL, video.H265PendingAt) + fileID := h265CloudFileID(video) + if fileURL == "" || fileID == "" { + return laosiji.TranscodeResult{}, false, fmt.Errorf("empty signed app transcode m3u8 url, videoId:%s", video.ID.Hex()) + } + return laosiji.QueryH264ToH265Task(ctx, fileID, fileURL, domains) +} + +// hevcAppTranscodeM3u8URL 使用 claim 时间生成稳定、限时且绑定源路径的 +// HMAC 拉流 URL。云端 file_id 独立计算,签名或域名变化不会影响轮询。 +func hevcAppTranscodeM3u8URL(source string, claimedAt time.Time) string { + if skdg.Conf == nil { + return "" + } + if claimedAt.IsZero() { + return "" + } + return buildH265AppTranscodeM3u8URL( + skdg.Conf.Url.AppApiUrl, + source, + skdg.Conf.Hevc.PullSecret, + claimedAt.UTC().Truncate(time.Second).Add(h265PullURLTTL), + ) +} + +func buildH265AppTranscodeM3u8URL(baseURL, source, pullSecret string, expiresAt time.Time) string { + baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/") + normalizedSource, sourceErr := hevcpull.NormalizeSource(source) + if baseURL == "" || sourceErr != nil { + return "" + } + parsed, err := url.Parse(baseURL) + if err != nil || + //parsed.Scheme != "https" || + parsed.Host == "" || + parsed.User != nil || + (parsed.Path != "" && parsed.Path != "/") || + parsed.RawPath != "" || + parsed.RawQuery != "" || + parsed.Fragment != "" { + return "" + } + rawURL := baseURL + "/api/app/vid/transcode/m3u8/" + normalizedSource + signedURL, err := hevcpull.SignURL(rawURL, pullSecret, expiresAt) + if err != nil { + return "" + } + return signedURL +} + +// h265CloudFileID is independent from the temporary signed fetch URL. It +// changes when the source or retry attempt changes, but not when App domains, +// expiry timestamps, or signing keys rotate during an in-flight attempt. +func h265CloudFileID(video *vidmod.VideoModel) string { + if video == nil || video.ID.IsZero() { + return "" + } + source, err := hevcpull.NormalizeSource(video.SourceURL) + if err != nil { + return "" + } + identity := fmt.Sprintf( + "91porn:h265:v1\x00%s\x00%s\x00%d", + video.ID.Hex(), + source, + video.H265FailCount, + ) + return laosiji.TranscodeFileID(identity) +} + +func applyH265Result(video *vidmod.VideoModel, result laosiji.TranscodeResult) { + h265URL := "" + taskStatus := hevctaskmod.H265TaskStatusProcessing + switch { + case result.Done: + h265URL = h265PlayableURL(video, result) + if h265URL == "" { + marked, markErr := vidmod.MarkH265Failed(video.ID, video.SourceURL, video.H265PendingAt) + if markErr != nil { + log.Warn("applyH265Result mark empty result failed", log.Any("videoId", video.ID), log.E(markErr)) + } else if marked { + _ = hevctaskmod.UpsertWithError(video.ID, video.Title, result.FileURL, result.FileID, "", hevctaskmod.H265TaskStatusFailed, "empty H265 URL in completed task") + } + return + } + updated, err := vidmod.MarkPendingH265Success(video.ID, video.SourceURL, video.H265PendingAt, h265URL) + if err != nil { + log.Warn("applyH265Result MarkPendingH265Success failed", log.Any("videoId", video.ID), log.E(err)) + return + } + if !updated { + // 当前视频可能已经被老司机回填 H.265,或 sourceURL 已经变化。 + // 对仍处于 pending 且缺少地址的新源重新排队;已有成功地址时该操作自动跳过。 + _, _ = vidmod.MarkH265Queued(video.ID, video.SourceURL, video.H265PendingAt) + // 不更新 hevc_task:并发轮询的另一个实例可能已经成功落库, + // 此处覆盖会把正确的 success 审计状态错误回退成 failed。 + log.Info("applyH265Result ignored stale result", log.Any("videoId", video.ID)) + return + } + clearH265VideoCache(video.ID) + taskStatus = hevctaskmod.H265TaskStatusSuccess + case result.Failed: + marked, markErr := vidmod.MarkH265Failed(video.ID, video.SourceURL, video.H265PendingAt) + if markErr != nil { + log.Warn("applyH265Result MarkH265Failed failed", log.Any("videoId", video.ID), log.E(markErr)) + return + } + if !marked { + log.Info("applyH265Result ignored stale failure", log.Any("videoId", video.ID)) + return + } + taskStatus = hevctaskmod.H265TaskStatusFailed + default: + // processing 结果不改变视频状态,也没有可用于跨集合原子写入的 + // ModifiedCount。保留现有审计记录,避免旧轮询覆盖并发成功结果。 + return + } + if err := hevctaskmod.UpsertWithError(video.ID, video.Title, result.FileURL, result.FileID, h265URL, taskStatus, result.ErrorMsg); err != nil { + log.Warn("applyH265Result upsert task failed", log.Any("videoId", video.ID), log.E(err)) + } +} + +// h265PlayableURL 区分云端“无需转码”和真正生成了新 H265 文件的结果。 +// 无需转码时源地址本身已经可直接使用,不能把我方 App 拉流 URL 错存成老司机资源路径。 +func h265PlayableURL(video *vidmod.VideoModel, result laosiji.TranscodeResult) string { + if result.NoNeed { + if video == nil { + return "" + } + return strings.TrimSpace(video.SourceURL) + } + return laosiji.MovieM3u8SourcePath(result.HevcURL) +} + +func clearH265VideoCache(videoID primitive.ObjectID) { + if skdg.Redis == nil || videoID.IsZero() { + return + } + key := fmt.Sprintf(redisconst.VideoInfoKey(), videoID.Hex()) + if _, err := skdg.Redis.Del(key); err != nil { + log.Warn("clearH265VideoCache failed", log.Any("videoId", videoID), log.E(err)) + } +} + +func upsertH265TaskWithError(video *vidmod.VideoModel, domains laosiji.SystemDomainsResp, h265URL string, status hevctaskmod.H265TaskStatus, errorMsg string) { + if video == nil { + return + } + submitURL, fileID := h265TaskSubmitInfo(video, domains) + if err := hevctaskmod.UpsertWithError(video.ID, video.Title, submitURL, fileID, h265URL, status, errorMsg); err != nil { + log.Warn("upsertH265TaskWithError failed", log.Any("videoId", video.ID), log.E(err)) + } +} + +func h265TaskSubmitInfo(video *vidmod.VideoModel, _ laosiji.SystemDomainsResp) (string, string) { + if video == nil { + return "", "" + } + submitURL := strings.TrimSpace(hevcAppTranscodeM3u8URL(video.SourceURL, video.H265PendingAt)) + if submitURL == "" { + submitURL = strings.TrimSpace(video.SourceURL) + } + fileID := h265CloudFileID(video) + // 按运维要求持久化“可直接访问”的签名 URL,便于从 hevc_task 复制出来核验。 + // 代价:库中会保存有效期内可用的签名拉流地址(含 HMAC),务必收紧 DB 访问权限。 + return submitURL, fileID +} + +func h265MaxRunning() int { + cfg, err := sysconfmod.GetByVCode(sysconfmod.VCodeH265MaxRunning) + if err != nil { + return h265DefaultMaxRunning + } + maxRunning, err := strconv.Atoi(cfg.Value) + if err != nil || maxRunning <= 0 { + return h265DefaultMaxRunning + } + return maxRunning +} + +func h265HomeModuleIDs() ([]primitive.ObjectID, []string) { + modules, err := moduleconfmod.GetModuleConfByType(moduleconfmod.HomePage) + if err != nil { + log.Warn("h265HomeModuleIDs query homepage modules failed", log.E(err)) + return nil, nil + } + moduleIDs := make([]primitive.ObjectID, 0, len(modules)) + moduleIDHexes := make([]string, 0, len(modules)) + for _, module := range modules { + if module.Status != 1 || module.ID.IsZero() { + continue + } + moduleIDs = append(moduleIDs, module.ID) + moduleIDHexes = append(moduleIDHexes, module.ID.Hex()) + } + return moduleIDs, moduleIDHexes +} + +func appendUniqueH265VideoIDs(ids *[]primitive.ObjectID, seen map[primitive.ObjectID]struct{}, videos []*vidmod.VideoModel) { + for _, video := range videos { + if video == nil { + continue + } + if _, exists := seen[video.ID]; exists { + continue + } + seen[video.ID] = struct{}{} + *ids = append(*ids, video.ID) + } +} + +func homeH265Filter(moduleID string, sortType int) primitive.M { + return bson.M{ + "status": vidmod.CheckPass, + "newsType": vidmod.SP, + "mId": moduleID, + "sourceURL": bson.M{"$exists": true, "$ne": ""}, + "deleteAt": bson.M{"$exists": false}, + } +} + +func homeH265Options(sortType int) *options.FindOptions { + var sort bson.D + switch sortType { + case 2: + sort = bson.D{ + {Key: "liaoBaTopSort", Value: -1}, + {Key: "likeCount", Value: -1}, + {Key: "reviewAt", Value: -1}, + } + case 3: + sort = bson.D{{Key: "playCount", Value: -1}, {Key: "reviewAt", Value: -1}} + case 7: + sort = bson.D{{Key: "collectCount", Value: -1}, {Key: "reviewAt", Value: -1}} + case 9: + sort = bson.D{{Key: "hot", Value: -1}, {Key: "reviewAt", Value: -1}} + default: + sort = bson.D{{Key: "reviewAt", Value: -1}} + } + return options.Find().SetLimit(h265HomeCandidateSize).SetSort(sort) +} diff --git a/skd/job/hevc_transcode_test.go b/skd/job/hevc_transcode_test.go new file mode 100644 index 0000000..6ef9459 --- /dev/null +++ b/skd/job/hevc_transcode_test.go @@ -0,0 +1,330 @@ +package job + +import ( + "net/url" + "reflect" + "strings" + "testing" + "time" + + "91porn-server/common/hevcpull" + "91porn-server/common/laosiji" + "91porn-server/models/v/vidmod" + "91porn-server/skd/skdg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestBuildH265AppTranscodeM3u8URL(t *testing.T) { + const pullSecret = "test-pull-secret-strong-32-bytes!!" + now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC) + expiresAt := now.Add(h265PullURLTTL) + tests := []struct { + name string + base string + source string + wantPath string + }{ + { + name: "normalizes slashes", + base: " https://app.example.com/ ", + source: " /video/demo/index.m3u8 ", + wantPath: "/api/app/vid/transcode/m3u8/video/demo/index.m3u8", + }, + {name: "empty base", source: "video.m3u8"}, + {name: "empty source", base: "https://app.example.com"}, + {name: "base without scheme", base: "app.example.com", source: "video.m3u8"}, + {name: "plain http is forbidden", base: "http://app.example.com", source: "video.m3u8"}, + {name: "unsupported scheme", base: "ftp://app.example.com", source: "video.m3u8"}, + {name: "base with path", base: "https://app.example.com/prefix", source: "video.m3u8"}, + {name: "base with query", base: "https://app.example.com?token=x", source: "video.m3u8"}, + {name: "source traversal", base: "https://app.example.com", source: "video/../index.m3u8"}, + {name: "absolute source", base: "https://app.example.com", source: "https://cdn.example/index.m3u8"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := buildH265AppTranscodeM3u8URL(tt.base, tt.source, pullSecret, expiresAt) + if tt.wantPath == "" { + if got != "" { + t.Fatalf("buildH265AppTranscodeM3u8URL() = %q, want empty", got) + } + return + } + parsed, err := url.Parse(got) + if err != nil { + t.Fatalf("parse signed URL: %v", err) + } + if parsed.Path != tt.wantPath { + t.Fatalf("signed URL path = %q, want %q", parsed.Path, tt.wantPath) + } + if err = hevcpull.VerifyURL(parsed, pullSecret, now); err != nil { + t.Fatalf("signed URL did not verify: %v", err) + } + }) + } +} + +func TestHevcAppTranscodeM3u8URLUsesStableClaimTime(t *testing.T) { + oldConf := skdg.Conf + t.Cleanup(func() { + skdg.Conf = oldConf + }) + claimedAt := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC) + + skdg.Conf = nil + if got := hevcAppTranscodeM3u8URL("video.m3u8", claimedAt); got != "" { + t.Fatalf("nil config returned %q", got) + } + + skdg.Conf = &skdg.GlobalConfig{} + skdg.Conf.Url.AppApiUrl = "https://app.example.com" + skdg.Conf.Hevc.PullSecret = "test-pull-secret-strong-32-bytes!!" + first := hevcAppTranscodeM3u8URL("video.m3u8", claimedAt) + second := hevcAppTranscodeM3u8URL("video.m3u8", claimedAt) + if first == "" || first != second { + t.Fatalf("signed URL must be stable for one claim: first=%q second=%q", first, second) + } + parsed, err := url.Parse(first) + if err != nil { + t.Fatalf("parse signed URL: %v", err) + } + if !strings.HasPrefix(parsed.Path, "/api/app/vid/transcode/m3u8/") { + t.Fatalf("unexpected signed URL path: %s", parsed.Path) + } + if err = hevcpull.VerifyURL(parsed, skdg.Conf.Hevc.PullSecret, claimedAt); err != nil { + t.Fatalf("signed URL did not verify: %v", err) + } +} + +func TestH265CloudFileIDIsIndependentFromSignedPullURL(t *testing.T) { + video := &vidmod.VideoModel{ + ID: primitive.NewObjectID(), + SourceURL: " /sp/movie/index.m3u8 ", + H265FailCount: 1, + H265PendingAt: time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC), + } + first := h265CloudFileID(video) + if first == "" { + t.Fatal("empty cloud file ID") + } + + video.H265PendingAt = video.H265PendingAt.Add(time.Hour) + video.SourceURL = "/sp/movie/index.m3u8" + if got := h265CloudFileID(video); got != first { + t.Fatalf("equivalent source or claim time changed cloud file ID: %q != %q", got, first) + } + + video.SourceURL = "/sp/other/index.m3u8" + if got := h265CloudFileID(video); got == first { + t.Fatal("different source reused cloud file ID") + } + video.SourceURL = "/sp/movie/index.m3u8" + video.H265FailCount++ + if got := h265CloudFileID(video); got == first { + t.Fatal("new retry attempt reused cloud file ID") + } +} + +func TestH265TranscodingJobEnabledRequiresEnvironmentSwitchAndConfig(t *testing.T) { + oldConf := skdg.Conf + oldLSJConfig := laosiji.Config{ + AppID: laosiji.Appid, + APIKey: laosiji.APIKey, + APIUrl: laosiji.APIUrl, + ImageYuan: laosiji.IMAGEYUAN, + NoticeURL: laosiji.NoticeURL, + } + t.Cleanup(func() { + skdg.Conf = oldConf + laosiji.InitConfig(oldLSJConfig) + }) + + skdg.Conf = &skdg.GlobalConfig{} + skdg.Conf.Base.Env = "prod" + skdg.Conf.Url.AppApiUrl = "https://app.example.com" + laosiji.InitConfig(laosiji.Config{}) + if h265TranscodingJobEnabled() { + t.Fatal("job enabled without laosiji credentials") + } + + laosiji.InitConfig(laosiji.Config{ + AppID: "test-app", + APIKey: "test-key", + APIUrl: "https://laosiji.example.com", + }) + skdg.Conf.Base.Env = "test" + if h265TranscodingJobEnabled() { + t.Fatal("job enabled in test without explicit switch") + } + + enabled := true + skdg.Conf.Hevc.EnableTranscode = &enabled + if h265TranscodingJobEnabled() { + t.Fatal("job enabled in test without hevc.pullSecret") + } + + skdg.Conf.Hevc.PullSecret = "test-pull-secret-strong-32-bytes!!" + skdg.Conf.Url.AppApiUrl = "" + if h265TranscodingJobEnabled() { + t.Fatal("job enabled without appApiUrl") + } + + skdg.Conf.Url.AppApiUrl = "https://app.example.com" + if !h265TranscodingJobEnabled() { + t.Fatal("job disabled in test with explicit switch and complete configuration") + } + + skdg.Conf.Base.Env = "prod" + skdg.Conf.Hevc.EnableTranscode = nil + if !h265TranscodingJobEnabled() { + t.Fatal("job disabled with complete production configuration") + } + + disabled := false + skdg.Conf.Hevc.EnableTranscode = &disabled + if h265TranscodingJobEnabled() { + t.Fatal("job enabled after explicit production kill switch") + } +} + +func TestH265PlayableURL(t *testing.T) { + video := &vidmod.VideoModel{SourceURL: "sp/source/index.m3u8"} + + if got := h265PlayableURL(video, laosiji.TranscodeResult{ + Done: true, + NoNeed: true, + HevcURL: "https://app.example.com/api/app/vid/h5/light/m3u8/sp/source/index.m3u8", + }); got != video.SourceURL { + t.Fatalf("no-need result returned %q, want original source %q", got, video.SourceURL) + } + + if got := h265PlayableURL(video, laosiji.TranscodeResult{ + Done: true, + HevcURL: "https://cdn.example.com/m3m/transcoded/index.m3u8", + }); got != "laosiji/m3m/transcoded/index.m3u8" { + t.Fatalf("transcoded result returned %q", got) + } + + if got := h265PlayableURL(nil, laosiji.TranscodeResult{Done: true, NoNeed: true}); got != "" { + t.Fatalf("nil video no-need result returned %q", got) + } +} + +func TestH265PendingTimedOut(t *testing.T) { + now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC) + tests := []struct { + name string + video *vidmod.VideoModel + want bool + }{ + {name: "nil video"}, + { + name: "not pending", + video: &vidmod.VideoModel{ + H265Status: vidmod.H265StatusQueued, + H265PendingAt: now.Add(-25 * time.Hour), + }, + }, + { + name: "missing claim time", + video: &vidmod.VideoModel{ + H265Status: vidmod.H265StatusPending, + }, + }, + { + name: "still within timeout", + video: &vidmod.VideoModel{ + H265Status: vidmod.H265StatusPending, + H265PendingAt: now.Add(-h265PendingTimeout + time.Second), + }, + }, + { + name: "at timeout", + video: &vidmod.VideoModel{ + H265Status: vidmod.H265StatusPending, + H265PendingAt: now.Add(-h265PendingTimeout), + }, + want: true, + }, + { + name: "past timeout", + video: &vidmod.VideoModel{ + H265Status: vidmod.H265StatusPending, + H265PendingAt: now.Add(-h265PendingTimeout - time.Second), + }, + want: true, + }, + { + name: "future timestamp", + video: &vidmod.VideoModel{ + H265Status: vidmod.H265StatusPending, + H265PendingAt: now.Add(time.Second), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := h265PendingTimedOut(tt.video, now); got != tt.want { + t.Fatalf("h265PendingTimedOut() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestHomeH265QueryMatchesAppHomepageSorts(t *testing.T) { + for _, sortType := range []int{1, 2, 3, 7, 9} { + filter := homeH265Filter("module-id", sortType) + if _, exists := filter["reviewAt"]; exists { + t.Fatalf("sort %d unexpectedly limits reviewAt: %#v", sortType, filter["reviewAt"]) + } + } + + tests := []struct { + sortType int + want bson.D + }{ + { + sortType: 1, + want: bson.D{{Key: "reviewAt", Value: -1}}, + }, + { + sortType: 2, + want: bson.D{ + {Key: "liaoBaTopSort", Value: -1}, + {Key: "likeCount", Value: -1}, + {Key: "reviewAt", Value: -1}, + }, + }, + { + sortType: 3, + want: bson.D{ + {Key: "playCount", Value: -1}, + {Key: "reviewAt", Value: -1}, + }, + }, + { + sortType: 7, + want: bson.D{ + {Key: "collectCount", Value: -1}, + {Key: "reviewAt", Value: -1}, + }, + }, + { + sortType: 9, + want: bson.D{ + {Key: "hot", Value: -1}, + {Key: "reviewAt", Value: -1}, + }, + }, + } + + for _, tt := range tests { + got := homeH265Options(tt.sortType).Sort + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("sort %d = %#v, want %#v", tt.sortType, got, tt.want) + } + } +} diff --git a/skd/job/hotvalue/video.go b/skd/job/hotvalue/video.go new file mode 100644 index 0000000..53477c9 --- /dev/null +++ b/skd/job/hotvalue/video.go @@ -0,0 +1,125 @@ +package hotvalue + +import ( + "91porn-server/common/log" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/vidmod" + "github.com/golang-module/carbon/v2" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "math" + "time" +) + +type updateFn func(models []mongo.WriteModel) error + +var ( + actions = map[string]updateFn{ + cmtmod.OTypeVideo: vidmod.Bulk, + cmtmod.OTypeCartoon: mediamod.Bulk, + } +) + +// ResetHot 重置热度值 +func ResetHot() { + start := carbon.NewCarbon().SubDays(30).StdTime() + end := time.Now() + writes := make([]mongo.WriteModel, 0) + + for objType, fn := range actions { + skip := 0 + limit := 100 + + for { + // 获取30天内有评论的objID + objIds, hasNext, err := cmtmod.GetObjIdsBetweenTime(objType, start, end, skip, limit) + if err != nil { + log.Error("Job ResetHot cmtmod.GetObjIdsBetweenTime fail", log.E(err)) + return + } + + for _, objId := range objIds { + hotData := resetObjectHot(objId, objType, start, end) + if len(hotData) == 0 { + continue + } + update := mongo.NewUpdateOneModel().SetFilter(bson.M{"_id": objId}).SetUpdate(hotData).SetUpsert(true) + writes = append(writes, update) + } + + if len(writes) > 0 { + err = fn(writes) + if err != nil { + log.Error("Job ResetHot fail", log.E(err), + log.Any("objType", objType), + log.Any("skip", skip)) + } + } + writes = writes[0:0] + skip = skip + limit + + if !hasNext { + break + } + } + } +} + +// 热度=Z历史所有评论数 × 30% × e^(−{评论天数}×0.1) +//+Z近7天评论数 × 70% + +func resetObjectHot(objId primitive.ObjectID, objType string, start, end time.Time) bson.M { + // 暂时只支持视频和ACG的评论 + if objType != cmtmod.OTypeVideo && objType != cmtmod.OTypeCartoon { + return nil + } + // 获取该对性一段时间内,按天分组,每天的评论总数 + list, err := cmtmod.GetCountGroupByDay(objId, start, end) + if err != nil { + log.Error("Job ResetHot resetObjectHot cmtmod.GetCountGroupByDay fail", log.E(err)) + return nil + } + var historyHot float64 + var sevenDayCount float64 + // 计算出该对象的热度值 + for _, v := range list { + // 获取跟今天相差的天数 + day := float64(carbon.Now().DiffAbsInDays(carbon.ParseByFormat(v.Time, "Y-m-d"))) + // 计算历史评论热度 + hot := calculateCommentScore(v.Count, day) + historyHot = historyHot + hot + // 累加近七天的总数 + if day <= 7 { + sevenDayCount = sevenDayCount + float64(v.Count) + } + } + currentHot := historyHot + sevenDayCount*0.7 + log.Info("Job ResetHot resetObjectHot result", log.Any("hot", currentHot)) + + data := bson.M{"hot": currentHot} + + if objType == cmtmod.OTypeVideo { + data["updatedAt"] = time.Now() + } else { + data["updateTime"] = time.Now() + } + + return bson.M{"$set": data} +} + +func calculateCommentScore(totalComments int64, daysSinceComment float64) float64 { + // Ensure we don't have negative days + if daysSinceComment < 0 { + daysSinceComment = 0 + } + + // Calculate the decay factor: e^(-days * 0.1) + decayFactor := math.Exp(-daysSinceComment * 0.1) + + // Calculate the final score: totalComments * 0.3 * decayFactor + score := float64(totalComments) * 0.3 * decayFactor + + return score +} diff --git a/skd/job/im_ad_notify.go b/skd/job/im_ad_notify.go new file mode 100644 index 0000000..9227951 --- /dev/null +++ b/skd/job/im_ad_notify.go @@ -0,0 +1,150 @@ +package job + +import ( + "fmt" + "strconv" + "time" + + "91porn-server/common/enum/imad" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/v/sysconfmod" + "91porn-server/skd/skdg" + "91porn-server/services/srv_im" +) + +const ( + imAdNotifyLockKey = "skd:im_ad_notify:online:lock" + imAdNotifyLastRunKey = "skd:im_ad_notify:online:last_run" + imAdNotifyLockTTL = 10 * time.Minute +) + +type imAdNotifyConfig struct { + Enable bool + IntervalMinutes int64 + Positions []string + MaxUsers int +} + +// IMAdNotifyOnline 按 sysconf.imConfig 定时向当前 IM 在线用户透传广告刷新通知。 +func IMAdNotifyOnline() { + if err := imAdNotifyOnline(); err != nil { + log.Error("IMAdNotifyOnline failed", log.E(err)) + } +} + +func imAdNotifyOnline() error { + conf := loadIMAdNotifyConfig() + now := time.Now() + lastRunAt, err := loadIMAdNotifyLastRunAt() + if err != nil { + return err + } + if !shouldRunIMAdNotify(conf, lastRunAt, now) { + return nil + } + + acquired, err := skdg.Redis.Setnx_NewOK(imAdNotifyLockKey, 1, imAdNotifyLockTTL) + if err != nil { + return err + } + if !acquired { + log.Info("IMAdNotifyOnline skipped: lock held") + return nil + } + defer func() { + if _, err := skdg.Redis.Del(imAdNotifyLockKey); err != nil { + log.Warn("IMAdNotifyOnline release lock failed", log.E(err)) + } + }() + + lastRunAt, err = loadIMAdNotifyLastRunAt() + if err != nil { + return err + } + if !shouldRunIMAdNotify(conf, lastRunAt, now) { + return nil + } + + resp, code, msg := srv_im.SendAdNotify(srv_im.AdNotifyReq{ + Target: "online", + Positions: conf.Positions, + MaxUsers: conf.MaxUsers, + }) + if code != stderr.Success { + return fmt.Errorf("im ad notify failed: code=%d msg=%s", code, msg) + } + log.Info("IMAdNotifyOnline done", log.Any("resp", resp)) + return saveIMAdNotifyLastRunAt(now) +} + +func loadIMAdNotifyConfig() imAdNotifyConfig { + conf := imAdNotifyConfig{IntervalMinutes: 60, MaxUsers: 1000} + all, err := sysconfdata.GetAllFromCache() + if err != nil { + log.Warn("load im sysconf failed", log.E(err)) + return conf + } + conf.Enable = all.GetBool(sysconfmod.VCodeIMAdEnable) + if v := all.GetInt(sysconfmod.VCodeIMAdIntervalMinutes); v > 0 { + conf.IntervalMinutes = v + } + if v := all.GetInt(sysconfmod.VCodeIMAdMaxUsers); v > 0 { + conf.MaxUsers = int(v) + } + // 每个广告位独立开关,开了就加进去 + conf.Positions = collectEnabledIMAdPositions(all) + return conf +} + +// collectEnabledIMAdPositions 把启用了的广告位 code 收集成列表 +func collectEnabledIMAdPositions(all sysconfmod.ConfMap) []string { + positions := make([]string, 0, 4) + if all.GetBool(sysconfmod.VCodeIMAdPosChatNotificationBar) { + positions = append(positions, imad.ChatNotificationBar) + } + if all.GetBool(sysconfmod.VCodeIMAdPosChatFloatingGifBall) { + positions = append(positions, imad.ChatFloatingGifBall) + } + if all.GetBool(sysconfmod.VCodeIMAdPosListFeedNative) { + positions = append(positions, imad.ListFeedNative) + } + if all.GetBool(sysconfmod.VCodeIMAdPosListBanner) { + positions = append(positions, imad.ListBanner) + } + return positions +} + +func shouldRunIMAdNotify(conf imAdNotifyConfig, lastRunAt time.Time, now time.Time) bool { + if !conf.Enable || len(conf.Positions) == 0 { + return false + } + if lastRunAt.IsZero() { + return true + } + return !now.Before(lastRunAt.Add(normalizeIMAdNotifyInterval(conf.IntervalMinutes))) +} + +func normalizeIMAdNotifyInterval(intervalMinutes int64) time.Duration { + if intervalMinutes <= 0 { + intervalMinutes = 60 + } + return time.Duration(intervalMinutes) * time.Minute +} + +func loadIMAdNotifyLastRunAt() (time.Time, error) { + val, err := skdg.Redis.Get(imAdNotifyLastRunKey) + if err != nil || val == nil || *val == "" { + return time.Time{}, err + } + unixSec, err := strconv.ParseInt(*val, 10, 64) + if err != nil || unixSec <= 0 { + return time.Time{}, nil + } + return time.Unix(unixSec, 0), nil +} + +func saveIMAdNotifyLastRunAt(now time.Time) error { + return skdg.Redis.Set(imAdNotifyLastRunKey, strconv.FormatInt(now.Unix(), 10), 0) +} diff --git a/skd/job/job.go b/skd/job/job.go new file mode 100644 index 0000000..9f9580a --- /dev/null +++ b/skd/job/job.go @@ -0,0 +1,162 @@ +package job + +import ( + "91porn-server/common" + "91porn-server/common/aiService" + "91porn-server/common/log" + mediaService "91porn-server/common/media" + "91porn-server/common/mediaCenter" + "91porn-server/models/commod" + "91porn-server/skd/job/checkin" + "91porn-server/skd/job/dailyAdverCalc" + "91porn-server/skd/job/dataclean" + "91porn-server/skd/job/hotvalue" + "91porn-server/skd/job/shortrecommend" + "91porn-server/skd/job/user" + "91porn-server/skd/service" + "91porn-server/skd/service/contentreviewser" + "91porn-server/skd/service/export_task" + "91porn-server/skd/skdg" + "context" + "time" +) + +// start 定时任务 开启 +func start() { + _, _ = statCron.AddFunc("0 0 0 * * ?", func() { //每天0点执行一次 + // 重置预售卡预付权益 + user.ResetLimit() + }) + if skdg.Conf.ShortRecommendEnabled() { + // 定时检查队列完整性;当天健康时只做一次Redis脚本,异常时自动重建。 + if _, err := statCron.AddFunc(skdg.Conf.ShortRecommendCron(), shortrecommend.Run); err != nil { + log.Error("register short recommend cron failed", + log.E(err), log.Any("cron", skdg.Conf.ShortRecommendCron())) + } + // 启动时补建当日队列;分布式锁保证多实例只执行一次。 + common.Go(shortrecommend.Run) + } + _, _ = statCron.AddFunc("0 0/5 * * * ?", func() { //每5分钟执行一次统计任务 + runStat() + }) + _, _ = statCron.AddFunc("@every 5m", func() { //每5分钟执行一次数据同步任务 + runSync() + }) + _, _ = statCron.AddFunc("0 0/5 * * * ?", func() { //每5分钟执行一次sql折叠任务 + runSyncSqlcollaspe() + }) + // _, _ = statCron.AddFunc("0 0/5 * * * ?", func() { //每5分钟刷新一下排行榜 + // runRankStat() + // }) + // _, _ = statCron.AddFunc("0 0/1 * * * ?", func() { //每1分钟执行一次统计任务 + // runPromoteStat() + // }) + // _, _ = statCron.AddFunc("0 0 0/1 * * ?", func() { //每1个小时增加内部人员推广人数 + // runInnerFakePromote() + // }) + // _, _ = statCron.AddFunc("0 0/2 * * * ?", func() { //每2分钟生成备用推广码 + // runMakePromotionCode() + // }) + // _, _ = statCron.AddFunc("0 0 0,1,3,6,7,9,10,12,15,20,21,23 * * ?", func() { //每天11,15,18,23点执行一次消息任务 + // service.GoldEggLuckyRunSync() + // }) + _, _ = statCron.AddFunc("0 0 4 * * ?", func() { //每天凌晨4点执行一次 + dataclean.RunSync() + }) + + _, _ = statCron.AddFunc("0/10 * * * * ?", func() { //每10秒执行一次 + service.VidTimeOnline() + }) + + common.Go(func() { + hotvalue.ResetHot() + }) + _, _ = statCron.AddFunc("0 0 6 * * ?", func() { //每天凌晨6点执行一次 + hotvalue.ResetHot() + }) + + // 每10秒检测需要导出的任务列表 + _, _ = statCron.AddFunc("0/10 * * * * ?", func() { //每10秒执行一次 + export_task.ExecExportTask() + }) + // 每3秒检测有无待执行的内容审查任务(同一时刻仅 1 个任务运行) + _, _ = statCron.AddFunc("0/3 * * * * ?", func() { + contentreviewser.TryRunPendingTask() + }) + + _, _ = statCron.AddFunc("0 * * * * ?", func() { // 每分钟检查是否需要发送 IM 广告通知 + IMAdNotifyOnline() + }) + + // 每 5 秒检查 web 端是否发起了 IM 用户同步请求 + _, _ = statCron.AddFunc("0/5 * * * * ?", func() { + RunPendingSyncIMUsers() + }) + // 同步acg订阅的动漫 + _, _ = statCron.AddFunc("0 0/30 * * * ?", func() { //每30分钟执行一次 + ms := mediaService.NewMediaSync(skdg.Redis, mediaCenter.NewMediaCenterService(mediaCenter.Config{ + Url: skdg.Conf.MediaResourcesCfg.UploadMediaUrl, + ProId: skdg.Conf.MediaResourcesCfg.ProId, + Secret: skdg.Conf.MediaResourcesCfg.AppSecret, + })) + ms.SyncAll("系统") + }) + common.Go(func() { + err := aiService.NewAiService( + aiService.AppId(int(commod.KFK_APPID)), + aiService.Url(skdg.Conf.Url.AiServer), + aiService.Redis(skdg.Redis), + ).Sync() + if err != nil { + log.Error("Sync AI Template fail") + } + }) + // 定期同步ai服务里的模版 + _, _ = statCron.AddFunc("0 0/10 * * * ?", func() { //每10分钟执行一次 + err := aiService.NewAiService( + aiService.AppId(int(commod.KFK_APPID)), + aiService.Url(skdg.Conf.Url.AiServer), + aiService.Redis(skdg.Redis), + ).Sync() + if err != nil { + log.Error("Sync AI Template fail") + } + }) + _, _ = statCron.AddFunc("0/2 * * * * ?", func() { //每2秒检查签到奖励视频切片状态 + checkin.VideoCheckRS() + }) + _, _ = statCron.AddFunc("0 0/1 * * * ?", func() { // 每分钟提交 H265 等待队列 + SubmitH265TranscodingQueue() + }) + _, _ = statCron.AddFunc("0 0/1 * * * ?", func() { // 每分钟轮询 H265 转码结果 + CheckH265TranscodingResults() + }) + _, _ = statCron.AddFunc("0 0 0/1 * * ?", func() { // 每小时保障首页重点视频的 H265 资源 + ScanHomeH265TranscodingQueue() + }) + common.Go(func() { + ScanHomeH265TranscodingQueue() + SubmitH265TranscodingQueue() + CheckH265TranscodingResults() + }) + spec := "0 0/30 * * * ?" + if skdg.Conf.Base.Env == "test" { + spec = "0 0/1 * * * ?" // 测试环境3分钟刷新一次 + } + + _, _ = statCron.AddFunc(spec, func() { //每30分钟执行一次 + now := time.Now() + if now.Hour() == 0 || now.Hour() == 1 || now.Hour() == 2 { + yesterday := now.AddDate(0, 0, -1) + if err := dailyAdverCalc.CalcRetention(context.Background(), yesterday); err != nil { + log.Error("CalcRetention failed", log.Any("error", err)) + } + } + // 当天的数据,每小时计算一次,确保及时更新看板 + if err := dailyAdverCalc.CalcRetention(context.Background(), now); err != nil { + log.Error("CalcRetention failed", log.Any("error", err)) + } + + }) + statCron.Start() +} diff --git a/skd/job/schedule.go b/skd/job/schedule.go new file mode 100644 index 0000000..d20bceb --- /dev/null +++ b/skd/job/schedule.go @@ -0,0 +1,34 @@ +package job + +import ( + "context" + "time" + + "91porn-server/common/log" + + "github.com/robfig/cron/v3" +) + +var statCron *cron.Cron + +func init() { + statCron = cron.New(cron.WithSeconds()) //从秒开始的定时任务 +} + +// 开启定时任务 +func Start() { + log.Info("start job...", log.Any("beginAt", time.Now())) + start() +} + +func Stop() { + ctx := statCron.Stop() + <-ctx.Done() + err := ctx.Err() + //statCron.Stop()后,若job处于静默可能立即退出,再次执行<-ctx.Done()产生Canceled错误,此处忽略该错误 + if err != nil && err != context.Canceled { + log.Error("Skd Job Stop error", log.E(err)) + return + } + log.Info("end job", log.Any("endAt", time.Now())) +} diff --git a/skd/job/shortrecommend/job.go b/skd/job/shortrecommend/job.go new file mode 100644 index 0000000..bdf26cb --- /dev/null +++ b/skd/job/shortrecommend/job.go @@ -0,0 +1,231 @@ +package shortrecommend + +import ( + "context" + "fmt" + "time" + + "91porn-server/common/log" + redisutil "91porn-server/common/redis" + recommendqueue "91porn-server/common/shortrecommend" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/vidmod" + "91porn-server/skd/skdg" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + buildLockTTL = 10 * time.Minute + buildLockRenewInterval = 3 * time.Minute + buildTimeout = 30 * time.Minute + healthCheckTimeout = 5 * time.Second +) + +const renewBuildLockScriptSource = ` +if redis.call('GET', KEYS[1]) ~= ARGV[1] then + return 0 +end +return redis.call('EXPIRE', KEYS[1], ARGV[2]) +` + +const releaseBuildLockScriptSource = ` +if redis.call('GET', KEYS[1]) ~= ARGV[1] then + return 0 +end +return redis.call('DEL', KEYS[1]) +` + +var ( + renewBuildLockScript = redisutil.NewScript(renewBuildLockScriptSource) + releaseBuildLockScript = redisutil.NewScript(releaseBuildLockScriptSource) +) + +func evalLockScript( + ctx context.Context, + script *redisutil.Script, + key, token string, + args ...interface{}, +) (bool, error) { + values := append([]interface{}{token}, args...) + raw, err := skdg.Redis.RunScriptContext(ctx, script, []string{key}, values...) + if err != nil { + return false, err + } + return fmt.Sprint(raw) == "1", nil +} + +func renewBuildLock(ctx context.Context, key, token string) (bool, error) { + return evalLockScript( + ctx, + renewBuildLockScript, + key, + token, + int64(buildLockTTL/time.Second), + ) +} + +func releaseBuildLock(ctx context.Context, key, token string) error { + _, err := evalLockScript(ctx, releaseBuildLockScript, key, token) + return err +} + +func keepBuildLock(ctx context.Context, cancel context.CancelFunc, key, token, version string) { + ticker := time.NewTicker(buildLockRenewInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + renewed, err := renewBuildLock(ctx, key, token) + if err != nil || !renewed { + log.Error("short recommend renew build lock failed", + log.E(err), log.Any("version", version)) + cancel() + return + } + } + } +} + +func buildQueueVersion(now time.Time, buildToken string) string { + return recommendqueue.VersionWithRevision(now, buildToken) +} + +// Run 刷新真实互动分并发布当日全局短视频队列。 +func Run() { + now := time.Now() + dateVersion := recommendqueue.VersionAt(now) + healthCtx, healthCancel := context.WithTimeout(context.Background(), healthCheckTimeout) + health, err := recommendqueue.HealthContext(healthCtx, skdg.Redis, dateVersion) + healthCancel() + if err != nil { + log.Error("short recommend queue health check failed", + log.E(err), log.Any("dateVersion", dateVersion)) + return + } + if health.Healthy { + return + } + log.Warn("short recommend queue unhealthy, start rebuild", + log.Any("dateVersion", dateVersion), + log.Any("currentVersion", health.Version), + log.Any("healthStatus", health.Status), + log.Any("queueLength", health.QueueLength), + log.Any("metadataLength", health.MetadataLength), + log.Any("queueTTLSeconds", int64(health.QueueTTL/time.Second)), + log.Any("metadataTTLSeconds", int64(health.MetadataTTL/time.Second))) + lockKey := recommendqueue.LockKey(dateVersion) + lockToken := primitive.NewObjectID().Hex() + lockCtx, lockCancel := context.WithTimeout(context.Background(), healthCheckTimeout) + locked, err := skdg.Redis.SetNXContext(lockCtx, lockKey, lockToken, buildLockTTL) + lockCancel() + if err != nil { + log.Error("short recommend acquire build lock failed", + log.E(err), log.Any("dateVersion", dateVersion)) + return + } + if !locked { + return + } + buildCtx, cancel := context.WithTimeout(context.Background(), buildTimeout) + lockKeeperDone := make(chan struct{}) + go func() { + defer close(lockKeeperDone) + keepBuildLock(buildCtx, cancel, lockKey, lockToken, dateVersion) + }() + defer func() { + cancel() + <-lockKeeperDone + releaseCtx, releaseCancel := context.WithTimeout(context.Background(), healthCheckTimeout) + defer releaseCancel() + if err := releaseBuildLock(releaseCtx, lockKey, lockToken); err != nil { + log.Error("short recommend release build lock failed", + log.E(err), log.Any("dateVersion", dateVersion)) + } + }() + + // 首次健康检查与加锁之间可能已有其他实例完成发布;持锁后复检避免重复全量扫描。 + recheckCtx, recheckCancel := context.WithTimeout(buildCtx, healthCheckTimeout) + rechecked, recheckErr := recommendqueue.HealthContext(recheckCtx, skdg.Redis, dateVersion) + recheckCancel() + if recheckErr != nil { + log.Error("short recommend queue health recheck failed", + log.E(recheckErr), log.Any("dateVersion", dateVersion)) + return + } + if rechecked.Healthy { + return + } + + queueVersion := buildQueueVersion(now, lockToken) + buildStartedAt := time.Now() + excludedModuleIDs, err := moduleconfmod.ExcludedVideoModuleIDs(now, true) + if err != nil { + log.Error("short recommend load excluded modules failed", + log.E(err), log.Any("version", queueVersion)) + return + } + refreshStartedAt := time.Now() + initializationLimit := skdg.Conf.ShortRecommendInitializationLimit() + candidates, err := vidmod.RefreshRecommendScoresWithInitializationLimit( + buildCtx, now, initializationLimit, excludedModuleIDs..., + ) + refreshDuration := time.Since(refreshStartedAt) + if err != nil { + log.Error("short recommend refresh scores failed", + log.E(err), log.Any("version", queueVersion), + log.Any("initializationWriteLimit", initializationLimit), + log.Any("refreshDurationMs", refreshDuration.Milliseconds())) + return + } + assembleStartedAt := time.Now() + ids, assemblyStats := recommendqueue.AssembleWithStats(candidates, now) + assembleDuration := time.Since(assembleStartedAt) + if len(ids) == 0 { + log.Warn("short recommend candidates empty, keep previous queue", + log.Any("version", queueVersion)) + return + } + if err := buildCtx.Err(); err != nil { + log.Error("short recommend build lock lost before publish", + log.E(err), log.Any("version", queueVersion)) + return + } + if currentVersion := recommendqueue.VersionAt(time.Now()); currentVersion != dateVersion { + log.Warn("short recommend build crossed version boundary, skip publish", + log.Any("version", queueVersion), log.Any("currentDateVersion", currentVersion)) + return + } + renewed, err := renewBuildLock(buildCtx, lockKey, lockToken) + if err != nil || !renewed { + log.Error("short recommend verify build lock failed", + log.E(err), log.Any("version", queueVersion)) + return + } + publishStartedAt := time.Now() + err = recommendqueue.Publish(buildCtx, skdg.Redis, recommendqueue.QueueMeta{ + Version: queueVersion, Length: len(ids), GeneratedAt: now, StartOffset: 0, + }, ids, lockKey, lockToken) + publishDuration := time.Since(publishStartedAt) + if err != nil { + log.Error("short recommend publish queue failed", + log.E(err), log.Any("version", queueVersion), + log.Any("publishDurationMs", publishDuration.Milliseconds())) + return + } + log.Info("short recommend queue published", + log.Any("version", queueVersion), + log.Any("dateVersion", dateVersion), + log.Any("candidateCount", len(candidates)), + log.Any("excludedModuleCount", len(excludedModuleIDs)), + log.Any("queueLength", len(ids)), + log.Any("highScoreCount", assemblyStats.HighCount), + log.Any("newVideoCount", assemblyStats.NewCount), + log.Any("blockCount", assemblyStats.BlockCount), + log.Any("refreshDurationMs", refreshDuration.Milliseconds()), + log.Any("assembleDurationMs", assembleDuration.Milliseconds()), + log.Any("publishDurationMs", publishDuration.Milliseconds()), + log.Any("totalDurationMs", time.Since(buildStartedAt).Milliseconds())) +} diff --git a/skd/job/shortrecommend/job_test.go b/skd/job/shortrecommend/job_test.go new file mode 100644 index 0000000..b6b925c --- /dev/null +++ b/skd/job/shortrecommend/job_test.go @@ -0,0 +1,18 @@ +package shortrecommend + +import ( + "testing" + "time" +) + +func TestBuildQueueVersionIsUniqueWithinSameDay(t *testing.T) { + now := time.Date(2026, 7, 31, 2, 0, 0, 0, time.UTC) + first := buildQueueVersion(now, "build-a") + second := buildQueueVersion(now, "build-b") + if first != "20260731-build-a" || second != "20260731-build-b" { + t.Fatalf("versions = %q, %q", first, second) + } + if first == second { + t.Fatal("same-day rebuilds must not reuse a queue version") + } +} diff --git a/skd/job/sqlcollapser/shordvideo/method.go b/skd/job/sqlcollapser/shordvideo/method.go new file mode 100644 index 0000000..1daf9c7 --- /dev/null +++ b/skd/job/sqlcollapser/shordvideo/method.go @@ -0,0 +1,27 @@ +package shortvideo + +import ( + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/models/v/vidmod" +) + +// SyncDataToRedis 同步数据到redis +func SyncDataToRedis(data []*vidmod.VideoModel) error { + if len(data) == 0 { + return nil + } + + for _, v := range data { + if v.NewsType != vidmod.SHORT || (v.Status != vidmod.CheckPass && v.Status != vidmod.Free) { + // 去除 + _, _ = redis.Handler.SRem(redisconst.ShortVideosKey, v.ID.Hex()) + } else { + _, _ = redis.Handler.SAdd(redisconst.ShortVideosKey, v.ID.Hex()) + } + } + + log.Info("Redis sync video data completed.") + return nil +} diff --git a/skd/job/sqlcollapser/shordvideo/sync.go b/skd/job/sqlcollapser/shordvideo/sync.go new file mode 100644 index 0000000..0c22eb2 --- /dev/null +++ b/skd/job/sqlcollapser/shordvideo/sync.go @@ -0,0 +1,89 @@ +package shortvideo + +import ( + "91porn-server/common/constant/redisconst" + "91porn-server/common/redis" + "fmt" + "time" + + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/v/vidmod" +) + +func RunSync(defaultDate time.Time) { + log.Info("Redis short video sync start...") + defer log.Info("Redis short video sync end.") + // 如果短视频缓存存在的数据量过低,表示缓存被清理过,需要重新同步 + c, err := redis.Handler.SCard(redisconst.ShortVideosKey) + if err == nil && c < 1000 { + _ = statrecordmod.UpsertOneTrans(nil, statrecordmod.RedisSyncShortVideoJob, defaultDate) + } + + // 获取上次数据同步的记录时间 + lastSyncAt, err := statrecordmod.LastRecordTime(statrecordmod.RedisSyncShortVideoJob, defaultDate) + if err != nil { + log.Error("Redis sync short video data RunSync get last record time failed", log.E(err)) + return + } + var now = time.Now() + var recentMinute = timerange.RecentMinute(now, statrecordmod.OneDayScale) + for recentMinute.After(lastSyncAt) { + var end = lastSyncAt.Add(statrecordmod.OneDayScale * time.Minute) + if end.After(recentMinute) { + end = recentMinute + } + + //// 获取MID + //ret, err := moduleconfmod.GetModuleConfByType(moduleconfmod.ShortPage) + //if err != nil { + // log.Error("Redis sync short video data RunSync get module conf failed", log.E(err)) + // return + //} + //midMap := make(map[string]any) + //for _, v := range ret { + // midMap[v.ID.Hex()] = nil + //} + + var subTimeRange = timerange.TimeRange{ + Head: lastSyncAt, + Tail: end, + } + var page, size = 1, 1000 + hasNext := true + var data []*vidmod.VideoModel + for hasNext { + data, hasNext, err = vidmod.GetShortVideoListByUpdateTimeRange(subTimeRange.Head, subTimeRange.Tail, page, size) + if err != nil { + log.Error("Redis sync short video data GetVideoListByUpdateTimeRange err! ", log.E(err)) + return + } + if hasNext && len(data) > 0 { + _ = SyncDataToRedis(data) + page++ + } else { + if err = commit(data, subTimeRange.Tail); err != nil { + log.Error("Redis sync short video data RunSync commit err! ", log.E(err)) + return + } + } + } + + lastSyncAt = end + } +} + +func commit(data []*vidmod.VideoModel, syncTime time.Time) error { + if len(data) <= 0 { + _ = statrecordmod.UpsertOneTrans(nil, statrecordmod.RedisSyncShortVideoJob, syncTime) + return nil + } + if err := SyncDataToRedis(data); err != nil { + return fmt.Errorf("Redis sync commit short video data SyncDataToES failed!, err:%+v\n", err) + } + if err := statrecordmod.UpsertOneTrans(nil, statrecordmod.RedisSyncShortVideoJob, syncTime); err != nil { + return fmt.Errorf("Redis sync commit short video data UpsertOneTrans failed!, err:%+v\n", err) + } + return nil +} diff --git a/skd/job/sqlcollapser/user/method.go b/skd/job/sqlcollapser/user/method.go new file mode 100644 index 0000000..5f4337b --- /dev/null +++ b/skd/job/sqlcollapser/user/method.go @@ -0,0 +1,44 @@ +package user + +import ( + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "time" +) + +func doSyncWorkCount(start, end time.Time) error { + + mats := []vidmod.Matcher{ + (&vidmod.StatusMatch{Status: vidmod.CheckPass}).New(), + (&vidmod.UpdatedAtGTEAndLTMatch{GTE: &start, LT: &end}).New(), + } + publisherIDs, err := vidmod.PublisherIDList(mats...) + if err != nil { + return err + } + // 分批操作 + for len(publisherIDs) > 0 { + var uids []uint64 + if len(publisherIDs) > 100 { + uids = publisherIDs[:100] + publisherIDs = publisherIDs[100:] + } else { + uids = publisherIDs + publisherIDs = nil + } + + mats = []vidmod.Matcher{ + (&vidmod.StatusMatch{Status: vidmod.CheckPass}).New(), + (&vidmod.PublisherIDInMatch{PublisherIDList: uids}).New(), + } + m, err := vidmod.CountMapByPublisherID(mats...) + if err != nil { + return err + } + err = usermod.UpdateUserCountWorks(m) + if err != nil { + return err + } + } + return nil +} diff --git a/skd/job/sqlcollapser/user/runSync.go b/skd/job/sqlcollapser/user/runSync.go new file mode 100644 index 0000000..7a5e6ee --- /dev/null +++ b/skd/job/sqlcollapser/user/runSync.go @@ -0,0 +1,54 @@ +package user + +import ( + "fmt" + "time" + + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" +) + +func RunSync(defaultDate time.Time) { + log.Info("SqlCollasper sync") + defer log.Info("SqlCollasper sync user total work end.") + // 获取上次数据同步的记录时间 + lastSyncAt, err := statrecordmod.LastRecordTime(statrecordmod.SqlCollasperUserTotalWork, defaultDate) + if err != nil { + log.Error("SqlCollasper RunSync user total work get last record time failed", log.E(err)) + return + } + var now = time.Now() + var recentMinute = timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) + for recentMinute.After(lastSyncAt) { + var end time.Time + if lastSyncAt.Add(time.Hour * 24).Before(now) { + end = lastSyncAt.Add(time.Hour * 24) + } else { + end = lastSyncAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + if end.After(recentMinute) { + end = recentMinute + } + } + + var subTimeRange = timerange.TimeRange{ + Head: lastSyncAt, + Tail: end, + } + if err = commit(subTimeRange); err != nil { + log.Error("SqlCollasper sync audiobook data RunSync commit err! ", log.E(err)) + return + } + lastSyncAt = end + } +} + +func commit(t timerange.TimeRange) error { + if err := doSyncWorkCount(t.Head, t.Tail); err != nil { + return fmt.Errorf("SqlCollasper sync commit incPageView failed!, err:%+v\n", err) + } + if err := statrecordmod.UpsertOneTrans(nil, statrecordmod.SqlCollasperUserTotalWork, t.Tail); err != nil { + return fmt.Errorf("SqlCollasper sync commit UpsertOneTrans failed!, err:%+v\n", err) + } + return nil +} diff --git a/skd/job/sqlcollapser/vid/method.go b/skd/job/sqlcollapser/vid/method.go new file mode 100644 index 0000000..8db2571 --- /dev/null +++ b/skd/job/sqlcollapser/vid/method.go @@ -0,0 +1,51 @@ +package vid + +import ( + "time" + + "91porn-server/common/constant/redisconst" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/v/vidmod" + "91porn-server/skd/skdg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +func incPageView(syncTime time.Time) error { + now := syncTime + recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) + key := redisconst.VidPageViewListKey(recentMinute.Unix()) + count, err := skdg.Redis.LCount(key) + if err != nil { + return err + } + if count == 0 { + return nil + } + strList, err := skdg.Redis.LRange(key, 0, count-1) + if err != nil { + return err + } + dmap := make(map[string]int64) + for i := range strList { + dmap[strList[i]]++ + } + models := make([]mongo.WriteModel, 0, len(dmap)) + for k, v := range dmap { + _id, err1 := primitive.ObjectIDFromHex(k) + if err1 != nil { + continue + } + filter := bson.M{"_id": _id} + update := bson.M{"$inc": bson.M{"pageViewCount": v}} + models = append(models, mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update)) + } + if err = vidmod.Bulk(models); err != nil { + return err + } + _, _ = skdg.Redis.Del(key) + return nil +} diff --git a/skd/job/sqlcollapser/vid/runSync.go b/skd/job/sqlcollapser/vid/runSync.go new file mode 100644 index 0000000..3e40fa5 --- /dev/null +++ b/skd/job/sqlcollapser/vid/runSync.go @@ -0,0 +1,48 @@ +package vid + +import ( + "fmt" + "time" + + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" +) + +func RunSync(defaultDate time.Time) { + log.Info("SqlCollasper sync start...") + defer log.Info("SqlCollasper sync end.") + // 获取上次数据同步的记录时间 + lastSyncAt, err := statrecordmod.LastRecordTime(statrecordmod.SqlCollasperVidPageView, defaultDate) + if err != nil { + log.Error("SqlCollasper RunSync get last record time failed", log.E(err)) + return + } + var now = time.Now().Add(-time.Minute * 5) + var recentMinute = timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) + for recentMinute.After(lastSyncAt) { + var end = lastSyncAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + if end.After(recentMinute) { + end = recentMinute + } + var subTimeRange = timerange.TimeRange{ + Head: lastSyncAt, + Tail: end, + } + if err = commit(subTimeRange.Tail); err != nil { + log.Error("SqlCollasper sync audiobook data RunSync commit err! ", log.E(err)) + return + } + lastSyncAt = end + } +} + +func commit(syncTime time.Time) error { + if err := incPageView(syncTime); err != nil { + return fmt.Errorf("SqlCollasper sync commit incPageView failed!, err:%+v\n", err) + } + if err := statrecordmod.UpsertOneTrans(nil, statrecordmod.SqlCollasperVidPageView, syncTime); err != nil { + return fmt.Errorf("SqlCollasper sync commit UpsertOneTrans failed!, err:%+v\n", err) + } + return nil +} diff --git a/skd/job/sqlcollasper.go b/skd/job/sqlcollasper.go new file mode 100644 index 0000000..ce451dd --- /dev/null +++ b/skd/job/sqlcollasper.go @@ -0,0 +1,50 @@ +package job + +import ( + "91porn-server/common" + "91porn-server/common/log" + shortvideo "91porn-server/skd/job/sqlcollapser/shordvideo" + "91porn-server/skd/job/sqlcollapser/user" + "91porn-server/skd/job/sqlcollapser/vid" + "sync" +) + +var syncSqlcollaspeIsRuining = false + +// start 定时任务 开启 +func runSyncSqlcollaspe() { + //防止重入 + if syncSqlcollaspeIsRuining { + return + } + syncSqlcollaspeIsRuining = true + defer func() { + syncSqlcollaspeIsRuining = false + }() + doSyncSqlcollasper() //做折叠业务 +} + +// 做折叠业务 +func doSyncSqlcollasper() { + log.Info("SqlCollasper doSyncSqlcollasper start...") + defaultSyncDate, err := getDefaultSyncDate() + if err != nil { + log.Error("defaultStatDate invalid", log.E(err)) + return + } + wg := sync.WaitGroup{} + wg.Add(3) + common.Go(func() { + defer wg.Done() + vid.RunSync(defaultSyncDate) + }) + common.Go(func() { + defer wg.Done() + user.RunSync(defaultSyncDate) + }) + common.Go(func() { + defer wg.Done() + shortvideo.RunSync(defaultSyncDate) + }) + wg.Wait() +} diff --git a/skd/job/stat.go b/skd/job/stat.go new file mode 100644 index 0000000..b4055b3 --- /dev/null +++ b/skd/job/stat.go @@ -0,0 +1,104 @@ +package job + +import ( + "sync" + "time" + + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/skd/job/cache/tonecache" + "91porn-server/skd/job/stat/keyword" + "91porn-server/skd/job/stat/kwrank" + "91porn-server/skd/job/stat/rank" + "91porn-server/skd/job/stat/rechorder" + "91porn-server/skd/job/stat/statuser" + "91porn-server/skd/job/stat/tagplay" + "91porn-server/skd/job/stat/video" + "91porn-server/skd/job/stat/vidtotal" + "91porn-server/skd/job/stat/withorder" + "91porn-server/skd/skdg" +) + +var ( + statIsRunning = false //true:统计任务运行中,反之停止, + statRankIsRunning = false +) + +// 运行统计任务 +func runStat() { + //防止重入 + if statIsRunning { + return + } + statIsRunning = true + defer func() { + statIsRunning = false + }() + doStat() //做统计业务 +} + +// 做统计业务 +func doStat() { + defaultStatDate, err := getDefaultStatDate() + if err != nil { + log.Error("defaultStatDate invalid", log.E(err)) + return + } + methodM := []func(){ + //主要用于app + func() { //"关键字" + keyword.RunStat(defaultStatDate) + kwrank.RunStat(defaultStatDate) //有执行顺序 + }, + func() { //"标签播放" + tagplay.RunStat(defaultStatDate) + }, + func() { //"用户数据" + statuser.RunStat(defaultStatDate) + }, + // func() { //"ranks" + //rankcache.Update() + // }, + func() { //"tone" + tonecache.Update() + }, + func() { //"视屏" + video.RunStat(defaultStatDate) + vidtotal.RunStat(defaultStatDate) //有执行顺序 + }, + func() { //"充值订单" + rechorder.RunStat(defaultStatDate) + }, + func() { //"兑换订单" + withorder.RunStat(defaultStatDate) + }, + } + time.Sleep(time.Duration(30) * time.Second) + wg := sync.WaitGroup{} + wg.Add(len(methodM)) + for _, method := range methodM { + _method := method + common.Go(func() { + defer wg.Done() + _method() + }) + } + wg.Wait() +} + +func getDefaultStatDate() (time.Time, error) { + return time.ParseInLocation("2006-01-02", skdg.Conf.StartDate, time.Local) +} + +// 运行排行榜 +func runRankStat() { + //防止重入 + if statRankIsRunning { + return + } + statRankIsRunning = true + defer func() { + statRankIsRunning = false + }() + rank.RunStat() +} diff --git a/skd/job/stat/keyword/method.go b/skd/job/stat/keyword/method.go new file mode 100644 index 0000000..a8ad28f --- /dev/null +++ b/skd/job/stat/keyword/method.go @@ -0,0 +1,59 @@ +package keyword + +import ( + "91porn-server/common/constant" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/l/searchlogmod" + "91porn-server/models/s/kwstatmod" +) + +type TimeRange = timerange.TimeRange + +func toKeywordIncDoc(realm constant.RealmType, data map[string]int64) []kwstatmod.KeywordIncDoc { + list := make([]kwstatmod.KeywordIncDoc, len(data)) + i := 0 + for k, v := range data { + word := k + count := v + list[i] = kwstatmod.KeywordIncDoc{ + Realm: realm, + Word: word, + Count: count, + } + i++ + } + return list +} + +func GetIncDocList(timeRange TimeRange) ([]kwstatmod.KeywordIncDoc, error) { + incDocList := []kwstatmod.KeywordIncDoc{} + //综合关键字搜索数 + complexKeywordCountMap, err := searchlogmod.GetKeywordCountMapByTime(timeRange.Head, timeRange.Tail, constant.SearchComplex) + if err != nil { + return nil, err + } + complexIncDocList := toKeywordIncDoc(constant.SearchComplex, complexKeywordCountMap) + incDocList = append(incDocList, complexIncDocList...) + //视屏关键字搜索数 + vidKeywordCountMap, err := searchlogmod.GetKeywordCountMapByTime(timeRange.Head, timeRange.Tail, constant.SearchSP) + if err != nil { + return nil, err + } + videoIncDocList := toKeywordIncDoc(constant.SearchSP, vidKeywordCountMap) + incDocList = append(incDocList, videoIncDocList...) + //标签关键字搜索数 + tagKeywordCountMap, err := searchlogmod.GetKeywordCountMapByTime(timeRange.Head, timeRange.Tail, constant.SearchTag) + if err != nil { + return nil, err + } + tagIncDocList := toKeywordIncDoc(constant.SearchTag, tagKeywordCountMap) + incDocList = append(incDocList, tagIncDocList...) + //用户关键字搜索数 + userKeywordCountMap, err := searchlogmod.GetKeywordCountMapByTime(timeRange.Head, timeRange.Tail, constant.SearchUser) + if err != nil { + return nil, err + } + userIncDocList := toKeywordIncDoc(constant.SearchUser, userKeywordCountMap) + incDocList = append(incDocList, userIncDocList...) + return incDocList, nil +} diff --git a/skd/job/stat/keyword/stat.go b/skd/job/stat/keyword/stat.go new file mode 100644 index 0000000..2bbe2ad --- /dev/null +++ b/skd/job/stat/keyword/stat.go @@ -0,0 +1,72 @@ +package keyword + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/kwstatmod" + "91porn-server/models/s/statrecordmod" + "91porn-server/skd/skdg" +) + +// RunStat +func RunStat(defaultStatDate time.Time) { + log.Info("keyword stat running...") + defer log.Info("keyword stat finished...") + now := time.Now() + //从历史提交记录中获取上次记录时间 + lastStatAt, err := statrecordmod.LastRecordTime(statrecordmod.KeywordStatJob, defaultStatDate) //第一次从默认记录时间开始统计 + if err != nil { + log.Error("Keyword RunStat lastRecordTime faild!", log.E(err)) + return + } + recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) //对齐本次统计时间 Minute % frequency == 0 + //拆分时间区间并依次提交,均衡数据库读写压力 + for recentMinute.After(lastStatAt) { + end := lastStatAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + //防止时间超出范围 + if end.After(recentMinute) { + end = recentMinute + } + subTimeRange := timerange.TimeRange{ //连续子切片 + Head: lastStatAt, + Tail: end, + } + //此时的零日点 + sumDate := timerange.LocDayRange(subTimeRange.Head).Head + //装配基础统计数据 + incDocList, err := GetIncDocList(subTimeRange) + if err != nil { + log.Error("Keyword RunStat GetIncDocList faild!", log.E(err)) + return + } + //提交数据 + log.Info("keywordStat committing...", log.Any("sumDate", sumDate), log.Any("location", sumDate.Location().String())) + if err = commit(sumDate, subTimeRange.Tail, incDocList); err != nil { + log.Error("Keyword RunStat commit faild!", log.E(err)) + return + } + log.Info("keywordStat committed.", log.Any("sumDate", sumDate), log.Any("location", sumDate.Location().String())) + lastStatAt = end + } +} + +// commit 提交统计数据 +func commit(sumDate time.Time, recordTime time.Time, incDocList []kwstatmod.KeywordIncDoc) error { + opt := (&db.TransOpts{}).SetReEntry(10) + return skdg.StatDB.Trans(func(trans *db.MongoTool) (err error) { + if len(incDocList) != 0 { + if err = kwstatmod.ChangeStatTrans(trans, sumDate, recordTime, incDocList); err != nil { + return fmt.Errorf(" keyword commit ChangeStatTrans faild!, err:%+v\n", err) + } + } + //提交本次修改记录 + if err = statrecordmod.UpsertOneTrans(trans, statrecordmod.KeywordStatJob, recordTime); err != nil { + return fmt.Errorf(" keyword commit UpsertOneTrans faild!, err:%+v\n", err) + } + return + }, opt) +} diff --git a/skd/job/stat/kwrank/method.go b/skd/job/stat/kwrank/method.go new file mode 100644 index 0000000..7ddcb7a --- /dev/null +++ b/skd/job/stat/kwrank/method.go @@ -0,0 +1,29 @@ +package kwrank + +import ( + "time" + + "91porn-server/models/s/kwrankmod" + "91porn-server/models/s/kwstatmod" +) + +type RankSetDoc = kwrankmod.RankSetDoc + +func GetStatSetDocList(sumDate time.Time) ([]RankSetDoc, error) { + keywordCountMap, err := kwstatmod.KeywordCountMapBySumDate(sumDate) + if err != nil { + return nil, err + } + rankDocList := make([]RankSetDoc, len(keywordCountMap)) + i := 0 + for w, c := range keywordCountMap { + word := w + count := c + rankDocList[i] = RankSetDoc{ + Word: &word, + Count: &count, + } + i++ + } + return rankDocList, nil +} diff --git a/skd/job/stat/kwrank/stat.go b/skd/job/stat/kwrank/stat.go new file mode 100644 index 0000000..b16b753 --- /dev/null +++ b/skd/job/stat/kwrank/stat.go @@ -0,0 +1,52 @@ +package kwrank + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/kwrankmod" + "91porn-server/models/s/statrecordmod" + "91porn-server/skd/skdg" +) + +// RunStat +func RunStat(defaultStatDate time.Time) { + log.Info("keyword rank stat running...") + defer log.Info("keyword rank stat finished...") + now := time.Now() + sumDate := timerange.LocDayRange(now).Head //获取零日点 + //装配基础统计数据 + setList, err := GetStatSetDocList(sumDate) + if err != nil { + log.Error("kwrank RunStat fillStatSetDoc faild!", log.E(err)) + return + } + //提交数据 + log.Info("keyword rank stat committing...", log.Any("sumDate", sumDate), log.Any("location", sumDate.Location().String())) + recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) //对齐本次统计时间 Minute % frequency == 0 + if err = commit(sumDate, recentMinute, setList); err != nil { + log.Error("kwrank RunStat commit faild!", log.E(err)) + return + } + log.Info("keyword rank stat committed.", log.Any("sumDate", sumDate), log.Any("location", sumDate.Location().String())) +} + +// commit 提交统计数据 +func commit(sumDate time.Time, recordTime time.Time, setDocList []RankSetDoc) error { + opt := (&db.TransOpts{}).SetReEntry(10) + return skdg.StatDB.Trans(func(trans *db.MongoTool) (err error) { + if len(setDocList) != 0 { + if err = kwrankmod.ChangeStatTrans(trans, sumDate, recordTime, setDocList); err != nil { + return fmt.Errorf("kwrank commit ChangeStatTrans faild!, err:%+v\n", err) + } + } + //提交本次修改记录 + if err = statrecordmod.UpsertOneTrans(trans, statrecordmod.KeywordRankStatJob, recordTime); err != nil { + return fmt.Errorf("kwrank commit UpsertOneTrans faild!, err:%+v\n", err) + } + return + }, opt) +} diff --git a/skd/job/stat/rank/stat.go b/skd/job/stat/rank/stat.go new file mode 100644 index 0000000..3a03603 --- /dev/null +++ b/skd/job/stat/rank/stat.go @@ -0,0 +1,53 @@ +package rank + +import ( + "time" + + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/hotstatmod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// RunStat 统计实时排行榜 +func RunStat() { + log.Info("rank stat running...") + defer log.Info("rank stat finished...") + now := time.Now() + sumDate := timerange.LocDayRange(now).Head + infos, err := vidmod.GetTotalNewsList() + if err != nil { + return + } + mNow := calNowHot(infos) + mYesterday, err := hotstatmod.GetYesterdayHotValue(sumDate) + if err != nil { + return + } + mLastWeek, err := hotstatmod.GetLastWeekHotValue(sumDate) + if err != nil { + return + } + mLastMonth, _ := hotstatmod.GetLastMonthHotValue(sumDate) + mDayli := make(map[primitive.ObjectID]int) + mWeek := make(map[primitive.ObjectID]int) + mMonth := make(map[primitive.ObjectID]int) + for vid, v := range mNow { + mDayli[vid] = v - mYesterday[vid] + mWeek[vid] = v - mLastWeek[vid] + mMonth[vid] = v - mLastMonth[vid] + } + //入库 + _ = hotstatmod.UpdateInsertHotValue(sumDate, mDayli, mWeek, mMonth, mNow) +} + +func calNowHot(infos []*vidmod.VideoModel) map[primitive.ObjectID]int { + mNow := make(map[primitive.ObjectID]int) + for _, v := range infos { + value := 1*v.FakePlayCount + 4*v.FakeLikeCount + 2*v.FakeCommentCount + v.FakeShareCount + mNow[v.ID] = value + } + return mNow +} diff --git a/skd/job/stat/rechorder/method.go b/skd/job/stat/rechorder/method.go new file mode 100644 index 0000000..d67f1de --- /dev/null +++ b/skd/job/stat/rechorder/method.go @@ -0,0 +1,138 @@ +package rechorder + +import ( + "time" + + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statordermod" + "91porn-server/models/v/rchgordmod" +) + +type RechargeOrderStateDoc = statordermod.RechargeOrderStateDoc +type RecOrderStateMap = statordermod.RecOrderStateMap + +func fillRecOrderStatDocMap(sumDate time.Time, timeRange timerange.TimeRange, docMap *RecOrderStateMap) error { + //订单数 OrderCount + if err := fillRecOrderCount(timeRange, docMap); err != nil { + return err + } + //完成订单数 CompleteOrderCount + if err := fillRecCompleteOrderCount(timeRange, docMap); err != nil { + return err + } + //订单金额 OrderAmount + if err := fillRecOrderAmount(timeRange, docMap); err != nil { + return err + } + if err := fillRecCompleteOrderPayMoney(timeRange, docMap); err != nil { + return err + } + //用户已支付订单数 + if err := fillRecPaidOrderCount(timeRange, docMap); err != nil { + return err + } + //用户已支付金额 + return fillRecPaidMoney(timeRange, docMap) +} + +// fillRecOrderCount fillRecOrderCount +func fillRecOrderCount(timeRange timerange.TimeRange, docMap *RecOrderStateMap) error { + orderCountMap, err := rchgordmod.ChannelOrderCountMap(timeRange.Head, timeRange.Tail) + if err != nil { + return err + } + docM := *docMap + for channel, v := range orderCountMap { + if _, existed := docM[channel]; !existed { + docM[channel] = &RechargeOrderStateDoc{} + } + val := v + docM[channel].OrderCount = &val + } + return nil +} + +// fillRecCompleteOrderCount fillRecCompleteOrderCount} +func fillRecCompleteOrderCount(timeRange timerange.TimeRange, docMap *RecOrderStateMap) error { + completeOrderCountMap, err := rchgordmod.ChannelCompleteOrderCountMap(timeRange.Head, timeRange.Tail) + if err != nil { + return err + } + docM := *docMap + for channel, v := range completeOrderCountMap { + if _, existed := docM[channel]; !existed { + docM[channel] = &RechargeOrderStateDoc{} + } + val := v + docM[channel].CompleteOrderCount = &val + } + return nil +} + +// fillRecOrderAmount fillRecOrderAmount +func fillRecOrderAmount(timeRange timerange.TimeRange, docMap *RecOrderStateMap) error { + orderAmountMap, err := rchgordmod.ChannelOrderMoneyMap(timeRange.Head, timeRange.Tail) + if err != nil { + return err + } + docM := *docMap + for channel, v := range orderAmountMap { + if _, existed := docM[channel]; !existed { + docM[channel] = &RechargeOrderStateDoc{} + } + val := v + docM[channel].OrderAmount = &val + } + return nil +} + +// fillRecCompleteOrderPayMoney fillRecCompleteOrderPayMoney +func fillRecCompleteOrderPayMoney(timeRange timerange.TimeRange, docMap *RecOrderStateMap) error { + payMoneyMap, err := rchgordmod.ChannelCompleteOrderPayMoneyMap(timeRange.Head, timeRange.Tail) + if err != nil { + return err + } + docM := *docMap + for channel, v := range payMoneyMap { + if _, existed := docM[channel]; !existed { + docM[channel] = &RechargeOrderStateDoc{} + } + val := v + docM[channel].CompleteOrderAmount = &val + } + return nil +} + +// fillRecPaidOrderCount fillRecPaidOrderCount +func fillRecPaidOrderCount(timeRange timerange.TimeRange, docMap *RecOrderStateMap) error { + paiedOrderCountMap, err := rchgordmod.ChannelPaymentOrderCountMap(timeRange.Head, timeRange.Tail) + if err != nil { + return err + } + docM := *docMap + for channel, v := range paiedOrderCountMap { + if _, existed := docM[channel]; !existed { + docM[channel] = &RechargeOrderStateDoc{} + } + val := v + docM[channel].PaidOrderCount = &val + } + return nil +} + +// fillRecPaidMoney fillRecPaidMoney +func fillRecPaidMoney(timeRange timerange.TimeRange, docMap *RecOrderStateMap) error { + paiedMoneyMap, err := rchgordmod.ChannelPaymentMoneyMap(timeRange.Head, timeRange.Tail) + if err != nil { + return err + } + docM := *docMap + for channel, v := range paiedMoneyMap { + if _, existed := docM[channel]; !existed { + docM[channel] = &RechargeOrderStateDoc{} + } + val := v + docM[channel].PaidAmount = &val + } + return nil +} diff --git a/skd/job/stat/rechorder/stat.go b/skd/job/stat/rechorder/stat.go new file mode 100644 index 0000000..4f1a1bb --- /dev/null +++ b/skd/job/stat/rechorder/stat.go @@ -0,0 +1,97 @@ +package rechorder + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statordermod" + "91porn-server/models/s/statrecordmod" + "91porn-server/skd/skdg" +) + +func RunStat(defaultStatDate time.Time) { + log.Info("rechorderStat stat running...") + defer log.Info("rechorderStat stat finished...") + now := time.Now() + //从历史提交记录中获取上次记录时间 + lastStatAt, err := statrecordmod.LastRecordTime(statrecordmod.RechargeOrderStatJob, defaultStatDate) //第一次从默认记录时间开始统计 + if err != nil { + log.Error("rechorder RunStat lastRecordTime faild!", log.E(err)) + return + } + //过去几天的数据按一日为单位统计,帮助数据恢复 + dayHead := timerange.LocDayRange(now).Head + for dayHead.After(lastStatAt) { + end := lastStatAt.AddDate(0, 0, 1) + //防止时间超出范围 + if end.After(dayHead) { + end = dayHead + } + subTimeRange := timerange.TimeRange{ //连续子切片 + Head: lastStatAt, + Tail: end, + } + if err := statByTimeRange(subTimeRange); err != nil { + log.Error("rechorder RunStat statByTimeRange faild!", log.E(err)) + return + } + lastStatAt = end + } + //当天的数据按照5分钟统计 + recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) //对齐本次统计时间 Minute % frequency == 0 + //拆分时间区间并依次提交,控制数据库读写压力 + for recentMinute.After(lastStatAt) { + end := lastStatAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + //防止时间超出范围 + if end.After(recentMinute) { + end = recentMinute + } + subTimeRange := timerange.TimeRange{ //连续子切片 + Head: lastStatAt, + Tail: end, + } + if err := statByTimeRange(subTimeRange); err != nil { + log.Error("rechorder RunStat statByTimeRange faild!", log.E(err)) + return + } + lastStatAt = end + } +} + +func statByTimeRange(timeRange timerange.TimeRange) error { + sumDate := timerange.LocDayRange(timeRange.Head).Head //获取此时的零日点 + //装配渠道统计数据 + recOrderStateMap := make(RecOrderStateMap) + if err := fillRecOrderStatDocMap(sumDate, timeRange, &recOrderStateMap); err != nil { + log.Error("rechorder RunStat fillRecOrderStatDocMap faild!", log.E(err)) + return err + } + //提交渠道统计数据 + log.Info("rechorderStat recharge committing...", log.Any("sumDate", sumDate), log.Any("location", sumDate.Location().String())) + if err := commitRec(sumDate, timeRange.Tail, recOrderStateMap); err != nil { + log.Error("rechorderStat RunStat commitRec faild!", log.E(err)) + return err + } + log.Info("rechorderStat recharge committed.", log.Any("sumDate", sumDate), log.Any("location", sumDate.Location().String())) + return nil +} + +// commit 提交订单统计数据 +func commitRec(sumDate time.Time, recordTime time.Time, docMap RecOrderStateMap) error { + opt := (&db.TransOpts{}).SetReEntry(10) + return skdg.StatDB.Trans(func(trans *db.MongoTool) error { + if len(docMap) != 0 { + if err := statordermod.ChangeRechargeStatTrans(trans, sumDate, recordTime, docMap); err != nil { + return fmt.Errorf("stat commitRec ChangeRechargeStatTrans faild!, err:%+v\n", err) + } + } + //提交本次修改记录 + if err := statrecordmod.UpsertOneTrans(trans, statrecordmod.RechargeOrderStatJob, recordTime); err != nil { + return fmt.Errorf("stat commitRec UpsertOneTrans faild!, err:%+v\n", err) + } + return nil + }, opt) +} diff --git a/skd/job/stat/statuser/method.go b/skd/job/stat/statuser/method.go new file mode 100644 index 0000000..58820ec --- /dev/null +++ b/skd/job/stat/statuser/method.go @@ -0,0 +1,91 @@ +package statuser + +import ( + "time" + + "91porn-server/common/timeutil/timerange" + "91porn-server/models/l/payvidlgmod" + "91porn-server/models/s/statusermod" + "91porn-server/models/v/vidmod" +) + +type TimeRange = timerange.TimeRange + +type IncDoc = statusermod.IncDoc + +type SetDoc = statusermod.SetDoc + +type Itemtype = statusermod.Itemtype + +const ( + SetType string = "set" + IncType string = "inc" +) + +type Method struct { + MethodType string + GetIncDocListMethod func(timeRange TimeRange) ([]IncDoc, error) + GetSetDocListMethod func(timeRange TimeRange) ([]SetDoc, error) + IncCommitMethod func(incDocList []IncDoc, name string, recordAt time.Time) error + SetCommitMethod func(setDocList []SetDoc, name string, recordAt time.Time) error +} + +var methodMap = map[Itemtype]Method{ + //视频收益 + statusermod.VidIncome: {MethodType: IncType, GetIncDocListMethod: getIncomneDocList, IncCommitMethod: commitIncDoc}, + //上传数量 + statusermod.UploadCount: {MethodType: SetType, GetSetDocListMethod: getUploadCountDocList, SetCommitMethod: commitSetDoc}, +} + +func getIncomneDocList(timeRange TimeRange) ([]IncDoc, error) { + m, err := payvidlgmod.PublisherIncomeMapByTime(timeRange.Head, timeRange.Tail) + if err != nil { + return nil, err + } + list := make([]IncDoc, len(m)) + i := 0 + for uid, income := range m { + list[i] = IncDoc{ + UID: uid, + Item: statusermod.VidIncome, + Count: income, + RecordAt: timeRange.Tail, + } + i++ + } + return list, nil +} + +func getUploadCountDocList(timeRange TimeRange) ([]SetDoc, error) { + mats := []vidmod.Matcher{ + (&vidmod.StatusMatch{Status: vidmod.CheckPass}).New(), + (&vidmod.ReviewAtGTEAndLTMatch{GTE: &timeRange.Head, LT: &timeRange.Tail}).New(), + } + publisherIDs, err := vidmod.PublisherIDList(mats...) + if err != nil { + return nil, err + } + if len(publisherIDs) == 0 { + return nil, nil + } + mats = []vidmod.Matcher{ + (&vidmod.StatusMatch{Status: vidmod.CheckPass}).New(), + (&vidmod.PublisherIDInMatch{PublisherIDList: publisherIDs}).New(), + } + m, err := vidmod.CountMapByPublisherID(mats...) + if err != nil { + return nil, err + } + list := make([]SetDoc, len(m)) + i := 0 + for uid, uploadCount := range m { + list[i] = SetDoc{ + UID: uid, + Item: statusermod.UploadCount, + Count: uploadCount, + RecordAt: timeRange.Tail, + } + i++ + } + return list, nil +} diff --git a/skd/job/stat/statuser/stat.go b/skd/job/stat/statuser/stat.go new file mode 100644 index 0000000..09ab65a --- /dev/null +++ b/skd/job/stat/statuser/stat.go @@ -0,0 +1,143 @@ +package statuser + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/s/statusermod" + "91porn-server/skd/skdg" +) + +func RunStat(defaultStatDate time.Time) { + log.Info("user stat running...") + defer log.Info("user stat finished...") + defaultTimeMap := map[string]time.Time{ + string(statusermod.VidIncome): defaultStatDate, + string(statusermod.UploadCount): defaultStatDate, + } + //从历史提交记录中获取上次记录时间 + lastStatAtMap, err := statrecordmod.LastRecordTimeMap(statrecordmod.UserStatJob, defaultTimeMap) //第一次从默认记录时间开始统计 + if err != nil { + log.Error("user stat statuser LastRecordTimeMap faild!", log.E(err)) + return + } + now := time.Now() + dayHead := timerange.LocDayRange(now).Head + for item, lastStatAt := range lastStatAtMap { + _lastStatAt := lastStatAt + //过去几天的数据按一日为单位统计,帮助数据恢复 + for dayHead.After(_lastStatAt) { + end := _lastStatAt.AddDate(0, 0, 1) + //防止时间超出范围 + if end.After(dayHead) { + end = dayHead + } + subTimeRange := timerange.TimeRange{ //连续子切片 + Head: _lastStatAt, + Tail: end, + } + if err = statByTimeRange(item, subTimeRange); err != nil { + log.Error("user stat statuser statByTimeRange 1 faild!", log.E(err)) + return + } + _lastStatAt = end + } + lastStatAtMap[item] = _lastStatAt + } + recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) //对齐本次统计时间 Minute % frequency == 0 + //拆分时间区间并依次提交,控制数据库读写压力 + for item, lastStatAt := range lastStatAtMap { + _lastStatAt := lastStatAt + for recentMinute.After(_lastStatAt) { + end := _lastStatAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + //防止时间超出范围 + if end.After(recentMinute) { + end = recentMinute + } + subTimeRange := timerange.TimeRange{ //连续子切片 + Head: _lastStatAt, + Tail: end, + } + if err = statByTimeRange(item, subTimeRange); err != nil { + log.Error("user stat statuser statByTimeRange 2 faild!", log.E(err)) + return + } + _lastStatAt = end + } + lastStatAtMap[item] = _lastStatAt + } +} + +func statByTimeRange(item string, timeRange timerange.TimeRange) error { + method, ok := methodMap[Itemtype(item)] + if !ok { + return nil + } + if method.MethodType == IncType { + //装配INC基础统计数据 + incDocList, err := method.GetIncDocListMethod(timeRange) + if err != nil { + log.Error("user stat statByTimeRange GetIncDocListMethod faild!", log.Any("item", item), log.E(err)) + return err + } + //提交Inc统计数据 + log.Info("user stat statByTimeRange IncCommitMethod committing...", log.Any("item", item)) + if err = method.IncCommitMethod(incDocList, item, timeRange.Tail); err != nil { + log.Error("user stat statByTimeRange IncCommitMethod faild!", log.Any("item", item), log.E(err)) + return err + } + log.Info("user stat statByTimeRange IncCommitMethod committed.", log.Any("item", item)) + } else if method.MethodType == SetType { + //装配SET基础统计数据 + setDocList, err := method.GetSetDocListMethod(timeRange) + if err != nil { + log.Error("user stat statByTimeRange GetSetDocListMethod faild!", log.Any("item", item), log.E(err)) + return err + } + //提交SET统计数据 + log.Info("user stat statByTimeRange SetCommitMethod committing...", log.Any("item", item)) + if err = method.SetCommitMethod(setDocList, item, timeRange.Tail); err != nil { + log.Error("user stat statByTimeRange SetCommitMethod faild!", log.Any("item", item), log.E(err)) + return err + } + log.Info("user stat statByTimeRange SetCommitMethod committed.", log.Any("item", item)) + } + return nil +} + +func commitIncDoc(incDocList []IncDoc, item string, recordAt time.Time) error { + opt := (&db.TransOpts{}).SetReEntry(10) + return skdg.StatDB.Trans(func(trans *db.MongoTool) error { + if len(incDocList) != 0 { + if err := statusermod.ChangeIncStatTrans(trans, incDocList); err != nil { + return fmt.Errorf("user stat commitCount ChangeIncStatTrans faild!, err:%+v\n", err) + } + } + //提交本次修改记录 + if err := statrecordmod.UpsertOneItemTrans(trans, statrecordmod.UserStatJob, item, recordAt); err != nil { + return fmt.Errorf("user stat commitIncDoc UpsertOneItemTrans faild!, err:%+v\n", err) + } + return nil + }, opt) +} + +func commitSetDoc(setDocList []SetDoc, item string, recordAt time.Time) error { + opt := (&db.TransOpts{}).SetReEntry(10) + return skdg.StatDB.Trans(func(trans *db.MongoTool) error { + if len(setDocList) != 0 { + err := statusermod.ChangeSetStatTrans(trans, setDocList) + if err != nil { + return fmt.Errorf("user stat commitCount ChangeSetStatTrans faild!, err:%+v\n", err) + } + } + //提交本次修改记录 + if err := statrecordmod.UpsertOneItemTrans(trans, statrecordmod.UserStatJob, item, recordAt); err != nil { + return fmt.Errorf("user stat commitSetDoc UpsertOneItemTrans faild!, item: %s+v, err:%+v\n", item, err) + } + return nil + }, opt) +} diff --git a/skd/job/stat/tagplay/method.go b/skd/job/stat/tagplay/method.go new file mode 100644 index 0000000..f3a3107 --- /dev/null +++ b/skd/job/stat/tagplay/method.go @@ -0,0 +1,25 @@ +package tagplay + +import ( + "91porn-server/common/timeutil/timerange" + "91porn-server/models/l/playlgmod" + "91porn-server/models/s/stattagplaymod" +) + +type IncDoc = stattagplaymod.IncDoc + +func GetTagPlayIncDocList(timeRange timerange.TimeRange) ([]IncDoc, error) { + playLogList, err := playlgmod.GetTagPlayByTimeRange(timeRange.Head, timeRange.Tail) + if err != nil { + return []IncDoc{}, err + } + tagPlayList := make([]IncDoc, len(playLogList)) + for i, v := range playLogList { + tagPlayList[i] = IncDoc{ + VideoID: v.GroupID.VideoID, + TagID: v.GroupID.TagID, + PlayCount: v.PlayCount, + } + } + return tagPlayList, nil +} diff --git a/skd/job/stat/tagplay/stat.go b/skd/job/stat/tagplay/stat.go new file mode 100644 index 0000000..42ed49d --- /dev/null +++ b/skd/job/stat/tagplay/stat.go @@ -0,0 +1,71 @@ +package tagplay + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/s/stattagplaymod" + "91porn-server/skd/skdg" +) + +func RunStat(defaultStatDate time.Time) { + log.Info("tag play stat start...") + defer log.Info("tag play stat end...") + // 获取上次统计的记录时间 + lastStatAt, err := statrecordmod.LastRecordTime(statrecordmod.TagPlayStatJob, defaultStatDate) //第一次从默认记录时间开始统计 + if err != nil { + log.Error("tag play RunStat lastRecordTime faild!", log.E(err)) + return + } + now := time.Now() + // 按照每个钟的5分钟刻度统计: 0,05,10,15,20,25,30,35,40,45,50,55 + // 获取最近的时间刻度 + recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) + for recentMinute.After(lastStatAt) { + end := lastStatAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + //防止时间超出范围 + if end.After(recentMinute) { + end = recentMinute + } + // 时间区间5分钟 + subTimeRange := timerange.TimeRange{ + Head: lastStatAt, + Tail: end, + } + //5分钟内的统计数据 + incDocList, err := GetTagPlayIncDocList(subTimeRange) + if err != nil { + log.Error("tag play RunStat GetTagPlayList err!", log.E(err)) + return + } + //每次循环都应提交 + log.Info("tag play stat committing...") + if err = commit(incDocList, subTimeRange.Tail); err != nil { + log.Error("tag play RunStat commit err! ", log.E(err)) + return + } + log.Info("tag play stat committed.") + // 统计数据 + lastStatAt = end + } +} + +func commit(statData []IncDoc, recordTime time.Time) error { + opt := (&db.TransOpts{}).SetReEntry(10) + return skdg.StatDB.Trans(func(trans *db.MongoTool) error { + if len(statData) != 0 { + if err := stattagplaymod.ChangeStatTrans(trans, statData); err != nil { + return fmt.Errorf("tag play commit ChangeStatTrans faild!, err:%+v\n", err) + } + } + //提交本次修改记录 + if err := statrecordmod.UpsertOneTrans(trans, statrecordmod.TagPlayStatJob, recordTime); err != nil { + return fmt.Errorf("tag play commit UpsertOneTrans faild!, err:%+v\n", err) + } + return nil + }, opt) +} diff --git a/skd/job/stat/video/method.go b/skd/job/stat/video/method.go new file mode 100644 index 0000000..dbaba95 --- /dev/null +++ b/skd/job/stat/video/method.go @@ -0,0 +1,130 @@ +package video + +import ( + "time" + + "91porn-server/common/timeutil/timerange" + "91porn-server/models/l/payvidlgmod" + "91porn-server/models/l/playlgmod" + "91porn-server/models/s/statvidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +type TimeRange = timerange.TimeRange + +type VideoStatIncDoc = statvidmod.VideoStatIncDoc + +type VideoStatSetDoc = statvidmod.VideoStatSetDoc + +type VideoStatDoc = statvidmod.VideoStatDoc + +type VideoStatDocMap = statvidmod.VideoStatDocMap //ObjectID->VideoStatDoc + +// fillPayCoin +func fillPayCoin(timeRange TimeRange, docMap *VideoStatDocMap) error { + incomeMap, err := payvidlgmod.PayCoinMapByTime(timeRange.Head, timeRange.Tail) + if err != nil { + return err + } + docM := *docMap + for vid, v := range incomeMap { + if _, existed := docM[vid]; !existed { + docM[vid] = &VideoStatDoc{} + } + val := v + docM[vid].Income = &val + } + return nil +} + +// fillPayCount +func fillPayCount(timeRange TimeRange, docMap *VideoStatDocMap) error { + payCountMap, err := payvidlgmod.PayCountMapByTime(timeRange.Head, timeRange.Tail) + if err != nil { + return err + } + docM := *docMap + for vid, v := range payCountMap { + if _, existed := docM[vid]; !existed { + docM[vid] = &VideoStatDoc{} + } + val := v + docM[vid].PayCount = &val + } + return nil +} + +// fillPlayCount +func fillPlayCount(timeRange TimeRange, docMap *VideoStatDocMap) error { + playCountMap, err := playlgmod.PlayCountMapByTime(timeRange.Head, timeRange.Tail, []int{0, 1, 2}) //0,免费 1付费 2.试看 + if err != nil { + return err + } + docM := *docMap + for vid, v := range playCountMap { + if _, existed := docM[vid]; !existed { + docM[vid] = &VideoStatDoc{} + } + val := v + docM[vid].PlayCount = &val + } + return nil +} + +// fillPayPlayCount +func fillPayPlayCount(timeRange TimeRange, docMap *VideoStatDocMap) error { + payPlayCountMap, err := playlgmod.PlayCountMapByTime(timeRange.Head, timeRange.Tail, []int{1}) //0,免费 1付费 2.试看 + if err != nil { + return err + } + docM := *docMap + for vid, v := range payPlayCountMap { + if _, existed := docM[vid]; !existed { + docM[vid] = &VideoStatDoc{} + } + val := v + docM[vid].PayPlayCount = &val + } + return nil +} + +func fillTax(timeRange TimeRange, docMap *VideoStatDocMap) error { + payPlayCountMap, err := payvidlgmod.TaxAmountMapByTime(timeRange.Head, timeRange.Tail) //0,免费 1付费 2.试看 + if err != nil { + return err + } + docM := *docMap + for vid, v := range payPlayCountMap { + if _, existed := docM[vid]; !existed { + docM[vid] = &VideoStatDoc{} + } + val := v + docM[vid].Tax = &val + } + return nil +} + +// fillStatDocMap +func fillStatDocMap(sumDate time.Time, timeRange TimeRange, docMap *VideoStatDocMap) error { + //收入 + if err := fillPayCoin(timeRange, docMap); err != nil { + return err + } + //购买次数 + if err := fillPayCount(timeRange, docMap); err != nil { + return err + } + //视屏播放次数 + if err := fillPlayCount(timeRange, docMap); err != nil { + return err + } + //付费视屏播放次数 + if err := fillPayPlayCount(timeRange, docMap); err != nil { + return err + } + //税 + return fillTax(timeRange, docMap) +} diff --git a/skd/job/stat/video/stat.go b/skd/job/stat/video/stat.go new file mode 100644 index 0000000..74e2e01 --- /dev/null +++ b/skd/job/stat/video/stat.go @@ -0,0 +1,97 @@ +package video + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/s/statvidmod" + "91porn-server/skd/skdg" +) + +func RunStat(defaultStatDate time.Time) { + log.Info("video stat running...") + defer log.Info("video stat finished...") + now := time.Now() + //从历史提交记录中获取上次记录时间 + lastStatAt, err := statrecordmod.LastRecordTime(statrecordmod.VideoStatJob, defaultStatDate) //第一次从默认记录时间开始统计 + if err != nil { + log.Error("video stat RunVideoStat lastRecordTime faild!", log.E(err)) + return + } + //过去几天的数据按一日为单位统计,帮助数据恢复 + dayHead := timerange.LocDayRange(now).Head + for dayHead.After(lastStatAt) { + end := lastStatAt.AddDate(0, 0, 1) + //防止时间超出范围 + if end.After(dayHead) { + end = dayHead + } + subTimeRange := timerange.TimeRange{ //连续子切片 + Head: lastStatAt, + Tail: end, + } + if err = statByTimeRange(subTimeRange); err != nil { + log.Error("video RunStat statByTimeRange faild!", log.E(err)) + return + } + lastStatAt = end + } + //当天的数据按照5分钟统计 + recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) //对齐本次统计时间 Minute % frequency == 0 + //拆分时间区间并依次提交,控制数据库读写压力 + for recentMinute.After(lastStatAt) { + end := lastStatAt.Add(statrecordmod.OneMinuteScale * time.Minute) + //防止时间超出范围 + if end.After(recentMinute) { + end = recentMinute + } + subTimeRange := timerange.TimeRange{ //连续子切片 + Head: lastStatAt, + Tail: end, + } + if err = statByTimeRange(subTimeRange); err != nil { + log.Error("video RunStat statByTimeRange faild!", log.E(err)) + return + } + lastStatAt = end + } +} + +func statByTimeRange(timeRange timerange.TimeRange) error { + sumDate := timerange.LocDayRange(timeRange.Head).Head //获取此时的零日点 + //装配基础统计数据 + statDocMap := make(VideoStatDocMap) + if err := fillStatDocMap(sumDate, timeRange, &statDocMap); err != nil { + log.Error("video stat RunVideoStat fillStatDocMap faild!", log.E(err)) + return err + } + //提交渠道统计数据 + log.Info("videoStat committing...", log.Any("sumDate", sumDate), log.Any("location", sumDate.Location().String())) + if err := commit(sumDate, timeRange.Tail, statDocMap); err != nil { + log.Error("video stat RunVideoStat commit faild!", log.E(err)) + return err + } + log.Info("videoStat committed.", log.Any("sumDate", sumDate), log.Any("location", sumDate.Location().String())) + return nil +} + +// commit 提交统计数据 +func commit(sumDate time.Time, recordTime time.Time, docMap VideoStatDocMap) error { + opt := (&db.TransOpts{}).SetReEntry(10) + return skdg.StatDB.Trans(func(trans *db.MongoTool) error { + if len(docMap) != 0 { + if err := statvidmod.ChangeStatTrans(trans, sumDate, recordTime, docMap); err != nil { + return fmt.Errorf("video stat commit ChangeStatTrans faild!, err:%+v\n", err) + } + } + //提交本次修改记录 + if err := statrecordmod.UpsertOneTrans(trans, statrecordmod.VideoStatJob, recordTime); err != nil { + return fmt.Errorf("video stat commit UpsertOneTrans faild!, err:%+v\n", err) + } + return nil + }, opt) +} diff --git a/skd/job/stat/vidtotal/method.go b/skd/job/stat/vidtotal/method.go new file mode 100644 index 0000000..65a5616 --- /dev/null +++ b/skd/job/stat/vidtotal/method.go @@ -0,0 +1,46 @@ +package vidtotal + +import ( + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statvidmod" + "91porn-server/models/s/statvidtotalmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +type TimeRange = timerange.TimeRange + +type VideoTotalIncDoc = statvidtotalmod.IncDoc + +type VideoTotalIncDocMap = statvidtotalmod.IncDocMap + +type RecordAtGTEAndLTMatch = statvidmod.RecordAtGTEAndLTMatch + +func getVideoTotalIncDocMap(timeRange TimeRange) (VideoTotalIncDocMap, error) { + recordAtGTEAndLTMatch := RecordAtGTEAndLTMatch{GTE: &timeRange.Head, LT: &timeRange.Tail} + groupList, err := statvidmod.SumByRecordAt(recordAtGTEAndLTMatch) + if err != nil { + return nil, err + } + m := toVideoTotalIncDocMap(groupList) + return m, nil +} + +func toVideoTotalIncDocMap(groupList []statvidmod.SumGroup) VideoTotalIncDocMap { + m := make(VideoTotalIncDocMap, len(groupList)) + for _, v := range groupList { + _, ok := m[v.Vid] + if !ok { + m[v.Vid] = &VideoTotalIncDoc{} + } + doc := m[v.Vid] + doc.Income = v.Income + doc.PayCount = v.PayCount + doc.PlayCount = v.PlayCount + doc.PayPlayCount = v.PayPlayCount + doc.Tax = v.Tax + } + return m +} diff --git a/skd/job/stat/vidtotal/stat.go b/skd/job/stat/vidtotal/stat.go new file mode 100644 index 0000000..38620f2 --- /dev/null +++ b/skd/job/stat/vidtotal/stat.go @@ -0,0 +1,96 @@ +package vidtotal + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/s/statvidtotalmod" + "91porn-server/skd/skdg" +) + +func RunStat(defaultStatDate time.Time) { + log.Info("video total stat running...") + defer log.Info("video total stat finished...") + now := time.Now() + //从历史提交记录中获取上次记录时间 + lastStatAt, err := statrecordmod.LastRecordTime(statrecordmod.VideoTotalStatJob, defaultStatDate) //第一次从默认记录时间开始统计 + if err != nil { + log.Error("video total stat RunVideoStat lastRecordTime faild!", log.E(err)) + return + } + //过去几天的数据按一日为单位统计,帮助数据恢复 + dayHead := timerange.LocDayRange(now).Head + for dayHead.After(lastStatAt) { + end := lastStatAt.AddDate(0, 0, 1) + //防止时间超出范围 + if end.After(dayHead) { + end = dayHead + } + subTimeRange := timerange.TimeRange{ //连续子切片 + Head: lastStatAt, + Tail: end, + } + if err = statByTimeRange(subTimeRange); err != nil { + log.Error("video total RunStat statByTimeRange faild!", log.E(err)) + return + } + lastStatAt = end + } + //当天的数据按照5分钟统计 + recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) //对齐本次统计时间 Minute % frequency == 0 + //拆分时间区间并依次提交,控制数据库读写压力 + for recentMinute.After(lastStatAt) { + end := lastStatAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + //防止时间超出范围 + if end.After(recentMinute) { + end = recentMinute + } + subTimeRange := timerange.TimeRange{ //连续子切片 + Head: lastStatAt, + Tail: end, + } + if err = statByTimeRange(subTimeRange); err != nil { + log.Error("video total RunStat statByTimeRange faild!", log.E(err)) + return + } + lastStatAt = end + } +} + +func statByTimeRange(timeRange timerange.TimeRange) error { + //装配基础统计数据 + statDocMap, err := getVideoTotalIncDocMap(timeRange) + if err != nil { + log.Error("video total stat RunVideoStat fillStatDocMap faild!", log.E(err)) + return err + } + //提交视屏总计数据 + log.Info("videoTotalStat committing...") + if err = commit(timeRange.Tail, statDocMap); err != nil { + log.Error("video total stat RunVideoStat commit faild!", log.E(err)) + return err + } + log.Info("videoTotalStat committed.") + return nil +} + +// commit 提交统计数据 +func commit(recordTime time.Time, docMap VideoTotalIncDocMap) error { + opt := (&db.TransOpts{}).SetReEntry(10) + return skdg.StatDB.Trans(func(trans *db.MongoTool) error { + if len(docMap) != 0 { + if err := statvidtotalmod.ChangeStatTrans(trans, recordTime, docMap); err != nil { + return fmt.Errorf("video total stat commit ChangeStatTrans faild!, err:%+v\n", err) + } + } + //提交本次修改记录 + if err := statrecordmod.UpsertOneTrans(trans, statrecordmod.VideoTotalStatJob, recordTime); err != nil { + return fmt.Errorf("video total stat commit UpsertOneTrans faild!, err:%+v\n", err) + } + return nil + }, opt) +} diff --git a/skd/job/stat/withorder/method.go b/skd/job/stat/withorder/method.go new file mode 100644 index 0000000..ee0b06e --- /dev/null +++ b/skd/job/stat/withorder/method.go @@ -0,0 +1,139 @@ +package withorder + +import ( + "time" + + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statordermod" + "91porn-server/models/v/wdordmod" +) + +type WithDrawOrderStateDoc = statordermod.WithDrawOrderStateDoc +type WitOrderStateMap = statordermod.WitOrderStateMap + +func fillWitOrderStatDocMap(sumDate time.Time, timeRange timerange.TimeRange, docMap *WitOrderStateMap) error { + //订单数 OrderCount + if err := fillWitOrderCount(timeRange, docMap); err != nil { + return err + } + //完成订单数 CompleteOrderCount + if err := fillWitCompleteOrderCount(timeRange, docMap); err != nil { + return err + } + //订单金额 OrderAmount + if err := fillWitOrderAmount(timeRange, docMap); err != nil { + return err + } + //实际兑换金额 CompleteOrderAmount + if err := fillWitCompleteOrderPayAmount(timeRange, docMap); err != nil { + return err + } + //三方已出款订单数 fillWitPaiedOrderCount + if err := fillWitPaiedOrderCount(timeRange, docMap); err != nil { + return err + } + //三方已出款金额 fillWitPaiedMoney + return fillWitPaiedMoney(timeRange, docMap) +} + +// fillWitOrderCount fillWitOrderCount +func fillWitOrderCount(timeRange timerange.TimeRange, docMap *WitOrderStateMap) error { + orderCountMap, err := wdordmod.ChannelOrderCountMap(timeRange.Head, timeRange.Tail) + if err != nil { + return err + } + docM := *docMap + for channel, v := range orderCountMap { + if _, existed := docM[channel]; !existed { + docM[channel] = &WithDrawOrderStateDoc{} + } + val := v + docM[channel].OrderCount = &val + } + return nil +} + +// fillWitCompleteOrderCount fillWitCompleteOrderCount +func fillWitCompleteOrderCount(timeRange timerange.TimeRange, docMap *WitOrderStateMap) error { + completeOrderCountMap, err := wdordmod.ChannelCompleteOrderCountMap(timeRange.Head, timeRange.Tail) + if err != nil { + return err + } + docM := *docMap + for channel, v := range completeOrderCountMap { + if _, existed := docM[channel]; !existed { + docM[channel] = &WithDrawOrderStateDoc{} + } + val := v + docM[channel].CompleteOrderCount = &val + } + return nil +} + +// fillWitOrderAmount fillWitOrderAmount +func fillWitOrderAmount(timeRange timerange.TimeRange, docMap *WitOrderStateMap) error { + orderAmountMap, err := wdordmod.ChannelOrderMoneyMap(timeRange.Head, timeRange.Tail) + if err != nil { + return err + } + docM := *docMap + for channel, v := range orderAmountMap { + if _, existed := docM[channel]; !existed { + docM[channel] = &WithDrawOrderStateDoc{} + } + val := v + docM[channel].OrderAmount = &val + } + return nil +} + +// fillWitCompleteOrderPayAmount fillWitCompleteOrderPayAmount +func fillWitCompleteOrderPayAmount(timeRange timerange.TimeRange, docMap *WitOrderStateMap) error { + completeOrderAmountMap, err := wdordmod.ChannelCompleteOrderPayMoneyMap(timeRange.Head, timeRange.Tail) + if err != nil { + return err + } + docM := *docMap + for channel, v := range completeOrderAmountMap { + if _, existed := docM[channel]; !existed { + docM[channel] = &WithDrawOrderStateDoc{} + } + val := v + docM[channel].CompleteOrderAmount = &val + } + return nil +} + +// fillWitPaiedOrderCount fillWitPaiedOrderCount +func fillWitPaiedOrderCount(timeRange timerange.TimeRange, docMap *WitOrderStateMap) error { + receivedOrderCountMap, err := wdordmod.ChannelReceivedOrderCountMap(timeRange.Head, timeRange.Tail) + if err != nil { + return err + } + docM := *docMap + for channel, v := range receivedOrderCountMap { + if _, existed := docM[channel]; !existed { + docM[channel] = &WithDrawOrderStateDoc{} + } + val := v + docM[channel].PaidOrderCount = &val + } + return nil +} + +// fillWitPaiedMoney fillWitPaiedMoney +func fillWitPaiedMoney(timeRange timerange.TimeRange, docMap *WitOrderStateMap) error { + receivedMoneyMap, err := wdordmod.ChannelReceivedMoneyMap(timeRange.Head, timeRange.Tail) + if err != nil { + return err + } + docM := *docMap + for channel, v := range receivedMoneyMap { + if _, existed := docM[channel]; !existed { + docM[channel] = &WithDrawOrderStateDoc{} + } + val := v + docM[channel].PaidAmount = &val + } + return nil +} diff --git a/skd/job/stat/withorder/stat.go b/skd/job/stat/withorder/stat.go new file mode 100644 index 0000000..97c64f8 --- /dev/null +++ b/skd/job/stat/withorder/stat.go @@ -0,0 +1,97 @@ +package withorder + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statordermod" + "91porn-server/models/s/statrecordmod" + "91porn-server/skd/skdg" +) + +func RunStat(defaultStatDate time.Time) { + log.Info("withorder stat running...") + defer log.Info("withorder stat finished...") + now := time.Now() + //从历史提交记录中获取上次记录时间 + lastStatAt, err := statrecordmod.LastRecordTime(statrecordmod.WithdrawOrderStatJob, defaultStatDate) //第一次从默认记录时间开始统计 + if err != nil { + log.Error("withorder RunStat lastRecordTime faild!", log.E(err)) + return + } + //过去几天的数据按一日为单位统计,帮助数据恢复 + dayHead := timerange.LocDayRange(now).Head + for dayHead.After(lastStatAt) { + end := lastStatAt.AddDate(0, 0, 1) + //防止时间超出范围 + if end.After(dayHead) { + end = dayHead + } + subTimeRange := timerange.TimeRange{ //连续子切片 + Head: lastStatAt, + Tail: end, + } + if err = statByTimeRange(subTimeRange); err != nil { + log.Error("withorder RunStat statByTimeRange 1 faild!", log.E(err)) + return + } + lastStatAt = end + } + //当天的数据按照5分钟统计 + recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) //对齐本次统计时间 Minute % frequency == 0 + //拆分时间区间并依次提交,控制数据库读写压力 + for recentMinute.After(lastStatAt) { + end := lastStatAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + //防止时间超出范围 + if end.After(recentMinute) { + end = recentMinute + } + subTimeRange := timerange.TimeRange{ //连续子切片 + Head: lastStatAt, + Tail: end, + } + if err = statByTimeRange(subTimeRange); err != nil { + log.Error("withorder RunStat statByTimeRange 2 faild!", log.E(err)) + return + } + lastStatAt = end + } +} + +func statByTimeRange(timeRange timerange.TimeRange) error { + sumDate := timerange.LocDayRange(timeRange.Head).Head //获取此时的零日点 + //装配渠道统计数据 + witOrderStateMap := make(WitOrderStateMap) + if err := fillWitOrderStatDocMap(sumDate, timeRange, &witOrderStateMap); err != nil { + log.Error("withorder RunStat fillWitOrderStatDocMap faild!", log.E(err)) + return err + } + //提交渠道统计数据 + log.Info("withorderStat withdraw committing...", log.Any("sumDate", sumDate), log.Any("location", sumDate.Location().String())) + if err := commitWit(sumDate, timeRange.Tail, witOrderStateMap); err != nil { + log.Error("withorder RunStat commitWit faild!", log.E(err)) + return err + } + log.Info("withorderStat withdraw committed.", log.Any("sumDate", sumDate), log.Any("location", sumDate.Location().String())) + return nil +} + +// commit 提交订单统计数据 +func commitWit(sumDate time.Time, recordTime time.Time, stateMap WitOrderStateMap) error { + opt := (&db.TransOpts{}).SetReEntry(10) + return skdg.StatDB.Trans(func(trans *db.MongoTool) error { + if len(stateMap) != 0 { + if err := statordermod.ChangeWithDrawStatTrans(trans, sumDate, recordTime, stateMap); err != nil { + return fmt.Errorf("stat commitWit ChangeWithDrawStatTrans faild!, err:%+v\n", err) + } + } + //提交本次修改记录 + if err := statrecordmod.UpsertOneTrans(trans, statrecordmod.WithdrawOrderStatJob, recordTime); err != nil { + return fmt.Errorf("stat commitWit UpsertOneTrans faild!, err:%+v\n", err) + } + return nil + }, opt) +} diff --git a/skd/job/sync.go b/skd/job/sync.go new file mode 100644 index 0000000..c15c1ea --- /dev/null +++ b/skd/job/sync.go @@ -0,0 +1,74 @@ +package job + +import ( + "91porn-server/skd/job/elastic/esmedia" + "91porn-server/skd/job/elastic/esuser" + "sync" + "time" + + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/skd/job/elastic/estag" + "91porn-server/skd/job/elastic/esvideo" + "91porn-server/skd/skdg" +) + +var syncIsRuining = false + +// start 定时任务 开启 +func runSync() { + //防止重入 + if syncIsRuining { + return + } + syncIsRuining = true + defer func() { + syncIsRuining = false + }() + doSync() //做统计业务 +} + +// 做统计业务 +func doSync() { + defaultSyncDate, err := getDefaultSyncDate() + if err != nil { + log.Error("defaultStatDate invalid", log.E(err)) + return + } + wg := sync.WaitGroup{} + wg.Add(5) + // 同步数据 + { + common.Go(func() { + defer wg.Done() + esuser.RunSync(defaultSyncDate) + }) + common.Go(func() { + defer wg.Done() + esvideo.RunSync(defaultSyncDate) + }) + common.Go(func() { + defer wg.Done() + esmedia.RunSync(defaultSyncDate) + }) + common.Go(func() { + defer wg.Done() + estag.RunSync(defaultSyncDate) + }) + + common.Go(func() { + defer wg.Done() + vidDate, err := time.ParseInLocation("2006-01-02", "2023-09-22", time.Local) + if err == nil { + defaultSyncDate = vidDate + } + // 同步短视频数据到REDIS + //rdvideo.RunSync(defaultSyncDate) + }) + } + wg.Wait() +} + +func getDefaultSyncDate() (time.Time, error) { + return time.ParseInLocation("2006-01-02", skdg.Conf.StartDate, time.Local) +} diff --git a/skd/job/sync_im_users.go b/skd/job/sync_im_users.go new file mode 100644 index 0000000..3d1ff42 --- /dev/null +++ b/skd/job/sync_im_users.go @@ -0,0 +1,176 @@ +package job + +import ( + "encoding/json" + "fmt" + "time" + + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/skd/skdg" + "91porn-server/services/srv_im" + "91porn-server/web/webg" +) + +const ( + phaseQueued = "queued" + phaseScanningDialogs = "scanning_chat_sessions" + phaseRegisteringUsers = "registering_users" + phaseAddingFriends = "adding_friends" + phaseDone = "done" + + syncIMUsersTriggerKey = "im:sync_users:trigger" + syncIMUsersLockKey = "im:sync_users:lock" + syncIMUsersStatusKey = "im:sync_users:status" + + syncIMUsersTriggerTTL = 10 * time.Minute + syncIMUsersLockTTL = 1 * time.Hour +) + +// SyncIMUsersTaskStatus 旧私聊会话用户同步任务状态。 +// 状态完全落在 Redis 里——web 进程写 trigger、读 status;skd 进程消费 trigger、执行任务、写 status。 +type SyncIMUsersTaskStatus struct { + Running bool `json:"running"` + Phase string `json:"phase,omitempty"` + StartedAt *time.Time `json:"startedAt,omitempty"` + FinishedAt *time.Time `json:"finishedAt,omitempty"` + LastStats srv_im.SyncDialogUsersStats `json:"lastStats"` + LastError string `json:"lastError,omitempty"` +} + +// imSyncRedis 拿当前进程可用的 Redis 客户端: +// skd 主进程用 skdg.Redis;web 主进程用 webg.Redis。 +// 两端写的是同一个 Redis 实例,靠 key 协议互通。 +func imSyncRedis() *redis.Client { + if skdg.Redis != nil { + return skdg.Redis + } + if webg.Redis != nil { + return webg.Redis + } + return nil +} + +// RequestSyncIMUsers 由 web admin 接口调用:写 trigger key,由 skd 接管执行。 +// 返回 accepted=true 表示成功排队;false 表示已有任务在跑或在排队。 +func RequestSyncIMUsers() (accepted bool, status SyncIMUsersTaskStatus) { + status = GetSyncIMUsersStatus() + if status.Running { + return false, status + } + r := imSyncRedis() + if r == nil { + status.LastError = "redis unavailable" + return false, status + } + if existing, _ := r.Get(syncIMUsersTriggerKey); existing != nil && *existing != "" { + // 已经有 trigger 在排队,等 skd 消费 + return false, status + } + if err := r.Set(syncIMUsersTriggerKey, "1", syncIMUsersTriggerTTL); err != nil { + status.LastError = err.Error() + return false, status + } + now := time.Now() + status = SyncIMUsersTaskStatus{ + Phase: phaseQueued, + StartedAt: &now, + LastStats: srv_im.SyncDialogUsersStats{Enabled: true}, + } + saveSyncIMUsersStatus(status) + return true, status +} + +// GetSyncIMUsersStatus 读 Redis 里最新状态,任何进程都能调 +func GetSyncIMUsersStatus() SyncIMUsersTaskStatus { + r := imSyncRedis() + if r == nil { + return SyncIMUsersTaskStatus{} + } + raw, err := r.Get(syncIMUsersStatusKey) + if err != nil || raw == nil || *raw == "" { + return SyncIMUsersTaskStatus{} + } + var status SyncIMUsersTaskStatus + if err := json.Unmarshal([]byte(*raw), &status); err != nil { + return SyncIMUsersTaskStatus{} + } + return status +} + +// RunPendingSyncIMUsers 由 skd cron 定时调用:检测 trigger 并接管执行同步 +func RunPendingSyncIMUsers() { + r := imSyncRedis() + if r == nil { + return + } + // 没有 trigger 就直接退 + trig, _ := r.Get(syncIMUsersTriggerKey) + if trig == nil || *trig == "" { + return + } + // 加锁防止 skd 多副本同时跑 + ok, err := r.Setnx_NewOK(syncIMUsersLockKey, 1, syncIMUsersLockTTL) + if err != nil || !ok { + return + } + defer func() { _, _ = r.Del(syncIMUsersLockKey) }() + // 消费 trigger,避免下个 tick 重复拉起 + _, _ = r.Del(syncIMUsersTriggerKey) + + now := time.Now() + status := SyncIMUsersTaskStatus{ + Running: true, + Phase: phaseScanningDialogs, + StartedAt: &now, + LastStats: srv_im.SyncDialogUsersStats{Enabled: true}, + } + saveSyncIMUsersStatus(status) + + stats, runErr := runSyncIMUsersOnce(&status) + + finished := time.Now() + status.Running = false + status.Phase = phaseDone + status.FinishedAt = &finished + status.LastStats = stats + if runErr != nil { + status.LastError = runErr.Error() + log.Error("RunPendingSyncIMUsers failed", log.E(runErr), log.Any("stats", stats)) + } else { + status.LastError = "" + log.Info("RunPendingSyncIMUsers done", log.Any("stats", stats)) + } + saveSyncIMUsersStatus(status) +} + +func runSyncIMUsersOnce(status *SyncIMUsersTaskStatus) (stats srv_im.SyncDialogUsersStats, err error) { + defer func() { + if rec := recover(); rec != nil { + err = fmt.Errorf("sync panic: %v", rec) + } + }() + status.Phase = phaseRegisteringUsers + saveSyncIMUsersStatus(*status) + + stats, err = srv_im.SyncDialogUsers() + + status.Phase = phaseAddingFriends + status.LastStats = stats + saveSyncIMUsersStatus(*status) + return stats, err +} + +func saveSyncIMUsersStatus(status SyncIMUsersTaskStatus) { + r := imSyncRedis() + if r == nil { + return + } + raw, err := json.Marshal(status) + if err != nil { + return + } + if err := r.Set(syncIMUsersStatusKey, string(raw), 0); err != nil { + log.Warn("saveSyncIMUsersStatus set failed", log.E(err)) + } +} diff --git a/skd/job/syncspiderdata/audiobookspider/method.go b/skd/job/syncspiderdata/audiobookspider/method.go new file mode 100644 index 0000000..bb0dc05 --- /dev/null +++ b/skd/job/syncspiderdata/audiobookspider/method.go @@ -0,0 +1,147 @@ +package audiobookspider + +import ( + "fmt" + "time" + + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/models/v/audioanchormod" + "91porn-server/models/v/audiobookmod" + "91porn-server/skd/skdg" +) + +func SyncDataFromSpider(data []AudioBook) error { + dataLen := len(data) + if dataLen == 0 { + return nil + } + now := time.Now() + la := make([]audiobookmod.AudioBookUpsertEdit, dataLen) + laa := make([]audioanchormod.EditSelector, dataLen) + for i := range data { + temp := audiobookmod.AudioBookUpsertEdit{ + Title: data[i].Title, + Anchor: data[i].Announcer, + Cover: data[i].Cover, + FType: data[i].Category, + Summary: data[i].Desc, + Number: data[i].Id, + UpdatedAt: now, + } + temp.TotalEpisode = len(data[i].EpisodeList) + if data[i].Status == 1 { + temp.IsActive = true + } else { + temp.IsActive = false + } + if data[i].Announcer == "" { + temp.IsActive = false + } + temp.ContentSet = make([]audiobookmod.Content, len(data[i].EpisodeList)) + for di := range data[i].EpisodeList { + temp.ContentSet[di] = audiobookmod.Content{ + EpisodeNumber: di + 1, + ListenPermission: 0, + Name: data[i].EpisodeList[di].Name, + URL: data[i].EpisodeList[di].FileName, + MediaSize: data[i].EpisodeList[di].Size, + } + } + temp.TrialSet = make([]audiobookmod.Content, len(data[i].TrialEpisodeList)) + for di := range data[i].TrialEpisodeList { + temp.TrialSet[di] = audiobookmod.Content{ + EpisodeNumber: di + 1, + ListenPermission: 3, + Name: data[i].TrialEpisodeList[di].Name, + URL: data[i].TrialEpisodeList[di].FileName, + MediaSize: data[i].TrialEpisodeList[di].Size, + } + } + tempa := audioanchormod.EditSelector{ + Name: data[i].Announcer, + Avatar: data[i].AnnouncerCover, + UpdatedAt: now} + la[i] = temp + laa[i] = tempa + } + + log.Info("audiobook sync audiobookSpider data committing...") + if err := audiobookmod.InsertBulket(la); err != nil { + log.Info("audiobook sync audiobookSpider data commit failed.", log.E(err)) + return err + } + if err := audioanchormod.InsertBulket(laa); err != nil { + log.Info("audiobook sync audiobookSpider data commit failed.", log.E(err)) + return err + } + log.Info("audiobook sync audiobookSpider data committed.") + return nil +} + +type Episode struct { + Name string `json:"name" bson:"name" ` //集数名称 + Size int64 `json:"size" bson:"size" ` //本集mp3文件大小 + FileName string `json:"fileName" bson:"fileName"` //小说文件 +} + +type AudioBook struct { + Id int64 `json:"id" bson:"_id"` + SpiderId int64 `json:"spiderId" bson:"spiderId"` //爬虫库的表Id + Timestamp int64 `json:"timestamp" bson:"timestamp" ` //时间戳 + Title string `json:"title" bson:"title"` //标题 + Announcer string `json:"announcer" bson:"announcer"` //播音员 + Category string `json:"category" bson:"category"` //分类 + Desc string `json:"desc" bson:"desc"` //描述 + Cover string `json:"cover" bson:"cover" ` //封面 + AnnouncerCover string `json:"announcerCover"` //播音员封面 + EpisodeList []Episode `json:"episodeList" bson:"episodeList" ` //集数列表 + TrialEpisodeList []Episode `json:"trialEpisodeList" bson:"trialEpisodeList"` //试听集数列表 + Status int `json:"status" bson:"status"` //状态 1:上架 2:下架 + CreatedAt time.Time `json:"createdAt" bson:"createdAt" ` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt" ` //更新时间 +} + +// /api/stat/novel/audio/sync POST +type SyncAudioBookReq struct { + SyncSize int64 `json:"syncSize" binding:"required"` + StartTime time.Time `json:"startTime" binding:"required"` +} + +type SyncAudioBookResp struct { + Data []AudioBook `json:"data"` +} + +// todo skd +// 获取ES同步数据 +func GetListByUpdateTimeRange_net(start time.Time, end time.Time) (data []AudioBook, err error) { + p := SyncAudioBookReq{ + SyncSize: 10000, + StartTime: start, + } + var msg struct { + Code int `json:"code"` + Data SyncAudioBookResp `json:"data"` + } + url := skdg.Conf.Url.SpiderDataURL + "/api/stat/novel/audio/sync" + code, err := httputil.DefaultClientPostJsonWithResp(&msg, url, nil, p) + if err != nil || code != 200 { + log.Error(fmt.Sprintf("[METHOD-%s]==> %s fail error:%+v:", "GetListByUpdateTimeRange_net", "POSTJsonWithJResp", err), + log.Any("start", start), + log.Any("url", end), + log.Any("code", code), + ) + return + } + if msg.Code != 200 { + log.Error(fmt.Sprintf("[METHOD-%s]==> %s fail error:%+v:", "GetListByUpdateTimeRange_net", "GetWithJResp", err), + log.Any("start", start), + log.Any("url", end), + log.Any("code", code), + log.Any("msg", msg), + ) + return + } + data = msg.Data.Data + return +} diff --git a/skd/job/syncspiderdata/audiobookspider/sync.go b/skd/job/syncspiderdata/audiobookspider/sync.go new file mode 100644 index 0000000..2c394c2 --- /dev/null +++ b/skd/job/syncspiderdata/audiobookspider/sync.go @@ -0,0 +1,56 @@ +package audiobookspider + +import ( + "fmt" + "time" + + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" +) + +func RunSync(defaultDate time.Time) { + log.Info("Audiobook sync audiobookSpider data start...") + defer log.Info("Audiobook sync audiobookSpider data end.") + // 获取上次数据同步的记录时间 + lastSyncAt, err := statrecordmod.LastRecordTime(statrecordmod.SpiderSyncAudiobookJob, defaultDate) + if err != nil { + log.Error("Audiobook sync audiobookSpider data RunSync get last record time failed", log.E(err)) + return + } + var now = time.Now().Add(-time.Minute * 10) + var recentMinute = timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) + for recentMinute.After(lastSyncAt) { + var end = lastSyncAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + if end.After(recentMinute) { + end = recentMinute + } + var subTimeRange = timerange.TimeRange{ + Head: lastSyncAt, + Tail: end, + } + data, err := GetListByUpdateTimeRange_net(subTimeRange.Head, subTimeRange.Tail) + if err != nil { + return + } + if err = commit(data, subTimeRange.Tail); err != nil { + log.Error("Audiobook sync audiobookSpider commit err! ", log.E(err)) + return + } + if len(data) > 0 { + lastSyncAt = data[len(data)-1].UpdatedAt + } else { + lastSyncAt = end + } + } +} + +func commit(data []AudioBook, syncTime time.Time) error { + if err := SyncDataFromSpider(data); err != nil { + return fmt.Errorf("Audiobook sync audiobookSpider commit audiobook data SyncDataFromSpider failed!, err:%+v\n", err) + } + if err := statrecordmod.UpsertOneTrans(nil, statrecordmod.SpiderSyncAudiobookJob, syncTime); err != nil { + return fmt.Errorf("Audiobook sync audiobookSpider commit audiobook data UpsertOneTrans failed!, err:%+v\n", err) + } + return nil +} diff --git a/skd/job/syncspiderdata/fictionspider/method.go b/skd/job/syncspiderdata/fictionspider/method.go new file mode 100644 index 0000000..47f577c --- /dev/null +++ b/skd/job/syncspiderdata/fictionspider/method.go @@ -0,0 +1,99 @@ +package fictionspider + +import ( + "fmt" + "time" + + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/models/v/fictionmod" + "91porn-server/skd/skdg" +) + +func SyncDataFromSpider(data []Novel) error { + now := time.Now() + if len(data) == 0 { + return nil + } + ll := make([]fictionmod.FictionUpsert, len(data)) + for i := range data { + temp := fictionmod.FictionUpsert{ + Title: data[i].Title, + FType: data[i].Type, + Summary: data[i].Desc, + ContentURL: data[i].FileName, + Number: data[i].Id, + UpdatedAt: now, + } + if data[i].Status == 1 { + temp.IsActive = true + } else { + temp.IsActive = false + } + ll[i] = temp + } + log.Info("fiction sync fictionSpider data committing...") + if err := fictionmod.InsertBulket(ll); err != nil { + log.Info("fiction sync fictionSpider data commit failed.") + return err + } + log.Info("fiction sync fictionSpider data committed.") + return nil +} + +type Novel struct { + Id int64 `json:"id" bson:"_id"` + SpiderId int64 `json:"spiderId" bson:"spiderId"` //爬虫库的表Id + Title string `json:"title" bson:"title" ` //小说标题 + Type string `json:"type" bson:"type" ` //小说类型 + Desc string `json:"desc" bson:"desc" ` //描述 + FileName string `json:"fileName" bson:"fileName" ` //小说文件 + Timestamp int64 `json:"timestamp" bson:"timestamp" ` //时间戳 + Status int `json:"status" bson:"status"` //状态 1:上架 2:下架 + CreatedAt time.Time `json:"createdAt" bson:"createdAt" ` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //更新时间 +} + +// /api/stat/novel/sync POST +type SyncNovelReq struct { + SyncSize int64 `json:"syncSize" binding:"required"` + StartTime time.Time `json:"startTime" binding:"required"` +} + +type SyncNovelResp struct { + Data []Novel `json:"data"` +} + +// todo skd +// 获取ES同步数据 +func GetListByUpdateTimeRange_net(start time.Time, end time.Time) (data []Novel, err error) { + p := SyncNovelReq{ + SyncSize: 10000, + StartTime: start, + } + var msg struct { + Code int `json:"code"` + Data SyncNovelResp `json:"data"` + } + url := skdg.Conf.Url.SpiderDataURL + "/api/stat/novel/sync" + code, err := httputil.DefaultClientPostJsonWithResp(&msg, url, nil, p) + if err != nil || code != 200 { + log.Error(fmt.Sprintf("[METHOD-%s]==> %s fail error:%+v:", "GetListByUpdateTimeRange_net", "GetWithJResp", err), + log.Any("start", start), + log.Any("url", end), + log.Any("code", code), + ) + return + } + if msg.Code != 200 { + log.Error(fmt.Sprintf("[METHOD-%s]==> %s fail error:%+v:", "GetListByUpdateTimeRange_net", "GetWithJResp", err), + log.Any("start", start), + log.Any("url", end), + log.Any("code", code), + log.Any("msg", msg), + ) + return + } + data = msg.Data.Data + return +} diff --git a/skd/job/syncspiderdata/fictionspider/sync.go b/skd/job/syncspiderdata/fictionspider/sync.go new file mode 100644 index 0000000..65e52bd --- /dev/null +++ b/skd/job/syncspiderdata/fictionspider/sync.go @@ -0,0 +1,56 @@ +package fictionspider + +import ( + "fmt" + "time" + + "91porn-server/common/log" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/s/statrecordmod" +) + +func RunSync(defaultDate time.Time) { + log.Info("fiction sync fictionSpider data start...") + defer log.Info("fiction sync fictionSpider data end.") + // 获取上次数据同步的记录时间 + lastSyncAt, err := statrecordmod.LastRecordTime(statrecordmod.SpiderSyncfictionJob, defaultDate) + if err != nil { + log.Error("fiction sync fictionSpider data RunSync get last record time failed", log.E(err)) + return + } + var now = time.Now().Add(-time.Minute * 10) + var recentMinute = timerange.RecentMinute(now, statrecordmod.FiveMinuteScale) + for recentMinute.After(lastSyncAt) { + var end = lastSyncAt.Add(statrecordmod.FiveMinuteScale * time.Minute) + if end.After(recentMinute) { + end = recentMinute + } + var subTimeRange = timerange.TimeRange{ + Head: lastSyncAt, + Tail: end, + } + data, err := GetListByUpdateTimeRange_net(subTimeRange.Head, subTimeRange.Tail) + if err != nil { + return + } + if err = commit(data, subTimeRange.Tail); err != nil { + log.Error("fiction sync fictionSpider commit err! ", log.E(err)) + return + } + if len(data) > 0 { + lastSyncAt = data[len(data)-1].UpdatedAt + } else { + lastSyncAt = end + } + } +} + +func commit(data []Novel, syncTime time.Time) error { + if err := SyncDataFromSpider(data); err != nil { + return fmt.Errorf("fiction sync fictionSpider commit fiction data SyncDataFromSpider failed!, err:%+v\n", err) + } + if err := statrecordmod.UpsertOneTrans(nil, statrecordmod.SpiderSyncfictionJob, syncTime); err != nil { + return fmt.Errorf("fiction sync fictionSpider commit fiction data UpsertOneTrans failed!, err:%+v\n", err) + } + return nil +} diff --git a/skd/job/user/privilege.go b/skd/job/user/privilege.go new file mode 100644 index 0000000..58284e4 --- /dev/null +++ b/skd/job/user/privilege.go @@ -0,0 +1,16 @@ +package user + +import ( + "91porn-server/common/log" + "91porn-server/models/v/advanceordermod" +) + +// ResetLimit 重置预售卡预付权益 +func ResetLimit() { + err := advanceordermod.ResetToday() + if err != nil { + log.Error("重置预售卡预付权益错误", log.E(err)) + } + + log.Info("--重置预售卡预付权益完成!") +} diff --git a/skd/main.go b/skd/main.go new file mode 100644 index 0000000..0514dd9 --- /dev/null +++ b/skd/main.go @@ -0,0 +1,161 @@ +package main + +import ( + "91porn-server/common/cachev2" + "91porn-server/common/dataReport" + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "91porn-server/common" + "91porn-server/common/conf" + "91porn-server/common/db" + "91porn-server/common/laosiji" + "91porn-server/common/log" + recommendqueue "91porn-server/common/shortrecommend" + "91porn-server/common/tg" + "91porn-server/models" + "91porn-server/models/Init" + "91porn-server/skd/job" + "91porn-server/skd/service" + "91porn-server/skd/skdg" +) + +func init() { + cfg := skdg.GlobalConfig{} + log.Info("load config start...") + if err := conf.LoadJSON("config/skd.json", &cfg); err != nil { + log.Error("startUp get config error: %v", log.E(err)) + panic(err) + } + skdg.Conf = &cfg + log.Info("load config end..") + log.Init(log.Options{Level: skdg.Conf.Log.Level}) + log.Info("daichong conf Domain", log.Any("donmain", skdg.Conf.DaiChong.Domain)) +} + +var httpServer *http.Server + +func main() { + time.Local, _ = time.LoadLocation("Asia/Shanghai") + common.Go(func() { + startUp() + httpServerStart() + job.Start() + }) + waitQuit() +} + +func startUp() { + if skdg.Conf.Base.Env == "prod" { + skdg.Bot = tg.New(tg.TianTian_Bot, fmt.Sprintf("短视频[env:%s][Skd-Server]", skdg.Conf.Base.Env)) + common.SetPanicHandler(func(s string) { + _ = skdg.Bot.Send(tg.Msg{ChatId: tg.ChatIdServerTeam, Text: `\[SKD-Server\]` + s}) + }) + } + ds := db.InitDS(initMongodbParam(models.RegisterPool)) + if len(ds) > 0 { + for k, v := range ds { + switch k { + case models.VideoDb: + skdg.VideoDB = v + case models.LogDb: + skdg.LogDB = v + case models.StatDb: + skdg.StatDB = v + } + } + } + dataReport.Init(dataReport.Config{ + ApiUrl: skdg.Conf.DataReport.Url, + }) + laosiji.InitConfig(laosiji.Config{ + AppID: skdg.Conf.LSJ.AppID, + APIKey: skdg.Conf.LSJ.APIKey, + APIUrl: skdg.Conf.LSJ.APIUrl, + ImageYuan: skdg.Conf.LSJ.ImageYuan, + NoticeURL: skdg.Conf.LSJ.NoticeURL, + }) + skdg.InitRedis() + if err := recommendqueue.SetKeyTTLHours(skdg.Conf.ShortRecommendKeyTTL()); err != nil { + log.Error("short recommend key TTL config invalid, use default", + log.E(err), log.Any("configuredHours", skdg.Conf.ShortRecommendKeyTTL())) + _ = recommendqueue.SetKeyTTLHours(72) + } + // 初始化缓存操作中间件 + cachev2.Init(skdg.Redis, log.ZapLog) + skdg.InitElastic() + skdg.InitStatic() + Init.InitMongo() + Init.ESInit() + if skdg.Conf.Switch.NotifyExpire { + service.DoRegister() + } + // service.RobotPublishComment() + // service.Init() + // service.VideoInfo.FullUpdate() +} + +func waitQuit() { + //graceful exit + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT) + <-ch + log.Info("Recv Signal, Shutdown Server Start...") + log.Info("Pleace Wait Job Stop...") + job.Stop() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if httpServer != nil { + if err := httpServer.Shutdown(ctx); err != nil { + log.Error("Server Shutdown error", log.E(err)) + } + } + db.CloseDS() + if err := skdg.RedisClose(); err != nil { + log.Error("redis close error", log.E(err)) + return + } + log.Warn("Shutdown Server OK...") +} + +func initMongodbParam(registerPool []models.Register) ([]db.DBmap, []db.Register) { + pri := db.DBmap{ + Key: models.VideoDb, + URL: skdg.Conf.Mongo.VideoDbUrl, + } + sta := db.DBmap{ + Key: models.StatDb, + URL: skdg.Conf.Mongo.StatDbUrl, + } + log := db.DBmap{ + Key: models.LogDb, + URL: skdg.Conf.Mongo.LogDbUrl, + } + dbgister := make([]db.Register, len(registerPool)) + for i, v := range registerPool { + dbgister[i] = db.Register{Key: v.Key, Table: v.Table} + } + dbmaps := []db.DBmap{pri, sta, log} + return dbmaps, dbgister +} + +func httpServerStart() { + common.Go(func() { + //开始http + port := skdg.Conf.Base.Port + httpServer = &http.Server{ + Addr: fmt.Sprintf(":%d", port), + } + log.Info("Start Listen ...", log.Any("port", port)) + if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatal("server listen err", log.E(err)) + } + // Block Here + log.Info("httpServer quit...") + }) +} diff --git a/skd/once/main.go b/skd/once/main.go new file mode 100644 index 0000000..565f7b7 --- /dev/null +++ b/skd/once/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "sync" + + "91porn-server/common/conf" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/Init" + "91porn-server/skd/skdg" +) + +/* + 本函数会将mongo库内的video_info数据, audiobook数据, fiction数据写入es + 一般在项目部署, 数据导入完成以后运行一次便可 +*/ + +func init() { + cfg := skdg.GlobalConfig{} + log.Info("load config start...") + if err := conf.LoadJSON("config/skd.json", &cfg); err != nil { + log.Error("startUp get config error: %v", log.E(err)) + panic(err) + } + skdg.Conf = &cfg + log.Info("load config end..") + log.Init(log.Options{Level: skdg.Conf.Log.Level}) + log.Info("daichong conf Domain", log.Any("donmain", skdg.Conf.DaiChong.Domain)) +} + +func main() { + ds := db.InitDS(initMongodbParam(models.RegisterPool)) + if len(ds) > 0 { + for k, v := range ds { + switch k { + case models.VideoDb: + skdg.VideoDB = v + case models.LogDb: + skdg.LogDB = v + case models.StatDb: + skdg.StatDB = v + } + } + } + skdg.InitElastic() + Init.InitMongo() + log.Info("所有同步开始") + wg := new(sync.WaitGroup) + wg.Add(3) + go syncAudioBook(wg) + go syncVideoInfo(wg) + go syncFiction(wg) + wg.Wait() + log.Info("所有同步结束") +} + +func initMongodbParam(registerPool []models.Register) ([]db.DBmap, []db.Register) { + pri := db.DBmap{ + Key: models.VideoDb, + URL: skdg.Conf.Mongo.VideoDbUrl, + } + sta := db.DBmap{ + Key: models.StatDb, + URL: skdg.Conf.Mongo.StatDbUrl, + } + log := db.DBmap{ + Key: models.LogDb, + URL: skdg.Conf.Mongo.LogDbUrl, + } + dbgister := make([]db.Register, len(registerPool)) + for i, v := range registerPool { + dbgister[i] = db.Register{Key: v.Key, Table: v.Table} + } + dbmaps := []db.DBmap{pri, sta, log} + return dbmaps, dbgister +} diff --git a/skd/once/syncAudioBook.go b/skd/once/syncAudioBook.go new file mode 100644 index 0000000..fe8a27f --- /dev/null +++ b/skd/once/syncAudioBook.go @@ -0,0 +1,40 @@ +package main + +import ( + "sync" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/v/audiobookmod" + "91porn-server/skd/skdg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func syncAudioBook(wg *sync.WaitGroup) { + log.Info("audiobook sync start...") + for i := int64(0); ; i++ { + opts := options.Find().SetSkip(i * 1000).SetLimit(1000).SetSort(bson.D{{Key: "_id", Value: 1}}) + au, err := audiobookmod.GetListByCond(bson.M{}, opts) + if err != nil { + panic(err) + } + var source = elastic.M{} + for i := range au { + tmp := au[i] + source[au[i].ID.Hex()] = tmp + } + if err := skdg.VideoES.Bulk(models.ESInfoAudioBookTable, source); err != nil { + panic(err) + } + auLen := len(au) + if len(au) < 1000 { + break + } + log.Info("audiobook sync:", log.Any("start", i*1000), log.Any("end", i*1000+int64(auLen))) + } + log.Info("audiobook sync complete...") + wg.Done() +} diff --git a/skd/once/syncFiction.go b/skd/once/syncFiction.go new file mode 100644 index 0000000..ef4e2f4 --- /dev/null +++ b/skd/once/syncFiction.go @@ -0,0 +1,40 @@ +package main + +import ( + "sync" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/v/fictionmod" + "91porn-server/skd/skdg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func syncFiction(wg *sync.WaitGroup) { + log.Info("fiction sync start...") + for i := int64(0); ; i++ { + opts := options.Find().SetSkip(i * 1000).SetLimit(1000).SetSort(bson.D{{Key: "_id", Value: 1}}) + fc, err := fictionmod.GetListByCond(bson.M{}, opts) + if err != nil { + panic(err) + } + var source = elastic.M{} + for i := range fc { + tmp := fc[i] + source[fc[i].ID.Hex()] = tmp + } + if err := skdg.VideoES.Bulk(models.ESInfoFictionTable, source); err != nil { + panic(err) + } + fcLen := len(fc) + if fcLen < 1000 { + break + } + log.Info("fiction sync:", log.Any("start", i*1000), log.Any("end", i*1000+int64(fcLen))) + } + log.Info("fiction sync complete...") + wg.Done() +} diff --git a/skd/once/syncVideoInfo.go b/skd/once/syncVideoInfo.go new file mode 100644 index 0000000..913aba9 --- /dev/null +++ b/skd/once/syncVideoInfo.go @@ -0,0 +1,40 @@ +package main + +import ( + "sync" + + "91porn-server/common/elastic" + "91porn-server/common/log" + "91porn-server/models" + "91porn-server/models/v/vidmod" + "91porn-server/skd/skdg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func syncVideoInfo(wg *sync.WaitGroup) { + log.Info("video sync start...") + for i := int64(0); ; i++ { + opts := options.Find().SetSkip(i * 1000).SetLimit(1000).SetSort(bson.D{{Key: "_id", Value: 1}}) + vd, err := vidmod.GetListByCond(bson.M{}, opts) + if err != nil { + panic(err) + } + var source = elastic.M{} + for i := range vd { + tmp := vd[i] + source[vd[i].ID.Hex()] = tmp + } + if err := skdg.VideoES.Bulk(models.ESInfoVideoTable, source); err != nil { + panic(err) + } + vdLen := len(vd) + if vdLen < 1000 { + break + } + log.Info("video sync:", log.Any("start", i*1000), log.Any("end", i*1000+int64(vdLen))) + } + log.Info("video sync complete...") + wg.Done() +} diff --git a/skd/service/chatrobot.go b/skd/service/chatrobot.go new file mode 100644 index 0000000..d46fa9f --- /dev/null +++ b/skd/service/chatrobot.go @@ -0,0 +1,218 @@ +package service + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/modulesectionmod" + "91porn-server/models/v/modulevidmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/vidmod" + "91porn-server/models/v/vidtimeonlinemod" + "91porn-server/web/service/video_media_service" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "strconv" + "strings" + "time" +) + +type vidTimeOnlineInfo struct { + VidId primitive.ObjectID `json:"vidId"` + VidType string `json:"vidType"` + SourceURL string `json:"sourceURL"` + ReviewAccount string `json:"reviewAccount"` +} + +var vidTimeOnlineIsRuining = false + +// 视频自动上架 +func VidTimeOnline() { + common.Go(func() { + log.Info("video time online start...") + if vidTimeOnlineIsRuining { + log.Info("video time online already executed") + return + } + vidTimeOnlineIsRuining = true + defer func() { + vidTimeOnlineIsRuining = false + }() + log.Info("video time online execute") + handleVidTimeOnline() + log.Info("video time online end...") + }) +} + +func handleVidTimeOnline() { + //每次取十条数据,定期上线 + vidInfs, err := vidtimeonlinemod.GetByPage(commod.Page{PageNumber: 1, PageSize: 100}) + if err != nil { + log.Error("video time online eonlinemod.GetByPage err", log.E(err)) + return + } + if len(vidInfs) == 0 { + log.Info("video time online vidInfs == 0") + return + } + + //上架 + resIds := passVids(vidInfs) + //从自动上架表中移除已经上架的帖子数据 + if len(resIds) > 0 { + if err = vidtimeonlinemod.DeleteByVids(resIds); err != nil { + log.Warn("video time online vidtimeonlinemod.DeleteByVids err", log.Any("err", err)) + return + } + } + log.Info("video time online resIds", log.Any("resIds", resIds)) +} + +// passVids 批量自动上架 +func passVids(vidIds []vidtimeonlinemod.VidTimeOnlineModel) (resIds []primitive.ObjectID) { + var uploadRecord []vidmod.UploadMediaVideo + var records []modulevidmod.SectionVideo + for _, vid := range vidIds { + //非图集才需要同步 + isCover := vid.VidType == vidmod.COVER || vid.VidType == vidmod.PIC + log.Info("vid time online", log.Any("isCover", isCover), log.Any("vid", vid)) + // 查询视频信息 + videoInfo, err := vidmod.GetVideoInfo(vid.VidId.Hex()) + if err != nil { + log.Warn("vid time online passVids vidmod.GetVideoInfo err,", log.Any("err", err), log.Any("vid", vid.VidId)) + return resIds + } + log.Info("vid time online", log.Any("status", videoInfo.Status), log.Any("MDSID", videoInfo.MDSID), log.Any("videoInfo.Filename", videoInfo.Filename)) + // 媒资库的不做任何校验,因为给过来的这部分数据的sourceID有问题 + if videoInfo.MDSID == "" && !isCover && !strings.Contains(videoInfo.Filename, "mzk_") { + if code := SyncFileFromFs(vid.VidId.Hex()); code != stderr.UpLoadFileComplete { + log.Warn("vid time online passVids syncFileFromFs err,", log.Any("code", code), log.Any("vid", vid.VidId)) + continue + } + } + tagSort := make(bson.M, len(videoInfo.Tags)) + for _, v := range videoInfo.Tags { + tagSort[v.Hex()] = 0 + } + + if videoInfo.Status == 1 { + resIds = append(resIds, videoInfo.ID) + continue + } + if (videoInfo.NewsType == vidmod.SP || videoInfo.NewsType == vidmod.SHORT) && !strings.Contains(videoInfo.Filename, "mzk_") { + var width, height int + resolutions := strings.Split(videoInfo.Resolution, "*") + if len(resolutions) >= 2 { + width, _ = strconv.Atoi(resolutions[0]) + height, _ = strconv.Atoi(resolutions[1]) + } + if len(resolutions) != 2 { + resolutions = strings.Split(videoInfo.Resolution, "x") + if len(resolutions) >= 2 { + width, _ = strconv.Atoi(resolutions[0]) + height, _ = strconv.Atoi(resolutions[1]) + } + } + var tags string + if len(videoInfo.Tags) > 0 { + for _, v := range videoInfo.Tags { + tagData, err := tagmod.FindOneTagByID(v) + if err == nil { + if tags != "" { + tags += "," + tagData.TagName + } else { + tags += tagData.TagName + } + } + } + } + uploadRecord = append(uploadRecord, vidmod.UploadMediaVideo{ + FsResourceId: videoInfo.SourceID, + HashId: videoInfo.MD5, + Title: videoInfo.Title, + Length: int(videoInfo.PlayTime), + FileSize: videoInfo.Size, + M3u8Src: videoInfo.SourceURL, + Height: height, + Width: width, + CoverImage: videoInfo.Cover, + TagsText: tags, + }) + } + video, err := vidmod.PassVidsOnlineTime(vid.VidId, 1, "", vid.ReviewAccount, vid.OnlineTime, tagSort) + if err != nil { + log.Warn("vid time online passVids vidmod.PassVids err,", log.Any("err", err), log.Any("vid", video.ID)) + return resIds + } + if !vid.SectionId.IsZero() { + // 获取专题 + section, err := modulesectionmod.GetBySectionByID(vid.SectionId) + if err != nil { + log.Error("vid time online modulesectionmod.GetBySectionByID fail", log.Any("vid.SectionId", vid.SectionId), log.E(err)) + continue + } + if !section.ID.IsZero() { + // 判断当前专题下是否已经有该记录,没有则添加 + sectionVids, err := modulevidmod.GetBySectionIDAndVids(section.ID, []primitive.ObjectID{vid.VidId}) + if err != nil { + log.Error("vid time online modulevidmod.GetBySectionIDAndVids( fail", log.Any("vid.SectionId", vid.SectionId), log.Any("vid.VidId", vid.VidId), log.E(err)) + continue + } + if len(sectionVids) == 0 { + records = append(records, modulevidmod.SectionVideo{ + SectionID: section.ID, + VideoID: vid.VidId, + VideoReviewedAt: vid.OnlineTime, + }) + } + // 修改帖子的mId + update := bson.M{} + update["updatedAt"] = time.Now() + update["mId"] = section.SubModuleID.Hex() + vidmod.UpdateOneByID(vid.VidId, update) + } + + } + + resIds = append(resIds, video.ID) + } + + if len(records) > 0 { + err := modulevidmod.InsertMany(records) + if err != nil { + log.Error("vid time online AddVidTimeOnlineInfo moduleVidMod InsertMany error", log.Any("records", records), log.Any("err", err)) + return resIds + } + + } + + log.Info("vid time online", log.Any("uploadRecord", uploadRecord)) + // 处理上传媒资库 + if len(uploadRecord) > 0 { + var err error + var sourceIDs []string + var errSourceIDs []string + for _, info := range uploadRecord { + if info.Length > 300 { + err = video_media_service.UploadSkdVideoMedia(info) + } else { + err = video_media_service.UploadSkdShortVideoMedia(info) + } + if err != nil { + errSourceIDs = append(errSourceIDs, info.FsResourceId) + log.Info(fmt.Sprintf("vid time online UploadVideoMedia err%v;errSourceIDs%v", err, errSourceIDs)) + continue + } + sourceIDs = append(sourceIDs, info.FsResourceId) + } + if err != nil { + log.Info(fmt.Sprintf("vid time online UploadVideoMedia err%v;errSourceIDs%v", err, errSourceIDs)) + return resIds + } + log.Info(fmt.Sprintf("vid time online UploadVideoMedia sucess sourceIDs%v", sourceIDs)) + } + + return resIds +} diff --git a/skd/service/contentreviewser/acg.go b/skd/service/contentreviewser/acg.go new file mode 100644 index 0000000..1d0f4ce --- /dev/null +++ b/skd/service/contentreviewser/acg.go @@ -0,0 +1,68 @@ +package contentreviewser + +import ( + "91porn-server/common/log" + "91porn-server/models/v/contentreviewmod" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// acgScanner ACG 扫描策略:每个 Media 可能产生 1 个 ACG_MEDIA + N 个 ACG_CONTENT 命中 +// 仅匹配 title / summary / 章节名 / 章节内容(不检测 tag) +type acgScanner struct{} + +func (s *acgScanner) CountTotal() (int64, error) { + return mediamod.CountForReview() +} + +func (s *acgScanner) ScanBatch(taskID, lastID primitive.ObjectID, terms []string) (*ScanResult, error) { + batch, err := mediamod.FindForReviewBatch(lastID, scanBatchSize) + if err != nil { + return nil, err + } + if len(batch) == 0 { + return &ScanResult{Done: true}, nil + } + + issues := make([]*contentreviewmod.ReviewIssue, 0) + for _, m := range batch { + issues = append(issues, collectMediaIssues(taskID, m, terms)...) + } + + return &ScanResult{ + Issues: issues, + Checked: int64(len(batch)), + NextID: batch[len(batch)-1].ID, + }, nil +} + +// collectMediaIssues 检测一个 Media 自身及其全部 MediaContent +func collectMediaIssues(taskID primitive.ObjectID, m *mediamod.Media, terms []string) []*contentreviewmod.ReviewIssue { + out := make([]*contentreviewmod.ReviewIssue, 0) + + mb := newIssueBuilder(taskID, contentreviewmod.IssueTargetACGMedia, m.ID). + MatchTitle("标题", m.Title, terms). + MatchContent("内容", m.Summary, terms) + if mb.HasHits() { + out = append(out, mb.Build()) + } + + contents, err := mediacontentmod.QueryAllList(bson.M{"mediaId": m.ID, "isActive": true}) + if err != nil { + log.Warn("query media contents fail", log.Any("mid", m.ID), log.E(err)) + return out + } + for _, c := range contents { + cb := newIssueBuilder(taskID, contentreviewmod.IssueTargetACGContent, c.ID). + WithMediaID(m.ID). + MatchTitle("章节名", c.Name, terms). + MatchContent("章节内容", c.Text, terms) + if cb.HasHits() { + out = append(out, cb.Build()) + } + } + return out +} diff --git a/skd/service/contentreviewser/issue_builder.go b/skd/service/contentreviewser/issue_builder.go new file mode 100644 index 0000000..b9c135f --- /dev/null +++ b/skd/service/contentreviewser/issue_builder.go @@ -0,0 +1,92 @@ +package contentreviewser + +import ( + "fmt" + "strings" + + "91porn-server/models/v/contentreviewmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// issueBuilder 用 Builder 模式构建 ReviewIssue: +// 通过链式调用 MatchXxx 收集各字段的命中,HasHits/Build 在所有匹配后调用 +type issueBuilder struct { + issue *contentreviewmod.ReviewIssue + hitSet map[string]struct{} + descLines []string +} + +func newIssueBuilder(taskID primitive.ObjectID, targetType contentreviewmod.IssueTargetType, targetID primitive.ObjectID) *issueBuilder { + return &issueBuilder{ + issue: &contentreviewmod.ReviewIssue{ + TaskID: taskID, + TargetType: targetType, + TargetID: targetID, + }, + hitSet: make(map[string]struct{}), + } +} + +// WithMediaID 设置关联的 MediaID(仅 ACG_CONTENT 需要) +func (b *issueBuilder) WithMediaID(id primitive.ObjectID) *issueBuilder { + b.issue.MediaID = id + return b +} + +// MatchTitle 匹配标题字段;命中时存入高亮版本并累计描述 +// label 是描述文案中的字段名(如"标题"、"章节名") +func (b *issueBuilder) MatchTitle(label, text string, terms []string) *issueBuilder { + if hits, hl := matchAndHighlight(text, terms); len(hits) > 0 { + b.issue.Title = hl + b.addHits(label, hits) + } + return b +} + +func (b *issueBuilder) MatchContent(label, text string, terms []string) *issueBuilder { + if hits, hl := matchAndHighlight(text, terms); len(hits) > 0 { + b.issue.Content = hl + b.addHits(label, hits) + } + return b +} + +func (b *issueBuilder) MatchTags(label, text string, terms []string) *issueBuilder { + if hits, hl := matchAndHighlight(text, terms); len(hits) > 0 { + b.issue.Tags = hl + b.addHits(label, hits) + } + return b +} + +func (b *issueBuilder) MatchRichText(label, text string, terms []string) *issueBuilder { + if hits, hl := matchAndHighlight(text, terms); len(hits) > 0 { + b.issue.RichText = hl + b.addHits(label, hits) + } + return b +} + +// HasHits 是否至少有一个字段命中 +func (b *issueBuilder) HasHits() bool { + return len(b.hitSet) > 0 +} + +// Build 收尾:拼装 Description 与去重后的 HitWords +func (b *issueBuilder) Build() *contentreviewmod.ReviewIssue { + hits := make([]string, 0, len(b.hitSet)) + for h := range b.hitSet { + hits = append(hits, h) + } + b.issue.HitWords = hits + b.issue.Description = strings.Join(b.descLines, "\n") + return b.issue +} + +func (b *issueBuilder) addHits(label string, hits []string) { + b.descLines = append(b.descLines, fmt.Sprintf("%s命中 %s", label, strings.Join(hits, "、"))) + for _, h := range hits { + b.hitSet[h] = struct{}{} + } +} diff --git a/skd/service/contentreviewser/matcher.go b/skd/service/contentreviewser/matcher.go new file mode 100644 index 0000000..6b316d6 --- /dev/null +++ b/skd/service/contentreviewser/matcher.go @@ -0,0 +1,114 @@ +package contentreviewser + +import ( + "sort" + "strings" + + "91porn-server/models/v/contentreviewmod" +) + +const ( + markPrefix = "" + markSuffix = "" +) + +// allTerms 把快照展开为一组待匹配的词 +func allTerms(snaps []contentreviewmod.SensitiveWordSnap) []string { + seen := make(map[string]struct{}) + out := make([]string, 0, len(snaps)) + for _, s := range snaps { + if s.Word == "" { + continue + } + if _, ok := seen[s.Word]; ok { + continue + } + seen[s.Word] = struct{}{} + out = append(out, s.Word) + } + // 长词优先,避免短词先替换破坏长词的匹配 + sort.Slice(out, func(i, j int) bool { + return len(out[i]) > len(out[j]) + }) + return out +} + +// findHits 返回 input 中命中的词(去重,保持长→短顺序) +func findHits(input string, terms []string) []string { + if input == "" || len(terms) == 0 { + return nil + } + hits := make([]string, 0) + seen := make(map[string]struct{}) + for _, t := range terms { + if t == "" { + continue + } + if _, ok := seen[t]; ok { + continue + } + if strings.Contains(input, t) { + seen[t] = struct{}{} + hits = append(hits, t) + } + } + return hits +} + +// highlight 用 ... 包裹命中的词 +// hits 已按长度降序;为避免再次包裹已 mark 的内容,分段替换 +func highlight(input string, hits []string) string { + if input == "" || len(hits) == 0 { + return input + } + result := input + for _, h := range hits { + if h == "" { + continue + } + result = replaceNotInMark(result, h) + } + return result +} + +// replaceNotInMark 在 input 中把 term 全部替换为 term, +// 但跳过已经包在 ... 内的片段(避免嵌套) +func replaceNotInMark(input, term string) string { + if term == "" { + return input + } + var b strings.Builder + i := 0 + for i < len(input) { + // 已经在 mark 块里,整段原样输出 + if strings.HasPrefix(input[i:], markPrefix) { + end := strings.Index(input[i:], markSuffix) + if end < 0 { + b.WriteString(input[i:]) + return b.String() + } + b.WriteString(input[i : i+end+len(markSuffix)]) + i += end + len(markSuffix) + continue + } + if strings.HasPrefix(input[i:], term) { + b.WriteString(markPrefix) + b.WriteString(term) + b.WriteString(markSuffix) + i += len(term) + continue + } + b.WriteByte(input[i]) + i++ + } + return b.String() +} + +// matchAndHighlight 在 input 上一步到位:返回命中词列表 + 高亮后的文本 +func matchAndHighlight(input string, terms []string) ([]string, string) { + hits := findHits(input, terms) + if len(hits) == 0 { + return nil, input + } + return hits, highlight(input, hits) +} diff --git a/skd/service/contentreviewser/runner.go b/skd/service/contentreviewser/runner.go new file mode 100644 index 0000000..07b8501 --- /dev/null +++ b/skd/service/contentreviewser/runner.go @@ -0,0 +1,129 @@ +package contentreviewser + +import ( + "fmt" + "runtime/debug" + "sync/atomic" + + "91porn-server/common/log" + "91porn-server/models/v/contentreviewmod" + "91porn-server/models/v/sensitivewordmod" + "91porn-server/skd/service/export_task" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" +) + +// tgChatID 内容审查任务完成通知群(与导出任务复用同一群) +const tgChatID int64 = -1003399433452 + +var runningFlag int32 // 进程内互斥:0=空闲 1=运行中 + +// TryRunPendingTask 调度入口,由 skd 的 cron 定时调用 +// skd 单进程,仅需 atomic 互斥防止 cron tick 重入;每次只挑一个最早的任务处理 +// 取到 pending 视为首跑;取到心跳超时的 running 视为续跑,从 lastTargetId 继续 +func TryRunPendingTask() { + if !atomic.CompareAndSwapInt32(&runningFlag, 0, 1) { + return + } + defer atomic.StoreInt32(&runningFlag, 0) + + task, err := contentreviewmod.PickNextTask() + if err != nil { + log.Warn("content review: pick next fail", log.E(err)) + return + } + if task == nil { + return + } + if task.ID.IsZero() { + return + } + isResume := task.Status == contentreviewmod.TaskStatusRunning + if err := contentreviewmod.AcquireTask(task.ID, isResume); err != nil { + log.Warn("content review: acquire task fail", log.Any("taskId", task.ID), log.E(err)) + return + } + if isResume { + log.Info("content review: resume stale task", + log.Any("taskId", task.ID), log.Any("lastTargetId", task.LastTargetID)) + } + + executeTask(task) +} + +// executeTask 真正执行扫描 +func executeTask(task *contentreviewmod.ReviewTask) { + defer func() { + if r := recover(); r != nil { + stack := string(debug.Stack()) + log.Error("content review task panic", + log.Any("taskId", task.ID), log.Any("panic", r), log.Any("stack", stack)) + _ = contentreviewmod.MarkFailed(task.ID, fmt.Sprintf("panic: %v", r)) + } + }() + + log.Info("content review task start", + log.Any("taskId", task.ID), log.Any("type", task.TaskType)) + + checked, issues, err := runScan(task) + if err != nil { + log.Error("content review task fail", + log.Any("taskId", task.ID), log.E(err)) + _ = contentreviewmod.MarkFailed(task.ID, err.Error()) + return + } + + if err := contentreviewmod.MarkFinished(task.ID, checked, issues); err != nil { + log.Error("content review mark finished fail", log.E(err)) + return + } + + // 发送 TG + if sendTGNotify(task, checked, issues) { + _ = contentreviewmod.MarkTgSent(task.ID) + } + + log.Info("content review task done", + log.Any("taskId", task.ID), log.Any("checked", checked), log.Any("issues", issues)) +} + +func sendTGNotify(task *contentreviewmod.ReviewTask, checked, issues int64) bool { + if export_task.Bot == nil { + log.Warn("content review: tg bot not initialized, skip notify") + return false + } + typeName := "视频帖子" + if task.TaskType == contentreviewmod.TaskTypeACG { + typeName = "ACG" + } + operator := task.Operator + if operator == "" { + operator = "-" + } + text := fmt.Sprintf( + "【%s 内容检测任务完成】\n操作人: %s\n类型: %s\n任务ID: %s\n总记录: %d\n已检测: %d\n命中: %d", + export_task.GetProName(), operator, typeName, task.ID.Hex(), task.TotalCount, checked, issues, + ) + if _, err := export_task.Bot.Send(tgbotapi.NewMessage(tgChatID, text)); err != nil { + log.Warn("content review: send tg fail", log.Any("taskId", task.ID), log.E(err)) + return false + } + return true +} + +// BuildSnapshot 拉取当前启用的敏感词作为快照(web 创建任务时复用) +func BuildSnapshot() ([]contentreviewmod.SensitiveWordSnap, error) { + enabled := sensitivewordmod.StatusEnabled + list, err := sensitivewordmod.FindAll(&sensitivewordmod.ListReq{Status: &enabled}) + if err != nil { + return nil, err + } + out := make([]contentreviewmod.SensitiveWordSnap, 0, len(list)) + for _, w := range list { + out = append(out, contentreviewmod.SensitiveWordSnap{ + Word: w.Word, + Category: w.Category, + }) + } + return out, nil +} diff --git a/skd/service/contentreviewser/scanner.go b/skd/service/contentreviewser/scanner.go new file mode 100644 index 0000000..ada5158 --- /dev/null +++ b/skd/service/contentreviewser/scanner.go @@ -0,0 +1,95 @@ +package contentreviewser + +import ( + "fmt" + + "91porn-server/common/log" + "91porn-server/models/v/contentreviewmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const scanBatchSize = 100 + +// ScanResult 一批扫描的产出 +type ScanResult struct { + Issues []*contentreviewmod.ReviewIssue // 本批生成的命中记录 + Checked int64 // 本批实际扫描的目标数量 + NextID primitive.ObjectID // 下一批的游标 + Done bool // true 表示已扫完,runScan 退出循环 +} + +// Scanner 内容审查扫描策略;具体任务类型实现各自的 ScanBatch +type Scanner interface { + CountTotal() (int64, error) + ScanBatch(taskID, lastID primitive.ObjectID, terms []string) (*ScanResult, error) +} + +// newScanner Strategy 工厂:根据任务类型返回具体扫描器 +func newScanner(taskType string) (Scanner, error) { + switch taskType { + case contentreviewmod.TaskTypeVideo: + return &videoScanner{}, nil + case contentreviewmod.TaskTypeACG: + return &acgScanner{}, nil + default: + return nil, fmt.Errorf("unsupported task type: %s", taskType) + } +} + +// runScan Template Method:把"分页游标 + 进度回写 + 批量插入"骨架抽出,差异由 Scanner 提供 +// 支持从 task.LastTargetID 断点续跑 +func runScan(task *contentreviewmod.ReviewTask) (int64, int64, error) { + scanner, err := newScanner(task.TaskType) + if err != nil { + return task.CheckedCount, task.IssueCount, err + } + + terms := allTerms(task.SnapshotWords) + + total := task.TotalCount + if total <= 0 { + t, err := scanner.CountTotal() + if err != nil { + return task.CheckedCount, task.IssueCount, fmt.Errorf("count total: %w", err) + } + total = t + } + // 同步到内存对象,便于 executeTask 拼 TG 消息时正确取值 + task.TotalCount = total + + lastID := task.LastTargetID + checked := task.CheckedCount + issues := task.IssueCount + + flushProgress(task.ID, lastID, checked, issues, total) + + for { + result, err := scanner.ScanBatch(task.ID, lastID, terms) + if err != nil { + return checked, issues, fmt.Errorf("scan batch: %w", err) + } + if result.Done { + break + } + if len(result.Issues) > 0 { + if err := contentreviewmod.InsertIssues(result.Issues); err != nil { + log.Warn("insert issues fail", log.Any("count", len(result.Issues)), log.E(err)) + } else { + issues += int64(len(result.Issues)) + } + } + checked += result.Checked + lastID = result.NextID + + flushProgress(task.ID, lastID, checked, issues, total) + } + return checked, issues, nil +} + +func flushProgress(id, lastID primitive.ObjectID, checked, issues, total int64) { + if err := contentreviewmod.UpdateProgress(id, lastID, checked, issues, total); err != nil { + log.Warn("update progress fail", log.E(err)) + } +} + diff --git a/skd/service/contentreviewser/video.go b/skd/service/contentreviewser/video.go new file mode 100644 index 0000000..be3e4ba --- /dev/null +++ b/skd/service/contentreviewser/video.go @@ -0,0 +1,42 @@ +package contentreviewser + +import ( + "91porn-server/models/v/contentreviewmod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// videoScanner 视频帖子扫描策略;匹配 title / content / richText(不检测 tag) +type videoScanner struct{} + +func (s *videoScanner) CountTotal() (int64, error) { + return vidmod.CountForReview() +} + +func (s *videoScanner) ScanBatch(taskID, lastID primitive.ObjectID, terms []string) (*ScanResult, error) { + batch, err := vidmod.FindForReviewBatch(lastID, scanBatchSize) + if err != nil { + return nil, err + } + if len(batch) == 0 { + return &ScanResult{Done: true}, nil + } + + issues := make([]*contentreviewmod.ReviewIssue, 0, len(batch)) + for _, v := range batch { + b := newIssueBuilder(taskID, contentreviewmod.IssueTargetVideo, v.ID). + MatchTitle("标题", v.Title, terms). + MatchContent("内容", v.Content, terms). + MatchRichText("富文本", v.RichText, terms) + if b.HasHits() { + issues = append(issues, b.Build()) + } + } + + return &ScanResult{ + Issues: issues, + Checked: int64(len(batch)), + NextID: batch[len(batch)-1].ID, + }, nil +} diff --git a/skd/service/doregister.go b/skd/service/doregister.go new file mode 100644 index 0000000..35cadfa --- /dev/null +++ b/skd/service/doregister.go @@ -0,0 +1,119 @@ +package service + +import ( + "fmt" + "strconv" + "strings" + "time" + + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/redis" + "91porn-server/common/usertruth" + "91porn-server/models/l/registermod" + "91porn-server/models/v/usermod" + "91porn-server/skd/skdg" +) + +const ( + RestartSignal string = "RESTART" + NotifyKeyPrefix string = "notify:user:new:register:" + TrimPrefix string = "notify:" +) + +var MessageCh = make(chan *redis.Message, 100) +var ChStatus = make(chan string) + +// DoRegister +func DoRegister() { + common.Go(func() { Sub(redisconst.Db0ExpiredChannel) }) + common.Go(func() { + for { + select { + case v, ok := <-MessageCh: + if !ok { + log.Warn("MessChan is nil waiting some second") + } + if v != nil { + LoadNewRegister(v.Payload) + } + case v, ok := <-ChStatus: + if !ok { + log.Warn("Restart signal is not receive") + } + if v == RestartSignal { + Sub(redisconst.Db0ExpiredChannel) + } + } + } + }) +} + +// 接收过期的键 将信息存入到数据库中 只处理指定的过期键 notify:user:new:register +func LoadNewRegister(expiredKey interface{}) { + if v, ok := expiredKey.(string); ok { + if strings.HasPrefix(v, NotifyKeyPrefix) { + common.Go(func() { + key := strings.TrimPrefix(v, TrimPrefix) + uidstr := strings.TrimPrefix(v, NotifyKeyPrefix) + uid, _ := strconv.ParseUint(uidstr, 10, 64) + log.Info("Expired Key And Handle Data", log.Any("expiredKey", expiredKey), log.Any("needHandleKey", key), log.Any("uid", uid)) + handleRegisterData(key, uid) + }) + } + } +} + +func handleRegisterData(key string, uid uint64) { + fileds := skdg.Redis.Hkeys(key) + //获取IP + ip, _ := skdg.Redis.Hget(key, "ip") + if len(fileds) > 0 { + values, err := skdg.Redis.HMget(key, fileds) + if err != nil { + log.Warn("[Method] HandleRegisterData HMget Data from redis error", log.Any("key", key), log.E(err)) + } + qCountMap := make(map[string]int64) + var qsArray []registermod.QueryStat + rgister := ®istermod.NewRegisterLog{} + for i := 0; i < len(fileds); i++ { + if v, ok := values[i].(string); ok { + cnt, err := strconv.ParseInt(v, 10, 64) + if err != nil { + continue + } + qCountMap[fileds[i]] = cnt + qs := registermod.QueryStat{QueryUrl: fileds[i], Count: cnt} + qsArray = append(qsArray, qs) + } + } + _, _ = skdg.Redis.Del(key) + rgister.CreatedAt = time.Now() + rgister.UID = uid + rgister.Query = qsArray + rgister.IP = ip + _ = registermod.InsertRegisterLog(rgister) + score := usertruth.Score(qCountMap) + //为区别默认值0 将计算出来的得分为0的情况 设置为-1 + if score == 0 { + score = -1 + } + _, _ = usermod.Update(uid, usermod.UserSelector{TrueScore: &score}) + log.Info("[Method] HandleRegisterData success", log.Any("key", key), log.Any("trueScore", score), log.Any("uid", uid)) + } +} + +// 订阅 +func Sub(channel string) { + subpush, err := skdg.Redis.Subscribe(channel) + if err != nil { + ChStatus <- RestartSignal + log.Warn(fmt.Sprintf("skd sever sub channel %s err", channel), log.E(err)) + } + ch := subpush.Channel() + for msg := range ch { + message := &redis.Message{Channel: msg.Channel, Payload: msg.Payload} + MessageCh <- message + } +} diff --git a/skd/service/export_task/advance_order_export.go b/skd/service/export_task/advance_order_export.go new file mode 100644 index 0000000..2530c08 --- /dev/null +++ b/skd/service/export_task/advance_order_export.go @@ -0,0 +1,102 @@ +package export_task + +import ( + "91porn-server/common/log" + "91porn-server/models/v/advanceordermod" + "91porn-server/models/v/export_task_mod" + "encoding/json" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func ExecExportAdvanceOrderTask(task *export_task_mod.ExportTask) (total int64, err error) { + arg := advanceordermod.QueryAllCond{} + err = json.Unmarshal([]byte(task.Param), &arg) + if err != nil { + return + } + + var skip int64 + var size int64 = 100 + index := 1 + path := fmt.Sprintf("./temp/%v%v", "预售订单列表-", task.ID.Hex()) + zipName := fmt.Sprintf("%v_%v_预售订单列表_%v.zip", task.Admin, GetProName(), task.CreatedAt.Format("2006-01-02_15:04:05")) + var records []*advanceordermod.AdvanceOrderExport + excelList := []string{} + for { + fmt.Println("skip", skip, " now ", time.Now().Format("2006-01-02 15:04:05")) + opt := options.Find().SetSkip(skip).SetLimit(size).SetSort(bson.M{"createdAt": -1}) + list, err := advanceordermod.QueryAllDocument(arg.Filter(), opt) + if err != nil { + log.Error("ExecExportAdvanceOrderTask advanceordermod.QueryAllDocument fail", log.E(err)) + return total, err + } + for _, v := range list { + item := &advanceordermod.AdvanceOrderExport{ + ID: v.ID, + UID: v.UID, + AdvOid: v.AdvOid, + BalOid: v.BalOid, + ProductID: v.ProductID, + TotalAmount: v.TotalAmount, + AdvanceAmount: v.AdvanceAmount, + BalanceAmount: v.BalanceAmount, + StartTime: v.StartTime, + EndTime: v.EndTime, + CreatedAt: v.CreatedAt, + } + switch v.Status { + case advanceordermod.AdvanceProcessing: + item.Status = "预付中" + case advanceordermod.AdvanceSUCCESS: + item.Status = "预付成功" + case advanceordermod.BalanceProcessing: + item.Status = "尾款预付中" + case advanceordermod.BalanceSUCCESS: + item.Status = "尾款预付成功" + } + records = append(records, item) + } + + var fileName = fmt.Sprintf("预售订单列表_%v.xlsx", index) + if len(records) >= maxDataNum { + // 获取到的数据已经达到了单文件最大限制数据量 + filePath, err := SaveExcel(records, path, fileName) + if err != nil { + log.Error("ExecExportAdvanceOrderTask SaveExcel fail", log.E(err)) + return total, err + } + // 重新归0 + records = []*advanceordermod.AdvanceOrderExport{} + index = index + 1 + excelList = append(excelList, filePath) + fmt.Println("index:", index) + total = total + int64(len(records)) + } else if len(list) < int(size) { + if len(records) > 0 { + // 数据库已经取不到更多的数据了 + filePath, err := SaveExcel(records, path, fileName) + if err != nil { + log.Error("ExecExportAdvanceOrderTask SaveExcel fail", log.E(err)) + return total, err + } + excelList = append(excelList, filePath) + total = total + int64(len(records)) + } + // 结束循环 + break + } + skip = skip + size + } + + // 发送到tg + err = SendTg(task.Admin, excelList, zipName) + if err != nil { + log.Error("ExecExportVideoTask SendTg fail", log.E(err)) + return + } + return total, nil +} diff --git a/skd/service/export_task/base.go b/skd/service/export_task/base.go new file mode 100644 index 0000000..25d3d1a --- /dev/null +++ b/skd/service/export_task/base.go @@ -0,0 +1,282 @@ +package export_task + +import ( + "91porn-server/common/file" + "91porn-server/common/log" + commonWorker "91porn-server/common/worker" + "91porn-server/models/v/export_task_mod" + "fmt" + "os" + "os/exec" + "path/filepath" + "reflect" + "time" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" + "github.com/pkg/errors" + "github.com/xuri/excelize/v2" + "go.mongodb.org/mongo-driver/bson" +) + +const ( + proName = "91PORN" + tgToken = "5852134617:AAEO0_GPZiUJS_LeR5dM7byEfOpnRg4dGqU" + maxDataNum = 20000 // 每个excel存储的数据量 + maxWorkerNum = 3 + maxSize = 1024 * 1024 * 20 +) + +var Bot *tgbotapi.BotAPI +var worker *commonWorker.Worker + +func init() { + bot, err := tgbotapi.NewBotAPI(tgToken) + if err != nil || bot == nil { + log.Fatal("初始化tgbot失败", log.E(err)) + } + Bot = bot + worker = commonWorker.NewWorker(maxWorkerNum) +} + +var execExportTaskRuining bool + +func ExecExportTask() { + //防止重入 + if execExportTaskRuining { + return + } + execExportTaskRuining = true + defer func() { + execExportTaskRuining = false + }() + // 获取任务列表 + list, err := export_task_mod.GetTaskList() + if err != nil { + log.Error("ExecExportTask export_task_mod.GetTaskList fail", log.E(err)) + return + } + for _, task := range list { + switch task.Type { + case export_task_mod.ExportUserTask: + CallExportFunc(task, ExecExportUserTask) + case export_task_mod.ExportVidTask: + CallExportFunc(task, ExecExportVideoTask) + case export_task_mod.ExportAdvanceOrderTask: + CallExportFunc(task, ExecExportAdvanceOrderTask) + case export_task_mod.ExportProductHistoryTask: + CallExportFunc(task, ExecExportProductHistoryTask) + } + } +} + +func GetProName() string { + //if skdg.Conf.Base.Env == "test" { + // return "测试环境_" + proName + //} + return proName +} + +func CallExportFunc(task *export_task_mod.ExportTask, f func(t *export_task_mod.ExportTask) (int64, error)) { + if task == nil { + return + } + worker.Exec(func() { + // 已经执行过了 + if task.Status != 0 { + return + } + //key := fmt.Sprintf("ExecExportTask:%v", task.ID.Hex()) + //ok, err := skdg.Redis.SetNX(key, 1, time.Minute*30) + //if err != nil { + // log.Error("ExecExportTask fail", log.E(err)) + // return + //} + //if !ok { + // log.Info("ExportTask already executed") + // return + //} + //defer skdg.Redis.Del(key) + start := time.Now().Unix() + total, err := f(task) + if err != nil { + log.Error("CallExportFunc fail", log.Any("task", task), log.E(err)) + // 修改任务信息 + export_task_mod.UpdateTask(task.ID, bson.M{ + "status": 2, + "reason": err.Error(), + }) + return + } + // 修改任务信息 + export_task_mod.UpdateTask(task.ID, bson.M{ + "status": 1, + "time": time.Now().Unix() - start, // 耗时 + "total": total, + }) + }) + return +} + +// SendTg 发送到tg +func SendTg(admin string, excelList []string, zipFileName string) (err error) { + exportPath, err := filepath.Abs("temp") + if err != nil { + return + } + // 尝试创建 + file.MakeDir(exportPath) + zipList, err := Zip(excelList, zipFileName) + if err != nil { + log.Error("SendTg Zip fail", log.Any("excelList", excelList), log.Any("zipFileName", zipFileName), log.E(err)) + return + } + // 测试环境不需要发送到tg群 + //if skdg.Conf.Base.Env == "test" { + // return + //} + if len(zipList) == 0 { + return + } + var chatId int64 = -1003399433452 + + content := fmt.Sprintf("%v总共有%v个压缩包,请查收", admin, len(zipList)) + _, err = Bot.Send(tgbotapi.NewMessage(chatId, content)) + if err != nil { + log.Error("tgbot send fail", log.E(err)) + return errors.Wrap(err, "发送TG消息失败") + } + for _, zipName := range zipList { + fileBytes, err := os.ReadFile("./temp/" + zipName) + if err != nil { + log.Error("ReadFile fail", log.Any("excelList", excelList), log.Any("zipFileName", zipFileName), log.E(err)) + return errors.Wrap(err, "打开压缩文件失败") + } + + // 创建要发送的文件 + tFile := tgbotapi.FileBytes{Name: zipName, Bytes: fileBytes} + + //生成csv发送到tg群 + newDocument := tgbotapi.NewDocumentUpload(chatId, tFile) + _, err = Bot.Send(newDocument) + if err != nil { + log.Error("tgbot send fail", log.E(err)) + return errors.Wrap(err, "发送TG消息失败") + } + } + + return nil +} + +func Zip(filePathList []string, zipFileName string) (zipList []string, err error) { + if len(filePathList) == 0 { + return + } + var currentSize int64 + zipIndex := 1 + currentZipName := fmt.Sprintf("%v_%v.zip", zipFileName, zipIndex) + args := []string{currentZipName} + for k, filePath := range filePathList { + // 获取文件大小 + stat, err := os.Stat(filePath) + if err != nil { + return nil, err + } + currentSize = currentSize + stat.Size() + args = append(args, filePath) + // 判断当前所有文件加起来是否已经超过20M,超过就直接压缩,否则就接着等待压缩 + if currentSize < maxSize && k != (len(filePathList)-1) { + continue + } + cmd1 := exec.Command("zip", args...) + out1, err := cmd1.CombinedOutput() + if err != nil { + return nil, err + } + fmt.Println(string(out1)) + // 移动文件到目录里 + cmd2 := exec.Command("mv", currentZipName, "./temp") + if err != nil { + return nil, err + } + out2, err := cmd2.CombinedOutput() + if err != nil { + return nil, err + } + fmt.Println(string(out2)) + zipList = append(zipList, currentZipName) + // 重置 + zipIndex = zipIndex + 1 + currentZipName = fmt.Sprintf("%v_%v.zip", zipFileName, zipIndex) + args = []string{currentZipName} + currentSize = 0 + } + + return +} + +// SaveExcel 保存数据到excel中 +func SaveExcel(records interface{}, path string, fileName string) (filePath string, err error) { + + sheet := "sheet1" + xlsx := excelize.NewFile() // new file + index, _ := xlsx.NewSheet(sheet) // new sheet + xlsx.SetActiveSheet(index) // set active (default) sheet + t := reflect.TypeOf(records) + if t.Kind() != reflect.Slice { + panic("records must be slice") + } + + s := reflect.ValueOf(records) + for i := 0; i < s.Len(); i++ { + elem := s.Index(i).Interface() + elemType := reflect.TypeOf(elem) + elemValue := reflect.ValueOf(elem) + if elemType.Kind() == reflect.Ptr { + elemType = elemType.Elem() + elemValue = elemValue.Elem() + } + if elemType.Kind() != reflect.Struct { + panic("record in slice must be a struct") + } + k := 0 + for j := 0; j < elemType.NumField(); j++ { + field := elemType.Field(j) + tag := field.Tag.Get("xlsx") + if tag == "-" { + continue + } + if tag == "" { + tag = field.Name + } + column, _ := excelize.ColumnNumberToName(k + 1) + k++ + name := tag + // 设置表头 + if i == 0 { + _ = xlsx.SetCellValue(sheet, fmt.Sprintf("%s%d", column, i+1), name) + } + // 判断是否是时间类型 + + colValue := elemValue.Field(j).Interface() + if colTime, ok := colValue.(time.Time); ok { + colValue = colTime.Format("2006-01-02 15:04:05") + } + // 设置内容 + _ = xlsx.SetCellValue(sheet, fmt.Sprintf("%s%d", column, i+2), colValue) + } + } + // 保存的目录看是否存在不存在就创建 + _, err = os.Stat(path) + if os.IsNotExist(err) { + err = os.MkdirAll(path, os.ModePerm) + if err != nil { + return "", err + } + } + filePath = path + "/" + fileName + err = xlsx.SaveAs(filePath) + if err != nil { + return "", err + } + return filePath, nil +} diff --git a/skd/service/export_task/product_history_export.go b/skd/service/export_task/product_history_export.go new file mode 100644 index 0000000..4c48bec --- /dev/null +++ b/skd/service/export_task/product_history_export.go @@ -0,0 +1,115 @@ +package export_task + +import ( + "91porn-server/common/log" + "91porn-server/models/v/advanceordermod" + "91porn-server/models/v/export_task_mod" + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/productmod" + "encoding/json" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func ExecExportProductHistoryTask(task *export_task_mod.ExportTask) (total int64, err error) { + arg := prdcthsomod.ProductHistoryQueryReq{} + err = json.Unmarshal([]byte(task.Param), &arg) + if err != nil { + return + } + + var skip int64 + var size int64 = 100 + index := 1 + path := fmt.Sprintf("./temp/%v%v", "会员卡购买记录-", task.ID.Hex()) + zipName := fmt.Sprintf("%v_%v_会员卡购买记录_%v.zip", task.Admin, GetProName(), task.CreatedAt.Format("2006-01-02_15:04:05")) + var records []*prdcthsomod.ProductHistoryExport + excelList := []string{} + for { + fmt.Println("skip", skip, " now ", time.Now().Format("2006-01-02 15:04:05")) + opt := options.Find().SetSkip(skip).SetLimit(size).SetSort(bson.M{"createdAt": -1}) + list, err := prdcthsomod.FindList(arg.GetCond(), opt) + if err != nil { + log.Error("ExecExportProductHistoryTask prdcthsomod.FindList fail", log.E(err)) + return total, err + } + for _, v := range list { + item := &prdcthsomod.ProductHistoryExport{ + ID: v.ID, // 交易订单号 + UID: v.UID, // 用户id + ProductID: v.ProductID, // 会员卡id + Name: v.Name, // 商品名字 + IsUpgrade: v.IsUpgrade, // 是否VIP升级 + Amount: v.Amount, // 花费的余额 + Income: v.Income, // 花费的收益 + SysType: v.SysType, // 设备系统类型 ios pc android + CreatedAt: v.CreatedAt, + } + if v.ProductSnapShot != nil { + item.Duration = v.ProductSnapShot.Duration + item.VipLevel = v.ProductSnapShot.VipLevel + item.OriginalPrice = v.ProductSnapShot.OriginalPrice + if v.ProductSnapShot.DiscountedPriceIos != nil { + item.DiscountedPriceIos = *v.ProductSnapShot.DiscountedPriceIos + } + if v.ProductSnapShot.DiscountedPriceAnd != nil { + item.DiscountedPriceAnd = *v.ProductSnapShot.DiscountedPriceAnd + } + } + switch v.ProductType { + case productmod.VIP: + item.ProductType = "会员卡" + case productmod.AdvanceCard: + item.ProductType = "预售卡" + } + switch v.AdvanceOrderStatus { + case advanceordermod.AdvanceSUCCESS: + item.AdvanceOrderStatus = "预付成功" + case advanceordermod.BalanceSUCCESS: + item.AdvanceOrderStatus = "尾款支付成功" + } + records = append(records, item) + } + + var fileName = fmt.Sprintf("预售订单列表_%v.xlsx", index) + if len(records) >= maxDataNum { + // 获取到的数据已经达到了单文件最大限制数据量 + filePath, err := SaveExcel(records, path, fileName) + if err != nil { + log.Error("ExecExportProductHistoryTask SaveExcel fail", log.E(err)) + return total, err + } + // 重新归0 + records = []*prdcthsomod.ProductHistoryExport{} + index = index + 1 + excelList = append(excelList, filePath) + fmt.Println("index:", index) + total = total + int64(len(records)) + } else if len(list) < int(size) { + if len(records) > 0 { + // 数据库已经取不到更多的数据了 + filePath, err := SaveExcel(records, path, fileName) + if err != nil { + log.Error("ExecExportProductHistoryTask SaveExcel fail", log.E(err)) + return total, err + } + excelList = append(excelList, filePath) + total = total + int64(len(records)) + } + // 结束循环 + break + } + skip = skip + size + } + + // 发送到tg + err = SendTg(task.Admin, excelList, zipName) + if err != nil { + log.Error("ExecExportProductHistoryTask SendTg fail", log.E(err)) + return + } + return total, nil +} diff --git a/skd/service/export_task/user_export.go b/skd/service/export_task/user_export.go new file mode 100644 index 0000000..904f7d6 --- /dev/null +++ b/skd/service/export_task/user_export.go @@ -0,0 +1,192 @@ +package export_task + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/models/l/playlgmod" + "91porn-server/models/v/export_task_mod" + "91porn-server/models/v/usermod" + "encoding/json" + "errors" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "sync" +) + +// ExecExportUserTask 执行用户导出任务 +func ExecExportUserTask(task *export_task_mod.ExportTask) (total int64, err error) { + type userListReq struct { + usermod.UserListSelector + common.StandQuery + IsPretendAcc *int `form:"isPretendAcc" json:"isPretendAcc"` //是否马甲账号 + } + var arg userListReq + err = json.Unmarshal([]byte(task.Param), &arg) + if err != nil { + return + } + if arg.StartTime == nil || arg.EndTime == nil { + return 0, errors.New("没有选择时间") + } + cond, _ := common.StandQueryMap(arg.StandQuery, arg.UserListSelector) + //马甲账户 + if arg.IsPretendAcc != nil && *arg.IsPretendAcc == 1 { + cond["devID"] = bson.M{"$regex": usermod.SystemDevIDPrex, "$options": "i"} + } + var skip int64 + var size int64 = 500 + records := []usermod.ExportUser{} + + zipName := fmt.Sprintf("%v_%v_用户列表_%v.zip", task.Admin, GetProName(), task.CreatedAt.Format("2006-01-02_15:04:05")) + index := 1 + + path := fmt.Sprintf("./temp/%v%v", "用户列表-", task.ID.Hex()) + excelList := []string{} + // 最后一条记录的时间 + for { + userList, err := usermod.ExportFindMany(cond, skip, size) + if err != nil { + log.Error("ExecExportUserTask fail", log.E(err)) + return total, err + } + videoCntMap := make(map[uint64]int) + unDealVideoCntMap := make(map[uint64]int) + // 针对91PORN做的优化,不然数据太多,导出数据文本太大,速度慢 + //if arg.IsPretendAcc != nil && *arg.IsPretendAcc == 1 { + // videoCntMap, unDealVideoCntMap, _ = vidmod.GetVideosByUIDs(getUIDs(userList)) + //} + eUsers := encodeUsers(userList, videoCntMap, unDealVideoCntMap) + + records = append(records, eUsers...) + var fileName = fmt.Sprintf("用户列表_%v.xlsx", index) + // 最多容纳5000条 + if len(records) >= maxDataNum { + // 直接写入excel + filePath, err := SaveExcel(records, path, fileName) + if err != nil { + log.Error("ExecExportVideoTask SaveExcel fail", log.E(err)) + return total, err + } + // 重置 + records = []usermod.ExportUser{} + index = index + 1 + excelList = append(excelList, filePath) + total = total + int64(len(records)) + } else if len(userList) < int(size) { + if len(records) > 0 { + // 直接写入excel + filePath, err := SaveExcel(records, path, fileName) + if err != nil { + log.Error("ExecExportVideoTask SaveExcel fail", log.E(err)) + return total, err + } + excelList = append(excelList, filePath) + total = total + int64(len(records)) + } + // 已经没有更多数据了,直接返回 + break + } + + skip = skip + size + } + // 发送到tg + err = SendTg(task.Admin, excelList, zipName) + if err != nil { + log.Error("ExecExportUserTask SendTg fail", log.E(err)) + return + } + return total, nil +} + +func encodeUsers(users []*usermod.User, videoCntMap map[uint64]int, unDealVideoCntMap map[uint64]int) []usermod.ExportUser { + usersLen := len(users) + uids := make([]uint64, 0, usersLen) + uInfos := make([]usermod.ExportUser, 0, usersLen) + for _, u := range users { + if u != nil { + user := usermod.ExportUser{ + UID: u.UID, + //DevID: u.DevID, + DevType: u.DevType, + RegisterIP: u.RegisterIP, + Mobile: u.Mobile, + //Gender: u.Gender, + Channel: u.DistrictCode, + //Name: u.Name, + PromotionCode: u.PromCode, + //Summary: u.Summary, + //Region: u.Region, + //Birthday: u.Birthday, + //VipLevel: u.VipLevel, + VipExpireDate: u.VipExpireDate, + CreatedAt: u.CreatedAt, + //MobileBindAt: u.MobileBindAt, + //HasLocked: u.HasLocked, + //HasBanned: u.HasBanned, + //TotalVideoCnt: videoCntMap[u.UID], + //UndealVideoCnt: unDealVideoCntMap[u.UID], + LastVisitAt: u.LastVisitAt, + } + uInfos = append(uInfos, user) + uids = append(uids, u.UID) + } + } + wCnt := setArray2Map(calWatchedVideoCnt(uids)) + for i, u := range uInfos { + uInfos[i].WatchCount = wCnt[u.UID] + } + return uInfos +} + +func getUIDs(users []*usermod.User) []uint64 { + uids := make([]uint64, 0, len(users)) + for _, v := range users { + if v == nil { + continue + } + uids = append(uids, v.UID) + } + return uids +} + +func setArray2Map(datas []playlgmod.UIDCount) map[uint64]int { + m := make(map[uint64]int) + for _, d := range datas { + m[d.UID] = d.Count + } + return m +} + +func calWatchedVideoCnt(uids []uint64) []playlgmod.UIDCount { + le := len(uids) + if le < 1000 { + datas, _ := playlgmod.HasWatchedVideoCnt(uids) + return datas + } + //创建多个协程去拉取 + const goRoutineCnt = 10 + var wg sync.WaitGroup + var mData [goRoutineCnt][]playlgmod.UIDCount + size := le / goRoutineCnt + wg.Add(goRoutineCnt) + for i := 0; i < goRoutineCnt-1; i++ { + common.GoParam(i, func(i int) { + defer wg.Done() + begin := i * size + end := begin + size - 1 + mData[i], _ = playlgmod.HasWatchedVideoCnt(uids[begin:end]) + }) + } + common.Go(func() { + defer wg.Done() + begin := (goRoutineCnt - 1) * size + end := le - 1 + mData[goRoutineCnt-1], _ = playlgmod.HasWatchedVideoCnt(uids[begin:end]) + }) + wg.Wait() + datas := []playlgmod.UIDCount{} + for i := 0; i < goRoutineCnt; i++ { + datas = append(datas, mData[i]...) + } + return datas +} diff --git a/skd/service/export_task/video_export.go b/skd/service/export_task/video_export.go new file mode 100644 index 0000000..d840f18 --- /dev/null +++ b/skd/service/export_task/video_export.go @@ -0,0 +1,219 @@ +package export_task + +import ( + "91porn-server/common/log" + "91porn-server/models/v/export_task_mod" + "91porn-server/models/v/modulevidmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/vidmod" + "91porn-server/web/vidhelp" + "encoding/json" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +func ExecExportVideoTask(task *export_task_mod.ExportTask) (total int64, err error) { + arg := vidmod.ListReq{} + err = json.Unmarshal([]byte(task.Param), &arg) + if err != nil { + return + } + cond, err := assembleConditions(arg) + if err != nil { + return + } + secID, cond, _, err := dealVidListCond(cond) + if err != nil { + return + } + + var skip int64 + var size int64 = 100 + index := 1 + path := fmt.Sprintf("./temp/%v%v", "帖子列表-", task.ID.Hex()) + zipName := fmt.Sprintf("%v_%v_帖子列表_%v.zip", task.Admin, GetProName(), task.CreatedAt.Format("2006-01-02_15:04:05")) + var records []*vidmod.WebVideo + excelList := []string{} + for { + fmt.Println("skip", skip, " now ", time.Now().Format("2006-01-02 15:04:05")) + list, err := vidmod.ExportFindMany(cond, skip, size) + if err != nil { + log.Error("ExecExportVideoTask vidmod.ExportFindMany fail", log.E(err)) + return total, err + } + infos, err := vidhelp.EncodeVideoInfo(list, secID) + if err != nil { + log.Error("ExecExportVideoTask vidhelp.EncodeVideoInfo fail", log.E(err)) + return total, err + } + records = append(records, infos...) + var fileName = fmt.Sprintf("帖子列表_%v.xlsx", index) + if len(records) >= maxDataNum { + // 获取到的数据已经达到了单文件最大限制数据量 + filePath, err := SaveExcel(records, path, fileName) + if err != nil { + log.Error("ExecExportVideoTask SaveExcel fail", log.E(err)) + return total, err + } + // 重新归0 + records = []*vidmod.WebVideo{} + index = index + 1 + excelList = append(excelList, filePath) + fmt.Println("index:", index) + total = total + int64(len(records)) + } else if len(list) < int(size) { + if len(records) > 0 { + // 数据库已经取不到更多的数据了 + filePath, err := SaveExcel(records, path, fileName) + if err != nil { + log.Error("ExecExportVideoTask SaveExcel fail", log.E(err)) + return total, err + } + excelList = append(excelList, filePath) + total = total + int64(len(records)) + } + // 结束循环 + break + } + skip = skip + size + } + + // 发送到tg + err = SendTg(task.Admin, excelList, zipName) + if err != nil { + log.Error("ExecExportVideoTask SendTg fail", log.E(err)) + return + } + return total, nil +} + +func dealVidListCond(cond bson.M) (primitive.ObjectID, bson.M, []primitive.ObjectID, error) { + var secID primitive.ObjectID + var err error + sectionID, ok := cond["sectionID"] + if ok { + secID, err = primitive.ObjectIDFromHex(sectionID.(string)) + if err != nil { + log.Error("primitive.ObjectIDFromHex", log.Any("sectionID", sectionID), log.E(err)) + return secID, cond, nil, err + } + var flag bool + isSorted, ok := cond["isSortedUnderModule"] + if ok { + flag = isSorted.(bool) + } + videoIDs, err := modulevidmod.GetBySectionID(secID, flag) + if err != nil { + return secID, cond, nil, err + } + cond["_id"] = bson.M{"$in": videoIDs} + delete(cond, "sectionID") + delete(cond, "isSortedUnderModule") + return secID, cond, videoIDs, nil + } + delete(cond, "sectionID") + delete(cond, "isSortedUnderModule") + return secID, cond, nil, nil +} + +func assembleConditions(req vidmod.ListReq) (m map[string]interface{}, err error) { + m = make(map[string]interface{}) + if req.ShowType != nil { + m["showType"] = *req.ShowType + } + if req.Status != 4 { + if req.Status == 7 { + m["status"] = map[string]int{"$gt": 0} + } else { + m["status"] = req.Status + } + } + if req.IsFree == 0 { + m["coins"] = map[string]int{"$gt": 0} + } + if req.IsFree == 1 { + m["coins"] = map[string]int{"$eq": 0} + } + if req.IsUserUp == 2 { + m["publisherID"] = map[string]int{"$lt": 115000} + } + if req.IsUserUp == 1 { + m["publisherID"] = map[string]int{"$gt": 115000} + } + if len(req.Title) != 0 { + m["title"] = map[string]string{"$regex": req.Title, "$options": "i"} + } + if req.UID > 0 { + m["publisherID"] = req.UID + } + if req.Chosen == 1 { + m["chosen"] = true + } + if req.Chosen == 2 { + m["chosen"] = false + } + if req.FreeArea == 1 { + m["freeArea"] = true + } + if req.FreeArea == 2 { + m["freeArea"] = false + } + if !req.End.IsZero() { + m["createdAt"] = map[string]time.Time{"$gte": req.Start, "$lt": req.End} + } + if len(req.Tag) != 0 { + id, err := tagmod.GetTagIDByName(req.Tag) + if err != nil { + return m, err + } + m["tags"] = id + } + if len(req.ID) != 0 { + oid, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + return m, err + } + m["_id"] = oid + } + //通过初始价格判断是否是马甲账号 + if req.IsPretendAcc == 1 { + m["coins"] = vidmod.PretendAccInitCoins + } + if req.IsPretendAcc == 2 { + //i := make(map[string]int64) + //i["$ne"] = vidmod.PretendAccInitCoins + m["coins"] = map[string]int64{"$ne": vidmod.PretendAccInitCoins} + } + m["deleteAt"] = bson.M{"$exists": false} + if req.NewsType != "" { + m["newsType"] = req.NewsType + } + + if req.LiaoBaTop != nil { + m["liaoBaTop"] = *req.LiaoBaTop + } + if req.SectionID != "" { + m["sectionID"] = req.SectionID + } + m["isSortedUnderModule"] = req.IsSortedUnderModule + if req.IsRecommended != nil { + if *req.IsRecommended { + m["recoWeight"] = bson.M{"$gte": 0} + } else { + m["recoWeight"] = bson.M{"$lt": 0} + } + } + if req.Key == "likeRate" || req.Key == "purchaseRate" { + m[req.Key] = req.Value + } + if req.IsHappinessPlazaTop != nil && *req.IsHappinessPlazaTop { + if *req.IsHappinessPlazaTop { + m["happinessPlazaTop"] = bson.M{"$gt": 0} + } else { + m["happinessPlazaTop"] = bson.M{"$lte": 0} + } + } + return m, nil +} diff --git a/skd/service/fakeVideoInfo.go b/skd/service/fakeVideoInfo.go new file mode 100644 index 0000000..689e37a --- /dev/null +++ b/skd/service/fakeVideoInfo.go @@ -0,0 +1,122 @@ +package service + +import ( + "sync" + "time" + + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/chatrobotmod" + "91porn-server/models/v/vidmod" + + "github.com/robfig/cron/v3" + "go.mongodb.org/mongo-driver/bson" +) + +const SingleQueryLimit = 5000 + +var VideoInfo *VideoInfoFaker + +type VideoInfoFaker struct { + once sync.Once + limit int64 // 限制每次查询的数据量 + mu *sync.Mutex +} + +func newVideoInfoFaker() *VideoInfoFaker { + return &VideoInfoFaker{ + once: sync.Once{}, + limit: SingleQueryLimit, + mu: &sync.Mutex{}, + } +} + +// FullUpdate 单次更新,程序启动后执行一次 +func (f *VideoInfoFaker) FullUpdate() { + f.once.Do( + func() { + f.mu.Lock() + log.Debug("full update start...") + common.Go(func() { + defer f.mu.Unlock() + start := time.Now() + totalUpdate, err := f.fakeVideoInfo() + log.Info("FullUpdate fihish", + log.Any("time(s)", time.Since(start).Seconds()), + log.Any("total", totalUpdate), + log.E(err)) + }) + }, + ) +} + +// CronUpdate 定时更新 +func (f *VideoInfoFaker) CronUpdate() { + f.mu.Lock() + defer f.mu.Unlock() + start := time.Now() + totalUpdate, err := f.fakeVideoInfo() + log.Info("CronUpdate fihish", + log.Any("time(s)", time.Since(start).Seconds()), + log.Any("total", totalUpdate), + log.E(err)) +} + +func (f *VideoInfoFaker) RegisterCronJob(cron *cron.Cron) { + f.mu.Lock() + defer f.mu.Unlock() + _, _ = cron.AddFunc("0 0 10 */1 * *", f.CronUpdate) // 每隔三天,早上10点,仅在周一到周六执行 +} + +// FakeVideoInfo 更新视频相关数据 +func (f *VideoInfoFaker) fakeVideoInfo() (int64, error) { + robotConf, err := chatrobotmod.GetRobotConfByType(chatrobotmod.Comment) + if err != nil || robotConf.ID.IsZero() { + return 0, err + } + if (robotConf.MaxFakeLikes <= 0 && robotConf.MaxFakePlayCount <= 0) || + (robotConf.MinFakeLikes >= robotConf.MaxFakeLikes && + robotConf.MinFakePlayCount >= robotConf.MaxFakePlayCount) { + return 0, stderr.RobotInvalidConf + } + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + pageSize, page, totalUpdate := f.limit, int64(0), int64(0) + for pageSize == f.limit { + videoList, err := vidmod.GetVideoListLtMinPlayLikeCount(int64(robotConf.MinFakeLikes), + int64(robotConf.MinFakePlayCount), page*pageSize, pageSize) + if err != nil { + return totalUpdate, err + } + cnt := int64(0) + for _, video := range videoList { + set := bson.M{} + if video.FakeLikeCount < robotConf.MinFakeLikes { + destLike := common.RandInt(robotConf.MinFakeLikes, robotConf.MaxFakeLikes) + set["fakeLikeCount"] = destLike + } + if video.FakePlayCount < robotConf.MinFakePlayCount { + destPlay := common.RandInt(robotConf.MinFakePlayCount, robotConf.MaxFakePlayCount) + video.FakePlayCount = destPlay + set["fakePlayCount"] = destPlay + } + if set["fakePlayCount"] != 0 || set["fakeLikeCount"] != 0 { + // log.Info("update", log.Any("set", set), log.Any("vid", video.ID), log.Any("origin like", video.FakeLikeCount), log.Any("origin play", video.PlayCount)) + res, err := vidmod.UpdateOneByID(video.ID, set) + if err != nil { + // 遇到错误继续执行 + log.Error("fakeVideoInfo UpdateOneByID", log.Any("videoID", video.ID), log.E(err)) + } else { + cnt += res.ModifiedCount + } + } + } + page++ + pageSize = int64(len(videoList)) + totalUpdate += cnt + log.Info("fake progress", log.Any("page", page), log.Any("total", totalUpdate)) + <-ticker.C + } + return totalUpdate, nil +} diff --git a/skd/service/init.go b/skd/service/init.go new file mode 100644 index 0000000..9e2aaba --- /dev/null +++ b/skd/service/init.go @@ -0,0 +1,5 @@ +package service + +func Init() { + VideoInfo = newVideoInfoFaker() +} diff --git a/skd/service/promotionCode.go b/skd/service/promotionCode.go new file mode 100644 index 0000000..df51fd9 --- /dev/null +++ b/skd/service/promotionCode.go @@ -0,0 +1,58 @@ +package service + +import ( + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/models/v/usermod" + "91porn-server/skd/skdg" +) + +// 填充推广码 +func MakePromotionCode() { + redisKey := redisconst.PromotionCodeKey() + var low int64 = 5000 + total, err := skdg.Redis.SCard(redisKey) + if err != nil { + log.Warn("service MakePromotionCode SCard error", log.E(err), log.Any("total", total)) + return + } + if total < low { + savePromotionCode(total) + } +} + +func savePromotionCode(total int64) { + var max int64 = 9999 + size := 100 + redisKey := redisconst.PromotionCodeKey() + if total < max { + promotionCodeCollection := make([]string, size) + for i := 0; i < size; i++ { + p, err := getPromotionCode() + if err != nil { + break + } + promotionCodeCollection[i] = p + } + count, err := skdg.Redis.SAdd(redisKey, promotionCodeCollection) + if err != nil { + log.Warn("service savePromotionCode SAdd error", log.E(err), log.Any("total", total)) + return + } + savePromotionCode(total + count) + } +} + +// 获取不重复的推广码 +func getPromotionCode() (string, error) { + promotionCode := common.InvitePromotionCodeGenera() + u, err := usermod.FindUserPromotionCode(promotionCode) + if err != nil { + return "", err + } + if u == nil { + return promotionCode, nil + } + return getPromotionCode() +} diff --git a/skd/service/updownload.go b/skd/service/updownload.go new file mode 100644 index 0000000..7004248 --- /dev/null +++ b/skd/service/updownload.go @@ -0,0 +1,102 @@ +package service + +import ( + "91porn-server/common" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/vidmod" + "91porn-server/skd/skdg" + "strconv" + "strings" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// SyncFileFromFs 从文件服务器同步文件信息 +func SyncFileFromFs(vID string) (code stderr.Code) { + v, _ := vidmod.GetVideoInfo(vID) + resp, err := PullFileInfo(v.SourceID) + if err != nil { + return stderr.ErrConnectToFs + } + if resp.Code == stderr.UpLoadFileComplete { + oid, _ := primitive.ObjectIDFromHex(resp.Data.ID) + sourceID := resp.Data.ID + playTime := resp.Data.PlayTime + freeTime := vidmod.GetFreeTime(resp.Data.PlayTime) + if v.FreeTime != 0 && freeTime != v.FreeTime { + freeTime = v.FreeTime + } + seriesCover := v.SeriesCover + resolution := strconv.FormatInt(int64(resp.Data.Width), 10) + "*" + strconv.FormatInt(int64(resp.Data.Height), 10) + width := resp.Data.Width + height := resp.Data.Height + identifi := common.IdentifiVideoInfo(int64(resp.Data.Width), int64(resp.Data.Height)) + direction := string(identifi.Direction) + quality := string(identifi.Quality) + md5 := resp.Data.CheckSum + actor := strings.Join(resp.Data.Actors, ",") + size := resp.Data.Size + filename := resp.Data.Filename + via := resp.Data.Via + ratio := resp.Data.Ratio + update := vidmod.WebVideoUpdateDoc{ + ID: oid, + SourceID: &sourceID, + PlayTime: &playTime, + FreeTime: &freeTime, + SeriesCover: &seriesCover, + Resolution: &resolution, + Width: &width, + Height: &height, + MD5: &md5, + Actor: &actor, + Size: &size, + Filename: &filename, + Via: &via, + Ratio: &ratio, + Direction: &direction, + Quality: &quality, + } + if v.Cover == "" { + cover := resp.Data.FieldNameFs + "-1.jpg" + update.Cover = &cover + } + if v.CoverThumb == "" { + coverThumb := resp.Data.FieldNameFs + "-2.jpg" + update.CoverThumb = &coverThumb + } + _, _ = vidmod.UpdateVideoResolutionPlayTime(v.ID, update) + } + return resp.Code +} + +// PullFileInfo 获取文件转码情况及详情 +func PullFileInfo(id string) (respBody vidmod.PuFinfoResp, err error) { + client := httputil.Client() + code, err := client.POSTWithJResp(&respBody, common.BindUrl(skdg.Conf.Url.PullFileInfo, id), nil, nil) + log.Info("http method PullFileInfo response code ==>", log.Any("statusCode", code), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("Pull FileInfo From Aws ", log.Any("Error", err)) + respBody.Code = stderr.ErrConnectToFs + return + } + switch respBody.Data.Status { + case vidmod.Merging, vidmod.MergeCompleted: + respBody.Code = stderr.FileMerging + case vidmod.MergeError: + respBody.Code = stderr.ErrMergeFile + case vidmod.Converting: + respBody.Code = stderr.FileConverting + case vidmod.ConvertError: + respBody.Code = stderr.ErrConvertFile + case vidmod.UploadLoadingToFs: + respBody.Code = stderr.UploadLoadingToFs + case vidmod.FileUploadError: + respBody.Code = stderr.ErrUploadError + case vidmod.ConvertCompleted, vidmod.Completed: + respBody.Code = stderr.UpLoadFileComplete + } + return +} diff --git a/skd/service/versionser.go b/skd/service/versionser.go new file mode 100644 index 0000000..79cce7d --- /dev/null +++ b/skd/service/versionser.go @@ -0,0 +1,33 @@ +package service + +import ( + "91porn-server/common" + "91porn-server/common/crypt" +) + +func GetTFUrl(tf string) string { + if tf != "" { + type tfStc struct { + Url string `json:"url"` + Name string `json:"name"` + } + var tfArr []tfStc + _ = crypt.JSON2Struct(tf, &tfArr) + if len(tfArr) == 0 { + return tf + } + weight := 100 / len(tfArr) + chioce := make([]common.Choice, len(tfArr)) + for i := range tfArr { + chioce[i] = common.Choice{ + Weight: weight, + Item: tfArr[i].Url, + } + } + ch, _ := common.WeightedChoice(chioce) + if v, ok := ch.Item.(string); ok { + return v + } + } + return tf +} diff --git a/skd/skdg/elastic.go b/skd/skdg/elastic.go new file mode 100644 index 0000000..7cfcc90 --- /dev/null +++ b/skd/skdg/elastic.go @@ -0,0 +1,24 @@ +package skdg + +import ( + "os" + + "91porn-server/common/elastic" + "91porn-server/common/log" +) + +func InitElastic() { + opts := elastic.Options{ + Address: []string{Conf.Elastic.VideoUrl}, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 30, + UserName: Conf.Elastic.UserName, + PassWord: Conf.Elastic.PassWord, + } + c, err := elastic.InitElastic(opts) + if err != nil { + log.Error("startUp VideoES InitElastic error", log.E(err)) + os.Exit(1) + } + VideoES = c +} diff --git a/skd/skdg/redisg.go b/skd/skdg/redisg.go new file mode 100644 index 0000000..f7919fd --- /dev/null +++ b/skd/skdg/redisg.go @@ -0,0 +1,28 @@ +package skdg + +import ( + "os" + + "91porn-server/common/log" + "91porn-server/common/redis" +) + +func InitRedis() { + var err error + Redis, err = redis.WithURL(Conf.Redis.URL) + if err != nil { + log.Error("startUp udb redis error", log.E(err)) + os.Exit(1) + } +} + +func RedisClose() (err error) { + if Redis != nil { + if err := Redis.Close(); err != nil { + log.Error("RUdb Redis Close Error", log.E(err)) + return err + } + log.Info("RUdb Redis Close OK") + } + return nil +} diff --git a/skd/skdg/shortrecommend.go b/skd/skdg/shortrecommend.go new file mode 100644 index 0000000..20f05cf --- /dev/null +++ b/skd/skdg/shortrecommend.go @@ -0,0 +1,49 @@ +package skdg + +const ( + defaultShortRecommendCron = "CRON_TZ=Asia/Shanghai 0 5/10 * * * ?" + defaultShortRecommendKeyTTLHours = 72 + defaultProdInitializationLimit = 50_000 +) + +// ShortRecommendConfig 控制定时队列生成。Enabled 未配置时默认启用。 +type ShortRecommendConfig struct { + Enabled *bool `json:"enabled"` + Cron string `json:"cron"` + KeyTTLHours int `json:"keyTTLHours"` + MaxInitializationWrites *int64 `json:"maxInitializationWrites"` +} + +func (c *GlobalConfig) ShortRecommendEnabled() bool { + return c == nil || c.ShortRecommend.Enabled == nil || *c.ShortRecommend.Enabled +} + +func (c *GlobalConfig) ShortRecommendCron() string { + if c == nil || c.ShortRecommend.Cron == "" { + return defaultShortRecommendCron + } + return c.ShortRecommend.Cron +} + +func (c *GlobalConfig) ShortRecommendKeyTTL() int { + if c == nil || c.ShortRecommend.KeyTTLHours <= 0 { + return defaultShortRecommendKeyTTLHours + } + return c.ShortRecommend.KeyTTLHours +} + +func (c *GlobalConfig) ShortRecommendInitializationLimit() int64 { + if c == nil { + return 0 + } + if c.ShortRecommend.MaxInitializationWrites != nil { + if *c.ShortRecommend.MaxInitializationWrites < 0 { + return 0 + } + return *c.ShortRecommend.MaxInitializationWrites + } + if c.Base.Env == "prod" { + return defaultProdInitializationLimit + } + return 0 +} diff --git a/skd/skdg/shortrecommend_test.go b/skd/skdg/shortrecommend_test.go new file mode 100644 index 0000000..ad13f12 --- /dev/null +++ b/skd/skdg/shortrecommend_test.go @@ -0,0 +1,51 @@ +package skdg + +import "testing" + +func TestShortRecommendDefaultsPreserveLegacyDeployment(t *testing.T) { + var nilConfig *GlobalConfig + if !nilConfig.ShortRecommendEnabled() { + t.Fatal("nil config should default to enabled") + } + if got := nilConfig.ShortRecommendCron(); got != defaultShortRecommendCron { + t.Fatalf("cron = %q, want %q", got, defaultShortRecommendCron) + } + if got := nilConfig.ShortRecommendKeyTTL(); got != 72 { + t.Fatalf("TTL hours = %d, want 72", got) + } +} + +func TestShortRecommendExplicitConfig(t *testing.T) { + disabled := false + cfg := &GlobalConfig{} + cfg.ShortRecommend.Enabled = &disabled + cfg.ShortRecommend.Cron = "0 0 5 * * ?" + cfg.ShortRecommend.KeyTTLHours = 96 + initializationLimit := int64(10_000) + cfg.ShortRecommend.MaxInitializationWrites = &initializationLimit + if cfg.ShortRecommendEnabled() { + t.Fatal("configured disabled should be honored") + } + if got := cfg.ShortRecommendCron(); got != cfg.ShortRecommend.Cron { + t.Fatalf("cron = %q", got) + } + if got := cfg.ShortRecommendKeyTTL(); got != 96 { + t.Fatalf("TTL hours = %d", got) + } + if got := cfg.ShortRecommendInitializationLimit(); got != 10_000 { + t.Fatalf("initialization limit = %d", got) + } +} + +func TestShortRecommendProductionDefaultsToFailClosedInitializationGuard(t *testing.T) { + cfg := &GlobalConfig{} + cfg.Base.Env = "prod" + if got := cfg.ShortRecommendInitializationLimit(); got != defaultProdInitializationLimit { + t.Fatalf("initialization limit = %d, want %d", got, defaultProdInitializationLimit) + } + unlimited := int64(0) + cfg.ShortRecommend.MaxInitializationWrites = &unlimited + if got := cfg.ShortRecommendInitializationLimit(); got != 0 { + t.Fatalf("explicit initialization limit = %d, want 0", got) + } +} diff --git a/skd/skdg/skdg.go b/skd/skdg/skdg.go new file mode 100644 index 0000000..8713427 --- /dev/null +++ b/skd/skdg/skdg.go @@ -0,0 +1,105 @@ +package skdg + +import ( + "91porn-server/common/constant" + "91porn-server/common/db" + "91porn-server/common/elastic" + "91porn-server/common/redis" + "91porn-server/common/tg" +) + +type GlobalConfig struct { + Base struct { + Port int64 `json:"port"` + Env string `json:"env"` + // 媒体分片(TS)鉴权签名密钥对象(含 keyVersion/key);未配置时回退内置默认(版本 default + constant.MediaSourceAuthKey) + TsAuth constant.TsAuthKeyConfig `json:"tsAuth"` + } `json:"base"` + Mongo struct { + VideoDbUrl string `json:"videoDbUrl"` //主数据库 存放强业务相关的数据 + StatDbUrl string `json:"statDbUrl"` //统计数据库 存放统计 日志 渠道相关的数据 + LogDbUrl string `json:"logDbUrl"` //日志数据库 + SpiderDbUrl string `json:"spiderDbUrl"` //爬虫库 + } `json:"mongo"` + Log struct { + Level string `json:"level"` + } `json:"log"` + Redis struct { + URL string `json:"url"` + } `json:"redis"` + ShortRecommend ShortRecommendConfig `json:"shortRecommend"` + DataReport struct { + Url string `json:"url"` + AppUrl string `json:"appUrl"` + } `json:"dataReport"` + DaiChong struct { + AppID string `json:"appId"` + AppSecret string `json:"appSecret"` + PlatName string `json:"platName"` + Domain string `json:"domain"` + } `json:"daichong"` + LSJ struct { + AppID string `json:"appId"` + APIKey string `json:"apiKey"` + APIUrl string `json:"apiUrl"` + ImageYuan string `json:"imageYuan"` + NoticeURL string `json:"noticeUrl"` + } `json:"lsj"` + Hevc struct { + EnableTranscode *bool `json:"enableTranscode"` // nil: 仅生产环境默认开启;true: 显式开启;false: 所有环境熔断 + PullSecret string `json:"pullSecret"` // 与 App 共享的 H265 拉流签名密钥,至少 32 个随机字节 + } `json:"hevc"` + AdCenter struct { + ApiDomain string `json:"apiDomain"` // api 地址 + MerchantCode string `json:"merchantCode"` // 商户 code + AppCode string `json:"appCode"` // 应用 code + AesKey string `json:"aesKey"` // 数据解密 KEY + } `json:"adCenter"` + ImV2 struct { + BaseURL string `json:"baseUrl"` + DynamicConfigDomain string `json:"dynamicConfigDomain"` + SocketURL string `json:"socketUrl"` + MerchantCode string `json:"merchantCode"` + TenantCode string `json:"tenantCode"` + AppKey string `json:"appKey"` + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + } `json:"imv2"` + Elastic struct { + VideoUrl string `json:"videoUrl"` + UserName string `json:"userName"` + PassWord string `json:"passWord"` + } `json:"elastic"` + Url struct { + CheckUrl string `json:"checkUrl"` + SpiderDataURL string `json:"spiderDataUrl"` //统计中心数据同步域名 + PullFileInfo string `json:"pullFileInfo"` + AiServer string `json:"aiServer"` //ai订单请求地址 + AppApiUrl string `json:"appApiUrl"` // App 对外 HTTPS origin(无路径),供 H265 云转码拉取源 m3u8 + } `json:"url"` + Switch struct { + NotifyExpire bool `json:"notifyExpire"` + } `json:"switch"` + StartDate string `json:"startDate"` + MediaResourcesCfg struct { + UploadMediaUrl string `json:"uploadMediaUrl"` // 请求域名 + ProId string `json:"proId"` // 商户号 + AppSecret string `json:"appSecret"` // 商户密钥 + } `json:"mediaResourceCfg"` //媒体资源库配置 +} + +var ( + Conf *GlobalConfig + Redis *redis.Client + VideoES *elastic.Client + Static *StaticSource + StatDB *db.MongoDB + VideoDB *db.MongoDB + LogDB *db.MongoDB + SpiderDB *db.MongoDB + Bot *tg.Bot +) + +type StaticSource struct { + Portrait []string `json:"portrait"` +} diff --git a/skd/skdg/static.go b/skd/skdg/static.go new file mode 100644 index 0000000..dd29147 --- /dev/null +++ b/skd/skdg/static.go @@ -0,0 +1,20 @@ +package skdg + +import ( + "fmt" + + "91porn-server/common/conf" +) + +func InitStatic() { + initStatic() +} + +func initStatic() { + static := StaticSource{} + err := conf.LoadJSON("config/static.json", &static) + if err != nil { + fmt.Printf("startUp load static error: %v\n", err) + } + Static = &static +} diff --git a/sshpass.rb b/sshpass.rb new file mode 100644 index 0000000..af66d52 --- /dev/null +++ b/sshpass.rb @@ -0,0 +1,17 @@ +require 'formula' + +class Sshpass < Formula + url 'http://sourceforge.net/projects/sshpass/files/sshpass/1.06/sshpass-1.06.tar.gz' + homepage 'http://sourceforge.net/projects/sshpass' + sha256 'c6324fcee608b99a58f9870157dfa754837f8c48be3df0f5e2f3accf145dee60' + + def install + system "./configure", "--disable-debug", "--disable-dependency-tracking", + "--prefix=#{prefix}" + system "make install" + end + + def test + system "sshpass" + end +end \ No newline at end of file diff --git a/swagger/main.go b/swagger/main.go new file mode 100644 index 0000000..657ee54 --- /dev/null +++ b/swagger/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + "gopkg.in/yaml.v2" + "io/ioutil" + _ "91porn-server/docs" +) + +var SwagHandler gin.HandlerFunc + +func init() { + SwagHandler = ginSwagger.WrapHandler(swaggerFiles.Handler) +} + +type GlobalCfgEx struct { + ListenAddr string `yaml:"listenAddr"` +} + +// @title soul2-server 项目API文档 +// @version 1.0 +// @description 项目接口文档 +// @termsOfService soul2-Server + +// @contact.name soul2 +// @contact.url soul2@gamil.com +// @contact.email soul2@gamil.com + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +func main() { + gin.SetMode(gin.ReleaseMode) + buf, err := ioutil.ReadFile("conf/swagger.yaml") + if err != nil { + panic(err) + return + } + conf := GlobalCfgEx{} + if err = yaml.Unmarshal(buf, &conf); err != nil { + panic(err) + return + } + r := gin.Default() + if SwagHandler != nil { + r.GET("/swagger/*any", SwagHandler) + } + fmt.Printf("swagger server start list port:[%v]\n", conf.ListenAddr) + _ = r.Run(fmt.Sprintf(":%v", conf.ListenAddr)) +} diff --git a/tests/checkin/go.mod b/tests/checkin/go.mod new file mode 100644 index 0000000..3c95832 --- /dev/null +++ b/tests/checkin/go.mod @@ -0,0 +1,118 @@ +module 91porn-server/tests/checkin + +go 1.26.1 + +require ( + 91porn-server v0.0.0 + git.ruishengces.com/pub_tool/autotest v0.0.0-20260430042504-d6b5bf976a06 + github.com/gin-gonic/gin v1.9.1 + go.mongodb.org/mongo-driver v1.17.9 +) + +require ( + github.com/IBM/sarama v1.45.2 // indirect + github.com/aws/aws-sdk-go v1.44.260 // indirect + github.com/badoux/checkmail v1.2.1 // indirect + github.com/bxcodec/faker/v3 v3.1.0 // indirect + github.com/bytedance/sonic v1.9.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/ddliu/go-httpclient v0.7.1 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/didip/tollbooth v4.0.2+incompatible // indirect + github.com/didip/tollbooth_gin v0.0.0-20170928041415-5752492be505 // indirect + github.com/eapache/go-resiliency v1.7.0 // indirect + github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect + github.com/eapache/queue v1.1.0 // indirect + github.com/elastic/elastic-transport-go/v8 v8.2.0 // indirect + github.com/elastic/go-elasticsearch/v8 v8.8.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/pprof v1.4.0 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-ego/gpy v0.42.1 // indirect + github.com/go-ego/gse v0.80.2 // indirect + github.com/go-playground/form v3.1.4+incompatible // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/go-redis/redis v6.15.9+incompatible // indirect + github.com/go-redsync/redsync/v4 v4.8.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/golang-module/carbon/v2 v2.3.10 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gopherjs/gopherjs v1.20.2 // indirect + github.com/grafov/m3u8 v0.12.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect + github.com/jinzhu/copier v0.3.5 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/jtolds/gls v4.20.0+incompatible // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/makiuchi-d/gozxing v0.1.1 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/moul/http2curl v1.0.0 // indirect + github.com/mozillazg/go-pinyin v0.19.0 // indirect + github.com/nyaruka/phonenumbers v1.1.6 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/redis/go-redis/v9 v9.18.0 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.3 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect + github.com/satori/go.uuid v1.2.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect + github.com/smarty/assertions v1.16.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + github.com/vcaesar/cedar v0.20.1 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 // indirect + github.com/xuri/excelize/v2 v2.7.1 // indirect + github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace 91porn-server => ../.. diff --git a/tests/checkin/go.sum b/tests/checkin/go.sum new file mode 100644 index 0000000..c2c1bb9 --- /dev/null +++ b/tests/checkin/go.sum @@ -0,0 +1,446 @@ +git.ruishengces.com/pub_tool/autotest v0.0.0-20260430042504-d6b5bf976a06 h1:ZRcu8KfH+aHrKMIIYF6a03EKkbTWF5b6iE98POBt1oY= +git.ruishengces.com/pub_tool/autotest v0.0.0-20260430042504-d6b5bf976a06/go.mod h1:HnkqUUd7M6qkYdyuR61pdVeHYtTsjP5HbPsEp6t3iiI= +github.com/IBM/sarama v1.45.2 h1:8m8LcMCu3REcwpa7fCP6v2fuPuzVwXDAM2DOv3CBrKw= +github.com/IBM/sarama v1.45.2/go.mod h1:ppaoTcVdGv186/z6MEKsMm70A5fwJfRTpstI37kVn3Y= +github.com/aws/aws-sdk-go v1.44.260 h1:78IJkDpDPXvLXvIkNAKDP/i3z8Vj+3sTAtQYw/v/2o8= +github.com/aws/aws-sdk-go v1.44.260/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/badoux/checkmail v1.2.1 h1:TzwYx5pnsV6anJweMx2auXdekBwGr/yt1GgalIx9nBQ= +github.com/badoux/checkmail v1.2.1/go.mod h1:XroCOBU5zzZJcLvgwU15I+2xXyCdTWXyR9MGfRhBYy0= +github.com/bsm/ginkgo/v2 v2.5.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.20.0/go.mod h1:JifAceMQ4crZIWYUKrlGcmbN3bqHogVTADMD2ATsbwk= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bxcodec/faker/v3 v3.1.0 h1:VCCPusvvk1My6RjWFnqVbh6EdHDqjWmrHJCHduUksV0= +github.com/bxcodec/faker/v3 v3.1.0/go.mod h1:gF31YgnMSMKgkvl+fyEo1xuSMbEuieyqfeslGYFjneM= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ddliu/go-httpclient v0.7.1 h1:COWYBalfbaFNe6e0eQU38++vCD5kzLh1H1RFs3xcn9g= +github.com/ddliu/go-httpclient v0.7.1/go.mod h1:uwipe9x9SYGk4JhBemO7+dD87QbiY224y0DLB9OY0Ik= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M= +github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY= +github.com/didip/tollbooth_gin v0.0.0-20170928041415-5752492be505 h1:VkJBA707rG0mOUM5nuqTs53hlJEb6peXnY7elFDWh88= +github.com/didip/tollbooth_gin v0.0.0-20170928041415-5752492be505/go.mod h1:ieayd+rxBVaj62fhAdF5p1U70Y4ZCcfpk0+4jesd0f8= +github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= +github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/elastic/elastic-transport-go/v8 v8.0.0-20230329154755-1a3c63de0db6/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI= +github.com/elastic/elastic-transport-go/v8 v8.2.0 h1:hkK5IIs/15mpSXzd5THWVlWTKJyMw6cbCWM3T/B2S5E= +github.com/elastic/elastic-transport-go/v8 v8.2.0/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI= +github.com/elastic/go-elasticsearch/v8 v8.8.0 h1:yNBPlXNo6wstMG7I3KiZPbLFgA82RMryYqkh1xBMV3A= +github.com/elastic/go-elasticsearch/v8 v8.8.0/go.mod h1:NGmpvohKiRHXI0Sw4fuUGn6hYOmAXlyCphKpzVBiqDE= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg= +github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-ego/gpy v0.42.1 h1:P/doarxZi2J5qwNzLIM0ESd3VXLXMpdhwO/GNontLdE= +github.com/go-ego/gpy v0.42.1/go.mod h1:eBtY3/uCDqBFog4ES9L00xytTpXar/0mGf45Rb7zcgg= +github.com/go-ego/gse v0.80.2 h1:3LRfkaBuwlsHsmkOZvnhTcsYPXUAhiP06Sqcid7mO1M= +github.com/go-ego/gse v0.80.2/go.mod h1:kesekpZfcFQ/kwd9b27VZHUOH5dQUjaaQUZ4OGt4Hj4= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/form v3.1.4+incompatible h1:lvKiHVxE2WvzDIoyMnWcjyiBxKt2+uFJyZcPYWsLnjI= +github.com/go-playground/form v3.1.4+incompatible/go.mod h1:lhcKXfTuhRtIZCIKUeJ0b5F207aeQCPbZU09ScKjwWg= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4= +github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= +github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= +github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= +github.com/go-redsync/redsync/v4 v4.8.1 h1:rq2RvdTI0obznMdxKUWGdmmulo7lS9yCzb8fgDKOlbM= +github.com/go-redsync/redsync/v4 v4.8.1/go.mod h1:LmUAsQuQxhzZAoGY7JS6+dNhNmZyonMZiiEDY9plotM= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-module/carbon/v2 v2.3.10 h1:C25x4A4UrIch6bisV3j37eU+op5+cp4gw/Fffv5c/FA= +github.com/golang-module/carbon/v2 v2.3.10/go.mod h1:XDALX7KgqmHk95xyLeaqX9/LJGbfLATyruTziq68SZ8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= +github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v1.20.2 h1:mzF/NBZH47L63jqg19OQgXv32FYRvFZVWom8PiQ2HbU= +github.com/gopherjs/gopherjs v1.20.2/go.mod h1:h+FTmmLgbXMmmtuZFp9bUqXciN429Wx0sJEJuMnpyfM= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/grafov/m3u8 v0.12.0 h1:T6iTwTsSEtMcwkayef+FJO8kj+Sglr4Lh81Zj8Ked/4= +github.com/grafov/m3u8 v0.12.0/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= +github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I= +github.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/mozillazg/go-pinyin v0.19.0 h1:p+J8/kjJ558KPvVGYLvqBhxf8jbZA2exSLCs2uUVN8c= +github.com/mozillazg/go-pinyin v0.19.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/nyaruka/phonenumbers v1.1.6 h1:DcueYq7QrOArAprAYNoQfDgp0KetO4LqtnBtQC6Wyes= +github.com/nyaruka/phonenumbers v1.1.6/go.mod h1:yShPJHDSH3aTKzCbXyVxNpbl2kA+F+Ne5Pun/MvFRos= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= +github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM= +github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko= +github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/smarty/assertions v1.16.0 h1:EvHNkdRA4QHMrn75NZSoUQ/mAUXAYWfatfB01yTCzfY= +github.com/smarty/assertions v1.16.0/go.mod h1:duaaFdCS0K9dnoM50iyek/eYINOZ64gbh1Xlf6LG7AI= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= +github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/vcaesar/cedar v0.20.1 h1:cDOmYWdprO7ZW8cngJrDi8Zivnscj9dA/y8Y+2SB1P0= +github.com/vcaesar/cedar v0.20.1/go.mod h1:iMDweyuW76RvSrCkQeZeQk4iCbshiPzcCvcGCtpM7iI= +github.com/vcaesar/tt v0.20.0 h1:9t2Ycb9RNHcP0WgQgIaRKJBB+FrRdejuaL6uWIHuoBA= +github.com/vcaesar/tt v0.20.0/go.mod h1:GHPxQYhn+7OgKakRusH7KJ0M5MhywoeLb8Fcffs/Gtg= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 h1:ge5g8vsTQclA5lXDi+PuiAFw5GMIlMHOB/5e1hsf96E= +github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.7.1 h1:gm8q0UCAyaTt3MEF5wWMjVdmthm2EHAWesGSKS9tdVI= +github.com/xuri/excelize/v2 v2.7.1/go.mod h1:qc0+2j4TvAUrBw36ATtcTeC1VCM0fFdAXZOmcF4nTpY= +github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83 h1:xVwnvkzzi+OiwhIkWOXvh1skFI6bagk8OvGuazM80Rw= +github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= +golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/tests/checkin/main.go b/tests/checkin/main.go new file mode 100644 index 0000000..8b1598e --- /dev/null +++ b/tests/checkin/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "os" + "time" + + fw "git.ruishengces.com/pub_tool/autotest" + "git.ruishengces.com/pub_tool/autotest/reporter" +) + +func main() { + cfg := fw.ParseFlags("checkin") + + env := &Env{} + base, cleanup := fw.ManageEnv(env, fw.EnvConfig{}, cfg) + defer cleanup() + + summary := fw.NewRunner("签到模块测试", cfg). + Report(reporter.Console()). + Report(reporter.HTML("签到模块测试", "reports", env.Meta())). + Report(reporter.JSON("reports")). + Register( + &CheckinSuite{env: env, url: base.AppBaseURL}, + ). + Build(). + Run() + + fmt.Printf("\n总计: %d 通过, %d 失败, 耗时 %s\n", + summary.Stats.Passed, summary.Stats.Failed, summary.Duration.Round(time.Millisecond)) + + if summary.Stats.Failed > 0 { + os.Exit(1) + } +} diff --git a/tests/checkin/reports/last_run.json b/tests/checkin/reports/last_run.json new file mode 100644 index 0000000..28eb4ce --- /dev/null +++ b/tests/checkin/reports/last_run.json @@ -0,0 +1,5 @@ +{ + "total": 32, + "passed": 32, + "failed": 0 +} \ No newline at end of file diff --git a/tests/checkin/reports/latest.html b/tests/checkin/reports/latest.html new file mode 100644 index 0000000..074768c --- /dev/null +++ b/tests/checkin/reports/latest.html @@ -0,0 +1,1137 @@ + + + + + +签到模块测试 - 测试报告 + + + + + +
+

签到模块测试

+
+
32
总计
+
32
通过
+
0
失败
+
100.0%
通过率
+
+
+ 📅 2026-05-08 16:32:19 + ⏱ 15s + 项目: 91porn-server模块: 签到时间: 2026-05-08 16:32:04 +
+
+
+ +
+ + + + + +
+
👈 点击左侧用例查看详情
+
+ +
+ + + + \ No newline at end of file diff --git a/tests/checkin/reports/report_20260507_212008.html b/tests/checkin/reports/report_20260507_212008.html new file mode 100644 index 0000000..82bb463 --- /dev/null +++ b/tests/checkin/reports/report_20260507_212008.html @@ -0,0 +1,962 @@ + + + + + +签到模块测试 - 测试报告 + + + + + +
+

签到模块测试

+
+
25
总计
+
25
通过
+
0
失败
+
100.0%
通过率
+
+
+ 📅 2026-05-07 21:20:08 + ⏱ 11s + 项目: 91porn-server模块: 签到时间: 2026-05-07 21:19:57 +
+
+
+ +
+ + + + + +
+
👈 点击左侧用例查看详情
+
+ +
+ + + + \ No newline at end of file diff --git a/tests/checkin/reports/report_20260508_162923.html b/tests/checkin/reports/report_20260508_162923.html new file mode 100644 index 0000000..a57e068 --- /dev/null +++ b/tests/checkin/reports/report_20260508_162923.html @@ -0,0 +1,962 @@ + + + + + +签到模块测试 - 测试报告 + + + + + +
+

签到模块测试

+
+
25
总计
+
25
通过
+
0
失败
+
100.0%
通过率
+
+
+ 📅 2026-05-08 16:29:23 + ⏱ 12s + 项目: 91porn-server模块: 签到时间: 2026-05-08 16:29:10 +
+
+
+ +
+ + + + + +
+
👈 点击左侧用例查看详情
+
+ +
+ + + + \ No newline at end of file diff --git a/tests/checkin/reports/report_20260508_163219.html b/tests/checkin/reports/report_20260508_163219.html new file mode 100644 index 0000000..074768c --- /dev/null +++ b/tests/checkin/reports/report_20260508_163219.html @@ -0,0 +1,1137 @@ + + + + + +签到模块测试 - 测试报告 + + + + + +
+

签到模块测试

+
+
32
总计
+
32
通过
+
0
失败
+
100.0%
通过率
+
+
+ 📅 2026-05-08 16:32:19 + ⏱ 15s + 项目: 91porn-server模块: 签到时间: 2026-05-08 16:32:04 +
+
+
+ +
+ + + + + +
+
👈 点击左侧用例查看详情
+
+ +
+ + + + \ No newline at end of file diff --git a/tests/checkin/reports/result_20260507_212008.json b/tests/checkin/reports/result_20260507_212008.json new file mode 100644 index 0000000..455fad0 --- /dev/null +++ b/tests/checkin/reports/result_20260507_212008.json @@ -0,0 +1,363 @@ +{ + "duration": "11.335346958s", + "stats": { + "total": 25, + "passed": 25, + "failed": 0, + "skipped": 0 + }, + "suites": [ + { + "name": "签到功能", + "results": [ + { + "name": "空数据查询-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 90, + "func": "testQueryEmpty", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgxNTk5OTcsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.INz0ZZ2ZoGM7FnncWzec2jDZwQGbL8lJEoI6LETadAM", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[],\"checkin\":{\"continuouslyDays\":0,\"cumulativeDays\":0,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"\",\"description\":\"测试签到配置\",\"enable\":true,\"integerExchangeList\":null},\"prizes\":[]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-07T13:19:58.522Z\",\"tip\":\"\"}" + } + }, + { + "name": "空数据-todayChecked=false", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 95, + "func": "testQueryEmpty", + "got": false, + "want": false + }, + { + "name": "空数据-连续=0", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 96, + "func": "testQueryEmpty", + "got": 0, + "want": 0 + }, + { + "name": "空数据-累计=0", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 97, + "func": "testQueryEmpty", + "got": 0, + "want": 0 + }, + { + "name": "连续3天-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 112, + "func": "testContinuous3Days", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgxNTk5OTcsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.INz0ZZ2ZoGM7FnncWzec2jDZwQGbL8lJEoI6LETadAM", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[],\"checkin\":{\"continuouslyDays\":3,\"cumulativeDays\":3,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"\",\"description\":\"测试签到配置\",\"enable\":true,\"integerExchangeList\":null},\"prizes\":[]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-07T13:19:59.645Z\",\"tip\":\"\"}" + } + }, + { + "name": "连续3天-todayChecked=false", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 115, + "func": "testContinuous3Days", + "got": false, + "want": false + }, + { + "name": "连续3天-continuously=3", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 116, + "func": "testContinuous3Days", + "got": 3, + "want": 3 + }, + { + "name": "连续3天-cumulative=3", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 117, + "func": "testContinuous3Days", + "got": 3, + "want": 3 + }, + { + "name": "断签-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 129, + "func": "testBrokenStreak", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgxNTk5OTcsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.INz0ZZ2ZoGM7FnncWzec2jDZwQGbL8lJEoI6LETadAM", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[],\"checkin\":{\"continuouslyDays\":0,\"cumulativeDays\":2,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"\",\"description\":\"测试签到配置\",\"enable\":true,\"integerExchangeList\":null},\"prizes\":[]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-07T13:20:00.972Z\",\"tip\":\"\"}" + } + }, + { + "name": "断签-continuously=0", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 131, + "func": "testBrokenStreak", + "got": 0, + "want": 0 + }, + { + "name": "断签-cumulative=2", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 132, + "func": "testBrokenStreak", + "got": 2, + "want": 2 + }, + { + "name": "断一天-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 144, + "func": "testBrokenStreakOneDayGap", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgxNTk5OTcsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.INz0ZZ2ZoGM7FnncWzec2jDZwQGbL8lJEoI6LETadAM", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[],\"checkin\":{\"continuouslyDays\":1,\"cumulativeDays\":2,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"\",\"description\":\"测试签到配置\",\"enable\":true,\"integerExchangeList\":null},\"prizes\":[]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-07T13:20:02.170Z\",\"tip\":\"\"}" + } + }, + { + "name": "断一天-continuously=1", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 146, + "func": "testBrokenStreakOneDayGap", + "got": 1, + "want": 1 + }, + { + "name": "断一天-cumulative=2", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 147, + "func": "testBrokenStreakOneDayGap", + "got": 2, + "want": 2 + }, + { + "name": "跨月-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 160, + "func": "testCrossMonthContinuous", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgxNTk5OTcsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.INz0ZZ2ZoGM7FnncWzec2jDZwQGbL8lJEoI6LETadAM", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[],\"checkin\":{\"continuouslyDays\":0,\"cumulativeDays\":3,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"\",\"description\":\"测试签到配置\",\"enable\":true,\"integerExchangeList\":null},\"prizes\":[]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-07T13:20:03.531Z\",\"tip\":\"\"}" + } + }, + { + "name": "跨月-continuously=0", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 162, + "func": "testCrossMonthContinuous", + "got": 0, + "want": 0 + }, + { + "name": "今天已签到-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 180, + "func": "testTodayChecked", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgxNTk5OTcsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.INz0ZZ2ZoGM7FnncWzec2jDZwQGbL8lJEoI6LETadAM", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[],\"checkin\":{\"continuouslyDays\":3,\"cumulativeDays\":3,\"doubleReward\":false,\"todayChecked\":true},\"config\":{\"backgroundImage\":\"\",\"description\":\"测试签到配置\",\"enable\":true,\"integerExchangeList\":null},\"prizes\":[]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-07T13:20:04.950Z\",\"tip\":\"\"}" + } + }, + { + "name": "今天已签到-todayChecked=true", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 182, + "func": "testTodayChecked", + "got": true, + "want": true + }, + { + "name": "今天已签到-continuously=3", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 183, + "func": "testTodayChecked", + "got": 3, + "want": 3 + }, + { + "name": "双倍奖励-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 201, + "func": "testDoubleReward", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgxNTk5OTcsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.INz0ZZ2ZoGM7FnncWzec2jDZwQGbL8lJEoI6LETadAM", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[],\"checkin\":{\"continuouslyDays\":6,\"cumulativeDays\":6,\"doubleReward\":true,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"\",\"description\":\"测试签到配置\",\"enable\":true,\"integerExchangeList\":null},\"prizes\":[]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-07T13:20:06.570Z\",\"tip\":\"\"}" + } + }, + { + "name": "6天连续+未签到-doubleReward=true", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 203, + "func": "testDoubleReward", + "got": true, + "want": true + }, + { + "name": "6天连续-continuously=6", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 204, + "func": "testDoubleReward", + "got": 6, + "want": 6 + }, + { + "name": "断签恢复-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 222, + "func": "testCheckinAfterBroken", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgxNTk5OTcsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.INz0ZZ2ZoGM7FnncWzec2jDZwQGbL8lJEoI6LETadAM", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[],\"checkin\":{\"continuouslyDays\":1,\"cumulativeDays\":3,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"\",\"description\":\"测试签到配置\",\"enable\":true,\"integerExchangeList\":null},\"prizes\":[]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-07T13:20:08.267Z\",\"tip\":\"\"}" + } + }, + { + "name": "断签恢复-continuously=1", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 224, + "func": "testCheckinAfterBroken", + "got": 1, + "want": 1 + }, + { + "name": "断签恢复-cumulative=3", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 225, + "func": "testCheckinAfterBroken", + "got": 3, + "want": 3 + } + ], + "elapsed": 11335345667 + } + ], + "time": "2026-05-07T21:20:08+08:00" +} \ No newline at end of file diff --git a/tests/checkin/reports/result_20260508_162923.json b/tests/checkin/reports/result_20260508_162923.json new file mode 100644 index 0000000..e623c2a --- /dev/null +++ b/tests/checkin/reports/result_20260508_162923.json @@ -0,0 +1,363 @@ +{ + "duration": "12.377991708s", + "stats": { + "total": 25, + "passed": 25, + "failed": 0, + "skipped": 0 + }, + "suites": [ + { + "name": "签到功能", + "results": [ + { + "name": "空数据查询-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 90, + "func": "testQueryEmpty", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjg5NTAsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.Rp5rRh5CNUDwUahtYoVGtej_cEqTuinyOIZ_vbr03js", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":0,\"cumulativeDays\":0,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a2\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99db2354ba0975cac478\",\"prizeName\":\"积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:29:12.515Z\",\"tip\":\"\"}" + } + }, + { + "name": "空数据-todayChecked=false", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 95, + "func": "testQueryEmpty", + "got": false, + "want": false + }, + { + "name": "空数据-连续=0", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 96, + "func": "testQueryEmpty", + "got": 0, + "want": 0 + }, + { + "name": "空数据-累计=0", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 97, + "func": "testQueryEmpty", + "got": 0, + "want": 0 + }, + { + "name": "连续3天-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 112, + "func": "testContinuous3Days", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjg5NTAsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.Rp5rRh5CNUDwUahtYoVGtej_cEqTuinyOIZ_vbr03js", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":3,\"cumulativeDays\":3,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a2\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fc99db2354ba0975cac478\",\"prizeName\":\"积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:29:13.756Z\",\"tip\":\"\"}" + } + }, + { + "name": "连续3天-todayChecked=false", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 115, + "func": "testContinuous3Days", + "got": false, + "want": false + }, + { + "name": "连续3天-continuously=3", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 116, + "func": "testContinuous3Days", + "got": 3, + "want": 3 + }, + { + "name": "连续3天-cumulative=3", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 117, + "func": "testContinuous3Days", + "got": 3, + "want": 3 + }, + { + "name": "断签-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 129, + "func": "testBrokenStreak", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjg5NTAsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.Rp5rRh5CNUDwUahtYoVGtej_cEqTuinyOIZ_vbr03js", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":0,\"cumulativeDays\":2,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a2\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99db2354ba0975cac478\",\"prizeName\":\"积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:29:15.122Z\",\"tip\":\"\"}" + } + }, + { + "name": "断签-continuously=0", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 131, + "func": "testBrokenStreak", + "got": 0, + "want": 0 + }, + { + "name": "断签-cumulative=2", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 132, + "func": "testBrokenStreak", + "got": 2, + "want": 2 + }, + { + "name": "断一天-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 144, + "func": "testBrokenStreakOneDayGap", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjg5NTAsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.Rp5rRh5CNUDwUahtYoVGtej_cEqTuinyOIZ_vbr03js", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":1,\"cumulativeDays\":2,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a2\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99db2354ba0975cac478\",\"prizeName\":\"积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:29:16.448Z\",\"tip\":\"\"}" + } + }, + { + "name": "断一天-continuously=1", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 146, + "func": "testBrokenStreakOneDayGap", + "got": 1, + "want": 1 + }, + { + "name": "断一天-cumulative=2", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 147, + "func": "testBrokenStreakOneDayGap", + "got": 2, + "want": 2 + }, + { + "name": "跨月-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 160, + "func": "testCrossMonthContinuous", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjg5NTAsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.Rp5rRh5CNUDwUahtYoVGtej_cEqTuinyOIZ_vbr03js", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":0,\"cumulativeDays\":3,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a2\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99db2354ba0975cac478\",\"prizeName\":\"积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:29:17.871Z\",\"tip\":\"\"}" + } + }, + { + "name": "跨月-continuously=0", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 162, + "func": "testCrossMonthContinuous", + "got": 0, + "want": 0 + }, + { + "name": "今天已签到-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 180, + "func": "testTodayChecked", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjg5NTAsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.Rp5rRh5CNUDwUahtYoVGtej_cEqTuinyOIZ_vbr03js", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":3,\"cumulativeDays\":3,\"doubleReward\":false,\"todayChecked\":true},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a2\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fc99db2354ba0975cac478\",\"prizeName\":\"积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:29:19.408Z\",\"tip\":\"\"}" + } + }, + { + "name": "今天已签到-todayChecked=true", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 182, + "func": "testTodayChecked", + "got": true, + "want": true + }, + { + "name": "今天已签到-continuously=3", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 183, + "func": "testTodayChecked", + "got": 3, + "want": 3 + }, + { + "name": "双倍奖励-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 201, + "func": "testDoubleReward", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjg5NTAsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.Rp5rRh5CNUDwUahtYoVGtej_cEqTuinyOIZ_vbr03js", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":6,\"cumulativeDays\":6,\"doubleReward\":true,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a2\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fc99db2354ba0975cac478\",\"prizeName\":\"积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:29:21.166Z\",\"tip\":\"\"}" + } + }, + { + "name": "6天连续+未签到-doubleReward=true", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 203, + "func": "testDoubleReward", + "got": true, + "want": true + }, + { + "name": "6天连续-continuously=6", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 204, + "func": "testDoubleReward", + "got": 6, + "want": 6 + }, + { + "name": "断签恢复-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 222, + "func": "testCheckinAfterBroken", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjg5NTAsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.Rp5rRh5CNUDwUahtYoVGtej_cEqTuinyOIZ_vbr03js", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":1,\"cumulativeDays\":3,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a2\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99db2354ba0975cac478\",\"prizeName\":\"积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:29:23.020Z\",\"tip\":\"\"}" + } + }, + { + "name": "断签恢复-continuously=1", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 224, + "func": "testCheckinAfterBroken", + "got": 1, + "want": 1 + }, + { + "name": "断签恢复-cumulative=3", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 225, + "func": "testCheckinAfterBroken", + "got": 3, + "want": 3 + } + ], + "elapsed": 12377968917 + } + ], + "time": "2026-05-08T16:29:23+08:00" +} \ No newline at end of file diff --git a/tests/checkin/reports/result_20260508_163219.json b/tests/checkin/reports/result_20260508_163219.json new file mode 100644 index 0000000..4338201 --- /dev/null +++ b/tests/checkin/reports/result_20260508_163219.json @@ -0,0 +1,441 @@ +{ + "duration": "14.859319041s", + "stats": { + "total": 32, + "passed": 32, + "failed": 0, + "skipped": 0 + }, + "suites": [ + { + "name": "签到功能", + "results": [ + { + "name": "空数据查询-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 126, + "func": "testQueryEmpty", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjkxMjQsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.iin-dSFNLY267ImH9PhARkSfGXnkX2yhUsTFdAeIKDk", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":0,\"cumulativeDays\":0,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"0001-01-01T08:05:43+08:05\",\"id\":\"69fd9f84fdb9ef08e4d0451a\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fd9f84fdb9ef08e4d0451b\",\"prizeName\":\"测试奖品1\",\"prizeType\":0,\"score\":0,\"status\":true,\"title\":\"测试奖品第1天\",\"updatedAt\":\"0001-01-01T08:05:43+08:05\"},\"... (34 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:32:06.203Z\",\"tip\":\"\"}" + } + }, + { + "name": "空数据-todayChecked=false", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 131, + "func": "testQueryEmpty", + "got": false, + "want": false + }, + { + "name": "空数据-连续=0", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 132, + "func": "testQueryEmpty", + "got": 0, + "want": 0 + }, + { + "name": "空数据-累计=0", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 133, + "func": "testQueryEmpty", + "got": 0, + "want": 0 + }, + { + "name": "连续3天-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 148, + "func": "testContinuous3Days", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjkxMjQsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.iin-dSFNLY267ImH9PhARkSfGXnkX2yhUsTFdAeIKDk", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":3,\"cumulativeDays\":3,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"0001-01-01T08:05:43+08:05\",\"id\":\"69fd9f84fdb9ef08e4d0451a\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fd9f84fdb9ef08e4d0451b\",\"prizeName\":\"测试奖品1\",\"prizeType\":0,\"score\":0,\"status\":true,\"title\":\"测试奖品第1天\",\"updatedAt\":\"0001-01-01T08:05:43+08:05\"},\"... (34 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:32:07.531Z\",\"tip\":\"\"}" + } + }, + { + "name": "连续3天-todayChecked=false", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 151, + "func": "testContinuous3Days", + "got": false, + "want": false + }, + { + "name": "连续3天-continuously=3", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 152, + "func": "testContinuous3Days", + "got": 3, + "want": 3 + }, + { + "name": "连续3天-cumulative=3", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 153, + "func": "testContinuous3Days", + "got": 3, + "want": 3 + }, + { + "name": "断签-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 165, + "func": "testBrokenStreak", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjkxMjQsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.iin-dSFNLY267ImH9PhARkSfGXnkX2yhUsTFdAeIKDk", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":0,\"cumulativeDays\":2,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"0001-01-01T08:05:43+08:05\",\"id\":\"69fd9f84fdb9ef08e4d0451a\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fd9f84fdb9ef08e4d0451b\",\"prizeName\":\"测试奖品1\",\"prizeType\":0,\"score\":0,\"status\":true,\"title\":\"测试奖品第1天\",\"updatedAt\":\"0001-01-01T08:05:43+08:05\"},\"... (34 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:32:08.957Z\",\"tip\":\"\"}" + } + }, + { + "name": "断签-continuously=0", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 167, + "func": "testBrokenStreak", + "got": 0, + "want": 0 + }, + { + "name": "断签-cumulative=2", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 168, + "func": "testBrokenStreak", + "got": 2, + "want": 2 + }, + { + "name": "断一天-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 180, + "func": "testBrokenStreakOneDayGap", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjkxMjQsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.iin-dSFNLY267ImH9PhARkSfGXnkX2yhUsTFdAeIKDk", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":1,\"cumulativeDays\":2,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"0001-01-01T08:05:43+08:05\",\"id\":\"69fd9f84fdb9ef08e4d0451a\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fd9f84fdb9ef08e4d0451b\",\"prizeName\":\"测试奖品1\",\"prizeType\":0,\"score\":0,\"status\":true,\"title\":\"测试奖品第1天\",\"updatedAt\":\"0001-01-01T08:05:43+08:05\"},\"... (34 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:32:10.517Z\",\"tip\":\"\"}" + } + }, + { + "name": "断一天-continuously=1", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 182, + "func": "testBrokenStreakOneDayGap", + "got": 1, + "want": 1 + }, + { + "name": "断一天-cumulative=2", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 183, + "func": "testBrokenStreakOneDayGap", + "got": 2, + "want": 2 + }, + { + "name": "跨月-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 196, + "func": "testCrossMonthContinuous", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjkxMjQsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.iin-dSFNLY267ImH9PhARkSfGXnkX2yhUsTFdAeIKDk", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":0,\"cumulativeDays\":3,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"0001-01-01T08:05:43+08:05\",\"id\":\"69fd9f84fdb9ef08e4d0451a\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fd9f84fdb9ef08e4d0451b\",\"prizeName\":\"测试奖品1\",\"prizeType\":0,\"score\":0,\"status\":true,\"title\":\"测试奖品第1天\",\"updatedAt\":\"0001-01-01T08:05:43+08:05\"},\"... (34 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:32:12.074Z\",\"tip\":\"\"}" + } + }, + { + "name": "跨月-continuously=0", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 198, + "func": "testCrossMonthContinuous", + "got": 0, + "want": 0 + }, + { + "name": "今天已签到-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 216, + "func": "testTodayChecked", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjkxMjQsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.iin-dSFNLY267ImH9PhARkSfGXnkX2yhUsTFdAeIKDk", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":3,\"cumulativeDays\":3,\"doubleReward\":false,\"todayChecked\":true},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"0001-01-01T08:05:43+08:05\",\"id\":\"69fd9f84fdb9ef08e4d0451a\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fd9f84fdb9ef08e4d0451b\",\"prizeName\":\"测试奖品1\",\"prizeType\":0,\"score\":0,\"status\":true,\"title\":\"测试奖品第1天\",\"updatedAt\":\"0001-01-01T08:05:43+08:05\"},\"... (34 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:32:13.703Z\",\"tip\":\"\"}" + } + }, + { + "name": "今天已签到-todayChecked=true", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 218, + "func": "testTodayChecked", + "got": true, + "want": true + }, + { + "name": "今天已签到-continuously=3", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 219, + "func": "testTodayChecked", + "got": 3, + "want": 3 + }, + { + "name": "双倍奖励-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 237, + "func": "testDoubleReward", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjkxMjQsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.iin-dSFNLY267ImH9PhARkSfGXnkX2yhUsTFdAeIKDk", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":6,\"cumulativeDays\":6,\"doubleReward\":true,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"0001-01-01T08:05:43+08:05\",\"id\":\"69fd9f84fdb9ef08e4d0451a\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fd9f84fdb9ef08e4d0451b\",\"prizeName\":\"测试奖品1\",\"prizeType\":0,\"score\":0,\"status\":true,\"title\":\"测试奖品第1天\",\"updatedAt\":\"0001-01-01T08:05:43+08:05\"},\"... (34 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:32:15.525Z\",\"tip\":\"\"}" + } + }, + { + "name": "6天连续+未签到-doubleReward=true", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 239, + "func": "testDoubleReward", + "got": true, + "want": true + }, + { + "name": "6天连续-continuously=6", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 240, + "func": "testDoubleReward", + "got": 6, + "want": 6 + }, + { + "name": "断签恢复-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 258, + "func": "testCheckinAfterBroken", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjkxMjQsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.iin-dSFNLY267ImH9PhARkSfGXnkX2yhUsTFdAeIKDk", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":1,\"cumulativeDays\":3,\"doubleReward\":false,\"todayChecked\":false},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"0001-01-01T08:05:43+08:05\",\"id\":\"69fd9f84fdb9ef08e4d0451a\",\"image\":\"\",\"isCheckedIn\":false,\"isReceive\":false,\"prizeId\":\"69fd9f84fdb9ef08e4d0451b\",\"prizeName\":\"测试奖品1\",\"prizeType\":0,\"score\":0,\"status\":true,\"title\":\"测试奖品第1天\",\"updatedAt\":\"0001-01-01T08:05:43+08:05\"},\"... (34 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:32:17.371Z\",\"tip\":\"\"}" + } + }, + { + "name": "断签恢复-continuously=1", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 260, + "func": "testCheckinAfterBroken", + "got": 1, + "want": 1 + }, + { + "name": "断签恢复-cumulative=3", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 261, + "func": "testCheckinAfterBroken", + "got": 3, + "want": 3 + }, + { + "name": "isCheckedIn-code=200", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 279, + "func": "testIsCheckedIn", + "got": 200, + "want": 200, + "request": { + "method": "POST", + "url": "http://127.0.0.1:13051/api/app/checkin/prize", + "headers": { + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lc3RhbXAiOjE3NzgyMjkxMjQsInR5cGUiOjAsInVpZCI6MzAwMTAwfQ.iin-dSFNLY267ImH9PhARkSfGXnkX2yhUsTFdAeIKDk", + "Content-Type": "application/json", + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0\u0026sysType=android\u0026devType=test\u0026devId=checkin-test-001" + }, + "body": "{}" + }, + "response": { + "statusCode": 200, + "body": "{\"code\":200,\"data\":{\"bigPrizes\":[{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c0\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第1天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},{\"bigPrize\":true,\"canClaim\":false,\"checkinDays\":2,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e32354ba0975cac4c1\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fc99dc2354ba0975cac481\",\"prizeName\":\"VIP积分x5\",\"prizeType\":8,\"score\":5,\"status\":true,\"title\":\"VIP第2天\",\"updatedAt\":\"2026-05-07T21:55:38.682+08:00\"},\"... (31 items total)\"],\"checkin\":{\"continuouslyDays\":3,\"cumulativeDays\":3,\"doubleReward\":false,\"todayChecked\":true},\"config\":{\"backgroundImage\":\"cf230705/image/2mf/x9/2n2/v/ddddedb148a7bdc7b4ded365861ddc55.jpeg\",\"description\":\"每日签到领好礼!连续签到天数越多奖励越丰厚,第7天可获得双倍奖励!月卡VIP用户每天额外领取专属奖品。坚持签到31天有超级大礼!\",\"enable\":true,\"integerExchangeList\":[{\"icon\":\"cf230705/image/kc/1ad/2t8/1yc/521c547f934cfb05bc0a105a2eb93c3b.jpeg\",\"name\":\"观影券 X2\"},{\"icon\":\"cf230705/image/4/2ha/x0/2qt/1db16a6ef0d197daf4d084bad2aff665.jpeg\",\"name\":\"VIP体验 X2\"}]},\"prizes\":[{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"2026-05-07T21:55:38.682+08:00\",\"id\":\"69fc99e02354ba0975cac4a1\",\"image\":\"cf230705/image/1wc/sl/na/14c/164e8a0b0496b2c4525e5f98d116331f.png\",\"isCheckedIn\":true,\"isReceive\":true,\"prizeId\":\"69fc99dc2354ba0975cac485\",\"prizeName\":\"积分x3\",\"prizeType\":8,\"score\":3,\"status\":true,\"title\":\"第1天\",\"updatedAt\":\"2026-05-08T13:09:01.62+08:00\"},{\"bigPrize\":false,\"canClaim\":false,\"checkinDays\":1,\"checkinType\":1,\"createdAt\":\"0001-01-01T08:05:43+08:05\",\"id\":\"69fd9f84fdb9ef08e4d0451a\",\"image\":\"\",\"isCheckedIn\":true,\"isReceive\":false,\"prizeId\":\"69fd9f84fdb9ef08e4d0451b\",\"prizeName\":\"测试奖品1\",\"prizeType\":0,\"score\":0,\"status\":true,\"title\":\"测试奖品第1天\",\"updatedAt\":\"0001-01-01T08:05:43+08:05\"},\"... (34 items total)\"]},\"hash\":false,\"msg\":\"success\",\"time\":\"2026-05-08T08:32:18.998Z\",\"tip\":\"\"}" + } + }, + { + "name": "第1天-isCheckedIn=true", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 290, + "func": "testIsCheckedIn", + "got": true, + "want": true + }, + { + "name": "第1天-isReceive=true", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 291, + "func": "testIsCheckedIn", + "got": true, + "want": true + }, + { + "name": "第2天-isCheckedIn=true(签到未发奖)", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 295, + "func": "testIsCheckedIn", + "got": true, + "want": true + }, + { + "name": "第2天-isReceive=false(奖品未发放)", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 296, + "func": "testIsCheckedIn", + "got": false, + "want": false + }, + { + "name": "第3天-isCheckedIn=true", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 300, + "func": "testIsCheckedIn", + "got": true, + "want": true + }, + { + "name": "第3天-isReceive=true", + "passed": true, + "file": "tests/checkin/suite_checkin.go", + "line": 301, + "func": "testIsCheckedIn", + "got": true, + "want": true + } + ], + "elapsed": 14859290417 + } + ], + "time": "2026-05-08T16:32:19+08:00" +} \ No newline at end of file diff --git a/tests/checkin/sandbox.go b/tests/checkin/sandbox.go new file mode 100644 index 0000000..8fa5e9e --- /dev/null +++ b/tests/checkin/sandbox.go @@ -0,0 +1,98 @@ +package main + +import ( + "fmt" + "time" + + "91porn-server/app/appg" + "91porn-server/app/middleware/authuser" + "91porn-server/app/router" + "91porn-server/common" + "91porn-server/common/cachev2" + "91porn-server/common/conf" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/services" + "91porn-server/models" + "91porn-server/models/Init" + + fw "git.ruishengces.com/pub_tool/autotest" + "github.com/gin-gonic/gin" +) + +type Env struct { + AppToken string // APP 用户 token + TestUID uint64 +} + +func (e *Env) Start(cfg fw.Config, base *fw.BaseEnv) { + time.Local, _ = time.LoadLocation("Asia/Shanghai") + + // 读取配置 + appCfg := appg.GlobalConfig{} + if err := conf.LoadJSON("../../app/config/app.json", &appCfg); err != nil { + panic("load config error: " + err.Error()) + } + appg.Conf = &appCfg + log.Init(log.Options{Level: appCfg.Log.Level, DisableStack: appCfg.Log.DisableStacktrace}) + common.InitGinSecret(appCfg.Base.ResKey, appCfg.Base.ResKey360, appCfg.Base.Env) + + // 初始化 MongoDB + ds := db.InitDS(appg.InitMongodbParam(models.RegisterPool)) + for k, v := range ds { + switch k { + case models.VideoDb: + appg.VideoDB = v + case models.LogDb: + appg.LogDB = v + case models.StatDb: + appg.StatDB = v + } + } + + // 初始化 Redis + appg.InitRedis() + services.Init(appg.Redis) + cachev2.Init(appg.Redis, log.ZapLog) + + // 初始化 Models + Init.InitMongo() + + // 生成 APP 测试 token + e.TestUID = 300100 + var err error + e.AppToken, err = authuser.GenToken(&authuser.Claims{ + UID: e.TestUID, + Timestamp: time.Now().Unix(), + Type: 0, + }) + if err != nil { + panic("gen token error: " + err.Error()) + } + // 缓存到 Redis + tokenKey := fmt.Sprintf("token:user:%d", e.TestUID) + _ = appg.Redis.Set(tokenKey, e.AppToken, 24*time.Hour) + + // 启动 HTTP 服务 + gin.SetMode(gin.TestMode) + base.AppBaseURL = base.StartHTTP("APP", 13051, router.SetupGin()) +} + +func (e *Env) SetupData() { + // 基础数据由各 Suite 的 Setup 负责 +} + +func (e *Env) CleanData() {} + +func (e *Env) Destroy() { + db.CloseDS() + _ = appg.RedisClose() +} + +func (e *Env) Meta() []fw.ReportMeta { + return []fw.ReportMeta{ + {Key: "项目", Value: "91porn-server"}, + {Key: "模块", Value: "签到"}, + {Key: "时间", Value: time.Now().Format("2006-01-02 15:04:05")}, + } +} diff --git a/tests/checkin/suite_checkin.go b/tests/checkin/suite_checkin.go new file mode 100644 index 0000000..a01b67c --- /dev/null +++ b/tests/checkin/suite_checkin.go @@ -0,0 +1,302 @@ +package main + +import ( + "fmt" + "time" + + "91porn-server/common/timeutil" + "91porn-server/models/v/checkinconfigmod" + "91porn-server/models/v/checkinprizemod" + "91porn-server/models/v/usercheckinmod" + + fw "git.ruishengces.com/pub_tool/autotest" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type CheckinSuite struct { + env *Env + url string // AppBaseURL + prizeIds []primitive.ObjectID +} + +func (s *CheckinSuite) Name() string { return "签到功能" } + +func (s *CheckinSuite) Setup(ctx *fw.TestContext) error { + // 确保签到功能已启用 + cfg, _ := checkinconfigmod.FindOne(bson.M{}) + if cfg == nil { + if err := checkinconfigmod.InsertOne(&checkinconfigmod.CheckinConfig{ + Enable: true, + Description: "测试签到", + }); err != nil { + return err + } + } else if !cfg.Enable { + if err := checkinconfigmod.UpdateOne(bson.M{"_id": cfg.ID}, bson.M{"$set": bson.M{"enable": true}}); err != nil { + return err + } + } + + // 创建测试用签到奖品配置(连续签到第1~3天) + s.prizeIds = make([]primitive.ObjectID, 3) + for i := 0; i < 3; i++ { + id := primitive.NewObjectID() + s.prizeIds[i] = id + if err := checkinprizemod.InsertOne(&checkinprizemod.CheckinPrize{ + ID: id, + Title: fmt.Sprintf("测试奖品第%d天", i+1), + CheckinDays: int64(i + 1), + CheckinType: checkinprizemod.CheckinTypeContinuously, + PrizeId: primitive.NewObjectID(), // 故意用不存在的奖品ID + PrizeName: fmt.Sprintf("测试奖品%d", i+1), + Status: true, + BigPrize: false, + }); err != nil { + return err + } + } + return nil +} + +func (s *CheckinSuite) Teardown(ctx *fw.TestContext) { + s.cleanCheckins() + s.cleanPrizes() +} + +func (s *CheckinSuite) cleanCheckins() { + for { + n, _ := usercheckinmod.DeleteOne(bson.M{"userId": s.env.TestUID}) + if n == 0 { + break + } + } +} + +func (s *CheckinSuite) cleanPrizes() { + for _, id := range s.prizeIds { + checkinprizemod.DeleteOne(bson.M{"_id": id}) + } +} + +func (s *CheckinSuite) insertRecord(date time.Time, contDays, cumDays int64) { + s.insertRecordWithGave(date, contDays, cumDays, true) +} + +func (s *CheckinSuite) insertRecordWithGave(date time.Time, contDays, cumDays int64, gave bool) { + _ = usercheckinmod.InsertOne(&usercheckinmod.UserCheckin{ + ID: primitive.NewObjectID(), + Date: timeutil.BeginOfTime(date), + UserId: s.env.TestUID, + Gave: gave, + ContinuouslyDays: contDays, + CumulativeDays: cumDays, + CreatedAt: date, + }) +} + +func (s *CheckinSuite) headers() map[string]string { + return map[string]string{ + "Authorization": s.env.AppToken, + "User-Agent": "91porn/3.8.0 (Android; test)", + "X-User-Agent": "ver=3.8.0&sysType=android&devType=test&devId=checkin-test-001", + } +} + +func (s *CheckinSuite) getPrize() *fw.Resp { + return fw.DoWithHeaders("POST", s.url+"/api/app/checkin/prize", map[string]interface{}{}, s.headers()) +} + +func (s *CheckinSuite) Run(ctx *fw.TestContext) { + s.testQueryEmpty(ctx) + s.testContinuous3Days(ctx) + s.testBrokenStreak(ctx) + s.testBrokenStreakOneDayGap(ctx) + s.testCrossMonthContinuous(ctx) + s.testTodayChecked(ctx) + s.testDoubleReward(ctx) + s.testCheckinAfterBroken(ctx) + s.testIsCheckedIn(ctx) +} + +// 空数据查询 +func (s *CheckinSuite) testQueryEmpty(ctx *fw.TestContext) { + s.cleanCheckins() + resp := s.getPrize() + ctx.CodeEqual("空数据查询-code=200", resp, 200) + data := resp.Data() + if data != nil { + checkin, _ := data["checkin"].(map[string]interface{}) + if checkin != nil { + ctx.Equal("空数据-todayChecked=false", checkin["todayChecked"], false) + ctx.Equal("空数据-连续=0", fw.Num(checkin, "continuouslyDays"), float64(0)) + ctx.Equal("空数据-累计=0", fw.Num(checkin, "cumulativeDays"), float64(0)) + } + } +} + +// 连续签到3天 +func (s *CheckinSuite) testContinuous3Days(ctx *fw.TestContext) { + s.cleanCheckins() + now := time.Now().Local() + today := timeutil.BeginOfTime(now) + s.insertRecord(today.Add(-3*24*time.Hour), 1, 1) + s.insertRecord(today.Add(-2*24*time.Hour), 2, 2) + s.insertRecord(today.Add(-1*24*time.Hour), 3, 3) + + resp := s.getPrize() + ctx.CodeEqual("连续3天-code=200", resp, 200) + data := resp.Data() + checkin, _ := data["checkin"].(map[string]interface{}) + ctx.Equal("连续3天-todayChecked=false", checkin["todayChecked"], false) + ctx.Equal("连续3天-continuously=3", fw.Num(checkin, "continuouslyDays"), float64(3)) + ctx.Equal("连续3天-cumulative=3", fw.Num(checkin, "cumulativeDays"), float64(3)) +} + +// 断签:4天前、3天前签到,昨天和前天没签 +func (s *CheckinSuite) testBrokenStreak(ctx *fw.TestContext) { + s.cleanCheckins() + now := time.Now().Local() + today := timeutil.BeginOfTime(now) + s.insertRecord(today.Add(-4*24*time.Hour), 1, 1) + s.insertRecord(today.Add(-3*24*time.Hour), 2, 2) + + resp := s.getPrize() + ctx.CodeEqual("断签-code=200", resp, 200) + checkin, _ := resp.Data()["checkin"].(map[string]interface{}) + ctx.Equal("断签-continuously=0", fw.Num(checkin, "continuouslyDays"), float64(0)) + ctx.Equal("断签-cumulative=2", fw.Num(checkin, "cumulativeDays"), float64(2)) +} + +// 中间断一天:3天前签,2天前没签,昨天签 +func (s *CheckinSuite) testBrokenStreakOneDayGap(ctx *fw.TestContext) { + s.cleanCheckins() + now := time.Now().Local() + today := timeutil.BeginOfTime(now) + s.insertRecord(today.Add(-3*24*time.Hour), 1, 1) + s.insertRecord(today.Add(-1*24*time.Hour), 1, 2) + + resp := s.getPrize() + ctx.CodeEqual("断一天-code=200", resp, 200) + checkin, _ := resp.Data()["checkin"].(map[string]interface{}) + ctx.Equal("断一天-continuously=1", fw.Num(checkin, "continuouslyDays"), float64(1)) + ctx.Equal("断一天-cumulative=2", fw.Num(checkin, "cumulativeDays"), float64(2)) +} + +// 跨月:上月记录不计入连续 +func (s *CheckinSuite) testCrossMonthContinuous(ctx *fw.TestContext) { + s.cleanCheckins() + now := time.Now().Local() + beginOfMonth := timeutil.BeginningOfMonth(now) + s.insertRecord(beginOfMonth.Add(-3*24*time.Hour), 1, 1) + s.insertRecord(beginOfMonth.Add(-2*24*time.Hour), 2, 2) + s.insertRecord(beginOfMonth.Add(-1*24*time.Hour), 3, 3) + + resp := s.getPrize() + ctx.CodeEqual("跨月-code=200", resp, 200) + checkin, _ := resp.Data()["checkin"].(map[string]interface{}) + ctx.Equal("跨月-continuously=0", fw.Num(checkin, "continuouslyDays"), float64(0)) + ctx.Info(fmt.Sprintf("跨月-cumulative=%v (含上月记录,原版行为)", fw.Num(checkin, "cumulativeDays"))) +} + +// 今天已签到 +func (s *CheckinSuite) testTodayChecked(ctx *fw.TestContext) { + s.cleanCheckins() + now := time.Now().Local() + today := timeutil.BeginOfTime(now) + if now.Day() < 3 { + ctx.Info("月初不足3天,跳过今天已签到测试") + return + } + s.insertRecord(today.Add(-2*24*time.Hour), 1, 1) + s.insertRecord(today.Add(-1*24*time.Hour), 2, 2) + s.insertRecord(today, 3, 3) + + resp := s.getPrize() + ctx.CodeEqual("今天已签到-code=200", resp, 200) + checkin, _ := resp.Data()["checkin"].(map[string]interface{}) + ctx.Equal("今天已签到-todayChecked=true", checkin["todayChecked"], true) + ctx.Equal("今天已签到-continuously=3", fw.Num(checkin, "continuouslyDays"), float64(3)) +} + +// 第7天双倍奖励 +func (s *CheckinSuite) testDoubleReward(ctx *fw.TestContext) { + s.cleanCheckins() + now := time.Now().Local() + today := timeutil.BeginOfTime(now) + if now.Day() < 7 { + ctx.Info("本月不足7天,跳过双倍奖励测试") + return + } + for i := 6; i >= 1; i-- { + d := today.Add(time.Duration(-i) * 24 * time.Hour) + s.insertRecord(d, int64(7-i), int64(7-i)) + } + + resp := s.getPrize() + ctx.CodeEqual("双倍奖励-code=200", resp, 200) + checkin, _ := resp.Data()["checkin"].(map[string]interface{}) + ctx.Equal("6天连续+未签到-doubleReward=true", checkin["doubleReward"], true) + ctx.Equal("6天连续-continuously=6", fw.Num(checkin, "continuouslyDays"), float64(6)) +} + +// 断签后恢复 +func (s *CheckinSuite) testCheckinAfterBroken(ctx *fw.TestContext) { + s.cleanCheckins() + now := time.Now().Local() + today := timeutil.BeginOfTime(now) + if now.Day() < 5 { + ctx.Info("月初不足5天,跳过断签恢复测试") + return + } + s.insertRecord(today.Add(-5*24*time.Hour), 1, 1) + s.insertRecord(today.Add(-4*24*time.Hour), 2, 2) + // 3天前和2天前断签 + s.insertRecord(today.Add(-1*24*time.Hour), 1, 3) + + resp := s.getPrize() + ctx.CodeEqual("断签恢复-code=200", resp, 200) + checkin, _ := resp.Data()["checkin"].(map[string]interface{}) + ctx.Equal("断签恢复-continuously=1", fw.Num(checkin, "continuouslyDays"), float64(1)) + ctx.Equal("断签恢复-cumulative=3", fw.Num(checkin, "cumulativeDays"), float64(3)) +} + +// isCheckedIn字段:签到但未发奖时isCheckedIn=true,isReceive=false +func (s *CheckinSuite) testIsCheckedIn(ctx *fw.TestContext) { + s.cleanCheckins() + now := time.Now().Local() + today := timeutil.BeginOfTime(now) + if now.Day() < 3 { + ctx.Info("月初不足3天,跳过isCheckedIn测试") + return + } + // 第1天正常发奖,第2天签到但未发奖(Gave=false),第3天正常发奖 + s.insertRecord(today.Add(-2*24*time.Hour), 1, 1) + s.insertRecordWithGave(today.Add(-1*24*time.Hour), 2, 2, false) // 签到了但奖品未发放 + s.insertRecord(today, 3, 3) + + resp := s.getPrize() + ctx.CodeEqual("isCheckedIn-code=200", resp, 200) + data := resp.Data() + + prizes, _ := data["prizes"].([]interface{}) + if len(prizes) < 3 { + ctx.Info(fmt.Sprintf("prizes数量不足3个(got=%d),跳过isCheckedIn断言", len(prizes))) + return + } + + // 第1天:isCheckedIn=true, isReceive=true + p1, _ := prizes[0].(map[string]interface{}) + ctx.Equal("第1天-isCheckedIn=true", p1["isCheckedIn"], true) + ctx.Equal("第1天-isReceive=true", p1["isReceive"], true) + + // 第2天:签到了但未发奖 → isCheckedIn=true, isReceive=false + p2, _ := prizes[1].(map[string]interface{}) + ctx.Equal("第2天-isCheckedIn=true(签到未发奖)", p2["isCheckedIn"], true) + ctx.Equal("第2天-isReceive=false(奖品未发放)", p2["isReceive"], false) + + // 第3天:isCheckedIn=true, isReceive=true + p3, _ := prizes[2].(map[string]interface{}) + ctx.Equal("第3天-isCheckedIn=true", p3["isCheckedIn"], true) + ctx.Equal("第3天-isReceive=true", p3["isReceive"], true) +} diff --git a/web/api/acth5ctrl/lotnum.go b/web/api/acth5ctrl/lotnum.go new file mode 100644 index 0000000..0a3ab6e --- /dev/null +++ b/web/api/acth5ctrl/lotnum.go @@ -0,0 +1,52 @@ +package acth5ctrl + +import ( + "time" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/lotterylgmod" + + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 中奖号列表 +// @Description 中奖号列表 +// @Tags web-活动 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "当前页" +// @Param pageSize formData integer true "每页条数" +// @Param title formData string false "过滤项-标题" +// @Param actId formData string false "过滤项-活动ID" +// @Param enable formData bool false "过滤项-活动开关" +// @Param status formData string false "过滤项-活动状态" +// @Param startTime formData string false "过滤项-活动开始时间" +// @Param endTime formData string false "过滤项-活动结束时间" +// @Success 200 {string} user "{"msg": "操作成功", "date": { "total":10, "list":[] }}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/lottnum/list [get] +func List(c *gin.Context) { + var arg struct { + Start *time.Time `form:"start" json:"start"` + End *time.Time `form:"end" json:"end"` + UID *uint64 `form:"uid" json:"uid"` + Code *int `form:"code" json:"code"` + commod.Page + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web lottnum list arg error"+err.Error()) + return + } + info, total, err := lotterylgmod.LottnumList(arg.PageNumber, arg.PageSize, arg.Start, arg.End, arg.UID, arg.Code) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(c, stderr.Success, gin.H{ + "list": info, + "total": total, + }) +} diff --git a/web/api/activityctrl/activity.go b/web/api/activityctrl/activity.go new file mode 100644 index 0000000..c1bd357 --- /dev/null +++ b/web/api/activityctrl/activity.go @@ -0,0 +1,60 @@ +package activityctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/activitymod" + "91porn-server/web/service/activityser" + + "github.com/gin-gonic/gin" +) + +// 新增 +func Add(c *gin.Context) { + var in activitymod.AddCond + if err := c.ShouldBind(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web activity Add arg error "+err.Error()) + return + } + code := activityser.Add(in) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, "success") +} + +// 修改 +func Modify(c *gin.Context) { + var in *activitymod.ModifyCond + if err := c.ShouldBind(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web activity Modify arg error "+err.Error()) + return + } + code := activityser.Modify(in) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, "success") +} + +// 查询 +func QueryAll(c *gin.Context) { + var in *activitymod.QueryAllCond + if err := c.ShouldBindQuery(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web activity QueryAll arg error "+err.Error()) + return + } + data, code := activityser.QueryAll(in) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, data) +} + +func QueryDropDownBox(c *gin.Context) { + data, code := activityser.QueryDropDownBox() + common.ServeJSON(c, code, data) +} diff --git a/web/api/activityctrl/prize.go b/web/api/activityctrl/prize.go new file mode 100644 index 0000000..10bfb2d --- /dev/null +++ b/web/api/activityctrl/prize.go @@ -0,0 +1,65 @@ +package activityctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/prizemod" + "91porn-server/web/service/activityser" + + "github.com/gin-gonic/gin" +) + +// 新增 +func AddPrize(c *gin.Context) { + var in prizemod.AddPrizeCond + if err := c.ShouldBind(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web activity Add arg error "+err.Error()) + return + } + code := activityser.AddPrize(in) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, "success") +} + +// 修改 +func ModifyPrize(c *gin.Context) { + var in *prizemod.ModifyPrizeCond + if err := c.ShouldBind(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web activity Modify arg error "+err.Error()) + return + } + code := activityser.ModifyPrize(in) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, "success") +} + +// 查询 +func QueryAllPrize(c *gin.Context) { + var in *prizemod.QueryAllPrizeCond + if err := c.ShouldBindQuery(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web activity QueryAll arg error "+err.Error()) + return + } + data, code := activityser.QueryAllPrize(in) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, data) +} + +func QueryVipDropDownBox(c *gin.Context) { + data, code := activityser.QueryVipDropDownBox() + common.ServeJSON(c, code, data) +} + +func QueryPrizeDropDownBox(c *gin.Context) { + data, code := activityser.QueryPrizeDropDownBox() + common.ServeJSON(c, code, data) +} diff --git a/web/api/activityctrl/prize_pool.go b/web/api/activityctrl/prize_pool.go new file mode 100644 index 0000000..b0d4cd1 --- /dev/null +++ b/web/api/activityctrl/prize_pool.go @@ -0,0 +1,55 @@ +package activityctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/activitymod" + "91porn-server/web/service/activityser" + + "github.com/gin-gonic/gin" +) + +// 新增 +func AddPrizePool(c *gin.Context) { + var in activitymod.AddPrizePoolCond + if err := c.ShouldBind(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web prizePool Add arg error "+err.Error()) + return + } + code := activityser.AddPrizePool(in) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, "success") +} + +// 修改 +func ModifyPrizePool(c *gin.Context) { + var in *activitymod.ModifyPrizePoolCond + if err := c.ShouldBind(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web prizePool Modify arg error "+err.Error()) + return + } + code := activityser.ModifyPrizePool(in) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, "success") +} + +// 查询 +func QueryAllPrizePool(c *gin.Context) { + var in *activitymod.QueryAllPrizePoolCond + if err := c.ShouldBindQuery(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web prizePool QueryAll arg error "+err.Error()) + return + } + data, code := activityser.QueryAllPrizePool(in) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, data) +} diff --git a/web/api/activityctrl/user_record.go b/web/api/activityctrl/user_record.go new file mode 100644 index 0000000..e36965d --- /dev/null +++ b/web/api/activityctrl/user_record.go @@ -0,0 +1,24 @@ +package activityctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/activitymod" + "91porn-server/web/service/activityser" + + "github.com/gin-gonic/gin" +) + +func QueryAllRecord(c *gin.Context) { + var in *activitymod.QueryAllRecordCond + if err := c.ShouldBindQuery(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web activity QueryAllRecord arg error "+err.Error()) + return + } + data, code := activityser.QueryAllRecord(in) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, data) +} diff --git a/web/api/actvctrl/actv.go b/web/api/actvctrl/actv.go new file mode 100644 index 0000000..2d9fdb9 --- /dev/null +++ b/web/api/actvctrl/actv.go @@ -0,0 +1,57 @@ +package actvctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/web/service/actvser" + + "github.com/gin-gonic/gin" +) + +func GetActivities(c *gin.Context) { + var r actvser.GetActitiesRequest + if err := c.Bind(&r); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + count, activities, hasNext, err := actvser.GetActities(r) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(c, stderr.Success, gin.H{ + "total": count, + "list": activities, + "hasNext": hasNext, + }) +} + +func UpdateActivity(c *gin.Context) { + var r actvser.UpdateActivityRequest + if err := c.Bind(&r); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + if r.ActvID.IsZero() { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + if err := actvser.UpdateActivity(r); err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(c, stderr.Success, nil) +} + +func AddActivity(c *gin.Context) { + var r actvser.AddActivityRequest + if err := c.Bind(&r); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + if err := actvser.AddActivity(r); err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(c, stderr.Success, nil) +} diff --git a/web/api/addwlefarectrl/addwelfare.go b/web/api/addwlefarectrl/addwelfare.go new file mode 100644 index 0000000..3f24d1e --- /dev/null +++ b/web/api/addwlefarectrl/addwelfare.go @@ -0,0 +1,40 @@ +package addwlefarectrl + +import ( + "net/http" + + "91porn-server/common" + "91porn-server/common/daichong" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/web/service/addwelfareser" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" +) + +func AddWelfare(ctx *gin.Context) { + var args struct { + AppID string `form:"appId" json:"appId" binding:"required"` + Data string `form:"data" json:"data" binding:"required"` + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web activity List arg error "+err.Error()) + return + } + if args.AppID != webg.Conf.Welfare.AppID { + ctx.JSON(http.StatusOK, gin.H{"data": nil, "code": stderr.ErrAppIDNotExist, "msg": stderr.ErrAppIDNotExist.Msg()}) + return + } + var sign = struct { + Time string `json:"time"` + Uid string `json:"uid"` + }{} + if err := daichong.Sign2Struct(&sign, args.Data, webg.Conf.Welfare.AppSecret); err != nil { + log.Warn("AddWelfare callback sign data fail error", log.E(err)) + ctx.JSON(http.StatusOK, gin.H{"data": nil, "code": stderr.Failure, "msg": stderr.Failure.Msg()}) + return + } + code, err := addwelfareser.AddWelfare(sign.Uid) + common.ServeJSON(ctx, code, err) +} diff --git a/web/api/adminctrl/admin.go b/web/api/adminctrl/admin.go new file mode 100644 index 0000000..efcc220 --- /dev/null +++ b/web/api/adminctrl/admin.go @@ -0,0 +1,524 @@ +package adminctrl + +import ( + "encoding/base32" + "encoding/json" + "fmt" + "strings" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/googauth" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/adminmod" + "91porn-server/models/v/authoritymod" + "91porn-server/web/middleware/authweb" + "91porn-server/web/service/adminser" + "91porn-server/web/service/authorser" + + "github.com/gin-gonic/gin" + "github.com/skip2/go-qrcode" + "go.mongodb.org/mongo-driver/bson/primitive" + "golang.org/x/crypto/bcrypt" +) + +// Login doc +// @Summary 登陆 - 管理员登陆 +// @Description 管理员登陆 +// @Tags web-登陆 +// @Accept mpfd,json +// @Produce json,html +// @Param name formData string true "账号" +// @Param password formData string true "密码" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/login [post] +func Login(c *gin.Context) { + var arg struct { + Name string `form:"name" json:"name" binding:"required"` + Password string `form:"password" json:"password" binding:"required"` + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "admin Login arg error: "+err.Error()) + return + } + adm, err := adminmod.FindOneByName(arg.Name) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, "admin Login FindOne error: "+err.Error()) + return + } + if adm.Name == "" { + common.ServeJSON(c, stderr.AdminIsNotExist, "Login admin is not exist") + return + } + //校验密码 + if err = bcrypt.CompareHashAndPassword([]byte(adm.Password), []byte(arg.Password)); err != nil { + common.ServeJSON(c, stderr.AdminIsNotExist, "Login admin password error") + return + } + if adm.HasLocked { + common.ServeJSON(c, stderr.AdminIsHasLockedErr, "Login admin haslocked") + return + } + var qrCodePNG []byte + if !adm.HasBind { + url := googauth.New(adm.Secret, adm.Name+"@91porn") + qrCodePNG, err = qrcode.Encode(url, qrcode.Medium, 256) + if err != nil { + common.ServeJSON(c, stderr.AdminIsNotExist, "Login GetQRCodePNG error: "+err.Error()) + return + } + } + common.ServeJSON(c, stderr.Success, gin.H{ + "qrCodePNG": qrCodePNG, + }) +} + +// SLogin doc +// @Summary 管理员sso登陆 +// @Description 管理员sso登陆 +// @Tags web-单点登陆 +// @Accept mpfd,json +// @Produce json,html +// @Param name formData string true "账号" +// @Param password formData string true "密码" +// @Success 200 {string} json "{"msg": "管理员信息""}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/slogin [post] +func SLogin(c *gin.Context) { + var arg struct { + Name string `form:"name" json:"name" binding:"required"` + Password string `form:"password" json:"password" binding:"required"` + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "admin SLogin arg error: "+err.Error()) + return + } + adm, err := adminmod.FindOneByName(arg.Name) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, "admin SLogin FindOne error: "+err.Error()) + return + } + if adm.Name == "" { + common.ServeJSON(c, stderr.AdminIsNotExist, "SLogin admin is not exist") + return + } + //校验密码 + if err = bcrypt.CompareHashAndPassword([]byte(adm.Password), []byte(arg.Password)); err != nil { + common.ServeJSON(c, stderr.AdminIsNotExist, "Login admin password error") + return + } + if adm.HasLocked { + common.ServeJSON(c, stderr.AdminIsHasLockedErr, "SLogin admin haslocked") + return + } + // SLogin 仅允许角色名包含"客服"的账号登录,其余角色一律拒绝 + if !strings.Contains(adm.Role, "客服") { + common.ServeJSON(c, stderr.ErrAccessForbid, "SLogin 仅限客服角色登录") + return + } + var cid string + token, err := authweb.GenAndSaveToken(&authweb.Claims{Type: authweb.Admin, Act: adm.Name, Role: adm.Role, CID: cid}) + if err != nil { + common.ServeJSON(c, stderr.Failure, "SLogin admin GenerateToken error: "+err.Error()) + return + } + _ = adminmod.Bind(arg.Name) + authJSON, _ := authoritymod.FineOneByRole(adm.Role) + common.ServeJSON(c, stderr.Success, gin.H{ + "token": token, + "authJson": authJSON.AuthJson, + "actJson": authJSON.ActJson, + "admin": adminmod.AdminDoc{ + ID: &adm.ID, + Name: &adm.Name, + Role: &adm.Role, + Nickname: &adm.Nickname, + }, + }) +} + +// Verify doc +// @Summary 管理员二步验证 +// @Description 管理员二步验证 +// @Tags web-登陆 +// @Accept mpfd,json +// @Produce json,html +// @Param name formData string true "账号" +// @Param otp formData string true "动态口令" +// @Success 200 {string} json "{"msg": "管理员信息""}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/verify [post] +func Verify(c *gin.Context) { + var arg struct { + Name string `form:"name" json:"name" binding:"required"` + Otp string `form:"otp" json:"otp" binding:"required"` + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "admin Verification arg error: "+err.Error()) + return + } + adm, err := adminmod.FindOneByName(arg.Name) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, "admin Verification FindOne error: "+err.Error()) + return + } + if adm.Name == "" { + log.Error(fmt.Sprintf("用户[%v]不存在", arg.Name)) + common.ServeJSON(c, stderr.ErrAccessForbid, "admin is not exist") + return + } + //测试模式不使用两步验证 + if gin.Mode() != gin.DebugMode { + ok, err := googauth.Verify(adm.Secret, arg.Otp) + if err != nil { + common.ServeJSON(c, stderr.GoogleAuthFail, "admin Verification 2-step error: "+err.Error()) + return + } + if !ok { + common.ServeJSON(c, stderr.GoogleAuthFail, "admin Verification 2-step verification faild") + return + } + } + var cid string + if adm.Role == "裸聊商家" { + common.ServeJSON(c, stderr.GoogleAuthFail, "role faild") + return + } + token, err := authweb.GenAndSaveToken(&authweb.Claims{Type: authweb.Admin, Act: adm.Name, Role: adm.Role, CID: cid}) + if err != nil { + common.ServeJSON(c, stderr.Failure, "admin GenerateToken error: "+err.Error()) + return + } + _ = adminmod.Bind(arg.Name) + authJSON, _ := authoritymod.FineOneByRole(adm.Role) + common.ServeJSON(c, stderr.Success, gin.H{ + "token": token, + "authJson": authJSON.AuthJson, + "actJson": authJSON.ActJson, + "admin": adminmod.AdminDoc{ + ID: &adm.ID, + Name: &adm.Name, + Role: &adm.Role, + Nickname: &adm.Nickname, + }, + }) +} + +// Logout doc +// @Summary 注销 - 管理员注销 +// @Description 管理员登陆 +// @Tags web-登陆 +// @Accept mpfd,json +// @Produce json,html +// @Param name formData string true "账号" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/logout [post] +func Logout(c *gin.Context) { + if name, err := common.GetAdminAct(c); err == nil { + authweb.RevokeAdminToken(name) + } else { + log.Error("admin logout error", log.E(err)) + } + common.ServeJSON(c, stderr.Success, "") +} + +// Logout doc +// @Summary 刷新token +// @Description 刷新token +// @Tags web-登陆 +// @Accept json +// @Produce json,html +// @Success 200 {string} json "{"token": "token1"}" +// @Router /web/admin/refresh [post] +func Refresh(c *gin.Context) { + name, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err) + return + } + cid, err := common.GetJuShangID(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err) + return + } + role, err := common.GetAdminRole(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err) + return + } + token, err := authweb.GenAndSaveToken(&authweb.Claims{Type: authweb.Admin, Act: name, Role: role, CID: cid}) + if err != nil { + common.ServeJSON(c, stderr.AdminRefreshTokenErr, err) + return + } + common.ServeJSON(c, stderr.Success, gin.H{"token": token}) +} + +// Add doc +// @Summary 登陆 - 添加管理员 +// @Description 添加管理员 +// @Tags web-admin +// @Accept mpfd,json +// @Produce json,html +// @Param name formData string true "账号" +// @Param password formData string true "密码" +// @Param nickname formData string false "昵称" +// @Param email formData string false "邮箱" +// @Param privilegeID formData string false "权限ID" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/add [post] +func Add(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + Name string `form:"name" json:"name" binding:"required"` + Password string `form:"password" json:"password" binding:"required"` + Nickname *string `form:"nickname" json:"nickname" binding:""` //昵称 + Email *string `form:"email" json:"email" binding:"omitempty,email"` //邮箱 + PrivilegeID *primitive.ObjectID `form:"privilegeID" json:"privilegeID" binding:""` + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, fmt.Errorf("auth Add arg error: %+v", err)) + return + } + existed, err := adminmod.ExistsByName(arg.Name) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, fmt.Errorf("auth Add Exists error: %+v", err)) + return + } + if existed { + common.ServeJSON(c, stderr.AdminIsExisted, fmt.Errorf("auth Add admin is existed")) + return + } + var role *string + if arg.PrivilegeID != nil { + auth, err := authoritymod.FindOne(authoritymod.AuthorityDoc{ID: *arg.PrivilegeID}) + if err != nil { + common.ServeJSON(c, stderr.Failure, fmt.Errorf("auth FindOne error: %+v", err)) + return + } + if auth.ID.IsZero() { + common.ServeJSON(c, stderr.AuthIsNotExist, gin.H{"privilegeID": arg.PrivilegeID}) + return + } + role = &auth.Role + } + //生成密码 + hashPwd, _ := bcrypt.GenerateFromPassword([]byte(arg.Password), bcrypt.DefaultCost) + hashPwdStr := string(hashPwd) + secret := base32.StdEncoding.EncodeToString([]byte(fmt.Sprintf("admin%s", common.RandStr(10)))) + hasBind := false + if err = adminmod.Insert(nil, adminmod.AdminDoc{ + Name: &arg.Name, + Password: &hashPwdStr, + Secret: &secret, + HasBind: &hasBind, + Nickname: arg.Nickname, + Email: arg.Email, + Role: role, + }); err != nil { + common.ServeJSON(c, stderr.ErrDbInsertError, fmt.Errorf("auth Add Admin.Insert error: %+v", err)) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.Administrator, constant.Add, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// List doc +// @Summary 获取管理员列表 +// @Description 获取管理员列表 +// @Tags web-admin +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query int true "当前页" +// @Param pageSize query int true "每页条数" +// @Param name query string false "过滤条件-管理员账号" +// @Success 200 {string} json "{"msg": "操作成功", "date": { "total":10, "list":[] }}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/list [get] +func List(c *gin.Context) { + type Query struct { + Name *string `form:"name" json:"name" bson:",omitempty"` //用户名 + } + var arg struct { + commod.Page + Query + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "admin List arg error "+err.Error()) + return + } + skip := int64((arg.PageNumber - 1) * arg.PageSize) + limit := int64(arg.PageSize) + page, err := adminser.AdminPages(skip, limit, arg.Name) + if err != nil { + common.ServeJSON(c, stderr.Failure, "admin List.AdminPages error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} + +// Delete doc +// @Summary 登陆 - 删除管理员 +// @Description 删除管理员 +// @Tags web-admin +// @Accept mpfd,json +// @Produce json,html +// @Param adminID formData string true "目标管理员ID" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/del [delete] +func Delete(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + AdminID string `form:"adminID" json:"adminID,omitempty" binding:"required"` + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "author Delete arg error "+err.Error()) + return + } + adminID, err := primitive.ObjectIDFromHex(arg.AdminID) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + // 删除前查询账号名,用于清除token + adm, _ := adminmod.FindOneByID(adminID) + if err := adminmod.Delete(adminmod.AdminDoc{ID: &adminID}); err != nil { + common.ServeJSON(c, stderr.AdminIsNotExist, "author Delete error "+err.Error()) + return + } + // 清除已删除账号的token + if adm.Name != "" { + authweb.RevokeAdminToken(adm.Name) + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.Administrator, constant.Delete, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// Update doc +// @Summary 登陆 - 修改管理员 +// @Description 修改管理员 +// @Tags web-admin +// @Accept mpfd,json +// @Produce json,html +// @Param adminID formData string true "目标管理员ID" +// @Param nickname formData string false "昵称" +// @Param email formData string false "邮箱" +// @Param privilegeID formData string false "权限ID, "": 删除权限" +// @Param password formData string false "密码" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/update [post] +func Update(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + AdminID string `form:"adminID" json:"adminID" binding:"required"` + Nickname *string `form:"nickname" json:"nickname" binding:""` //昵称 + Email *string `form:"email" json:"email" binding:"omitempty,email"` //邮箱 + PrivilegeID *primitive.ObjectID `form:"privilegeID" json:"privilegeID" binding:""` + Password *string `form:"password" json:"password" binding:""` + HasLocked *bool `json:"hasLocked,omitempty" form:"hasLocked"` //已禁止登陆 + LockReason *string `json:"lockReason,omitempty" form:"lockReason"` //禁止登陆的原因 + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "author Update arg error "+err.Error()) + return + } + adminID, err := primitive.ObjectIDFromHex(arg.AdminID) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if arg.PrivilegeID != nil { + if err := authorser.SetAuthority(adminID, *arg.PrivilegeID); err != nil { + common.ServeJSON(c, stderr.Failure, "auth SetAuthority error: "+err.Error()) + return + } + } + doc := adminmod.AdminDoc{ + Nickname: arg.Nickname, + Email: arg.Email, + HasLocked: arg.HasLocked, + LockReason: arg.LockReason, + } + //更新密码 + if arg.Password != nil { + hashPwd, _ := bcrypt.GenerateFromPassword([]byte(*arg.Password), bcrypt.DefaultCost) + hashPwdStr := string(hashPwd) + doc.Password = &hashPwdStr + } + if err := adminmod.UpdateByID(adminID, doc); err != nil { + common.ServeJSON(c, stderr.ErrDbUpdateError, "author UpdateByID error "+err.Error()) + return + } + // 禁用账号时清除token,使其立即失效 + if arg.HasLocked != nil && *arg.HasLocked { + adm, _ := adminmod.FindOneByID(adminID) + if adm.Name != "" { + authweb.RevokeAdminToken(adm.Name) + } + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.Administrator, constant.Modify, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// RefreshToken doc +// @Summary token刷新 +// @Description token刷新 +// @Tags web-登陆 +// @Accept mpfd,json +// @Produce json,html +// @Param name formData string true "账号" +// @Param password formData string true "密码" +// @Param otp formData string true "动态口令" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/channel/user/refresh [get] +func RefreshToken(c *gin.Context) { + account, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err) + return + } + role, err := common.GetAdminRole(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err) + return + } + cid, err := common.GetJuShangID(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err) + return + } + token, err := authweb.GenAndSaveToken(&authweb.Claims{Type: authweb.Admin, Act: account, Role: role, CID: cid}) + if err != nil { + common.ServeJSON(c, stderr.ErrUnrecognized, "generator token error!") + return + } + c.Writer.Header().Set("Refresh-Authorization", "false") + type RefreshTokenResp struct { + Token string `json:"token"` + } + common.ServeJSON(c, stderr.Success, RefreshTokenResp{Token: token}) +} diff --git a/web/api/adsctrl/ads.go b/web/api/adsctrl/ads.go new file mode 100644 index 0000000..c03ba9c --- /dev/null +++ b/web/api/adsctrl/ads.go @@ -0,0 +1,263 @@ +package adsctrl + +import ( + "encoding/json" + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/ysurl" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/adreviewmod" + "91porn-server/models/v/adsmod" + "91porn-server/web/service/adser" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// List doc +// @Summary 广告列表 +// @Description 广告列表 +// @Tags ads +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "id" +// @Param position formData integer true "广告位置" +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Param sort formData string true "排序的字段" +// @Param desc formData integer true "1 正序 -1倒序" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ads/list [get] +func List(ctx *gin.Context) { + type Query struct { + ID *string `form:"id" json:"id"` //id + AdsType *adsmod.AdsType `form:"adsType" json:"adsType"` //广告类型 + Position *adsmod.AdPosition `form:"position" json:"position"` //广告位置 + DiscCode *string `form:"discCode" json:"discCode"` //商区码 + } + var args struct { + commod.Page + Query + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "adsctrl List arg error "+err.Error()) + return + } + skip := int64((args.PageNumber - 1) * args.PageSize) + limit := int64(args.PageSize) + var id *primitive.ObjectID + if args.ID != nil { + i, err := primitive.ObjectIDFromHex(*args.ID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "adsctrl List arg error "+err.Error()) + return + } + id = &i + } + page, err := adser.AdPages(skip, limit, + adsmod.IDMatch{ID: id}, + adsmod.AdsTypeMatch{AdsType: args.AdsType}, + adsmod.AdPositionMatch{Position: args.Position}, + adsmod.DistrictCodeMatch{DistrictCode: args.DiscCode}, + ) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, "adsctrl AdPages error: "+err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, page) +} + +// Update doc +// @Summary 更新广告信息 +// @Description 更新广告信息 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "ID" +// @Param title formData string false "广告标题" +// @Param cover formData string false "广告封面图" +// @Param sortCode formData integer false "排序号" +// @Param type formData integer false "广告类型" +// @Param href formData string false "广告跳转地址" +// @Param position formData integer false "广告位置" +// @Param active formData boolean false "激活状态" +// @Param remark formData string false "备注" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ads/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + type Update struct { + Title *string `form:"title" json:"title" binding:""` //广告标题 + Cover *string `form:"cover" json:"cover" binding:"required_with=Position"` //广告封面图 + HrefType *adsmod.URLJumpType `form:"hrefType" json:"hrefType" binding:"required_with=Href"` //广告类型 + Href *string `form:"href" json:"href" binding:"required_with=HrefType"` //广告跳转地址 + Position *adsmod.AdPosition `form:"position" json:"position" binding:"required_with=Cover"` //广告位置 + SortCode *int `form:"sortCode" json:"sortCode" binding:""` //排序号 + Active *bool `form:"active" json:"active" binding:""` //激活状态 + Remark *string `form:"remark" json:"remark" binding:""` //备注 + Start *time.Time `form:"start" json:"start" binding:""` //开始时间 + End *time.Time `form:"end" json:"end" binding:""` //结束时间 + } + var args struct { + ID primitive.ObjectID `form:"id" json:"id" binding:"required"` + Update + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "adsctrl Update arg error "+err.Error()) + return + } + if args.HrefType != nil && args.Href != nil { + if *args.Href != "" && !ysurl.IsValidURL(*args.HrefType, *args.Href) { + common.ServeJSON(ctx, stderr.URLInvalid, "") + return + } + } + doc := adsmod.UpdateDoc{ + Title: args.Title, + Cover: args.Cover, + HrefType: args.HrefType, + Href: args.Href, + Position: args.Position, + SortCode: args.SortCode, + Active: args.Active, + Remark: args.Remark, + Start: args.Start, + End: args.End, + } + if doc.Cover != nil { + prop := common.GetPictureProp(common.ImgUrlHost + *doc.Cover) + doc.CoverProp = prop + } + if err = adsmod.UpdateOneByID(nil, args.ID, doc); err != nil { + switch err.(type) { + case adsmod.ADSNotExistError: + common.ServeJSON(ctx, stderr.ADSNotExist, err.Error()) + default: + log.Error("adsctrl Update failed", log.E(err)) + common.ServeJSON(ctx, stderr.Failure, fmt.Errorf("adsctrl Update error: %+v", err)) + } + return + } + log, _ := json.Marshal(args) + _ = operatorlgmod.RecordOperation(manager, constant.AdsManageAdsList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Delete doc +// @Summary 删除广告 +// @Description 删除广告 +// @Tags follow +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData array true "广告id数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ads/del [delete] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var args struct { + ID primitive.ObjectID `form:"id" json:"id" binding:"required"` + ArID *primitive.ObjectID `form:"arID" json:"arID" binding:""` + } + if err = ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "adsctrl Delete arg error "+err.Error()) + return + } + if err = adser.DeleteAd(&args.ID, args.ArID); err != nil { + switch err.(type) { + case adreviewmod.ADReviewNotExistError: + common.ServeJSON(ctx, stderr.ADReviewNotExist, err.Error()) + case adsmod.ADSNotExistError: + common.ServeJSON(ctx, stderr.ADSNotExist, err.Error()) + default: + common.ServeJSON(ctx, stderr.Failure, err.Error()) + } + return + } + log, _ := json.Marshal(args) + _ = operatorlgmod.RecordOperation(manager, constant.AdsManageAdsList, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// AddSysAds doc +// @Summary 增加一条广告 +// @Description 增加一条广告 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param title formData string false "广告标题" +// @Param cover formData string false "广告封面图" +// @Param sortCode formData integer false "排序号" +// @Param type formData integer false "广告类型" +// @Param href formData string false "广告跳转地址" +// @Param position formData integer false "广告位置" +// @Param active formData bool false "激活状态" +// @Param remark formData string false "备注" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ads/add [post] +func Add(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var args struct { + Title string `form:"title" json:"title" binding:"required"` //广告标题 + Cover string `form:"cover" json:"cover" binding:"required"` //广告封面图 + HrefType *adsmod.URLJumpType `form:"hrefType" json:"hrefType" binding:"required"` //广告跳转类型, + Href *string `form:"href" json:"href" binding:"required"` //广告跳转地址 + Position *adsmod.AdPosition `form:"position" json:"position" binding:"required"` //广告位置 + Active *bool `form:"active" json:"active" binding:"required"` //激活状态 + SortCode *int `form:"sortCode" json:"sortCode" binding:"required"` //排序号 + Remark string `form:"remark" json:"remark" binding:""` //备注 + Start *time.Time `form:"start" json:"start" binding:""` //开始时间 + End *time.Time `form:"end" json:"end" binding:""` //结束时间 + } + if err = ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if *args.Href != "" && !ysurl.IsValidURL(*args.HrefType, *args.Href) { + common.ServeJSON(ctx, stderr.URLInvalid, "") + return + } + doc := adsmod.InsertDoc{ + AdsType: adsmod.SysAds, + Cover: args.Cover, + Title: args.Title, + HrefType: *args.HrefType, + Href: *args.Href, + Position: *args.Position, + SortCode: *args.SortCode, + Active: *args.Active, + Remark: args.Remark, + Start: *args.Start, + End: *args.End, + } + prop := common.GetPictureProp(common.ImgUrlHost + doc.Cover) + doc.CoverProp = prop + if _, err = adsmod.InsertOne(nil, doc); err != nil { + common.ServeJSON(ctx, stderr.Failure, fmt.Errorf("adsctrl Add error: %+v", err)) + return + } + log, _ := json.Marshal(args) + _ = operatorlgmod.RecordOperation(manager, constant.AdsManageAdsList, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/adv_group_ctrl/adv_group.go b/web/api/adv_group_ctrl/adv_group.go new file mode 100644 index 0000000..863e823 --- /dev/null +++ b/web/api/adv_group_ctrl/adv_group.go @@ -0,0 +1,33 @@ +package adv_group_ctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/web/service/advgroupser" + + "github.com/gin-gonic/gin" +) + +// @Tags 广告AB测试 +// @Summary 查询广告统计数据 +// @Description 查询广告统计数据 +// @Accept json +// @Produce json +// @Param param body advgroupser.AdGroupStatReq true "参数列表" +// @Success 200 {object} advgroupser.QueryRes "成功" +// @Failure 400 {string} string "获取失败的返回结果" +// @Router /api/web/admin/adgroup/list [POST] +func GetAdGroupStatList(ctx *gin.Context) { + var req = &advgroupser.AdGroupStatReq{} + if err := ctx.ShouldBind(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + resp, serr := advgroupser.GetAdGroupStatList(ctx, req) + if serr != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + + common.ServeJSON(ctx, stderr.Success, resp) +} diff --git a/web/api/advance_config_ctrl/advance_config.go b/web/api/advance_config_ctrl/advance_config.go new file mode 100644 index 0000000..d77e4c7 --- /dev/null +++ b/web/api/advance_config_ctrl/advance_config.go @@ -0,0 +1,158 @@ +package advance_config_ctrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/advanceconfigmod" + "91porn-server/web/service/advance_config_ser" + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" +) + +// QueryAll doc +// @Summary 查询预售配置列表 +// @Description 查询预售配置列表 +// @Tags 后台-预售配置 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData primitive.ObjectID true "文档id" +// @Param status formData integer true "预售订单状态" +// @Param oid formData string true "支付平台订单号" +// @Param uid formData integer true "用户ID" +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {object} advanceconfigmod.QueryAllRes +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/advance_config/list [get] +func QueryAll(c *gin.Context) { + var in advanceconfigmod.QueryAllCond + if err := c.ShouldBindQuery(&in); err != nil { + log.Error(fmt.Sprintf("Query all advance_config param err:%v", err)) + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + + data, err := advance_config_ser.QueryAll(&in) + if err != nil { + log.Error(fmt.Sprintf("Query all advance_config err:%v\n%d", data, err)) + common.ServeJSON(c, stderr.Failure, nil) + return + } + common.ServeJSON(c, stderr.Success, data) +} + +// Create doc +// @Summary 新增预售配置 +// @Description 新增预售配置 +// @Tags 后台-预售配置 +// @Accept mpfd,json +// @Produce json +// @Param q query advanceconfigmod.WebCreateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/advance_config/create [post] +func Create(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &advanceconfigmod.WebCreateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + log.Error(fmt.Sprintf("Create advance_config param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 创建 + err = advance_config_ser.Create(p, manager) + if err != stderr.Success { + log.Error(fmt.Sprintf("Create advance_config err:%v\n", err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.AdvanceConfig, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 更新预售配置 +// @Description 更新预售配置 +// @Tags 后台-预售配置 +// @Accept mpfd,json +// @Produce json +// @Param q query advanceconfigmod.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/advance_config/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &advanceconfigmod.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + log.Error(fmt.Sprintf("Update all advance_config param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = advance_config_ser.Update(p, manager) + if err != stderr.Success { + log.Error(fmt.Sprintf("Update all advance_config err:%v\n", err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.AdvanceConfig, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除预售配置 +// @Description 删除预售配置 +// @Tags 后台-预售配置 +// @Accept mpfd,json +// @Produce json +// @Param q query advanceconfigmod.WebDeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/advance_config/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &advanceconfigmod.WebDeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + log.Error(fmt.Sprintf("Del all advance_config param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = advance_config_ser.Del(p, manager) + if err != stderr.Success { + log.Error(fmt.Sprintf("Del advance_config err:%v", err)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.AdvanceConfig, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/advance_order_ctrl/advance_order.go b/web/api/advance_order_ctrl/advance_order.go new file mode 100644 index 0000000..d4d8e59 --- /dev/null +++ b/web/api/advance_order_ctrl/advance_order.go @@ -0,0 +1,83 @@ +package advance_order_ctrl + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/advanceordermod" + "91porn-server/models/v/export_task_mod" + "91porn-server/web/service/advance_order_ser" + "fmt" + + "github.com/gin-gonic/gin" +) + +// QueryAll doc +// @Summary 查询预售订单列表 +// @Description 查询预售订单列表 +// @Tags 预售订单 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData primitive.ObjectID true "文档id" +// @Param status formData integer true "预售订单状态" +// @Param oid formData string true "支付平台订单号" +// @Param uid formData integer true "用户ID" +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {object} advanceordermod.QueryAllRes +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/advance_order [get] +func QueryAll(c *gin.Context) { + var in advanceordermod.QueryAllCond + if err := c.ShouldBindQuery(&in); err != nil { + log.Error(fmt.Sprintf("Query all advance_order param err:%v\n", err)) + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + + data, err := advance_order_ser.QueryAll(&in) + if err != nil { + log.Error(fmt.Sprintf("Query all advance_order err:%v\n%d", data, err)) + common.ServeJSON(c, stderr.Failure, nil) + return + } + common.ServeJSON(c, stderr.Success, data) +} + +// Export doc +// @Summary 导出预售订单列表 +// @Description 导出预售订单列表 +// @Tags 预售订单 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData primitive.ObjectID true "文档id" +// @Param status formData integer true "预售订单状态" +// @Param oid formData string true "支付平台订单号" +// @Param uid formData integer true "用户ID" +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Param timeField formData integer true "时间筛选字段 createdAt 创建时间 endTime 预售尾款结束时间" +// @Param start formData integer true "开始时间" +// @Param end formData integer true "结束时间" +// @Success 200 {object} advanceordermod.QueryAllRes +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/advance_order/export [post] +func Export(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req advanceordermod.QueryAllCond + if err = ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("Query all advance_order param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + err = export_task_mod.CreateTask(export_task_mod.ExportAdvanceOrderTask, manager, req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrExportFileFail, err) + return + } + common.ServeJSON(ctx, stderr.Success, "success") +} diff --git a/web/api/ai_changeface_ctrl/ai_changeface.go b/web/api/ai_changeface_ctrl/ai_changeface.go new file mode 100644 index 0000000..ea36c1d --- /dev/null +++ b/web/api/ai_changeface_ctrl/ai_changeface.go @@ -0,0 +1,417 @@ +package ai_changeface_ctrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/aichangefaceimgmod" + "91porn-server/models/v/aichangefacemod" + "91porn-server/models/v/aichangefacevidmod" + "91porn-server/web/service/ai_changeface_img_ser" + "91porn-server/web/service/ai_changeface_ser" + "encoding/json" + "fmt" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// List doc +// @Summary AI换脸订单列表 +// @Description AI换脸订单列表 +// @Tags AI视频换脸-WEB +// @Accept json +// @Produce json +// @Param status query int false "记录状态" +// @Param pageNumber query int true "第几页" +// @Param pageSize query int true "每页数量" +// @Success 200 object interface{} "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ai/changeface/list [get] +func List(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req aichangefacemod.ListRequest + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + var id *primitive.ObjectID + if req.ID != nil { + tmpId, err := primitive.ObjectIDFromHex(*req.ID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if tmpId.IsZero() { + common.ServeJSON(ctx, stderr.ErrParamError, "zero id") + return + } + id = &tmpId + } + if req.Uid != nil && *req.Uid == 0 { + common.ServeJSON(ctx, stderr.ErrParamError, "zero uid") + return + } + + list, count, hasNext, err := ai_changeface_ser.List(id, &req) + if err != nil { + log.Error(fmt.Sprintf("ai_changeface_ser web List error%v, manager%v", err.Error(), manager)) + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": list, + "total": count, + "hasNext": hasNext, + }) +} + +// Update doc +// @Summary 更新AI换脸记录 +// @Description 更新AI换脸记录 +// @Tags AI视频换脸-WEB +// @Accept json +// @Produce json +// @Param id formData string true "AI换脸订单ID" +// @Param status formData int false "状态 0 未完成; 1 已完成; -1 已退款" +// @Param cover formData string false "换脸后封面大图" +// @Param url formData string false "换脸后视频地址" +// @Param remark formData string false "拒绝理由" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ai/changeface/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req aichangefacemod.EditAiChangeFace + if err = ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("ai changeface Update param err%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if req.ID.IsZero() { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + //if code := ai_changeface_ser.Update(manager, &req); code != stderr.Success { + if code := ai_changeface_ser.UpdateNew(manager, &req); code != stderr.Success { + log.Error(fmt.Sprintf("ai changeface Update err%v", code)) + common.ServeJSON(ctx, code, code.Error()) + return + } + updateLog, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.AiChangeface, constant.Modify, string(updateLog), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// ModList doc +// @Summary AI换脸视频模版列表 +// @Description AI换脸视频模版列表 +// @Tags AI模版模块-WEB +// @Accept json +// @Produce json +// @Param status query int false "记录状态" +// @Param pageNumber query int true "第几页" +// @Param pageSize query int true "每页数量" +// @Success 200 object interface{} "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ai/changeface/mod/list [get] +func ModList(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req aichangefacevidmod.WebListRequest + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, code := ai_changeface_ser.ModList(&req) + if code != stderr.Success { + log.Error(fmt.Sprintf("ai_changeface_ser web List error%v,manager%v", code.Error(), manager)) + common.ServeJSON(ctx, code, code.Error()) + return + } + if err != nil { + log.Error(fmt.Sprintf("ai_changeface_ser web List error%v,manager%v", err.Error(), manager)) + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// ModUpdate doc +// @Summary AI换脸视频模版编辑 +// @Description AI换脸视频模版编辑 +// @Tags AI模版模块-WEB +// @Accept json +// @Produce json +// @Param status query int false "记录状态" +// @Param pageNumber query int true "第几页" +// @Param pageSize query int true "每页数量" +// @Success 200 object interface{} "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ai/changeface/mod/update [post] +func ModUpdate(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req aichangefacevidmod.EditAiChangeFaceVid + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if req.ID.IsZero() { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if err = ai_changeface_ser.UpdateMod(req); err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + updateLog, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.AiChangefaceVidMod, constant.Modify, string(updateLog), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// ModAdd doc +// @Summary 添加AI换脸视频模版 +// @Description 添加AI换脸视频模版 +// @Tags AI模版模块-WEB +// @Accept json +// @Produce json +// @Param status query int false "记录状态" +// @Param pageNumber query int true "第几页" +// @Param pageSize query int true "每页数量" +// @Success 200 object interface{} "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ai/changeface/mod/add [post] +func ModAdd(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req aichangefacevidmod.AddAiChangeFaceVidReq + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if err = ai_changeface_ser.AddMod(req); err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + addLog, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.AiChangefaceVidMod, constant.Add, string(addLog), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// GetById doc +// @Summary 根据ID获取AI模版 +// @Description 根据ID获取AI模版 +// @Tags AI模版模块-WEB +// @Accept json +// @Produce json +// @Param id query string false "模版ID" +// @Success 200 object interface{} "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ai/changeface/mod/getById [get] +func GetById(ctx *gin.Context) { + _, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req struct { + ID string `form:"id" json:"id" binding:"required"` + } + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + modId, err := primitive.ObjectIDFromHex(req.ID) + if err != nil || modId.IsZero() { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + mod, err := aichangefacevidmod.GetModById(modId) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "data": mod, + }) +} + +// AIChangeFaceImgList doc +// @Summary AI图片换脸列表 +// @Description AI图片换脸列表 +// @Tags AI图片换脸-WEB +// @Accept json +// @Produce json +// @Param status query int false "记录状态" +// @Param uid query int false "用户id" +// @Param id query int false "流水id" +// @Param mId query string false "AI模版id" +// @Param pageNumber query int true "第几页" +// @Param pageSize query int true "每页数量" +// @Success 200 object interface{} "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/change_face_img/list [get] +func AIChangeFaceImgList(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req aichangefaceimgmod.WebListRequest + if err := ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + data, code := ai_changeface_img_ser.List(&req) + if code != stderr.Success { + log.Error(fmt.Sprintf("AIChangeFaceImg web List error%v,manager%v", code.Error(), manager)) + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, code, data) +} + +// AIChangeFaceImgUpdate doc +// @Summary 审核AI图片换脸记录 +// @Description 审核AI图片换脸记录 +// @Tags AI图片换脸-WEB +// @Accept json +// @Produce json +// @Param id formData string true "订单ID" +// @Param newPic formData []string false "AI图片换脸后新图" +// @Param status formData string true "状态 1、进行中 2、生成成功 3、生成失败" +// @Param remark formData string false "拒绝理由" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/change_face_img/update [post] +func AIChangeFaceImgUpdate(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req aichangefaceimgmod.EditCond + if err = ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("AIChangeFaceImg Update param err%v\n", err)) + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + //code := ai_changeface_img_ser.Update(manager, &req) + code := ai_changeface_img_ser.UpdateNew(manager, &req) + if code != stderr.Success { + log.Error(fmt.Sprintf("AIChangeFaceImg Update err%v", code)) + common.ServeJSON(ctx, code, code.Error()) + return + } + updateLog, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.AiChangeFaceImgList, constant.Modify, string(updateLog), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// 回调 +func Callback(ctx *gin.Context) { + var req aichangefacevidmod.CallbackReq + if err := ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("ai undress update param err%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code := ai_changeface_ser.CallBackOrder(&req) + if code != stderr.Success { + log.Error(fmt.Sprintf("ai undress Update err%v", code)) + common.ServeJSON(ctx, code, code.Error()) + return + } + ctx.String(200, "success", "success") +} + +func CallbackByImg(ctx *gin.Context) { + var req aichangefacevidmod.CallbackReq + if err := ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("ai undress update param err%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code := ai_changeface_img_ser.CallBackOrder(&req) + if code != stderr.Success { + log.Error(fmt.Sprintf("CallbackByImg Update err%v", code)) + common.ServeJSON(ctx, code, code.Error()) + return + } + ctx.String(200, "success", "success") +} + +// BatchModUpdate doc +// @Summary AI换脸视频模版批量修改 +// @Description AI换脸视频模版批量修改 +// @Tags AI模版模块-WEB +// @Accept json +// @Produce json +// @Param q body ai_changeface_ser.BatchUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/changeface/mod/batch/update [post] +func BatchModUpdate(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req ai_changeface_ser.BatchUpdateReq + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + if err = req.BatchUpdateMod(); err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + updateLog, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.AiChangefaceVidMod, constant.Modify, string(updateLog), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// AllSceneList doc +// @Summary AI换脸视频模版列表 +// @Description AI换脸视频模版列表 +// @Tags AI模版模块-WEB +// @Accept json +// @Produce json +// @Success 200 object interface{} "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Security BasicAuth +// @Router /api/web/admin/ai/change_face/mod/all/scene [get] +func AllSceneList(ctx *gin.Context) { + var in ai_changeface_ser.AllSceneListReq + if err := ctx.ShouldBind(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, err := in.All() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/ai_image_to_video_ctrl/ai_image_to_video.go b/web/api/ai_image_to_video_ctrl/ai_image_to_video.go new file mode 100755 index 0000000..7f96e45 --- /dev/null +++ b/web/api/ai_image_to_video_ctrl/ai_image_to_video.go @@ -0,0 +1,136 @@ +package ai_image_to_video_ctrl + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/ai_image_to_video_ser" + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + "net/http" +) + +// List doc +// @Summary 获取AI图生视频列表列表 +// @Description 获取AI图生视频列表列表 +// @Tags 后台-AI图生视频列表 +// @Accept mpfd,json +// @Produce json +// @Param q query ai_image_to_video_ser.WebListReq false "请求参数" +// @Success 200 object ai_image_to_video_ser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/image_to_video/list [get] +func List(ctx *gin.Context) { + var req = &ai_image_to_video_ser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Update doc +// @Summary 更新AI图生视频列表 +// @Description 更新AI图生视频列表 +// @Tags 后台-AI图生视频列表 +// @Accept mpfd,json +// @Produce json +// @Param q body ai_image_to_video_ser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/image_to_video/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &ai_image_to_video_ser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + log.Error(fmt.Sprintf("image_to_video update err:%v", err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "AI图生视频列表管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除AI图生视频列表 +// @Description 删除AI图生视频列表 +// @Tags 后台-AI图生视频列表 +// @Accept mpfd,json +// @Produce json +// @Param q body ai_image_to_video_ser.WebDeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/image_to_video/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &ai_image_to_video_ser.WebDeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = p.Delete() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "AI图生视频列表管理", "删除", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Callback doc +// @Summary 回调AI图生视频订单 +// @Description 回调AI图生视频订单 +// @Tags 后台-AI图生视频列表 +// @Accept mpfd,json +// @Produce json +// @Param q body ai_image_to_video_ser.CallbackReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/image_to_video/callback [post] +func Callback(ctx *gin.Context) { + var in ai_image_to_video_ser.CallbackReq + if err := ctx.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("ai image_to_video Callback param err:%v", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + err := in.Callback() + if err != nil { + log.Error(fmt.Sprintf("ai image_to_video Callback err:%v", err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + ctx.String(http.StatusOK, stderr.Success.Msg()) +} diff --git a/web/api/ai_mate_ctr/ai_mate.go b/web/api/ai_mate_ctr/ai_mate.go new file mode 100644 index 0000000..d608e86 --- /dev/null +++ b/web/api/ai_mate_ctr/ai_mate.go @@ -0,0 +1,42 @@ +package ai_mate_ctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/web/service/ai_mate_ser" + + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary AI办理交易列表 +// @Description AI办理交易列表 +// @Tags AiMate +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "id" +// @Param position formData integer true "广告位置" +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Param sort formData string true "排序的字段" +// @Param desc formData integer true "1 正序 -1倒序" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/aimate/trans_list [get] +func List(ctx *gin.Context) { + var args struct { + Uid uint64 `form:"uid" json:"uid" binding:"required"` //用户id + commod.Page + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "ai_mate_ctrl List arg error "+err.Error()) + return + } + code, data := ai_mate_ser.GetUserTransactionsList(args.Uid, args.PageNumber, args.PageSize) + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/ai_mate_v2_ctrl/ai_mate.go b/web/api/ai_mate_v2_ctrl/ai_mate.go new file mode 100644 index 0000000..32685b1 --- /dev/null +++ b/web/api/ai_mate_v2_ctrl/ai_mate.go @@ -0,0 +1,66 @@ +package ai_mate_v2_ctrl + +import ( + "time" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/web/service/ai_mate_v2_ser" + + "github.com/gin-gonic/gin" +) + +// OrderLogs doc +// @Summary AI女友V2消费流水 +// @Description 查询老司机AI女友V2第三方消费流水 +// @Tags AiMateV2 +// @Accept mpfd,json +// @Produce json,html +// @Param uid query integer true "用户ID" +// @Param pageNumber query integer true "查询页码" +// @Param pageSize query integer true "每页条数,最大100" +// @Param startTime query string false "开始时间,不传默认近一个月" +// @Param endTime query string false "结束时间,不传默认当前时间" +// @Success 200 {string} json "{\"msg\": \"操作成功\"}" +// @Failure 400 {string} json "{\"msg\": \"操作失败\"}" +// @Router /api/web/admin/aimatev2/order_logs [get] +func OrderLogs(ctx *gin.Context) { + var args struct { + UID uint64 `form:"uid" json:"uid" binding:"required"` + StartTime time.Time `form:"startTime" json:"startTime"` + EndTime time.Time `form:"endTime" json:"endTime"` + commod.Page + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "ai_mate_v2_ctrl OrderLogs arg error "+err.Error()) + return + } + + endTime := args.EndTime + if endTime.IsZero() { + endTime = time.Now() + } + startTime := args.StartTime + if startTime.IsZero() { + startTime = endTime.AddDate(0, -1, 0) + } + if !startTime.Before(endTime) { + common.ServeJSON(ctx, stderr.ErrParamError, "开始时间必须早于结束时间") + return + } + + code, data := ai_mate_v2_ser.GetUserOrderLogs( + ctx.Request.Context(), + args.UID, + args.PageNumber, + args.PageSize, + startTime, + endTime, + ) + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/ai_template_module_ctrl/aitemplatemodule.go b/web/api/ai_template_module_ctrl/aitemplatemodule.go new file mode 100755 index 0000000..4d4feb9 --- /dev/null +++ b/web/api/ai_template_module_ctrl/aitemplatemodule.go @@ -0,0 +1,167 @@ +package ai_template_module_ctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/ai_template_module_ser" + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取AI模版模块列表列表 +// @Description 获取AI模版模块列表列表 +// @Tags 后台-AI模版模块列表 +// @Accept mpfd,json +// @Produce json +// @Param q query ai_template_module_ser.WebListReq false "请求参数" +// @Success 200 object ai_template_module_ser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai_template_module/list [get] +func List(ctx *gin.Context) { + var req = &ai_template_module_ser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// AllList doc +// @Summary 获取所有AI模版模块接口 +// @Description 获取所有AI模版模块 +// @Tags 后台-AI模版模块列表 +// @Accept mpfd,json +// @Produce json +// @Param q query ai_template_module_ser.WebAllListReq false "请求参数" +// @Success 200 object ai_template_module_ser.WebAllListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai_template_module/all [get] +func AllList(ctx *gin.Context) { + var req = &ai_template_module_ser.WebAllListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Create doc +// @Summary 新增AI模版模块列表 +// @Description 新增AI模版模块列表 +// @Tags 后台-AI模版模块列表 +// @Accept mpfd,json +// @Produce json +// @Param q body ai_template_module_ser.WebCreateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai_template_module/create [post] +func Create(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &ai_template_module_ser.WebCreateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 创建 + err = p.Create() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "AI模版模块列表管理", "创建", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 更新AI模版模块列表 +// @Description 更新AI模版模块列表 +// @Tags 后台-AI模版模块列表 +// @Accept mpfd,json +// @Produce json +// @Param q body ai_template_module_ser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai_template_module/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &ai_template_module_ser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "AI模版模块列表管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除AI模版模块列表 +// @Description 删除AI模版模块列表 +// @Tags 后台-AI模版模块列表 +// @Accept mpfd,json +// @Produce json +// @Param q body ai_template_module_ser.WebDeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai_template_module/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &ai_template_module_ser.WebDeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = p.Delete() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "AI模版模块列表管理", "删除", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/ai_text_to_image_ctrl/aitexttoimage.go b/web/api/ai_text_to_image_ctrl/aitexttoimage.go new file mode 100755 index 0000000..5e5513a --- /dev/null +++ b/web/api/ai_text_to_image_ctrl/aitexttoimage.go @@ -0,0 +1,139 @@ +package ai_text_to_image_ctrl + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/ai_text_to_image_ser" + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + "net/http" +) + +// List doc +// @Summary 获取AI绘图列表列表 +// @Description 获取AI绘图列表列表 +// @Tags 后台-AI绘图列表 +// @Accept mpfd,json +// @Produce json +// @Param q query ai_text_to_image_ser.WebListReq false "请求参数" +// @Success 200 object ai_text_to_image_ser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/text_to_image/list [get] +func List(ctx *gin.Context) { + var req = &ai_text_to_image_ser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Update doc +// @Summary 更新AI绘图列表 +// @Description 更新AI绘图列表 +// @Tags 后台-AI绘图列表 +// @Accept mpfd,json +// @Produce json +// @Param q body ai_text_to_image_ser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/text_to_image/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &ai_text_to_image_ser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "AI绘图列表管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除AI绘图列表 +// @Description 删除AI绘图列表 +// @Tags 后台-AI绘图列表 +// @Accept mpfd,json +// @Produce json +// @Param q body ai_text_to_image_ser.WebDeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/text_to_image/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &ai_text_to_image_ser.WebDeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = p.Delete() + if err != nil { + if code, ok := err.(stderr.Code); ok { + common.ServeJSON(ctx, code, code.Error()) + return + } + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "AI绘图列表管理", "删除", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Callback doc +// @Summary 回调AI图生视频订单 +// @Description 回调AI图生视频订单 +// @Tags 后台-AI图生视频列表 +// @Accept mpfd,json +// @Produce json +// @Param q body ai_text_to_image_ser.CallbackReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/text_to_image/callback [post] +func Callback(ctx *gin.Context) { + var in ai_text_to_image_ser.CallbackReq + if err := ctx.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("ai text_to_image Callback param err:%v", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + err := in.Callback() + if err != nil { + log.Error(fmt.Sprintf("ai text_to_image Callback err:%v", err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + ctx.String(http.StatusOK, stderr.Success.Msg()) +} diff --git a/web/api/ai_text_to_novel_ctrl/aitexttonovel.go b/web/api/ai_text_to_novel_ctrl/aitexttonovel.go new file mode 100755 index 0000000..f2c48a6 --- /dev/null +++ b/web/api/ai_text_to_novel_ctrl/aitexttonovel.go @@ -0,0 +1,140 @@ +package ai_text_to_novel_ctrl + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/ai_text_to_novel_ser" + "encoding/json" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取AI小说列表列表 +// @Description 获取AI小说列表列表 +// @Tags 后台-AI小说列表 +// @Accept mpfd,json +// @Produce json +// @Param q query ai_text_to_novel_ser.WebListReq false "请求参数" +// @Success 200 object ai_text_to_novel_ser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/text_to_novel/list [get] +func List(ctx *gin.Context) { + var req = &ai_text_to_novel_ser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Update doc +// @Summary 更新AI小说列表 +// @Description 更新AI小说列表 +// @Tags 后台-AI小说列表 +// @Accept mpfd,json +// @Produce json +// @Param q body ai_text_to_novel_ser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/text_to_novel/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &ai_text_to_novel_ser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "AI小说列表管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除AI小说列表 +// @Description 删除AI小说列表 +// @Tags 后台-AI小说列表 +// @Accept mpfd,json +// @Produce json +// @Param q body ai_text_to_novel_ser.WebDeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/text_to_novel/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &ai_text_to_novel_ser.WebDeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = p.Delete() + if err != nil { + if code, ok := err.(stderr.Code); ok { + common.ServeJSON(ctx, code, code.Error()) + return + } + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "AI小说列表管理", "删除", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Callback doc +// @Summary 回调AI小说订单 +// @Description 回调AI小说订单 +// @Tags 后台-AI小说列表 +// @Accept mpfd,json +// @Produce json +// @Param q body ai_text_to_novel_ser.CallbackReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/text_to_novel/callback [post] +func Callback(ctx *gin.Context) { + var in ai_text_to_novel_ser.CallbackReq + if err := ctx.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("ai ai_text_to_novel callback param err:%v", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + err := in.Callback() + if err != nil { + log.Error(fmt.Sprintf("ai ai_text_to_novel callback err:%v", err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + ctx.String(http.StatusOK, stderr.Success.Msg()) +} diff --git a/web/api/ai_undress_ctrl/ai_undress.go b/web/api/ai_undress_ctrl/ai_undress.go new file mode 100644 index 0000000..41c72e7 --- /dev/null +++ b/web/api/ai_undress_ctrl/ai_undress.go @@ -0,0 +1,213 @@ +package ai_undress_ctrl + +import ( + "encoding/json" + "fmt" + "sync" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/aiUnDressmod" + "91porn-server/web/service/ai_undress_service" + + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary AI脱衣列表 +// @Description AI脱衣列表 +// @Tags AI脱衣-WEB +// @Accept json +// @Produce json +// @Param status query int false "记录状态" +// @Param pageNumber query int true "第几页" +// @Param pageSize query int true "每页数量" +// @Success 200 object interface{} "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ai/undress/list [get] +func List(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req aiUnDressmod.WebListRequest + if err := ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + data, code := ai_undress_service.List(&req) + if code != stderr.Success { + log.Error(fmt.Sprintf("ai_undress_service web List error%v,manager%v", code.Error(), manager)) + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, code, data) +} + +// Update doc +// @Summary 生成AI脱衣记录 +// @Description 生成AI脱衣记录 +// @Tags AI脱衣-WEB +// @Accept json +// @Produce json +// @Param originPic formData string false "脱衣原图" +// @Param coin formData string false "金币价格" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ai/undress/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req aiUnDressmod.EditCond + if err = ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("ai undress Generate param err%v\n", err)) + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + code := ai_undress_service.Update(manager, &req) + if code != stderr.Success { + log.Error(fmt.Sprintf("ai undress Update err%v", code)) + common.ServeJSON(ctx, code, code.Error()) + return + } + updateLog, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.AiUndressList, constant.Modify, string(updateLog), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// Auto doc +// @Summary 生成AI脱衣记录,自动处理 +// @Description 生成AI脱衣记录,自动处理 +// @Tags AI脱衣-WEB +// @Accept json +// @Produce json +// @Param originPic formData string false "脱衣原图" +// @Param coin formData string false "金币价格" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/undress/auto [post] +func Auto(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req aiUnDressmod.AutoCond + if err = ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("ai undress auto Generate param err%v\n", err)) + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + code := ai_undress_service.Auto(manager, &req) + if code != stderr.Success { + log.Error(fmt.Sprintf("ai undress auto err%v", code)) + common.ServeJSON(ctx, code, code.Error()) + return + } + updateLog, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.AiUndressList, constant.Modify, string(updateLog), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// Update doc +// @Summary 生成AI脱衣记录,批量自动处理 +// @Description 生成AI脱衣记录,批量自动处理 +// @Tags AI脱衣-WEB +// @Accept json +// @Produce json +// @Param ids formData []string false "id列表" +// @Param pass formData bool false "是否通过" +// @Param remark formData string false "不通过的原因" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ai/undress/auto/batch [post] +func AutoBatch(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req aiUnDressmod.AutoBatchCond + if err = ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("ai undress auto Generate param err%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + w := &sync.WaitGroup{} + errCodes := []stderr.Code{} + lock := &sync.Mutex{} + for _, v := range req.IDs { + w.Add(1) + id := v + common.Go(func() { + defer w.Done() + if !req.Pass { + status := aiUnDressmod.FAILURE + // 拒绝 + code := ai_undress_service.Update(manager, &aiUnDressmod.EditCond{ + ID: id, + Status: &status, + Remark: &req.Remark, + }) + lock.Lock() + defer lock.Unlock() + errCodes = append(errCodes, code) + return + } + // 审核通过 + code := ai_undress_service.Auto(manager, &aiUnDressmod.AutoCond{ + ID: id, + }) + lock.Lock() + defer lock.Unlock() + errCodes = append(errCodes, code) + }) + } + w.Wait() + + for _, code := range errCodes { + if code != stderr.Success { + log.Error(fmt.Sprintf("ai undress auto err%v", code)) + common.ServeJSON(ctx, code, code.Error()) + return + } + } + updateLog, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.AiUndressList, constant.Modify, string(updateLog), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, stderr.Success.Msg()) +} + +// Callback doc +// @Summary AI脱衣回调,自动服务 +// @Description AI脱衣回调,自动服务 +// @Tags AI脱衣-WEB +// @Accept json +// @Produce json +// @Param originPic formData string false "脱衣原图" +// @Param coin formData string false "金币价格" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ai/undress/callback [post] +func Callback(ctx *gin.Context) { + var req aiUnDressmod.CallBackReq + if err := ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("ai undress update param err%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code := ai_undress_service.CallBackOrder(&req) + if code != stderr.Success { + log.Error(fmt.Sprintf("ai undress Update err%v", code)) + common.ServeJSON(ctx, code, code.Error()) + return + } + ctx.String(200, "success") +} diff --git a/web/api/aiplazactrl/aiplaza.go b/web/api/aiplazactrl/aiplaza.go new file mode 100755 index 0000000..1c3bef4 --- /dev/null +++ b/web/api/aiplazactrl/aiplaza.go @@ -0,0 +1,178 @@ +package aiplazactrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/aiplazaser" + + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取ai广场帖子列表 +// @Description 获取ai广场帖子列表 +// @Tags 后台-ai广场帖子 +// @Accept mpfd,json +// @Produce json +// @Param q query aiplazaser.WebListReq false "请求参数" +// @Success 200 object aiplazaser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/aiplaza/list [get] +func List(ctx *gin.Context) { + var req = &aiplazaser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Create doc +// @Summary 新增ai广场帖子 +// @Description 新增ai广场帖子 +// @Tags 后台-ai广场帖子 +// @Accept mpfd,json +// @Produce json +// @Param q body aiplazaser.WebCreateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/aiplaza/create [post] +func Create(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &aiplazaser.WebCreateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 创建 + err = p.Create() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "ai广场帖子管理", "创建", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 更新ai广场帖子 +// @Description 更新ai广场帖子 +// @Tags 后台-ai广场帖子 +// @Accept mpfd,json +// @Produce json +// @Param q body aiplazaser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/aiplaza/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &aiplazaser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "ai广场帖子管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// BatchUpdate doc +// @Summary 更新ai广场帖子 +// @Description 更新ai广场帖子 +// @Tags 后台-ai广场帖子 +// @Accept mpfd,json +// @Produce json +// @Param q body aiplazaser.WebBatchUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/aiplaza/batch/update [post] +func BatchUpdate(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &aiplazaser.WebBatchUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "ai广场帖子管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除ai广场帖子 +// @Description 删除ai广场帖子 +// @Tags 后台-ai广场帖子 +// @Accept mpfd,json +// @Produce json +// @Param q body aiplazaser.WebDeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/aiplaza/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &aiplazaser.WebDeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil || len(p.ID) == 0 { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = p.Delete() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "ai广场帖子管理", "删除", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/annouctrl/annou.go b/web/api/annouctrl/annou.go new file mode 100644 index 0000000..cc7ea0a --- /dev/null +++ b/web/api/annouctrl/annou.go @@ -0,0 +1,118 @@ +package annouctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/annoumod" + "91porn-server/web/service/annouser" + + "github.com/gin-gonic/gin" +) + +// GetAnnouList doc +// @Summary 获取公告列表 +// @Description 获取公告列表 +// @Tags annou +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/annou/list [get] +func GetAnnouList(ctx *gin.Context) { + req := annoumod.ListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := annouser.GetAnnouList(req) + common.ServeJSON(ctx, code, data) +} + +// UpdateAnnou doc +// @Summary 更新公告 +// @Description 更新公告 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/annou/update [post] +func UpdateAnnou(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := annoumod.EditReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := annouser.UpdateAnnou(req) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.AdsManageAnnousList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, data) +} + +// DeleteAnnou doc +// @Summary 删除公告 +// @Description 删除公告 +// @Tags follow +// @Accept mpfd,json +// @Produce json,html +// @Param vids formData []string true "公告id数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/annou/del [delete] +func DeleteAnnou(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := annoumod.DeleteReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := annouser.DeleteAnnou(req.IDs) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.AdsManageAnnousList, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, data) +} + +// AddAnnou doc +// @Summary 增加一条公告 +// @Description 增加一条公告 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/annou/add [post] +func AddAnnou(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := annoumod.AddReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := annouser.AddAnnou(req) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.AdsManageAnnousList, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, data) +} diff --git a/web/api/annouctrl/announce.go b/web/api/annouctrl/announce.go new file mode 100644 index 0000000..88ea61e --- /dev/null +++ b/web/api/annouctrl/announce.go @@ -0,0 +1,115 @@ +package annouctrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/annoumod" + "91porn-server/web/service/annouser" + "encoding/json" + + "github.com/gin-gonic/gin" +) + +// GetAnnounceList doc +// @Summary 获取会员中心跑马灯列表 +// @Description 获取会员中心跑马灯列表 +// @Tags annou +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/announce/list [get] +func GetAnnounceList(ctx *gin.Context) { + req := annoumod.AnnounceInfoReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := annouser.GetAnnounceList() + common.ServeJSON(ctx, code, data) +} + +// UpdateAnnounce doc +// @Summary 更新会员中心跑马灯 +// @Description 更新会员中心跑马灯 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param id formData integer true "id" +// @Param content formData string true "内容" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/announce/update [post] +func UpdateAnnounce(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := annoumod.EditAnnounceReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := annouser.UpdateAnnounce(req) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.AdsManageAnnounceList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, data) +} + +// DeleteAnnounce doc +// @Summary 删除会员中心跑马灯 +// @Description 删除会员中心跑马灯 +// @Tags follow +// @Accept mpfd,json +// @Produce json,html +// @Param vids formData []string true "跑马灯id数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/announce/del [delete] +func DeleteAnnounce(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := annoumod.DeleteAnnounceReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := annouser.DeleteAnnounce(req.IDs) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.AdsManageAnnounceList, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, data) +} + +// AddAnnounce doc +// @Summary 增加一条会员中心跑马灯 +// @Description 增加一条会员中心跑马灯 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/announce/add [post] +func AddAnnounce(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := annoumod.AddAnnounceReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code := annouser.AddAnnounce(req) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.AdsManageAnnounceList, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, nil) +} diff --git a/web/api/audiobookctrl/audiobook.go b/web/api/audiobookctrl/audiobook.go new file mode 100644 index 0000000..0dec170 --- /dev/null +++ b/web/api/audiobookctrl/audiobook.go @@ -0,0 +1,196 @@ +package audiobookctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/audiobookmod" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Get doc +// @Summary +// @Description 查询电子书 +// @Tags audiobook +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /audiobook/list [post] +func List(ctx *gin.Context) { + var p struct { + audiobookmod.QuerySelector + commod.Page + } + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, total, err := audiobookmod.StdFind(p.QuerySelector, p.Page) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": data, + "total": total, + }) +} + +// Insert doc +// @Summary 新增 +// @Description 新增 +// @Tags audiobook +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /audiobook/add [post] +func Insert(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + v := audiobookmod.AudioBook{} + if err = ctx.ShouldBind(&v); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = audiobookmod.InsertAudioBook(&v); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(v) + _ = operatorlgmod.RecordOperation(manager, constant.Audiobook, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 修改 +// @Description 修改 +// @Tags audiobook +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /audiobook/edit [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + p := audiobookmod.EditSelector{} + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = audiobookmod.UpdateAudioBook(p); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, "") + return + } + redisKey := redisconst.DataCachKey(audiobookmod.RedisSetKey, "FindaudiobookByType") + _, _ = webg.Redis.Del(redisKey) + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Audiobook, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 修改 +// @Description 修改 +// @Tags audiobook +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /audiobook/edit [post] +func UpdateBatch(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + List []audiobookmod.EditSelector `json:"list"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = audiobookmod.UpdateAudioBookBatch(p.List); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, "") + return + } + redisKey := redisconst.DataCachKey(audiobookmod.RedisSetKey, "FindaudiobookByType") + _, _ = webg.Redis.Del(redisKey) + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Audiobook, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除 +// @Description 删除 +// @Tags audiobook +// @Accept mpfd,json +// @Produce json,html +// @Param id formData int true "audiobook等级"" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /audiobook/del [delete] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + ID string `json:"id" form:"id"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "") + return + } + if err = audiobookmod.RemoveAudioBook(p.ID); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, "") + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Audiobook, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Get doc +// @Summary +// @Description 查询电子书 +// @Tags audiobook +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /audiobook/list [post] +func Get(ctx *gin.Context) { + var p struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + } + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := audiobookmod.GetByID(p.ID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/authorctrl/author.go b/web/api/authorctrl/author.go new file mode 100644 index 0000000..39eda00 --- /dev/null +++ b/web/api/authorctrl/author.go @@ -0,0 +1,186 @@ +package authorctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/adminmod" + "91porn-server/models/v/authoritymod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// List doc +// @Summary 获取权限列表 +// @Description 获取权限列表 +// @Tags web-权限 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query int true "当前页" +// @Param pageSize query int true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "date": { "total":10, "list":[] }}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/author/list [get] +func List(c *gin.Context) { + var arg struct { + commod.Page + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "auth List arg error "+err.Error()) + return + } + skip := int64((arg.PageNumber - 1) * arg.PageSize) + limit := int64(arg.PageSize) + page, err := authoritymod.AuthorPages(skip, limit) + if err != nil { + common.ServeJSON(c, stderr.Failure, "auth List error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} + +// Update doc +// @Summary 修改权限 +// @Description 修改权限 +// @Tags web-权限 +// @Accept mpfd,json +// @Produce json,html +// @Param authID formData string true "权限ID" +// @Param Role formData string true "角色" +// @Param AuthJson formData string true "权限数据,json格式" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/author/update [post] +func Update(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + AuthID primitive.ObjectID `form:"authID" json:"authID,omitempty" binding:"required"` + Role *string `form:"role" json:"role" binding:""` //角色 + AuthJson *string `form:"authJson" json:"authJson" binding:""` //"路由权限数据,json格式" + ActJson *string `form:"actJson" json:"actJson" binding:""` //"功能权限数据,json格式" + } + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "author Add arg error "+err.Error()) + return + } + doc := authoritymod.AuthorityDoc{ + Role: arg.Role, + AuthJson: arg.AuthJson, + ActJson: arg.ActJson, + } + if err = authoritymod.UpdateTo(arg.AuthID, doc); err != nil { + common.ServeJSON(c, stderr.Failure, "auth Update error: "+err.Error()) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.AuthorManager, constant.Modify, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// Add doc +// @Summary 添加权限 +// @Description 批量删除权限 +// @Tags web-权限 +// @Accept mpfd,json +// @Produce json,html +// @Param Role formData string true "角色" +// @Param AuthJson formData string false "权限数据,json格式" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/author/add [post] +func Add(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + Role *string `form:"role" json:"role" binding:"required"` //角色 + AuthJson *string `form:"authJson" json:"authJson" binding:""` //"权限数据,json格式" + } + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "author Add arg error "+err.Error()) + return + } + doc := authoritymod.AuthorityDoc{ + Role: arg.Role, + AuthJson: arg.AuthJson, + } + if err = authoritymod.Insert(doc); err != nil { + common.ServeJSON(c, stderr.Failure, "auth Add error: "+err.Error()) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.AuthorManager, constant.Add, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// Delete doc +// @Summary 批量删除权限 +// @Description 批量删除权限 +// @Tags web-权限 +// @Accept mpfd,json +// @Produce json,html +// @Param authIDArray formData array true "权限ID数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/author/del [delete] +func Delete(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + AuthIDArray []string `form:"authIDArray" json:"authIDArray" binding:"required"` + } + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "author Del arg error "+err.Error()) + return + } + authIDArray, err := common.IDArray(arg.AuthIDArray) + if err != nil || len(authIDArray) == 0 { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + //检查权限是否存在 + for _, authID := range authIDArray { + doc := authoritymod.AuthorityDoc{ + ID: authID, + } + auth, err := authoritymod.FindOne(doc) + if err != nil { + common.ServeJSON(c, stderr.Failure, "") + return + } + if auth.ID.IsZero() { + common.ServeJSON(c, stderr.AuthIsNotExist, "") + return + } + using, err := adminmod.IsUsing(auth.Role) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, "") + return + } + if using { + common.ServeJSON(c, stderr.AuthInUsing, "") + return + } + } + if err = authoritymod.DeleteMany(authIDArray); err != nil { + common.ServeJSON(c, stderr.Failure, "auth DeleteMany error: "+err.Error()) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.AuthorManager, constant.Delete, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} diff --git a/web/api/backpack_ctrl/backpack.go b/web/api/backpack_ctrl/backpack.go new file mode 100644 index 0000000..2a0036f --- /dev/null +++ b/web/api/backpack_ctrl/backpack.go @@ -0,0 +1,40 @@ +package backpack_ctrl + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/backpackmod" + "91porn-server/web/service/backpack_service" + "fmt" + "github.com/gin-gonic/gin" +) + +// QueryAll doc +// @Summary 查询用户背包 +// @Description 查询用户背包 +// @Tags 用户 +// @Accept mpfd,json +// @Produce json,html +// @Param Uid query int false "搜索用户ID" +// @Param page query int64 true "第几页" +// @Param limit query int64 true "每页数量" +// @Success 200 object backpackmod.QueryAllRes "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/backpack [get] +func QueryAll(c *gin.Context) { + var in backpackmod.QueryAllWebCond + if err := c.ShouldBindQuery(&in); err != nil { + log.Error(fmt.Sprintf("Query all backpack param err%v\n", err)) + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + + data, err := backpack_service.QueryAll(&in) + if err != nil { + log.Error(fmt.Sprintf("Query all backpack err%v\n%d", data, err)) + common.ServeJSON(c, stderr.Failure, nil) + return + } + common.ServeJSON(c, stderr.Success, data) +} diff --git a/web/api/bannerjumpctrl/bannerjump.go b/web/api/bannerjumpctrl/bannerjump.go new file mode 100755 index 0000000..fdc36d0 --- /dev/null +++ b/web/api/bannerjumpctrl/bannerjump.go @@ -0,0 +1,142 @@ +package bannerjumpctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/bannerjumpser" + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取限时Banner活动列表 +// @Description 获取限时Banner活动列表 +// @Tags 后台-限时Banner活动 +// @Accept mpfd,json +// @Produce json +// @Param q query bannerjumpser.WebListReq false "请求参数" +// @Success 200 object bannerjumpser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/bannerjump/list [get] +func List(ctx *gin.Context) { + var req = &bannerjumpser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Create doc +// @Summary 新增限时Banner活动 +// @Description 新增限时Banner活动 +// @Tags 后台-限时Banner活动 +// @Accept mpfd,json +// @Produce json +// @Param q body bannerjumpser.WebCreateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/bannerjump/create [post] +func Create(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &bannerjumpser.WebCreateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 创建 + err = p.Create() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "限时Banner活动管理", "创建", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 更新限时Banner活动 +// @Description 更新限时Banner活动 +// @Tags 后台-限时Banner活动 +// @Accept mpfd,json +// @Produce json +// @Param q body bannerjumpser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/bannerjump/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &bannerjumpser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "限时Banner活动管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除限时Banner活动 +// @Description 删除限时Banner活动 +// @Tags 后台-限时Banner活动 +// @Accept mpfd,json +// @Produce json +// @Param q body bannerjumpser.WebDeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/bannerjump/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &bannerjumpser.WebDeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = p.Delete() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "限时Banner活动管理", "删除", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/chat_message_ctrl/chat_message.go b/web/api/chat_message_ctrl/chat_message.go new file mode 100644 index 0000000..174ed0b --- /dev/null +++ b/web/api/chat_message_ctrl/chat_message.go @@ -0,0 +1,116 @@ +package chat_message_ctrl + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/messagemod" + "91porn-server/web/service/chat_message_ser" + "fmt" + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 用户私信列表 +// @Description 用户私信列表 +// @Tags 用户私信 +// @Accept json +// @Produce json +// @Param sendUid query int false "发送者uid" +// @Param takeUid query int false "接收者uid" +// @Param isRead query boolean false "消息已读or未读" +// @Param pageNumber query int true "第几页" +// @Param pageSize query int true "每页数量" +// @Success 200 object interface{} "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/chatMessage/list [get] +func List(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req messagemod.WebListRequest + if err := ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + data, code := chat_message_ser.List(&req) + if code != stderr.Success { + log.Error(fmt.Sprintf("chat_message_ser web List error%v,manager%v", code.Error(), manager)) + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, code, data) +} + +// Del doc +// @Summary 删除用户私信 +// @Description 删除用户私信 +// @Tags 用户私信 +// @Accept json +// @Produce json +// @Param sendUid query int false "发送者uid" +// @Param takeUid query int false "接收者uid" +// @Param isRead query boolean false "消息已读or未读" +// @Param pageNumber query int true "第几页" +// @Param pageSize query int true "每页数量" +// @Success 200 object interface{} "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/chatMessage/del [delete] +func Del(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req messagemod.WebDelReq + if err := ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrNoToken, err) + return + } + code := chat_message_ser.Del(&req) + if code != stderr.Success { + log.Error(fmt.Sprintf("chat_message_ser web Del error%v,manager%v", code.Error(), manager)) + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, code, stderr.Success.Msg()) +} + +// Send doc +// @Summary 发送用户私信 +// @Description 发送用户私信 +// @Tags 用户私信 +// @Accept json +// @Produce json +// @Param sender formData int true "发送者uid" +// @Param receiver formData int true "接收者uid" +// @Param content formData string true "消息内容" +// @Success 200 object interface{} "成功后返回" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/user/chatMessage/send [post] +func Send(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req messagemod.WebSendReq + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code := chat_message_ser.AddPrivateLetter(&messagemod.SendMsg{ + Sender: req.Sender, + Receiver: req.Receiver, + Content: req.Content, + MsgType: messagemod.PrivateLetterMsg, + }) + if code != stderr.Success { + log.Error(fmt.Sprintf("chat_message_ser web Del error%v,manager%v", code.Error(), manager)) + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, code, stderr.Success.Msg()) +} diff --git a/web/api/chatrobotctrl/chatrobotctrl.go b/web/api/chatrobotctrl/chatrobotctrl.go new file mode 100644 index 0000000..2d8b7e1 --- /dev/null +++ b/web/api/chatrobotctrl/chatrobotctrl.go @@ -0,0 +1,96 @@ +package chatrobotctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/chatrobotmod" + "91porn-server/web/service/chatrobotser" + + "github.com/gin-gonic/gin" +) + +// RobotConfList doc +// @Summary 机器人配置列表 +// @Description 机器人配置 +// @Tags Robot config +// @Accept mpfd,json +// @Produce json,html +// @Param type query string false "机器人类型 COMMENT: 评论机器人 FILM: 影院机器人" +// @Param pageNumber query integer true "页数" +// @Param pageSize query integer true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功" "data": []}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/robot/conf/list [get] +func RobotConfList(ctx *gin.Context) { + var param chatrobotmod.ReqConfList + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, data, err := chatrobotser.GetRobotConfList(param) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, data) +} + +// AddRobotConf doc +// @Summary 添加机器人配置 +// @Description 机器人配置 +// @Tags Robot config +// @Accept mpfd,json +// @Produce json,html +// @Param type formData string true "机器人类型 COMMENT: 评论机器人 FILM: 影院机器人" +// @Param from formData string true "机器人类型 HH:SS:MM格式" +// @Param to formData string true "机器人类型 HH:SS:MM格式" +// @Param totalLimit formData integer false "机器人每天评论总数" +// @Param pureLimit formData integer false "单个机器人每天评论数" +// @Param frequency formData integer false "机器人发表评论频率 单位:秒/条" +// @Param isActive formData bool false "是否开启" +// @Success 200 {string} json "{"msg": "操作成功" "data": []}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/robot/conf/add [post] +func AddRobotConf(ctx *gin.Context) { + var param chatrobotmod.ReqAdd + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, data, err := chatrobotser.AddRobotConf(param) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, data) +} + +// UpdateRobotConf doc +// @Summary 更新机器人配置 +// @Description 机器人配置 +// @Tags Robot config +// @Accept mpfd,json +// @Produce json,html +// @Param type formData string true "机器人类型 COMMENT: 评论机器人 FILM: 影院机器人" +// @Param from formData string false "机器人类型 HH:SS:MM格式" +// @Param to formData string false "机器人类型 HH:SS:MM格式" +// @Param totalLimit formData integer false "机器人每天评论总数" +// @Param pureLimit formData integer false "单个机器人每天评论数" +// @Param frequency formData integer false "机器人发表评论频率 单位:秒/条" +// @Param isActive formData bool false "是否开启" +// @Success 200 {string} json "{"msg": "操作成功" "data": []}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/robot/conf/update [post] +func UpdateRobotConf(ctx *gin.Context) { + var param chatrobotmod.ReqUpdate + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, data, err := chatrobotser.UpdateRobotConf(param) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, data) +} diff --git a/web/api/checkinctrl/checkin.go b/web/api/checkinctrl/checkin.go new file mode 100644 index 0000000..d87a629 --- /dev/null +++ b/web/api/checkinctrl/checkin.go @@ -0,0 +1,281 @@ +package checkinctrl + +import ( + "91porn-server/app/service/checkinser" + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/checkinconfigmod" + "91porn-server/models/v/checkinprizemod" + "strings" + "time" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ========== 签到配置 ========== + +// GetCheckinConfig 获取签到配置 +func GetCheckinConfig(c *gin.Context) { + cfg, err := checkinser.GetCheckinConfig() + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(c, stderr.Success, cfg) +} + +type checkinConfigUpdateReq struct { + Enable *bool `json:"enable" bson:"enable,omitempty"` + Description *string `json:"description" bson:"description,omitempty"` + BackgroundImage *string `json:"backgroundImage" bson:"backgroundImage,omitempty"` + RewardBgVideos *[]rewardBgVideoReq `json:"rewardBgVideos" bson:"rewardBgVideos,omitempty"` + IntegerExchangeList *[]giftItemReq `json:"integerExchangeList" bson:"integerExchangeList,omitempty"` +} + +type rewardBgVideoReq struct { + PrizeType int `json:"prizeType" bson:"prizeType"` + BgMediaUrl string `json:"bgMediaUrl" bson:"bgMediaUrl"` + FsResourceId string `json:"fsResourceId" bson:"fsResourceId"` + FsResourceStatus string `json:"-" bson:"fsResourceStatus"` +} + +type giftItemReq struct { + Name string `json:"name" bson:"name"` + Icon string `json:"icon" bson:"icon"` +} + +// UpdateCheckinConfig 修改签到配置 +func UpdateCheckinConfig(c *gin.Context) { + var req checkinConfigUpdateReq + if err := c.ShouldBindJSON(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err.Error()) + return + } + // BgMediaUrl 不以 .m3u8 结尾时自动追加 + // fsResourceId 变更时重置 fsResourceStatus + if req.RewardBgVideos != nil { + // 查询当前配置,用于比对 fsResourceId 是否变更 + oldCfg, _ := checkinconfigmod.FindOne(bson.M{}) + oldIdMap := make(map[int]string) // prizeType -> fsResourceId + oldStatusMap := make(map[int]string) // prizeType -> fsResourceStatus + if oldCfg != nil { + for _, v := range oldCfg.RewardBgVideos { + oldIdMap[int(v.PrizeType)] = v.FsResourceId + oldStatusMap[int(v.PrizeType)] = v.FsResourceStatus + } + } + for i := range *req.RewardBgVideos { + item := &(*req.RewardBgVideos)[i] + if item.BgMediaUrl != "" && !strings.HasSuffix(item.BgMediaUrl, ".m3u8") { + item.BgMediaUrl = item.BgMediaUrl + ".m3u8" + } + // fsResourceId 不为空且与旧值不同,重置状态让定时任务重新检查 + if item.FsResourceId != "" && item.FsResourceId != oldIdMap[item.PrizeType] { + item.FsResourceStatus = "" + } else { + item.FsResourceStatus = oldStatusMap[item.PrizeType] + } + } + } + update := bson.M{"$set": req} + if err := checkinser.UpdateCheckinConfig(update); err != nil { + common.ServeJSON(c, stderr.ErrDbUpdateError, nil) + return + } + common.ServeJSON(c, stderr.Success, nil) +} + +// ========== 签到奖品管理 ========== + +type checkinPrizeListReq struct { + PageNumber int64 `json:"pageNumber" form:"pageNumber"` + PageSize int64 `json:"pageSize" form:"pageSize"` +} + +// GetCheckinPrizeList 获取签到奖品列表 +func GetCheckinPrizeList(c *gin.Context) { + var req checkinPrizeListReq + if err := c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + if req.PageNumber <= 0 { + req.PageNumber = 1 + } + if req.PageSize <= 0 { + req.PageSize = 20 + } + list, total, err := checkinser.GetCheckinPrizeList(req.PageNumber, req.PageSize) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(c, stderr.Success, gin.H{ + "list": list, + "total": total, + }) +} + +// AddOneCheckinPrize 添加签到奖品 +func AddOneCheckinPrize(c *gin.Context) { + var req checkinprizemod.CheckinPrize + if err := c.ShouldBindJSON(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err.Error()) + return + } + if err := checkinser.AddCheckinPrize(&req); err != nil { + common.ServeJSON(c, stderr.ErrDbInsertError, nil) + return + } + common.ServeJSON(c, stderr.Success, nil) +} + +type checkinPrizeUpdateReq struct { + ID string `json:"id"` + checkinPrizeParams +} + +type checkinPrizeParams struct { + Title *string `json:"title" bson:"title,omitempty"` + Image *string `json:"image" bson:"image,omitempty"` + CheckinDays *int64 `json:"checkinDays" bson:"checkinDays,omitempty"` + CheckinType *checkinprizemod.CheckinType `json:"checkinType" bson:"checkinType,omitempty"` + PrizeId *string `json:"prizeId" bson:"prizeId,omitempty"` + PrizeName *string `json:"prizeName" bson:"prizeName,omitempty"` + Status *bool `json:"status" bson:"status,omitempty"` + BigPrize *bool `json:"bigPrize" bson:"bigPrize,omitempty"` +} + +// UpdateOneCheckinPrize 更新签到奖品 +func UpdateOneCheckinPrize(c *gin.Context) { + var req checkinPrizeUpdateReq + if err := c.ShouldBindJSON(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + id, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, "无效的ID") + return + } + now := time.Now() + updateFields := bson.M{"updatedAt": now} + if req.Title != nil { + updateFields["title"] = *req.Title + } + if req.Image != nil { + updateFields["image"] = *req.Image + } + if req.CheckinDays != nil { + updateFields["checkinDays"] = *req.CheckinDays + } + if req.CheckinType != nil { + updateFields["checkinType"] = *req.CheckinType + } + if req.PrizeId != nil { + pid, err := primitive.ObjectIDFromHex(*req.PrizeId) + if err == nil { + updateFields["prizeId"] = pid + } + } + if req.PrizeName != nil { + updateFields["prizeName"] = *req.PrizeName + } + if req.Status != nil { + updateFields["status"] = *req.Status + } + if req.BigPrize != nil { + updateFields["bigPrize"] = *req.BigPrize + } + if err := checkinser.UpdateCheckinPrize(id, updateFields); err != nil { + common.ServeJSON(c, stderr.ErrDbUpdateError, nil) + return + } + common.ServeJSON(c, stderr.Success, nil) +} + +type delReq struct { + ID string `json:"id"` +} + +// DelOneCheckinPrize 删除签到奖品 +func DelOneCheckinPrize(c *gin.Context) { + var req delReq + if err := c.ShouldBindJSON(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + id, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, "无效的ID") + return + } + if err := checkinser.DeleteCheckinPrize(id); err != nil { + common.ServeJSON(c, stderr.ErrDbDeleteError, nil) + return + } + common.ServeJSON(c, stderr.Success, nil) +} + +// ========== 签到历史记录 ========== + +type userCheckinsReq struct { + PageNumber int64 `json:"pageNumber" form:"pageNumber"` + PageSize int64 `json:"pageSize" form:"pageSize"` + UserId uint64 `json:"userId" form:"userId"` + Start time.Time `json:"start" form:"start"` + End time.Time `json:"end" form:"end"` +} + +// GetUserCheckins 获取用户签到记录列表 +func GetUserCheckins(c *gin.Context) { + var req userCheckinsReq + if err := c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + if req.PageNumber <= 0 { + req.PageNumber = 1 + } + if req.PageSize <= 0 { + req.PageSize = 20 + } + filter := bson.M{} + if req.UserId != 0 { + filter["userId"] = req.UserId + } + if !req.Start.IsZero() && !req.End.IsZero() { + filter["createdAt"] = bson.M{"$gte": req.Start, "$lte": req.End} + + } + list, total, err := checkinser.GetUserCheckinList(filter, req.PageNumber, req.PageSize) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(c, stderr.Success, gin.H{ + "list": list, + "total": total, + }) +} + +// DelOneUserCheckin 删除用户签到记录 +func DelOneUserCheckin(c *gin.Context) { + var req delReq + if err := c.ShouldBindJSON(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + id, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, "无效的ID") + return + } + if err := checkinser.DeleteUserCheckin(id); err != nil { + common.ServeJSON(c, stderr.ErrDbDeleteError, nil) + return + } + common.ServeJSON(c, stderr.Success, nil) +} diff --git a/web/api/commentAgentctrl/commentAgent.go b/web/api/commentAgentctrl/commentAgent.go new file mode 100644 index 0000000..3c23fff --- /dev/null +++ b/web/api/commentAgentctrl/commentAgent.go @@ -0,0 +1,144 @@ +package commentAgentctrl + +import ( + "91porn-server/common" + "91porn-server/common/crypt" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/cmtmod" + "91porn-server/web/service/commentser" + "net/http" + + "github.com/gin-gonic/gin" +) + +func ComicsTranslatePull(ctx *gin.Context) { + var req cmtmod.ComicsTitlePullReq + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + resp, err := commentser.ComicsTranslatePull(&req) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +func ComicsTranslatePush(ctx *gin.Context) { + var req cmtmod.ComicsTitlePushReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + resp, err := commentser.ComicsTranslatePush(&req) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// Pull doc +// @Summary 拉取未审核的评论列表 +// @Description 拉取未审核的评论列表 +// @Tags 后台-评论列表 +// @Accept mpfd,json +// @Produce json +// @Param q query cmtmod.PullReq false "请求参数" +// @Success 200 object cmtmod.PullResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/v1/comments/pull [get] +func Pull(ctx *gin.Context) { + param := cmtmod.PullReq{} + if err := ctx.ShouldBindQuery(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if param.AppId == "" || param.Sign == "" { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + isTrue := crypt.CheckSign(param.Sign, param, "pull") + if !isTrue { + log.Error("pull comment list fail", log.Any("param", param), log.Any("isTrue", isTrue)) + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + data, err := commentser.PullCommentList(param.Status, param.Limit) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + ctx.JSON(http.StatusOK, data) +} + +// Approve doc +// @Summary 评论通过审核 +// @Description 评论通过审核 +// @Tags 后台-评论列表 +// @Accept mpfd,json +// @Produce json +// @Param q query cmtmod.ApproveReq false "请求参数" +// @Success 200 object cmtmod.PullResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/v1/comments/pull [get] +func Approve(ctx *gin.Context) { + var arg cmtmod.AppIdJson + if err := ctx.ShouldBindQuery(&arg); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + sign := ctx.Query("sign") + if arg.AppId == "" || sign == "" { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + param := cmtmod.ApproveReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + isTrue := crypt.CheckSign(sign, arg, "") + if !isTrue { + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + err := commentser.CommentApprove(param) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + ctx.JSON(http.StatusOK, gin.H{"code": 200, "message": "success"}) +} + +func Reply(ctx *gin.Context) { + var arg cmtmod.AppIdJson + if err := ctx.ShouldBindQuery(&arg); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + sign := ctx.Query("sign") + if arg.AppId == "" || sign == "" { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + param := cmtmod.RepliesReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + isTrue := crypt.CheckSign(sign, arg, "") + if !isTrue { + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + data, err := commentser.ReplyComment(param) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + ctx.JSON(http.StatusOK, data) +} diff --git a/web/api/commentctrl/commentCtrl.go b/web/api/commentctrl/commentCtrl.go new file mode 100644 index 0000000..11e9f9b --- /dev/null +++ b/web/api/commentctrl/commentCtrl.go @@ -0,0 +1,503 @@ +package commentctrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/likemod" + "91porn-server/models/v/vidmod" + "91porn-server/web/service/commentser" + "91porn-server/web/webg" + "encoding/json" + "errors" + "fmt" + "io" + "math/rand" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/tealeg/xlsx" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// List doc +// @Summary 评论管理 +// @Description 评论列表 +// @Tags Web-Comment +// @Accept mpfd,json +// @Produce json,html +// @Param vidID query string false "视频id" +// @Param uid query integer false "用户id" +// @Param content query string false "评论内容" +// @Param status query integer false "审核状态" +// @Param pageNumber query integer true "页码" +// @Param pageSize query integer true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/comment/list [get] +func List(ctx *gin.Context) { + param := cmtmod.WebCmtListReqInfo{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, err := commentser.GetCommentList(param.VidID, param.UID, param.Content, param.IsRobot, param.IsAdvertiser, param.Status, param.Page) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// Delete doc +// @Summary 评论管理 - 删除评论(逻辑删除) +// @Description 评论删除 +// @Tags Web-Comment +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData array true "需要屏蔽的评论id" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/comment/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + param := cmtmod.WebCmtDeleteReqInfo{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if len(param.IDS) == 0 && param.UID == 0 { + err := errors.New(fmt.Sprintf("ids is null")) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if err := commentser.DeleteCommentsByID(param.IDS, param.UID, param.Content); err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + log, _ := json.Marshal(param) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageComment, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Info doc +// @Summary 评论管理 +// @Description 评论详情 +// @Tags Web-Comment +// @Accept mpfd,json +// @Produce json,html +// @Param id query string true "评论id" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/comment/info [get] +func Info(ctx *gin.Context) { + param := cmtmod.WebCmtReqInfo{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, err := commentser.GetCommentInfo(param.ID) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// Access doc +// @Summary 评论管理 +// @Description 通过审核 +// @Tags Web-Comment +// @Accept mpfd,json +// @Produce json,html +// @Param id query string true "评论id" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/comment/access [post] +func Access(ctx *gin.Context) { + param := cmtmod.WebCmtReqInfo{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if err := commentser.AccessComment(param.ID); err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// BatchAccess doc +// @Summary 评论管理 +// @Description 批量通过审核 +// @Tags Web-Comment +// @Accept mpfd,json +// @Produce json,html +// @Param ids query []string true "评论ids" +// @Param status query integer true "审核状态" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/comment/batch/access [post] +func BatchAccess(ctx *gin.Context) { + param := cmtmod.BatchPassReqInfo{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + if err := commentser.BatchAccessComment(param.IDS, param.Status); err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Update doc +// @Summary 修改评论管理 +// @Description 修改评论为神评论 +// @Tags Web-Comment +// @Accept mpfd,json +// @Produce json,html +// @Param id query string true "评论id" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/comment/update [post] +func Update(ctx *gin.Context) { + in := cmtmod.EditCond{} + if err := ctx.ShouldBind(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + if code := commentser.UpdateComment(&in); code != stderr.Success { + log.Error("web service comment UpdateComment error", log.E(code), log.Any("id", in.ID)) + common.ServeJSON(ctx, stderr.Failure, code) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// AddContentExcel +// 导入评论内容 doc +// @Summary 导入评论内容 +// @Description 导入评论内容 +// @Tags Web-Comment +// @Accept mpfd,json +// @Produce json,html +// @Param upload formData file true "需要导入的文件 只支持 .xls .xlsx文件" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/comment/importExcel [post] +func AddContentExcel(ctx *gin.Context) { + now := time.Now() + pwd, err := os.Getwd() + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err) + return + } + file, fHeader, err := ctx.Request.FormFile("upload") + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err) + return + } + ext := filepath.Ext(fHeader.Filename) + if !strings.ContainsAny(ext, "xls") { + common.ServeJSON(ctx, stderr.ErrMimeType, nil) + return + } + fPath := pwd + "/" + fHeader.Filename + //defer os.Remove(fPath) + // header调用Filename方法,就可以得到文件名 + out, err := os.Create(fPath) + if err != nil { + return + } + + //defer out.Close() + + // 将file的内容拷贝到out + _, err = io.Copy(out, file) + if err != nil { + return + } + + var citys = webg.Static.Cities + var uids = webg.Static.SysUser + xlFile, err := xlsx.OpenFile(fPath) + if err != nil { + return + } + rand.Seed(time.Now().Unix()) + comments := make([]*cmtmod.Comment, 0) + likes := make([]*likemod.Like, 0) + commentCountMap := make(map[primitive.ObjectID]int) + // 遍历sheet页读取 + for _, sheet := range xlFile.Sheets { + fmt.Println("sheet name: ", sheet.Name) + //遍历行读取 + for _, row := range sheet.Rows { + // 遍历每行的列读取 + var content string + var vid string + var likeCount int64 + + for k, cell := range row.Cells { + str := cell.String() + if str == "" { + break + } + if k == 0 { + content = strings.TrimSpace(cell.String()) + } + if k == 1 { + vid = strings.TrimSpace(cell.String()) + } + if k == 2 { + likeCount, _ = strconv.ParseInt(cell.String(), 10, 64) + } + } + v, err := vidmod.GetVideoInfo(vid) + if err != nil || v.ID.IsZero() { + continue + } + if _, ok := commentCountMap[v.ID]; !ok { + commentCountMap[v.ID] = 0 + } + commentCountMap[v.ID] = commentCountMap[v.ID] + 1 + reviewT := v.ReviewAt.UTC().Unix() + duration := time.Now().UTC().Unix() - reviewT + rangeMinu := duration / 60 + id := primitive.NewObjectID() + comments = append(comments, &(cmtmod.Comment{ + ID: id, + ObjID: v.ID, + Type: "video", + PublisherID: v.PublisherID, + UserID: uids[rand.Intn(len(uids)-1)], + AuthorMark: true, + IsAuthor: false, + Content: content, + Level: 1, + IPAddr: fmt.Sprintf("%d.%d.%d.%d", rand.Intn(255), rand.Intn(255), rand.Intn(255), rand.Intn(255)), + Status: 1, + City: citys[rand.Intn(len(citys)-1)], + IsDelete: false, + IsRobot: true, + CreatedAt: now.Add(-time.Minute * time.Duration(rand.Intn(int(rangeMinu)+1))), + })) + for c := int64(0); c < likeCount; c++ { + like := likemod.Like{ + Type: "comment", + ObjID: id, + UserID: uids[rand.Intn(len(uids)-1)], + LikedUserID: v.PublisherID, + CreatedAt: time.Now(), + } + like.Uniq = likemod.Unique(like.UserID, like.Type, v.ID) + likes = append(likes, &like) + } + } + } + + contentCount, _ := cmtmod.InsertManyBulket(comments) + + likeCount, _ := likemod.InsertManyBulk(likes) + + _, _ = vidmod.UpdateManyForContentCount(commentCountMap) + + common.ServeJSON(ctx, stderr.Success, gin.H{ + "contentCount": contentCount, + "likeCount": likeCount, + }) +} + +// ImportComment +// 导入评论内容 doc +// @Summary 导入评论内容 +// @Description 导入评论内容 +// @Tags Web-Comment +// @Accept mpfd,json +// @Produce json,html +// @Param upload formData file true "需要导入的文件 只支持 .xls .xlsx文件" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/comment/importComment [post] +func ImportComment(ctx *gin.Context) { + //now := time.Now() + pwd, err := os.Getwd() + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err) + return + } + file, fHeader, err := ctx.Request.FormFile("upload") + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err) + return + } + ext := filepath.Ext(fHeader.Filename) + if !strings.ContainsAny(ext, "xls") { + common.ServeJSON(ctx, stderr.ErrMimeType, nil) + return + } + fPath := pwd + "/" + fHeader.Filename + //defer os.Remove(fPath) + // header调用Filename方法,就可以得到文件名 + out, err := os.Create(fPath) + if err != nil { + return + } + + //defer out.Close() + + // 将file的内容拷贝到out + _, err = io.Copy(out, file) + if err != nil { + return + } + + var citys = webg.Static.Cities + //var uids = webg.Static.SysUser + xlFile, err := xlsx.OpenFile(fPath) + if err != nil { + return + } + rand.Seed(time.Now().Unix()) + comments := make([]*cmtmod.Comment, 0) + //likes := make([]*likemod.Like, 0) + commentCountMap := make(map[primitive.ObjectID]int) + // 遍历sheet页读取 + for _, sheet := range xlFile.Sheets { + fmt.Println("sheet name: ", sheet.Name) + //遍历行读取 + for _, row := range sheet.Rows { + // 遍历每行的列读取 + var content string + var vid string + var contentDateStr string + var uid uint64 + for k, cell := range row.Cells { + str := cell.String() + if str == "" { + break + } + if k == 0 { + // 用户ID + cuid, _ := strconv.ParseInt(cell.String(), 10, 64) + uid = uint64(cuid) + } + if k == 1 { + // 评论内容 + content = strings.TrimSpace(cell.String()) + } + if k == 2 { + // 评论对象 + vid = strings.TrimSpace(cell.String()) + } + if k == 3 { + // 评论时间 + contentDateStr = strings.TrimSpace(cell.String()) + } + } + v, err := vidmod.GetVideoInfo(vid) + if err != nil || v.ID.IsZero() { + continue + } + if _, ok := commentCountMap[v.ID]; !ok { + commentCountMap[v.ID] = 0 + } + commentCountMap[v.ID] = commentCountMap[v.ID] + 1 + //reviewT := v.ReviewAt.UTC().Unix() + //duration := time.Now().UTC().Unix() - reviewT + //rangeMinu := duration / 60 + t, _ := time.Parse("2006-01-02 15:04:05", contentDateStr) + t = t.Add(-time.Hour * 8) + id := primitive.NewObjectID() + comments = append(comments, &(cmtmod.Comment{ + ID: id, + ObjID: v.ID, + Type: "video", + PublisherID: v.PublisherID, + UserID: uid, + AuthorMark: true, + IsAuthor: false, + Content: content, + Level: 1, + IPAddr: fmt.Sprintf("%d.%d.%d.%d", rand.Intn(255), rand.Intn(255), rand.Intn(255), rand.Intn(255)), + Status: 1, + City: citys[rand.Intn(len(citys)-1)], + IsDelete: false, + IsRobot: true, + CreatedAt: t, + })) + //for c := int64(0); c < likeCount; c++ { + // like := likemod.Like{ + // Type: "comment", + // ObjID: id, + // UserID: uids[rand.Intn(len(uids)-1)], + // LikedUserID: v.PublisherID, + // CreatedAt: time.Now(), + // } + // like.Uniq = likemod.Unique(like.UserID, like.Type, v.ID) + // likes = append(likes, &like) + //} + } + } + var contentCount int64 + if len(comments) > 0 { + contentCount, _ = cmtmod.InsertManyBulket(comments) + } + + //likeCount, _ := likemod.InsertManyBulk(likes) + + _, _ = vidmod.UpdateManyForContentCount(commentCountMap) + + common.ServeJSON(ctx, stderr.Success, gin.H{ + "contentCount": contentCount, + //"likeCount": likeCount, + }) +} + +// Send doc +// @Summary 评论模块 - 发表评论 +// @Description 用户评论 +// @Tags Web-Comment +// @Accept mpfd,json +// @Produce json,html +// @Param objID formData string true "评论对象的ID" +// @Param cid formData string false "此评论是对某条评论的评论或回复,如果为空,则为对该视频的评论" +// @Param rid formData string false "被回复的评论id" +// @Param level formData integer false "评论层级 1:一级评论 2:二级评论" +// @Param toUserID formData integer false "对某用户回复评论 用户ID" +// @Param content formData string true "评论内容" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/comment/send [post] +func Send(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + param := cmtmod.PublishReqInfo{} + err = ctx.ShouldBind(¶m) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, err := commentser.PublishComment(param) + + log, _ := json.Marshal(param) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageComment, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, err) +} diff --git a/web/api/contentlibctrl/contentlibctrl.go b/web/api/contentlibctrl/contentlibctrl.go new file mode 100644 index 0000000..8cbea82 --- /dev/null +++ b/web/api/contentlibctrl/contentlibctrl.go @@ -0,0 +1,85 @@ +package contentlibctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/contentlibmod" + "91porn-server/web/service/contentlibser" + + "github.com/gin-gonic/gin" +) + +// ContentList doc +// @Summary 机器人聊天内容库列表 +// @Description 机器人聊天内容库 +// @Tags Web-Robot Content Library +// @Accept mpfd,json +// @Produce json,html +// @Param robotType query string false "机器人类型 COMMENT: 评论机器人 FILM: 影院机器人" +// @Param pageNumber query integer true "页数" +// @Param pageSize query integer true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/robot/content/list [get] +func ContentList(ctx *gin.Context) { + var param = contentlibmod.ReqContentList{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, data, err := contentlibser.GetContentList(param) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, data) +} + +// ContentAdd doc +// @Summary 增加机器人聊天内容 +// @Description 机器人聊天内容库 +// @Tags Web-Robot Content Library +// @Accept mpfd,json +// @Produce json,html +// @Param robotType query string true "机器人类型 COMMENT: 评论机器人 FILM: 影院机器人" +// @Param content query string true "需要发表的内容" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/robot/content/add [post] +func ContentAdd(ctx *gin.Context) { + var param = contentlibmod.ReqContentAdd{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, data, err := contentlibser.AddChatContent(param) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, data) +} + +// ContentDel doc +// @Summary 删除内容库的指定内容 +// @Description 机器人聊天内容库 +// @Tags Web-Robot Content Library +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData array true "机器人聊天内容的id数组" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/robot/content/del [delete] +func ContentDel(ctx *gin.Context) { + var param = contentlibmod.ReqContentDel{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, data, err := contentlibser.DeleteChatContent(param) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, data) +} diff --git a/web/api/contentreviewctrl/contentreviewctrl.go b/web/api/contentreviewctrl/contentreviewctrl.go new file mode 100644 index 0000000..324db86 --- /dev/null +++ b/web/api/contentreviewctrl/contentreviewctrl.go @@ -0,0 +1,450 @@ +package contentreviewctrl + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/contentreviewmod" + "91porn-server/models/v/sensitivewordmod" + "91porn-server/web/service/contentreviewser" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// createReq 发起任务请求体 +type createReq struct { + TaskType string `json:"taskType" binding:"required"` // VIDEO / ACG +} + +// Create doc +// @Summary 发起内容检测任务 +// @Description 立即将当前启用的敏感词库做快照写入任务记录,状态为待执行,由 skd 定时调度器异步执行 +// @Tags Web-ContentReview +// @Accept json +// @Produce json +// @Param body body createReq true "任务参数" +// @Success 200 {string} json "{"msg":"操作成功","data":{"id":"..."}}" +// @Failure 400 {string} json "{"msg":"操作失败"}" +// @Router /api/web/admin/content-review/create [post] +func Create(ctx *gin.Context) { + var req createReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if req.TaskType != contentreviewmod.TaskTypeVideo && req.TaskType != contentreviewmod.TaskTypeACG { + common.ServeJSON(ctx, stderr.ErrParamError, "invalid taskType") + return + } + + snap, err := buildSnapshot() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + if len(snap) == 0 { + common.ServeJSON(ctx, stderr.ErrParamError, "敏感词库为空,请先配置") + return + } + + operator, _ := common.GetAdminAct(ctx) + task := &contentreviewmod.ReviewTask{ + TaskType: req.TaskType, + SnapshotWords: snap, + Operator: operator, + } + if err := contentreviewmod.CreateTask(task); err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"id": task.ID.Hex()}) +} + +// listReq 任务列表请求 +type listReq struct { + Page int64 `form:"page"` // 页码,从 1 开始 + Size int64 `form:"size"` // 每页条数 + TaskType string `form:"taskType"` // 任务类型筛选 + Status *int `form:"status"` // 任务状态筛选 +} + +// taskWithStat 任务列表返回元素:嵌入 ReviewTask,附带由 IssueCount-ResolvedCount 算出的待解决数 +type taskWithStat struct { + *contentreviewmod.ReviewTask + UnresolvedCount int64 `json:"unresolvedCount"` // 待解决问题数 = IssueCount - ResolvedCount +} + +// List doc +// @Summary 检测任务列表 +// @Description 分页查询检测任务;待解决问题数 unresolvedCount = issueCount - resolvedCount,读时算差值,无需实时聚合 +// @Tags Web-ContentReview +// @Accept json +// @Produce json +// @Param page query int false "页码,默认1" +// @Param size query int false "每页条数,默认20" +// @Param taskType query string false "任务类型 VIDEO/ACG" +// @Param status query int false "状态 0待执行 1执行中 2已完成 3失败" +// @Success 200 {string} json "{"msg":"操作成功","data":{"list":[{...,"resolvedCount":0,"unresolvedCount":0}],"total":0}}" +// @Failure 400 {string} json "{"msg":"操作失败"}" +// @Router /api/web/admin/content-review/list [get] +func List(ctx *gin.Context) { + var req listReq + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + list, total, err := contentreviewmod.ListTasks(req.Page, req.Size, req.TaskType, req.Status) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + enriched := make([]taskWithStat, 0, len(list)) + for _, t := range list { + unresolved := t.IssueCount - t.ResolvedCount + if unresolved < 0 { + unresolved = 0 + } + enriched = append(enriched, taskWithStat{ReviewTask: t, UnresolvedCount: unresolved}) + } + common.ServeJSON(ctx, stderr.Success, gin.H{"list": enriched, "total": total}) +} + +// Detail doc +// @Summary 检测任务详情 +// @Description 根据任务ID查询任务详情(含敏感词快照、进度、状态等) +// @Tags Web-ContentReview +// @Accept json +// @Produce json +// @Param id query string true "任务ID" +// @Success 200 {string} json "{"msg":"操作成功","data":{}}" +// @Failure 400 {string} json "{"msg":"操作失败"}" +// @Router /api/web/admin/content-review/detail [get] +func Detail(ctx *gin.Context) { + idStr := ctx.Query("id") + id, err := primitive.ObjectIDFromHex(idStr) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "invalid id") + return + } + t, err := contentreviewmod.GetTaskByID(id) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + if t == nil { + common.ServeJSON(ctx, stderr.ErrParamError, "task not found") + return + } + common.ServeJSON(ctx, stderr.Success, t) +} + +// issuesReq 命中记录列表请求 +type issuesReq struct { + TaskID string `form:"taskId" binding:"required"` // 所属任务 ID + Page int64 `form:"page"` // 页码 + Size int64 `form:"size"` // 每页条数 + ResolveStatus *int `form:"resolveStatus"` // 解决状态筛选 0未解决 1待审核 2已解决 +} + +// Issues doc +// @Summary 检测命中记录列表 +// @Description 分页查询某个任务下命中的问题记录;Title/Content/Tags/RichText 字段已通过 ... 标红;支持按解决状态筛选 +// @Tags Web-ContentReview +// @Accept json +// @Produce json +// @Param taskId query string true "任务ID" +// @Param page query int false "页码,默认1" +// @Param size query int false "每页条数,默认20" +// @Param resolveStatus query int false "解决状态筛选 0未解决 1待审核 2已解决" +// @Success 200 {string} json "{"msg":"操作成功","data":{"list":[],"total":0}}" +// @Failure 400 {string} json "{"msg":"操作失败"}" +// @Router /api/web/admin/content-review/issues [get] +func Issues(ctx *gin.Context) { + var req issuesReq + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + tid, err := primitive.ObjectIDFromHex(req.TaskID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "invalid taskId") + return + } + list, total, err := contentreviewmod.ListIssues(tid, req.Page, req.Size, req.ResolveStatus) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"list": list, "total": total}) +} + +// resolveReq 解决(提交修改并直接生效)请求 +type resolveReq struct { + ID string `json:"id" binding:"required"` // 命中记录 ID + SubmittedTitle string `json:"submittedTitle"` // 修改后的标题(VIDEO.Title / ACG_MEDIA.Title / ACG_CONTENT.Name) + SubmittedContent string `json:"submittedContent"` // 修改后的内容(VIDEO.Content / ACG_MEDIA.Summary / ACG_CONTENT.Text) + SubmittedRichText string `json:"submittedRichText"` // 修改后的富文本(仅 VIDEO 有效) +} + +// ResolveIssue doc +// @Summary 解决命中记录(提交修改并直接生效) +// @Description 管理员提交修改即刻生效:先对提交文本做敏感词复校验(仍命中则拒绝并返回详细命中信息),通过后按 TargetType 把 Submitted* 写回数据源(VideoModel/Media/MediaContent),写回成功后标记 issue 为已解决;必须提供至少一个 submittedXxx 字段 +// @Tags Web-ContentReview +// @Accept json +// @Produce json +// @Param body body resolveReq true "解决参数" +// @Success 200 {string} json "{"msg":"操作成功"}" +// @Failure 400 {string} json "{"msg":"操作失败"}" +// @Router /api/web/admin/content-review/issue/resolve [post] +func ResolveIssue(ctx *gin.Context) { + var req resolveReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if req.SubmittedTitle == "" && req.SubmittedContent == "" && req.SubmittedRichText == "" { + common.ServeJSON(ctx, stderr.ErrParamError, "至少提交一个修改字段") + return + } + id, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "invalid id") + return + } + issue, err := contentreviewmod.GetIssueByID(id) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + if issue == nil { + common.ServeJSON(ctx, stderr.ErrParamError, "记录不存在") + return + } + if issue.ResolveStatus == contentreviewmod.ResolveStatusResolved { + common.ServeJSON(ctx, stderr.ErrParamError, "记录已解决,无需再次提交") + return + } + // 复校验:管理员提交的修正文本不得再含敏感词,否则拒绝写回并返回详细命中信息 + if tip := checkSubmittedSensitive(req.SubmittedTitle, req.SubmittedContent, req.SubmittedRichText); tip != "" { + common.ServeJSON(ctx, stderr.ContentSensitiveHit, tip) + return + } + // 用提交内容覆盖 issue 字段(仅用于 applier 的入参,不持久化到这里) + issue.SubmittedTitle = req.SubmittedTitle + issue.SubmittedContent = req.SubmittedContent + issue.SubmittedRichText = req.SubmittedRichText + // 先写回数据源;成功后才把 issue 标记为已解决 + if err := contentreviewser.ApplyResolution(issue); err != nil { + log.Error("apply resolution fail", log.Any("issueId", id), log.E(err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + operator, _ := common.GetAdminAct(ctx) + ok, err := contentreviewmod.ResolveIssue(id, issue.TaskID, req.SubmittedTitle, req.SubmittedContent, req.SubmittedRichText, operator) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + if !ok { + common.ServeJSON(ctx, stderr.ErrParamError, "记录状态已变化,请刷新") + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// checkSubmittedSensitive 对管理员提交的修正文本做敏感词复校验。 +// 任一字段仍命中启用状态的敏感词即返回详细提示(命中字段 + 命中词),全部通过返回空串。 +// 词库为空(含 DB 降级)时不拦截,返回空串。 +func checkSubmittedSensitive(title, content, richText string) string { + terms := sensitivewordmod.LoadEnabledTerms() + if len(terms) == 0 { + return "" + } + titleHits := sensitivewordmod.MatchHits(title, terms) + contentHits := sensitivewordmod.MatchHits(content, terms) + richHits := sensitivewordmod.MatchHits(richText, terms) + if len(titleHits) == 0 && len(contentHits) == 0 && len(richHits) == 0 { + return "" + } + return "提交的修正文本仍含敏感词,请修改后重新提交:" + + sensitivewordmod.FormatHitDetail(titleHits, contentHits, richHits) +} + +// batchOffShelfReq 批量下架命中请求 +type batchOffShelfReq struct { + IDs []string `json:"ids" binding:"required,min=1"` // 命中记录 ID 列表 +} + +// batchOffShelfFailure 单条失败明细 +type batchOffShelfFailure struct { + ID string `json:"id"` + Reason string `json:"reason"` +} + +// batchOffShelfResp 批量下架返回 +type batchOffShelfResp struct { + Success int `json:"success"` // 成功条数 + Fail int `json:"fail"` // 失败条数 + Failures []batchOffShelfFailure `json:"failures,omitempty"` // 失败明细 +} + +// BatchOffShelfIssues doc +// @Summary 批量下架命中记录 +// @Description 对一批命中记录直接执行"下架"处置:按 TargetType 分组下架对应资源(VIDEO→status=5;ACG_MEDIA→Media.status=0;ACG_CONTENT→MediaContent.isActive=false),下架成功后把 issue 标记为已解决,resolveAction=offshelf。前置校验阶段(id 非法 / 记录不存在 / 已解决)逐条剔除并返回原因;下架与标记走批量 UpdateMany,整体失败则该批全记入 failures。 +// @Tags Web-ContentReview +// @Accept json +// @Produce json +// @Param body body batchOffShelfReq true "批量下架参数" +// @Success 200 {object} batchOffShelfResp +// @Failure 400 {string} json "{"msg":"操作失败"}" +// @Router /api/web/admin/content-review/issue/batch-offshelf [post] +func BatchOffShelfIssues(ctx *gin.Context) { + var req batchOffShelfReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + operator, _ := common.GetAdminAct(ctx) + resp := batchOffShelfResp{Failures: make([]batchOffShelfFailure, 0)} + + // 1. 解析 ID,非法的直接计入失败 + objIDs := make([]primitive.ObjectID, 0, len(req.IDs)) + rawByID := make(map[primitive.ObjectID]string, len(req.IDs)) + for _, raw := range req.IDs { + id, err := primitive.ObjectIDFromHex(raw) + if err != nil { + resp.Fail++ + resp.Failures = append(resp.Failures, batchOffShelfFailure{ID: raw, Reason: "invalid id"}) + continue + } + objIDs = append(objIDs, id) + rawByID[id] = raw + } + if len(objIDs) == 0 { + common.ServeJSON(ctx, stderr.Success, resp) + return + } + + // 2. 一次性拉取 issue + issues, err := contentreviewmod.GetIssuesByIDs(objIDs) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + found := make(map[primitive.ObjectID]*contentreviewmod.ReviewIssue, len(issues)) + for _, iss := range issues { + found[iss.ID] = iss + } + + // 3. 过滤出可下架的 issue(存在 + 未解决) + eligible := make([]*contentreviewmod.ReviewIssue, 0, len(objIDs)) + for _, id := range objIDs { + iss, ok := found[id] + if !ok { + resp.Fail++ + resp.Failures = append(resp.Failures, batchOffShelfFailure{ID: rawByID[id], Reason: "记录不存在"}) + continue + } + if iss.ResolveStatus == contentreviewmod.ResolveStatusResolved { + resp.Fail++ + resp.Failures = append(resp.Failures, batchOffShelfFailure{ID: rawByID[id], Reason: "记录已解决"}) + continue + } + eligible = append(eligible, iss) + } + if len(eligible) == 0 { + common.ServeJSON(ctx, stderr.Success, resp) + return + } + + // 4. 按类型分组批量下架资源 + if err := contentreviewser.ApplyOffShelfBatch(eligible); err != nil { + log.Error("batch offshelf apply fail", log.E(err)) + reason := "下架失败: " + err.Error() + for _, iss := range eligible { + resp.Fail++ + resp.Failures = append(resp.Failures, batchOffShelfFailure{ID: rawByID[iss.ID], Reason: reason}) + } + common.ServeJSON(ctx, stderr.Success, resp) + return + } + + // 5. 批量标记已解决 + 按 taskID 聚合 $inc resolvedCount + eligibleIDs := make([]primitive.ObjectID, 0, len(eligible)) + perTask := make(map[primitive.ObjectID]int64, len(eligible)) + for _, iss := range eligible { + eligibleIDs = append(eligibleIDs, iss.ID) + perTask[iss.TaskID]++ + } + if err := contentreviewmod.BatchOffShelfResolveIssues(eligibleIDs, perTask, operator); err != nil { + log.Error("batch mark resolved fail", log.E(err)) + reason := "标记已解决失败: " + err.Error() + for _, iss := range eligible { + resp.Fail++ + resp.Failures = append(resp.Failures, batchOffShelfFailure{ID: rawByID[iss.ID], Reason: reason}) + } + common.ServeJSON(ctx, stderr.Success, resp) + return + } + + resp.Success = len(eligible) + common.ServeJSON(ctx, stderr.Success, resp) +} + +// rejectReq 搁置/驳回请求 +type rejectReq struct { + ID string `json:"id" binding:"required"` // 命中记录 ID + Reason string `json:"reason" binding:"required"` // 搁置原因 +} + +// RejectIssue doc +// @Summary 搁置命中记录 +// @Description 管理员认为暂不需要修改数据源,仅记录搁置原因;状态保持未解决,后续可重新提交解决 +// @Tags Web-ContentReview +// @Accept json +// @Produce json +// @Param body body rejectReq true "搁置参数" +// @Success 200 {string} json "{"msg":"操作成功"}" +// @Failure 400 {string} json "{"msg":"操作失败"}" +// @Router /api/web/admin/content-review/issue/reject [post] +func RejectIssue(ctx *gin.Context) { + var req rejectReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + id, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "invalid id") + return + } + operator, _ := common.GetAdminAct(ctx) + ok, err := contentreviewmod.RejectIssue(id, operator, req.Reason) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + if !ok { + common.ServeJSON(ctx, stderr.ErrParamError, "记录不存在或已解决") + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// buildSnapshot 拉取当前启用的敏感词作为任务快照 +func buildSnapshot() ([]contentreviewmod.SensitiveWordSnap, error) { + enabled := sensitivewordmod.StatusEnabled + list, err := sensitivewordmod.FindAll(&sensitivewordmod.ListReq{Status: &enabled}) + if err != nil { + return nil, err + } + out := make([]contentreviewmod.SensitiveWordSnap, 0, len(list)) + for _, w := range list { + out = append(out, contentreviewmod.SensitiveWordSnap{ + Word: w.Word, + Category: w.Category, + }) + } + return out, nil +} diff --git a/web/api/currencyctrl/currency.go b/web/api/currencyctrl/currency.go new file mode 100644 index 0000000..56d2908 --- /dev/null +++ b/web/api/currencyctrl/currency.go @@ -0,0 +1,55 @@ +package currencyctrl + +import ( + "fmt" + + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/web/service/currencyser" + + "github.com/gin-gonic/gin" +) + +func Add(c *gin.Context) { + var in *currencyser.AddCond + if err := c.ShouldBindJSON(&in); err != nil { + log.Error(fmt.Sprintf("新增裸聊 参数异常 [%v]", err)) + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if code := currencyser.Add(in); code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, stderr.Success, "success") +} + +func Edit(c *gin.Context) { + var in *currencyser.EditCond + if err := c.ShouldBindJSON(&in); err != nil { + log.Error(fmt.Sprintf("编辑裸聊 参数异常 [%v]", err)) + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if code := currencyser.Edit(in); code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, stderr.Success, "success") +} + +func QueryAll(c *gin.Context) { + var in *currencyser.QueryAllCond + if err := c.ShouldBindQuery(&in); err != nil { + log.Error(fmt.Sprintf("查询裸聊 参数异常 [%v]", err)) + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + data, code := currencyser.QueryAll(in) + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, data) +} diff --git a/web/api/daichongctrl/daichong.go b/web/api/daichongctrl/daichong.go new file mode 100644 index 0000000..9aac45f --- /dev/null +++ b/web/api/daichongctrl/daichong.go @@ -0,0 +1,71 @@ +package daichongctrl + +import ( + "net/http" + "strconv" + "strings" + + "91porn-server/common/daichong" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/e/daichongmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" +) + +// 查询玩家信息 doc +// @Summary 查询玩家信息 通知订单生成 +// @Description 通知订单生成 +// @Tags WEB-daichong +// @Accept mpfd,json +// @Produce json,html +// @Param appId formData string true "代充平台提供的appId" +// @Param data formData string true "加密结果字符串" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /daichong/api/chat/queryPlayerInfo [post] +func QueryPlayerInfo(ctx *gin.Context) { + param := daichongmod.CommnReq{} + if err := ctx.ShouldBind(¶m); err != nil { + log.Warn("daichong callback api queryPlayerInfo post param fail error", log.E(err)) + ctx.JSON(http.StatusOK, gin.H{"data": nil, "code": stderr.ErrParamError, "msg": stderr.ErrParamError.Msg()}) + return + } + if param.AppId != webg.Conf.DaiChong.AppID { + ctx.JSON(http.StatusOK, gin.H{"data": nil, "code": stderr.ErrAppIDNotExist, "msg": stderr.ErrAppIDNotExist.Msg()}) + return + } + querySign := daichongmod.BalanceSign{} + if err := daichong.Sign2Struct(&querySign, param.Data, webg.Conf.DaiChong.AppSecret); err != nil { + log.Warn("daichong callback api queryPlayerInfo decrypt sign data fail error", log.E(err)) + ctx.JSON(http.StatusOK, gin.H{"data": nil, "code": stderr.Failure, "msg": stderr.Failure.Msg()}) + return + } + uidstr := strings.Split(strings.TrimSpace(querySign.Uids), ",") + type userForThird struct { + Uid string `json:"uid"` + NickName string `json:"nickName"` + CoinNum string `json:"coinNum"` + BankCoin string `json:"bankCoin"` + } + ut := make([]userForThird, len(uidstr)) + for i, v := range uidstr { + uid, _ := strconv.Atoi(strings.TrimSpace(v)) + user, err := usermod.FindUserByUID(uint64(uid)) + if err != nil || user == nil { + continue + } + w, _ := walletmod.GetWallet(uint64(uid)) + coinNum := w.Amount + ut[i] = userForThird{ + Uid: strings.TrimSpace(v), + NickName: user.Name, + CoinNum: strconv.Itoa(int(coinNum)), + BankCoin: "0", + } + } + ctx.JSON(http.StatusOK, gin.H{"data": ut, "code": stderr.Success, "msg": stderr.Success.Msg()}) +} diff --git a/web/api/daichongctrl/order.go b/web/api/daichongctrl/order.go new file mode 100644 index 0000000..d324abb --- /dev/null +++ b/web/api/daichongctrl/order.go @@ -0,0 +1,84 @@ +package daichongctrl + +import ( + "encoding/json" + "net/http" + "time" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/daichong" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/rchgordmod" + "91porn-server/web/service/daichongser" + + "github.com/gin-gonic/gin" +) + +// 代充 doc +// @Summary 手动回调代充订单结果 +// @Description 手动回调代充订单结果 +// @Tags 代充-订单 +// @Accept mpfd,json +// @Produce json,html +// @Param oid formData string true "订单ID" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /daichong/order/handScore [post] +func HandAddScore(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + OID string `form:"oid" json:"oid" binding:"required"` + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + order, _ := rchgordmod.FindRechargeOrderByThirdOrderId(arg.OID) + if order.NotifyStatus == rchgordmod.Notify_SUCCESS { + c.JSON(http.StatusOK, gin.H{"code": stderr.Failure, "msg": "不可重复手动通知,已经成功通知的订单"}) + return + } + var orderStatus int + switch order.Status { + case rchgordmod.SUCCESS: + orderStatus = daichong.Success + case rchgordmod.FAILURE: + orderStatus = daichong.Failed + } + if orderStatus != daichong.Success && orderStatus != daichong.Failed { + c.JSON(http.StatusOK, gin.H{"code": stderr.Failure, "msg": "订单状态不是成功或失败状态,无法手动通知"}) + return + } + resp, code := daichongser.OrderCallBack(c, daichong.OrderCallSign{ + OrderId: arg.OID, + Time: order.NotifyTime, + Status: orderStatus, + }) + selector := rchgordmod.ReplaceEditSelector{} + if code == http.StatusOK { + notifySuccess := rchgordmod.Notify_SUCCESS + selector.NotifyStatus = ¬ifySuccess + log.InfoX(c, "daichong order call back send http request successfully", + log.Any("msg", resp.Msg), + log.Any("orderStatus", orderStatus), + log.Any("orderId", arg.OID), + log.Any("time", time.Now().Format(time.RFC3339))) + } else { + notifySuccess := rchgordmod.Notify_SUCCESS + selector.NotifyStatus = ¬ifySuccess + log.WarnX(c, "daichong order call back send http request failed", log.Any("responseCode", code), + log.Any("msg", resp.Msg)) + } + _ = rchgordmod.UpdateBaseOnOidAndStatus(nil, arg.OID, order.Status, selector) + logFiled, _ := json.Marshal(&arg) + _ = operatorlgmod.RecordOperation(manager, constant.TradeManageRechargeOrder, constant.Modify, string(logFiled), + c.Request.URL.RequestURI()) + c.JSON(http.StatusOK, resp) +} diff --git a/web/api/dramatopicctrl/topic.go b/web/api/dramatopicctrl/topic.go new file mode 100644 index 0000000..b69ac42 --- /dev/null +++ b/web/api/dramatopicctrl/topic.go @@ -0,0 +1,139 @@ +package dramatopicctrl + +import ( + "encoding/json" + "errors" + "time" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/dramatopicser" + + "github.com/gin-gonic/gin" +) + +func List(ctx *gin.Context) { + var req dramatopicser.ListRequest + if err := ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + response, err := req.List(time.Now()) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + common.ServeJSON(ctx, stderr.Success, response) +} + +func Create(ctx *gin.Context) { + manager, ok := admin(ctx) + if !ok { + return + } + var req dramatopicser.CreateRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + id, err := req.Create(time.Now()) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + record(ctx, manager, "创建", req) + common.ServeJSON(ctx, stderr.Success, gin.H{"topicId": id.Hex()}) +} + +func Update(ctx *gin.Context) { + manager, ok := admin(ctx) + if !ok { + return + } + var req dramatopicser.UpdateRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if err := req.Update(time.Now()); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + record(ctx, manager, "修改", req) + common.ServeJSON(ctx, stderr.Success, "") +} + +func Delete(ctx *gin.Context) { + manager, ok := admin(ctx) + if !ok { + return + } + var req dramatopicser.DeleteRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if err := req.Delete(time.Now()); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + record(ctx, manager, "删除", req) + common.ServeJSON(ctx, stderr.Success, "") +} + +func Sort(ctx *gin.Context) { + manager, ok := admin(ctx) + if !ok { + return + } + var req dramatopicser.SortRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if err := req.Update(time.Now()); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + record(ctx, manager, "排序", req) + common.ServeJSON(ctx, stderr.Success, "") +} + +func UpdateWorks(ctx *gin.Context) { + manager, ok := admin(ctx) + if !ok { + return + } + var req dramatopicser.UpdateWorksRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + response, err := req.Update(time.Now()) + if err != nil { + var conflict *dramatopicser.WorksConflictError + if errors.As(err, &conflict) { + common.ServeJSON(ctx, stderr.ErrParamError, conflict) + return + } + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + record(ctx, manager, "配置作品", req) + common.ServeJSON(ctx, stderr.Success, response) +} + +func admin(ctx *gin.Context) (string, bool) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err) + return "", false + } + return manager, true +} + +func record(ctx *gin.Context, manager, action string, request interface{}) { + data, _ := json.Marshal(request) + _ = operatorlgmod.RecordOperation(manager, "短剧专题管理", action, string(data), ctx.Request.URL.RequestURI()) +} diff --git a/web/api/errfeedbackctrl/errfeedback.go b/web/api/errfeedbackctrl/errfeedback.go new file mode 100644 index 0000000..36b1d98 --- /dev/null +++ b/web/api/errfeedbackctrl/errfeedback.go @@ -0,0 +1,105 @@ +package errfeedbackctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/errfedbkmod" + "91porn-server/models/v/verifyreportmod" + + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary +// @Description 查询楼凤举报信息 +// @Tags loufeng +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /loufeng/getVerifyReport [post] +func List(ctx *gin.Context) { + var p struct { + errfedbkmod.QuerySelector + commod.Page + } + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, total, err := errfedbkmod.StdFind(p.QuerySelector, p.Page) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": data, + "total": total, + "v": verifyreportmod.VerifyReport{}, + }) +} + +// Process doc +// @Summary 处理举报信息 +// @Description 处理举报信息 +// @Tags loufeng +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /loufeng/processVerifyReport [post] +func Process(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + s := errfedbkmod.EditSelector{} + if err = ctx.ShouldBind(&s); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if _, err = errfedbkmod.FindUpdate(&s); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(s) + _ = operatorlgmod.RecordOperation(manager, constant.VerifyReport, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// DelFeedback doc +// @Summary 删除举报信息 +// @Description 处理举报信息 +// @Tags loufeng +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /verifyReport/del [post] +func DelVerifyReport(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + ID string `json:"id"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = verifyreportmod.Remove(p.ID); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.VerifyReport, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/exchcodectrl/exchcodectrl.go b/web/api/exchcodectrl/exchcodectrl.go new file mode 100644 index 0000000..c739281 --- /dev/null +++ b/web/api/exchcodectrl/exchcodectrl.go @@ -0,0 +1,129 @@ +package exchcodectrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/exchcodemod" + "91porn-server/web/service/exchcodeser" + + "github.com/gin-gonic/gin" +) + +// CodeList doc +// @Summary 兑换码列表 +// @Description 兑换码管理 +// @Tags Web-ExchangeCode +// @Accept mpfd,json +// @Produce json,html +// @Param batchNum query string false "批次号" +// @Param channel query string false "渠道号" +// @Param code query string false "兑换码" +// @Param app query string false "APP:ysAppVideo--音色短视频 pfAppVideo--泡芙视频 ltAppVpn--雷霆加速器" +// @Param authority formData string false "兑换权限:shortVideoVip--短视频vip filmVip--影视vip(影院)superVip--超级vip(可以兑换短视频,也可兑换影视) gold--兑换金币" +// @Param status query string false "状态 unused--未使用,used--已兑换,expired--已过期" +// @Param pageNumber query integer true "页码" +// @Param pageSize query integer true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/exchange/code/list [get] +func CodeList(ctx *gin.Context) { + var manager, err = common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var param = exchcodemod.ListReqParam{} + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, data, err := exchcodeser.GetExchangeCodeList(param, manager) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, data) +} + +// CodeAdd doc +// @Summary 添加兑换码 +// @Description 兑换码管理 +// @Tags Web-ExchangeCode +// @Accept mpfd,json +// @Produce json,html +// @Param app formData string true "APP类型:ysAppVideo--音色短视频 pfAppVideo--泡芙视频 ltAppVpn--雷霆加速器" +// @Param channels formData array true "渠道号列表" +// @Param authority formData string true "兑换权限:shortVideoVip--短视频vip filmVip--影视vip(影院)superVip--超级vip(可以兑换短视频,也可兑换影视) gold--兑换金币" +// @Param reward formData integer true "兑换量" +// @Param count formData integer true "兑换码生成数量" +// @Param effectiveAt formData string true "生效时间" +// @Param invalidAt formData string true "失效时间" +// @Param remark formData string false "备注" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/exchange/code/add [post] +func CodeAdd(ctx *gin.Context) { + var manager, err = common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var param = exchcodemod.AddReqParam{} + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, err := exchcodeser.AddExchangeCode(param, manager) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + log, _ := json.Marshal(param) + _ = operatorlgmod.RecordOperation(manager, constant.ExchangeCode, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, nil) +} + +// CodeUpdate doc +// @Summary 编辑兑换码 +// @Description 兑换码管理 +// @Tags Web-ExchangeCode +// @Accept mpfd,json +// @Produce json,html +// @Param id query string true "兑换码id" +// @Param status query string false "状态" +// @Param authority query string false "兑换权限" +// @Param reward query integer false "兑换量(奖励值)" +// @Param remark query integer false "备注" +// @Param effectiveAt query string false "生效时间" +// @Param invalidAt query string false "失效时间" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/exchange/code/update [post] +func CodeUpdate(ctx *gin.Context) { + var manager, err = common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var param = exchcodemod.UpdateReqParam{} + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, err := exchcodeser.UpdateExchangeCode(param, manager) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + log, _ := json.Marshal(param) + _ = operatorlgmod.RecordOperation(manager, constant.ExchangeCode, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, nil) +} + +func ChannelList(ctx *gin.Context) { + common.ServeJSON(ctx, 200, gin.H{"total": 1, "list": []map[string]interface{}{{"device": "any", "channel": "system"}}}) +} diff --git a/web/api/exchcodectrl/exchlogctrl.go b/web/api/exchcodectrl/exchlogctrl.go new file mode 100644 index 0000000..575aff3 --- /dev/null +++ b/web/api/exchcodectrl/exchlogctrl.go @@ -0,0 +1,39 @@ +package exchcodectrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/exchlogmod" + "91porn-server/web/service/exchcodeser" + + "github.com/gin-gonic/gin" +) + +// ExchangeLogList doc +// @Summary 兑换码兑换日志列表 +// @Description 兑换码类型管理 +// @Tags Web-ExchangeCode +// @Accept mpfd,json +// @Produce json,html +// @Param code query string false "兑换码" +// @Param userID query integer false "兑换者" +// @Param channel query string false "所属渠道" +// @Param authority query string false "兑换权限" +// @Param pageNumber query integer true "页码" +// @Param pageSize query integer true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/exchange/log/list [get] +func LogList(ctx *gin.Context) { + var param = exchlogmod.ListReqParam{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, data, err := exchcodeser.GetExchangeTypeList(param) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, data) +} diff --git a/web/api/exportctrl/export.go b/web/api/exportctrl/export.go new file mode 100644 index 0000000..8d9335d --- /dev/null +++ b/web/api/exportctrl/export.go @@ -0,0 +1,558 @@ +package exportctrl + +import ( + "91porn-server/common/timeutil" + "91porn-server/models/v/export_task_mod" + "fmt" + "net/http" + "path/filepath" + "strconv" + "sync" + "time" + + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/commod" + "91porn-server/models/l/playlgmod" + "91porn-server/models/s/statordermod" + "91porn-server/models/s/statvidmod" + "91porn-server/models/v/exchcodemod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "91porn-server/models/v/wdordmod" + "91porn-server/web/service/exchcodeser" + "91porn-server/web/service/exportser" + "91porn-server/web/service/withdrawser" + + "github.com/gin-gonic/gin" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type userListReq struct { + usermod.UserListSelector + common.StandQuery + IsPretendAcc *int `form:"isPretendAcc" json:"isPretendAcc"` //是否马甲账号 +} + +// Export doc +// @Summary 导出文件 +// @Description 导出excel文件 +// @Tags Web-Export +// @Accept mpfd,json +// @Produce json,html +// @Param fileType query integer true "导出文件的类型" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/export/:fileType [get] +func Export(c *gin.Context) { + typeFile := c.Param("fileType") + t, err := strconv.ParseInt(typeFile, 10, 64) + if typeFile == "" || err != nil { + common.ServeJSON(c, stderr.ErrParamError, err.Error()) + return + } + fpath, err := exportser.Export(t) + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrExportFileFail, err.Error()) + return + } + c.Writer.WriteHeader(http.StatusOK) + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(fpath))) + c.Header("Content-Type", "application/octet-stream") + c.File(fpath) +} + +func getMuchUser(arg userListReq) []*usermod.User { + uInfo := []*usermod.User{} + cond, opt := common.StandQueryMap(arg.StandQuery, arg.UserListSelector) + //马甲账户 + if arg.IsPretendAcc != nil && *arg.IsPretendAcc == 1 { + cond["devID"] = bson.M{"$regex": usermod.SystemDevIDPrex, "$options": "i"} + } + //total, err := usermod.GetTotalCnt(cond, opt) + //if err != nil { + // return uInfo + //} + + // 每次最多导出1000条 + uInfo, _ = usermod.FindMany(cond, opt, int64(arg.PageNumber), int64(1000)) + return uInfo + + //if total < 1000 { + // uInfo, err = usermod.FindMany(cond, opt, int64(0), int64(1000)) + // return uInfo + //} + ////创建多个协程去拉取 + //const goRoutineCnt int = 60 + //var wg sync.WaitGroup + //size := total / int64(goRoutineCnt) + //wg.Add(goRoutineCnt) + //var mData [goRoutineCnt][]*usermod.User + //for i := 0; i < goRoutineCnt-1; i++ { + // common.GoParam(i, func(i int) { + // defer wg.Done() + // skip := int64(i) * size + // mData[i], err = usermod.FindMany(cond, opt, int64(skip), int64(size)) + // }) + //} + //common.Go(func() { + // defer wg.Done() + // skip := int64(goRoutineCnt-1) * size + // mData[goRoutineCnt-1], err = usermod.FindMany(cond, opt, int64(skip), int64(size+size)) + //}) + //wg.Wait() + //for i := 0; i < goRoutineCnt; i++ { + // uInfo = append(uInfo, mData[i]...) + //} + //return uInfo +} + +func setArray2Map(datas []playlgmod.UIDCount) map[uint64]int { + m := make(map[uint64]int) + for _, d := range datas { + m[d.UID] = d.Count + } + return m +} + +func calWatchedVideoCnt(uids []uint64) []playlgmod.UIDCount { + le := len(uids) + if le < 1000 { + datas, _ := playlgmod.HasWatchedVideoCnt(uids) + return datas + } + //创建多个协程去拉取 + const goRoutineCnt = 10 + var wg sync.WaitGroup + var mData [goRoutineCnt][]playlgmod.UIDCount + size := le / goRoutineCnt + wg.Add(goRoutineCnt) + for i := 0; i < goRoutineCnt-1; i++ { + common.GoParam(i, func(i int) { + defer wg.Done() + begin := i * size + end := begin + size - 1 + mData[i], _ = playlgmod.HasWatchedVideoCnt(uids[begin:end]) + }) + } + common.Go(func() { + defer wg.Done() + begin := (goRoutineCnt - 1) * size + end := le - 1 + mData[goRoutineCnt-1], _ = playlgmod.HasWatchedVideoCnt(uids[begin:end]) + }) + wg.Wait() + datas := []playlgmod.UIDCount{} + for i := 0; i < goRoutineCnt; i++ { + datas = append(datas, mData[i]...) + } + return datas +} + +// +//func encodeUsers(users []*usermod.User, videoCntMap map[uint64]int, unDealVideoCntMap map[uint64]int) []usermod.ExportUser { +// usersLen := len(users) +// uids := make([]uint64, 0, usersLen) +// uInfos := make([]usermod.ExportUser, 0, usersLen) +// for _, u := range users { +// if u != nil { +// user := usermod.ExportUser{ +// UID: u.UID, +// DevID: u.DevID, +// DevType: u.DevType, +// RegisterIP: u.RegisterIP, +// Mobile: u.Mobile, +// Gender: u.Gender, +// Channel: u.DistrictCode, +// Name: u.Name, +// PromotionCode: u.PromCode, +// Summary: u.Summary, +// Region: u.Region, +// Birthday: u.Birthday, +// VipLevel: u.VipLevel, +// VipExpireDate: u.VipExpireDate, +// CreatedAt: u.CreatedAt, +// MobileBindAt: u.MobileBindAt, +// HasLocked: u.HasLocked, +// HasBanned: u.HasBanned, +// TotalVideoCnt: videoCntMap[u.UID], +// UndealVideoCnt: unDealVideoCntMap[u.UID], +// LastVisitAt: u.LastVisitAt, +// } +// uInfos = append(uInfos, user) +// uids = append(uids, u.UID) +// } +// } +// wCnt := setArray2Map(calWatchedVideoCnt(uids)) +// for i, u := range uInfos { +// uInfos[i].WatchCount = wCnt[u.UID] +// } +// return uInfos +//} + +func getUIDs(users []*usermod.User) []uint64 { + uids := make([]uint64, 0, len(users)) + for _, v := range users { + if v == nil { + continue + } + uids = append(uids, v.UID) + } + return uids +} + +// Export doc +// @Summary 导出文件 +// @Description 导出excel文件 +// @Tags Web-Export +// @Accept mpfd,json +// @Produce json,html +// @Param fileType query integer true "导出文件的类型" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/export/:fileType [get] +func UserList(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg userListReq + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "user List arg error "+err.Error()) + return + } + if code := IsWithinWeek(arg.StartTime, arg.EndTime); code != stderr.Success { + common.ServeJSON(c, code, code) + return + } + err = export_task_mod.CreateTask(export_task_mod.ExportUserTask, manager, arg) + if err != nil { + common.ServeJSON(c, stderr.ErrExportFileFail, "") + return + } + common.ServeJSON(c, stderr.Success, "成功") +} + +// IsWithinWeek 判断是否是一周内 +func IsWithinWeek(start *string, end *string) (code stderr.Code) { + if start == nil || end == nil { + return stderr.ErrTimeIsEmpty + } + startTime := timeutil.StrTimeToTime(*start) + endTime := timeutil.StrTimeToTime(*end) + hours := endTime.Sub(startTime).Hours() + if hours > 24*7 { + return stderr.ErrTimeWithinWeek + } + return stderr.Success +} + +// RechargeOrder doc +// @Summary 导出文件 +// @Description 导出excel文件 +// @Tags Web-Export +// @Accept mpfd,json +// @Produce json,html +// @Param fileType query integer true "导出文件的类型" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/export/rechargeOrder [get] +func RechargeOrder(ctx *gin.Context) { + var arg struct { + common.StandQuery + rchgordmod.RchgQueryReq + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + //导出条数写死 + arg.StandQuery.PageNumber = 1 + arg.StandQuery.PageSize = 100000 + _, data, err := rchgordmod.FindRechargesOrders(common.StandQueryMap(arg.StandQuery, arg.RchgQueryReq)) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + exptData := make([]*rchgordmod.RchargeOrderExport, len(data)) + _100 := decimal.NewFromInt(100) + for i, v := range data { + exptData[i] = &rchgordmod.RchargeOrderExport{ + ID: v.ID, + UID: v.UID, + DevID: v.DevID, + OID: v.OID, + UserIP: v.UserIP, + Name: v.Name, + Tel: v.Tel, + PayAct: v.PayAct, + DevType: v.DevType, + Amount: decimal.NewFromInt(v.Amount).Div(_100).StringFixed(4), + Money: decimal.NewFromInt(v.Money).Div(_100).StringFixed(4), + PayMoney: decimal.NewFromInt(v.PayMoney).Div(_100).StringFixed(4), + RechargeType: v.RechargeType, + ProductID: v.ProductID, + VipID: v.VipID, + Channel: v.Channel, + Status: v.Status, + StatusDesc: v.StatusDesc, + ProgressAt: v.ProgressAt, + FailureAt: v.FailureAt, + SuccessAt: v.SuccessAt, + PaymentAt: v.PaymentAt, + CreatedAt: v.CreatedAt, + UpdatedAt: v.UpdatedAt, + SourcePage: v.SourcePage, + SourceRef: v.SourceRef, + VideoID: v.VideoID, + ActivityID: v.ActivityID, + ExperimentID: v.ExperimentID, + ExperimentVariant: v.ExperimentVariant, + SessionID: v.SessionID, + } + } + fpath, err := exportser.RechargeOrderExport(exptData) + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(ctx, stderr.ErrExportFileFail, err.Error()) + return + } + ctx.Writer.WriteHeader(http.StatusOK) + ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(fpath))) + ctx.Header("Content-Type", "application/octet-stream") + ctx.File(fpath) +} + +// WithdrawOrder doc +// @Summary 导出文件 +// @Description 导出excel文件 +// @Tags Web-Export +// @Accept mpfd,json +// @Produce json,html +// @Param fileType query integer true "导出文件的类型" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/export/withdrawOrder [get] +func WithdrawOrder(ctx *gin.Context) { + var arg struct { + common.StandQuery + wdordmod.WithdrawWebQueryReq + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + //导出条数写死 + arg.StandQuery.PageNumber = 1 + arg.StandQuery.PageSize = 100000 + _, data, err := withdrawser.GetAllOrders(common.StandQueryMap(arg.StandQuery, arg.WithdrawWebQueryReq)) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + fpath, err := exportser.WithdrawOrderExport(data) + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(ctx, stderr.ErrExportFileFail, err.Error()) + return + } + ctx.Writer.WriteHeader(http.StatusOK) + ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(fpath))) + ctx.Header("Content-Type", "application/octet-stream") + ctx.File(fpath) +} + +// GoldTurnover doc +// @Summary 导出文件 +// @Description 导出excel文件 +// @Tags Web-Export +// @Accept mpfd,json +// @Produce json,html +// @Param fileType query integer true "导出文件的类型" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/export/withdrawOrder [get] +func GoldTurnover(ctx *gin.Context) { + req := txnmod.CoinLogReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + m := make(map[string]interface{}) + if req.UID > 0 { + m["uid"] = req.UID + } + if req.Balance == 1 { + i := make(map[string]interface{}) + i["$gt"] = 0 + m["amount"] = i + } + if req.Balance == 2 { + i := make(map[string]interface{}) + i["$lt"] = 0 + m["amount"] = i + } + if !req.End.IsZero() { + i := make(map[string]interface{}) + i["$gte"] = req.Start + i["$lt"] = req.End + m["createdAt"] = i + } + if len(req.TranType) != 0 { + m["tranType"] = req.TranType + } + infos, _, _, _ := txnmod.GetCoinLogs(1, 100000, m) + fpath, err := exportser.GoldTurnoverExport(infos) + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(ctx, stderr.ErrExportFileFail, err.Error()) + return + } + ctx.Writer.WriteHeader(http.StatusOK) + ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(fpath))) + ctx.Header("Content-Type", "application/octet-stream") + ctx.File(fpath) +} + +// VideoIncome doc +// @Summary 导出文件 +// @Description 导出excel文件 +// @Tags Web-Export +// @Accept mpfd,json +// @Produce json,html +// @Param fileType query integer true "导出文件的类型" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/export/withdrawOrder [get] +func VideoIncome(ctx *gin.Context) { + type Query struct { + StartTime *time.Time `form:"startTime" json:"startTime" binding:""` //开始时间 + EndTime *time.Time `form:"endTime" json:"endTime" binding:""` //结束时间 + UID *uint64 `form:"uid" json:"uid" binding:""` //uid + Vid *string `form:"vid" json:"vid" binding:""` //vid + } + var arg struct { + commod.Page + Query + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "stat VideoEarnStats arg error "+err.Error()) + return + } + endTime := time.Now() + if arg.EndTime != nil { + endTime = timerange.LocDayRange(*arg.EndTime).Head + } + startTime := time.Time{} + if arg.StartTime != nil { + startTime = timerange.LocDayRange(*arg.StartTime).Head + } + var vid *primitive.ObjectID + if arg.Vid != nil { + id, _ := primitive.ObjectIDFromHex(*arg.Vid) + vid = &id + } + var vids []primitive.ObjectID + if arg.UID != nil { + list, _ := vidmod.FindMany((&vidmod.PublisherIDMatch{PublisherID: arg.UID}).New()) + vids = list.IDs() + } + limit := int64(100000) + skip := int64(0) + page, _ := statvidmod.VideoStatPages(skip, limit, + statvidmod.SumDateGTEAndLTMatch{GTE: &startTime, LT: &endTime}, + statvidmod.VidMatch{Vid: vid}, + statvidmod.VidInMatch{Vids: vids}, + ) + fpath, err := exportser.VideoIncomeExport(page.List) + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(ctx, stderr.ErrExportFileFail, err.Error()) + return + } + ctx.Writer.WriteHeader(http.StatusOK) + ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(fpath))) + ctx.Header("Content-Type", "application/octet-stream") + ctx.File(fpath) +} + +// OrderStat doc +// @Summary 导出文件 +// @Description 导出excel文件 +// @Tags Web-Export +// @Accept mpfd,json +// @Produce json,html +// @Param fileType query integer true "导出文件的类型" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/export/withdrawOrder [get] +func OrderStat(ctx *gin.Context) { + var args struct { + Type *int `form:"type" json:"type"` //订单类型 + Channel *string `form:"channel" json:"channel"` + Start *time.Time `form:"start" json:"start"` + End *time.Time `form:"end" json:"end"` + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + endTime := time.Now() + if args.End != nil { + endTime = *args.End + } + var startTime time.Time + if args.Start != nil { + startTime = *args.Start + } + skip := int64(0) + limit := int64(100000) + mats := []statordermod.Matcher{ + (&statordermod.ChannelMatch{}).New(), + (&statordermod.TypeMatch{}).New(), + (&statordermod.SumDateGTEAndLTMatch{GTE: &startTime, LT: &endTime}).New(), + } + list, err := statordermod.List(nil, &skip, &limit, mats...) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + fpath, err := exportser.OrderStatExport(list) + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(ctx, stderr.ErrExportFileFail, err.Error()) + return + } + ctx.Writer.WriteHeader(http.StatusOK) + ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(fpath))) + ctx.Header("Content-Type", "application/octet-stream") + ctx.File(fpath) +} + +// CodeList 导出兑换码 +func CodeList(ctx *gin.Context) { + var param = exchcodemod.ListReqParam{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data := exchcodeser.ExchangeCodes(param) + fpath, err := exportser.ExchangeCodeExport(data) + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(ctx, stderr.ErrExportFileFail, err.Error()) + return + } + ctx.Writer.WriteHeader(http.StatusOK) + ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(fpath))) + ctx.Header("Content-Type", "application/octet-stream") + ctx.File(fpath) +} diff --git a/web/api/extctrl/extctrl.go b/web/api/extctrl/extctrl.go new file mode 100644 index 0000000..ceff52d --- /dev/null +++ b/web/api/extctrl/extctrl.go @@ -0,0 +1,91 @@ +package extctrl + +import ( + "net/http" + "strings" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/maths" + "91porn-server/common/stderr" + "91porn-server/common/ysphone" + "91porn-server/web/service/vidser" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" +) + +// 拓展功能接口 doc +// @Summary 查看用户验证码 +// @Description 查看用户验证码 +// @Tags Ext-拓展功能 +// @Accept mpfd,json +// @Produce json,html +// @Param mobile formData string true "手机号" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/extend/viewCaptcha [get] +func ViewCaptcha(ctx *gin.Context) { + var args struct { + Mobile string `form:"mobile" json:"mobile" binding:"required"` + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + args.Mobile = strings.TrimSpace(args.Mobile) + args.Mobile = ysphone.FormatPhoneNumber(args.Mobile) + redisKey := redisconst.MobileCaptchaKey(args.Mobile) + redisCode, _ := webg.Redis.Get(redisKey) + var captcha string + if redisCode == nil { + captcha = maths.RandDigits(constant.CaptchaLen) + if err := webg.Redis.Set(redisKey, captcha, redisconst.MobileCaptchaExpire); err != nil { + log.Error("SendCaptcha Save Redis error", log.Any("mobile", args.Mobile), log.E(err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + } else { + captcha = *redisCode + } + common.ServeJSON(ctx, stderr.Success, gin.H{"captcha": captcha}) +} + +// 从Server-file同步基础信息 doc +// @Summary +// @Description 从Server-file同步基础信息 +// @Tags Ext-拓展功能 +// @Accept mpfd,json +// @Produce json,html +// @Param mobile formData string true "手机号" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/extend/syncServerFile [post] +func SyncServerFile(ctx *gin.Context) { + var args struct { + IDs []string `form:"ids" json:"ids" binding:"required"` + Token string `form:"token" json:"token" binding:"required"` + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if args.Token != "q7ydjHVIVBYiTqQ&" { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + var successCnt, failCount int64 + failIds := make([]string, 0, len(args.IDs)) + for _, v := range args.IDs { + code := vidser.SyncBaseInfoFromFs(v) + if code != stderr.UpLoadFileComplete { + failCount++ + failIds = append(failIds, v) + continue + } + successCnt++ + } + ctx.JSON(http.StatusOK, gin.H{"successCnt": successCnt, "failCount": failCount, "failIds": failIds}) +} diff --git a/web/api/fictionctrl/fiction.go b/web/api/fictionctrl/fiction.go new file mode 100644 index 0000000..5a45592 --- /dev/null +++ b/web/api/fictionctrl/fiction.go @@ -0,0 +1,165 @@ +package fictionctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/fictionmod" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Get doc +// @Summary +// @Description 查询电子书 +// @Tags fiction +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /fiction/list [post] +func List(ctx *gin.Context) { + var p struct { + fictionmod.QuerySelector + commod.Page + } + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, total, err := fictionmod.StdFind(p.QuerySelector, p.Page) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": data, + "total": total, + }) +} + +// Insert doc +// @Summary 新增 +// @Description 新增 +// @Tags fiction +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /fiction/add [post] +func Insert(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + v := fictionmod.Fiction{} + if err = ctx.ShouldBind(&v); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = fictionmod.InsertFiction(&v); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(v) + _ = operatorlgmod.RecordOperation(manager, constant.Fiction, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 修改 +// @Description 修改 +// @Tags fiction +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /fiction/edit [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + p := fictionmod.EditSelector{} + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = fictionmod.UpdateFiction(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, "") + return + } + redisKey := redisconst.DataCachKey(fictionmod.RedisSetKey, "FindfictionByType") + _, _ = webg.Redis.Del(redisKey) + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Fiction, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除 +// @Description 删除 +// @Tags fiction +// @Accept mpfd,json +// @Produce json,html +// @Param id formData int true "fiction等级"" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /fiction/del [delete] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + ID string `json:"id" form:"id"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "") + return + } + if err = fictionmod.RemoveFiction(p.ID); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, "") + return + } + redisKey := redisconst.DataCachKey(fictionmod.RedisSetKey, "FindfictionByType") + _, _ = webg.Redis.Del(redisKey) + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Fiction, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Get doc +// @Summary +// @Description 查询电子书 +// @Tags fiction +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /fiction/list [post] +func Get(ctx *gin.Context) { + var p struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + } + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := fictionmod.GetByID(p.ID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/filterctrl/filter.go b/web/api/filterctrl/filter.go new file mode 100644 index 0000000..2a87d5f --- /dev/null +++ b/web/api/filterctrl/filter.go @@ -0,0 +1,88 @@ +package filterctrl + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/filtermod" + "91porn-server/web/service/filterser" + + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 过滤词管理 +// @Description 过滤词列表 +// @Tags Web-WordsFilter +// @Accept mpfd,json +// @Produce json,html +// @Param content query string false "搜索内容" +// @Param pageNumber query integer true "评论id" +// @Param pageSize query integer true "评论id" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/filter/list [get] +func List(ctx *gin.Context) { + var param filtermod.WordListReq + if err := ctx.ShouldBind(¶m); err != nil { + log.Error("api web filter List bind param error", log.E(err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, data, err := filterser.GetFilterWordList(param.Content, param.Page) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, data) +} + +// Add doc +// @Summary 过滤词管理 +// @Description 添加过滤词 +// @Tags Web-WordsFilter +// @Accept mpfd,json +// @Produce json,html +// @Param words formData array true "过滤词数组" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/filter/add [post] +func Add(ctx *gin.Context) { + var param filtermod.WordAddReq + if err := ctx.ShouldBind(¶m); err != nil { + log.Error("api web filter Add bind param error", log.E(err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, err := filterser.AddFilterWord(param.Words) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, nil) +} + +// Delete doc +// @Summary 过滤词管理 +// @Description 删除过滤词 +// @Tags Web-WordsFilter +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData array true "过滤词id数组" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/filter/delete [delete] +func Delete(ctx *gin.Context) { + var param filtermod.WordDeleteReq + if err := ctx.ShouldBind(¶m); err != nil { + log.Error("api web filter Delete bind param error", log.E(err)) + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, err := filterser.DeleteFilterWords(param.IDS) + if err != nil { + common.ServeJSON(ctx, code, err) + return + } + common.ServeJSON(ctx, code, nil) +} diff --git a/web/api/freevidcfgctrl/freevidcfg.go b/web/api/freevidcfgctrl/freevidcfg.go new file mode 100644 index 0000000..89ee5bc --- /dev/null +++ b/web/api/freevidcfgctrl/freevidcfg.go @@ -0,0 +1,138 @@ +package freevidcfgctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/freeVidmod" + + "github.com/gin-gonic/gin" +) + +// 免费观看视频配置 doc +// @Summary 免费观看配置新增 +// @Description 免费观看配置新增 +// @Tags 免费视频观看-配置 +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer true "免费观看受益人" +// @Param publisherID formData array true "可免费观看视频的发布者列表" +// @Param startTime formData string false "免费观看开始时间" +// @Param endTime formData string false "免费观看开始时间" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /freeVid/cfg/add [post] +func Add(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + incr := freeVidmod.Incr{} + if err := c.ShouldBind(&incr); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if err = freeVidmod.InsertOne(&incr); err != nil { + common.ServeJSON(c, stderr.ErrDbInsertError, err) + return + } + log, _ := json.Marshal(&incr) + _ = operatorlgmod.RecordOperation(manager, constant.FreeVidConfig, constant.Add, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, nil) +} + +// 免费观看视频配置 doc +// @Summary 免费观看配置修改 +// @Description 免费观看配置修改 +// @Tags 免费视频观看-配置 +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer true "免费观看受益人" +// @Param publisherID formData array false "可免费观看视频的发布者列表" +// @Param startTime formData string false "免费观看开始时间" +// @Param endTime formData string false "免费观看开始时间" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /freeVid/cfg/edit [post] +func Edit(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + Uid uint64 `form:"uid" json:"uid" binding:"required"` + freeVidmod.EditDoc + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if _, err = freeVidmod.Update(arg.Uid, arg.EditDoc); err != nil { + common.ServeJSON(c, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(&arg) + _ = operatorlgmod.RecordOperation(manager, constant.FreeVidConfig, constant.Modify, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, nil) +} + +// 免费观看视频配置 doc +// @Summary 免费观看配置列表 +// @Description 免费观看配置列表 +// @Tags 免费视频观看-配置 +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer false "免费观看受益人" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /freeVid/cfg/list [get] +func List(ctx *gin.Context) { + param := freeVidmod.ListParam{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web freeVid config list arg error "+err.Error()) + return + } + result, err := freeVidmod.List(param) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, result) +} + +// 免费观看视频配置 doc +// @Summary 免费观看配置删除 +// @Description 免费观看配置删除 +// @Tags 免费视频观看-配置 +// @Accept mpfd,json +// @Produce json,html +// @Param uids formData array true "用户ID数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /freeVid/cfg/del [delete] +func Del(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var param struct { + UIDs []uint64 `form:"uids" json:"uids" binding:"required"` + } + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if _, err = freeVidmod.Delete(param.UIDs); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(¶m) + _ = operatorlgmod.RecordOperation(manager, constant.FreeVidConfig, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/goldcfgctrl/goldcfg.go b/web/api/goldcfgctrl/goldcfg.go new file mode 100644 index 0000000..b299e98 --- /dev/null +++ b/web/api/goldcfgctrl/goldcfg.go @@ -0,0 +1,163 @@ +package goldcfgctrl + +import ( + "encoding/json" + "fmt" + "strings" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/goldcfgmod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 支付优惠配置 doc +// @Summary 支付优惠配置新增 +// @Description 支付优惠新增 +// @Tags 支付-配置 +// @Accept mpfd,json +// @Produce json,html +// @Param amount formData integer true "基础货币额度" +// @Param incrAmount formData integer false "增加的优惠额度 incrAmount与incTax 不可同时为空" +// @Param incTax formData number false "按比率增加额外优惠额" +// @Param type formData string true "充值方式" +// @Param typeName formData string true "类型名称:支付宝,微信,代充" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /gold/cfg/add [post] +func Add(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + incr := goldcfgmod.Incr{} + if err := c.ShouldBind(&incr); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if incr.IncTax > 1 || incr.IncTax < 0 { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if err = goldcfgmod.InsertOne(&incr); err != nil { + common.ServeJSON(c, stderr.ErrDbInsertError, err) + return + } + log, _ := json.Marshal(&incr) + _ = operatorlgmod.RecordOperation(manager, constant.GoldConfig, constant.Add, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, nil) +} + +// 支付优惠配置 doc +// @Summary 支付优惠配置 +// @Description 支付优惠编辑 +// @Tags 支付-配置 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "id" +// @Param amount formData integer false "基础货币额度" +// @Param incrAmount formData integer false "增加的优惠额度" +// @Param incTax formData number false "按比率增加额外优惠额" +// @Param type formData string false "充值方式" +// @Param typeName formData string false "类型名称:支付宝,微信,代充" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /gold/cfg/edit [post] +func Edit(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + ID primitive.ObjectID `json:"id" bson:"id" binding:"required"` //基础货币额度 + goldcfgmod.EditDoc + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if arg.IncTax != nil { + if *arg.IncTax > 1 || *arg.IncTax < 0 { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + } + if _, err = goldcfgmod.Update(arg.ID, arg.EditDoc); err != nil { + //判断是否有重复插入 + if strings.Contains(err.Error(), "E11000 duplicate key error") { + common.ServeJSON(c, stderr.ErrDbUpdateError, err) + return + } + common.ServeJSON(c, stderr.Failure, fmt.Errorf("goldcfgmod updateOne error: %+v", err)) + return + } + log, _ := json.Marshal(&arg) + _ = operatorlgmod.RecordOperation(manager, constant.GoldConfig, constant.Modify, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, nil) +} + +// 支付优惠配置 doc +// @Summary 支付优惠配置列表 +// @Description 支付优惠列表 +// @Tags 支付-配置 +// @Accept mpfd,json +// @Produce json,html +// @Param amount formData integer true "基础货币额度" +// @Param type formData string true "充值方式" +// @Param typeName formData string true "类型名称:支付宝,微信,代充" +// @Param pageNumber query integer true "当前页" mininum(1) +// @Param pageSize query integer true "每页条数" mininum(1) +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /gold/cfg/list [get] +func List(ctx *gin.Context) { + param := goldcfgmod.ListParam{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web gold config list arg error "+err.Error()) + return + } + result, err := goldcfgmod.List(param) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, result) +} + +// 支付优惠配置 doc +// @Summary 支付优惠配置删除 +// @Description 支付删除 +// @Tags 支付-配置 +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData array true "ID数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /gold/cfg/del [delete] +func Del(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var param struct { + IDs []primitive.ObjectID `form:"ids" json:"ids" binding:"required"` + } + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if _, err = goldcfgmod.Delete(param.IDs); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(¶m) + _ = operatorlgmod.RecordOperation(manager, constant.GoldConfig, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/imagetopctrl/imagetop.go b/web/api/imagetopctrl/imagetop.go new file mode 100644 index 0000000..d0e01fa --- /dev/null +++ b/web/api/imagetopctrl/imagetop.go @@ -0,0 +1,46 @@ +package imagetopctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/imagetopmod" + "91porn-server/web/service/imagetopser" + + "github.com/gin-gonic/gin" +) + +// 获取置顶帖子列表 +func GetImageTopList(ctx *gin.Context) { + req := imagetopmod.ImageTopListReq{} + err := ctx.ShouldBind(&req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + code, data := imagetopser.FindImageTopList(req) + common.ServeJSON(ctx, code, data) +} + +// 更新帖子置顶列表 +func UpdateImageTop(ctx *gin.Context) { + req := imagetopmod.EditReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + //更新置顶帖子信息 + code, data := imagetopser.UpdateImageTop(req) + common.ServeJSON(ctx, code, data) +} + +// 置顶帖子列表 +func DeleteImageTop(ctx *gin.Context) { + req := imagetopmod.DeleteReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + //删除置顶贴子 + code, data := imagetopser.DeleteImageTop(req) + common.ServeJSON(ctx, code, data) +} diff --git a/web/api/imgroupctrl/imgroup.go b/web/api/imgroupctrl/imgroup.go new file mode 100755 index 0000000..c2cf42f --- /dev/null +++ b/web/api/imgroupctrl/imgroup.go @@ -0,0 +1,106 @@ +package imgroupctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/imgroupser" + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取im群组列表 +// @Description 获取im群组列表 +// @Tags 后台-im群组 +// @Accept mpfd,json +// @Produce json +// @Param q query imgroupser.WebListReq false "请求参数" +// @Success 200 object imgroupser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/imgroup/list [get] +func List(ctx *gin.Context) { + var req = &imgroupser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Create doc +// @Summary 新增im群组 +// @Description 新增im群组 +// @Tags 后台-im群组 +// @Accept mpfd,json +// @Produce json +// @Param q body imgroupser.WebCreateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/imgroup/create [post] +func Create(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &imgroupser.WebCreateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 创建 + err = p.Create() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "im群组管理", "创建", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 更新im群组 +// @Description 更新im群组 +// @Tags 后台-im群组 +// @Accept mpfd,json +// @Produce json +// @Param q body imgroupser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/imgroup/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &imgroupser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "im群组管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/imgroupmemberctrl/imgroupmember.go b/web/api/imgroupmemberctrl/imgroupmember.go new file mode 100755 index 0000000..dc1ff28 --- /dev/null +++ b/web/api/imgroupmemberctrl/imgroupmember.go @@ -0,0 +1,106 @@ +package imgroupmemberctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/imgroupmemberser" + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取im群组成员列表 +// @Description 获取im群组成员列表 +// @Tags 后台-im群组成员 +// @Accept mpfd,json +// @Produce json +// @Param q query imgroupmemberser.WebListReq false "请求参数" +// @Success 200 object imgroupmemberser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/imgroupmember/list [get] +func List(ctx *gin.Context) { + var req = &imgroupmemberser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Create doc +// @Summary 新增im群组成员 +// @Description 新增im群组成员 +// @Tags 后台-im群组成员 +// @Accept mpfd,json +// @Produce json +// @Param q body imgroupmemberser.WebCreateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/imgroupmember/create [post] +func Create(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &imgroupmemberser.WebCreateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 创建 + err = p.Create() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "im群组成员管理", "创建", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 更新im群组成员 +// @Description 更新im群组成员 +// @Tags 后台-im群组成员 +// @Accept mpfd,json +// @Produce json +// @Param q body imgroupmemberser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/imgroupmember/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &imgroupmemberser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "im群组成员管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/immessagectrl/immessage.go b/web/api/immessagectrl/immessage.go new file mode 100755 index 0000000..84f4449 --- /dev/null +++ b/web/api/immessagectrl/immessage.go @@ -0,0 +1,142 @@ +package immessagectrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/immessageser" + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取im消息列表 +// @Description 获取im消息列表 +// @Tags 后台-im消息 +// @Accept mpfd,json +// @Produce json +// @Param q query immessageser.WebListReq false "请求参数" +// @Success 200 object immessageser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/immessage/list [get] +func List(ctx *gin.Context) { + var req = &immessageser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Create doc +// @Summary 新增im消息 +// @Description 新增im消息 +// @Tags 后台-im消息 +// @Accept mpfd,json +// @Produce json +// @Param q body immessageser.WebCreateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/immessage/create [post] +func Create(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &immessageser.WebCreateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 创建 + err = p.Create() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "im消息管理", "创建", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 更新im消息 +// @Description 更新im消息 +// @Tags 后台-im消息 +// @Accept mpfd,json +// @Produce json +// @Param q body immessageser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/immessage/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &immessageser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "im消息管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除im消息 +// @Description 删除im消息 +// @Tags 后台-im消息 +// @Accept mpfd,json +// @Produce json +// @Param q body immessageser.WebDeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/immessage/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &immessageser.WebDeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = p.Delete() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "im消息管理", "删除", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/infmtctrl/infmt.go b/web/api/infmtctrl/infmt.go new file mode 100644 index 0000000..12f597a --- /dev/null +++ b/web/api/infmtctrl/infmt.go @@ -0,0 +1,459 @@ +package infmtctrl + +import ( + "encoding/json" + "sort" + "time" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/noticefmtmod" + "91porn-server/web/service/infmtser" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Sender = noticefmtmod.Sender + +type Receiver = noticefmtmod.Receiver + +type NoticeType = noticefmtmod.NoticeType + +type UIDSlice = noticefmtmod.UIDSlice + +type TimeUnit = noticefmtmod.TimeUnit + +type TimeSlice = noticefmtmod.TimeSlice + +type ObjectID = primitive.ObjectID + +type RegularNotice = noticefmtmod.RegularNotice + +type PlanNotice = noticefmtmod.PlanNotice + +type SpecifyNotice = noticefmtmod.SpecifyNotice + +type MailNotice = noticefmtmod.MailNotice + +type AfterRegistNotice = noticefmtmod.AfterRegistNotice + +// NoticeList doc +// @Summary 通知列表 +// @Description 通知列表 +// @Tags web-Notice +// @Accept mpfd,json +// @Produce json,html +// @Param noticeCode query string false "通知码" +// @Param title query string false "标题" +// @Param noticeType query string false "通知类型" +// @Param sender query string false "发送者" +// @Param receiver query string false "接受者" +// @Param enable query bool false "开关" +// @Param pageNumber query int true "当前页" +// @Param pageSize query int true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "date": { "total":10, "list":[] }}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/infmt/notice/list [get] +func NoticeList(c *gin.Context) { + type Query struct { + NoticeCode *string `form:"noticeCode" json:"noticeCode" binding:""` //通知码 唯一标记不同的通知计划,可代替_id,方便复制数据 + Title *string `form:"title" json:"title" binding:""` //标题 + NoticeType *NoticeType `form:"noticeType" json:"noticeType" binding:""` //通知类型 + Sender *Sender `form:"sender" json:"sender" binding:""` //发送者 + Receiver *Receiver `form:"receiver" json:"receiver" binding:""` //接受者 + Enable *bool `form:"enable" json:"enable" binding:""` //开关 + } + var arg struct { + commod.Page + Query + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "infmtctrl NoticeList arg error "+err.Error()) + return + } + titleMatch := infmtser.TitleMatch{Title: arg.Title} + noticeCodeMatch := infmtser.NoticeCodeMatch{NoticeCode: arg.NoticeCode} + noticeTypeMatch := infmtser.NoticeTypeMatch{NoticeType: arg.NoticeType} + senderMatch := infmtser.SenderMatch{Sender: arg.Sender} + receiverMatch := infmtser.ReceiverMatch{Receiver: arg.Receiver} + enableMatch := infmtser.EnableMatch{Enable: arg.Enable} + skip := int64((arg.PageNumber - 1) * arg.PageSize) + limit := int64(arg.PageSize) + page, err := infmtser.NoticePages(skip, limit, titleMatch, noticeCodeMatch, noticeTypeMatch, senderMatch, receiverMatch, enableMatch) + if err != nil { + common.ServeJSON(c, stderr.Failure, "web infmtctrl NoticeList error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} + +// NoticeUpdate doc +// @Summary 编辑通知 +// @Description 编辑通知 +// @Tags web-Notice +// @Accept mpfd,json +// @Produce json,html +// @Param ID formData string true "id" +// @Param title query string false "标题" +// @Param sender query string false "发送者" +// @Param receiver query string false "接受者" +// @Param enable query bool false "开关" +// @Param uidList query array false "用户列表" +// @Param content query string false "内容" +// @Param remark query string false "备注" +// @Param noticeType query string false "通知类型" +// @Param startAt query string false "开始时间" +// @Param timeUnit query string false "单位 分钟/小时/天/周" +// @Param timeCount query int false "量" +// @Param endAt query string false "结束时间" +// @Param planCount query int false "执行次数" +// @Param specifyTimeList query array false "指定执行时间数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/infmt/notice/update [post] +func NoticeUpdate(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + type NoticeFMT struct { + StartAt *time.Time `form:"startAt" json:"startAt" binding:""` //开始时间 + TimeUnit *TimeUnit `form:"timeUnit" json:"timeUnit" binding:""` //单位 分钟/小时/天/周 + TimeCount *int64 `form:"timeCount" json:"timeCount" binding:""` //量 + EndAt *time.Time `form:"endAt" json:"endAt" binding:""` //结束时间 + PlanCount *int64 `form:"planCount" json:"planCount" binding:""` //执行次数 + SpecifyTimeList TimeSlice `form:"specifyTimeList" json:"specifyTimeList" binding:""` //指定执行时间数组 + } + type Update struct { + Title *string `form:"title" json:"title" binding:""` //标题 + Sender *Sender `form:"sender" json:"sender" binding:""` //发送者 + Receiver *Receiver `form:"receiver" json:"receiver" binding:""` //接受者 + Enable *bool `form:"enable" json:"enable" binding:""` //开关 + UIDList UIDSlice `form:"uidList" json:"uidList" binding:""` //用户列表 + Content *string `form:"content" json:"content" binding:""` //内容 + Remark *string `form:"remark" json:"remark" binding:""` //备注 + NoticeType *NoticeType `form:"noticeType" json:"noticeType" binding:""` //通知类型 + NoticeFMT + } + var arg struct { + ID ObjectID `form:"id" json:"id" binding:"required"` + Update + } + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web infmtctrl NoticeUpdate arg error "+err.Error()) + return + } + updateDoc := noticefmtmod.UpdateDoc{ + Sender: arg.Sender, + Receiver: arg.Receiver, + UIDList: arg.UIDList, + Title: arg.Title, + Content: arg.Content, + Enable: arg.Enable, + Remark: arg.Remark, + NoticeType: arg.NoticeType, + } + if arg.NoticeType != nil { + switch *arg.NoticeType { + case noticefmtmod.RegularNoticeType: + updateDoc.RegularNotice = RegularNotice{ + StartAt: arg.StartAt, + TimeUnit: arg.TimeUnit, + TimeCount: arg.TimeCount, + } + case noticefmtmod.PlanNoticeType: + updateDoc.PlanNotice = PlanNotice{ + StartAt: arg.StartAt, + EndAt: arg.EndAt, + PlanCount: arg.PlanCount, + } + case noticefmtmod.SpecifyNoticeType: + updateDoc.SpecifyNotice = SpecifyNotice{ + SpecifyTimeList: arg.SpecifyTimeList, + } + case noticefmtmod.AfterRegistNoticeType: + updateDoc.AfterRegistNotice = AfterRegistNotice{ + TimeUnit: arg.TimeUnit, + TimeCount: arg.TimeCount, + } + } + } + if err = noticefmtmod.UpdateOne(arg.ID, updateDoc); err != nil { + common.ServeJSON(c, stderr.ErrDbUpdateError, "web infmtctrl NoticeUpdate error: "+err.Error()) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.UserManageList, constant.Modify, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// NoticeAdd doc +// @Summary 添加通知 +// @Description 添加通知 +// @Tags web-Notice +// @Accept mpfd,json +// @Produce json,html +// @Param noticeType query string true "通知类型" +// @Param title query string true "标题" +// @Param sender query string true "发送者" +// @Param receiver query string true "接受者" +// @Param enable query bool true "开关" +// @Param uidList query array false "用户列表" +// @Param content query string false "内容" +// @Param remark query string false "备注" +// @Param startAt query string false "开始时间" +// @Param timeUnit query string false "单位 分钟/小时/天/周" +// @Param timeCount query int false "量" +// @Param endAt query string false "结束时间" +// @Param planCount query int false "执行次数" +// @Param specifyTimeList query array false "指定执行时间数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/infmt/notice/add [post] +func NoticeAdd(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + type NoticeFMT struct { + StartAt *time.Time `form:"startAt" json:"startAt" binding:""` //开始时间 + TimeUnit *TimeUnit `form:"timeUnit" json:"timeUnit" binding:""` //单位 分钟/小时/天/周 + TimeCount *int64 `form:"timeCount" json:"timeCount" binding:""` //量 + EndAt *time.Time `form:"endAt" json:"endAt" binding:""` //结束时间 + PlanCount *int64 `form:"planCount" json:"planCount" binding:""` //执行次数 + SpecifyTimeList TimeSlice `form:"specifyTimeList" json:"specifyTimeList" binding:""` //指定执行时间数组 + } + type Insert struct { + NoticeType NoticeType `form:"noticeType" json:"noticeType" binding:"required"` //通知类型 + Title string `form:"title" json:"title" binding:"required"` //标题 + Sender Sender `form:"sender" json:"sender" binding:"required"` //发送者 + Receiver Receiver `form:"receiver" json:"receiver" binding:"required"` //接受者 + Enable *bool `form:"enable" json:"enable" binding:"required"` //开关 + UIDList UIDSlice `form:"uidList" json:"uidList" binding:""` //用户列表 + Content *string `form:"content" json:"content" binding:""` //内容 + Remark *string `form:"remark" json:"remark" binding:""` //备注 + NoticeFMT + } + var arg struct { + Insert + } + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web infmtctrl NoticeAdd arg error "+err.Error()) + return + } + insertDoc := noticefmtmod.InsertDoc{ + Sender: arg.Sender, + Receiver: arg.Receiver, + UIDList: arg.UIDList, + Title: arg.Title, + NoticeType: arg.NoticeType, + Enable: *arg.Enable, + } + insertDoc.NoticeCode = common.UUID() + if arg.Content != nil { + insertDoc.Content = *arg.Content + } + if arg.Remark != nil { + insertDoc.Remark = *arg.Remark + } + switch arg.NoticeType { + case noticefmtmod.RegularNoticeType: + insertDoc.RegularNotice = RegularNotice{ + StartAt: arg.StartAt, + TimeUnit: arg.TimeUnit, + TimeCount: arg.TimeCount, + } + case noticefmtmod.PlanNoticeType: + insertDoc.PlanNotice = PlanNotice{ + StartAt: arg.StartAt, + EndAt: arg.EndAt, + PlanCount: arg.PlanCount, + } + case noticefmtmod.SpecifyNoticeType: + insertDoc.SpecifyNotice = SpecifyNotice{ + SpecifyTimeList: arg.SpecifyTimeList, + } + case noticefmtmod.AfterRegistNoticeType: + insertDoc.AfterRegistNotice = AfterRegistNotice{ + TimeUnit: arg.TimeUnit, + TimeCount: arg.TimeCount, + } + } + sort.Sort(insertDoc.UIDList) + if err = noticefmtmod.InsertOne(insertDoc); err != nil { + common.ServeJSON(c, stderr.ErrDbInsertError, "web infmtctrl NoticeAdd InsertOne error: "+err.Error()) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.UserManageList, constant.Add, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// NoticeDel doc +// @Summary 删除通知 +// @Description 删除通知 +// @Tags web-Notice +// @Accept mpfd,json +// @Produce json,html +// @Param ID formData string true "id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/infmt/notice/delete [delete] +func NoticeDel(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + ID ObjectID `form:"id" json:"id" binding:"required"` + } + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web infmtctrl NoticeDel arg error "+err.Error()) + return + } + if err = noticefmtmod.DeleteOne(arg.ID); err != nil { + common.ServeJSON(c, stderr.ErrDbDeleteError, "web infmtctrl NoticeDel DeleteOne error: "+err.Error()) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.UserManageList, constant.Delete, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// MailList doc +// @Summary 邮件通知列表 +// @Description 邮件通知列表 +// @Tags web-Mail +// @Accept mpfd,json +// @Produce json,html +// @Param noticeCode query string false "通知码" +// @Param title query string false "标题" +// @Param receiver query string false "接受者" +// @Param pageNumber query int true "当前页" +// @Param pageSize query int true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "date": { "total":10, "list":[] }}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/infmt/mail/list [get] +func MailList(c *gin.Context) { + type Query struct { + NoticeCode *string `form:"noticeCode" json:"noticeCode" binding:""` //通知码 唯一标记不同的通知计划,可代替_id,方便复制数据 + Title *string `form:"title" json:"title" binding:""` //标题 + Receiver *Receiver `form:"receiver" json:"receiver" binding:""` //接受者 + } + var arg struct { + commod.Page + Query + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "infmtctrl MailList arg error "+err.Error()) + return + } + titleMatch := infmtser.TitleMatch{Title: arg.Title} + noticeCodeMatch := infmtser.NoticeCodeMatch{NoticeCode: arg.NoticeCode} + receiverMatch := infmtser.ReceiverMatch{Receiver: arg.Receiver} + skip := int64((arg.PageNumber - 1) * arg.PageSize) + limit := int64(arg.PageSize) + page, err := infmtser.MailPages(skip, limit, titleMatch, noticeCodeMatch, receiverMatch) + if err != nil { + common.ServeJSON(c, stderr.Failure, "web infmtctrl MailList error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} + +// MailAdd doc +// @Summary 添加邮件 +// @Description 添加邮件 +// @Tags web-Mail +// @Accept mpfd,json +// @Produce json,html +// @Param title query string true "标题" +// @Param receiver query string true "接受者" +// @Param content query string true "内容" +// @Param uidList query array false "用户列表" +// @Param remark query string false "备注" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/infmt/mail/add [post] +func MailAdd(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + type Insert struct { + Title string `form:"title" json:"title" binding:"required"` //标题 + Receiver Receiver `form:"receiver" json:"receiver" binding:"required"` //接受者 + Content string `form:"content" json:"content" binding:"required"` //内容 + UIDList UIDSlice `form:"uidList" json:"uidList" binding:""` //用户列表 + Remark *string `form:"remark" json:"remark" binding:""` //备注 + } + var arg struct { + Insert + } + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web infmtctrl NoticeAdd arg error "+err.Error()) + return + } + sendAt := time.Now() + insertDoc := noticefmtmod.InsertDoc{ + NoticeCode: common.UUID(), + NoticeType: noticefmtmod.MailNoticeType, + Sender: noticefmtmod.System, + Enable: true, + Receiver: arg.Receiver, + UIDList: arg.UIDList, + Title: arg.Title, + Content: arg.Content, + MailNotice: MailNotice{SendAt: &sendAt}, + } + if arg.Remark != nil { + insertDoc.Remark = *arg.Remark + } + if err = noticefmtmod.InsertOne(insertDoc); err != nil { + common.ServeJSON(c, stderr.ErrDbInsertError, "web infmtctrl MailAdd InsertOne error: "+err.Error()) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.UserManageList, constant.Add, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// MailDel doc +// @Summary 删除邮件通知 +// @Description 删除邮件通知 +// @Tags web-Mail +// @Accept mpfd,json +// @Produce json,html +// @Param ID formData string true "id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/infmt/mail/delete [delete] +func MailDel(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + ID ObjectID `form:"id" json:"id" binding:"required"` + } + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web infmtctrl MailDel arg error "+err.Error()) + return + } + if err = noticefmtmod.DeleteOne(arg.ID); err != nil { + common.ServeJSON(c, stderr.ErrDbDeleteError, "web infmtctrl MailDel DeleteOne error: "+err.Error()) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.UserManageList, constant.Delete, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} diff --git a/web/api/integral_config_ctrl/integral_config.go b/web/api/integral_config_ctrl/integral_config.go new file mode 100644 index 0000000..a92f18e --- /dev/null +++ b/web/api/integral_config_ctrl/integral_config.go @@ -0,0 +1,120 @@ +package integral_config_ctrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/integralconfigmod" + "91porn-server/web/service/integralconfigser" + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" +) + +// QueryAll doc +// @Summary 查询积分配置 +// @Description 查询积分配置 +// @Tags 积分配置 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData primitive.ObjectID false "积分配置id" +// @Param status formData bool false "积分状态" +// @Param page formData string true "当前页面" +// @Param limit formData string true "页码" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/integral_config [get] +func QueryAll(c *gin.Context) { + var in integralconfigmod.QueryAllCond + if err := c.ShouldBindQuery(&in); err != nil { + log.Error(fmt.Sprintf("Query all integralconfigser param err%v\n", err)) + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + + data, err := integralconfigser.QueryAll(&in) + if err != nil { + log.Error(fmt.Sprintf("Query all integralconfigser err%v\n%d", data, err)) + common.ServeJSON(c, stderr.Failure, nil) + return + } + common.ServeJSON(c, stderr.Success, data) +} + +// Update doc +// @Summary 修改积分配置 +// @Description 修改积分配置 +// @Tags 积分配置 +// @Accept mpfd,json +// @Produce json,html +// @Param ID formData primitive.ObjectID true "文档id" +// @Param name formData string false "积分名称" +// @Param duration formData integer false "积分持续天数" +// @Param desc formData integer false "描述" +// @Param price formData integer false "积分价格" +// @Param status formData boolean false "积分状态" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/integral_config [put] +func Update(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var in integralconfigmod.EditCond + if err = c.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("Update integralconfigser param err%v\n", err)) + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + + if err = integralconfigser.Edit(&in, manager); err != nil { + log.Error(fmt.Sprintf("Update integralconfigser err%v\n", err)) + common.ServeJSON(c, stderr.Failure, nil) + return + } + updateLog, _ := json.Marshal(in) + _ = operatorlgmod.RecordOperation(manager, constant.IntegralConfig, constant.Modify, string(updateLog), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, stderr.Success.Msg()) +} + +// Add doc +// @Summary 新增积分配置 +// @Description 新增积分配置 +// @Tags 积分配置 +// @Accept mpfd,json +// @Produce json,html +// @Param name formData string true "积分名称" +// @Param duration formData integer true "积分持续天数" +// @Param desc formData integer false "描述" +// @Param price formData integer true "积分价格" +// @Param sortCode formData integer true "排序码" +// @Param status formData boolean true "积分状态" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/integral_config [post] +func Add(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var in integralconfigmod.AddCond + if err = c.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("Add integralconfigser param err%v\n", err)) + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + + if code := integralconfigser.Add(&in, manager); code != stderr.Success { + log.Error(fmt.Sprintf("Add integralconfigser err%v\n", code)) + common.ServeJSON(c, code, nil) + return + } + updateLog, _ := json.Marshal(in) + _ = operatorlgmod.RecordOperation(manager, constant.IntegralConfig, constant.Add, string(updateLog), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, stderr.Success.Msg()) +} diff --git a/web/api/integral_exchange_ctrl/integarl_exchange.go b/web/api/integral_exchange_ctrl/integarl_exchange.go new file mode 100644 index 0000000..c73304c --- /dev/null +++ b/web/api/integral_exchange_ctrl/integarl_exchange.go @@ -0,0 +1,81 @@ +package integral_exchange_ctrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/integralexcangemod" + "91porn-server/web/service/integarl_exchange_ser" + "encoding/json" + "fmt" + + "github.com/gin-gonic/gin" +) + +// QueryAll doc +// @Summary 查询兑换列表 +// @Description 查询兑换列表 +// @Tags 积分兑换列表 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData primitive.ObjectID false "积分兑换id" +// @Param status formData bool false "状态" +// @Param uid formData integer false "用户ID" +// @Param pageNumber formData integer true "当前页面" +// @Param pageSize formData integer true "页码" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/integral_exchange [get] +func QueryAll(c *gin.Context) { + var in integralexcangemod.WebListRequest + if err := c.ShouldBindQuery(&in); err != nil { + log.Error(fmt.Sprintf("Query all integralexchangeser param err%v\n", err)) + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + + data, err := integarl_exchange_ser.QueryAll(&in) + if err != nil { + log.Error(fmt.Sprintf("Query all integralexchangeser err%v\n%d", data, err)) + common.ServeJSON(c, stderr.Failure, nil) + return + } + common.ServeJSON(c, stderr.Success, data) +} + +// Update doc +// @Summary 修改兑换列表 +// @Description 修改兑换列表 +// @Tags 积分兑换列表 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData primitive.ObjectID true "文档id" +// @Param remark formData string false "拒绝理由" +// @Param status formData integer false "状态 2、发货中 3、已发货 4、拒绝发货" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/integral_exchange [put] +func Update(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var in integralexcangemod.EditCond + if err = c.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("Update integralexchangeser param err%v\n", err)) + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + + if err = integarl_exchange_ser.Edit(&in, manager); err != nil { + log.Error(fmt.Sprintf("Update integralexchangeser err:%v\n", err)) + common.ServeJSON(c, stderr.Failure, nil) + return + } + updateLog, _ := json.Marshal(in) + _ = operatorlgmod.RecordOperation(manager, constant.IntegralExchange, constant.Modify, string(updateLog), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, stderr.Success.Msg()) +} diff --git a/web/api/ipblockctrl/ipblock.go b/web/api/ipblockctrl/ipblock.go new file mode 100644 index 0000000..7aad38f --- /dev/null +++ b/web/api/ipblockctrl/ipblock.go @@ -0,0 +1,169 @@ +package ipblockctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/ipblockmod" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// IPBlockAdd doc +// @Summary 添加IP限制 类型 +// @Description 添加IP限制 +// @Tags WEB-IPBlock IP限制 +// @Accept mpfd,json +// @Produce json,html +// @Param ip formData array true "ip" +// @Param type formData string true "限制的类型" +// @Param remark formData string false "备注" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/ipblock/add [post] +func IPBlockAdd(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + p := ipblockmod.IPBlock{} + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = ipblockmod.Insert(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + common.Go(func() { + //加入到redis中 + _, _ = webg.Redis.SAdd(redisconst.IPBlockKey(string(p.Type)), p.IP) + _, _ = webg.Redis.ExpireKey(redisconst.IPBlockKey(string(p.Type)), redisconst.IPBlockExpire) + }) + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.IPBlockList, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// IPBlockList doc +// @Summary ip限制列表 +// @Description ip限制列表 +// @Tags WEB-IPBlock IP限制 +// @Accept mpfd,json +// @Produce json,html +// @Param type query string false "限制类型" +// @Param pageNumber query integer true "当前页" mininum(1) +// @Param pageSize query integer true "每页条数" mininum(1) +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/ipblock/list [get] +func IpBlockList(ctx *gin.Context) { + param := ipblockmod.ListParam{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web ipblock List arg error "+err.Error()) + return + } + result, err := ipblockmod.List(param) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, result) +} + +// IPBlockDel doc +// @Summary ip限制删除 +// @Description 删除ip限制 +// @Tags WEB-IPBlock IP限制 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "id字符串" +// @Param ip formData string true "ip地址" +// @Param type formData string true "操作类型" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/ipblock/del [delete] +func IPBlockDel(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + type Param struct { + ID primitive.ObjectID `form:"id" json:"id" binding:"required"` + Type string `form:"type" json:"type" binding:"required"` + IP string `form:"ip" json:"ip" binding:"required"` + } + var args struct { + Params []Param `form:"params" json:"params" binding:"required"` + } + if err = ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + objIds := make([]primitive.ObjectID, len(args.Params)) + for i, v := range args.Params { + objIds[i] = v.ID + } + if err = ipblockmod.DeleteMany(objIds); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + common.Go(func() { + for _, v := range args.Params { + _, _ = webg.Redis.SRem(redisconst.IPBlockKey(v.Type), v.IP) + } + }) + log, _ := json.Marshal(args) + _ = operatorlgmod.RecordOperation(manager, constant.IPBlockList, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// IPBlockEdit doc +// @Summary ip限制修改 +// @Description IP限制修改 +// @Tags WEB-IPBlock IP限制 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "记录id" +// @Param ip formData string false "ip" +// @Param type formData string true "限制的类型" +// @Param remark formData string false "备注" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/ipblock/edit [post] +func IPBlockEdit(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var args struct { + ID primitive.ObjectID `form:"id" json:"id" binding:"required"` + ipblockmod.IPBlockEdit + } + if err = ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = ipblockmod.Update(args.ID, args.IPBlockEdit); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, "") + return + } + if args.IP != nil { + common.Go(func() { + _, _ = webg.Redis.SAdd(redisconst.IPBlockKey(string(*args.Type)), *args.IP) + _, _ = webg.Redis.ExpireKey(redisconst.IPBlockKey(string(*args.Type)), redisconst.IPBlockExpire) + }) + } + log, _ := json.Marshal(args) + _ = operatorlgmod.RecordOperation(manager, constant.IPBlockList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/ipwhitectrl/ipwhite.go b/web/api/ipwhitectrl/ipwhite.go new file mode 100644 index 0000000..63c6869 --- /dev/null +++ b/web/api/ipwhitectrl/ipwhite.go @@ -0,0 +1,169 @@ +package ipwhitectrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/ipwhitemod" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +// IPWhiteAdd doc +// @Summary 添加IP白名单 +// @Description 添加IP白名单 +// @Tags WEB-IPBlock IP白名单 +// @Accept mpfd,json +// @Produce json,html +// @Param ip formData array true "ip" +// @Param remark formData string false "备注" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/ipwhite/add [post] +func IPWhiteAdd(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + p := ipwhitemod.IPWhite{} + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + p.CreateUser = manager + if err = ipwhitemod.Insert(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + common.Go(func() { + //加入到redis中 + _, _ = webg.Redis.SAdd(constant.IPWhiteRedisKey, p.IP) + }) + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.IPWhiteList, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// IPWhiteList doc +// @Summary ip白名单列表 +// @Description ip白名单列表 +// @Tags WEB-IPBlock IP白名单 +// @Accept mpfd,json +// @Produce json,html +// @Param type query string false "限制类型" +// @Param pageNumber query integer true "当前页" mininum(1) +// @Param pageSize query integer true "每页条数" mininum(1) +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/ipwhite/list [get] +func IpWhiteList(ctx *gin.Context) { + param := ipwhitemod.ListParam{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web ipwhite List arg error "+err.Error()) + return + } + result, err := ipwhitemod.List(param) + if err != nil && err != mongo.ErrNoDocuments { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, result) +} + +// IPWhiteDel doc +// @Summary ip白名单删除 +// @Description 删除ip白名单 +// @Tags WEB-IPWhite IP白名单 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "id字符串" +// @Param ip formData string true "ip地址" +// @Param type formData string true "操作类型" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/ipwhite/del [delete] +func IPWhiteDel(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + type Param struct { + ID primitive.ObjectID `form:"id" json:"id" binding:"required"` + IP string `form:"ip" json:"ip"` + } + var args struct { + Params []Param `form:"params" json:"params" binding:"required"` + } + if err = ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + objIds := make([]primitive.ObjectID, len(args.Params)) + for i, v := range args.Params { + objIds[i] = v.ID + } + filter := bson.M{"_id": bson.M{"$in": objIds}} + ipWhiteList, _ := ipwhitemod.FindMany(filter) + if err = ipwhitemod.DeleteMany(objIds); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + common.Go(func() { + for _, v := range ipWhiteList { + _, _ = webg.Redis.SRem(constant.IPWhiteRedisKey, v.IP) + } + }) + log, _ := json.Marshal(args) + _ = operatorlgmod.RecordOperation(manager, constant.IPWhiteList, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// IPWhiteEdit doc +// @Summary ip白名单修改 +// @Description IP白名单修改 +// @Tags WEB-IPWhite IP白名单 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "记录id" +// @Param ip formData string false "ip" +// @Param type formData string true "限制的类型" +// @Param remark formData string false "备注" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/ipwhite/edit [post] +func IPWhiteEdit(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var args struct { + ID primitive.ObjectID `form:"id" json:"id" binding:"required"` + ipwhitemod.IPWhiteEdit + } + if err = ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = ipwhitemod.Update(args.ID, args.IPWhiteEdit); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, "") + return + } + if args.IP != nil { + common.Go(func() { + _, _ = webg.Redis.SAdd(constant.IPWhiteRedisKey, *args.IP) + }) + } + log, _ := json.Marshal(args) + _ = operatorlgmod.RecordOperation(manager, constant.IPWhiteList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/jingangctrl/jingangcttrl.go b/web/api/jingangctrl/jingangcttrl.go new file mode 100644 index 0000000..e524c6e --- /dev/null +++ b/web/api/jingangctrl/jingangcttrl.go @@ -0,0 +1,158 @@ +package jingangctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/jingangmod" + "91porn-server/web/service/jingangser" + "github.com/gin-gonic/gin" +) + +// List 金刚区 doc +// @Summary 金刚区列表详情 +// @Description 金刚区列表详情 +// @Tags vipzz +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/jingang/list [get] +func List(c *gin.Context) { + var req jingangser.JingangListRequest + if err := c.Bind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + skip := (req.PageNum - 1) * req.PageSize + count, err := jingangmod.GetAllJingangCount(nil) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + if uint64(count) <= skip { + common.ServeJSON(c, stderr.Success, struct { + List interface{} `json:"list"` + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + }{ + List: nil, + Total: count, + HasNext: false, + }) + return + } + limit := req.PageSize + 1 + jingangList, err := jingangmod.GetAllJingangList(nil, skip, limit) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + hasNext := false + if uint64(len(jingangList)) > req.PageSize { + jingangList = jingangList[:req.PageSize] + hasNext = true + } + common.ServeJSON(c, stderr.Success, struct { + List interface{} `json:"list"` + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + }{ + List: jingangList, + Total: count, + HasNext: hasNext, + }) +} + +// Update 金刚区 doc +// @Summary 更新金刚区区块 +// @Description 更新金刚区区块 +// @Tags vipzz +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "被更新的金刚区块id 必需参数" +// @Param name formData string true "金刚区块名称" +// @Param img formData string true "图片地址" +// @Param desc formData string true "说明" +// @Param link_type formData string true "1 内部链接; 2 外部链接" +// @Param link_url formData string true "链接地址" +// @Param sort formData integer true " 排序" +// @Param status formData integer true " 1 正常; 2 禁用" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/jingang/update [post] +func Update(c *gin.Context) { + var r jingangser.UpdateJingangReq + if err := c.Bind(&r); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + if r.ID.IsZero() { + common.ServeJSON(c, stderr.ErrParamError, "empty id") + return + } + if err := jingangser.UpdateJingang(r); err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(c, stderr.Success, nil) +} + +// Add 金刚区 doc +// @Summary 金刚区 添加金刚区区块 +// @Description 金刚区 添加金刚区区块 +// @Tags vipzz +// @Accept mpfd,json +// @Produce json,html +// @Param name formData string true "金刚区块名称" +// @Param img formData string true "图片地址" +// @Param desc formData string true "说明" +// @Param link_type formData string true "1 内部链接; 2 外部链接" +// @Param link_url formData string true "链接地址" +// @Param sort formData integer true " 排序" +// @Param status formData integer true " 1 正常; 2 禁用" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/jingang/add [post] +func Add(c *gin.Context) { + var r jingangser.AddJingangReq + if err := c.Bind(&r); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + if r.Name == "" { + common.ServeJSON(c, stderr.ErrParamError, "empty name") + return + } + if _, err := jingangser.AddJingang(r); err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(c, stderr.Success, nil) +} + +// Delete 金刚区 doc +// @Summary 金刚区 删除金刚区块 +// @Description 金刚区 删除金刚区块 +// @Tags vipzz +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "Id必需参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/jingang/delete [post] +func Delete(c *gin.Context) { + var r jingangser.DeleteJingangReq + if err := c.Bind(&r); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + if r.ID.IsZero() { + common.ServeJSON(c, stderr.ErrParamError, "empty id") + return + } + if err := jingangser.DeleteJingang(r); err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(c, stderr.Success, nil) +} diff --git a/web/api/laosijictrl/comics.go b/web/api/laosijictrl/comics.go new file mode 100644 index 0000000..7460917 --- /dev/null +++ b/web/api/laosijictrl/comics.go @@ -0,0 +1,83 @@ +package laosijictrl + +import ( + "91porn-server/common" + "91porn-server/common/laosiji" + "91porn-server/common/stderr" + "91porn-server/web/service/laosijiser" + "github.com/gin-gonic/gin" +) + +// @Tags 老司机 +// @Summary 查询漫画列表 +// @Description 查询漫画列表 +// @Accept json +// @Produce json +// @Param param body laosiji.ComicsSearchListReq true "参数列表" +// @Success 200 {object} laosiji.ComicsSearchListResp "成功" +// @Failure 400 {string} string "失败" +// @Router /api/web/admin/laosiji/comics/search [POST] +func ComicsSearch(ctx *gin.Context) { + param := laosiji.ComicsSearchListReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, err := laosijiser.ComicsSearch(ctx, param) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// @Tags 老司机 +// @Summary 查询漫画详情 +// @Description 查询漫画详情 +// @Accept json +// @Produce json +// @Param param body laosiji.ComicsDetailReq true "参数列表" +// @Success 200 {object} laosiji.ComicsDetailResp "成功" +// @Failure 400 {string} string "失败" +// @Router /api/web/admin/laosiji/comics/detail [POST] +func ComicsDetail(ctx *gin.Context) { + param := laosiji.ComicsDetailReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, err := laosijiser.ComicsDetail(ctx, param) + if err != nil { + common.ServeJSON(ctx, stderr.ErrInterServerError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// @Tags 老司机 +// @Summary 添加漫画到本地 +// @Description 添加漫画到本地 +// @Accept json +// @Produce json +// @Param param body laosiji.ComicsSyncReq true "参数列表" +// @Success 200 {string} string "成功" +// @Failure 400 {string} string "失败" +// @Router /api/web/admin/laosiji/comics/add [POST] +func ComicsAdd(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + param := laosiji.ComicsSyncReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + hits, err := laosijiser.ComicsAddList(ctx, param, manager) + if err != nil { + common.ServeJSON(ctx, stderr.ErrInterServerError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, buildAddResp(hits)) +} diff --git a/web/api/laosijictrl/movie.go b/web/api/laosijictrl/movie.go new file mode 100644 index 0000000..5d2e641 --- /dev/null +++ b/web/api/laosijictrl/movie.go @@ -0,0 +1,97 @@ +package laosijictrl + +import ( + "91porn-server/common" + "91porn-server/common/laosiji" + "91porn-server/common/stderr" + "91porn-server/web/service/laosijiser" + "91porn-server/web/service/vidser" + + "github.com/gin-gonic/gin" +) + +// @Tags 老司机 +// @Summary 查询影片列表 +// @Description 查询影片列表 +// @Accept json +// @Produce json +// @Param param body laosiji.MovieSearchReq true "参数列表" +// @Success 200 {object} laosiji.MovieSearchResp "成功" +// @Failure 400 {string} string "失败" +// @Router /api/web/admin/laosiji/movie/search [POST] +func MovieSearch(ctx *gin.Context) { + param := laosiji.MovieSearchReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, err := laosijiser.MovieSearch(ctx, param) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// @Tags 老司机 +// @Summary 查询影片详情 +// @Description 查询影片详情 +// @Accept json +// @Produce json +// @Param param body laosiji.MovieDetailReq true "参数列表" +// @Success 200 {object} laosiji.MovieDetailResp "成功" +// @Failure 400 {string} string "失败" +// @Router /api/web/admin/laosiji/movie/detail [POST] +func MovieDetail(ctx *gin.Context) { + param := laosiji.MovieDetailReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, err := laosijiser.MovieDetail(ctx, param) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// @Tags 老司机 +// @Summary 添加影片到本地 +// @Description 添加影片到本地 +// @Accept json +// @Produce json +// @Param param body laosiji.MovieAddListReq true "参数列表" +// @Success 200 {string} string "成功" +// @Failure 400 {string} string "失败" +// @Router /api/web/admin/laosiji/movie/add [POST] +func MovieAdd(ctx *gin.Context) { + param := laosiji.MovieAddListReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + hits, err := laosijiser.MovieAddList(ctx, param, manager) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, buildAddResp(hits)) +} + +// buildAddResp 老司机 *Add 接口统一应答结构:data 中带 sensitiveHits 与提示文案 +// hits 为空时也返回,便于前端固定字段读取 +func buildAddResp(hits []vidser.SensitiveHit) gin.H { + resp := gin.H{ + "sensitiveHits": hits, + } + if len(hits) > 0 { + resp["sensitiveTip"] = vidser.SensitiveForcedOfflineTip + } + return resp +} diff --git a/web/api/laosijictrl/novel.go b/web/api/laosijictrl/novel.go new file mode 100644 index 0000000..aa2c9e7 --- /dev/null +++ b/web/api/laosijictrl/novel.go @@ -0,0 +1,83 @@ +package laosijictrl + +import ( + "91porn-server/common" + "91porn-server/common/laosiji" + "91porn-server/common/stderr" + "91porn-server/web/service/laosijiser" + "github.com/gin-gonic/gin" +) + +// @Tags 老司机 +// @Summary 查询小说列表 +// @Description 查询小说列表 +// @Accept json +// @Produce json +// @Param param body laosiji.NovelSearchListReq true "参数列表" +// @Success 200 {object} laosiji.NovelSearchListResp "成功" +// @Failure 400 {string} string "失败" +// @Router /api/web/admin/laosiji/novel/search [POST] +func NovelSearch(ctx *gin.Context) { + param := laosiji.NovelSearchListReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, err := laosijiser.NovelSearch(ctx, param) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// @Tags 老司机 +// @Summary 查询小说详情 +// @Description 查询小说详情 +// @Accept json +// @Produce json +// @Param param body laosiji.NovelDetailReq true "参数列表" +// @Success 200 {object} laosiji.NovelDetailResp "成功" +// @Failure 400 {string} string "失败" +// @Router /api/web/admin/laosiji/novel/detail [POST] +func NovelDetail(ctx *gin.Context) { + param := laosiji.NovelDetailReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, err := laosijiser.NovelDetail(ctx, param) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// @Tags 老司机 +// @Summary 添加小说到本地 +// @Description 添加小说到本地 +// @Accept json +// @Produce json +// @Param param body laosiji.NovelAddListReq true "参数列表" +// @Success 200 {string} string "成功" +// @Failure 400 {string} string "失败" +// @Router /api/web/admin/laosiji/novel/add [POST] +func NovelAdd(ctx *gin.Context) { + param := laosiji.NovelAddListReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + err = laosijiser.NovelAddList(ctx, param, manager) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/laosijictrl/post.go b/web/api/laosijictrl/post.go new file mode 100644 index 0000000..da85492 --- /dev/null +++ b/web/api/laosijictrl/post.go @@ -0,0 +1,83 @@ +package laosijictrl + +import ( + "91porn-server/common" + "91porn-server/common/laosiji" + "91porn-server/common/stderr" + "91porn-server/web/service/laosijiser" + "github.com/gin-gonic/gin" +) + +// @Tags 老司机 +// @Summary 查询帖子列表 +// @Description 查询帖子列表 +// @Accept json +// @Produce json +// @Param param body laosiji.PostSearchListReq true "参数列表" +// @Success 200 {object} laosiji.PostSearchListResp "成功" +// @Failure 400 {string} string "失败" +// @Router /api/web/admin/laosiji/post/search [POST] +func PostSearch(ctx *gin.Context) { + param := laosiji.PostSearchListReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, err := laosijiser.PostSearch(ctx, param) + if err != nil { + common.ServeJSON(ctx, stderr.ErrInterServerError, "") + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// @Tags 老司机 +// @Summary 查询帖子详情 +// @Description 查询帖子详情 +// @Accept json +// @Produce json +// @Param param body laosiji.PostDetailReq true "参数列表" +// @Success 200 {object} laosiji.PostDetailResp "成功" +// @Failure 400 {string} string "失败" +// @Router /api/web/admin/laosiji/post/detail [POST] +func PostDetail(ctx *gin.Context) { + param := laosiji.PostDetailReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, err := laosijiser.PostDetail(ctx, param) + if err != nil { + common.ServeJSON(ctx, stderr.ErrInterServerError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// @Tags 老司机 +// @Summary 添加帖子到本地 +// @Description 添加帖子到本地 +// @Accept json +// @Produce json +// @Param param body laosiji.PostAddListReq true "参数列表" +// @Success 200 {string} string "成功" +// @Failure 400 {string} string "失败" +// @Router /api/web/admin/laosiji/post/add [POST] +func PostAdd(ctx *gin.Context) { + param := laosiji.PostAddListReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + hits, err := laosijiser.PostAddList(ctx, param, manager) + if err != nil { + common.ServeJSON(ctx, stderr.ErrInterServerError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, buildAddResp(hits)) +} diff --git a/web/api/ldyctrl/ldyctrl.go b/web/api/ldyctrl/ldyctrl.go new file mode 100644 index 0000000..a565d25 --- /dev/null +++ b/web/api/ldyctrl/ldyctrl.go @@ -0,0 +1,54 @@ +package ldyctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/ldycfgmod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetCfg doc +// @Summary 获取落地页配置 +// @Description 获取落地页配置 +// @Tags 落地页 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} ldycfgmod.LdyCfg "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ldy/cfg [get] +func GetCfg(ctx *gin.Context) { + cfg := ldycfgmod.FindOne() + common.ServeJSON(ctx, stderr.Success, cfg) +} + +// Update doc +// @Summary 更新落地页配置 +// @Description 更新落地页配置 +// @Tags 落地页 +// @Accept mpfd,json +// @Produce json,html +// @Param param body ldycfgmod.LdyCfg true "参数列表" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/ldy/update [post] +func Update(c *gin.Context) { + var arg struct { + ID primitive.ObjectID `json:"id" binding:"required"` + ldycfgmod.LdyCfgUpdateDoc + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "ldyctrl Update arg error: "+err.Error()) + return + } + if arg.ID.IsZero() { + common.ServeJSON(c, stderr.ErrParamError, "ldyctrl Update ID is zero") + return + } + if err := ldycfgmod.UpsertOneByID(arg.ID, arg.LdyCfgUpdateDoc); err != nil { + common.ServeJSON(c, stderr.Failure, "ldyctrl Update failed: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, "") +} diff --git a/web/api/locationctrl/city.go b/web/api/locationctrl/city.go new file mode 100644 index 0000000..5c0c4f1 --- /dev/null +++ b/web/api/locationctrl/city.go @@ -0,0 +1,92 @@ +package locationctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/locmod" + "91porn-server/web/service/locationser" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// CityList doc +// @Summary 获取城市列表 +// @Description 获取城市列表 +// @Tags web-视频管理 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query int true "当前页" +// @Param pageSize query int true "每页条数" +// @Param city query string false "过滤条件:城市" +// @Success 200 {string} json "{"msg": "操作成功", "date": { "total":10, "list":[] }}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/location/city/list [get] +func CityList(c *gin.Context) { + req := locmod.CityPageReq{} + if err := c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "hotCity city List arg error "+err.Error()) + return + } + page, err := locationser.CityPages(req.City, req.PageNumber, req.PageSize) + if err != nil { + common.ServeJSON(c, stderr.Failure, "city List error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} + +// CityUpdate doc +// @Summary 编辑城市 +// @Description 编辑城市 +// @Tags web-视频管理 +// @Accept mpfd,json +// @Produce json,html +// @Param targetID formData string true "目标文档ID" +// @Param cover formData string false "封面"" +// @Param fakeVisit formData int false "假播访问量" +// @Param sortKey formData int false "排序键" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/location/city/update [post] +func CityUpdate(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + type Update struct { + Cover *string `form:"cover" json:"cover" binding:""` //封面 + FakeVisit *int `form:"fakeVisit" json:"fakeVisit" binding:""` //播发假数据 + SortKey *int `form:"sortKey" json:"sortKey" binding:""` + } + var arg struct { + TargetID string `form:"targetID" json:"targetID,omitempty" binding:"required"` + Update + } + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "hotCity Update arg error "+err.Error()) + return + } + doc := locmod.LocationDoc{ + Cover: arg.Cover, + FakeVisit: arg.FakeVisit, + SortKey: arg.SortKey, + } + targetID, err := primitive.ObjectIDFromHex(arg.TargetID) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if err = locmod.UpdateToLocation(targetID, doc); err != nil { + common.ServeJSON(c, stderr.ErrDbUpdateError, err) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageCity, constant.Modify, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} diff --git a/web/api/locationctrl/hotCity.go b/web/api/locationctrl/hotCity.go new file mode 100644 index 0000000..702c488 --- /dev/null +++ b/web/api/locationctrl/hotCity.go @@ -0,0 +1,186 @@ +package locationctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/pageopt" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/locmod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// HotCityList doc +// @Summary 获取热门城市列表 +// @Description 获取热门城市列表 +// @Tags web-视频管理-HotCity +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query int true "当前页" +// @Param pageSize query int true "每页条数" +// @Param city query string false "过滤条件:城市" +// @Success 200 {string} json "{"msg": "操作成功", "date": { "total":10, "list":[] }}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/location/hotCity/list [get] +func HotCityList(c *gin.Context) { + type Query struct { + City *string `form:"city" json:"city" bson:"city,omitempty"` //角色类型 + } + var arg struct { + commod.Page + Query + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "hotCity List arg error "+err.Error()) + return + } + match := &pageopt.AssignMatch{ + Key: "city", + Val: arg.City, + } + sort := bson.D{{Key: "createdAt", Value: -1}} + skip := int64((arg.PageNumber - 1) * arg.PageSize) + limit := int64(arg.PageSize) + page, err := locmod.HotCityPages(sort, skip, limit, match) + if err != nil { + common.ServeJSON(c, stderr.Failure, "hotCity List error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} + +// HotCityUpdate doc +// @Summary 编辑热门城市 +// @Description 编辑热门城市 +// @Tags web-视频管理-HotCity +// @Accept mpfd,json +// @Produce json,html +// @Param targetID formData string true "目标文档ID" +// @Param sortKey formData int false "排序键" +// @Param enable formData bool false "使能" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/location/hotCity/update [post] +func HotCityUpdate(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + type Update struct { + SortKey *int `form:"sortKey" json:"sortKey" binding:""` + Enable *bool `form:"enable" json:"enable" binding:""` + Province *string `form:"province" json:"province" binding:""` + } + var arg struct { + TargetID string `form:"targetID" json:"targetID,omitempty" binding:"required"` + Update + } + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "hotCity Update arg error "+err.Error()) + return + } + cityDoc := locmod.HotCityDoc{ + SortKey: arg.SortKey, + Enable: arg.Enable, + Province: arg.Province, + } + targetID, err := primitive.ObjectIDFromHex(arg.TargetID) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if err = locmod.UpdateTo(targetID, cityDoc); err != nil { + common.ServeJSON(c, stderr.ErrDbUpdateError, err) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageHotCity, constant.Modify, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// HotCityAdd doc +// @Summary 新增热门城市 +// @Description 新增热门城市 +// @Tags web-视频管理-HotCity +// @Accept mpfd,json +// @Produce json,html +// @Param city formData string true "城市" +// @Param sortKey formData int true "排序" +// @Param enable formData bool true "使能" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/location/hotCity/add [post] +func HotCityAdd(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + City *string `form:"city" json:"city" binding:"required"` //城市 + Province *string `form:"province" json:"province" binding:"required"` //省份 + SortKey *int `form:"sortKey" json:"sortKey" binding:"required"` //排序 + Enable *bool `form:"enable" json:"enable" binding:"required"` //使能 + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "tone Add arg error "+err.Error()) + return + } + doc := locmod.HotCityDoc{ + City: arg.City, + Province: arg.Province, + SortKey: arg.SortKey, + Enable: arg.Enable, + } + if err = locmod.Insert(doc); err != nil { + common.ServeJSON(c, stderr.ErrDbInsertError, err) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageHotCity, constant.Add, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// HotCityDelete doc +// @Summary 批量删除热门城市 +// @Description 批量删除热门城市 +// @Tags web-视频管理-HotCity +// @Accept mpfd,json +// @Produce json,html +// @Param idArray formData array true "ID数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/location/hotCity/delete [delete] +func HotCityDelete(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + IDArray []string `form:"idArray" json:"idArray" binding:"required"` + } + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "hotCity Delete arg error "+err.Error()) + return + } + idArray, err := common.IDArray(arg.IDArray) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if err = locmod.DeleteMany(idArray); err != nil { + common.ServeJSON(c, stderr.ErrDbDeleteError, err) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageHotCity, constant.Delete, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} diff --git a/web/api/logctrl/log.go b/web/api/logctrl/log.go new file mode 100644 index 0000000..5cf5c3a --- /dev/null +++ b/web/api/logctrl/log.go @@ -0,0 +1,154 @@ +package logctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/adminmod" + "91porn-server/models/v/txnmod" + "91porn-server/web/service/logser" + + "github.com/gin-gonic/gin" +) + +func accountTransfer2Nick(infos []*operatorlgmod.WebOperatorLog) []*operatorlgmod.WebOperatorLog { + accounts := make([]string, len(infos)) + for i, v := range infos { + accounts[i] = v.Manager + } + mNick, err := adminmod.GetNickByAccount(accounts) + if err != nil { + return infos + } + for _, v := range infos { + v.Manager = mNick[v.Manager] + } + return infos +} + +// GetRechargeGoldLog doc +// @Summary 获取用户金币日志 +// @Description 获取用户金币日志 +// @Tags log +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/log/gold [get] +func GetRechargeGoldLog(ctx *gin.Context) { + req := txnmod.CoinLogReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + m := make(map[string]interface{}) + if req.UID > 0 { + m["uid"] = req.UID + } + if req.Balance == 1 { + i := make(map[string]interface{}) + i["$gt"] = 0 + m["amount"] = i + } + if req.Balance == 2 { + i := make(map[string]interface{}) + i["$lt"] = 0 + m["amount"] = i + } + if !req.End.IsZero() { + i := make(map[string]interface{}) + i["$gte"] = req.Start + i["$lt"] = req.End + m["createdAt"] = i + } + if len(req.TranType) != 0 { + m["tranType"] = req.TranType + } + if req.DistrictCode != "" { + m["districtCode"] = req.DistrictCode + } + code, data := logser.GetRechargeGoldLog(m, req.PageNumber, req.PageSize) + common.ServeJSON(ctx, code, data) +} + +// GetRechargeGoldTranType doc +// @Summary 获取用户金币类型 +// @Description获取用户金币类型 +// @Tags log +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/log/goldType [get] +func GetRechargeGoldTranType(ctx *gin.Context) { + data, err := txnmod.GetTranTypes() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"list": data}) +} + +// GetOperatorLog doc +// @Summary 获取后台操作日志 +// @Description 获取后台操作日志 +// @Tags log +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/log/operator [get] +func GetOperatorLog(ctx *gin.Context) { + if _, err := common.GetAdminAct(ctx); err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := operatorlgmod.OperatorListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if len(req.Manager) != 0 { + account, err := adminmod.GetNameByNick(req.Manager) + if err != nil { + req.Manager = "" + } else { + req.Manager = account + } + } + infos, total, err := operatorlgmod.GetOperatorLog(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + infos = accountTransfer2Nick(infos) + common.ServeJSON(ctx, stderr.Success, operatorlgmod.OperatorListResp{Infos: infos, Total: total}) +} + +// ModifyOperatorLog doc +// @Summary 修改后台操作日志 +// @Description 修改后台操作日志 +// @Tags log +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/log/operator/modify [post] +func ModifyOperatorLog(ctx *gin.Context) { + req := operatorlgmod.OperatorModifyReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err := operatorlgmod.EditRemarks(req.ID, req.Remarks); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, "") + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/media_resource/media_resource.go b/web/api/media_resource/media_resource.go new file mode 100644 index 0000000..9f3b03e --- /dev/null +++ b/web/api/media_resource/media_resource.go @@ -0,0 +1,131 @@ +package media_resource + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/web/service/mediaresourceser" + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取媒资库动漫列表列表 +// @Description 获取媒资库动漫列表列表 +// @Tags 后台-动漫列表-媒体库相关 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaresourceser.MediaListReq false "请求参数" +// @Success 200 object mediaresourceser.MediaListResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/mediaResource/media/list [get] +func List(ctx *gin.Context) { + var req = &mediaresourceser.MediaListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err := req.Validate(); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, res) +} + +// ContentList doc +// @Summary 获取媒资库动漫子集列表列表 +// @Description 获取媒资库动漫子集列表列表 +// @Tags 后台-动漫列表-媒体库相关 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaresourceser.MediaContentListReq false "请求参数" +// @Success 200 object mediaresourceser.MediaContentListResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/mediaResource/media/content_list [get] +func ContentList(ctx *gin.Context) { + var req = &mediaresourceser.MediaContentListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err := req.Validate(); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, res) +} + +// UseMedia doc +// @Summary 使用媒资库动漫 +// @Description 使用媒资库动漫 +// @Tags 后台-动漫列表-媒体库相关 +// @Accept mpfd,json +// @Produce json,html +// @Param q body mediaresourceser.MediaUseReq false "请求参数" +// @Router /api/web/admin/mediaResource/media/use [post] +func UseMedia(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + return + } + var p mediaresourceser.MediaUseReq + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err = p.Validate(); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = p.Use(manager); err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, "") +} + +// IgnoreMedia doc +// @Summary 忽略媒资库动漫 +// @Description 忽略媒资库动漫 +// @Tags 后台-动漫列表-媒体库相关 +// @Accept mpfd,json +// @Produce json,html +// @Param q body mediaresourceser.MediaIgnoreReq false "请求参数" +// @Router /api/web/admin/mediaResource/media/ignore [post] +func IgnoreMedia(ctx *gin.Context) { + var p mediaresourceser.MediaIgnoreReq + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err := p.Ignore(); err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, "") +} + +// SyncMedia doc +// @Summary 同步媒资库动漫 +// @Description 同步媒资库动漫 +// @Tags 后台-动漫列表-媒体库相关 +// @Accept mpfd,json +// @Produce json,html +// @Router /api/web/admin/mediaResource/media/sync [post] +func SyncMedia(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + return + } + p := &mediaresourceser.MediaSyncReq{} + p.Sync(manager) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/mediacontentctrl/mediacontent.go b/web/api/mediacontentctrl/mediacontent.go new file mode 100755 index 0000000..e9edb78 --- /dev/null +++ b/web/api/mediacontentctrl/mediacontent.go @@ -0,0 +1,236 @@ +package mediacontentctrl + +import ( + "91porn-server/common/log" + "91porn-server/models/v/mediamod" + "91porn-server/web/service/mediaser" + "encoding/json" + "fmt" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/mediacontentser" + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取动漫内容列表列表 +// @Description 获取动漫内容列表列表 +// @Tags 后台-动漫内容列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediacontentser.WebListReq false "请求参数" +// @Success 200 object mediacontentser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media_content/list [get] +func List(ctx *gin.Context) { + var req = &mediacontentser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Create doc +// @Summary 新增动漫内容列表 +// @Description 新增动漫内容列表 +// @Tags 后台-动漫内容列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediacontentser.WebCreateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media_content/create [post] +func Create(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &mediacontentser.WebCreateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + log.Error(fmt.Sprintf("Create media content param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if p.EpisodeNumber == 0 { + log.Error("Create media content EpisodeNumber == 0") + common.ServeJSON(ctx, stderr.ErrParamError, "剧集编号必须大于0") + return + } + if p.MediaType == mediamod.MediaTypeDrama { + code := mediaser.SendBaseInfo2AWs(mediamod.SendBaseInfo2AWsReq{ + SourceID: p.SourceID, + Md5: p.Md5, + Title: p.Name, + Filename: p.Filename, + }) + if code != stderr.Success { + common.ServeJSON(ctx, code, "视频资源登记失败") + return + } + } + + // 创建 + err, vid := p.Create(manager) + if err != nil { + log.Error(fmt.Sprintf("Create media content err:%v\n", err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + if p.MediaType == mediamod.MediaTypeVideo { + code := mediaser.SendBaseInfo2AWs(mediamod.SendBaseInfo2AWsReq{ + SourceID: p.SourceID, + Md5: p.Md5, + Title: p.Name, + Filename: p.Filename, + }) + if code != stderr.Success { + _, _ = mediaser.DeleteManyByTTL(vid) + } + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "动漫内容列表管理", "创建", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, gin.H{"contentId": vid}) +} + +// Update doc +// @Summary 更新动漫内容列表 +// @Description 更新动漫内容列表 +// @Tags 后台-动漫内容列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediacontentser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media_content/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &mediacontentser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update(manager) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "动漫内容列表管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// BatchUpdate doc +// @Summary 批量更新动漫内容 +// @Description 批量更新动漫内容 +// @Tags 后台-动漫内容列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediacontentser.WebBatchUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media_content/batch/update [post] +func BatchUpdate(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &mediacontentser.WebBatchUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if _, err = p.ValidatedIDs(); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update(manager) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "动漫内容列表管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除动漫内容列表 +// @Description 删除动漫内容列表 +// @Tags 后台-动漫内容列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediacontentser.WebDeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media_content/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &mediacontentser.WebDeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = p.Delete() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "动漫内容列表管理", "删除", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// ViewMedia doc +// @Summary 查看媒体状态 +// @Description 查看媒体状态 +// @Tags 后台-动漫内容列表 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "查看媒体id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/media/view/:id [get] +func ViewMedia(ctx *gin.Context) { + id := ctx.Param("id") + if id == "" { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := mediacontentser.ViewMediaFromFs(id) + common.ServeJSON(ctx, code, data) +} diff --git a/web/api/mediactrl/media.go b/web/api/mediactrl/media.go new file mode 100755 index 0000000..b3f0961 --- /dev/null +++ b/web/api/mediactrl/media.go @@ -0,0 +1,313 @@ +package mediactrl + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/mediamod" + "91porn-server/web/service/mediaser" + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取动漫列表列表 +// @Description 获取动漫列表列表 +// @Tags 后台-动漫列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaser.WebListReq false "请求参数" +// @Success 200 object mediaser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media/list [get] +func List(ctx *gin.Context) { + var req = &mediaser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("Query media List param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + log.Error(fmt.Sprintf("media GetList err:%v", err)) + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Find doc +// @Summary 获取动漫信息 +// @Description 获取动漫信息 +// @Tags 后台-动漫列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaser.WebFindMediaReq false "请求参数" +// @Success 200 object mediaser.WebFindMediaRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media/find [get] +func Find(ctx *gin.Context) { + var req = &mediaser.WebFindMediaReq{} + if err := ctx.ShouldBind(&req); err != nil { + log.Error(fmt.Sprintf("Query media find param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetMediaByID() + if err != nil { + log.Error(fmt.Sprintf("media find err:%v", err)) + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Create doc +// @Summary 新增动漫列表 +// @Description 新增动漫列表 +// @Tags 后台-动漫列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaser.WebCreateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media/create [post] +func Create(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &mediaser.WebCreateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + log.Error(fmt.Sprintf("media create param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = p.Validate(); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 创建 + code, resultID := p.Create(manager) + if code != stderr.Success { + if p.MediaType == mediamod.MediaTypeVideo { + _, _ = mediaser.DeleteManyByTTL(resultID) + } + log.Error(fmt.Sprintf("media create err:%v\n", err)) + common.ServeJSON(ctx, code, resultID) + return + } + if p.MediaType == mediamod.MediaTypeVideo { + code = mediaser.SendBaseInfo2AWs(mediamod.SendBaseInfo2AWsReq{ + SourceID: p.SourceID, + Md5: p.Md5, + Title: p.EpisodeNumberName, + PlayTime: p.PlayTime, + MediaSize: p.MediaSize, + Filename: p.Filename, + Tags: p.Tags, + }) + if code != stderr.Success { + _, _ = mediaser.DeleteManyByTTL(resultID) + } + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "动漫列表管理", "创建", string(log), ctx.Request.URL.RequestURI()) + if p.MediaType == mediamod.MediaTypeDrama { + common.ServeJSON(ctx, stderr.Success, gin.H{"mediaId": resultID}) + return + } + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 更新动漫列表 +// @Description 更新动漫列表 +// @Tags 后台-动漫列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &mediaser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + log.Error(fmt.Sprintf("media update param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 上架相关时做敏感词校验,命中直接拒绝 + if detail, hit := p.CheckSensitive(); hit { + common.ServeJSON(ctx, stderr.ContentSensitiveHit, detail) + return + } + // 更新 + err = p.Update(manager) + if err != nil { + log.Error(fmt.Sprintf("media update err:%v\n", err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "动漫列表管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// BatchUpdate doc +// @Summary 批量更新动漫列表 +// @Description 批量更新动漫列表 +// @Tags 后台-动漫列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaser.WebBatchUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media/batch/update [post] +func BatchUpdate(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &mediaser.WebBatchUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + log.Error(fmt.Sprintf("media BatchUpdate param err:%v\n", err)) + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if _, err = p.ValidatedIDs(); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 批量上架时做敏感词校验,命中直接拒绝 + if detail, hit := p.CheckSensitive(); hit { + common.ServeJSON(ctx, stderr.ContentSensitiveHit, detail) + return + } + // 更新 + err = p.Update(manager) + if err != nil { + log.Error(fmt.Sprintf("media BatchUpdate err:%v\n", err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "动漫列表管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除动漫列表 +// @Description 删除动漫列表 +// @Tags 后台-动漫列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaser.WebDeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &mediaser.WebDeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = p.Delete() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "动漫列表管理", "删除", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// BatchDelete doc +// @Summary 批量删除动漫列表 +// @Description 批量删除动漫列表 +// @Tags 后台-动漫列表 +// @Accept mpfd,json +// @Produce json +// @Param q body mediaser.WebBatchDeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media/batch/delete [post] +func BatchDelete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &mediaser.WebBatchDeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = p.Delete() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "动漫列表管理", "删除", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Export doc +// @Summary 导出动漫列表 +// @Description 导出动漫列表 +// @Tags 后台-动漫管理 +// @Accept mpfd,json +// @Produce json +// @Param q query mediaser.WebListReq false "请求参数" +// @Success 200 {string} string "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media/export [post] +func Export(ctx *gin.Context) { + var req = &mediaser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + buff, err := req.Export() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + + common.ServeFile(ctx, "动漫列表", common.FileExcel, &buff) +} diff --git a/web/api/mediatagctrl/mediatag.go b/web/api/mediatagctrl/mediatag.go new file mode 100755 index 0000000..47dd7db --- /dev/null +++ b/web/api/mediatagctrl/mediatag.go @@ -0,0 +1,167 @@ +package mediatagctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/mediatagser" + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取动漫标签列表 +// @Description 获取动漫标签列表 +// @Tags 后台-动漫标签 +// @Accept mpfd,json +// @Produce json +// @Param q query mediatagser.WebListReq false "请求参数" +// @Success 200 object mediatagser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media_tag/list [get] +func List(ctx *gin.Context) { + var req = &mediatagser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// AllList doc +// @Summary 获取所有动漫标签列表 +// @Description 获取所有动漫标签列表 +// @Tags 后台-动漫标签 +// @Accept mpfd,json +// @Produce json +// @Param q query mediatagser.WebAllListReq false "请求参数" +// @Success 200 object mediatagser.WebAllListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media_tag/all [get] +func AllList(ctx *gin.Context) { + var req = &mediatagser.WebAllListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetALLList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Create doc +// @Summary 新增动漫标签 +// @Description 新增动漫标签 +// @Tags 后台-动漫标签 +// @Accept mpfd,json +// @Produce json +// @Param q query mediatagser.WebCreateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media_tag/create [post] +func Create(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &mediatagser.WebCreateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 创建 + err = p.Create(manager) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "动漫标签管理", "创建", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 更新动漫标签 +// @Description 更新动漫标签 +// @Tags 后台-动漫标签 +// @Accept mpfd,json +// @Produce json +// @Param q query mediatagser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media_tag/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &mediatagser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update(manager) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "动漫标签管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除动漫标签 +// @Description 删除动漫标签 +// @Tags 后台-动漫标签 +// @Accept mpfd,json +// @Produce json +// @Param q query mediatagser.WebDeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media_tag/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &mediatagser.WebDeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = p.Delete() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "动漫标签管理", "删除", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/mediatagdimensionctrl/mediatagdimension.go b/web/api/mediatagdimensionctrl/mediatagdimension.go new file mode 100755 index 0000000..98927fe --- /dev/null +++ b/web/api/mediatagdimensionctrl/mediatagdimension.go @@ -0,0 +1,167 @@ +package mediatagdimensionctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/mediatagdimensionser" + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取媒体纬度列表列表 +// @Description 获取媒体纬度列表列表 +// @Tags 后台-媒体纬度列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediatagdimensionser.WebListReq false "请求参数" +// @Success 200 object mediatagdimensionser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media_tag_dimension/list [get] +func List(ctx *gin.Context) { + var req = &mediatagdimensionser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// AllList doc +// @Summary 获取所有媒体纬度列表配置 +// @Description 获取所有媒体纬度列表配置 +// @Tags 后台-媒体纬度列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediatagdimensionser.WebAllListReq false "请求参数" +// @Success 200 object mediatagdimensionser.WebAllListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media_tag_dimension/all [get] +func AllList(ctx *gin.Context) { + var req = &mediatagdimensionser.WebAllListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetAllList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Create doc +// @Summary 新增媒体纬度列表 +// @Description 新增媒体纬度列表 +// @Tags 后台-媒体纬度列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediatagdimensionser.WebCreateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media_tag_dimension/create [post] +func Create(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &mediatagdimensionser.WebCreateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 创建 + err = p.Create(manager) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "媒体纬度列表管理", "创建", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 更新媒体纬度列表 +// @Description 更新媒体纬度列表 +// @Tags 后台-媒体纬度列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediatagdimensionser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media_tag_dimension/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &mediatagdimensionser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update(manager) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "媒体纬度列表管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除媒体纬度列表 +// @Description 删除媒体纬度列表 +// @Tags 后台-媒体纬度列表 +// @Accept mpfd,json +// @Produce json +// @Param q query mediatagdimensionser.WebDeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/media_tag_dimension/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &mediatagdimensionser.WebDeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = p.Delete() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "媒体纬度列表管理", "删除", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/modulectrl/moduleconf.go b/web/api/modulectrl/moduleconf.go new file mode 100644 index 0000000..888d8a2 --- /dev/null +++ b/web/api/modulectrl/moduleconf.go @@ -0,0 +1,437 @@ +package modulectrl + +import ( + "91porn-server/common/log" + "encoding/json" + "fmt" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/redis" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/s/sectionstatmod" + "91porn-server/models/v/marqueemod" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/modulesectionmod" + "91porn-server/web/service/moduleser" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Add doc +// @Summary 新增模块 +// @Description 新增模块 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param moduleName body string true "模块名称" +// @Param subModuleName body string true "亚模块名称" +// @Param sectionLimit body integer true "亚模块下专题数量限制,0-不限制" +// @Param status body integer true "状态,1-启用,0-不启用" +// @Param type body integer true "模块类型,1-首页,2-社区" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/conf/add [post] +func Add(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + moduleConf := moduleconfmod.ModuleConf{} + if err := ctx.ShouldBind(&moduleConf); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = moduleser.AddModule(moduleConf); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + clearAppModuleCache() + log, _ := json.Marshal(moduleConf) + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// AddSection doc +// @Summary 新增专题 +// @Description 新增专题 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param sectionName body string true "专题名称" +// @Param subModuleID body string true "专题所属亚模块id" +// @Param originalUserID body integer false "原创博主用户ID" +// @Param status body integer true "状态,1-启用,0-不启用" +// @Param sort body integer true "排序" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/section/add [post] +func AddSection(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + section := modulesectionmod.Section{} + if err = ctx.ShouldBind(§ion); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = moduleser.AddSection(section); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(section) + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// EditSection doc +// @Summary 修改专题 +// @Description 修改专题 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param id body string true "专题id" +// @Param subModuleID body string false "亚模块id" +// @Param sectionName body string false "专题名称" +// @Param originalUserID body integer false "原创播主用户ID" +// @Param status body integer false "状态,1-启用,0-不启用" +// @Param sort body integer false "排序" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/section/edit [post] +func EditSection(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + p := modulesectionmod.EditSelector{} + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = moduleser.UpdateSection(p); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// DeleteSection doc +// @Summary 删除专题 +// @Description 删除专题 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param id body string true "专题id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/section/delete [post] +func DeleteSection(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + ID primitive.ObjectID `json:"id" binding:"required"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = moduleser.DeleteSection(p.ID); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Section, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Edit doc +// @Summary 修改 +// @Description 修改 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param id body string true "模块id" +// @Param moduleName body string true "模块名称" +// @Param subModuleName body string true "亚模块名称" +// @Param sectionLimit body integer true "专题数量限制,0-不限制" +// @Param status body integer false "状态,1-启用,0-不启用" +// @Param sort body integer false "排序" +// @Param excludeSearch body boolean false "亚模块内容不进入搜索列表" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/conf/edit [post] +func Edit(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + p := moduleconfmod.EditSelector{} + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = moduleser.UpdateModule(p); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + clearAppModuleCache() + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除 +// @Description 删除 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param id body string true "模块id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/conf/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + ID primitive.ObjectID `json:"id" binding:"required"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = moduleconfmod.DeleteOne(p.ID); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, "") + return + } + clearAppModuleCache() + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +func clearAppModuleCache() { + if redis.Handler != nil { + _, _ = redis.Handler.Del(redisconst.ModulesCache, redisconst.ContentUpdateMarkersCache) + } +} + +// Search doc +// @Summary 获取模块配置列表 +// @Description 根据条件匹配获取配置列表,模块名称和专题名称进行模糊匹配 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param id body string false "专题id" +// @Param moduleName body string false "模块名称" +// @Param subModuleName body string false "亚模块名称" +// @Param status body integer false "状态,1-启用,0-不启用" +// @Param pageNumber body integer true "当前页码" +// @Param pageSize body integer true "一页数据容量" +// @Success 200 {object} moduleconfmod.ListResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/conf/list [post] +func Search(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + moduleconfmod.QuerySelector + commod.Page + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, err := moduleconfmod.Search(p.QuerySelector, p.Page) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, resp) +} + +// SearchSection doc +// @Summary 检索专题列表 +// @Description 根据条件匹配获取专题列表,专题名称进行模糊匹配 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param id body string true "专题id" +// @Param subModuleID body string true "亚模块id" +// @Param sectionName body string true "专题名称" +// @Param status body integer true "状态,1-启用,0-不启用" +// @Param pageNumber body integer true "当前页码" +// @Param pageSize body integer true "一页数据容量" +// @Success 200 {object} modulesectionmod.ListResp +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/section/list [post] +func SearchSection(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + modulesectionmod.QuerySelector + commod.Page + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, err := moduleser.SectionSearch(p.QuerySelector, p.Page) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, resp) +} + +// AllSections doc +// @Summary 所有专题 +// @Description 返回所有有效专题 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {array} modulesectionmod.ModuleSection +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/section/all [post] +func AllSections(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + resp, err := moduleser.AllSections() + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Modify, "", ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, resp) +} + +// ClicksList doc +// @Summary 获取专题点击量 +// @Description 分页搜索专题每日点击量 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param sectionID body string true "专题ID" +// @Param pageNumber body integer true "页码" +// @Param pageSize body integer true "页容量" +// @Success 200 {object} sectionstatmod.ListResponse "Success" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/section/clicks [post] +func ClicksList(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req sectionstatmod.ListRequest + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, err := sectionstatmod.ListSectionHits(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Modify, "", ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, resp) +} + +// ChangeModuleMarquee doc +// @Summary 修改模块跑马灯 +// @Description 修改模块跑马灯 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param marquee body string true "跑马灯" +// @Success 200 {string} string "Success" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/conf/marquee [post] +func ChangeModuleMarquee(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + Marquee string `json:"marquee" binding:"required"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = marqueemod.Set(p.Marquee); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Modify, "", ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// GetModuleMarquee doc +// @Summary 获取模块跑马灯 +// @Description 获取模块跑马灯 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} marqueemod.Marquee "Success" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/conf/marquee [get] +func GetModuleMarquee(ctx *gin.Context) { + c, err := marqueemod.Get() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, c) +} + +// AllCartoonList doc +// @Summary 获取动漫所有模块配置列表 +// @Description 获取动漫所有模块配置列表 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} modulesectionmod.AllSectionConf +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/conf/cartoon/all [get] +func AllCartoonList(ctx *gin.Context) { + resp, err := moduleser.QueryAllCartoonList() + if err != nil { + log.Error(fmt.Sprintf("Query all module err:%v\n,%d", resp, err)) + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} diff --git a/web/api/modulevideoctrl/modulevideo.go b/web/api/modulevideoctrl/modulevideo.go new file mode 100644 index 0000000..acf3590 --- /dev/null +++ b/web/api/modulevideoctrl/modulevideo.go @@ -0,0 +1,219 @@ +package modulevideoctrl + +import ( + "91porn-server/app/service/modulevideoser" + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/modulevidmod" + "91porn-server/web/service/moduleser" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// AddVideo doc +// @Summary 新增视频 +// @Description 在专题下新增视频 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param sectionID body string true "专题id" +// @Param videoID body string true "视频id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/video/add [post] +func AddVideo(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + p := modulevidmod.SectionVideo{} + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + code := modulevideoser.AddVideo(&p) + if code != stderr.Success { + common.ServeJSON(ctx, code, code.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// AddVideoBatch doc +// @Summary 批量新增视频 +// @Description 在专题下批量新增视频 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param sectionID body string true "专题id" +// @Param videoIDs body string true "视频id列表,以','分隔" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/video/add/batch [post] +func AddVideoBatch(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + p := moduleser.AddVideoBatchReq{} + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err := moduleser.AddVideoBatch(p); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertManyError, err.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// UpdateVideo doc +// @Summary 修改视频 +// @Description 设置视频在专题下的排序码 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param id body string false "id primary key" +// @Param sectionID body string false "section id" +// @Param videoID body string false "video id" +// @Param sortCode body integer false "视频排序" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/video/edit [post] +func UpdateVideo(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + p := modulevidmod.EditSelector{} + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + code := modulevideoser.UpdateVideo(&p) + if code != stderr.Success { + common.ServeJSON(ctx, code, code.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// DeleteVideo doc +// @Summary 删除视频 +// @Description 删除视频 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param id body string true "id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/video/delete [post] +func DeleteVideo(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + ID primitive.ObjectID `json:"id"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = modulevidmod.DeleteOne(p.ID); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// BatchDeleteVideo doc +// @Summary 批量删除视频 +// @Description 批量删除专题下视频 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param sectionID body string true "专题ID" +// @Param request body modulevidmod.BatchDeleteVideoRequest true "request" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/video/delete/batch [post] +func BatchDeleteVideo(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p modulevidmod.BatchDeleteVideoRequest + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if p.SectionID.IsZero() || len(p.VideoIDs) == 0 { + common.ServeJSON(ctx, stderr.ErrParamError, "invalid params") + return + } + if err = modulevidmod.BatchDeleteVideo(ctx, p.SectionID, p.VideoIDs); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// List doc +// @Summary 获取视频列表 +// @Description 获取视频列表 +// @Tags 模块配置 +// @Accept mpfd,json +// @Produce json,html +// @Param sectionid query string true "专题id" +// @Param videoid query string true "视频id" +// @Param pageNumber query integer true "当前页码" +// @Param pageSize query integer true "一页数据量" +// @Success 200 {object} moduleser.ModuleVideoList +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /module/video/list [post] +func List(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var q struct { + modulevidmod.QuerySelector + commod.Page + } + if err = ctx.ShouldBind(&q); err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + resp, err := moduleser.GetModuleList(q.QuerySelector, q.Page) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + log, _ := json.Marshal(q) + _ = operatorlgmod.RecordOperation(manager, constant.Loufeng, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, resp) +} diff --git a/web/api/nakedchatctrl/nakedchat.go b/web/api/nakedchatctrl/nakedchat.go new file mode 100755 index 0000000..7bf3064 --- /dev/null +++ b/web/api/nakedchatctrl/nakedchat.go @@ -0,0 +1,141 @@ +package nakedchatctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/nakedchatser" + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取裸聊列表 +// @Description 获取裸聊列表 +// @Tags 后台-裸聊 +// @Accept mpfd,json +// @Produce json +// @Param q query nakedchatser.WebListReq false "请求参数" +// @Success 200 object nakedchatser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/nakedchat/list [get] +func List(ctx *gin.Context) { + var req = &nakedchatser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Create doc +// @Summary 新增裸聊 +// @Description 新增裸聊 +// @Tags 后台-裸聊 +// @Accept mpfd,json +// @Produce json +// @Param q body nakedchatser.WebCreateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/nakedchat/create [post] +func Create(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &nakedchatser.WebCreateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 创建 + err = p.Create() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "裸聊管理", "创建", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 更新裸聊 +// @Description 更新裸聊 +// @Tags 后台-裸聊 +// @Accept mpfd,json +// @Produce json +// @Param q body nakedchatser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/nakedchat/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &nakedchatser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "裸聊管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// BatchUpdate doc +// @Summary 批量更新裸聊 +// @Description 批量更新裸聊 +// @Tags 后台-裸聊 +// @Accept mpfd,json +// @Produce json +// @Param q body nakedchatser.WebBatchUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/nakedchat/batch/update [post] +func BatchUpdate(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &nakedchatser.WebBatchUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "裸聊管理", "批量更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/nakedchatorderctrl/nakedchatorder.go b/web/api/nakedchatorderctrl/nakedchatorder.go new file mode 100755 index 0000000..6f4627d --- /dev/null +++ b/web/api/nakedchatorderctrl/nakedchatorder.go @@ -0,0 +1,71 @@ +package nakedchatorderctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/nakedchatorderser" + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取裸聊订单列表 +// @Description 获取裸聊订单列表 +// @Tags 后台-裸聊订单 +// @Accept mpfd,json +// @Produce json +// @Param q query nakedchatorderser.WebListReq false "请求参数" +// @Success 200 object nakedchatorderser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/nakedchatorder/list [get] +func List(ctx *gin.Context) { + var req = &nakedchatorderser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Update doc +// @Summary 更新裸聊订单 +// @Description 更新裸聊订单 +// @Tags 后台-裸聊订单 +// @Accept mpfd,json +// @Produce json +// @Param q body nakedchatorderser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/nakedchatorder/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &nakedchatorderser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "裸聊订单管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/newactivityctrl/activity.go b/web/api/newactivityctrl/activity.go new file mode 100644 index 0000000..f1ddf86 --- /dev/null +++ b/web/api/newactivityctrl/activity.go @@ -0,0 +1,288 @@ +package newactivityctrl + +import ( + "bytes" + "encoding/json" + "net/http" + "strings" + "time" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/newactivity" + "91porn-server/models/v/usermod" + + "github.com/gin-gonic/gin" + "github.com/tealeg/xlsx" + "go.mongodb.org/mongo-driver/mongo" +) + +// @Tags 特制H5活动 +// @Summary 导入所有嫩模 +// @Description 导入所有嫩模 +// @Security ApiKeyAuth +// @Tags web-H5嫩模活动 +// @Accept json +// @Produce json +// @Param file formData file false "选择的文件" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/newactivity/importModels [post] +func ImportModels(ctx *gin.Context) { + token := ctx.PostForm("token") + if token != "q7ydjHVIVBYiTqQ&" { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + form, err := ctx.MultipartForm() + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(ctx, stderr.ErrUploadError, err.Error()) + return + } + fs := form.File["file"] + if len(fs) == 0 { + common.ServeJSON(ctx, stderr.ErrUploadError, err.Error()) + return + } + file, err := fs[0].Open() + if err != nil { + common.ServeJSON(ctx, stderr.ErrUploadError, err.Error()) + return + } + buffer := bytes.Buffer{} + buffer.Reset() + _, err = buffer.ReadFrom(file) + file.Close() + if err != nil { + common.ServeJSON(ctx, stderr.ErrUploadError, err.Error()) + return + } + excelFile, err := xlsx.OpenBinary(buffer.Bytes()) + if err != nil { + common.ServeJSON(ctx, stderr.ErrUploadError, err.Error()) + return + } + id := uint32(130) + insertSqls := make([]mongo.WriteModel, 0, 50) + stockInsertSqls := make([]mongo.WriteModel, 0, 500) + for _, row := range excelFile.Sheets[0].Rows[1:51] { + excelModel := &struct { + Name string `xlsx:"4"` //姓名 + Location string `xlsx:"5"` //地区 + BgImg string `xlsx:"6"` //图片 + Video string `xlsx:"7"` //视频 + }{} + if err = row.ReadStruct(excelModel); err != nil { + continue + } + model := newactivity.Model{ + Id: id, + Name: strings.TrimSpace(excelModel.Name), + BgImg: strings.Split(excelModel.BgImg, ","), + Location: strings.TrimSpace(excelModel.Location), + Video: excelModel.Video, + } + insert := mongo.NewInsertOneModel() + insert.SetDocument(&model) + insertSqls = append(insertSqls, insert) + date := time.Date(2020, 6, 20, 0, 0, 0, 0, time.Local) + for i := 0; i < 11; i++ { + stockModel := newactivity.GiftStock{ + ModelId: id, + Date: date.Add(time.Duration(i) * 24 * time.Hour), + GiftLeft: 300, + SoldOut: false, + } + stockInsert := mongo.NewInsertOneModel() + stockInsert.SetDocument(&stockModel) + stockInsertSqls = append(stockInsertSqls, stockInsert) + } + id++ + } + if err = newactivity.BulkStocks(stockInsertSqls); err != nil { + common.ServeJSON(ctx, stderr.ErrUploadError, err.Error()) + return + } + if err = newactivity.BulkAddModels(insertSqls); err != nil { + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// @Tags 特制H5活动 +// @Summary 写入参数活动人数 +// @Description 写入参数活动人数 +// @Security ApiKeyAuth +// @Tags web-H5嫩模活动 +// @Accept json +// @Produce json +// @Param num formData integer false "人数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/newactivity/updateJoinNum [post] +func UpdateJoinNum(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req struct { + Num int64 `json:"num"` + } + if err = ctx.ShouldBind(&req); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"data": err.Error(), "code": stderr.ErrParamError, "msg": stderr.ErrParamError.Msg()}) + return + } + if err = newactivity.UpdateJoinNum(req.Num); err != nil { + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.NewsModel, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// @Tags 特制H5活动 +// @Summary 获取参加活动人数 +// @Description 获取参加活动人数 +// @Security ApiKeyAuth +// @Tags web-H5嫩模活动 +// @Accept json +// @Produce json +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/newactivity/joinNum [get] +func GetJoinNum(ctx *gin.Context) { + num, err := newactivity.GetJoinNum() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, nil) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"num": num}) +} + +// @Tags 特制H5活动 +// @Summary 购买列表 +// @Description 购买列表 +// @Security ApiKeyAuth +// @Tags web-H5嫩模活动 +// @Accept json +// @Produce json +// @Param page formData integer false "页码" +// @Param pageSize formData integer false "条数" +// @Param userId formData integer false "用户id" +// @Param userName formData string false "用户昵称" +// @Param modelId formData integer false "模特id" +// @Param date formData string false "期号" +// @Param start formData string false "开始时间" +// @Param end formData string false "结束时间" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/newactivity/buyPage [post] +func BuyPage(ctx *gin.Context) { + var req newactivity.BuyPageReq + if err := ctx.ShouldBind(&req); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"data": err.Error(), "code": stderr.ErrParamError, "msg": stderr.ErrParamError.Msg()}) + return + } + rs, err := newactivity.BuyPage(req) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, nil) + return + + } + common.ServeJSON(ctx, stderr.Success, rs) +} + +// @Tags 特制H5活动 +// @Summary 添加获奖记录 +// @Description 添加获奖记录 +// @Security ApiKeyAuth +// @Tags web-H5嫩模活动 +// @Accept json +// @Produce json +// @Param date formData string false "日期" +// @Param userId formData uint32 false "用户id" +// @Param userName formData string false "用户昵称" +// @Param userLogo formData string false "用户头像" +// @Param winNum formData uint32 false "中奖号码" +// @Param modelId formData uint32 false "模特Id" +// @Param createdAt formData string false "创建时间" +// @Param updatedAt formData string false "更新时间" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/newactivity/addWinRecord [post] +func AddWinRecord(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var req struct { + Quantity uint32 `json:"quantity" bson:"quantity"` //购买份数 + newactivity.WinRecords + } + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + exists, err := newactivity.ExistsOrder(req.Quantity, req.WinRecords) + if err != nil { + return + } + if !exists { + common.ServeJSON(ctx, stderr.Failure, "中奖信息有误") + return + } + userInfo, err := usermod.FindUserByUID(uint64(req.UserId)) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + req.UserName = userInfo.Name + req.UserLogo = userInfo.Portrait + req.WinNum = req.Quantity + if err = newactivity.AddWinRecord(req.WinRecords); err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.RewardList, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// @Tags 特制H5活动 +// @Summary 获奖记录列表 +// @Description 获奖记录列表 +// @Security ApiKeyAuth +// @Tags web-H5嫩模活动 +// @Accept json +// @Produce json +// @Param page formData int64 false "页码" +// @Param pageSize formData int64 false "条数" +// @Param userId formData uint32 false "用户id" +// @Param userName formData string false "用户昵称" +// @Param modelId formData uint32 false "模特id" +// @Param date formData string false "期号" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/newactivity/winRecordPage [post] +func WinRecordPage(ctx *gin.Context) { + var req newactivity.WinRcdPageReq + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + rs, err := newactivity.WinRecordPage(req) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, nil) + return + + } + common.ServeJSON(ctx, stderr.Success, rs) +} diff --git a/web/api/officialWebsitectrl/album.go b/web/api/officialWebsitectrl/album.go new file mode 100644 index 0000000..c420e75 --- /dev/null +++ b/web/api/officialWebsitectrl/album.go @@ -0,0 +1,89 @@ +package officialWebsitectrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + officialwebsiteser "91porn-server/web/service/officialWebsiteser" + "encoding/json" + + "github.com/gin-gonic/gin" +) + +func ListAlbum(ctx *gin.Context) { + req := &officialwebsiteser.AlbumListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + total, data, err := officialwebsiteser.ListAlbum(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"total": total, "list": data}) +} + +func CreateAlbum(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyAlbumReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.CreateAlbum(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func UpdateAlbum(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyAlbumReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.UpdateAlbum(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func DeleteAlbum(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.DeleteAlbumReq{} + if err := ctx.ShouldBind(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.DeleteAlbum(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/officialWebsitectrl/basic.go b/web/api/officialWebsitectrl/basic.go new file mode 100644 index 0000000..1f2c3bc --- /dev/null +++ b/web/api/officialWebsitectrl/basic.go @@ -0,0 +1,84 @@ +package officialWebsitectrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/officialWebsiteser" + "encoding/json" + + "github.com/gin-gonic/gin" +) + +func GetBasicData(ctx *gin.Context) { + data, err := officialwebsiteser.GetBasicData() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +func CreateBasicData(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyBasicDataReq{} + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.CreateBasicData(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func UpdateBasicData(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyBasicDataReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.UpdateBasicData(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func DeleteBasicData(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.DeleteBasicDataReq{} + if err := ctx.ShouldBind(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.DeleteBasicData(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/officialWebsitectrl/business.go b/web/api/officialWebsitectrl/business.go new file mode 100644 index 0000000..155ee3b --- /dev/null +++ b/web/api/officialWebsitectrl/business.go @@ -0,0 +1,89 @@ +package officialWebsitectrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + officialwebsiteser "91porn-server/web/service/officialWebsiteser" + "encoding/json" + + "github.com/gin-gonic/gin" +) + +func ListBusiness(ctx *gin.Context) { + req := &officialwebsiteser.BusinessListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + total, data, err := officialwebsiteser.ListBusiness(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"total": total, "list": data}) +} + +func CreateBusiness(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyBusinessReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.CreateBusiness(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func UpdateBusiness(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyBusinessReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.UpdateBusiness(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func DeleteBusiness(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.DeleteBusinessReq{} + if err := ctx.ShouldBind(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.DeleteBusiness(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/officialWebsitectrl/hero.go b/web/api/officialWebsitectrl/hero.go new file mode 100644 index 0000000..baa23d2 --- /dev/null +++ b/web/api/officialWebsitectrl/hero.go @@ -0,0 +1,89 @@ +package officialWebsitectrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + officialwebsiteser "91porn-server/web/service/officialWebsiteser" + "encoding/json" + + "github.com/gin-gonic/gin" +) + +func ListHero(ctx *gin.Context) { + req := &officialwebsiteser.HeroListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + total, data, err := officialwebsiteser.ListHero(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"total": total, "list": data}) +} + +func CreateHero(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyHeroReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.CreateHero(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func UpdateHero(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyHeroReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.UpdateHero(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func DeleteHero(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.DeleteHeroReq{} + if err := ctx.ShouldBind(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.DeleteHero(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/officialWebsitectrl/job.go b/web/api/officialWebsitectrl/job.go new file mode 100644 index 0000000..b75122b --- /dev/null +++ b/web/api/officialWebsitectrl/job.go @@ -0,0 +1,89 @@ +package officialWebsitectrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + officialwebsiteser "91porn-server/web/service/officialWebsiteser" + "encoding/json" + + "github.com/gin-gonic/gin" +) + +func ListJobList(ctx *gin.Context) { + req := &officialwebsiteser.JobListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + total, data, err := officialwebsiteser.ListJobList(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"total": total, "list": data}) +} + +func CreateJobList(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyJobReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.CreateJobList(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func UpdateJobList(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyJobReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.UpdateJobList(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func DeleteJobList(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.DeleteJobReq{} + if err := ctx.ShouldBind(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.DeleteJobList(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/officialWebsitectrl/news.go b/web/api/officialWebsitectrl/news.go new file mode 100644 index 0000000..0574a37 --- /dev/null +++ b/web/api/officialWebsitectrl/news.go @@ -0,0 +1,89 @@ +package officialWebsitectrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + officialwebsiteser "91porn-server/web/service/officialWebsiteser" + "encoding/json" + + "github.com/gin-gonic/gin" +) + +func ListNews(ctx *gin.Context) { + req := &officialwebsiteser.NewsListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + total, data, err := officialwebsiteser.ListNews(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"total": total, "list": data}) +} + +func CreateNews(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyNewsReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.CreateNews(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func UpdateNews(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyNewsReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.UpdateNews(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func DeleteNews(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.DeleteNewsReq{} + if err := ctx.ShouldBind(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.DeleteNews(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/officialWebsitectrl/partner.go b/web/api/officialWebsitectrl/partner.go new file mode 100644 index 0000000..fd78490 --- /dev/null +++ b/web/api/officialWebsitectrl/partner.go @@ -0,0 +1,89 @@ +package officialWebsitectrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + officialwebsiteser "91porn-server/web/service/officialWebsiteser" + "encoding/json" + + "github.com/gin-gonic/gin" +) + +func ListPartner(ctx *gin.Context) { + req := &officialwebsiteser.PartnerListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + total, data, err := officialwebsiteser.ListPartner(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"total": total, "list": data}) +} + +func CreatePartner(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyPartnerReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.CreatePartner(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func UpdatePartner(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyPartnerReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.UpdatePartner(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func DeletePartner(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.DeletePartnerReq{} + if err := ctx.ShouldBind(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.DeletePartner(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/officialWebsitectrl/photograph.go b/web/api/officialWebsitectrl/photograph.go new file mode 100644 index 0000000..bb43ba8 --- /dev/null +++ b/web/api/officialWebsitectrl/photograph.go @@ -0,0 +1,103 @@ +package officialWebsitectrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + officialwebsiteser "91porn-server/web/service/officialWebsiteser" + "encoding/json" + + "github.com/gin-gonic/gin" +) + +func ListPhotograph(ctx *gin.Context) { + req := &officialwebsiteser.ListPhotographReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + total, data, err := officialwebsiteser.ListPhotograph(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"total": total, "list": data}) +} + +func CreatePhotograph(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyPhotographReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.CreatePhotograph(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func UpdatePhotograph(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyPhotographReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.UpdatePhotograph(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func DeletePhotograph(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.DeletePhotographReq{} + if err := ctx.ShouldBind(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.DeletePhotograph(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func BatchUpdatePhotograph(ctx *gin.Context) { + req := &officialwebsiteser.BatchUpdatePhotographOwnerReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.BatchUpdatePhotographOwner(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/officialWebsitectrl/recruit_form.go b/web/api/officialWebsitectrl/recruit_form.go new file mode 100644 index 0000000..e30cd0c --- /dev/null +++ b/web/api/officialWebsitectrl/recruit_form.go @@ -0,0 +1,89 @@ +package officialWebsitectrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + officialwebsiteser "91porn-server/web/service/officialWebsiteser" + "encoding/json" + + "github.com/gin-gonic/gin" +) + +func ListRecruitForm(ctx *gin.Context) { + req := &officialwebsiteser.RecruitFormListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + total, data, err := officialwebsiteser.ListRecruitForm(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"total": total, "list": data}) +} + +func CreateRecruitForm(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyRecruitFormReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.CreateRecruitForm(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func UpdateRecruitForm(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyRecruitFormReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.UpdateRecruitForm(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func DeleteRecruitForm(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.DeleteRecruitFormReq{} + if err := ctx.ShouldBind(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.DeleteRecruitForm(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/officialWebsitectrl/tag.go b/web/api/officialWebsitectrl/tag.go new file mode 100644 index 0000000..69d2e19 --- /dev/null +++ b/web/api/officialWebsitectrl/tag.go @@ -0,0 +1,89 @@ +package officialWebsitectrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + officialwebsiteser "91porn-server/web/service/officialWebsiteser" + "encoding/json" + + "github.com/gin-gonic/gin" +) + +func ListTag(ctx *gin.Context) { + req := &officialwebsiteser.TagListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + total, data, err := officialwebsiteser.ListTag(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"total": total, "list": data}) +} + +func CreateTag(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyTagReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.CreateTag(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func UpdateTag(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyTagReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.UpdateTag(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func DeleteTag(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.DeleteTagReq{} + if err := ctx.ShouldBind(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.DeleteTag(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/officialWebsitectrl/video.go b/web/api/officialWebsitectrl/video.go new file mode 100644 index 0000000..5e7ba08 --- /dev/null +++ b/web/api/officialWebsitectrl/video.go @@ -0,0 +1,131 @@ +package officialWebsitectrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + officialwebsiteser "91porn-server/web/service/officialWebsiteser" + "encoding/json" + + "github.com/gin-gonic/gin" +) + +func ListVideo(ctx *gin.Context) { + req := &officialwebsiteser.VideoListReq{} + if err := ctx.ShouldBindQuery(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + total, data, err := officialwebsiteser.ListVideo(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"total": total, "list": data}) +} + +func CreateVideo(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyVideoReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.CreateVideo(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func UpdateVideo(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.ModifyVideoReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.UpdateVideo(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func DeleteVideo(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.DeleteVideoReq{} + if err := ctx.ShouldBind(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.DeleteVideo(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func BatchUpdateVideos(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.BatchUpdateVideoOwnerReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.BatchUpdateVideoOwner(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} + +func BatchImportVideos(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := &officialwebsiteser.BatchImportVideoReq{} + if err := ctx.ShouldBindJSON(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := officialwebsiteser.BatchImportVideo(req) + if err != nil { + common.ServeJSON(ctx, stderr.OfficialWebsiteImportVideoFailed, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialWebsiteConfig, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/officialWebsitectrl/video_ext.go b/web/api/officialWebsitectrl/video_ext.go new file mode 100644 index 0000000..cc159d1 --- /dev/null +++ b/web/api/officialWebsitectrl/video_ext.go @@ -0,0 +1,184 @@ +package officialWebsitectrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/vidmod" + officialwebsiteser "91porn-server/web/service/officialWebsiteser" + "91porn-server/web/service/vidser" + "time" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type VideoListFromVidResp struct { + VInfos []*VideoResp `json:"vInfos"` // 帖子列表 + Total int64 `json:"total"` // 帖子总量 +} + +type VideoResp struct { + *vidmod.WebVideo + AddedInfoOfficialWebsite bool `json:"added_info_official_website"` +} + +func ListVideoFromVid(ctx *gin.Context) { + req := vidmod.ListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + m, sort, err := assembleConditions(req) + if err != stderr.Success { + common.ServeJSON(ctx, err, err) + return + } + code, data := vidser.GetVidList(m, sort, req.PageNumber, req.PageSize, req.IsPush) + + videoList := make([]*VideoResp, 0) + var ids []primitive.ObjectID + for _, v := range data.VInfos { + ids = append(ids, v.ID) + videoList = append(videoList, &VideoResp{ + WebVideo: v, + AddedInfoOfficialWebsite: false, + }) + } + if len(ids) == 0 { + common.ServeJSON(ctx, code, videoList) + return + } + + checkData, checkErr := officialwebsiteser.CheckVideoExistByVid(ids) + if checkErr != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + for i := range videoList { + for j := range checkData { + if videoList[i].WebVideo.ID == checkData[j].ID { + videoList[i].AddedInfoOfficialWebsite = true + break + } + } + } + common.ServeJSON(ctx, code, &VideoListFromVidResp{ + VInfos: videoList, + Total: data.Total, + }) +} + +func assembleConditions(req vidmod.ListReq) (m map[string]interface{}, sort vidser.SortModeList, err stderr.Code) { + sort = vidser.SortModeList{{Field: "createdAt", Value: -1}} + m = make(map[string]interface{}) + if req.ShowType != nil { + m["showType"] = *req.ShowType + } + if req.Status != 4 { + if req.Status == 7 { + m["status"] = map[string]int{"$gt": 0} + } else { + m["status"] = req.Status + } + } + if req.IsFree == 0 { + m["coins"] = map[string]int{"$gt": 0} + } + if req.IsFree == 1 { + m["coins"] = map[string]int{"$eq": 0} + } + if req.IsUserUp == 2 { + m["publisherID"] = map[string]int{"$lt": 115000} + } + if req.IsUserUp == 1 { + m["publisherID"] = map[string]int{"$gt": 115000} + } + if len(req.Title) != 0 { + m["title"] = map[string]string{"$regex": req.Title, "$options": "i"} + } + if req.UID > 0 { + m["publisherID"] = req.UID + } + if req.Chosen == 1 { + m["chosen"] = true + } + if req.Chosen == 2 { + m["chosen"] = false + } + if req.FreeArea == 1 { + m["freeArea"] = true + } + if req.FreeArea == 2 { + m["freeArea"] = false + } + if !req.End.IsZero() { + m["createdAt"] = map[string]time.Time{"$gte": req.Start, "$lt": req.End} + } + if req.ReviewAccount != "" { + m["reviewAccount"] = req.ReviewAccount + } + if len(req.Tag) != 0 { + id, err := tagmod.GetTagIDByName(req.Tag) + if err != nil { + return m, sort, stderr.ErrDbQueryError + } + m["tags"] = id + //sort = vidser.SortModeList{{Field: "tagSort." + id.Hex(), Value: -1}, {Field: "createdAt", Value: -1}} + } + if len(req.ID) != 0 { + oid, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + return m, sort, stderr.ErrParamError + } + m["_id"] = oid + } + //通过初始价格判断是否是马甲账号 + if req.IsPretendAcc == 1 { + m["coins"] = vidmod.PretendAccInitCoins + } + if req.IsPretendAcc == 2 { + //i := make(map[string]int64) + //i["$ne"] = vidmod.PretendAccInitCoins + m["coins"] = map[string]int64{"$ne": vidmod.PretendAccInitCoins} + } + m["deleteAt"] = bson.M{"$exists": false} + if req.NewsType != "" { + m["newsType"] = req.NewsType + } + + if req.LiaoBaTop != nil { + if *req.LiaoBaTop { + sort = vidser.LiaoBaTopSortModeList() + m["liaoBaTopSort"] = bson.M{"$gt": 0} + } else { + m["liaoBaTopSort"] = 0 + } + } + if req.SectionID != "" { + m["sectionID"] = req.SectionID + } + m["isSortedUnderModule"] = req.IsSortedUnderModule + if req.IsSortedUnderModule { + sort = nil + } + if req.IsRecommended != nil { + if *req.IsRecommended { + m["recoWeight"] = bson.M{"$gte": 0} + } else { + m["recoWeight"] = bson.M{"$lt": 0} + } + } + if req.Key == "likeRate" || req.Key == "purchaseRate" { + m[req.Key] = req.Value + } + if req.IsHappinessPlazaTop != nil && *req.IsHappinessPlazaTop { + if *req.IsHappinessPlazaTop { + m["happinessPlazaTop"] = bson.M{"$gt": 0} + } else { + m["happinessPlazaTop"] = bson.M{"$lte": 0} + } + } + return m, sort, stderr.Success +} diff --git a/web/api/officialctrl/officialCtrl.go b/web/api/officialctrl/officialCtrl.go new file mode 100644 index 0000000..906d34c --- /dev/null +++ b/web/api/officialctrl/officialCtrl.go @@ -0,0 +1,162 @@ +package officialctrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/officialmod" + "91porn-server/web/service/officialser" + "encoding/json" + "github.com/gin-gonic/gin" + "time" +) + +// List doc +// @Summary 查询官方配置列表 +// @Description 查询官方配置列表 +// @Tags 官方配置管理 +// @Accept mpfd,json +// @Produce json,html +// @Param isActive query bool false "状态" +// @Param pageNumber query integer true "页码" +// @Param pageSize query integer true "每页条数"。 +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/official/list [get] +func List(ctx *gin.Context) { + var in *officialmod.QueryAllCond + if err := ctx.ShouldBind(&in); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + data, code := officialser.QueryAll(in) + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// Update doc +// @Summary 编辑官方配置 +// @Description 编辑官方配置 +// @Tags 官方配置管理 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "id" +// @Param officialName formData string true "官方配置名字" +// @Param officialImg formData string true "官方配置图片" +// @Param officialUrl formData string true "官方配置链接" +// @Param isActive formData bool true "官方配置开关" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/official/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + param := officialmod.WebTagUpdateReqInfo{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + doc := officialmod.OfficiaUpdateDoc{ + OfficialName: ¶m.OfficialName, + OfficialDesc: ¶m.OfficialDesc, + OfficialImg: ¶m.OfficialImg, + OfficialUrl: ¶m.OfficialUrl, + UpdatedAt: time.Now(), + IsActive: ¶m.IsActive, + Sort: ¶m.Sort, + Position: ¶m.Position, + IsHomepageAds: ¶m.IsHomepageAds, + } + if err := officialser.UpdateOfficial(param.ID, doc); err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + log, _ := json.Marshal(param) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialConfig, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Delete doc +// @Summary 批量删除官方配置 +// @Description 批量删除官方配置 +// @Tags 官方配置管理 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData array true "标签id" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/official/delete [delete] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + param := officialmod.WebOfficialDeleteReqInfo{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if err := officialser.DelOfficial(param); err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + log, _ := json.Marshal(param) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialConfig, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Add doc +// @Summary 新增官方配置 +// @Description 新增官方配置 +// @Tags 官方配置管理 +// @Accept mpfd,json +// @Produce json,html +// @Param officialName formData string true "官方配置名字" +// @Param officialImg formData string true "官方配置图片" +// @Param officialUrl formData string true "官方配置链接" +// @Param isActive formData bool false "官方配置开关" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/official/add [post] +func Add(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + param := officialmod.WebOfficialAddReqInfo{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + now := time.Now() + official := officialmod.OfficialConfig{ + OfficialName: param.OfficialName, + OfficialDesc: param.OfficialDesc, + OfficialImg: param.OfficialImg, + OfficialUrl: param.OfficialUrl, + OfficialType: param.OfficialType, + IsHomepageAds: param.IsHomepageAds, + Sort: param.Sort, + Position: param.Position, + IsActive: false, + CreatedAt: now, + UpdatedAt: now, + } + if _, err := officialser.AddOfficial(official); err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + log, _ := json.Marshal(param) + _ = operatorlgmod.RecordOperation(manager, constant.OfficialConfig, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/operationlogctrl/operationlog.go b/web/api/operationlogctrl/operationlog.go new file mode 100644 index 0000000..890c47f --- /dev/null +++ b/web/api/operationlogctrl/operationlog.go @@ -0,0 +1,31 @@ +package operationlogctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/web/service/operationlogser" + + "github.com/gin-gonic/gin" +) + +// @Tags 帖子、用户操作日志 +// @Summary 查询帖子、用户操作日志列表 +// @Description 查询帖子、用户操作日志列表 +// @Accept json +// @Produce json +// @Success 200 {object} operationlogser.GetOperationLogListResp +// @Failure 400 {string} string "失败" +// @Router /web/operationlog/List [GET] +func OperationLogList(c *gin.Context) { + var req operationlogser.GetOperationLogListReq + if err := c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err.Error()) + return + } + data, serr := operationlogser.GetOperationLogList(req) + if serr != stderr.Success { + common.ServeJSON(c, serr, "") + return + } + common.ServeJSON(c, stderr.Success, data) +} diff --git a/web/api/paymentguidectrl/payment_guide.go b/web/api/paymentguidectrl/payment_guide.go new file mode 100644 index 0000000..8e35dd1 --- /dev/null +++ b/web/api/paymentguidectrl/payment_guide.go @@ -0,0 +1,108 @@ +package paymentguidectrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/paymentguidemod" + "91porn-server/web/service/paymentguideser" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func List(ctx *gin.Context) { + req := paymentguideser.ListReq{} + if err := ctx.ShouldBindQuery(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, err := req.List() + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +func Add(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := paymentguideser.AddReq{} + if err = ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if req.IsBatch() { + if req.Config == nil { + common.ServeJSON(ctx, stderr.ErrParamError, "config is required") + return + } + configs, batchErr := (paymentguideser.BatchAddReq{Scenes: req.Scenes, Config: *req.Config}).Configs() + if batchErr != nil { + common.ServeJSON(ctx, stderr.ErrParamError, batchErr.Error()) + return + } + resp, batchErr := paymentguideser.BatchAdd(ctx.Request.Context(), configs) + if batchErr != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertManyError, batchErr.Error()) + return + } + body, _ := json.Marshal(configs) + _ = operatorlgmod.RecordOperation(manager, "付费引导配置", "批量创建", string(body), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, resp) + return + } + config := req.PaymentGuide + if err = paymentguideser.Add(&config); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + body, _ := json.Marshal(config) + _ = operatorlgmod.RecordOperation(manager, "付费引导配置", "创建", string(body), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, config.ID) +} + +func Edit(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + config := paymentguidemod.PaymentGuide{} + if err = ctx.ShouldBindJSON(&config); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = paymentguideser.Edit(&config); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + body, _ := json.Marshal(config) + _ = operatorlgmod.RecordOperation(manager, "付费引导配置", "修改", string(body), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + id, err := primitive.ObjectIDFromHex(ctx.Query("id")) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "invalid id") + return + } + if err = paymentguideser.Delete(id); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + _ = operatorlgmod.RecordOperation(manager, "付费引导配置", "删除", id.Hex(), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/productBenefitctrl/productbenefit.go b/web/api/productBenefitctrl/productbenefit.go new file mode 100644 index 0000000..3106502 --- /dev/null +++ b/web/api/productBenefitctrl/productbenefit.go @@ -0,0 +1,107 @@ +package productBenefitctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/productbenefitmod" + "91porn-server/web/service/productBenefitser" + + "github.com/gin-gonic/gin" +) + +// GetProductBenefitList doc +// @Summary 获取会员卡权益配置列表 +// @Description 获取会员卡权益配置列表 +// @Tags VIP权益 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/productBenefit/list [get] +func GetProductBenefitList(ctx *gin.Context) { + req := productbenefitmod.ProductBenefitReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := productBenefitser.GetProductBenefitList(req) + common.ServeJSON(ctx, code, data) +} + +// GetAllProductBenefitList doc +// @Summary 获取全部会员卡权益配置列表 +// @Description 获取全部会员卡权益配置列表 +// @Tags VIP权益 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/productBenefit/all/list [get] +func GetAllProductBenefitList(ctx *gin.Context) { + code, data := productBenefitser.GetAllProductBenefitList() + common.ServeJSON(ctx, code, data) +} + +// UpdateProductBenefit doc +// @Summary 更新会员卡权益配置列表 +// @Description 更新会员卡权益配置列表 +// @Tags VIP权益 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData integer true "id" +// @Param content formData string true "内容" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/productBenefit/update [post] +func UpdateProductBenefit(ctx *gin.Context) { + req := &productbenefitmod.ProductBenefitSelector{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code := productBenefitser.UpdateProductBenefit(req) + common.ServeJSON(ctx, code, nil) +} + +// DeleteProductBenefit doc +// @Summary 删除会员卡权益配置列表 +// @Description 删除会员卡权益配置列表 +// @Tags VIP权益 +// @Accept mpfd,json +// @Produce json,html +// @Param vids formData []string true "跑马灯id数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/productBenefit/del [delete] +func DeleteProductBenefit(ctx *gin.Context) { + req := productbenefitmod.ProductBenefitDel{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code := productBenefitser.DeleteProductBenefit(req.ID) + common.ServeJSON(ctx, code, nil) +} + +// AddProductBenefit doc +// @Summary 增加会员卡权益配置 +// @Description 增加会员卡权益配置 +// @Tags VIP权益 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/productBenefit/add [post] +func AddProductBenefit(ctx *gin.Context) { + req := productbenefitmod.ProductBenefit{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code := productBenefitser.AddProductBenefit(req) + common.ServeJSON(ctx, code, nil) +} diff --git a/web/api/productctrl/productLog.go b/web/api/productctrl/productLog.go new file mode 100644 index 0000000..bddb93c --- /dev/null +++ b/web/api/productctrl/productLog.go @@ -0,0 +1,64 @@ +package productctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/export_task_mod" + "91porn-server/models/v/prdcthsomod" + "91porn-server/web/service/productser" + + "github.com/gin-gonic/gin" +) + +// ProductLog doc +// @Summary 会员卡购买记录 +// @Description 会员卡购买记录 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Param q query prdcthsomod.ProductHistoryQueryReq false "请求参数" +// @Success 200 object prdcthsomod.ProductHistoryQueryResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/business/product/order [get] +func ProductLog(ctx *gin.Context) { + arg := &prdcthsomod.ProductHistoryQueryReq{} + if err := ctx.ShouldBind(arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, err := productser.GetBuyHistoryList(arg) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// ProductLogExport doc +// @Summary 会员卡购买记录导出 +// @Description 会员卡购买记录 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Param q query prdcthsomod.ProductHistoryQueryReq false "请求参数" +// @Success 200 {string} string "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/business/product/order/export [post] +func ProductLogExport(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + arg := &prdcthsomod.ProductHistoryQueryReq{} + if err := ctx.ShouldBind(arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + err = export_task_mod.CreateTask(export_task_mod.ExportProductHistoryTask, manager, arg) + if err != nil { + common.ServeJSON(ctx, stderr.ErrExportFileFail, err) + return + } + common.ServeJSON(ctx, stderr.Success, "success") +} diff --git a/web/api/productctrl/productposition.go b/web/api/productctrl/productposition.go new file mode 100644 index 0000000..ce2b870 --- /dev/null +++ b/web/api/productctrl/productposition.go @@ -0,0 +1,118 @@ +package productctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/productposimod" + + "github.com/gin-gonic/gin" +) + +// Get doc +// @Summary +// @Description 查询 +// @Tags productposition +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /productposition/list [post] +func GetPosition(ctx *gin.Context) { + data, err := productposimod.FindAll() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": data, + }) +} + +// Get doc +// @Summary +// @Description 查询位置名字列表 +// @Tags productposition +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /productposition/names [post] +func GetPositionNames(ctx *gin.Context) { + data, err := productposimod.FindNames() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": data, + }) +} + +// Insert doc +// @Summary 新增 +// @Description 新增 +// @Tags productposition +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /productposition/add [post] +func InsertPosition(ctx *gin.Context) { + v := productposimod.ProductPosition{} + if err := ctx.ShouldBind(&v); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err := productposimod.Insert(&v); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Update doc +// @Summary 修改 +// @Description 修改 +// @Tags productposition +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /productposition/edit [post] +func UpdatePosition(ctx *gin.Context) { + p := productposimod.ProductPositionSelector{} + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err := productposimod.Update(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, "") + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Delete doc +// @Summary 删除 +// @Description 删除 +// @Tags productposition +// @Accept mpfd,json +// @Produce json,html +// @Param id formData int true "productposition等级"" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /productposition/del [delete] +func DeletePosition(ctx *gin.Context) { + var p struct { + ID string `json:"id" form:"id"` + } + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err := productposimod.Remove(p.ID); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, "") + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/productctrl/vip.go b/web/api/productctrl/vip.go new file mode 100644 index 0000000..8f579e1 --- /dev/null +++ b/web/api/productctrl/vip.go @@ -0,0 +1,248 @@ +package productctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/productbenefitmod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/productposimod" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" +) + +// FindVipList doc +// @Summary product种类 +// @Description 所有可购买的product种类 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /product/productTypeList [get] +func FindVipList(ctx *gin.Context) { + req := productmod.ProductListWeb{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := productmod.FindStd(req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + positionM, _, _, err := productposimod.FindNamesWithStatusTrue() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + for i := range data { + data[i].Position = positionM[data[i].Position] + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// Insert doc +// @Summary 新增 +// @Description 新增 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Param q body productmod.Product false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /product/productType [post] +func Insert(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + v := productmod.Product{} + if err = ctx.ShouldBind(&v); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + //默认为vip商品 + if v.ProductType == 0 { + v.ProductType = productmod.VIP + } + m, _, _, err := productposimod.FindNamesWithStatusTrue() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + for k := range m { + if m[k] == v.Position { + v.Position = k + break + } + } + //组装商品权益信息 + if len(v.Privilege) > 0 { + //查询商品权益配置 + data, err := productbenefitmod.FindProductBenefitByPrivilege(&v.Privilege) + if err != nil { + log.Fatal("Insert productBenefitmod.FindProductBenefitByPrivilege err", log.E(err), log.Any("privilege", v.Privilege)) + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + if dataLen := len(data); dataLen > 0 { + v.NewPrivilege = make([]productmod.PrivilegeInfo, dataLen) + for i, vv := range data { + v.NewPrivilege[i] = productmod.PrivilegeInfo{ + Image: vv.Img, + Name: vv.PrivilegeName, + Desc: vv.PrivilegeDesc, + IsCore: vv.IsCore, + } + } + } + } + if err = productmod.InsertProduct(&v); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + redisKey := redisconst.DataCachKey(productmod.RedisSetKey, "FindProductByType") + _, _ = webg.Redis.Del(redisKey) + log, _ := json.Marshal(v) + _ = operatorlgmod.RecordOperation(manager, constant.ProductManageVIP, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Update doc +// @Summary 修改producttype +// @Description 修改producttype +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "id" +// @Param q body productmod.ProductSelector false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /product/productType [put] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + ID string `json:"id" form:"id" bson:"id"` + productmod.ProductSelector + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if p.Position != nil { + m, _, _, err := productposimod.FindNamesWithStatusTrue() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + v := *p.Position + for k := range m { + if m[k] == v { + kv := k + p.Position = &kv + break + } + } + } + //组装商品权益信息 + if p.ProductSelector.Privilege != nil { + //查询商品权益配置 + data, err := productbenefitmod.FindProductBenefitByPrivilege(p.ProductSelector.Privilege) + if err != nil { + log.Fatal("productBenefitmod.FindProductBenefitByPrivilege err", log.E(err), log.Any("privilege", p.ProductSelector.Privilege)) + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + //清空NewPrivilege里面的数据 + if err = productmod.UpdateProductRights(p.ID); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, "") + return + } + if dataLen := len(data); dataLen > 0 { + p.ProductSelector.NewPrivilege = make([]*productmod.PrivilegeInfo, dataLen) + for i, v := range data { + p.ProductSelector.NewPrivilege[i] = &productmod.PrivilegeInfo{ + Image: v.Img, + Name: v.PrivilegeName, + Desc: v.PrivilegeDesc, + IsCore: v.IsCore, + } + } + } + } + if err = productmod.UpdateProduct(p.ID, &p.ProductSelector); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, "") + return + } + redisKey := redisconst.DataCachKey(productmod.RedisSetKey, "FindProductByType") + _, _ = webg.Redis.Del(redisKey) + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.ProductManageVIP, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Delete doc +// @Summary 删除 +// @Description 删除 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Param id formData int true "product等级"" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /product/productType [Delete] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + ID string `json:"id" form:"id"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err = productmod.RemoveProduct(p.ID); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, "") + return + } + redisKey := redisconst.DataCachKey(productmod.RedisSetKey, "FindProductByType") + _, _ = webg.Redis.Del(redisKey) + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.ProductManageVIP, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +func CheckboxList(c *gin.Context) { + var ( + in productmod.CheckboxListCond + data = map[string]interface{}{ + "data": []interface{}{}, + } + ) + if err := c.ShouldBindQuery(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + list, err := productmod.CheckboxByType(in.ProductType) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, err) + return + } + data["data"] = list + common.ServeJSON(c, stderr.Success, data) +} diff --git a/web/api/productctrl/vipConfig.go b/web/api/productctrl/vipConfig.go new file mode 100644 index 0000000..cc81ee2 --- /dev/null +++ b/web/api/productctrl/vipConfig.go @@ -0,0 +1,60 @@ +package productctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/vipconfigmod" + + "github.com/gin-gonic/gin" +) + +// GetConfig doc +// @Summary +// @Description 查询VIP配置 +// @Tags loufeng +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /business/getConfig [get] +func GetConfig(ctx *gin.Context) { + data, err := vipconfigmod.FindOne() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// SetConfig doc +// @Summary 修改配置 +// @Description 修改配置 +// @Tags loufeng +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /business/setConfig [post] +func SetConfig(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + s := vipconfigmod.EditSelector{} + if err = ctx.ShouldBind(&s); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = vipconfigmod.Update(s); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(s) + _ = operatorlgmod.RecordOperation(manager, constant.VIPConfig, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/promoteurlctrl/promoteurlctrl.go b/web/api/promoteurlctrl/promoteurlctrl.go new file mode 100644 index 0000000..19fc1f4 --- /dev/null +++ b/web/api/promoteurlctrl/promoteurlctrl.go @@ -0,0 +1,140 @@ +package promoteurlctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/sourcemod" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// promoteURL doc +// @Summary promoteURL 列表查询 +// @Description +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Param status formData integer false "状态" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /promoteUrl/list [get] +func FindMany(ctx *gin.Context) { + data := sourcemod.GetPromoteURLList() + common.ServeJSON(ctx, stderr.Success, data) +} + +// promoteURL doc +// @Summary 新增 +// @Description 新增 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /promoteUrl/insert [post] +func Insert(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + s := sourcemod.Source{} + if err = ctx.ShouldBind(&s); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + s.Type = sourcemod.PromoteURL + s.IsActive = true + if err = sourcemod.Insert(&s); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + //落地页域名加入到redis中 + common.Go(func() { + purls := sourcemod.GetPromoteURLArray() + _, _ = webg.Redis.Del(redisconst.LandDomainCacheKey) + _, _ = webg.Redis.SAdd(redisconst.LandDomainCacheKey, purls) + _, _ = webg.Redis.ExpireKey(redisconst.LandDomainCacheKey, redisconst.LandDomainCacheExpire) + }) + log, _ := json.Marshal(s) + _ = operatorlgmod.RecordOperation(manager, constant.ProductManagePage, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// promoteURL doc +// @Summary 修改 +// @Description 修改 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /promoteUrl/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + ID primitive.ObjectID `json:"id" form:"id" bson:"id"` + sourcemod.SourceEdit + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + p.SourceEdit.IsActive = true + if err = sourcemod.Update(p.ID, sourcemod.PromoteURL, &p.SourceEdit); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, "") + return + } + common.Go(func() { + _, _ = webg.Redis.Del(redisconst.LandDomainCacheKey) + }) + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.ProductManagePage, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// promoteURL doc +// @Summary 删除 +// @Description 删除 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Param id formData int true "product等级" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /promoteUrl/delete [Delete] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + IDs []primitive.ObjectID `json:"ids" form:"ids"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err = sourcemod.Remove(p.IDs); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, "") + return + } + common.Go(func() { + _, _ = webg.Redis.Del(redisconst.LandDomainCacheKey) + }) + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.ProductManagePage, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/proxyctrl/proxy.go b/web/api/proxyctrl/proxy.go new file mode 100644 index 0000000..946d18c --- /dev/null +++ b/web/api/proxyctrl/proxy.go @@ -0,0 +1,133 @@ +package proxyctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/proxyincomemod" + "91porn-server/models/v/proxymod" + "91porn-server/web/service/proxyser" + + "github.com/gin-gonic/gin" +) + +// ProxyLog doc +// @Summary 用户推广日志 +// @Description 用户推广日志 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /proxy/log [get] +func ProxyLog(ctx *gin.Context) { + var arg struct { + common.StandQuery + proxymod.InvitationQueryReq + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + total, data, err := proxymod.FindMany(common.StandQueryMap(arg.StandQuery, arg.InvitationQueryReq)) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, map[string]interface{}{ + "total": total, + "history": data, + }) +} + +// AllIncome doc +// @Summary 代理列表 +// @Description 代理列表 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /proxy/allIncome [get] +func AllIncome(ctx *gin.Context) { + var arg struct { + UID uint64 `json:"uid" form:"uid"` + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + data, err := proxyser.FindTotalIncome(arg.UID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// IncomeDetails doc +// @Summary 代理列表 +// @Description 查询收益详情 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /proxy/i/income/details [get] +func IncomeDetails(ctx *gin.Context) { + var arg struct { + proxyincomemod.StandReq + common.StandQuery + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + total, data, err := proxyincomemod.FindList(common.StandQueryMap(arg.StandQuery, arg.StandReq)) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + //获取条件下总收益 + totalIncome, err := proxyincomemod.GetTotalIncome(arg.UID, arg.StartTime, arg.EndTime) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "total": total, + "data": data, + "totalIncome": totalIncome, + }) +} + +// PromotionQuery doc +// @Summary 推广查询 +// @Description 推广查询 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /proxy/promotion [get] +func PromotionQuery(ctx *gin.Context) { + var arg struct { + UID uint64 `json:"uid" form:"uid"` + Type string `json:"type" form:"type"` + } + err := ctx.ShouldBind(&arg) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + var data interface{} + if arg.Type == "up" { //获取上级代理 + data, err = proxyser.GetInvrDetails(arg.UID) + } else { //获取下级代理 + data, err = proxyser.GetInveUsers(arg.UID) + } + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} diff --git a/web/api/quicksearchctrl/quicksearch.go b/web/api/quicksearchctrl/quicksearch.go new file mode 100755 index 0000000..4157470 --- /dev/null +++ b/web/api/quicksearchctrl/quicksearch.go @@ -0,0 +1,147 @@ +package quicksearchctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/quicksearchser" + + "github.com/gin-gonic/gin" +) + +// List doc +// @Summary 获取快捷搜索表列表 +// @Description 获取快捷搜索表列表 +// @Tags 后台-快捷搜索表 +// @Accept mpfd,json +// @Produce json +// @Param q query quicksearchser.WebListReq false "请求参数" +// @Success 200 object quicksearchser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Security BasicAuth +// @Router /api/web/admin/quickSearch/list [get] +func List(ctx *gin.Context) { + var req = &quicksearchser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Create doc +// @Summary 新增快捷搜索表 +// @Description 新增快捷搜索表 +// @Tags 后台-快捷搜索表 +// @Accept mpfd,json +// @Produce json +// @Param q body quicksearchser.WebCreateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Security BasicAuth +// @Router /api/web/admin/quickSearch/create [post] +func Create(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &quicksearchser.WebCreateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 创建 + err = p.Create() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "快捷搜索表管理", "创建", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 更新快捷搜索表 +// @Description 更新快捷搜索表 +// @Tags 后台-快捷搜索表 +// @Accept mpfd,json +// @Produce json +// @Param q body quicksearchser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Security BasicAuth +// @Router /api/web/admin/quickSearch/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &quicksearchser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "快捷搜索表管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除快捷搜索表 +// @Description 删除快捷搜索表 +// @Tags 后台-快捷搜索表 +// @Accept mpfd,json +// @Produce json +// @Param q body quicksearchser.WebDeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Security BasicAuth +// @Router /api/web/admin/quickSearch/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &quicksearchser.WebDeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = p.Delete() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "快捷搜索表管理", "删除", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/rechargectrl/DaBaiSha.go b/web/api/rechargectrl/DaBaiSha.go new file mode 100644 index 0000000..648d198 --- /dev/null +++ b/web/api/rechargectrl/DaBaiSha.go @@ -0,0 +1,70 @@ +package rechargectrl + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/web/service/rechargeser" + + "github.com/gin-gonic/gin" +) + +// DaBaiShaCallBack 金鱼结构回调函数 +func DaBaiShaCallBack(ctx *gin.Context) { + if err := func() error { + rchg := rchgutil.DaBaiShaRes{} + g := rchgutil.DaBaiSha{} + if err := ctx.ShouldBindJSON(&rchg); err != nil { + log.Error(fmt.Sprintf("DaBaiSha callback parameter bind fail error:%+v:", err)) + return err + } + bs, _ := json.Marshal(rchg) + log.Info(fmt.Sprintf("DaBaiSha callback parameter data:%+v:", string(bs))) + buf := bytes.Buffer{} + buf.WriteString(strconv.Itoa(rchg.Code)) + buf.WriteString(rchg.MercID) + buf.WriteString(rchg.OID) + buf.WriteString(rchg.PayMoney) + buf.WriteString(rchg.TradeNo) + buf.WriteString(g.GetAppSecret()) + if !rchgutil.VerifySign(rchg.Sign, buf.String()) { + log.Error("DaBaiSha callback sign verify fail") + return errors.New("check sign fail") + } + payMoneyf, err := strconv.ParseFloat(rchg.PayMoney, 64) + if err != nil { + log.Error(fmt.Sprintf("DaBaiSha callback ParseFloat fail error:%+v:", err)) + return fmt.Errorf("invalid payMoney %s", rchg.PayMoney) + } + payMoneyf = payMoneyf * 100 + payMoney := int64(payMoneyf) + g.TradeNo = rchg.TradeNo + msg, err := g.QueryOrder() + if err != nil { + return err + } + if msg.PayTime == "" { + return errors.New("querry order err,no payTime") + } + loc, _ := time.LoadLocation("Local") + paymentAt, err := time.ParseInLocation("2006-01-02T15:04:05Z07:00", msg.PayTime, loc) + if err != nil { + return err + } + if err = rechargeser.RechargeCallBack(ctx, rchg.OID, payMoney, rchg.TradeNo, rchg.Code, paymentAt); err != nil { + log.Error(fmt.Sprintf("DaBaiSha RechargeCallBack fail error:%+v:", err)) + } + return err + }(); err != nil { + ctx.String(http.StatusBadRequest, "fail") + return + } + ctx.String(http.StatusOK, "success") +} diff --git a/web/api/rechargectrl/dxzhifu.go b/web/api/rechargectrl/dxzhifu.go new file mode 100644 index 0000000..f0c3b8b --- /dev/null +++ b/web/api/rechargectrl/dxzhifu.go @@ -0,0 +1,53 @@ +package rechargectrl + +import ( + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/web/service/rechargeser" + + "github.com/gin-gonic/gin" +) + +// DXZhiFuCallBack 金鱼结构回调函数 +func DXZhiFuCallBack(ctx *gin.Context) { + if err := func() error { + rchg := rchgutil.DXZhiFuRes{} + err := ctx.ShouldBind(&rchg) + if err != nil { + log.Error(fmt.Sprintf("DXZhiFu callback parameter bind fail error:%+v:", err)) + return err + } + log.Info(fmt.Sprintf("DXZhiFu callback parameter data:%+v:", rchg)) + if rchg.Sign != rchg.ToSign() { + log.Error(fmt.Sprintf("DXZhiFu callback sign verify fail error:%+v:", err)) + return errors.New("check sign fail") + } + payMoneyf, err := strconv.ParseFloat(rchg.PayMoney, 64) + if err != nil { + log.Error(fmt.Sprintf("DXZhiFu callback ParseFloat fail error:%+v:", err)) + return fmt.Errorf("invalid payMoney %s", rchg.PayMoney) + } + payMoneyf = payMoneyf * 100 + payMoney := int64(payMoneyf) + var status int = 200 + if rchg.Code != "00" { + return nil + } + loc, _ := time.LoadLocation("Local") + paymentAt, _ := time.ParseInLocation("20060102150405", rchg.DateTime, loc) + if err = rechargeser.RechargeCallBack(ctx, rchg.OID, payMoney, rchg.TradeNo, status, paymentAt); err != nil { + log.Error(fmt.Sprintf("DXZhiFu RechargeCallBack fail error:%+v:", err)) + } + return err + }(); err != nil { + ctx.String(http.StatusBadRequest, "fail") + return + } + ctx.String(http.StatusOK, "OK") +} diff --git a/web/api/rechargectrl/gameGold.go b/web/api/rechargectrl/gameGold.go new file mode 100644 index 0000000..4c0fa7b --- /dev/null +++ b/web/api/rechargectrl/gameGold.go @@ -0,0 +1,176 @@ +package rechargectrl + +import ( + "encoding/json" + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/rchgamegoldmod" + "91porn-server/web/vidhelp" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetGameGoldList doc +// @Summary 获取金币列表 +// @Description 获取金币列表 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/recharge/gameGold/list [get] +func GetGameGoldList(ctx *gin.Context) { + infos, err := rchgamegoldmod.GetGoldList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, infos) +} + +// GameGoldInsert doc +// @Summary 新增 +// @Description 新增 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Param reachargeDetails formData rchgamegoldmod.GoldReq true "product卡" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/recharge/gameGold/add [post] +func GameGoldInsert(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := rchgamegoldmod.GoldReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if req.Coins*100 != req.Price { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + gd := rchgamegoldmod.Gold{ + Name: req.Name, + Coins: req.Coins, + Price: req.Price, + CouponDesc: req.CouponDesc, + GiveVipDays: req.GiveVipDays, + LouFengUnlockTimes: req.LouFengUnlockTimes, + GiveGameCoin: req.GiveGameCoin, + NotFirst: req.NotFirst, + Active: req.Active, + UpdatedAt: time.Now(), + CreatedAt: time.Now(), + } + if err := rchgamegoldmod.InsertGold(&gd); err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + redisKey := redisconst.DataCachKey(rchgamegoldmod.RedisKey, "GetPayChannel") + _, _ = webg.Redis.Del(redisKey) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.ProductManageCoin, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// GameGoldUpdate doc +// @Summary 编辑金币配置 +// @Description 编辑金币配置 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/recharge/gameGold/update [post] +func GameGoldUpdate(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := rchgamegoldmod.EditGoldReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + oid, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + log.Warn(fmt.Sprintf("exchange string(%s) to objectID error.", req.ID)) + return + } + req.UpdatedAt = time.Now() + m := vidhelp.Struct2Map(req.GoldReq) + cond := bson.M{"_id": oid} + updt := bson.M(m) + cnt, err := rchgamegoldmod.UpdateGold(cond, updt) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + redisKey := redisconst.DataCachKey(rchgamegoldmod.RedisKey, "GetPayChannel") + _, _ = webg.Redis.Del(redisKey) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.ProductManageCoin, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, rchgamegoldmod.OpeResp{Count: cnt}) +} + +// GameGoldDelete doc +// @Summary 删除 +// @Description 删除 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Param id formData int true "product等级"" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/recharge/gameGold/delete [delete] +func GameGoldDelete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := rchgamegoldmod.DelReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + oids := make([]primitive.ObjectID, 0, len(req.IDs)) + for _, id := range req.IDs { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("exchange string(%s) to objectID error.", id)) + continue + } + oids = append(oids, oid) + } + if oids == nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + cond := bson.M{"_id": bson.M{"$in": oids}} + cnt, err := rchgamegoldmod.DeleteGold(cond) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + redisKey := redisconst.DataCachKey(rchgamegoldmod.RedisKey, "GetPayChannel") + _, _ = webg.Redis.Del(redisKey) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.ProductManageCoin, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, rchgamegoldmod.OpeResp{Count: cnt}) +} diff --git a/web/api/rechargectrl/gold.go b/web/api/rechargectrl/gold.go new file mode 100644 index 0000000..50fa25c --- /dev/null +++ b/web/api/rechargectrl/gold.go @@ -0,0 +1,167 @@ +package rechargectrl + +import ( + "encoding/json" + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/rchgamtmod" + "91porn-server/web/vidhelp" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetGoldList doc +// @Summary 获取金币列表 +// @Description 获取金币列表 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/recharge/gold/list [get] +func GetGoldList(ctx *gin.Context) { + infos, err := rchgamtmod.GetGoldList() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, infos) +} + +// GoldInsert doc +// @Summary 新增 +// @Description 新增 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Param reachargeDetails formData rchgamtmod.GoldReq true "product卡" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/recharge/gold/add [post] +func GoldInsert(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := rchgamtmod.GoldReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + gd := rchgamtmod.Gold{ + Name: req.Name, + Coins: req.Coins, + Price: req.Price, + Active: req.Active, + UpdatedAt: time.Now(), + CreatedAt: time.Now(), + } + if err = rchgamtmod.InsertGold(&gd); err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + redisKey := redisconst.DataCachKey(rchgamtmod.RedisKey, "GetPayChannel") + _, _ = webg.Redis.Del(redisKey) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.ProductManageCoin, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// GoldUpdate doc +// @Summary 编辑金币配置 +// @Description 编辑金币配置 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/recharge/gold/update [post] +func GoldUpdate(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := rchgamtmod.EditGoldReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + oid, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + log.Warn(fmt.Sprintf("exchange string(%s) to objectID error.", req.ID)) + return + } + req.UpdatedAt = time.Now() + m := vidhelp.Struct2Map(req.GoldReq) + cond := bson.M{"_id": oid} + updt := bson.M(m) + cnt, err := rchgamtmod.UpdateGold(cond, updt) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + redisKey := redisconst.DataCachKey(rchgamtmod.RedisKey, "GetPayChannel") + _, _ = webg.Redis.Del(redisKey) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.ProductManageCoin, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, rchgamtmod.OpeResp{Count: cnt}) +} + +// GoldDelete doc +// @Summary 删除 +// @Description 删除 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Param id formData int true "product等级"" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/recharge/gold/delete [delete] +func GoldDelete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := rchgamtmod.DelReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + oids := make([]primitive.ObjectID, 0, len(req.IDs)) + for _, id := range req.IDs { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Warn(fmt.Sprintf("exchange string(%s) to objectID error.", id)) + continue + } + oids = append(oids, oid) + } + if oids == nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + cond := bson.M{"_id": bson.M{"$in": oids}} + cnt, err := rchgamtmod.DeleteGold(cond) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + redisKey := redisconst.DataCachKey(rchgamtmod.RedisKey, "GetPayChannel") + _, _ = webg.Redis.Del(redisKey) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.ProductManageCoin, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, rchgamtmod.OpeResp{Count: cnt}) +} diff --git a/web/api/rechargectrl/goldfishplus.go b/web/api/rechargectrl/goldfishplus.go new file mode 100644 index 0000000..dbaf326 --- /dev/null +++ b/web/api/rechargectrl/goldfishplus.go @@ -0,0 +1,68 @@ +package rechargectrl + +import ( + "bytes" + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/web/service/rechargeser" + + "github.com/gin-gonic/gin" +) + +// GoldfishPlusCallBack 金鱼结构回调函数 +func GoldfishPlusCallBack(ctx *gin.Context) { + if err := func() error { + rchg := rchgutil.GoldfishPlusRes{} + g := rchgutil.GoldfishPlus{} + if err := ctx.ShouldBindJSON(&rchg); err != nil { + log.Error(fmt.Sprintf("GoldfishPlus callback parameter bind fail error:%+v:", err)) + return err + } + log.Info(fmt.Sprintf("GoldfishPlus callback parameter data:%+v:", rchg)) + buf := bytes.Buffer{} + buf.WriteString(strconv.Itoa(rchg.Code)) + buf.WriteString(rchg.MercID) + buf.WriteString(rchg.OID) + buf.WriteString(rchg.PayMoney) + buf.WriteString(rchg.TradeNo) + buf.WriteString(g.GetAppSecret()) + if !rchgutil.VerifySign(rchg.Sign, buf.String()) { + log.Error("GoldfishPlus callback sign verify fail") + return errors.New("check sign fail") + } + payMoneyf, err := strconv.ParseFloat(rchg.PayMoney, 64) + if err != nil { + log.Error(fmt.Sprintf("GoldfishPlus callback ParseFloat fail error:%+v:", err)) + return fmt.Errorf("invalid payMoney %s", rchg.PayMoney) + } + payMoneyf = payMoneyf * 100 + payMoney := int64(payMoneyf) + g.TradeNo = rchg.TradeNo + msg, err := g.QueryOrder() + if err != nil { + return err + } + if msg.PayTime == "" { + return errors.New("querry order err,no payTime") + } + loc, _ := time.LoadLocation("Local") + paymentAt, err := time.ParseInLocation("2006-01-02T15:04:05Z07:00", msg.PayTime, loc) + if err != nil { + return err + } + if err = rechargeser.RechargeCallBack(ctx, rchg.OID, payMoney, rchg.TradeNo, rchg.Code, paymentAt); err != nil { + log.Error(fmt.Sprintf("GoldfishPlus RechargeCallBack fail error:%+v:", err)) + } + return err + }(); err != nil { + ctx.String(http.StatusBadRequest, "fail") + return + } + ctx.String(http.StatusOK, "success") +} diff --git a/web/api/rechargectrl/izhifu.go b/web/api/rechargectrl/izhifu.go new file mode 100644 index 0000000..e79353c --- /dev/null +++ b/web/api/rechargectrl/izhifu.go @@ -0,0 +1,46 @@ +package rechargectrl + +import ( + "errors" + "fmt" + "net/http" + "time" + + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/web/service/rechargeser" + + "github.com/gin-gonic/gin" +) + +// IZhiFuCallBack 金鱼结构回调函数 +func IZhiFuCallBack(ctx *gin.Context) { + if err := func() error { + rchg := rchgutil.IZhiFuRes{} + if err := ctx.ShouldBind(&rchg); err != nil { + log.Error(fmt.Sprintf("IZhiFu callback parameter bind fail error:%+v:", err)) + return err + } + log.Info(fmt.Sprintf("IZhiFu callback parameter data:%+v:", rchg)) + if rchg.Sign != rchg.ToSign() { + log.Error("IZhiFu callback sign verify fail") + return errors.New("check sign fail") + } + var status int = 100 + if rchg.Code == rchgutil.IZhiFuSuccess { + status = 200 + } + paymentAt, err := rchg.QueryOrder() + if err != nil { + paymentAt = time.Now() + } + if err = rechargeser.RechargeCallBack(ctx, "", rchg.PayMoney, rchg.TradeNo, status, paymentAt); err != nil { + log.Error(fmt.Sprintf("IZhiFu RechargeCallBack fail error:%+v:", err)) + } + return err + }(); err != nil { + ctx.String(http.StatusBadRequest, "fail") + return + } + ctx.String(http.StatusOK, "success") +} diff --git a/web/api/rechargectrl/leihuo.go b/web/api/rechargectrl/leihuo.go new file mode 100644 index 0000000..e31a463 --- /dev/null +++ b/web/api/rechargectrl/leihuo.go @@ -0,0 +1,70 @@ +package rechargectrl + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/web/service/rechargeser" + + "github.com/gin-gonic/gin" +) + +// LeiHuoCallBack 金鱼结构回调函数 +func LeiHuoCallBack(ctx *gin.Context) { + if err := func() error { + rchg := rchgutil.DaBaiShaRes{} + g := rchgutil.DaBaiSha{} + if err := ctx.ShouldBindJSON(&rchg); err != nil { + log.Error(fmt.Sprintf("DaBaiSha callback parameter bind fail error:%+v:", err)) + return err + } + bs, _ := json.Marshal(rchg) + log.Info(fmt.Sprintf("DaBaiSha callback parameter data:%+v:", string(bs))) + buf := bytes.Buffer{} + buf.WriteString(strconv.Itoa(rchg.Code)) + buf.WriteString(rchg.MercID) + buf.WriteString(rchg.OID) + buf.WriteString(rchg.PayMoney) + buf.WriteString(rchg.TradeNo) + buf.WriteString(g.GetAppSecret()) + if !rchgutil.VerifySign(rchg.Sign, buf.String()) { + log.Error("DaBaiSha callback sign verify fail") + return errors.New("check sign fail") + } + payMoneyf, err := strconv.ParseFloat(rchg.PayMoney, 64) + if err != nil { + log.Error(fmt.Sprintf("DaBaiSha callback ParseFloat fail error:%+v:", err)) + return fmt.Errorf("invalid payMoney %s", rchg.PayMoney) + } + payMoneyf = payMoneyf * 100 + payMoney := int64(payMoneyf) + g.TradeNo = rchg.TradeNo + msg, err := g.QueryOrder() + if err != nil { + return err + } + if msg.PayTime == "" { + return errors.New("querry order err,no payTime") + } + loc, _ := time.LoadLocation("Local") + paymentAt, err := time.ParseInLocation("2006-01-02T15:04:05Z07:00", msg.PayTime, loc) + if err != nil { + return err + } + if err = rechargeser.RechargeCallBack(ctx, rchg.OID, payMoney, rchg.TradeNo, rchg.Code, paymentAt); err != nil { + log.Error(fmt.Sprintf("DaBaiSha RechargeCallBack fail error:%+v:", err)) + } + return err + }(); err != nil { + ctx.String(http.StatusBadRequest, "fail") + return + } + ctx.String(http.StatusOK, "success") +} diff --git a/web/api/rechargectrl/lucky.go b/web/api/rechargectrl/lucky.go new file mode 100644 index 0000000..7ed1305 --- /dev/null +++ b/web/api/rechargectrl/lucky.go @@ -0,0 +1,52 @@ +package rechargectrl + +import ( + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/web/service/rechargeser" + + "github.com/gin-gonic/gin" +) + +// LuckyCallBack 金鱼结构回调函数 +func LuckyCallBack(ctx *gin.Context) { + if err := func() error { + rchg := rchgutil.LuckyRes{} + if err := ctx.ShouldBind(&rchg); err != nil { + log.Error(fmt.Sprintf("Lucky callback parameter bind fail error:%+v:", err)) + return err + } + log.Info(fmt.Sprintf("Lucky callback parameter data:%+v:", rchg)) + if rchg.Sign != rchg.ToSign() { + log.Error("Lucky callback sign verify fail") + return errors.New("check sign fail") + } + payMoneyf, err := strconv.ParseFloat(rchg.PayMoney, 64) + if err != nil { + log.Error(fmt.Sprintf("Lucky callback ParseFloat fail error:%+v:", err)) + return fmt.Errorf("invalid payMoney %s", rchg.PayMoney) + } + payMoneyf = payMoneyf * 100 + payMoney := int64(payMoneyf) + var status int = 200 + if rchg.Code != "00" { + return nil + } + loc, _ := time.LoadLocation("Local") + paymentAt, _ := time.ParseInLocation("20060102150405", rchg.DateTime, loc) + if err = rechargeser.RechargeCallBack(ctx, rchg.OID, payMoney, rchg.TradeNo, status, paymentAt); err != nil { + log.Error(fmt.Sprintf("Lucky RechargeCallBack fail error:%+v:", err)) + } + return err + }(); err != nil { + ctx.String(http.StatusBadRequest, "fail") + return + } + ctx.String(http.StatusOK, "OK") +} diff --git a/web/api/rechargectrl/payChannel.go b/web/api/rechargectrl/payChannel.go new file mode 100644 index 0000000..38e29de --- /dev/null +++ b/web/api/rechargectrl/payChannel.go @@ -0,0 +1,139 @@ +package rechargectrl + +import ( + "encoding/json" + "time" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/rchgchanmod" + + "github.com/gin-gonic/gin" +) + +// GetChannel doc +// @Summary 获取支付渠道 +// @Description 获取支付渠道 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/recharge/channel/list [get] +func GetChannel(ctx *gin.Context) { + infos, err := rchgchanmod.GetPayChannels() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, infos) +} + +// ChannelInsert doc +// @Summary 新增 +// @Description 新增 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Param reachargeDetails formData rchgchanmod.ChannelReq ture "product卡" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/recharge/channel/add [post] +func ChannelInsert(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := rchgchanmod.ChannelReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + pc := rchgchanmod.PayChannel{ + ChannelName: req.ChannelName, + CID: req.CID, + PayType: req.PayType, + MinMoney: req.MinMoney, + MaxMoney: req.MaxMoney, + Weight: req.Weight, + Active: req.Active, + Rate: req.Rate, + Category: req.Category, + ActivePeriod: req.ActivePeriod, + AmountTypes: req.AmountTypes, + + UpdatedAt: time.Now(), + CreatedAt: time.Now(), + } + if err := rchgchanmod.InsertPayChannel(&pc); err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.ProductManagePayType, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// ChannelUpdate doc +// @Summary 编辑充值渠道 +// @Description 编辑充值渠道 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/recharge/channel/update [post] +func ChannelUpdate(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := rchgchanmod.EditReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + cnt, err := rchgchanmod.UpdatePayChannel(req.ID, req.EditInfo) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.ProductManagePayType, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, rchgchanmod.OpeResp{Count: cnt}) +} + +// ChannelDelete doc +// @Summary 删除 +// @Description 删除 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Param id formData int true "product等级"" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/recharge/channel/delete [delete] +func ChannelDelete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := rchgchanmod.DelReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + cnt, err := rchgchanmod.DeletePayChannel(req.IDs) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.ProductManagePayType, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, rchgchanmod.OpeResp{Count: cnt}) +} diff --git a/web/api/rechargectrl/rchgorder.go b/web/api/rechargectrl/rchgorder.go new file mode 100644 index 0000000..a42d91c --- /dev/null +++ b/web/api/rechargectrl/rchgorder.go @@ -0,0 +1,84 @@ +package rechargectrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/rchgordmod" + "91porn-server/web/service/rechargeser" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" +) + +// OrderList doc +// @Summary 充值订单 +// @Description 充值订单 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /business/order/list [get] +func OrderList(ctx *gin.Context) { + var arg struct { + common.StandQuery + rchgordmod.RchgQueryReq + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + cond, opt := common.StandQueryMap(arg.StandQuery, arg.RchgQueryReq) + //兼容以前没有productType的订单 + if arg.ProductType != nil && *arg.ProductType == 0 { + cond["productType"] = bson.M{"$in": bson.A{0, nil}} + } + if arg.DistrictCode != nil { + cond["districtCode"] = arg.DistrictCode + } + total, data, err := rchgordmod.FindRechargesOrders(cond, opt) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, map[string]interface{}{ + "total": total, + "orders": rechargeser.BaiYuan(data), + }) +} + +// UpdateOrder doc +// @Summary 充值订单 +// @Description 充值订单 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /recharge/order/update [get] +func UpdateOrder(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + ID string `json:"id" form:"id" bson:"id"` + rchgordmod.RechargeUpdateReq + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = rchgordmod.UpdateRechargeOrder(p.ID, p.RechargeUpdateReq); err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.TradeManageRechargeOrder, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/rechargectrl/shark.go b/web/api/rechargectrl/shark.go new file mode 100644 index 0000000..5cf8155 --- /dev/null +++ b/web/api/rechargectrl/shark.go @@ -0,0 +1,69 @@ +package rechargectrl + +import ( + "bytes" + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/web/service/rechargeser" + + "github.com/gin-gonic/gin" +) + +// SharkCallBack 金鱼结构回调函数 +func SharkCallBack(ctx *gin.Context) { + if err := func() error { + rchg := rchgutil.SharkRes{} + s := rchgutil.Shark{} + if err := ctx.ShouldBindJSON(&rchg); err != nil { + log.Error(fmt.Sprintf("Shark callback parameter bind fail error:%+v:", err)) + return err + } + log.Info(fmt.Sprintf("Shark callback parameter data error:%+v:", rchg)) + buf := bytes.Buffer{} + buf.WriteString(strconv.Itoa(rchg.Code)) + buf.WriteString(rchg.MercID) + buf.WriteString(rchg.OID) + buf.WriteString(rchg.PayMoney) + buf.WriteString(rchg.TradeNo) + buf.WriteString(s.GetAppSecret()) + if !rchgutil.VerifySign(rchg.Sign, buf.String()) { + log.Error("Shark callback sign verify fail") + return errors.New("check sign fail") + } + payMoneyf, err := strconv.ParseFloat(rchg.PayMoney, 64) + if err != nil { + log.Error(fmt.Sprintf("Shark callback ParseFloat fail error:%+v:", err)) + return err + } + payMoneyf = payMoneyf * 100 + payMoney := int64(payMoneyf) + s.TradeNo = rchg.TradeNo + msg, err := s.QueryOrder() + if err != nil { + return err + } + loc, _ := time.LoadLocation("Local") + if msg.PayTime == "" { + return errors.New("querry order err,no payTime") + } + paymentAt, err := time.ParseInLocation("2006-01-02T15:04:05Z07:00", msg.PayTime, loc) + if err != nil { + return err + } + if err = rechargeser.RechargeCallBack(ctx, rchg.OID, payMoney, rchg.TradeNo, rchg.Code, paymentAt); err != nil { + log.Error(fmt.Sprintf("Shark RechargeCallBack fail error:%+v:", err)) + return err + } + return nil + }(); err != nil { + ctx.String(http.StatusBadRequest, "fail") + return + } + ctx.String(http.StatusOK, "success") +} diff --git a/web/api/rechargectrl/yilianfu.go b/web/api/rechargectrl/yilianfu.go new file mode 100644 index 0000000..01f200d --- /dev/null +++ b/web/api/rechargectrl/yilianfu.go @@ -0,0 +1,52 @@ +package rechargectrl + +import ( + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/web/service/rechargeser" + + "github.com/gin-gonic/gin" +) + +// YiLianFuCallBack 亿联富 +func YiLianFuCallBack(ctx *gin.Context) { + if err := func() error { + rchg := rchgutil.YiLIanFuRes{} + if err := ctx.ShouldBind(&rchg); err != nil { + log.Error(fmt.Sprintf("YiLianFu callback parameter bind fail error:%+v:", err)) + return err + } + log.Info(fmt.Sprintf("YiLianFu callback parameter data:%+v:", rchg)) + if rchg.Sign != rchg.ToSign() { + log.Error("YiLianFu callback sign verify fail") + return errors.New("check sign fail") + } + payMoneyf, err := strconv.ParseFloat(rchg.PayMoney, 64) + if err != nil { + log.Error(fmt.Sprintf("YiLianFu callback ParseFloat fail error:%+v:", err)) + return fmt.Errorf("invalid payMoney %s", rchg.PayMoney) + } + payMoneyf = payMoneyf * 100 + payMoney := int64(payMoneyf) + var status int = 200 + if rchg.ResCode != "SUCCESS" { + return nil + } + loc, _ := time.LoadLocation("Local") + paymentAt, _ := time.ParseInLocation("20060102150405", rchg.PayTime, loc) + if err = rechargeser.RechargeCallBack(ctx, rchg.OID, payMoney, rchg.TradeNo, status, paymentAt); err != nil { + log.Error(fmt.Sprintf("YiLianFu RechargeCallBack fail error:%+v:", err)) + } + return err + }(); err != nil { + ctx.String(http.StatusBadRequest, "fail") + return + } + ctx.String(http.StatusOK, "SUCCESS") +} diff --git a/web/api/rechargectrl/zhongfu.go b/web/api/rechargectrl/zhongfu.go new file mode 100644 index 0000000..ee7923f --- /dev/null +++ b/web/api/rechargectrl/zhongfu.go @@ -0,0 +1,52 @@ +package rechargectrl + +import ( + "errors" + "fmt" + "net/http" + "strconv" + "time" + + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/web/service/rechargeser" + + "github.com/gin-gonic/gin" +) + +// ZhongFuCallBack 金鱼结构回调函数 +func ZhongFuCallBack(ctx *gin.Context) { + if err := func() error { + rchg := rchgutil.ZhongFuRes{} + if err := ctx.ShouldBind(&rchg); err != nil { + log.Error(fmt.Sprintf("ZhongFu callback parameter bind fail error:%+v:", err)) + return err + } + log.Info(fmt.Sprintf("ZhongFu callback parameter data:%+v:", rchg)) + if rchg.Sign != rchg.ToSign() { + log.Error("ZhongFu callback sign verify fail") + return errors.New("check sign fail") + } + payMoneyf, err := strconv.ParseFloat(rchg.PayMoney, 64) + if err != nil { + log.Error(fmt.Sprintf("ZhongFu callback ParseFloat fail error:%+v:", err)) + return fmt.Errorf("invalid payMoney %s", rchg.PayMoney) + } + payMoneyf = payMoneyf * 100 + payMoney := int64(payMoneyf) + var status int = 200 + if rchg.Code != "00" { + return nil + } + loc, _ := time.LoadLocation("Local") + paymentAt, _ := time.ParseInLocation("20060102150405", rchg.PayTime, loc) + if err = rechargeser.RechargeCallBack(ctx, rchg.OID, payMoney, rchg.TradeNo, status, paymentAt); err != nil { + log.Error(fmt.Sprintf("ZhongFu RechargeCallBack fail error:%+v:", err)) + } + return err + }(); err != nil { + ctx.String(http.StatusBadRequest, "fail") + return + } + ctx.String(http.StatusOK, "OK") +} diff --git a/web/api/rolectrl/freevidcfg.go b/web/api/rolectrl/freevidcfg.go new file mode 100644 index 0000000..28fc869 --- /dev/null +++ b/web/api/rolectrl/freevidcfg.go @@ -0,0 +1,50 @@ +package rolectrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/rolemod" + + "github.com/gin-gonic/gin" +) + +// 权限接口 doc +// @Summary 新增权限 +// @Description 新增权限 +// @Tags 权限-配置 +// @Accept mpfd,json +// @Produce json,html +// @Param host formData string false "域名 可以为空" +// @Param path formData string true "路由路径" +// @Param method formData string false "方法 可为空" +// @Param authorized_roles formData string true "允许的角色" +// @Param allow_anyone formData string false "是否允许任何角色都可以访问" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /role/edit [post] +func Edit(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var args struct { + Incr []rolemod.Incr `form:"roles" json:"roles" binding:"required"` + } + incr := rolemod.Incr{} + if err = c.ShouldBind(&args); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if err = rolemod.UpdateRole(args.Incr); err != nil { + common.ServeJSON(c, stderr.ErrDbInsertError, err) + return + } + log, _ := json.Marshal(&incr) + _ = operatorlgmod.RecordOperation(manager, constant.RoleConfig, constant.Modify, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, nil) +} diff --git a/web/api/scenebannerctrl/banner.go b/web/api/scenebannerctrl/banner.go new file mode 100644 index 0000000..beb9fbc --- /dev/null +++ b/web/api/scenebannerctrl/banner.go @@ -0,0 +1,95 @@ +package scenebannerctrl + +import ( + "encoding/json" + "strings" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/scenebannermod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type listResp struct { + Total int `json:"total"` + List []scenebannermod.SceneBanner `json:"list"` +} + +func List(ctx *gin.Context) { + scene := strings.ToUpper(strings.TrimSpace(ctx.Query("scene"))) + if !scenebannermod.ValidScene(scene) { + common.ServeJSON(ctx, stderr.ErrParamError, "invalid scene") + return + } + list, err := scenebannermod.List(scene) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + if list == nil { + list = []scenebannermod.SceneBanner{} + } + common.ServeJSON(ctx, stderr.Success, listResp{Total: len(list), List: list}) +} + +func Add(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + banner := scenebannermod.SceneBanner{} + if err = ctx.ShouldBindJSON(&banner); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = scenebannermod.Insert(&banner); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + body, _ := json.Marshal(banner) + _ = operatorlgmod.RecordOperation(manager, "评论区Banner配置", "创建", string(body), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, banner.ID) +} + +func Edit(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + banner := scenebannermod.SceneBanner{} + if err = ctx.ShouldBindJSON(&banner); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = scenebannermod.Update(&banner); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + body, _ := json.Marshal(banner) + _ = operatorlgmod.RecordOperation(manager, "评论区Banner配置", "修改", string(body), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + id, err := primitive.ObjectIDFromHex(ctx.Query("id")) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "invalid id") + return + } + if err = scenebannermod.Delete(id); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + _ = operatorlgmod.RecordOperation(manager, "评论区Banner配置", "删除", id.Hex(), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/sensitivewordctrl/sensitivewordctrl.go b/web/api/sensitivewordctrl/sensitivewordctrl.go new file mode 100644 index 0000000..89ba8c0 --- /dev/null +++ b/web/api/sensitivewordctrl/sensitivewordctrl.go @@ -0,0 +1,373 @@ +package sensitivewordctrl + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/sensitivewordmod" + + "github.com/gin-gonic/gin" + "github.com/tealeg/xlsx" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 导入/导出模版列表头(顺序即列序) +var templateHeaders = []string{"一级分类", "词条"} + +// List doc +// @Summary 敏感词列表 +// @Description 分页查询敏感词,支持按分类、风险等级、关键词、状态筛选 +// @Tags Web-SensitiveWord +// @Accept json +// @Produce json +// @Param page query int false "页码,默认1" +// @Param size query int false "每页条数,默认20" +// @Param category query string false "一级分类" +// @Param keyword query string false "词条模糊搜索" +// @Param status query int false "状态 1-启用 0-禁用" +// @Success 200 {string} json "{"msg":"操作成功","data":{"list":[],"total":0}}" +// @Failure 400 {string} json "{"msg":"操作失败"}" +// @Router /api/web/admin/sensitive-word/list [get] +func List(ctx *gin.Context) { + var req sensitivewordmod.ListReq + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + list, total, err := sensitivewordmod.List(&req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": list, + "total": total, + }) +} + +// Add doc +// @Summary 新增敏感词 +// @Description 新增单条敏感词 +// @Tags Web-SensitiveWord +// @Accept json +// @Produce json +// @Param body body sensitivewordmod.AddReq true "敏感词信息" +// @Success 200 {string} json "{"msg":"操作成功"}" +// @Failure 400 {string} json "{"msg":"操作失败"}" +// @Router /api/web/admin/sensitive-word/add [post] +func Add(ctx *gin.Context) { + var req sensitivewordmod.AddReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + w := &sensitivewordmod.SensitiveWord{ + Category: req.Category, + Word: req.Word, + } + if err := sensitivewordmod.Add(w); err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Update doc +// @Summary 编辑敏感词 +// @Description 编辑单条敏感词,支持部分字段更新 +// @Tags Web-SensitiveWord +// @Accept json +// @Produce json +// @Param body body sensitivewordmod.UpdateReq true "更新信息,id必传" +// @Success 200 {string} json "{"msg":"操作成功"}" +// @Failure 400 {string} json "{"msg":"操作失败"}" +// @Router /api/web/admin/sensitive-word/update [post] +func Update(ctx *gin.Context) { + var req sensitivewordmod.UpdateReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + id, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "invalid id") + return + } + fields := bson.M{} + if req.Category != "" { + fields["category"] = req.Category + } + if req.Word != "" { + fields["word"] = req.Word + } + if req.Status != nil { + fields["status"] = *req.Status + } + if err := sensitivewordmod.Update(id, fields); err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Delete doc +// @Summary 批量删除敏感词 +// @Description 根据ID列表批量删除敏感词 +// @Tags Web-SensitiveWord +// @Accept json +// @Produce json +// @Param body body sensitivewordmod.DeleteReq true "ID列表" +// @Success 200 {string} json "{"msg":"操作成功","data":{"deleted":0}}" +// @Failure 400 {string} json "{"msg":"操作失败"}" +// @Router /api/web/admin/sensitive-word/delete [post] +func Delete(ctx *gin.Context) { + var req sensitivewordmod.DeleteReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + ids := make([]primitive.ObjectID, 0, len(req.IDs)) + for _, idStr := range req.IDs { + id, err := primitive.ObjectIDFromHex(idStr) + if err != nil { + continue + } + ids = append(ids, id) + } + if len(ids) == 0 { + common.ServeJSON(ctx, stderr.ErrParamError, "no valid ids") + return + } + cnt, err := sensitivewordmod.Delete(ids) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"deleted": cnt}) +} + +// Import doc +// @Summary 批量导入敏感词 +// @Description 上传 xlsx 文件批量导入敏感词,列:A-一级分类 B-词条,同词条重复导入覆盖更新 +// @Tags Web-SensitiveWord +// @Accept multipart/form-data +// @Produce json +// @Param upload formData file true "xlsx 文件,仅支持 .xls/.xlsx" +// @Success 200 {string} json "{"msg":"操作成功","data":{"success":0,"skip":0}}" +// @Failure 400 {string} json "{"msg":"操作失败"}" +// @Router /api/web/admin/sensitive-word/import [post] +func Import(ctx *gin.Context) { + file, fHeader, err := ctx.Request.FormFile("upload") + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "请上传文件") + return + } + ext := strings.ToLower(filepath.Ext(fHeader.Filename)) + if ext != ".xlsx" && ext != ".xls" { + common.ServeJSON(ctx, stderr.ErrMimeType, "仅支持 .xls/.xlsx 文件") + return + } + + pwd, _ := os.Getwd() + tmpPath := filepath.Join(pwd, "temp", fmt.Sprintf("sw_import_%d%s", time.Now().UnixNano(), ext)) + if err := os.MkdirAll(filepath.Dir(tmpPath), 0755); err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, nil) + return + } + out, err := os.Create(tmpPath) + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, nil) + return + } + defer os.Remove(tmpPath) + if _, err = out.ReadFrom(file); err != nil { + out.Close() + common.ServeJSON(ctx, stderr.ErrServerUnavailable, nil) + return + } + out.Close() + + xlFile, err := xlsx.OpenFile(tmpPath) + if err != nil { + log.Error("sensitiveword import open file fail", log.E(err)) + common.ServeJSON(ctx, stderr.ErrParamError, "文件解析失败") + return + } + + var success, skip int + for _, sheet := range xlFile.Sheets { + cols, dataStart := findSensitiveCols(sheet) + if dataStart < 0 { + log.Warn("sensitiveword import: header row not found", log.Any("sheet", sheet.Name)) + continue + } + for rowIdx, row := range sheet.Rows { + if rowIdx < dataStart { + continue + } + category := getCellText(row, cols.Category) + word := getCellText(row, cols.Word) + + // 词条必填;分类缺省视为空字符串(不强制要求) + if word == "" { + skip++ + continue + } + + w := &sensitivewordmod.SensitiveWord{ + Category: category, + Word: word, + } + if err := sensitivewordmod.UpsertByWord(w); err != nil { + log.Warn("sensitiveword import upsert fail", log.Any("word", word), log.E(err)) + skip++ + continue + } + success++ + } + } + + common.ServeJSON(ctx, stderr.Success, gin.H{ + "success": success, + "skip": skip, + }) +} + +// Export doc +// @Summary 批量导出敏感词 +// @Description 导出敏感词为 xlsx 文件,格式与导入一致,支持按分类/风险等级/关键词筛选后导出 +// @Tags Web-SensitiveWord +// @Accept json +// @Produce application/octet-stream +// @Param category query string false "一级分类" +// @Param keyword query string false "词条模糊搜索" +// @Param status query int false "状态 1-启用 0-禁用" +// @Success 200 {file} xlsx "xlsx 文件流" +// @Failure 400 {string} json "{"msg":"操作失败"}" +// @Router /api/web/admin/sensitive-word/export [get] +func Export(ctx *gin.Context) { + var req sensitivewordmod.ListReq + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + list, err := sensitivewordmod.FindAll(&req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, nil) + return + } + + f := xlsx.NewFile() + sheet, err := f.AddSheet("敏感词库") + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, nil) + return + } + + writeHeaderRow(sheet) + for _, w := range list { + row := sheet.AddRow() + row.AddCell().Value = w.Category + row.AddCell().Value = w.Word + } + + fileName := fmt.Sprintf("敏感词库_%s.xlsx", time.Now().Format("20060102150405")) + serveXlsx(ctx, f, fileName) +} + +// Template doc +// @Summary 下载敏感词导入模版 +// @Description 返回仅含表头的 xlsx 模版文件,用户填写后可通过 import 接口导入 +// @Tags Web-SensitiveWord +// @Produce application/octet-stream +// @Success 200 {file} xlsx "xlsx 文件流" +// @Failure 400 {string} json "{"msg":"操作失败"}" +// @Router /api/web/admin/sensitive-word/template [get] +func Template(ctx *gin.Context) { + f := xlsx.NewFile() + sheet, err := f.AddSheet("敏感词库") + if err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, nil) + return + } + writeHeaderRow(sheet) + serveXlsx(ctx, f, "敏感词库导入模版.xlsx") +} + +// writeHeaderRow 在 sheet 起始处写入表头行 +func writeHeaderRow(sheet *xlsx.Sheet) { + row := sheet.AddRow() + for _, h := range templateHeaders { + row.AddCell().Value = h + } +} + +// serveXlsx 把 xlsx 落到临时文件再以附件返回 +func serveXlsx(ctx *gin.Context, f *xlsx.File, fileName string) { + pwd, _ := os.Getwd() + tmpDir := filepath.Join(pwd, "temp") + _ = os.MkdirAll(tmpDir, 0755) + fpath := filepath.Join(tmpDir, fmt.Sprintf("%d_%s", time.Now().UnixNano(), fileName)) + if err := f.Save(fpath); err != nil { + log.Error("sensitiveword save xlsx fail", log.E(err)) + common.ServeJSON(ctx, stderr.ErrServerUnavailable, nil) + return + } + defer os.Remove(fpath) + + ctx.Writer.WriteHeader(http.StatusOK) + ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName)) + ctx.Header("Content-Type", "application/octet-stream") + ctx.File(fpath) +} + +// sensitiveCols 通过表头智能识别出的列索引 +type sensitiveCols struct { + Category int + Word int +} + +// findSensitiveCols 扫描前几行找出各字段所在的列索引 +// 返回值: cols 列索引集合; dataStart 数据起始行号;-1 表示未识别到表头 +// 兼容 "词条/变体" 旧表头 +func findSensitiveCols(sheet *xlsx.Sheet) (sensitiveCols, int) { + const maxHeaderScan = 5 // 表头一般在前几行 + for rowIdx, row := range sheet.Rows { + if rowIdx > maxHeaderScan { + break + } + cols := sensitiveCols{Category: -1, Word: -1} + for cellIdx, cell := range row.Cells { + text := strings.ReplaceAll(strings.TrimSpace(cell.String()), " ", "") + text = strings.ReplaceAll(text, "\n", "") + switch text { + case "一级分类", "分类": + cols.Category = cellIdx + case "词条", "词条/变体": + cols.Word = cellIdx + } + } + // 词条列识别成功才视为表头行(分类列可选,缺省时入库为空) + if cols.Word >= 0 { + return cols, rowIdx + 1 + } + } + return sensitiveCols{}, -1 +} + +// getCellText 安全地取出某列的文本,越界/未识别返回空串 +func getCellText(row *xlsx.Row, idx int) string { + if idx < 0 || idx >= len(row.Cells) { + return "" + } + return strings.TrimSpace(row.Cells[idx].String()) +} diff --git a/web/api/signctrl/config.go b/web/api/signctrl/config.go new file mode 100644 index 0000000..eb49fed --- /dev/null +++ b/web/api/signctrl/config.go @@ -0,0 +1,38 @@ +package signctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/taskmod" + "91porn-server/web/service/taskser" + + "github.com/gin-gonic/gin" +) + +func AddConfig(c *gin.Context) { + var in taskmod.AddConfigCond + if err := c.ShouldBind(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user LoginList arg error "+err.Error()) + return + } + common.ServeJSON(c, taskser.AddConfig(in), nil) +} + +func ModifyConfig(c *gin.Context) { + var in taskmod.ModifyConfigCond + if err := c.ShouldBind(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user LoginList arg error "+err.Error()) + return + } + common.ServeJSON(c, taskser.ModifyConfig(&in), nil) +} + +func QueryAllConfig(c *gin.Context) { + var in taskmod.QueryAllConfigCond + if err := c.ShouldBindQuery(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user LoginList arg error "+err.Error()) + return + } + data, code := taskser.QueryAllConfig(&in) + common.ServeJSON(c, code, data) +} diff --git a/web/api/signctrl/daily.go b/web/api/signctrl/daily.go new file mode 100644 index 0000000..46ac875 --- /dev/null +++ b/web/api/signctrl/daily.go @@ -0,0 +1,61 @@ +package signctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/dailytaskmod" + "91porn-server/web/service/taskser" + "errors" + + "github.com/gin-gonic/gin" +) + +func DailyTaskList(c *gin.Context) { + var req taskser.DailyTaskListReq + if err := c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + dailyTask, hasNext, err := taskser.GetDailyTaskList(req) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(c, stderr.Success, struct { + List []dailytaskmod.DailyTask `json:"list"` + HasNext bool `json:"hasNext"` + }{ + List: dailyTask, + HasNext: hasNext, + }) +} + +func AddDailyTask(c *gin.Context) { + var req dailytaskmod.AddDailyTaskReq + if err := c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + if err := taskser.AddDailyTask(req); err != nil { + if errors.Is(err, stderr.InsertExistError) { + common.ServeJSON(c, stderr.AddTaskRepeatedError, err) + return + } + common.ServeJSON(c, stderr.ErrDbQueryError, err) + return + } + common.ServeJSON(c, stderr.Success, nil) +} + +func EditDailyTask(c *gin.Context) { + var req dailytaskmod.EditDailyTaskReq + if err := c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + if err := taskser.EditDailyTask(req); err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, err) + return + } + common.ServeJSON(c, stderr.Success, nil) +} diff --git a/web/api/signctrl/once.go b/web/api/signctrl/once.go new file mode 100644 index 0000000..a295ff7 --- /dev/null +++ b/web/api/signctrl/once.go @@ -0,0 +1,62 @@ +package signctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/oncetaskmod" + "91porn-server/web/service/taskser" + "errors" + + "github.com/gin-gonic/gin" +) + +func OnceTaskList(c *gin.Context) { + var req commod.Page + if err := c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + onceTask, hasNext, err := taskser.GetOnceTaskList(req) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, nil) + return + } + common.ServeJSON(c, stderr.Success, struct { + List []oncetaskmod.OnceTask `json:"list"` + HasNext bool `json:"hasNext"` + }{ + List: onceTask, + HasNext: hasNext, + }) +} + +func AddOnceTask(c *gin.Context) { + var req oncetaskmod.AddOnceReq + if err := c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + if err := taskser.AddOnceTask(req); err != nil { + if errors.Is(err, stderr.InsertExistError) { + common.ServeJSON(c, stderr.AddTaskRepeatedError, err) + return + } + common.ServeJSON(c, stderr.ErrDbQueryError, err) + return + } + common.ServeJSON(c, stderr.Success, nil) +} + +func EditOnceTask(c *gin.Context) { + var req oncetaskmod.EditOnceReq + if err := c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + if err := taskser.EditOnceTask(req); err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, err) + return + } + common.ServeJSON(c, stderr.Success, nil) +} diff --git a/web/api/signctrl/sign.go b/web/api/signctrl/sign.go new file mode 100644 index 0000000..806025b --- /dev/null +++ b/web/api/signctrl/sign.go @@ -0,0 +1,67 @@ +package signctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/signtaskmod" + "91porn-server/web/service/taskser" + "github.com/gin-gonic/gin" +) + +// SignAddConfig doc +// @Summary 新增签到任务 +// @Description 新增签到任务 +// @Tags 后台-活动管理 +// @Accept mpfd,json +// @Produce json +// @Param q query signtaskmod.AddConfigCond false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/task/sign/add [post] +func SignAddConfig(c *gin.Context) { + var in signtaskmod.AddConfigCond + if err := c.ShouldBind(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user LoginList arg error "+err.Error()) + return + } + common.ServeJSON(c, taskser.SignAddConfig(in), nil) +} + +// SignModifyConfig doc +// @Summary 修改签到任务 +// @Description 修改签到任务 +// @Tags 后台-活动管理 +// @Accept mpfd,json +// @Produce json +// @Param q query signtaskmod.ModifyConfigCond false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/task/sign/edit [post] +func SignModifyConfig(c *gin.Context) { + var in signtaskmod.ModifyConfigCond + if err := c.ShouldBind(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user LoginList arg error "+err.Error()) + return + } + common.ServeJSON(c, taskser.SignModifyConfig(&in), nil) +} + +// SignQueryAllConfig doc +// @Summary 获取签到任务列表 +// @Description 获取签到任务列表 +// @Tags 后台-活动管理 +// @Accept mpfd,json +// @Produce json +// @Param q query signtaskmod.QueryAllConfigCond false "请求参数" +// @Success 200 object taskser.SignQueryAllConfigResp "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/task/sign/list [get] +func SignQueryAllConfig(c *gin.Context) { + var in signtaskmod.QueryAllConfigCond + if err := c.ShouldBindQuery(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user LoginList arg error "+err.Error()) + return + } + data, code := taskser.SignQueryAllConfig(&in) + common.ServeJSON(c, code, data) +} diff --git a/web/api/sourcectrl/source.go b/web/api/sourcectrl/source.go new file mode 100644 index 0000000..494193c --- /dev/null +++ b/web/api/sourcectrl/source.go @@ -0,0 +1,138 @@ +package sourcectrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/sourcemod" + "91porn-server/web/webg" + "encoding/json" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// SourceAdd doc +// @Summary 资源域名 +// @Description 添加资源域名 +// @Tags source +// @Accept mpfd,json +// @Produce json,html +// @Param domain formData string true "域名" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/source/add [post] +func SourceAdd(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + p := sourcemod.Source{} + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = sourcemod.Insert(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + if p.Type == sourcemod.Vid { + common.Go(func() { + _, _ = webg.Redis.Del(redisconst.CdnCacheKey) + }) + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageSource, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// SourceList doc +// @Summary 资源域名 +// @Description 资源域名列表 +// @Tags source +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query integer true "当前页" mininum(1) +// @Param pageSize query integer true "每页条数" mininum(1) +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/source/list [post] +func SourceList(ctx *gin.Context) { + result := sourcemod.WebList() + common.ServeJSON(ctx, stderr.Success, result) +} + +// SourceDel doc +// @Summary 资源域名 +// @Description 删除资源域名 +// @Tags source +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData array true "id数组 字符串数组"" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/source/del [delete] +func SourceDel(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + type args struct { + IDs []primitive.ObjectID `form:"ids" json:"ids"` + } + param := args{} + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = sourcemod.Remove(param.IDs); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + common.Go(func() { + _, _ = webg.Redis.Del(redisconst.CdnCacheKey) + }) + log, _ := json.Marshal(param) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageSource, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// SourceEdit doc +// @Summary 资源域名修改 +// @Description 资源域名修改 +// @Tags source +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/source/edit [post] +func SourceEdit(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var param struct { + ID primitive.ObjectID `form:"id" json:"id" binding:"required"` + sourcemod.SourceEdit + } + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = sourcemod.EditDomain(param.ID, ¶m.SourceEdit); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, "") + return + } + common.Go(func() { + _, _ = webg.Redis.Del(redisconst.CdnCacheKey) + _, _ = webg.Redis.Del(redisconst.LandDomainCacheKey) + }) + log, _ := json.Marshal(param) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageSource, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/statcenterctl/statsync.go b/web/api/statcenterctl/statsync.go new file mode 100644 index 0000000..711d186 --- /dev/null +++ b/web/api/statcenterctl/statsync.go @@ -0,0 +1,373 @@ +package statcenterctl + +import ( + "net/http" + "strconv" + "time" + + "91porn-server/app/proto" + "91porn-server/common/log" + "91porn-server/models/l/visitlogmod" + "91porn-server/models/v/proxymod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/usermod" + + "github.com/gin-gonic/gin" +) + +// SyncAllRouterList 返回所有同步接口路由 +func SyncAllRouterList(ctx *gin.Context) { + //返回实体 + var resp = gin.H{ + "code": 200, + "msg": "ok", + "data": gin.H{ + "syncNewUser": "/services/user/newUser", + "syncUserAccess": "/services/user/userAccess", + "syncInviteRecord": "/services/user/inviteRecord", + "syncNewOrder": "/services/order/newOrder", + "syncFinishOrder": "/services/order/finishOrder", + }, + } + ctx.JSON(http.StatusOK, resp) +} + +// NewUserAccessList 新日活数据同步 +func NewUserAccessList(ctx *gin.Context) { + //返回实体 + var resp struct { + Code int `json:"code"` // 200正常 其他异常 + Msg string `json:"msg"` // 错误消息 + Data []proto.NewUserAccessMsg `json:"data"` // 数据 + proto.AdsInfo + } + resp.Code = 200 + resp.Msg = "ok" + //请求实体 + var request struct { + EndTime int64 `form:"updated_at" json:"updated_at"` // 时间戳 + MaxSize int64 `form:"max" json:"max" binding:"required"` // 最大条数 + } + if err := ctx.ShouldBind(&request); err != nil { + resp.Code = 400 + resp.Msg = err.Error() + ctx.JSON(http.StatusOK, resp) + return + } + t := time.Unix(request.EndTime, 0) + //查询 + visitList, err := visitlogmod.AccessSyncByTime(t, request.MaxSize) + if err != nil { + log.Error("NewUserAccessList AccessSyncById ", log.E(err)) + resp.Code = 400 + resp.Msg = err.Error() + ctx.JSON(http.StatusOK, resp) + return + } + //用户id + visitListLen := len(visitList) + uidIds := make([]uint64, visitListLen) + accessList := make([]proto.NewUserAccessMsg, visitListLen) + for i, userInfo := range visitList { + accessList[i] = proto.NewUserAccessMsg{ + UserId: userInfo.UID, + DeviceId: userInfo.DevID, + Platform: userInfo.SysType, + Coin: 0, + IsVip: 0, + InviteUserId: 0, + LastOnlineTime: userInfo.CreatedAt.Unix(), + } + uidIds[i] = userInfo.UID + } + if len(uidIds) > 0 { + //查询用户上级信息 + invitationMap, err := proxymod.GetInvrLv1Map(uidIds) + if err != nil { + log.Error("NewUserAccessList proxymod.GetInvrLv1Map err ", log.E(err)) + resp.Code = 400 + resp.Msg = err.Error() + ctx.JSON(http.StatusOK, resp) + return + } + //查询用户信息 + usersMap, err := usermod.UserMap(uidIds) + if err != nil { + log.Error("NewUserRegisterList usermod.UserMap err ", log.E(err)) + resp.Code = 400 + resp.Msg = err.Error() + ctx.JSON(http.StatusOK, resp) + return + } + for index, r := range accessList { + //上级id + if invitationMap != nil { + fUid := invitationMap[r.UserId] + if fUid > 0 { + accessList[index].InviteUserId = fUid + } + } + //用户信息 + if usersMap != nil { + user := usersMap[r.UserId] + if user.UID > 0 { + accessList[index].RegistTime = user.CreatedAt.Unix() + accessList[index].RegisterIp = user.RegisterIP + accessList[index].ChannelCode = user.DistrictCode + if user.VipExpireDate.After(time.Now()) { + accessList[index].IsVip = 1 + } + } + } + } + } + resp.Data = accessList + ctx.JSON(http.StatusOK, resp) +} + +// NewUserRegisterList 新注册数据同步 +func NewUserRegisterList(ctx *gin.Context) { + //返回实体 + var resp struct { + Code int `json:"code"` // 200正常 其他异常 + Msg string `json:"msg"` // 错误消息 + Data []proto.NewUserRegisterMsg `json:"data"` // 数据 + } + resp.Code = 200 + resp.Msg = "ok" + //请求实体 + var request struct { + UserId uint64 `form:"id" json:"id"` // 用户ID + MaxSize int64 `form:"max" json:"max" binding:"required"` // 最大条数 + } + if err := ctx.ShouldBind(&request); err != nil { + resp.Code = 400 + resp.Msg = err.Error() + ctx.JSON(http.StatusOK, resp) + return + } + //查询 + userList, err := usermod.StatcenterSyncList(request.UserId, request.MaxSize) + if err != nil { + log.Error("NewUserRegisterList usermod.StatcenterSyncList err ", log.E(err)) + resp.Code = 400 + resp.Msg = err.Error() + ctx.JSON(http.StatusOK, resp) + return + } + //用户id + userListLen := len(userList) + uidIds := make([]uint64, userListLen) + registerList := make([]proto.NewUserRegisterMsg, userListLen) + for i, userInfo := range userList { + registerInfo := proto.NewUserRegisterMsg{ + UserId: userInfo.UID, + DeviceId: userInfo.DevID, + Platform: userInfo.SysType, + Coin: 0, + IsVip: 0, + InviteUserId: 0, + RegistTime: userInfo.CreatedAt.Unix(), + RegisterIp: userInfo.RegisterIP, + ChannelCode: userInfo.DistrictCode, + LastOnlineTime: userInfo.LastVisitAt.Unix(), + } + if userInfo.VipExpireDate.After(time.Now()) { + registerInfo.IsVip = 1 + } + registerList[i] = registerInfo + uidIds[i] = userInfo.UID + } + if len(uidIds) > 0 { + //查询用户上级信息 + invitationMap, err := proxymod.GetInvrLv1Map(uidIds) + if err != nil { + log.Error("NewUserRegisterList proxymod.GetInvrLv1Map err ", log.E(err)) + resp.Code = 400 + resp.Msg = err.Error() + ctx.JSON(http.StatusOK, resp) + return + } + if invitationMap != nil { + for index, r := range registerList { + //上级id + fUid := invitationMap[r.UserId] + if fUid > 0 { + registerList[index].InviteUserId = fUid + } + } + } + } + resp.Data = registerList + ctx.JSON(http.StatusOK, resp) +} + +// NewUserInviterList 新推广数据同步 +func NewUserInviterList(ctx *gin.Context) { + //返回实体 + var resp struct { + Code int `json:"code"` // 200正常 其他异常 + Msg string `json:"msg"` // 错误消息 + Data []interface{} `json:"data"` // 数据 + } + resp.Code = 200 + resp.Msg = "ok" + //请求实体 + var request struct { + Id string `form:"id" json:"id" binding:"required"` // 记录id + MaxSize int64 `form:"max" json:"max" binding:"required"` // 最大条数 + } + if err := ctx.ShouldBind(&request); err != nil { + resp.Code = 400 + resp.Msg = err.Error() + ctx.JSON(http.StatusOK, resp) + return + } + //查询 + data, err := proxymod.NewStatCenterSyncInviteList(request.Id, request.MaxSize) + if err != nil { + log.Error("NewUserInviterList proxymod.StatCenterSyncInviteList err ", log.E(err)) + resp.Code = 400 + resp.Msg = err.Error() + ctx.JSON(http.StatusOK, resp) + return + } + res := make([]interface{}, len(data)) + for i, v := range data { + res[i] = proto.NewUserInviteMsg{ + Id: v.ID.Hex(), + UserId: v.Invitee, + InviteUserId: v.UID, + } + } + resp.Data = res + ctx.JSON(http.StatusOK, resp) +} + +// NewUserRechargeList 新充值数据同步 +func NewUserRechargeList(ctx *gin.Context) { + //返回实体 + var resp struct { + Code int `json:"code"` // 200正常 其他异常 + Msg string `json:"msg"` // 错误消息 + Data []proto.NewUserRechargeMsg `json:"data"` // 数据 + } + resp.Code = 200 + resp.Msg = "ok" + //请求实体 + var request struct { + Id string `form:"id" json:"id" binding:"required"` // 记录id + MaxSize int64 `form:"max" json:"max" binding:"required"` // 最大条数 + } + if err := ctx.ShouldBind(&request); err != nil { + resp.Code = 400 + resp.Msg = err.Error() + ctx.JSON(http.StatusOK, resp) + return + } + //查询 + rechargeOrders, err := rchgordmod.NewStatCenterSyncRecharge(request.Id, request.MaxSize) + if err != nil { + log.Error("NewUserRechargeList rchgordmod.NewStatCenterSyncRecharge err ", log.E(err)) + resp.Code = 400 + resp.Msg = err.Error() + ctx.JSON(http.StatusOK, resp) + return + } + data := make([]proto.NewUserRechargeMsg, len(rechargeOrders)) + for i, v := range rechargeOrders { + temp := proto.NewUserRechargeMsg{ + Id: v.ID.Hex(), + OID: v.OID, + OrderId: v.ID.Hex(), + UserId: v.UID, + Money: v.Money, + Channel: v.Channel, + RechargeType: v.RechargeType, + OrderType: "vip", + OrderTime: v.CreatedAt.Unix(), + PayTime: v.SuccessAt.Unix(), + PayAmount: v.PayMoney, + IsPaid: 0, + Poundage: 0, + ProductType: v.ProductType, + } + if temp.OID == "" { + temp.OID = strconv.FormatInt(int64(temp.UserId), 10) + temp.OID + } + //是否vip + if v.Category == 0 { + temp.OrderType = "coin" + } + if v.Status == 3 { + temp.IsPaid = 1 + } else { + temp.PayTime = 0 + } + data[i] = temp + } + resp.Data = data + ctx.JSON(http.StatusOK, resp) +} + +// NewUserPaidRechargeList 新充值数据同步,已完成 +func NewUserPaidRechargeList(ctx *gin.Context) { + //返回实体 + var resp struct { + Code int `json:"code"` // 200正常 其他异常 + Msg string `json:"msg"` // 错误消息 + Data []proto.NewUserRechargeMsg `json:"data"` // 数据 + } + resp.Code = 200 + resp.Msg = "ok" + //请求实体 + var request struct { + SuccessAt int64 `form:"success_at" json:"success_at"` // 完成时间 + MaxSize int64 `form:"max" json:"max" binding:"required"` // 最大条数 + } + if err := ctx.ShouldBind(&request); err != nil { + resp.Code = 400 + resp.Msg = err.Error() + ctx.JSON(http.StatusOK, resp) + return + } + t := time.Unix(request.SuccessAt, 0) + //查询 + rechargeOrders, err := rchgordmod.StatCenterSyncRecharge(t, request.MaxSize) + if err != nil { + log.Error("NewUserRechargeList rchgordmod.StatCenterSyncRecharge err ", log.E(err)) + resp.Code = 400 + resp.Msg = err.Error() + ctx.JSON(http.StatusOK, resp) + return + } + data := make([]proto.NewUserRechargeMsg, len(rechargeOrders)) + for i, v := range rechargeOrders { + temp := proto.NewUserRechargeMsg{ + Id: v.ID.Hex(), + OID: v.OID, + OrderId: v.ID.Hex(), + UserId: v.UID, + Money: v.Money, + Channel: v.Channel, + RechargeType: v.RechargeType, + OrderType: "vip", + OrderTime: v.CreatedAt.Unix(), + PayTime: v.SuccessAt.Unix(), + PayAmount: v.PayMoney, + IsPaid: 1, + Poundage: 0, + ProductType: v.ProductType, + } + if temp.OID == "" { + temp.OID = strconv.FormatInt(int64(temp.UserId), 10) + temp.OID + } + //是否vip + if v.Category == 0 { + temp.OrderType = "coin" + } + data[i] = temp + } + resp.Data = data + ctx.JSON(http.StatusOK, resp) +} diff --git a/web/api/statctrl/orderCtrl.go b/web/api/statctrl/orderCtrl.go new file mode 100644 index 0000000..5d473f4 --- /dev/null +++ b/web/api/statctrl/orderCtrl.go @@ -0,0 +1,91 @@ +package statctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/s/statordermod" + "91porn-server/web/service/statser" + "github.com/gin-gonic/gin" + "time" +) + +// OrderStats doc +// @Summary 订单统计 +// @Description 订单统计 +// @Tags statctrl +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query integer true "当前页" +// @Param pageSize query integer true "每页条数" +// @Param type query integer true "订单类型" +// @Param channel query string false "渠道" +// @Param start query string false "开始时间UTC时间戳" +// @Param end query string false "结束时间UTC时间戳" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/stats/order/list [get] +func OrderStats(ctx *gin.Context) { + type Query struct { + Type *statordermod.OrderType `form:"type" json:"type"` //订单类型 + Channel *string `form:"channel" json:"channel"` + Start *time.Time `form:"start" json:"start"` + End *time.Time `form:"end" json:"end"` + } + var arg struct { + commod.Page + Query + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "stat VideoEarnStats arg error "+err.Error()) + return + } + endTime := time.Now() + if arg.End != nil { + endTime = *arg.End + } + var startTime time.Time + if arg.Start != nil { + startTime = *arg.Start + } + skip := int64((arg.PageNumber - 1) * arg.PageSize) + limit := int64(arg.PageSize) + page, err := statser.OrderPages(skip, limit, + statser.OrderChannelMatch{arg.Channel}, + statser.OrderTypeMatch{arg.Type}, + statser.OrderSumDateGTEAndLTMatch{&startTime, &endTime}, + ) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, "statctrl OrderPages error: "+err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, page) +} + +// RchSucRate doc +// @Summary 充值成功率 +// @Description 充值成功率 +// @Tags statctrl +// @Accept mpfd,json +// @Produce json,html +// @Param sumDate query string true "日期" +// @Param channel query string true "渠道" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/stats/order/sucRate [post] +func RchSucRate(ctx *gin.Context) { + var args struct { + SumDate time.Time `form:"sumDate" json:"sumDate" binding:"required"` + Channel string `form:"channel" json:"channel" binding:"required"` + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "stat RchSucRate arg error "+err.Error()) + return + } + rchSucRateMap, err := statser.RechargeSucRateMap(args.SumDate, args.Channel) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, "statctrl OrderPages error: "+err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, rchSucRateMap) +} diff --git a/web/api/statctrl/tarnsCtrl.go b/web/api/statctrl/tarnsCtrl.go new file mode 100644 index 0000000..0406e94 --- /dev/null +++ b/web/api/statctrl/tarnsCtrl.go @@ -0,0 +1,124 @@ +package statctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/web/service/statser" + "bytes" + "encoding/csv" + "fmt" + "strconv" + "time" + + "github.com/gin-gonic/gin" +) + +func DataStat(ctx *gin.Context) { + var args struct { + Start time.Time `form:"startTime" json:"startTime" binding:"required"` + End time.Time `form:"endTime" json:"endTime" binding:"required"` + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "stat RchSucRate arg error "+err.Error()) + return + } + res, err, titles := statser.DataStat(args.Start, args.End) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, "DataStat DataStat error: "+err.Error()) + return + } + var ( + headList []string + dataBytes = new(bytes.Buffer) + ) + headList = []string{"时间", "楼凤", "vip充值", "金币充值", "新增用户"} + for i := range titles { + headList = append(headList, titles[i]+"金额", titles[i]+"-数量") + } + headList = append(headList, titles...) + //设置编码格式 + dataBytes.WriteString("\xEF\xBB\xBF") + wr := csv.NewWriter(dataBytes) + wr.Write(headList) + for _, v := range res { + bodyList := []string{ + v.SumData, + strconv.FormatInt(v.LoufengTrade, 10), + strconv.FormatInt(v.VIPRecharge, 10), + strconv.FormatInt(v.CoinRecharge, 10), + strconv.FormatInt(v.NewUser, 10), + } + vipTrans := make([]string, 0) + for i := range titles { + if len(v.VipTrande) < 1 { + vipTrans = append(vipTrans, "0", "0") + } else if vipTrande, ok := v.VipTrande[titles[i]]; ok { + vipTrans = append(vipTrans, fmt.Sprintf("%d", vipTrande.Amount), fmt.Sprintf("%d", vipTrande.Count)) + } else { + vipTrans = append(vipTrans, "0", "0") + } + } + bodyList = append(bodyList, vipTrans...) + wr.Write(bodyList) + } + //清空 + wr.Flush() + ctx.Writer.Header().Set("Content-type", "application/octet-stream") + //c.Writer.Header().Set("Content-Type", "text/csv") + ctx.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment;filename=%s", "companys.csv")) + ctx.String(200, dataBytes.String()) + return +} +func VidStat(ctx *gin.Context) { + var args struct { + Start time.Time `form:"startTime" json:"startTime" binding:"required"` + End time.Time `form:"endTime" json:"endTime" binding:"required"` + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "stat RchSucRate arg error "+err.Error()) + return + } + var ( + headList []string + dataBytes = new(bytes.Buffer) + ) + headList = []string{"时间", "短视频销售额", "短视频数量", "长视频销售额", "长视频数量"} + //设置编码格式 + dataBytes.WriteString("\xEF\xBB\xBF") + wr := csv.NewWriter(dataBytes) + wr.Write(headList) + start := args.Start + end := args.End + + for start.Before(end) { + head := start + tail := head.Add(time.Hour * 24 * 15) + if tail.After(end) { + tail = end + } + res, err := statser.PayVidStat(head, tail) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, "statser PayVidStat error: "+err.Error()) + return + } + + for _, v := range res { + bodyList := []string{ + v.SumData, + strconv.FormatInt(v.ShortVidAmount, 10), + strconv.FormatInt(v.ShortVidCount, 10), + strconv.FormatInt(v.LongVidAmount, 10), + strconv.FormatInt(v.LongVidCount, 10), + } + wr.Write(bodyList) + } + start = tail + } + + //清空 + wr.Flush() + ctx.Writer.Header().Set("Content-type", "application/octet-stream") + ctx.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment;filename=%s", "videoSales.csv")) + ctx.String(200, dataBytes.String()) + return +} diff --git a/web/api/statctrl/userCtrl.go b/web/api/statctrl/userCtrl.go new file mode 100644 index 0000000..1994eaf --- /dev/null +++ b/web/api/statctrl/userCtrl.go @@ -0,0 +1,62 @@ +package statctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "time" + + "github.com/gin-gonic/gin" +) + +// DailyStats doc +// @Summary 用户登陆统计 +// @Description 从指定点时间点开始统计用户登陆 +// @Tags web-统计管理 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query integer true "当前页" +// @Param pageSize query integer true "每页条数" +// @Param startTime query integer false "开始时间UTC时间戳" +// @Param endTime query integer false "结束时间UTC时间戳" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/stats/user/daily/list [get] +func DailyStats(c *gin.Context) { + type Query struct { + StartTime *time.Time `form:"startTime" json:"startTime" binding:""` //开始时间 + EndTime *time.Time `form:"endTime" json:"endTime" binding:""` //结束时间 + ExpansionFactor *int64 `form:"expansionFactor" json:"expansionFactor" binding:"omitempty,min=1"` //膨胀系数 + } + var arg struct { + commod.Page + Query + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "DailyStats arg error "+err.Error()) + return + } + + //endTime := time.Now() + //if arg.EndTime != nil { + // endTime = *arg.EndTime + //} + // + //var startTime time.Time + //if arg.StartTime != nil { + // startTime = *arg.StartTime + //} + // + //skip := int64((arg.PageNumber - 1) * arg.PageSize) + //limit := int64(arg.PageSize) + //var expansionFactor int64 + //if arg.ExpansionFactor != nil { + // expansionFactor = *arg.ExpansionFactor + //} + //page, err := statser.DailyStatsPage(skip, limit, startTime, endTime, expansionFactor) + //if err != nil { + // common.ServeJSON(c, stderr.Failure, "statctrl DailyStatsPage error "+err.Error()) + // return + //} + common.ServeJSON(c, stderr.Success, "") +} diff --git a/web/api/statctrl/videoCtrl.go b/web/api/statctrl/videoCtrl.go new file mode 100644 index 0000000..8aedcb6 --- /dev/null +++ b/web/api/statctrl/videoCtrl.go @@ -0,0 +1,159 @@ +package statctrl + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/commod" + "91porn-server/models/s/statvidmod" + "91porn-server/models/v/vidmod" + "91porn-server/web/service/deduction" + "91porn-server/web/service/payvidlgser" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const VideoAggregateStatsTopN = 20 + +// VideoEarnStats doc +// @Summary 视屏收入统计 +// @Description 统计从指定时间区间的视屏收入 +// @Tags web-统计管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid query string true "用户ID" +// @Param startTime query string true "开始时间" +// @Param endTime query string true "结束时间" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/stats/video/earn/list [get] +func VideoEarnStats(c *gin.Context) { + type Query struct { + StartTime *time.Time `form:"startTime" json:"startTime,omitempty" binding:""` //开始时间 + EndTime *time.Time `form:"endTime" json:"endTime,omitempty" binding:""` //结束时间 + UID *uint64 `form:"uid" json:"uid" binding:""` //uid + Vid *string `form:"vid" json:"vid" binding:""` //vid + } + var arg struct { + commod.Page + Query + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "stat VideoEarnStats arg error "+err.Error()) + return + } + endTime := time.Now() + if arg.EndTime != nil { + endTime = timerange.LocDayRange(*arg.EndTime).Head + } + startTime := time.Time{} + if arg.StartTime != nil { + startTime = timerange.LocDayRange(*arg.StartTime).Head + } + var vid *primitive.ObjectID + if arg.Vid != nil { + id, _ := primitive.ObjectIDFromHex(*arg.Vid) + vid = &id + } + var vids []primitive.ObjectID + if arg.UID != nil { + list, _ := vidmod.FindMany((&vidmod.PublisherIDMatch{PublisherID: arg.UID}).New()) + vids = list.IDs() + } + limit := int64(arg.PageSize) + skip := int64((arg.PageNumber - 1) * arg.PageSize) + page, err := statvidmod.VideoStatPages(skip, limit, + statvidmod.SumDateGTEAndLTMatch{GTE: &startTime, LT: &endTime}, + statvidmod.VidMatch{Vid: vid}, + statvidmod.VidInMatch{Vids: vids}, + ) + if err != nil { + common.ServeJSON(c, stderr.Failure, "stat VideoEarnStats Failure, error: !"+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} + +// VideoEarnStats doc +// @Summary 视频收入聚合统计排序 +// @Description 聚合统计指定时间区间内视频收入,取总收益前20展示 +// @Tags web-统计管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid query string true "用户ID" +// @Param startTime query string true "开始时间" +// @Param endTime query string true "结束时间" +// @Success 200 {object} payvidlgser.VideoCoinIncomeAggregateTopNStat "success" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/stats/video/earn/aggregate [get] +func VideoAggregateStats(c *gin.Context) { + type Query struct { + StartTime *time.Time `form:"startTime" json:"startTime,omitempty" binding:""` //开始时间 + EndTime *time.Time `form:"endTime" json:"endTime,omitempty" binding:""` //结束时间 + UID *uint64 `form:"uid" json:"uid" binding:""` //uid + } + args := &Query{} + if err := c.ShouldBind(args); err != nil { + common.ServeJSON(c, stderr.ErrParamError, fmt.Sprintf("stat VideoAggregateStats arg error %s", err.Error())) + return + } + endTime := time.Now() + if args.EndTime != nil { + endTime = timerange.LocDayRange(*args.EndTime).Head + } + //最多允许查半年内的数据,防止数据量过大 + startTime := endTime.AddDate(0, -6, 0) + if args.StartTime != nil && args.StartTime.After(startTime) { + startTime = timerange.LocDayRange(*args.StartTime).Head + } + stat, err := payvidlgser.VideoCoinIncomeAggregateTopN(VideoAggregateStatsTopN, startTime, endTime) + if err != nil { + common.ServeJSON(c, stderr.Failure, "stat VideoAggregateStats Failure, error: !"+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, stat) +} + +// VideoPublisherDeductionStats doc +// @Summary 视频博主扣量前后视频总售卖 +// @Description 统计博主扣量前视频总售卖,扣量后总售卖 +// @Tags web-统计管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid query string true "用户ID" +// @Param startTime query string true "开始时间" +// @Param endTime query string true "结束时间" +// @Success 200 {string} json deduction.PublisherDeduction +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/stats/video/publisher/deduction [get] +func VideoPublisherDeductionStats(c *gin.Context) { + args := &deduction.Query{} + if err := c.ShouldBind(args); err != nil { + common.ServeJSON(c, stderr.ErrParamError, + fmt.Sprintf("stat VideoPublisherDeductionStats arg error %s", err.Error())) + return + } + if args.EndTime == nil { + *args.EndTime = timerange.LocDayRange(time.Now()).Head + } + // 早于这之前的历史数据无法查询 + startTime := time.Date(2021, time.October, 18, 23, 59, 59, 0, time.Local) + if args.StartTime != nil && args.StartTime.Before(startTime) { + common.ServeJSON(c, stderr.ErrParamError, fmt.Sprintf("stat VideoPublisherDeductionStats error %s", "start time too early")) + return + } + if args.EndTime.Before(*args.StartTime) { + common.ServeJSON(c, stderr.ErrParamError, fmt.Sprintf("stat VideoPublisherDeductionStats error %s", "start time is after end time")) + return + } + stat, err := deduction.StatPublisherDeduction(args) + if err != nil { + common.ServeJSON(c, stderr.Failure, fmt.Sprintf("stat VideoPublisherDeductionStats error %s", err.Error())) + return + } + common.ServeJSON(c, stderr.Success, stat) +} diff --git a/web/api/synccdnctrl/syncCtrl.go b/web/api/synccdnctrl/syncCtrl.go new file mode 100644 index 0000000..3bd9e72 --- /dev/null +++ b/web/api/synccdnctrl/syncCtrl.go @@ -0,0 +1,59 @@ +package synccdnctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/synccdnmod" + + "github.com/gin-gonic/gin" +) + +// SyncCdn doc +// @Summary +// @Description 同步m3u8到cdn节点 +// @Tags WEB-uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param token formData string true "调用此接口的token"" +// @Param pageSize formData integer true "视频来源" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/vid/syncCdn [post] +func SyncCdn(ctx *gin.Context) { + param := synccdnmod.SyncReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if param.Token != "NS@33E7r4GUW0Op3" { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + //code, data := synccdnser.SyncCdn() + common.ServeJSON(ctx, stderr.Failure, "") +} + +// SyncWithOptions doc +// @Summary +// @Description SyncWithOptions 预热cdn +// @Tags WEB-uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param cdn formData string true "cdn名字" +// @Param token formData string true "调用此接口的token"" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/vid/SyncWithOptions [post] +func SyncWithOptions(ctx *gin.Context) { + param := synccdnmod.SyncReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if param.Token != "seFMthomcQegB513" { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + //code, data := synccdnser.SyncWithOptions(param.Cdn, param.FilePath) + common.ServeJSON(ctx, stderr.Failure, "") +} diff --git a/web/api/sysconfctrl/sysconf.go b/web/api/sysconfctrl/sysconf.go new file mode 100755 index 0000000..26b7bc9 --- /dev/null +++ b/web/api/sysconfctrl/sysconf.go @@ -0,0 +1,178 @@ +package sysconfctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/web/service/sysconfser" + + "github.com/gin-gonic/gin" +) + +// All doc +// @Summary 获取系统通用配置列表 +// @Description 获取系统通用配置列表 +// @Tags 后台-系统通用配置 +// @Accept mpfd,json +// @Produce json +// @Param q query sysconfser.WebListReq false "请求参数" +// @Success 200 object sysconfser.WebListRes "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/sysconf/all [get] +func All(ctx *gin.Context) { + var req = &sysconfser.WebListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + res, err := req.GetAll() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + + common.ServeJSON(ctx, stderr.Success, res) +} + +// Create doc +// @Summary 新增系统通用配置 +// @Description 新增系统通用配置 +// @Tags 后台-系统通用配置 +// @Accept mpfd,json +// @Produce json +// @Param q body sysconfser.WebCreateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/sysconf/create [post] +func Create(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &sysconfser.WebCreateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 创建 + err = p.Create() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "系统通用配置管理", "创建", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Update doc +// @Summary 更新系统通用配置 +// @Description 更新系统通用配置 +// @Tags 后台-系统通用配置 +// @Accept mpfd,json +// @Produce json +// @Param q body sysconfser.WebUpdateReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/sysconf/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &sysconfser.WebUpdateReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "系统通用配置管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// Delete doc +// @Summary 删除系统通用配置 +// @Description 删除系统通用配置 +// @Tags 后台-系统通用配置 +// @Accept mpfd,json +// @Produce json +// @Param q body sysconfser.WebDeleteReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/sysconf/delete [post] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &sysconfser.WebDeleteReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + // 删除 + err = p.Delete() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "系统通用配置管理", "删除", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +// UpdateSelect doc +// @Summary 更新系统配置下拉框选项 +// @Description 更新系统配置下拉框选项 +// @Tags 后台-系统通用配置 +// @Accept mpfd,json +// @Produce json +// @Param q body sysconfser.WebUpdateSelectReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/sysconf/update/select [post] +func UpdateSelect(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + + p := &sysconfser.WebUpdateSelectReq{} + err = ctx.ShouldBindJSON(&p) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + // 更新 + err = p.Update() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, "系统通用配置管理", "更新", string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/tagctrl/recmdCtrl.go b/web/api/tagctrl/recmdCtrl.go new file mode 100644 index 0000000..a7eba96 --- /dev/null +++ b/web/api/tagctrl/recmdCtrl.go @@ -0,0 +1,222 @@ +package tagctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/recmdtag" + "91porn-server/models/v/tagmod" + "91porn-server/web/service/tagser" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// RecmdList doc +// @Summary 推荐标签列表 +// @Description 推荐标签列表 +// @Tags Web-Tag +// @Accept mpfd,json +// @Produce json,html +// @Param types query string false "种类" +// @Param name query string false "名字" +// @Param pageNumber query integer true "页码" +// @Param pageSize query integer true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/recmd/list [get] +func RecmdList(ctx *gin.Context) { + type Query struct { + Types *recmdtag.RecmdType `form:"types" json:"types"` //种类 + Name *string `form:"name" json:"name"` //名字 + } + var args struct { + commod.Page + Query + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "tagctrl RecmdList arg error "+err.Error()) + return + } + skip := int64((args.PageNumber - 1) * args.PageSize) + limit := int64(args.PageSize) + page, err := tagser.RecmdTagPages(skip, limit, + recmdtag.TypesMatch{Types: args.Types}, + recmdtag.NameMatch{Name: args.Name}, + ) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, "tagctrl RecmdTagPages error: "+err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, page) +} + +// RecmdAdd doc +// @Summary 新增推荐标签 +// @Description 新增推荐标签 +// @Tags Web-Tag +// @Accept mpfd,json +// @Produce json,html +// @Param types formData int true "种类" +// @Param name formData string true "名字" +// @Param sortCode formData int true "倒序" +// @Param enable formData bool true "使能" +// @Param remark formData string true "备注" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/recmd/add [post] +func RecmdAdd(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var args struct { + TID primitive.ObjectID `form:"id" json:"id" binding:"required"` //种类 + Types *recmdtag.RecmdType `form:"types" json:"types" binding:"required"` //种类 + SortCode *int64 `form:"sortCode" json:"sortCode" binding:"required"` //正序 + Enable *bool `form:"enable" json:"enable" binding:"required"` //使能 true:开 + Remark string `form:"remark" json:"remark" binding:""` //备注 + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "tagctrl RecmdAdd arg error "+err.Error()) + return + } + tag, _ := tagmod.FindOneTagByID(args.TID) + if tag.ID.IsZero() { + common.ServeJSON(ctx, stderr.TagNotExist, "") + return + } + doc := recmdtag.InsertDoc{ + Types: *args.Types, + TID: tag.ID, + Name: tag.TagName, + SortCode: *args.SortCode, + Enable: *args.Enable, + Remark: args.Remark, + } + if err = recmdtag.InsertOne(doc); err != nil { + switch err.(type) { + case recmdtag.ErrRecmdTagHasExist: + common.ServeJSON(ctx, stderr.RecmdTagIsExisted, err.Error()) + default: + common.ServeJSON(ctx, stderr.Failure, err.Error()) + } + return + } + log, _ := json.Marshal(args) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageTag, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// RecmdUpdate doc +// @Summary 编辑推荐标签 +// @Description 编辑推荐标签 +// @Tags Web-Tag +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "ID" +// @Param types formData int false "种类" +// @Param name formData string false "名字" +// @Param sortCode formData int false "正序" +// @Param enable formData bool false "使能" +// @Param remark formData string false "备注" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/recmd/update [post] +func RecmdUpdate(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + type Update struct { + Types *recmdtag.RecmdType `form:"types" json:"types" binding:""` //种类 + Name *string `form:"name" json:"name" binding:""` //名字 + SortCode *int64 `form:"sortCode" json:"sortCode" binding:""` //正序 + Enable *bool `form:"enable" json:"enable" binding:""` //使能 true:开 + Remark *string `form:"remark" json:"remark" binding:""` //备注 + } + var args struct { + ID primitive.ObjectID `form:"id" json:"id" binding:"required"` //ID + Update + } + if err = ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "tagctrl RecmdAdd arg error "+err.Error()) + return + } + var tid *primitive.ObjectID + if args.Name != nil { + tag, err := tagmod.FindOneTagByName(*args.Name) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + if tag.ID.IsZero() { + common.ServeJSON(ctx, stderr.TagNotExist, "") + return + } + tid = &tag.ID + } + doc := recmdtag.UpdateDoc{ + Types: args.Types, + TID: tid, + Name: args.Name, + SortCode: args.SortCode, + Enable: args.Enable, + Remark: args.Remark, + } + if err = recmdtag.UpdateOneByID(args.ID, doc); err != nil { + switch err.(type) { + case recmdtag.ErrRecmdTagHasExist: + common.ServeJSON(ctx, stderr.RecmdTagIsExisted, err.Error()) + default: + common.ServeJSON(ctx, stderr.Failure, err.Error()) + } + return + } + log, _ := json.Marshal(args) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageTag, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// RecmdDelete doc +// @Summary 删除推荐标签 +// @Description 删除推荐标签 +// @Tags Web-Tag +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "id" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/recmd/delete [delete] +func RecmdDelete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var args struct { + ID primitive.ObjectID `form:"id" json:"id" binding:"required"` //ID + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "tagctrl RecmdDelete arg error "+err.Error()) + return + } + if err = recmdtag.DeleteOne(args.ID); err != nil { + switch err.(type) { + case recmdtag.ErrRecmdTagHasExist: + common.ServeJSON(ctx, stderr.RecmdTagIsExisted, err.Error()) + default: + common.ServeJSON(ctx, stderr.Failure, err.Error()) + } + return + } + log, _ := json.Marshal(args) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageTag, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/tagctrl/tagCtrl.go b/web/api/tagctrl/tagCtrl.go new file mode 100644 index 0000000..9b6179c --- /dev/null +++ b/web/api/tagctrl/tagCtrl.go @@ -0,0 +1,382 @@ +package tagctrl + +import ( + "encoding/json" + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + v10 "91porn-server/common/v10" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/tagconfmod" + "91porn-server/models/v/tagmod" + "91porn-server/web/service/tagser" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// List doc +// @Summary 标签管理 +// @Description 标签列表 +// @Tags Web-Tag +// @Accept mpfd,json +// @Produce json,html +// @Param key query string false "排序字段" +// @Param value query integer false "排序顺序 1:顺序 -1:倒序" +// @Param filterValue query integer false "筛选精彩0:全部 1:精彩 2:非精彩" +// @Param content query integer false "搜索内容" +// @Param pageNumber query integer true "页码" +// @Param pageSize query integer true "每页条数"。 +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/tag/list [get] +func List(ctx *gin.Context) { + param := tagmod.WebTagListReqInfo{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + desc := false + var order []commod.OrderBy + if param.Key != "" { + if param.Value == -1 { + desc = true + } + order = []commod.OrderBy{commod.OrderBy{ + Key: param.Key, + Desc: desc, + }} + } else { + order = []commod.OrderBy{} + } + stdQuery := commod.StdQuery{ + Page: &commod.PageBy{ + Num: param.Page.PageNumber, + Size: param.Page.PageSize, + }, + Order: &order, + } + data, err := tagser.GetTagList(param.FilterValue, param.Content, stdQuery) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// PureList doc +// @Summary 标签管理 +// @Description 标签纯净版列表 +// @Tags Web-Tag +// @Accept mpfd,json +// @Produce json,html +// @Param key query string false "排序字段" +// @Param value query integer false "排序顺序 1:顺序 -1:倒序" +// @Param content query integer false "搜索内容" +// @Param pageNumber query integer true "页码" +// @Param pageSize query integer true "每页条数"。 +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/tag/pure/list [get] +func PureList(ctx *gin.Context) { + var param struct { + Key string `form:"key" json:"key" binding:"omitempty"` + Value int `form:"value" json:"value" binding:"omitempty"` + Content string `form:"content" json:"content" binding:"omitempty"` + } + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + desc := false + var order []commod.OrderBy + if param.Key != "" { + if param.Value == -1 { + desc = true + } + order = []commod.OrderBy{commod.OrderBy{ + Key: param.Key, + Desc: desc, + }} + } else { + order = []commod.OrderBy{} + } + stdQuery := commod.StdQuery{ + Order: &order, + } + data, err := tagser.GetPureTagList(param.Content, stdQuery) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// Update doc +// @Summary 标签管理 +// @Description 编辑标签 +// @Tags Web-Tag +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "标签id" +// @Param name formData string true "标签名字" +// @Param coverImg formData string true "封面" +// @Param fakePlayCount formData integer true "播放量/点击量" +// @Param sortCode formData integer true "排序码" +// @Param isWonder formData bool true "推送精彩开关" +// @Param wonderSortCode formData integer true "推送精彩排序码" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/tag/update [post] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + param := tagmod.WebTagUpdateReqInfo{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + tagName := v10.ExtractPureChar(param.TagName) + if tagName == "" { + common.ServeJSON(ctx, stderr.TagAddTagNameInvalidErr, fmt.Errorf("web api tag Update tag name is invalid! tagName:%s:", param.TagName)) + return + } + tagDesc := v10.ExtractPureChar(param.TagDesc) + doc := tagmod.TagUpdateDoc{ + TagName: &tagName, + TagDesc: &tagDesc, + CoverImg: ¶m.CoverImg, + SortCode: ¶m.SortCode, + FakePlayCount: ¶m.FakePlayCount, + IsWonder: ¶m.IsWonder, + WonderSortCode: ¶m.WonderSortCode, + IsCommonUsed: ¶m.IsCommonUsed, + UsedSortCode: ¶m.UsedSortCode, + UpdatedAt: time.Now(), + IsActive: ¶m.IsActive, + HotMark: ¶m.HotMark, + } + if err := tagser.UpdateTag(param.ID, doc); err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + log, _ := json.Marshal(param) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageTag, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Delete doc +// @Summary 标签管理 +// @Description 批量删除标签 +// @Tags Web-Tag +// @Accept mpfd,json +// @Produce json,html +// @Param id formData array true "标签id" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/tag/delete [delete] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + param := tagmod.WebTagDeleteReqInfo{} + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if err = tagser.DelTag(param); err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + log, _ := json.Marshal(param) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageTag, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Add doc +// @Summary 标签管理 +// @Description 新增标签 +// @Tags Web-Tag +// @Accept mpfd,json +// @Produce json,html +// @Param name formData string true "标签名字" +// @Param coverImg formData string true "封面" +// @Param sortCode formData integer false "排序码" +// @Param isWonder formData bool false "推送精彩开关" +// @Param wonderSortCode formData integer false "推送精彩排序码" +// @Param isCommonUsed formData bool false "是否常用标签" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/tag/add [post] +func Add(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + param := tagmod.WebTagAddReqInfo{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + tagName := v10.ExtractPureChar(param.TagName) + if tagName == "" { + common.ServeJSON(ctx, stderr.TagAddTagNameInvalidErr, fmt.Errorf("web api tag Update tag name is invalid! tagName:%s:", param.TagName)) + return + } + tagDesc := v10.ExtractPureChar(param.TagDesc) + now := time.Now() + tagMod := tagmod.Tag{ + TagName: tagName, + TagDesc: tagDesc, + CoverImg: param.CoverImg, + SortCode: param.SortCode, + IsWonder: param.IsWonder, + WonderSortCode: param.WonderSortCode, + IsActive: false, + IsCommonUsed: param.IsCommonUsed, + UsedSortCode: param.UsedSortCode, + CreatedAt: now, + UpdatedAt: now, + } + if _, err := tagser.AddTag(tagMod); err != nil { + common.ServeJSON(ctx, stderr.Failure, err) + return + } + log, _ := json.Marshal(param) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageTag, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// TagConfList doc +// @Summary 标签配置列表 +// @Description 分页获取标签配置列表 +// @Tags Web-Tag +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query integer true "页码" +// @Param pageSize query integer true "每页条数"。 +// @Success 200 {object} tagser.TagConfListResponse +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/tag/conf [get] +func TagConfList(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + req := commod.Page{} + if err = c.ShouldBindQuery(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + resp, err := tagser.GetTagconfList(req) + if err != nil { + common.ServeJSON(c, stderr.ErrDbQueryError, err) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageTag, constant.Add, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, resp) +} + +// @Summary 新增标签配置 +// @Description 新增标签配置 +// @Tags Web-Tag +// @Accept mpfd,json +// @Produce json,html +// @Param tagID formData string true "标签id" +// @Param sort formData integer false "排序码" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/tag/conf/add [post] +func AddTagConf(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + req := tagconfmod.TagConf{} + if err = c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if err = tagconfmod.InsertOne(&req); err != nil { + common.ServeJSON(c, stderr.ErrDbInsertError, err) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageTag, constant.Add, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, nil) +} + +// @Summary 修改标签配置 +// @Description 修改标签配置 +// @Tags Web-Tag +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "id" +// @Param tagID formData integer false "标签ID" +// @Param sort formData integer false "排序" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/tag/conf/update [post] +func UpdateTagConf(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + req := tagconfmod.EditSelector{} + if err = c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if err = tagconfmod.UpdateOne(req); err != nil { + common.ServeJSON(c, stderr.ErrDbUpdateError, err) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageTag, constant.Add, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, nil) +} + +// @Summary 删除标签配置 +// @Description 删除标签配置 +// @Tags Web-Tag +// @Accept mpfd,json +// @Produce json,html +// @Param id body string true "id" +// @Success 200 {string} json "{"msg": "操作成功", "data":[]}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/tag/conf/delete [post] +func DeleteTagConf(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var req struct { + ID primitive.ObjectID `json:"id"` + } + if err = c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if err = tagconfmod.DeleteOne(req.ID); err != nil { + common.ServeJSON(c, stderr.ErrDbDeleteError, err) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageTag, constant.Add, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, nil) +} diff --git a/web/api/tianyuctrl/http/http.go b/web/api/tianyuctrl/http/http.go new file mode 100644 index 0000000..47a019e --- /dev/null +++ b/web/api/tianyuctrl/http/http.go @@ -0,0 +1,195 @@ +package http + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "reflect" + "time" +) + +var ( + client = http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + MaxIdleConnsPerHost: 5, + MaxConnsPerHost: 100, + }, + } +) + +type ByteSize int64 + +const ( + _ = iota // ignore first value by assigning to blank identifier + KB ByteSize = 1 << (10 * iota) + MB +) + +var ( + outOfBodySizeErr = errors.New("out of body size limit") +) + +type ResponseBody struct { + StatusCode int + Data []byte +} + +type RequestFormData map[string]string + +// Post +// Note: 只会读取小于1M的数据 +func Post(url string, header map[string]string, body io.Reader) (fr *ResponseBody, err error) { + fr, err = do(http.MethodPost, url, header, body, int64(1024*MB)) + return +} + +// PostFormData 提交标准的form表单 +func PostFormData(url string, fromdata RequestFormData) (fr *ResponseBody, err error) { + var b bytes.Buffer + writer := multipart.NewWriter(&b) + + for k, v := range fromdata { + if err = writer.WriteField(k, v); err != nil { + return + } + } + if err = writer.Close(); err != nil { + return + } + + header := map[string]string{"Content-Type": writer.FormDataContentType()} + fr, err = do(http.MethodPost, url, header, &b, 2*int64(KB)) + return +} + +// Get +// Note: 只会读取小于1M的数据 +func Get(url string, header map[string]string) (fr *ResponseBody, err error) { + fr, err = do(http.MethodGet, url, header, nil, int64(KB)) + return +} + +func GetWithBody(url string, header map[string]string, body io.Reader) (fr *ResponseBody, err error) { + fr, err = do(http.MethodGet, url, header, body, int64(20*MB)) + return +} + +// GetLargeFile 获取大文件。2M +func GetLargeFile(url string, header map[string]string) (fr *ResponseBody, err error) { + fr, err = do(http.MethodGet, url, header, nil, int64(20*MB)) + return +} + +func do(method string, url string, header map[string]string, body io.Reader, size int64) (fr *ResponseBody, err error) { + req, err := http.NewRequest(method, url, body) + if err != nil { + return + } + for k, v := range header { + req.Header.Set(k, v) + } + + resp, err := client.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.ContentLength > size { + err = outOfBodySizeErr + return + } + + data, err := ioutil.ReadAll(io.LimitReader(resp.Body, size)) + if err != nil { + return + } + fr = &ResponseBody{ + StatusCode: resp.StatusCode, + Data: data, + } + return +} + +// 推送对象到文件服务器 +func PostObject(ctx context.Context, url string, header map[string]string, body []byte, objName string) (string, error) { + base64data := base64.StdEncoding.EncodeToString(body) + + req := struct { + Name string `json:"fileName"` + Data string `json:"fileData"` + }{ + Name: objName, + Data: base64data, + } + + jsonBytes, _ := json.Marshal(req) + + resp, err := Post(url, header, bytes.NewReader(jsonBytes)) + if err != nil { + return "", err + } + + type Obj struct { + Domain string `json:"domain"` + Name string `json:"fileName"` + } + + fsResp := struct { + Code int `json:"code"` + Object Obj `json:"data"` + Msg string `json:"msg"` + }{} + + if err = json.Unmarshal(resp.Data, &fsResp); err != nil { + return "", err + } + + if fsResp.Code != 200 || fsResp.Object.Name == "" { + return "", fmt.Errorf("上传fs失败") + } + + return fsResp.Object.Name, nil +} + +func PostWithBind(ctx context.Context, bind interface{}, url string, body io.Reader) error { + if err := verifyBind(bind); err != nil { + return err + } + req, err := http.NewRequestWithContext(ctx, "POST", url, body) + if err != nil { + return err + } + req.Header.Add("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return err + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("response status code:%d", resp.StatusCode) + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + return json.Unmarshal(data, bind) +} + +func verifyBind(bind interface{}) error { + bType := reflect.TypeOf(bind) + if bType.Kind() != reflect.Ptr { + return errors.New("bind must be a Ptr") + } + return nil +} diff --git a/web/api/tianyuctrl/http/http_test.go b/web/api/tianyuctrl/http/http_test.go new file mode 100644 index 0000000..d99de2f --- /dev/null +++ b/web/api/tianyuctrl/http/http_test.go @@ -0,0 +1,16 @@ +package http + +import "fmt" + +func ExampleGet() { + urlstr := "https://palce.hzbeisheng.com/api/sms" + data, err := Get(urlstr, nil) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(string(data.Data)) + + // Output: + +} diff --git a/web/api/tianyuctrl/media.go b/web/api/tianyuctrl/media.go new file mode 100644 index 0000000..1dae9da --- /dev/null +++ b/web/api/tianyuctrl/media.go @@ -0,0 +1,348 @@ +package tianyuctrl + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/mediatagmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "91porn-server/web/api/tianyuctrl/proxy" + "91porn-server/web/service/vidser" + "context" + "fmt" + + "math/rand" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// VideoSearch doc +// @Tags 媒体文件 +// @Summary 根据参数获取视频文件列表 +// @Description 获取视频列表文件 +// @Accept json +// @Produce json +// @Param req body proxy.MediaSearchReq true "参数列表" +// @Success 200 {object} proxy.MediaSearchResp "成功" +// @Failure 400 {string} string "获取失败的返回结果" +// @Router /api/web/admin/tianyu/search [post] +func VideoSearch(ctx *gin.Context) { + var req proxy.MediaSearchReq + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + + videoUploadDomain := proxy.BucketVideoAddressV2 + result, err := proxy.PostSearch(&req, videoUploadDomain) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + sourceMediaIds := make([]string, 0) + for _, mediaInfo := range result.Data.List { + sourceMediaIds = append(sourceMediaIds, mediaInfo.ID.Hex()) + } + + videoList, _ := vidmod.GetList(bson.M{"sourceID": bson.M{"$in": sourceMediaIds}}) + if err != nil { + log.Error(fmt.Sprintf("mediamod queryAllList error:%+v:", err)) + common.ServeJSON(ctx, stderr.Failure, err) + return + } + + for _, v := range result.Data.List { + if v.Type == "AI" { + v.FieldNameFs += ".mp4" + } + for _, v1 := range videoList { + if v.ID.Hex() == v1.SourceID { + v.IsAdd = true + } + } + } + + common.ServeJSON(ctx, stderr.Success, result.Data) + +} + +// AddOneMedia doc +// @Tags 视频 +// @Summary 添加一个视频视频 +// @Description 添加一个视频视频 +// @Accept json +// @Produce json +// @Param param body MediaAdd true "参数列表" +// @Success 200 {string} string "成功" +// @Failure 400 {string} string "获取失败的返回结果" +// @Router /api/web/admin/tianyu/add [post] +func AddOneMedia(ctx *gin.Context) { + var req = &MediaAdd{} + if err := ctx.ShouldBind(req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + var resp = MediaAddResp{SensitiveHits: make([]vidser.SensitiveHit, 0)} + terms := vidser.LoadEnabledSensitiveTerms() + for _, s := range req.FileId { + in := proxy.MediaQueryeReq{ + Id: s, + } + + videoUploadDomain := proxy.BucketVideoAddressV2 + result, err := proxy.QueryMedia(&in, videoUploadDomain) + if err != nil || result.Code != http.StatusOK { + resp.FailCount++ + continue + } + var hit *vidser.SensitiveHit + if result.Data.ContentType == 3 { + hit, err = AddMedia(ctx, &result.Data, manager, terms) + } else { + hit, err = AddVideo(ctx, &result.Data, manager, terms) + } + if err != nil { + log.Error(fmt.Sprintf("add tianyu media error:%+v:", err)) + resp.FailCount++ + continue + } + if hit != nil { + resp.SensitiveHits = append(resp.SensitiveHits, *hit) + } + resp.SuccessCount++ + } + if len(resp.SensitiveHits) > 0 { + resp.SensitiveTip = vidser.SensitiveForcedOfflineTip + } + + common.ServeJSON(ctx, stderr.Success, resp) +} + +func AddMedia(ctx context.Context, data *proxy.MediaResp, manager string, terms []string) (*vidser.SensitiveHit, error) { + now := time.Now() + mediaResource, err := mediamod.GetListByLsjId(data.FileId) + if err != nil { + err = fmt.Errorf("mediamod GetListByLsjId error:%+v", err) + return nil, err + } + if !mediaResource.ID.IsZero() { + return nil, nil + } + + tag, err := mediatagmod.FindOneTagOrInset(data.FirstTag) + if err != nil { + return nil, err + } + + media := mediamod.Media{ + ID: primitive.NewObjectID(), + LsjId: data.FileId, + SortCode: 0, + Status: 1, + Title: data.Title, + HorizontalCover: data.CoverImg, + VerticalCover: "", + Tags: []primitive.ObjectID{tag.ID}, + TagDetails: nil, + Summary: data.Desc, + TotalEpisode: 1, + CurrentEpisode: 1, + FreeEpisode: 0, + UpdateStatus: 2, + MediaType: "video", + MediaSubType: 0, + Kind: 0, + PermissionIconHide: false, + Permission: 0, + Style: 0, + Price: 100, + ContentsPrice: 0, + Direction: 0, + FreeTime: 30, + DefaultContent: mediamod.DefaultContent{}, + MID: primitive.NilObjectID, + ModuleName: "", + SectionName: "", + SID: primitive.NilObjectID, + SectionSort: 0, + Choice: primitive.NilObjectID, + ChoiceSort: 0, + CountComment: 0, + CountPurchases: 0, + CountContentPurchases: 0, + CountBrowse: 0, + CountCollect: 0, + CountLike: 0, + CountDisLike: 0, + HasFollow: false, + CountView: 0, + Hot: 0, + Number: 0, + Anchor: data.Studio, + CountPurchasesRate: 0, + IsDelete: false, + UpdatedAct: "", + CreatedAt: now, + UpdateTime: time.Now(), + //ContentUpdateTime: time.Time{}, + MediaCenterId: 0, + SyncTime: 0, + } + if data.CoverType == 1 { + media.VerticalCover = data.CoverImg + } + + // 创建子集 + mediaContent := mediacontentmod.MediaContent{ + ID: primitive.ObjectID{}, + MediaID: media.ID, + MediaType: "video", + EpisodeNumber: 1, + ListenPermission: 0, + Cover: data.CoverImg, + Price: 100, + Name: data.Title, + Text: "", + Md5: "", + AudioUrl: "", + Author: "", + VideoUrl: strings.TrimSpace(data.FieldNameFs) + ".m3u8", + UrlSet: nil, + Height: 0, + Weight: 0, + MediaSize: data.Size, + PlayTime: uint(data.VideoTime), + Status: 1, + IsActive: true, + CountComment: 0, + CountPurchases: 0, + CountBrowse: 0, + FakeCountBrowse: 0, + CountCollect: 0, + CountLike: 0, + CountDisLike: 0, + Ratio: 0, + IsDelete: false, + UpdatedAct: "", + CreatedAt: time.Now(), + HashId: data.FileId, + } + + // 敏感词命中 → 强制下架(Status=0) + hit := vidser.CheckTextHits(terms, data.FileId, media.Title, media.Summary, "") + if hit != nil { + media.Status = 0 + log.Warn("tianyu AddMedia sensitive hit, forced offline", + log.Any("sourceId", hit.SourceID), + log.Any("title", hit.Title), + log.Any("detail", hit.Detail)) + } + // 不处理标签 + _, err = mediamod.Insert(nil, media) + if err != nil { + return nil, err + } + if _, err = mediacontentmod.Insert(nil, mediaContent); err != nil { + return nil, err + } + return hit, nil +} + +func AddVideo(ctx context.Context, data *proxy.MediaResp, manager string, terms []string) (*vidser.SensitiveHit, error) { + now := time.Now() + postList, err := vidmod.GetList(bson.M{"sourceID": bson.M{"$in": []string{data.FileId}}}) + if err != nil { + log.Warn("syncOneMovie GetList failed", log.Any("id", data.FileId), log.E(err)) + return nil, err + } + if postList != nil && len(postList) > 0 { + return nil, nil + } + + upUsers, err := usermod.FindMany(bson.M{"uid": bson.M{"$gt": 300000}}, &options.FindOptions{}, 0, 300) + if err != nil { + log.Warn("PostAddList FindManyUser failed", log.E(err)) + return nil, err + } + + vidInfo := vidmod.VideoModel{ + ID: primitive.NewObjectID(), + Title: data.Title, + Content: data.Desc, + Tags: make([]primitive.ObjectID, 0), + MimeType: "video/mp4", + SeriesCover: make([]string, 0), + Via: "tianyu", + Status: vidmod.CheckPass, + Location: primitive.ObjectID{}, + Actor: strings.Join(data.Actors, " "), + NewUpdatedAt: "tianyu", + ReviewAt: now, + CreatedAt: now, + Cover: data.CoverImg, + ReviewAccount: manager, + } + if data.CoverType == 1 { + vidInfo.VerticalCover = data.CoverImg + } + + if data.CoverType == 1 { + vidInfo.NewsType = vidmod.SHORT + vidInfo.Cover = data.CoverImg + vidInfo.CoverThumb = data.CoverImg + vidInfo.Direction = "vertical" + } else { + vidInfo.NewsType = vidmod.SP + vidInfo.Cover = data.CoverImg + vidInfo.CoverThumb = data.CoverImg + vidInfo.Direction = "horizontal" + } + + vidInfo.PlayTime = uint(data.VideoTime) + uIndex := rand.Intn(len(upUsers)) + u := upUsers[uIndex] + vidInfo.PublisherID = u.UID + + vidInfo.SourceURL = data.FieldNameFs + vidInfo.PreviewURL = data.PreFileName + vidInfo.SourceID = data.FileId + vidInfo.Filename = data.FieldNameFs + + if !strings.Contains(data.PreFileName, ".m3u8") { + vidInfo.SourceURL = strings.TrimSpace(data.PreFileName) + ".m3u8" + } + + if !strings.Contains(data.FieldNameFs, ".m3u8") { + vidInfo.SourceURL = strings.TrimSpace(data.FieldNameFs) + ".m3u8" + } + + // 敏感词命中 → 强制为待审核(WaitingCheck=0) + hit := vidser.CheckTextHits(terms, data.FileId, vidInfo.Title, vidInfo.Content, "") + if hit != nil { + vidInfo.Status = vidmod.WaitingCheck + log.Warn("tianyu AddVideo sensitive hit, forced offline", + log.Any("sourceId", hit.SourceID), + log.Any("title", hit.Title), + log.Any("detail", hit.Detail)) + } + // 不处理标签 + if _, err = vidmod.InsertBase(vidInfo); err != nil { + return nil, err + } + return hit, nil +} diff --git a/web/api/tianyuctrl/proto.go b/web/api/tianyuctrl/proto.go new file mode 100644 index 0000000..72ba915 --- /dev/null +++ b/web/api/tianyuctrl/proto.go @@ -0,0 +1,14 @@ +package tianyuctrl + +import "91porn-server/web/service/vidser" + +type MediaAdd struct { + FileId []string `json:"fileId" bson:"fileId,required"` // 媒资库ID +} + +type MediaAddResp struct { + SuccessCount int `json:"successCount"` + FailCount int `json:"failCount"` + SensitiveHits []vidser.SensitiveHit `json:"sensitiveHits"` // 命中敏感词被强制下架的记录 + SensitiveTip string `json:"sensitiveTip,omitempty"` // 命中提示文案;无命中时为空 +} diff --git a/web/api/tianyuctrl/proxy/object.go b/web/api/tianyuctrl/proxy/object.go new file mode 100644 index 0000000..e16fc9c --- /dev/null +++ b/web/api/tianyuctrl/proxy/object.go @@ -0,0 +1,382 @@ +package proxy + +import ( + "91porn-server/common/log" + "91porn-server/web/api/tianyuctrl/http" + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + + "strings" +) + +const ( + apiDomain = BucketVideoAddressV2 + "/file/upload" + imgDomain = BucketImageAddressV2 + "/file/upload" +) + +var ( + header = map[string]string{"Content-Type": "application/json"} +) + +const ( + Success = 200 +) + +func UploadImg(fileName string, data []byte, domain string) (pathStr string, err error) { + r := objreq{ + Name: fileName, + Data: base64.StdEncoding.EncodeToString(data), + } + jsonbytes, err := json.Marshal(r) + if err != nil { + return + } + if len(domain) == 0 { + domain = imgDomain + } + url := domain + if !strings.Contains(domain, "/file/upload") { + url = domain + "/file/upload" + } + resp, err := http.Post(url, header, bytes.NewReader(jsonbytes)) + if err != nil { + return + } + + var or objresp + if err = json.Unmarshal(resp.Data, &or); err != nil { + return + } + if or.Code != Success { + err = fmt.Errorf("上传失败:%s", or.Msg) + return + } + pathStr = or.Object.Name + return +} + +func UploadBase64Img(fileName string, data string, domain string) (pathStr string, err error) { + r := objreq{ + Name: fileName, + Data: data, + } + jsonbytes, err := json.Marshal(r) + if err != nil { + return + } + if len(domain) == 0 { + domain = apiDomain + } + url := domain + if !strings.Contains(domain, "/file/upload") { + url = domain + "/file/upload" + } + resp, err := http.Post(url, header, bytes.NewReader(jsonbytes)) + if err != nil { + return + } + + var or objresp + if err = json.Unmarshal(resp.Data, &or); err != nil { + return + } + if or.Code != Success { + err = fmt.Errorf("上传失败:%s", or.Msg) + return + } + pathStr = or.Object.Name + return +} + +func fileServer() (url string) { + return BucketVideoAddressV2 +} + +func PostOk(baseInfo *ShardOk, domain string) (result *VideoOkResult, err error) { + result = &VideoOkResult{} + paramJson, err := json.Marshal(baseInfo) + if err != nil { + return + } + if len(domain) == 0 { + domain = fileServer() + } + url := domain + if !strings.Contains(domain, "/file/web/proxy/videoOk") { + url = domain + "/file/web/proxy/videoOk" + } + fmt.Println(paramJson) + fr, err := http.Post(url, header, bytes.NewReader(paramJson)) + if err != nil { + return + } + if err = json.Unmarshal(fr.Data, result); err != nil { + fmt.Println(result) + return nil, err + } + fmt.Println(result) + return +} +func PostOkV2(baseInfo *ShardOk, domain string) (result *VideoOkResult, err error) { + result = &VideoOkResult{} + paramJson, err := json.Marshal(baseInfo) + if err != nil { + return + } + if len(domain) == 0 { + domain = fileServer() + } + url := domain + if !strings.Contains(domain, "/file/web/proxy/videoOkv2") { + url = domain + "/file/web/proxy/videoOkv2" + } + fmt.Println(paramJson) + fr, err := http.Post(url, header, bytes.NewReader(paramJson)) + if err != nil { + return + } + if err = json.Unmarshal(fr.Data, result); err != nil { + fmt.Println(result) + return nil, err + } + fmt.Println(result) + return +} +func QueryMedia(baseInfo *MediaQueryeReq, domain string) (result *VideoQueryResult, err error) { + result = &VideoQueryResult{} + paramJson, err := json.Marshal(baseInfo) + if err != nil { + return + } + if len(domain) == 0 { + domain = fileServer() + } + url := domain + if !strings.Contains(domain, "/file/web/media/query") { + url = domain + "/file/web/media/query" + } + fr, err := http.Post(url, header, bytes.NewReader(paramJson)) + if err != nil { + return + } + if err = json.Unmarshal(fr.Data, result); err != nil { + fmt.Println(err) + return nil, err + } + return +} + +func QueryMedias(baseInfo *MediaQueryesReq, domain string) (result *VideoQuerysResult, err error) { + result = &VideoQuerysResult{ + Data: make([]MediaResp, 0), + } + paramJson, err := json.Marshal(baseInfo) + if err != nil { + return + } + if len(domain) == 0 { + domain = fileServer() + } + url := domain + if !strings.Contains(domain, "/file/web/media/querys") { + url = domain + "/file/web/media/querys" + } + fr, err := http.Post(url, header, bytes.NewReader(paramJson)) + if err != nil { + return + } + if err = json.Unmarshal(fr.Data, result); err != nil { + fmt.Println(err) + return nil, err + } + return +} +func SyncMedia(baseInfo *MediaSyncReq, domain string) (result *VideoSyncResult, err error) { + result = &VideoSyncResult{} + paramJson, err := json.Marshal(baseInfo) + if err != nil { + return + } + if len(domain) == 0 { + domain = fileServer() + } + url := domain + if !strings.Contains(domain, "/file/web/media/search") { + url = domain + "/file/web/media/search" + } + fr, err := http.Post(url, header, bytes.NewReader(paramJson)) + if err != nil { + return + } + if err = json.Unmarshal(fr.Data, result); err != nil { + fmt.Println(err, string(fr.Data)) + return nil, err + } + + return +} + +func UploadVideo(domain string, body *ShardBody) (result *Result, err error) { + if body == nil { + return nil, errors.New("no video to upload") + } + if len(domain) == 0 { + domain = fileServer() + } + url := domain + if !strings.Contains(domain, "/file/web/proxy/videoBody") { + url = domain + "/file/web/proxy/videoBody" + } + fmt.Println(url) + + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + resp, err := http.Post(url, header, bytes.NewBuffer(buf)) + if err != nil { + return nil, err + } + fmt.Println("resp-->", string(resp.Data)) + result = new(Result) + if err = json.Unmarshal(resp.Data, result); err != nil { + return nil, err + } + return +} +func PostSearch(param *MediaSearchReq, domain string) (result *MediaSearchResult, err error) { + result = &MediaSearchResult{} + paramJson, err := json.Marshal(param) + if err != nil { + return + } + if len(domain) == 0 { + domain = fileServer() + } + url := domain + if !strings.Contains(domain, "/file/web/media/search") { + url = domain + "/file/web/media/search" + } + fr, err := http.Post(url, header, bytes.NewReader(paramJson)) + if err != nil { + log.Error(fmt.Sprintf("ty media search err:%v", err)) + return + } + if fr.StatusCode != 200 { + log.Error(fmt.Sprintf("ty media search fr.StatusCode:%v", fr.StatusCode)) + return + } + + if err = json.Unmarshal(fr.Data, result); err != nil { + log.Error(fmt.Sprintf("ty json Unmarshal search err:%v", err)) + return nil, err + } + log.Info("proxy:test", log.Any("url", url), log.Any("result", result)) + return +} + +func PostUpdate(param *MediaUpdateReq, domain string) (result *MediaUpdateResult, err error) { + result = &MediaUpdateResult{} + paramJson, err := json.Marshal(param) + if err != nil { + return + } + if len(domain) == 0 { + domain = fileServer() + } + url := domain + if !strings.Contains(domain, "/file/web/media/update") { + url = domain + "/file/web/media/update" + } + fr, err := http.Post(url, header, bytes.NewReader(paramJson)) + if err != nil { + return + } + if err = json.Unmarshal(fr.Data, result); err != nil { + return nil, err + } + return +} + +func PostDelete(param *MediaDelReq, domain string) (result *MediaDelResult, err error) { + result = &MediaDelResult{} + paramJson, err := json.Marshal(param) + if err != nil { + return + } + if len(domain) == 0 { + domain = fileServer() + } + url := domain + if !strings.Contains(domain, "/file/web/media/delete") { + url = domain + "/file/web/media/delete" + } + fr, err := http.Post(url, header, bytes.NewReader(paramJson)) + if err != nil { + return + } + if err = json.Unmarshal(fr.Data, result); err != nil { + return nil, err + } + return +} + +func PostUpdatePreTime(param *MediaUpdatePreTime, domain string) (result *MediaDelResult, err error) { + result = &MediaDelResult{} + paramJson, err := json.Marshal(param) + if err != nil { + return + } + if len(domain) == 0 { + domain = fileServer() + } + url := domain + if !strings.Contains(domain, "/file/web/media/update/prefile") { + url = domain + "/file/web/media/update/prefile" + } + fr, err := http.Post(url, header, bytes.NewReader(paramJson)) + if err != nil { + return + } + if err = json.Unmarshal(fr.Data, result); err != nil { + return nil, err + } + return +} + +func UploadVideoV2(domain string, body *ShardBodyV2) (result *Result, err error) { + if body == nil { + return nil, errors.New("no video to upload") + } + if len(domain) == 0 { + domain = fileServer() + } + url := domain + if !strings.Contains(domain, "/file/web/proxy/videoBodyV2") { + url = domain + "/file/web/proxy/videoBodyV2" + } + fmt.Println(url) + + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + resp, err := http.Post(url, header, bytes.NewBuffer(buf)) + if err != nil { + return nil, err + } + fmt.Println("resp-->", string(resp.Data)) + result = new(Result) + if err = json.Unmarshal(resp.Data, result); err != nil { + return nil, err + } + + fmt.Println("result-->", result) + if result.Code != 200 { + err = fmt.Errorf("result code err") + } + return +} diff --git a/web/api/tianyuctrl/proxy/object_test.go b/web/api/tianyuctrl/proxy/object_test.go new file mode 100644 index 0000000..943b369 --- /dev/null +++ b/web/api/tianyuctrl/proxy/object_test.go @@ -0,0 +1 @@ +package proxy diff --git a/web/api/tianyuctrl/proxy/types.go b/web/api/tianyuctrl/proxy/types.go new file mode 100644 index 0000000..6e08005 --- /dev/null +++ b/web/api/tianyuctrl/proxy/types.go @@ -0,0 +1,351 @@ +package proxy + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type objreq struct { + Name string `json:"fileName"` + Data string `json:"fileData"` +} + +type Obj struct { + Domain string `json:"domain"` + Name string `json:"fileName"` +} + +type objresp struct { + Code int `json:"code"` + Object Obj `json:"data"` + Msg string `json:"msg"` +} + +type ShardBody struct { + TaskId string `json:"taskId,omitempty"` // 文档md5 + Pos int `json:"pos,omitempty"` // 位置 + FileData string `json:"fileData,omitempty"` // 数据 + Type string `json:"type,omitempty"` + TotalPos int `json:"totalPos,omitempty"` // 分片总数 +} + +type ShardBodyV2 struct { + TaskId string `json:"taskId,omitempty"` // 文档md5 + Pos int `json:"pos,omitempty"` // 位置 + FileData string `json:"fileData,omitempty"` // 数据 + Type string `json:"type,omitempty"` + TotalPos int `json:"totalPos,omitempty"` + FileName string `json:"fileName,omitempty"` + Publish string `json:"publish,omitempty"` + AppId int `json:"appId,omitempty"` + MP4Folder string `json:"mp4Folder"` // 保存mp4的文件夹名,为空表示不需要保存mp4 +} + +type Data struct { + Id string `json:"id,omitempty"` + VideoUri string `json:"videoUri,omitempty"` +} + +type Result struct { + Code int `json:"code,omitempty"` // + Data Data `json:"data,omitempty"` // + Msg string `json:"msg,omitempty"` // +} + +type VideoOkResult struct { + Code int `json:"code,omitempty"` + Msg string `json:"msg,omitempty"` // + Data string `json:"data,omitempty"` +} +type ShardOk struct { + TaskId string `json:"taskId,omitempty"` // 文档md5 + Id string `json:"id,omitempty"` // Id + Title *string `json:"title,omitempty"` // 标题 + CoverImg *string `json:"coverImg,omitempty"` // 封面 +} + +type MediaQueryeReq struct { + Id string `json:"id"` + TaskId string `json:"taskId"` +} +type MediaQueryesReq struct { + Ids []string `json:"ids"` +} +type VideoQueryResult struct { + Code int `json:"code,omitempty"` + Msg string `json:"msg,omitempty"` // + Data MediaResp `json:"data,omitempty"` +} + +type VideoQuerysResult struct { + Code int `json:"code,omitempty"` + Msg string `json:"msg,omitempty"` // + Data []MediaResp `json:"data,omitempty"` +} +type MediaSyncReq struct { + PageNum int64 `json:"pageNum"` + PageSize int64 `json:"pageSize"` + Param *SyncParam `json:"param"` +} +type SyncParam struct { + Status string `json:"status,omitempty"` // 当前状态 + FirstTag string `json:"firstTag,omitempty"` // 一级标签 + Publish string `json:"publish,omitempty"` // 视频发布者 + // SecondTags string `json:"secondTags,omitempty"` // 二级标签标签 +} +type VideoSyncResult struct { + Code int `json:"code,omitempty"` + Msg string `json:"msg,omitempty"` // + Data MediaSearchResp `json:"data,omitempty"` +} + +// type MediaSearchResp struct { +// Total int64 `json:"total"` +// List []*MediaSync `json:"list"` +// } + +type MediaSync struct { + ID string `json:"id" binding:"required"` + FileId string `json:"fileId" bson:"fileId"` // 媒资库ID + Title string `bson:"title" json:"title"` // 标题 + Publish string `json:"publish" bson:"publish"` // 视频发布者 + Type string `bson:"type" json:"type"` // 长视频/短视频 + Country string `bson:"country" json:"country"` // 国家 (国产、日本、欧美.... 前端固定) + FirstTag string `bson:"firstTag" json:"firstTag"` // 一级标签 + SecondTags []string `bson:"secondTags" json:"secondTags"` // 二级标签标签 + CoverImg string `bson:"coverImg" json:"coverImg"` // 封面 + PreFileName string `bson:"preFileName" json:"preFileName"` // 预览文件 + Desc string `bson:"desc" json:"desc"` // 详细描述 + Size int64 `bson:"size" json:"size"` // 文件size,单位byte + Bango string `bson:"bango" json:"bango"` // 番号,唯一 + Directors []string `bson:"directors" json:"directors"` // 导演 + Actors []string `bson:"actors" json:"actors"` // 演员 + Studio string `bson:"studio" json:"studio"` // 工作室/所有者 + AddedTime time.Time `bson:"addedTime" json:"addedTime"` // 上架时间 + VideoTime int64 `json:"videoTime" bson:"videoTime"` // 影片时长,单位秒 + OperatAt time.Time `json:"operatAt" bson:"operatAt"` // 编辑时间(排序时间) + FieldNameFs string `bson:"fieldNameFs,omitempty" json:"fieldNameFs,omitempty"` // 文件名称 + Height int64 `json:"height" bson:"height"` // 分辨率高 + Width int64 `json:"width" bson:"width"` // 分辨率宽 + ProxyUpload int32 `bson:"proxyUpload,omitempty" json:"proxyUpload,omitempty"` // 代理上传 1:代理 +} + +type MediaResp struct { + FileId string `json:"id" bson:"_id"` // 媒资库ID + SrcId int64 `bson:"srcId,omitempty" json:"srcId"` // 原始ID + Title string `bson:"title" json:"title"` // 标题 + Type string `bson:"type" json:"type"` // 类型:3.AI + Publish string `json:"publish" bson:"publish"` // 视频发布者 + ContentType int64 `bson:"contentType,omitempty" json:"contentType"` // 内容 1:日本 2:国产 3:动漫 + FormatType int64 `bson:"formatType,omitempty" json:"formatType"` // 视频格式:1:长视频 2:短视频 3:预览 + CoverType CoverType `bson:"coverType,omitempty" json:"coverType"` // 封面类型: 1:竖版封面 2:横版封面 3:系统设置 + Country string `bson:"country" json:"country"` // 国家 (国产、日本、欧美.... 前端固定) + FirstTag string `bson:"firstTag" json:"firstTag"` // 一级标签 + SecondTags []string `bson:"secondTags" json:"secondTags"` // 二级标签标签 + CoverImg string `bson:"coverImg" json:"coverImg"` // 封面 + PreFileName string `bson:"preFileName" json:"preFileName"` // 预览文件 + Status string `bson:"status" json:"status"` // 当前状态 + Desc string `bson:"desc" json:"desc"` // 详细描述 + MediaSet []string `bson:"mediaSet,omitempty" json:"mediaSet"` // 视频归属系列(连续剧、番剧) + Size int64 `bson:"size" json:"size"` // 文件size,单位byte + Bango string `bson:"bango" json:"bango"` // 番号,唯一 + Actors []string `bson:"actors" json:"actors"` // 演员 + JapanNames []string `json:"japanNames"` // 演员日本名称 + Studio string `bson:"studio" json:"studio"` // 工作室/所有者 + AddedTime time.Time `bson:"addedTime" json:"addedTime"` // 上架时间 + VideoTime int64 `json:"videoTime" bson:"videoTime"` // 影片时长,单位秒 + OperatAt time.Time `json:"operatAt" bson:"operatAt"` // 编辑时间(排序时间) + FieldNameFs string `bson:"fieldNameFs,omitempty" json:"fieldNameFs,omitempty"` // 文件名称 + Height int64 `json:"height" bson:"height"` // 分辨率高 + Width int64 `json:"width" bson:"width"` // 分辨率宽 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间 + MP4FileName string `json:"mp4FileName" bson:"mp4FileName"` +} + +type MediaType int + +const ( + ContentAv MediaType = 1 // 日本 + ContentChina MediaType = 2 // 国产 + ContentCartoon MediaType = 3 // 动漫 +) + +type CoverType int + +const ( + CoverHorizontal CoverType = 1 // 竖版封面 + CoverVertical CoverType = 2 // 横版封面 + CoverSystem CoverType = 3 // 系统设置封面 +) + +type FailReason string + +const ( + FailReasonNone FailReason = "" + FailReasonUploadTimeoutError FailReason = "UploadTimeoutError" // 上传超时 + FailReasonMergeError FailReason = "MergeError" // 合并错误 + FailReasonCompressError FailReason = "CompressError" // 压缩错误 + FailReasonSegmentError FailReason = "SegmentError" // 分片错误 + FailReasonLinodeError FailReason = "LinodeError" // Linode错误 +) + +type BaseInfo struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + SrcId int64 `bson:"srcId,omitempty" json:"srcId"` // 原始ID + Title string `bson:"title,omitempty" json:"title"` // 标题 + CheckSum string `bson:"checkSum,omitempty" json:"checkSum"` // md5校验码 + FormatType int64 `bson:"formatType,omitempty" json:"formatType"` // 视频格式:1:长视频 2:短视频 3:预览 + ContentType MediaType `bson:"contentType,omitempty" json:"contentType"` // 内容 1:日本 2:国产 3:动漫 + CoverType CoverType `bson:"coverType,omitempty" json:"coverType"` // 封面类型: 1:竖版封面 2:横版封面 3:系统设置 + CoverImg string `bson:"coverImg,omitempty" json:"coverImg"` // 封面 + CoverImgNew string `bson:"coverImgNew,omitempty" json:"coverImgNew"` // 封面(自动生成) + VideoTime int32 `bson:"videoTime,omitempty" json:"videoTime"` // 影片长度 + Publish string `json:"publish,omitempty" bson:"publish"` // 视频发布者 + Type string `bson:"type,omitempty" json:"type"` // 类型:1.AV 2.SP 3.AI + Country string `bson:"country,omitempty" json:"country"` // 国家 (国产、日本、欧美.... 前端固定) + FirstTag string `bson:"firstTag,omitempty" json:"firstTag"` // 一级标签 + SecondTags []string `bson:"secondTags,omitempty" json:"secondTags"` // 二级标签标签 + MediaSet []string `bson:"mediaSet,omitempty" json:"mediaSet"` // 视频归属系列(连续剧、番剧) + PreFileName string `bson:"preFileName,omitempty" json:"preFileName"` // 预览文件 + Status string `bson:"status,omitempty" json:"status"` // 当前状态 + Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述 + Size int64 `bson:"size,omitempty" json:"size"` // 文件size,单位byte + Bango string `bson:"bango,omitempty" json:"bango"` // 番号,唯一 + Actors []string `bson:"actors,omitempty" json:"actors"` // 演员 + Studio string `bson:"studio,omitempty" json:"studio"` // 工作室/所有者 + AddedTime time.Time `bson:"addedTime,omitempty" json:"addedTime"` // 上架时间 + AppIds []int64 `bson:"appids,omitempty" json:"appids,omitempty"` // 应用列表 + JapanNames []string `json:"japanNames"` // 演员日本名称 + + // 切片相关信息 + Filename string `bson:"filename,omitempty" json:"filename"` // 文件名称 + FieldNameFs string `bson:"fieldNameFs,omitempty" json:"fieldNameFs"` // 文件名称 + Ext string `bson:"ext,omitempty" json:"ext"` // 视频后缀 + TaskID string `bson:"taskId,omitempty" json:"taskId"` // 项目组传来的唯一标示 + Width int `bson:"width,omitempty" json:"width"` // 分辨率宽 + Height int `bson:"height,omitempty" json:"height"` // 分辨率高 + Ratio float64 `bson:"ratio,omitempty" json:"ratio"` // 宽高比 + FrameRate string `bson:"frameRate,omitempty" json:"frameRate"` // 视频贞数 + SyBitRate string `bson:"syBitRate,omitempty" json:"syBitRate"` // 音频bit率 + VidBitRate string `bson:"vidBitRate,omitempty" json:"vidBitRate"` // 视频bit率 + CreatedAt time.Time `bson:"createdAt,omitempty" json:"createdAt"` // 创建时间 + UpdatedAt time.Time `bson:"updatedAt,omitempty" json:"updatedAt"` // 更新时间 + ProxyUpload *int32 `bson:"proxyUpload,omitempty" json:"proxyUpload"` // 代理上传 + + IsAdd bool `bson:"isAdd,omitempty" json:"isAdd"` // 是否加入到本地 + Retry int `bson:"retry" json:"retry"` // 重试次数 默认为零 最大重试3次 + NotifySignal bool `bson:"notifySignal" json:"notifySignal"` //中止通知的信号 false 可以发送通知,true 中止发送通知 + ShowLocation int64 `bson:"showLocation" json:"showLocation"` // 显示位置 + + MergeRetry int `bson:"mergeRetry" json:"mergeRetry"` //合并重试次数 + CompressRetry int `bson:"compressRetry" json:"compressRetry"` //压缩重试次数 + SegmentRetry int `bson:"segmentRetry" json:"segmentRetry"` //切片重试次数 + LinodeRetry int `bson:"linodeRetry" json:"linodeRetry"` //上传linode重试次数 + FailReason FailReason `bson:"failReason" json:"failReason"` //失败原因 + DeleteDisk bool `bson:"deleteDisk" json:"deleteDisk"` //是否删除了对于磁盘文件 + InfoTsName string `bson:"infoTsName" json:"infoTsName"` //从该ts中获取视频分辨率等信息 + Remark string `bson:"remark,omitempty" json:"remark"` // 备注 +} + +type MediaUpdate struct { + ID string `json:"id,omitempty"` + Title *string `bson:"title,omitempty" json:"title,omitempty"` // 标题 + AddedTime *time.Time `bson:"addedTime,omitempty" json:"addedTime,omitempty"` // 上架时间 + Publish string `json:"publish,omitempty" bson:"publish"` // 视频发布者(麻豆、adday) + FormatType *int64 `bson:"formatType,omitempty" json:"formatType"` // 视频格式:1:长视频 2:短视频 3:预览 + FirstTag *string `bson:"firstTag,omitempty" json:"firstTag"` // 一级标签 + SecondTags *[]string `bson:"secondTags,omitempty" json:"secondTags"` // 二级标签标签 + MediaSet *[]string `bson:"mediaSet,omitempty" json:"mediaSet"` // 视频归属系列(连续剧、番剧) + Bango *string `bson:"bango,omitempty" json:"bango,omitempty"` // 番号,唯一 + AppIds *[]int64 `bson:"appids,omitempty" json:"appids,omitempty"` // 应用列表 + Desc *string `bson:"desc,omitempty" json:"desc,omitempty"` // 详细描述 + PreFileName *string `bson:"preFileName,omitempty" json:"preFileName,omitempty"` // 预览文件 + CoverImg *string `bson:"coverImg,omitempty" json:"coverImg,omitempty"` // 封面 + ContentType *MediaType `bson:"contentType,omitempty" json:"contentType"` // 内容 1:AV(日本) 2:国产(短视频) 3:动漫 + Actors *[]string `bson:"actors,omitempty" json:"actors"` // 演员 + Studio *string `bson:"studio,omitempty" json:"studio"` // 工作室/所有者 + CoverType *CoverType `bson:"coverType,omitempty" json:"coverType"` // 封面类型 1:竖版 2:横版 3:系统 + CreatedAt *time.Time `bson:"createdAt,omitempty" json:"createdAt,omitempty"` // 创建时间 + UpdatedAt *time.Time `bson:"updatedAt,omitempty" json:"updatedAt,omitempty"` // 更新时间 + ShowLocation *int64 `bson:"showLocation,omitempty" json:"showLocation"` // 显示位置 +} + +type VideoSearch struct { + ID string `json:"id,omitempty"` + Title *string `json:"title,omitempty"` // 标题 + TaskID *string `json:"taskId,omitempty"` // 项目组传来的唯一标示 + Publish *string `json:"publish,omitempty"` // 视频发布者 + FormatType *int64 `json:"formatType,omitempty"` // 视频格式:1:长视频 2:短视频 3:预览 + ContentType *MediaType `json:"contentType,omitempty"` // 内容 + CoverType *CoverType `json:"coverType,omitempty"` // 封面类型 + Appid *int64 `json:"appid"` // 应用ID + AppIds *[]int64 `json:"appids"` // 应用IDs + FirstTag *string `json:"firstTag,omitempty"` // 一级标签 + SecondTags *string `json:"secondTags,omitempty"` // 二级标签标签 + MediaSet *[]string `json:"mediaSet,omitempty"` // 视频归属系列(连续剧、番剧) + Status *string `json:"status,omitempty"` // 当前状态 + Actors *string `json:"actors,omitempty"` // 演员 + Studio *string `json:"studio,omitempty"` // 工作室 + Bango *string `json:"bango,omitempty"` // 番号,唯一 + Remark *string `json:"remark,omitempty"` // 备注 + AddedTime *string `json:"addedTime,omitempty"` // 上架时间 + UploadTime *string `json:"uploadTime,omitempty"` // 上传时间 + Start *time.Time `json:"start"` // 开始时间 + End *time.Time `json:"end"` // 结束时间 + Type *string `json:"type,omitempty"` // SP,AV,AI,MP3 + ShowLocation *int64 `json:"showLocation"` // 显示位置 +} + +type MediaSearchReq struct { + PageNum int64 `json:"pageNum"` + PageSize int64 `json:"pageSize"` + Param *VideoSearch `json:"param"` +} + +// MediaSearchReq 媒体搜索请求返回 +type MediaSearchResp struct { + Total int64 `json:"total"` + List []*BaseInfo `json:"list"` +} + +type MediaSearchResult struct { + Code int `json:"code,omitempty"` + Msg string `json:"msg,omitempty"` // + Data MediaSearchResp `json:"data,omitempty"` +} + +type MediaDelReq struct { + ID string `json:"id,omitempty"` +} + +// MediasDelResp 请求批量删除媒体回复 +type MediaDelResp struct { +} + +type MediaUpdatePreTime struct { + ID string `json:"id,omitempty"` + PreFileTime int64 `bson:"preFileTime,omitempty" json:"preFileTime"` // 预览文件(开始时间) +} + +type MediaDelResult struct { + Code int `json:"code,omitempty"` + Msg string `json:"msg,omitempty"` // + // Data MediaDelResp `json:"data,omitempty"` +} + +type MediaUpdateReq struct { + //Ids []string `json:"id"` + MediaUpdate +} + +// MediasUpdateResp 请求批量更新媒体回复 +type MediaUpdateResp struct { +} + +type MediaUpdateResult struct { + Code int `json:"code,omitempty"` + Msg string `json:"msg,omitempty"` // + Data MediaUpdateResp `json:"data,omitempty"` +} diff --git a/web/api/tianyuctrl/proxy/utils.go b/web/api/tianyuctrl/proxy/utils.go new file mode 100644 index 0000000..f025cfe --- /dev/null +++ b/web/api/tianyuctrl/proxy/utils.go @@ -0,0 +1,130 @@ +package proxy + +import ( + "fmt" + "strings" +) + +const ( + // 视频桶地址 + BucketVideoAddressV1 = "http://ts.bbaazz.me" + BucketVideoAddressV2 = "http://tong3yuants.bbaazz.me" + // 图片桶地址 + BucketImageAddressV1 = "http://img.bbaazz.me" + BucketImageAddressV2 = "http://tong3yuanimg.bbaazz.me" + + // 上传路径 + ImageUploadPathV1 = "/file/upload" + ImageUploadPathV2 = "/file/upload" // "/file/web/media/uploadImg" + VideoBodyPathV1 = "/file/web/proxy/videoBody" + VideoBodyPathV2 = "/file/web/proxy/videoBodyV2" + VideoOkPathV1 = "/file/web/proxy/videoOk" + VideoOkPathV2 = "/file/web/proxy/videoOkv2" +) + +func DomainAddressRepair(domain string, defaultBucketAddress ...string) string { + if domain == "" { + if len(defaultBucketAddress) > 0 && defaultBucketAddress[0] != "" { + domain = defaultBucketAddress[0] + } else { + domain = BucketVideoAddressV2 + } + } + return domain +} + +func ImageUploadAddressRepair(domain string, defaultBucketAddress ...string) string { + domain = DomainAddressRepair(domain, defaultBucketAddress...) + if !strings.Contains(domain, ImageUploadPathV1) && !strings.Contains(domain, ImageUploadPathV2) { + if strings.HasPrefix(domain, BucketImageAddressV1) { + return domain + ImageUploadPathV1 + } else if strings.HasPrefix(domain, BucketImageAddressV2) { + return domain + ImageUploadPathV2 + } + return domain + ImageUploadPathV2 + } + return domain +} + +func VideoBodyAddressRepair(domain string, defaultBucketAddress ...string) string { + domain = DomainAddressRepair(domain, defaultBucketAddress...) + if strings.HasPrefix(domain, BucketVideoAddressV1) { + if !strings.Contains(domain, VideoBodyPathV1) { + return domain + VideoBodyPathV1 + } + return domain + } else if strings.HasPrefix(domain, BucketVideoAddressV2) { + if !strings.Contains(domain, VideoBodyPathV2) { + return domain + VideoBodyPathV2 + } + return domain + } else { // 未知存储桶,默认v2 + if !strings.Contains(domain, VideoBodyPathV2) { + return domain + VideoBodyPathV2 + } + return domain + } +} + +func VideoOkAddressRepair(domain string, defaultBucketAddress ...string) string { + domain = DomainAddressRepair(domain, defaultBucketAddress...) + if strings.HasPrefix(domain, BucketVideoAddressV1) { + if !strings.Contains(domain, VideoOkPathV1) { + return domain + VideoOkPathV1 + } + return domain + } else if strings.HasPrefix(domain, BucketVideoAddressV2) { + if !strings.Contains(domain, VideoOkPathV2) { + return domain + VideoOkPathV2 + } + return domain + } else { // 未知存储桶,默认v2 + if !strings.Contains(domain, VideoOkPathV2) { + return domain + VideoOkPathV2 + } + return domain + } +} + +func GetExistsUrlPathBucketAddress(path string) string { + if strings.HasPrefix(path, "v1/") { + return BucketVideoAddressV1 + } else if strings.HasPrefix(path, "v2/") { + return BucketVideoAddressV2 + } else if strings.HasPrefix(path, "v3/") { + return BucketVideoAddressV2 + } else { + return BucketVideoAddressV1 // 欲漫涩、微博、快看禁漫 默认用桶2,其他的app,默认用桶1 + } +} + +func AppendV2PrefixToUrlPath(name string) string { + if name != "" && !strings.HasPrefix(name, "v2/") && !strings.HasPrefix(name, "v1/") && !strings.HasPrefix(name, "v3/") && + !strings.HasPrefix(name, "/v2") && !strings.HasPrefix(name, "/v1") && !strings.HasPrefix(name, "/v3") { + name = fmt.Sprintf("v2/%s", name) + } + return name +} + +func AppendV1PrefixToUrlPath(name string) string { + if name != "" && !strings.HasPrefix(name, "v2/") && !strings.HasPrefix(name, "v1/") && !strings.HasPrefix(name, "v3/") && + !strings.HasPrefix(name, "/v2") && !strings.HasPrefix(name, "/v1") && !strings.HasPrefix(name, "/v3") { + name = fmt.Sprintf("v1/%s", name) + } + return name +} + +func AppendsMediaS3Version(name string) string { + if strings.Contains(name, "bktadminup") || + strings.Contains(name, "bktappupdata") || + strings.Contains(name, "k12") || + strings.Contains(name, "bktappup") { + return "" + } + // 默认使用V1 + if name != "" && !strings.HasPrefix(name, "v2/") && !strings.HasPrefix(name, "v1/") && !strings.HasPrefix(name, "v3/") && + !strings.HasPrefix(name, "/v2") && !strings.HasPrefix(name, "/v1") && !strings.HasPrefix(name, "/v3") { + return "/v2" + } + return "" +} diff --git a/web/api/tonectrl/tonectrl.go b/web/api/tonectrl/tonectrl.go new file mode 100644 index 0000000..78a2280 --- /dev/null +++ b/web/api/tonectrl/tonectrl.go @@ -0,0 +1,195 @@ +package tonectrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/tonemod" + "91porn-server/models/v/tonerecomod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// List doc +// @Summary 获取音色最热列表 +// @Description 获取音色最热列表 +// @Tags web-视频管理-音色最热 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query int true "当前页" +// @Param pageSize query int true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "date": { "total":10, "list":[] }}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/tone/list [get] +func List(c *gin.Context) { + var arg struct { + commod.Page + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "tone List arg error "+err.Error()) + return + } + sort := bson.D{{Key: "createdAt", Value: -1}} + skip := int64((arg.PageNumber - 1) * arg.PageSize) + limit := int64(arg.PageSize) + page, err := tonemod.TonePages(sort, skip, limit) + if err != nil { + common.ServeJSON(c, stderr.Failure, "tone List error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} + +// Update doc +// @Summary 编辑音色最热 +// @Description 编辑音色最热 +// @Tags web-视频管理-音色最热 +// @Accept mpfd,json +// @Produce json,html +// @Param targetID formData string true "目标文档ID" +// @Param name formData string false "名字" +// @Param cover formData string false "封面" +// @Param sortKey formData int false "排序键" +// @Param enable formData bool false "使能" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/tone/update [post] +func Update(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + TargetID string `form:"targetID" json:"targetID,omitempty" binding:"required"` + Name *string `form:"name" json:"name,omitempty" binding:""` + Cover *string `form:"cover" json:"cover,omitempty" binding:""` + SortKey *int `form:"sortKey" json:"sortKey,omitempty" binding:""` + Enable *bool `form:"enable" json:"enable,omitempty" binding:""` + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "tone Update arg error "+err.Error()) + return + } + doc := tonemod.ToneDoc{ + Name: arg.Name, + Cover: arg.Cover, + SortKey: arg.SortKey, + Enable: arg.Enable, + } + targetID, err := primitive.ObjectIDFromHex(arg.TargetID) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if err := tonemod.UpdateTo(targetID, doc); err != nil { + common.ServeJSON(c, stderr.ErrDbUpdateError, err) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageTone, constant.Modify, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// OffiRecomAdd doc +// @Summary 音色最热-官方推荐视屏批量添加 +// @Description 音色最热-官方推荐视屏批量添加 +// @Tags web-视频管理-音色最热 +// @Accept mpfd,json +// @Produce json,html +// @Param vidArray formData array true "视屏ID数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/tone/offi_recom/add [post] +func OffiRecomAdd(c *gin.Context) { + var arg struct { + VIDArray []string `form:"vidArray" json:"vidArray" binding:"required"` + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "tone OffiRecomAdd arg error "+err.Error()) + return + } + vidArray, err := common.IDArray(arg.VIDArray) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + offiRecoms := make([]tonerecomod.OffiRecomDoc, len(vidArray)) + for i, _id := range vidArray { + offiRecoms[i] = tonerecomod.OffiRecomDoc{ + VID: _id, + } + } + if err := tonerecomod.InsertMany(offiRecoms); err != nil { + common.ServeJSON(c, stderr.ErrDbInsertError, err) + return + } + common.ServeJSON(c, stderr.Success, "") +} + +// OffiRecomList doc +// @Summary 音色最热-官方推荐视屏列表 +// @Description 音色最热-官方推荐视屏列表 +// @Tags web-视频管理-音色最热 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query int true "当前页" +// @Param pageSize query int true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "date": { "total":10, "list":[] }}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/tone/offi_recom/list [get] +func OffiRecomList(c *gin.Context) { + var arg struct { + commod.Page + commod.OrderBy + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "tone OffiRecomAList arg error "+err.Error()) + return + } + sort := tonemod.D{{Key: "createdAt", Value: -1}} + skip := int64((arg.PageNumber - 1) * arg.PageSize) + limit := int64(arg.PageSize) + page, err := tonerecomod.OffiRecomPages(sort, skip, limit) + if err != nil { + common.ServeJSON(c, stderr.Failure, "tone OffiRecomAList error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} + +// OffiRecomDelete doc +// @Summary 音色最热-官方推荐视屏批量删除 +// @Description 音色最热-官方推荐视屏批量删除 +// @Tags web-视频管理-音色最热 +// @Accept mpfd,json +// @Produce json,html +// @Param idArray formData array true "ID数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/tone/offi_recom/delete [delete] +func OffiRecomDelete(c *gin.Context) { + var arg struct { + IDArray []string `form:"idArray" json:"idArray" binding:"required"` + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "tone OffiRecomDelete arg error "+err.Error()) + return + } + idArray, err := common.IDArray(arg.IDArray) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if err = tonerecomod.DeleteMany(idArray); err != nil { + common.ServeJSON(c, stderr.ErrDbDeleteError, err) + return + } + common.ServeJSON(c, stderr.Success, "") +} diff --git a/web/api/updownloadctrl/updownloadCtrl.go b/web/api/updownloadctrl/updownloadCtrl.go new file mode 100644 index 0000000..510e7eb --- /dev/null +++ b/web/api/updownloadctrl/updownloadCtrl.go @@ -0,0 +1,697 @@ +package updownloadctrl + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "mime" + "net/http" + "path" + "path/filepath" + "strconv" + "strings" + "time" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/crypt" + "91porn-server/common/log" + "91porn-server/common/m3u8" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/sourcemod" + "91porn-server/models/v/vidmod" + "91porn-server/web/service/updownloadser" + "91porn-server/web/service/vidser" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" +) + +const Retries = 3 //重试3次 + +// buffer bytes.Buffer +var interval = []int64{5, 15, 30, 60} //通知时间间隔 + +// Upload doc +// @Summary 文件管理 - 表单上传文件 +// @Description 表单上传文件 用于上传视频对应的封面图 +// @Tags WEB-uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param upload formData file true "文件" +// @Param id formData string true "文件ID" +// @Success 200 {string} json "{"msg": "success" "data":{"coverImg":"xxxxxxxxxx.jpg"}}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/upload [post] +func Upload(c *gin.Context) { + headers, err := c.FormFile("upload") + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + id := c.PostForm("id") + if id == "" { + common.ServeJSON(c, stderr.ErrUploadError, "id is required") + return + } + f, err := headers.Open() + if err != nil { + log.Warn("headers Open file wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + ext := strings.TrimLeft(filepath.Ext(headers.Filename), ".") + byteData, err := io.ReadAll(f) + f.Close() + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + fileData := base64.StdEncoding.EncodeToString(byteData) + resp, err := updownloadser.SendVidCover2FS(id, ext, fileData) + if err != nil { + log.Warn("file upload wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + if resp.Code == http.StatusOK { + common.ServeJSON(c, stderr.Success, resp.Data) + return + } + common.ServeJSON(c, stderr.ErrUploadError, "") +} + +// UploadVideo doc +// @Summary 文件管理 - 上传视频 +// @Description 上传视频 +// @Tags WEB-uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param upload formData file true "文件类型" +// @Success 200 {string} json "{"msg": "success" "data":""}" +// @Failure 400 {string} json "{"msg": "fail"}" +// @Router /web/vid/uploadVideo [post] +func UploadVideo(c *gin.Context) { + var mime_type = []string{"video/x-flv", "video/mp4", "application/x-mpegURL", "video/MP2T", "video/3gpp", "video/x-msvideo", "video/x-ms-wmv"} + var isVideo bool + var resp vidmod.AwsResport + vidres := vidmod.WenUploadRes{} + var httpErr error + headers, err := c.FormFile("upload") + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + f, err := headers.Open() + if err != nil { + log.Warn("headers Open file wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + defer func() { _ = f.Close() }() + mtype := mime.TypeByExtension(strings.ToLower(filepath.Ext(headers.Filename))) + for _, v := range mime_type { + if mtype == v { + isVideo = true + break + } + } + if !isVideo { + common.ServeJSON(c, stderr.ErrMimeType, "") + return + } + byteData, _ := io.ReadAll(f) + md5 := crypt.ByteToMd5(byteData) + fileData := base64.StdEncoding.EncodeToString(byteData) + resp, httpErr = updownloadser.SendFile2FS(common.UUID(), fileData, 1, 1) + if httpErr != nil { + log.Warn("file upload wrong ", log.E(httpErr)) + common.ServeJSON(c, stderr.ErrUploadError, httpErr.Error()) + return + } + if resp.Code == http.StatusOK { + vidres.SourceID = resp.Data.VID + vidres.SourceURL = resp.Data.VideoURI + vidres.Size = int(headers.Size) + vidres.MD5 = md5 + fileName := strconv.FormatInt(time.Now().Unix(), 10) + common.RandStrRandr(8) + path.Ext(headers.Filename) + vidres.Filename = fileName + vidres.MimeType = mtype + common.ServeJSON(c, stderr.Success, vidres) + return + } + common.ServeJSON(c, stderr.ErrUploadError, "") +} + +// UploadDotStream doc +// @Summary 文件管理 - 流式断点续传文件 +// @Description 流式断点续传 +// @Tags uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "文件ID 文件MD5做ID" +// @Param chunkNumber formData string true "第几片视频" +// @Param totalChunks formData string true "视频总片数" +// @Success 200 {string} json "{"msg": "success" "data":{"id":"5d8a2af58747044ca077f358","videoUri":"xxxxxxx.m3u8"} }" +// @Failure 400 {string} json "{"msg": "fail"}" +// @Router /vid/uploadDotStream [post] +func UploadDotStream(c *gin.Context) { + var id, pos, totalPos, size string + var cnt int64 + id = c.PostForm("id") + pos = c.PostForm("chunkNumber") + totalPos = c.PostForm("totalChunks") + size = c.PostForm("totalSize") + var mime_type = []string{"video/x-flv", "video/mp4", "application/x-mpegURL", "video/MP2T", "video/3gpp", "video/x-msvideo", "video/x-ms-wmv"} + var isVideo bool + var resp vidmod.AwsResport + vidres := vidmod.WenUploadRes{} + if id == "" || pos == "" || totalPos == "" { + common.ServeJSON(c, stderr.ErrParamError, "upload args error") + return + } + posint, _ := strconv.ParseInt(pos, 10, 32) + total, _ := strconv.ParseInt(totalPos, 10, 32) + var httpErr error + headers, err := c.FormFile("upload") + if err != nil || headers.Filename == "" || headers.Size == 0 { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err) + return + } + f, err := headers.Open() + if err != nil { + log.Warn("headers Open file wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err) + return + } + defer func() { + _ = f.Close() + }() + mtype := mime.TypeByExtension(strings.ToLower(filepath.Ext(headers.Filename))) + for _, v := range mime_type { + if mtype == v { + isVideo = true + break + } + } + if !isVideo { + common.ServeJSON(c, stderr.ErrMimeType, "") + return + } + var buffer bytes.Buffer + _, err = buffer.ReadFrom(f) + if err != nil { + log.Warn("headers Open file wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err) + return + } + fileData := base64.StdEncoding.EncodeToString(buffer.Bytes()) + for cnt < Retries { + resp, httpErr = updownloadser.SendFile2FS(id, fileData, posint, total) + if httpErr == nil { + break + } + time.Sleep(time.Second) + log.Warn("retry to upload file to file-server", log.Any("重试次数", cnt), log.E(httpErr)) + cnt++ + } + if httpErr != nil { + log.Warn("file upload wrong ", log.E(httpErr)) + common.ServeJSON(c, stderr.ErrUploadError, nil) + return + } + if resp.Code != http.StatusOK { + common.ServeJSON(c, stderr.ErrUploadError, "") + return + } + vidres.Size, _ = strconv.Atoi(size) + vidres.SourceID = resp.Data.VID + vidres.SourceURL = resp.Data.VideoURI + fileName := strconv.FormatInt(time.Now().Unix(), 10) + common.RandStrRandr(8) + path.Ext(headers.Filename) + vidres.Filename = fileName + vidres.MimeType = mtype + common.ServeJSON(c, stderr.Success, vidres) +} + +// UploadDotJson doc +// @Summary 文件管理 - API断点续传文件 +// @Description API断点续传 +// @Tags uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "taskId 文件MD5做ID" +// @Param pos formData integer true "第几片视频" +// @Param totalPos formData integer true "视频总片数" +// @Param data formData string true "数据内容" +// @Success 200 {string} json "{"msg": "success" "data":{"id":"5d8a2af58747044ca077f358","videoUri":"xxxxxxx.m3u8"} }" +// @Failure 400 {string} json "{"msg": "fail"}" +// @Router /vid/uploadDotJson [post] +func UploadDotJson(c *gin.Context) { + var cnt int + var resp vidmod.AwsResport + vidres := vidmod.WenUploadRes{} + var httpErr error + var args struct { + ID string `form:"id" json:"id" binding:"required"` //taskId + POS int64 `form:"pos" json:"pos" binding:"required"` //分片序号 + TotalPos int64 `form:"totalPos" json:"totalPos" binding:"required"` //总分片数 + Data string `form:"data" json:"data" binding:"required"` //分片内容 + } + if err := c.ShouldBind(&args); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + for cnt < Retries { + resp, httpErr = updownloadser.SendFile2FS(args.ID, args.Data, args.POS, args.TotalPos) + if httpErr == nil { + break + } + time.Sleep(time.Duration(interval[cnt]) * time.Second) + log.Warn("retry to upload file to file-server", log.Any("重试次数", cnt), log.E(httpErr)) + cnt++ + } + if httpErr != nil { + log.Warn("file upload wrong ", log.E(httpErr)) + common.ServeJSON(c, stderr.ErrUploadError, nil) + return + } + if resp.Code == http.StatusOK { + vidres.SourceID = resp.Data.VID + vidres.SourceURL = resp.Data.VideoURI + common.ServeJSON(c, stderr.Success, vidres) + return + } + common.ServeJSON(c, stderr.ErrUploadError, resp) +} + +// Submit doc +// @Summary 提交视频的基本信息 +// @Description 视频基本信息提交 +// @Tags WEB-uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer true "用户ID" +// @Param title formData string true "视频标题" +// @Param tags formData array true "视频标签数组" +// @Param playTime formData string false "视频时长" +// @Param cover formData string false "封面图" +// @Param coverThumb formData string false "小图" +// @Param via formData string false "视频来源" +// @Param coins formData integer false "观看金币" +// @Param size formData integer false "文件大小" +// @Param mimeType formData string false "影片类型" +// @Param sourceURL formData string true "资源url" +// @Param sourceID formData string true "上传视频成功后 返回的ID" +// @Param filename formData string true "文件名" +// @Param resolution formData string false "分辨率" +// @Param md5 formData string true "文件摘要" +// @Param freeTime formData integer false "免费观影时长" +// @Param seedLinkUrl formData string false "种子下载链接" +// @Param seedSize formData integer false "种子影片大小" +// @Param seedPlayTime formData integer false "种子影片时长" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/submit [post] +func Submit(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + param := vidmod.WebSubmitReq{} + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + //对上传的视频的秒数做限制 + if param.PlayTime != 0 { + if param.PlayTime < vidmod.SubmitVideoPlayTimeLimit { + common.ServeJSON(ctx, stderr.ErrPlayTimeTooShort, "") + return + } + } + //默认newsType + if len(param.NewsType) == 0 { + param.NewsType = vidmod.SP + } + //图集的第一张放到封面中 + if (param.NewsType == vidmod.COVER || param.NewsType == vidmod.PIC) && len(param.SeriesCover) != 0 { + param.Cover = param.SeriesCover[0] + param.CoverThumb = param.SeriesCover[0] + } + code, vid := vidser.SubmitBase(param) + if code != stderr.Success { + _, _ = vidser.DeleteManyByTTL(vid) + common.ServeJSON(ctx, code, vid) + return + } + if param.NewsType == vidmod.COVER || param.NewsType == vidmod.PIC || + param.NewsType == vidmod.AD_COVER || (param.NewsType == vidmod.SEED_LINK && param.SourceURL == "") { + common.ServeJSON(ctx, code, vid) + return + } + + code = vidser.SendBaseInfo2AWs(param) + if code != stderr.Success { + _, _ = vidser.DeleteManyByTTL(vid) + } + log, _ := json.Marshal(param) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageList, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, vid) +} + +// PullFileInfoFromAws doc +// @Summary +// @Description 从AWS拉取视频信息 +// @Tags WEB-uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param token formData string true "调用此接口的token"" +// @Param uid formData array true "用户ID 数组" +// @Param cityes formData array true "城市名字 数组" +// @Param maxPlayTime formData integer false "最长播放时间" +// @Param minPlayTime formData integer false "最小播放时间" +// @Param pageSize formData integer true "分页数据" +// @Param newUpdateAt formData string false "批次更新标识" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/pullFileInfoFromAws [post] +func PullFileInfoFromAws(ctx *gin.Context) { + param := vidmod.PullReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if param.Token != "q7ydjHVIVBYiTqQ&" { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + var code stderr.Code + var data map[string]interface{} + switch param.SyncType { + case "SP": + code, data = updownloadser.PullFile(param) + case "SERIES": + code, data = updownloadser.PullSeriesFile(param) + } + if param.NewUpdateAt != "" { + updownloadser.SyncNewImportVideoToElastic(param.NewUpdateAt, time.Time{}, time.Time{}) + } + common.ServeJSON(ctx, code, data) +} + +// UploadStatic doc +// @Summary 文件管理 - 表单上传文件 +// @Description 表单上传文件,上传静态文件到AWS 用于独立文件上传 +// @Tags WEB-uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param upload formData file true "文件" +// @Success 200 {string} json "{"msg": "success" "data":{"coverImg":"xxxxxxxxxx.ext"}}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/uploadStatic [post] +func UploadStatic(c *gin.Context) { + var ( + resp vidmod.FsSendSingleResp + commres commod.Resp + err error + ) + headers, err := c.FormFile("upload") + id := c.PostForm("id") + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + f, err := headers.Open() + if err != nil { + log.Warn("headers Open file wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + byteData, err := io.ReadAll(f) + f.Close() + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + fileData := base64.StdEncoding.EncodeToString(byteData) + if id == "" { + resp, err = updownloadser.SendImageToFS(headers.Filename, fileData) + } else { + ext := strings.TrimLeft(filepath.Ext(headers.Filename), ".") + commres, err = updownloadser.SendVidCover2FS(id, ext, fileData) + } + if err != nil { + log.Warn("file upload wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + if resp.Code == stderr.Success { + common.ServeJSON(c, stderr.Success, gin.H{ + "coverImg": resp.Data.FileName, + "sizeB": headers.Size, //字节 + }) + return + } + if commres.Code == stderr.Success { + common.ServeJSON(c, stderr.Success, commres.Data) + return + } + common.ServeJSON(c, stderr.ErrUploadError, "") +} + +// UploadStaticBatch doc +// @Summary 文件管理 - 表单上传文件 批量上传 +// @Description 表单上传文件,上传静态文件到AWS 用于独立文件上传 +// @Tags WEB-uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param uploads formData file true "文件" +// @Success 200 {string} json "{"msg": "success" "data":{"coverImg":"xxxxxxxxxx.ext"}}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/vid/uploadStatic [post] +func UploadStaticBatch(c *gin.Context) { + form, err := c.MultipartForm() + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + files := form.File["upload[]"] + batch := make([]*updownloadser.FileInfo, len(files)) + for i, f := range files { + fi, err := f.Open() + if err != nil { + log.Warn("headers Open file wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + byteData, err := io.ReadAll(fi) + fi.Close() + if err != nil { + log.Warn("request multipart wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + fileData := base64.StdEncoding.EncodeToString(byteData) + fileInfo := &updownloadser.FileInfo{ + FileName: &f.Filename, + FileData: &fileData, + Resize: true, + } + batch[i] = fileInfo + } + resp, err := updownloadser.SendImageToFSBatch(updownloadser.InfoBatch{Batch: batch}) + if err != nil { + log.Warn("file upload wrong ", log.E(err)) + common.ServeJSON(c, stderr.ErrUploadError, err.Error()) + return + } + common.ServeJSON(c, stderr.Success, gin.H{"filePath": resp.Data.GetFileNames(), "success": resp.Data.Count()}) +} + +// DownloadM3u8 doc +// @Summary 文件管理 - 下载文件接口 +// @Description 下载文件 +// @Tags 正式 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /vid/m3u8/:source [get] +func DownloadM3u8(c *gin.Context) { + var cdnURL string + source := c.Param("source") + if source == "" { + common.ServeJSON(c, stderr.ErrParamError, "") + return + } + if ext := filepath.Ext(source); ext != ".m3u8" { + common.ServeJSON(c, stderr.ErrMimeType, "") + return + } + fileName := filepath.Base(source) + c.Writer.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName)) + //媒体资源库选择 + mds := m3u8.GetMediaResouce(source) + if mds == constant.MediaSourceLaoSiJi { + source = strings.Replace(source, "/laosiji/", "", -1) + } + data, err := updownloadser.FsIO(source, mds) + if err != nil { + log.ZapLog.Warn("download file error", log.E(err)) + c.JSON(http.StatusBadRequest, gin.H{"data": "", "mag": stderr.ErrDownloadError.Msg()}) + return + } + if webg.Conf.Switch.CdnOriginBaseOnLocal { + cdnURL = webg.Conf.URL.CdnUrl + } else { + cdns := sourcemod.GetCdnURL() + if len(cdns) > 0 { + cdnURL = cdns[0].Url + } else { + cdnURL = webg.Conf.URL.CdnUrl + } + } + // TS 分片鉴权签名密钥:优先取配置(webg.Conf.Base.TsAuth),未配置时回退内置默认(版本 default + 内置常量) + keyVersion, authKey := webg.Conf.Base.TsAuth.Resolve() + var bytebuff *bytes.Buffer + if mds == constant.MediaSourcePMS { + // bytebuff = m3u8.DecodeFromReader(data, "", "/api/web/admin/vid/pms/sec", webg.Conf.Base.PMCSecIV) + bytebuff = m3u8.DecodeFromReader(data, "", "/api/web/admin/vid", "", mds, authKey, keyVersion, source) + } else { + serUrl := "/api/web/admin/vid/sp/sec" + if mds == constant.MediaSourceJH1B { + serUrl = "/api/web/admin/vid/sp/m3u8sec" + } else if mds == constant.MediaSourceLaoSiJi { + serUrl = "/api/web/admin/vid/sp/lsjsec" + } + bytebuff = m3u8.DecodeFromReader(data, common.BindUrl(cdnURL, strings.Trim(source, fileName)), serUrl, "", mds, authKey, keyVersion, source) + } + if bytebuff == nil { + log.Warn("can't create m3u8 file") + c.JSON(http.StatusNoContent, gin.H{"data": "", "msg": stderr.CodeEmptyData.Msg()}) + return + } + c.Data(200, "application/octet-stream", bytebuff.Bytes()) +} + +// SyncNewVideo doc +// @Summary 文件管理 - 下载文件接口 +// @Description 下载文件 +// @Tags 正式 +// @Accept mpfd,json +// @Produce json,html +// @Param newUpdateAt formData string true "视频标识" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/vid/syncNewVideo [post] +func SyncNewVideo(c *gin.Context) { + type Info struct { + NewUpdateAt string `form:"newUpdateAt" json:"newUpdateAt"` + Token string `form:"token" json:"token" binging:"required"` + Start time.Time `form:"start" json:"start"` + End time.Time `form:"end" json:"end"` + } + var param = Info{} + if err := c.ShouldBind(¶m); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + if param.Token != "q7ydjHVIVBYiTqQ&" { + common.ServeJSON(c, stderr.ErrNoToken, "") + return + } + updownloadser.SyncNewImportVideoToElastic(param.NewUpdateAt, param.Start, param.End) + common.ServeJSON(c, stderr.Success, "") +} + +// SpiderSyncSubmit doc +// @Summary 爬虫同步视频基本信息 +// @Description 视频基本信息提交 +// @Tags WEB-uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer true "用户ID" +// @Param title formData string true "视频标题" +// @Param tags formData array true "视频标签数组" +// @Param playTime formData string false "视频时长" +// @Param cover formData string false "封面图" +// @Param coverThumb formData string false "小图" +// @Param via formData string false "视频来源" +// @Param coins formData integer false "观看金币" +// @Param size formData integer false "文件大小" +// @Param mimeType formData string false "影片类型" +// @Param sourceURL formData string true "资源url" +// @Param sourceID formData string true "上传视频成功后 返回的ID" +// @Param filename formData string true "文件名" +// @Param resolution formData string false "分辨率" +// @Param md5 formData string true "文件摘要" +// @Param freeTime formData integer false "免费观影时长" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /vid/spiderSyncSubmit [post] +func SpiderSyncSubmit(ctx *gin.Context) { + param := vidmod.WebSubmitReq{} + if err := ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + //默认newsType + if len(param.NewsType) == 0 { + param.NewsType = vidmod.SP + } + //图集的第一张放到封面中 + if (param.NewsType == vidmod.COVER || param.NewsType == vidmod.PIC) && len(param.SeriesCover) != 0 { + param.Cover = param.SeriesCover[0] + param.CoverThumb = param.SeriesCover[0] + } + code, vid := vidser.SubmitBase(param) + if code != stderr.Success { + _, _ = vidser.DeleteManyByTTL(vid) + common.ServeJSON(ctx, code, vid) + return + } + if param.NewsType == vidmod.COVER || param.NewsType == vidmod.PIC { + common.ServeJSON(ctx, code, vid) + return + } + common.ServeJSON(ctx, code, vid) +} + +// UploadVideoSuccess doc +// @Summary 文件管理 - 上传视频成功回调 +// @Description 上传视频成功回调 +// @Tags WEB-uploaddown +// @Accept mpfd,json +// @Produce json,html +// @Param upload formData file true "文件类型" +// @Success 200 {string} json "{"msg": "success" "data":""}" +// @Failure 400 {string} json "{"msg": "fail"}" +// @Router /web/vid/uploadSuccess [post] +func UploadVideoSuccess(c *gin.Context) { + var params struct { + Id string `json:"id"` + } + if err := c.ShouldBind(¶ms); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err.Error()) + return + } + //通知文件服, 视频文件上传完成 + if code := m3u8.UploadSuccess(params.Id); code != 200 { + log.Error("UploadVideoSuccess m3u8.UploadSuccess code != 200") + common.ServeJSON(c, stderr.Failure, "") + return + } + common.ServeJSON(c, stderr.Success, "") +} diff --git a/web/api/userctrl/faq.go b/web/api/userctrl/faq.go new file mode 100644 index 0000000..dd2b65f --- /dev/null +++ b/web/api/userctrl/faq.go @@ -0,0 +1,146 @@ +package userctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/faqmod" + "91porn-server/web/service/userser" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// FaqList doc +// @Summary 用户常见问题 +// @Description 用户常见问题列表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param category query string false "问题类型" +// @Param keywords query string false "查找关键字" +// @Param pageNumber query integer true "当前页" +// @Param pageSize query integer true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "date": { "total":10, "list":[] }}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/faq/list [get] +func FaqList(c *gin.Context) { + var args struct { + Category string `form:"category" json:"category"` + Keywords string `form:"keywords" json:"keywords"` + commod.Page + } + if err := c.ShouldBind(&args); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user FaqList arg error "+err.Error()) + return + } + pageBy := commod.PageBy{Num: args.PageNumber, Size: args.PageSize} + page, err := userser.FaqList(args.Category, args.Keywords, commod.StdQuery{Page: &pageBy}) + if err != nil { + common.ServeJSON(c, stderr.Failure, "web user FaqList error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} + +// FaqAdd doc +// @Summary 用户常见问题 +// @Description 新增用户常见问题 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param category formData string true "问题类型" +// @Param query formData string true "问题" +// @Param answer formData string true "问题答案" +// @Param status formData boolean true "状态 ture为开启 false为关闭" +// @Param sortCode formData string false "排序号"" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/faq/add [post] +func FaqAdd(ctx *gin.Context) { + f := faqmod.Faq{} + if err := ctx.ShouldBind(&f); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + code, err := faqmod.FaqInsert(f) + if err != nil { + common.ServeJSON(ctx, code, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// FaqDelete doc +// @Summary 用户常见问题 +// @Description 批量删除用户常见问题 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData array true "id数组 字符串数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/faq/del [delete] +func FaqDelete(ctx *gin.Context) { + var args struct { + IDs []primitive.ObjectID `form:"ids" json:"ids"` + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err := faqmod.FaqDelete(args.IDs); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// FaqUpdate doc +// @Summary 用户常见问题 +// @Description 编辑常见问题 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData array true "id字符串" +// @Param category formData string false "问题类型" +// @Param query formData string false "问题" +// @Param answer formData string false "问题答案" +// @Param status formData boolean false "状态 ture为开启 false为关闭" +// @Param sortCode formData string false "排序号"" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/faq/update [post] +func FaqUpdate(ctx *gin.Context) { + var args struct { + ID primitive.ObjectID `form:"id" json:"id" binding:"required"` + faqmod.FaqUpdateDoc + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err := faqmod.FaqUpdate(args.ID, args.FaqUpdateDoc); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// FaqCateList doc +// @Summary 用户常见问题 +// @Description 用户常见问题分类列表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功", "date": {}}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/faq/cate/list [get] +func FaqCateList(c *gin.Context) { + page, err := faqmod.FaqCateList() + if err != nil { + common.ServeJSON(c, stderr.Failure, "web user FaqCateList error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} diff --git a/web/api/userctrl/feedback.go b/web/api/userctrl/feedback.go new file mode 100644 index 0000000..c06aacb --- /dev/null +++ b/web/api/userctrl/feedback.go @@ -0,0 +1,234 @@ +package userctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/feedbackmod" + "91porn-server/models/v/userResourcemod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// FeedBackList doc +// @Summary +// @Description 用户反馈意见列表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid query integer false "用户ID" +// @Param content query string false "查找关键字" +// @Param pageNumber query integer true "当前页" +// @Param pageSize query integer true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功", "date": { "total":10, "list":[] }}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/feedback/list [get] +func FeedbackList(c *gin.Context) { + params := feedbackmod.ListParam{} + if err := c.ShouldBind(¶ms); err != nil { + common.ServeJSON(c, stderr.ErrParamError, err) + return + } + result, err := feedbackmod.FeedBackList(params) + if err != nil { + common.ServeJSON(c, stderr.Failure, err) + return + } + common.ServeJSON(c, stderr.Success, result) +} + +// FeedbackDelete doc +// @Summary 反馈删除 +// @Description 批量删除用户反馈 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData array true "id数组 字符串数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/feedback/del [delete] +func FeedbackDelete(ctx *gin.Context) { + var args struct { + IDs []primitive.ObjectID `form:"ids" json:"ids"` + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err := feedbackmod.FeedBackDelete(args.IDs); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// FeedbackUpdate doc +// @Summary 用户反馈问题 +// @Description 用户反馈编辑 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param id formData array true "id字符串" +// @Param replay formData string false "回复" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/feedback/update [post] +func FeedbackUpdate(ctx *gin.Context) { + var args struct { + ID primitive.ObjectID `form:"id" json:"id" binding:"required"` + UID int64 `form:"uid" json:"uid"` + Confirm bool `from:"confirm" json:"confirm"` //确认是否发送回复 + Content string `json:"content" bson:"content"` + feedbackmod.FeedBackUpdateDoc + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err := feedbackmod.FeedBackUpdate(args.ID, args.FeedBackUpdateDoc); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// GetUserResourceList doc +// @Summary 获取用户资源配置列表 +// @Description 获取用户资源配置列表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/resource/list [get] +func GetUserResourceList(ctx *gin.Context) { + _, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := userResourcemod.ResourceListReq{} + err1 := ctx.ShouldBind(&req) + if err1 != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + infos, total, err := userResourcemod.GetUserResource(req.PageNumber, req.PageSize, "") + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": infos, + "total": total, + }) +} + +// ModifyUserResource doc +// @Summary 修改用户资源配置列表 +// @Description 修改用户资源配置列表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/resource/modify [post] +func ModifyUserResource(ctx *gin.Context) { + _, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := userResourcemod.ResourceModifyReq{} + err1 := ctx.ShouldBind(&req) + if err1 != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + oid, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + userResourcemod.ModifyUserResource(oid, req.Type, req.Resource) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, "") + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// AddUserResource doc +// @Summary 添加用户资源配置列表 +// @Description 添加用户资源配置列表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/resource/add [post] +func AddUserResource(ctx *gin.Context) { + _, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := userResourcemod.ResourceAddReq{} + err = ctx.ShouldBind(&req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + ru := userResourcemod.UserResource{ + Type: req.Type, + Resource: req.Resource, + } + err = userResourcemod.Add(ru) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, "") + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// DelUserResource doc +// @Summary 删除用户资源配置列表 +// @Description 删除用户资源配置列表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/resource/del [post] +func DelUserResource(ctx *gin.Context) { + _, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := userResourcemod.ResourceDelReq{} + err1 := ctx.ShouldBind(&req) + if err1 != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + id, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + err = userResourcemod.RemoveUserResource(id) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, "") + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/userctrl/login.go b/web/api/userctrl/login.go new file mode 100644 index 0000000..ec4c4bf --- /dev/null +++ b/web/api/userctrl/login.go @@ -0,0 +1,60 @@ +package userctrl + +import ( + "time" + + "91porn-server/common" + "91porn-server/common/pageopt" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/web/service/userser" + + "github.com/gin-gonic/gin" +) + +// LoginList doc +// @Summary 获取用户登陆列表 +// @Description 获取用户登陆列表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query integer true "当前页" +// @Param pageSize query integer true "每页条数" +// @Param uid query integer false "过滤条件-用户ID" +// @Param startTime query integer false "过滤条件-开始时间" +// @Param endTime query integer false "过滤条件-结束时间" +// @Success 200 {string} user "{"msg": "操作成功", "date": { "total":10, "list":[] }}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/login/list [get] +func LoginList(c *gin.Context) { + type Query struct { + UID *uint64 `form:"uid" json:"uid" bson:"uid,omitempty"` //用户ID + StartTime time.Time `form:"startTime" json:"startTime,omitempty" binding:""` //开始时间 + EndTime time.Time `form:"endTime" json:"endTime,omitempty" binding:""` //结束时间 + } + var arg struct { + commod.Page + Query + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user LoginList arg error "+err.Error()) + return + } + skip := int64((arg.PageNumber - 1) * arg.PageSize) + limit := int64(arg.PageSize) + endTime := time.Now() + startTime := endTime.AddDate(0, -1, 0) + if !arg.EndTime.IsZero() { + startTime = arg.StartTime + endTime = arg.EndTime + } + page, err := userser.LoginInfoPages(skip, limit, + pageopt.CreatedAtGTEAndLTMatch{GTE: &startTime, LT: &endTime}, + pageopt.UIDMatch{UID: arg.UID}, + ) + if err != nil { + common.ServeJSON(c, stderr.Failure, "web user LoginList error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} diff --git a/web/api/userctrl/play.go b/web/api/userctrl/play.go new file mode 100644 index 0000000..11d02cf --- /dev/null +++ b/web/api/userctrl/play.go @@ -0,0 +1,74 @@ +package userctrl + +import ( + "time" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/playlgmod" + "91porn-server/models/v/vidmod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func encodePlayLog(infos []*playlgmod.PlayLog) []*playlgmod.LogInfo { + vids := make([]primitive.ObjectID, len(infos)) + for i, v := range infos { + vids[i] = v.VideoID + } + m, err := vidmod.GetTitleTimeByIDs(vids) + if err != nil { + return []*playlgmod.LogInfo{} + } + back := make([]*playlgmod.LogInfo, len(infos)) + for i, v := range infos { + back[i] = &playlgmod.LogInfo{ + UID: v.UID, + VideoID: v.VideoID, + Title: m[v.VideoID].Title, + PlayTime: m[v.VideoID].PlayTime, + Longer: v.Longer, + Progress: v.Progress, + Via: v.Via, + PlayWay: v.PlayWay, + CreatedAt: v.CreatedAt, + } + } + return back +} + +// PlayList doc +// @Summary 获取用户观看列表 +// @Description 获取用户观看列表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query integer true "当前页" +// @Param pageSize query integer true "每页条数" +// @Param payMehod query integer false "过滤条件-观看方式" +// @Param uid query integer false "过滤条件-用户ID" +// @Param startTime query integer false "过滤条件-开始时间" +// @Param endTime query integer false "过滤条件-结束时间" +// @Success 200 {string} json "{"msg": "操作成功", "date": { "total":10, "list":[] }}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/play/list [get] +func PlayList(c *gin.Context) { + req := playlgmod.WatchReq{} + if err := c.ShouldBind(&req); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "PlayList error "+err.Error()) + return + } + if req.Start.IsZero() { + req.End = time.Now() + h, _ := time.ParseDuration("-1h") + req.Start = req.End.Add(h) + } + infos, total, err := playlgmod.GetPlayLog(req) + if err != nil { + common.ServeJSON(c, stderr.Failure, "web user PlayList error: "+err.Error()) + return + } + logs := encodePlayLog(infos) + common.ServeJSON(c, stderr.Success, playlgmod.WatchResp{Logs: logs, Total: total}) +} diff --git a/web/api/userctrl/reco.go b/web/api/userctrl/reco.go new file mode 100644 index 0000000..7ccc44d --- /dev/null +++ b/web/api/userctrl/reco.go @@ -0,0 +1,130 @@ +package userctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/userrecomod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetUserRecoList doc +// @Summary 获取用户推荐列表 +// @Description 获取用户推荐列表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/reco/list [get] +func GetUserRecoList(ctx *gin.Context) { + if _, err := common.GetAdminAct(ctx); err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := userrecomod.RecoListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + infos, total, err := userrecomod.GetUserReco(req.PageNumber, req.PageSize, true) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, userrecomod.RecoListResp{Infos: infos, Total: total}) +} + +// ModifyUserReco doc +// @Summary 修改用户推荐列表 +// @Description 修改用户推荐列表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/reco/modify [post] +func ModifyUserReco(ctx *gin.Context) { + if _, err := common.GetAdminAct(ctx); err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := userrecomod.RecoModifyReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + oid, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + _ = userrecomod.ModifyUserReco(oid, req.UID, req.SortKey, req.Active) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// AddUserReco doc +// @Summary 添加用户推荐列表 +// @Description 添加用户推荐列表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/reco/add [post] +func AddUserReco(ctx *gin.Context) { + if _, err := common.GetAdminAct(ctx); err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := userrecomod.RecoAddReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err := userrecomod.AddUserReco(req.UID, req.SortKey, req.Active); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, "") + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// DelUserReco doc +// @Summary 删除用户推荐列表 +// @Description 删除用户推荐列表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/reco/del [post] +func DelUserReco(ctx *gin.Context) { + if _, err := common.GetAdminAct(ctx); err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := userrecomod.RecoDelReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + oid, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err = userrecomod.RemoveUserReco(oid); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, "") + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/userctrl/report.go b/web/api/userctrl/report.go new file mode 100644 index 0000000..8d56406 --- /dev/null +++ b/web/api/userctrl/report.go @@ -0,0 +1,131 @@ +package userctrl + +import ( + "time" + + "91porn-server/common" + "91porn-server/common/pageopt" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/reptmod" + "91porn-server/models/v/repttypemod" + "91porn-server/web/service/userser" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ReportList doc +// @Summary 获取用户举报列表 +// @Description 获取用户举报列表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query integer true "当前页" +// @Param pageSize query integer true "每页条数" +// @Param uid query integer false "过滤条件-举报者ID" +// @Param objType query string false "过滤条件-举报对象类型 video、comment、user" +// @Param types query array false "过滤条件-举报分类" +// @Param startTime query integer false "过滤条件-举报时间的开始时间" +// @Param endTime query integer false "过滤条件-举报时间的结束时间" +// @Success 200 {string} json "{"msg": "操作成功", "date": { "total":10, "list":[] }}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/report/list [get] +func ReportList(c *gin.Context) { + type Query struct { + UID *uint64 `form:"uid" json:"uid" bson:"uid,omitempty" binding:""` //用户ID + ObjType *reptmod.ReportObjType `form:"objType" json:"objType" bson:"objType,omitempty" binding:""` //举报对象类型数组 + Types *string `form:"types" json:"types" bson:"types,omitempty" binding:""` //举报分类 + HasLocked *bool `form:"hasLocked" json:"hasLocked,omitempty" bson:"hasLocked,omitempty" binding:""` //已禁止登陆 + HasBanned *bool `form:"hasBanned" json:"hasBanned,omitempty" bson:"hasBanned,omitempty" binding:""` //已禁言 + StartTime *int64 `form:"startTime" json:"startTime,omitempty" bson:"startTime,omitempty" binding:""` //开始时间 + EndTime *int64 `form:"endTime" json:"endTime,omitempty" bson:"endTime,omitempty" binding:""` //结束时间 + } + var arg struct { + commod.Page + Query + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user ReportList arg error "+err.Error()) + return + } + endTime := time.Now() + if arg.EndTime != nil { + endTime = time.Unix(*arg.EndTime, 0) + } + startTime := endTime.AddDate(0, -1, 0) + if arg.StartTime != nil { + startTime = time.Unix(*arg.StartTime, 0) + } + typesArray := []reptmod.ReportObjType{reptmod.Video, reptmod.Comment, reptmod.User} + if arg.ObjType != nil { + typesArray = []reptmod.ReportObjType{*arg.ObjType} + } + //分页项 + sort := bson.D{{Key: "createdAt", Value: -1}} + skip := int64((arg.PageNumber - 1) * arg.PageSize) + limit := int64(arg.PageSize) + //过滤项 + page, err := userser.ReportInfoPages(typesArray, sort, skip, limit, + pageopt.UIDMatch{UID: arg.UID}, + reptmod.TypesMatch{Types: arg.Types}, + pageopt.CreatedAtGTEAndLTMatch{GTE: &startTime, LT: &endTime}, + ) + if err != nil { + common.ServeJSON(c, stderr.Failure, "web user ReportInfoPages error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} + +// ReportList doc +// @Summary 获取用举报类型 +// @Description 获取举报类型 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": []}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/report/list [get] +func ReportTypesList(c *gin.Context) { + list, err := repttypemod.List() + if err != nil { + common.ServeJSON(c, stderr.Failure, "web user ReportTypesList error: "+err.Error()) + return + } + typesList := make([]string, 0, len(list)) + for _, report := range list { + if report.Name != nil { + typesList = append(typesList, *report.Name) + } + } + common.ServeJSON(c, stderr.Success, typesList) +} + +// SetStatus doc +// @Summary 设置举报状态 +// @Description 设置举报状态 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param status formData bool true "举报状态" +// @Param reptID formData bool true "举报ID" +// @Success 200 {string} json "{"msg": []}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/report/setStatus [post] +func SetStatus(c *gin.Context) { + var arg struct { + ReptID primitive.ObjectID `form:"reptID" json:"reptID" binding:"required"` + Status *bool `form:"status" json:"status" binding:"required"` + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user SetStatus arg error "+err.Error()) + return + } + if err := reptmod.SetStatus(arg.ReptID, *arg.Status); err != nil { + common.ServeJSON(c, stderr.Failure, "web user SetStatus error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, "") +} diff --git a/web/api/userctrl/user.go b/web/api/userctrl/user.go new file mode 100644 index 0000000..a78c0fc --- /dev/null +++ b/web/api/userctrl/user.go @@ -0,0 +1,1116 @@ +package userctrl + +import ( + "91porn-server/common/db" + "91porn-server/models/v/productmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/walletmod" + "91porn-server/web/webg" + "encoding/json" + "fmt" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/common/stderr" + v10 "91porn-server/common/v10" + "91porn-server/common/ysphone" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/operationlogmod" + "91porn-server/models/v/usermod" + "91porn-server/web/middleware/authweb" + "91porn-server/web/service/userser" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" +) + +// List doc +// @Summary 获取用户列表 +// @Description 获取用户列表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber query integer true "当前页" +// @Param pageSize query integer true "每页条数" +// @Param uid query integer false "过滤条件-uid" +// @Param devID query string false "过滤条件-设备ID" +// @Param vipLevel query integer false "过滤条件-VIP等级" +// @Param hasPromotionCode query bool false "过滤条件-有邀请码" +// @Param name query string false "过滤条件-用户昵称" +// @Param mobile query string false "过滤条件-手机号" +// @Param promotionCode query string false "过滤条件-邀请码" +// @Param hasLocked query bool false "过滤条件-已禁止登陆" +// @Param hasBanned query bool false "过滤条件-已禁言" +// @Param forbidUpload query bool false "过滤条件-禁止上传文件" +// @Param districtCode query bool false "过滤条件-商区码" +// @Param startTime query integer false "过滤条件-开始时间" +// @Param endTime query integer false "过滤条件-结束时间" +// @Param isPretendAcc query string false "过滤条件-是否马甲账号" +// @Param autoFollow query string false "过滤条件-是否配置被自动关注" +// @Param registerIP query string false "过滤条件-指定某IP地址注册的用户" +// @Success 200 {string} json "{"msg": "操作成功", "date": { "total":10, "list":[] }}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/list [get] +func List(c *gin.Context) { + type Filter struct { + UID *uint64 `form:"uid" json:"uid" binding:""` //uid + DevID *string `form:"devID" json:"devID" binding:""` //设备ID + VipLevel *int `form:"vipLevel" json:"vipLevel" binding:""` //vip等级 + Name *string `form:"name" json:"name" binding:""` //用户名 + Mobile *string `form:"mobile" json:"mobile" binding:""` //手机号 + PromotionCode *string `form:"promotionCode" json:"promotionCode" binding:""` //推广码 + HasPromotionCode *bool `form:"hasPromotionCode" json:"hasPromotionCode" binding:""` //有推广码 + HasLocked *bool `form:"hasLocked" json:"hasLocked" binding:""` //已禁止登陆 + HasBanned *bool `form:"hasBanned" json:"hasBanned" binding:""` //已禁言 + ForbidUpload *bool `form:"forbidUpload" json:"forbidUpload" binding:""` //是否禁止上传文件 + StartTime *time.Time `form:"startTime" json:"startTime" binding:""` //开始时间 + EndTime *time.Time `form:"endTime" json:"endTime" binding:""` //结束时间 + DistrictCode *string `form:"districtCode" json:"districtCode" binding:""` //商区码 + IsPretendAcc *int `form:"isPretendAcc" json:"isPretendAcc"` //是否马甲账号 + AutoFollow *bool `form:"autoFollow" json:"autoFollow" binding:""` //是否配置被自动关注 + RegisterIP *string `form:"registerIP" json:"registerIP" binding:"-"` // IP过滤 + } + var arg struct { + commod.Page + Filter + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "user List arg error "+err.Error()) + return + } + //马甲账户 + if arg.IsPretendAcc != nil && *arg.IsPretendAcc == 1 { + devID := "^" + usermod.SystemDevIDPrex + arg.DevID = &devID + } + if arg.Name != nil { + name := "^" + *arg.Name + arg.Name = &name + } + sort := bson.D{{Key: "createdAt", Value: -1}} + skip := int64((arg.PageNumber - 1) * arg.PageSize) + limit := int64(arg.PageSize) + page, err := userser.UserPages(sort, skip, limit, + pageopt.UIDMatch{UID: arg.UID}, + usermod.DevIDRegexMatch{DevID: arg.DevID}, + usermod.VipLevelMatch{VipLevel: arg.VipLevel}, + usermod.NameRegexMatch{Name: arg.Name}, + usermod.MobileMatch{Mobile: arg.Mobile}, + usermod.PromotionCodeMatch{PromotionCode: arg.PromotionCode}, + usermod.HasPromotionCodeMatch{IsExisted: arg.HasPromotionCode}, + usermod.LockedMatch{Locked: arg.HasLocked}, + usermod.BannedMatch{Banned: arg.HasBanned}, + usermod.ForbidUploadMatch{ForbidUpload: arg.ForbidUpload}, + usermod.CreatedAtGTEAndLTMatch{GTE: arg.StartTime, LT: arg.EndTime}, + usermod.DistrictCodeMatch{DistrictCode: arg.DistrictCode}, + usermod.AutoFollowMatch{AutoFollow: arg.AutoFollow}, + usermod.RegisterIpMatch{RegisterIp: arg.RegisterIP}, + ) + if err != nil { + common.ServeJSON(c, stderr.Failure, "user List error: "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, page) +} + +// Update doc +// @Summary 用户编辑 +// @Description 用户编辑 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData string false "uid" +// @Param vipLevel formData integer false "vip等级" +// @Param mobile formData integer false "手机号" +// @Param vipExpireDate formData string false "vip到期时间 format:2001-09-07T03:14:54.072Z" +// @Param dramaExpire formData string false "短剧权益有效期 format:2001-09-07T03:14:54.072Z" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/update [post] +func Update(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + type Update struct { + VipExpireDate *string `form:"vipExpireDate" json:"vipExpireDate" binding:""` //vip到期时间 + VipExpireDateModifyReason *string `form:"vipExpireDateModifyReason" json:"vipExpireDateModifyReason"` // 修改VIP到期时间原因 + VipLevel *int `form:"vipLevel" json:"vipLevel" binding:""` //vip等级 + Amount *int64 `form:"amount" json:"amount"` + Background *[]string `form:"background" json:"background"` //背景图 + //web端零值不传,导致无法将电话改为空 + //web端正确情况应是 空值不传(web后台无修改时), 零值应传(web后台修改为""后) + Mobile *string `form:"mobile" json:"mobile" binding:""` //手机号 + Portrait *string `form:"portrait" json:"portrait"` + Region *string `form:"region" json:"region"` //地区 + Summary *string `form:"summary" json:"summary"` + Name *string `form:"name" json:"name"` + SuperUser *bool `form:"superUser" json:"superUser"` //大v + OfficialCert *bool `form:"officialCert" json:"officialCert"` //是否官方认证 + IsMadou *bool `form:"isMadou" json:"isMadou"` //是否麻豆账号 + TaxLevel *int64 `form:"taxLevel" json:"taxLevel"` //扣税等级,默认0,3.7; + AutoFollow *bool `form:"autoFollow" json:"autoFollow"` //是否配置被自动关注 + BankActName *string `json:"bankActName,omitempty" bson:"bankActName,omitempty"` //银行卡绑定名字 + OriginalSort *int `json:"originalSort" bson:"originalSort"` // 新版原创排序 + VideoDeduction *float64 `json:"videoDeduction" bson:"videoDeduction"` //视频扣量,1:博主10单扣1单, 以此类推 + VideoDeductionPayCount *int `json:"videoDeductionPayCount" bson:"videoDeductionPayCount"` //视频扣量购买次数 + VideoDeductionCount *int `json:"videoDeductionCount" bson:"videoDeductionCount"` //视频扣量次数 + MerchantAccount *string `json:"merchantAccount,omitempty" bson:"merchantAccount,omitempty"` //商家账号 + VipID *string `form:"vipID" json:"vipID" bson:"vipID,omitempty"` // 会员卡ID + AllGoldVideoFree *bool `json:"allGoldVideoFree" bson:"allGoldVideoFree"` // 是否所有金币视频免费 + GoldVideoFreeExpire *time.Time `json:"goldVideoFreeExpire" bson:"goldVideoFreeExpire,omitempty"` // 金币视频免费日期 + GoldVideoFreeLimit *int64 `json:"goldVideoFreeLimit" bson:"goldVideoFreeLimit"` // 金币视频免费限制门槛(包含acg) + ChatPrice *int64 `form:"chatPrice" json:"chatPrice"` // 私信价格 + BroadcastExpire *time.Time `json:"broadcastExpire" bson:"broadcastExpire,omitempty"` //直播有效期 + DramaExpire *time.Time `json:"dramaExpire" bson:"dramaExpire,omitempty"` //短剧权益有效期 + + } + var arg struct { + UID uint64 `form:"uid" json:"uid" binding:"required"` + Update + } + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user Update arg error "+err.Error()) + return + } + if arg.VipExpireDate != nil && (arg.VipExpireDateModifyReason == nil || *arg.VipExpireDateModifyReason == "") { + common.ServeJSON(c, stderr.ErrParamError, "web user Update arg error: "+"请填写更新用户VIP到期时间的原因") + return + } + //获取更新用户信息 + u, err := usermod.FindUserByUIDForNoCache(arg.UID) + if err != nil || u == nil { + common.ServeJSON(c, stderr.UserIsNotExists, "web user Update arg error: 更新用户不存在 ") + return + } + //视频扣量更新, 则充值扣量次数 + if arg.Update.VideoDeduction != nil && *arg.Update.VideoDeduction != 0 && *arg.Update.VideoDeduction != u.VideoDeduction { + var n int = 0 + arg.Update.VideoDeductionPayCount = &n + arg.Update.VideoDeductionCount = &n + } + //修改账号金币 + opLogs := []operationlogmod.OperationLog{} + if arg.Amount != nil && *arg.Amount != 0 { + opLog, err := userser.ModifyUserAmount(arg.UID, *arg.Amount, manager, "") + if err != nil { + common.ServeJSON(c, stderr.Failure, err) + return + } + opLogs = []operationlogmod.OperationLog{opLog} + } + if arg.VipID != nil { + oid, err := primitive.ObjectIDFromHex(*arg.VipID) + if err != nil { + log.Error(fmt.Sprintf("admin add vip param VipID err:%v, VipID:%v", err, *arg.VipID)) + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + code := AddVIP(arg.UID, oid, u.DevType) + if code != stderr.Success { + log.Error(fmt.Sprintf("admin add vip err:%v,code:%v", code.Error(), code)) + common.ServeJSON(c, code, nil) + return + } + } + //修改用户手机 + if arg.Mobile != nil && u.Mobile != *arg.Mobile { //注意会有空字符串的情况 + mobile := *arg.Mobile + if *arg.Mobile != "" && (len(mobile) > 6 && mobile[:len(constant.FakeMobilePrefix)] != constant.FakeMobilePrefix) { + if !v10.IsGlobalizationPhoneNumber(mobile) { + common.ServeJSON(c, stderr.ErrParamError, "web user Update arg.Mobile error") + return + } + } + if err = userser.ModifyUserMobile(arg.UID, ysphone.FormatPhoneNumber(*arg.Mobile)); err != nil { + switch err.(type) { + case usermod.MobileHasBindError: + common.ServeJSON(c, stderr.ErrMobileHasBind, err) + return + } + if strings.Contains(err.Error(), "mobile_1") { + common.ServeJSON(c, stderr.ErrMobileHasBindByOther, err) + return + } + common.ServeJSON(c, stderr.Failure, err) + return + } + } + doc := usermod.UserSelector{ + VipLevel: arg.VipLevel, + Portrait: arg.Portrait, + Summary: arg.Summary, + Name: arg.Name, + Region: arg.Region, + Background: arg.Background, + SuperUser: arg.SuperUser, + OfficialCert: arg.OfficialCert, + TaxLevel: arg.TaxLevel, + AutoFollow: arg.AutoFollow, + BankActName: arg.BankActName, + OriginalSort: arg.OriginalSort, + VideoDeduction: arg.VideoDeduction, + VideoDeductionCount: arg.VideoDeductionCount, + VideoDeductionPayCount: arg.VideoDeductionPayCount, + ChatPrice: arg.ChatPrice, + BroadcastExpire: arg.BroadcastExpire, + DramaExpire: arg.DramaExpire, + } + if arg.AllGoldVideoFree != nil { + doc.AllGoldVideoFree = arg.AllGoldVideoFree + } + if arg.GoldVideoFreeExpire != nil { + doc.GoldVideoFreeExpire = arg.GoldVideoFreeExpire + } + if arg.GoldVideoFreeLimit != nil { + doc.GoldVideoFreeLimit = arg.GoldVideoFreeLimit + } + if arg.MerchantAccount != nil { + doc.MerchantAccount = arg.MerchantAccount + } + //修改vip到期时间 + if arg.VipExpireDate != nil { + vipExpireDate, err := time.Parse(time.RFC3339, *arg.VipExpireDate) + if err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user Update arg.VipExpireDate error "+err.Error()) + return + } + doc.VipExpireDate = &vipExpireDate + opLog := operationlogmod.OperationLog{ + UserID: int64(arg.UID), + OperationAPI: c.Request.URL.Path, + OperationType: constant.User_Edit_VIP_Expire, + BeforeContent: u.VipExpireDate.Local().String(), + AfterContent: vipExpireDate.Local().String(), + Reason: *arg.VipExpireDateModifyReason, + CreatedUser: manager, + CreatedID: 0, + CreatedAt: time.Now(), + } + opLogs = append(opLogs, opLog) + } + user, err := usermod.Update(arg.UID, doc) + if err != nil { + common.ServeJSON(c, stderr.ErrServerUnavailable, err) + return + } + if user.UID <= 0 { + common.ServeJSON(c, stderr.UserIsNotExists, nil) + return + } + common.Go(func() { + if err := operationlogmod.InsertMany(opLogs); err != nil { + log.ErrorX(c, "insert operation log failed", log.Any("logs", opLogs), log.Any("req", arg), + log.Any("operator", manager), log.E(err)) + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.UserManageList, constant.Modify, string(log), + c.Request.URL.RequestURI()) + }) + common.ServeJSON(c, stderr.Success, "") +} + +// Banned doc +// @Summary 批量使能用户禁言 +// @Description 批量使能用户禁言 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uidList formData array true "uid数组" +// @Param enable formData bool true "ture:开始禁言,false:关闭禁言" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/banned [post] +func Banned(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg userser.UserCommentBanned + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user Banned arg error "+err.Error()) + return + } + code := arg.Banned() + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.UserManageList, constant.Modify, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, code, "") +} + +// SetAdvertiser doc +// @Summary 批设置用户为打广告用户 +// @Description 批设置用户为打广告用户 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData array true "uid数组" +// @Param enable formData bool true "ture:开启,false:关闭" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/advertiser [post] +func SetAdvertiser(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg usermod.SetAdvertiseCond + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user SetAdvertiser arg error "+err.Error()) + return + } + err = userser.SetAdvertiser(&arg) + if err != nil { + common.ServeJSON(c, stderr.Failure, err) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.UserManageList, constant.Modify, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// Lock doc +// @Summary 批量使能禁止用户登陆 +// @Description 批量使能禁止用户登陆 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uidList formData array true "uid数组" +// @Param enable formData bool true "ture:禁止用户登陆,false:允许用户登陆" +// @Param reason formData string false "封禁原因" +// @Param lockAt formData string false "封禁开始时间,format:2006-01-02T15:04:05Z07:00" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/lock [post] +func Lock(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg userser.LockUserReq + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user Banned arg error "+err.Error()) + return + } + code := arg.Lock() + if code != stderr.Success { + common.ServeJSON(c, code, "") + return + } + authweb.RevokeTokenCache(arg.UID) + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.UserManageList, constant.Modify, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// ForbidUpload doc +// @Summary 批量使用户禁止上传文件 +// @Description 批量使用户禁止上传文件 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uidList formData array true "uid数组" +// @Param enable formData bool true "true:开始禁止上传文件,false:取消禁止上传文件" +// @Param reason formData string false "true:开始禁止上传文件,false:取消禁止上传文件" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/forbid [post] +func Forbid(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + UIDList []uint64 `form:"uidList" json:"uidList" binding:"required"` + Enable *bool `form:"enable" json:"enable" binding:"required"` + Reason *string `form:"reason" json:"reason"` + } + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user Forbid arg error "+err.Error()) + return + } + doc := usermod.UserSelector{ForbidUpload: arg.Enable} + if _, err = usermod.UpdateMany(arg.UIDList, doc); err != nil { + common.ServeJSON(c, stderr.ErrServerUnavailable, err) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.UserManageList, constant.Modify, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// PlayRecord doc +// @Summary 用户播放记录表 +// @Description 用户播放记录表 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer true "用户id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/devID/reset [get] +func DevIDReset(ctx *gin.Context) { + adminID, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset GetAdminAct error "+err.Error()) + return + } + role, err := common.GetAdminRole(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset GetAdminRole error "+err.Error()) + return + } + if role != "superAdmin" { + common.ServeJSON(ctx, stderr.ErrAccessForbid, "") + return + } + type Info struct { + UID uint64 `form:"uid" json:"uid"` + } + param := Info{} + if err = ctx.ShouldBind(¶m); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset param error "+err.Error()) + return + } + _uuid, err := uuid.NewRandom() + if err != nil { + common.ServeJSON(ctx, stderr.Failure, "web user DevIDReset uuid NewRandom error "+err.Error()) + return + } + if err = usermod.ResetDevID(param.UID, _uuid.String()); err != nil { + common.ServeJSON(ctx, stderr.Failure, "web user DevIDReset ResetDevID error "+err.Error()) + return + } + log, _ := json.Marshal(param) + _ = operatorlgmod.RecordOperation(adminID, constant.UserManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// CreditAmount doc +// @Summary 修改用户金币 +// @Description 修改用户金币 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer true "用户id" +// @Param amount formData integer true "增减金额" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/devID/reset [get] +func CreditAmount(ctx *gin.Context) { + adminID, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset GetAdminAct error "+err.Error()) + return + } + var p struct { + UID uint64 `json:"uid"` + Amount int64 `json:"amount"` + Reason string `json:"reason"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset param error "+err.Error()) + return + } + if err = userser.CreditAmount(p.UID, p.Amount, adminID, p.Reason); err != nil { + if err.Error() == "Insufficient balance" { + common.ServeJSON(ctx, stderr.InsufficientBalance, "web user DevIDReset ResetDevID error "+err.Error()) + return + } + common.ServeJSON(ctx, stderr.Failure, "web user DevIDReset ResetDevID error "+err.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(adminID, constant.UserManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +func FruitCoinChange(ctx *gin.Context) { + adminID, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset GetAdminAct error "+err.Error()) + return + } + var p struct { + UID uint64 `json:"uid"` + FruitCoin int64 `json:"fruitCoin"` + Reason string `json:"reason"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset param error "+err.Error()) + return + } + if err = userser.FruitCoinChange(p.UID, p.FruitCoin, adminID, p.Reason); err != nil { + if err.Error() == "Insufficient balance" { + common.ServeJSON(ctx, stderr.InsufficientBalance, "web user DevIDReset ResetDevID error "+err.Error()) + return + } + common.ServeJSON(ctx, stderr.Failure, "web user DevIDReset ResetDevID error "+err.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(adminID, constant.UserManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// PlayRecord doc +// @Summary 用户VIP权益 +// @Description 用户VIP权益 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer true "用户id" +// @Param pageNumber query integer true "当前页" +// @Param pageSize query integer true "每页条数" +// @Success 200 {object} usermod.UserVipInfoRes "success" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/user/vipInfo [get] +func VipInfo(c *gin.Context) { + var arg struct { + UID uint64 `form:"uid" json:"uid" binding:"required"` + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user VipInfo arg error "+err.Error()) + return + } + //查询 + userInfo, err := userser.Info(arg.UID) + if err != nil { + common.ServeJSON(c, stderr.ErrServerUnavailable, "web user VipInfo userser.Info error "+err.Error()) + return + } + common.ServeJSON(c, stderr.Success, userInfo) +} + +// CreditGold doc +// @Summary 用户金币回收 +// @Description 用户金币回收 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer true "用户id" +// @Param amount formData integer true "增减金额" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/credit/gold [post] +func CreditGold(ctx *gin.Context) { + adminID, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset GetAdminAct error "+err.Error()) + return + } + var p struct { + UID uint64 `json:"uid"` + Amount int64 `json:"amount"` + Income int64 `json:"income"` + Mark string `json:"mark"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset param error "+err.Error()) + return + } + if err = userser.CreditGold(p.UID, p.Amount, p.Income, adminID, p.Mark); err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(adminID, constant.UserManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// GenerateQRCode doc +// @Summary 生成用户登录用二维码 +// @Description 生成用户登录用二维码 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer true "用户ID" +// @Success 200 {string} string "qr content" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/user/qr_code [post] +func GenerateQRCode(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + UID uint64 `json:"uid" binding:"required"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + content, err := userser.GenerateQRCode(p.UID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrInterServerError, err) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, content) +} + +// UpdateFreeTimes doc +// @Summary 修改用户AI脱衣免费次数 +// @Description 修改用户AI脱衣免费次数 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer true "用户id" +// @Param freeTimes formData integer true "增减免费次数" +// @Param mark formData string true "备注" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/update/freeTimes [post] +func UpdateFreeTimes(ctx *gin.Context) { + adminAct, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset GetAdminAct error "+err.Error()) + return + } + + var in usermod.UpdateFreeTimesCond + err = ctx.ShouldBind(&in) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset param error "+err.Error()) + return + } + err = userser.UpdateFreeTimes(&in, adminAct) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + log, _ := json.Marshal(in) + _ = operatorlgmod.RecordOperation(adminAct, constant.UserManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// UpdateDownload doc +// @Summary 修改用户下载次数 +// @Description 修改用户下载次数 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer true "用户id" +// @Param download formData integer true "增减下载次数" +// @Param mark formData string true "备注" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/update/download [post] +func UpdateDownload(ctx *gin.Context) { + adminAct, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset GetAdminAct error "+err.Error()) + return + } + + var in usermod.UpdateDownloadCond + err = ctx.ShouldBind(&in) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset param error "+err.Error()) + return + } + err = userser.UpdateDownloadCounts(&in, adminAct) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + log, _ := json.Marshal(in) + _ = operatorlgmod.RecordOperation(adminAct, constant.UserManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// DeleteComment doc +// @Summary 删除用户评论 +// @Description 删除用户评论 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer true "用户id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/deleteComment [post] +func DeleteComment(ctx *gin.Context) { + adminAct, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset GetAdminAct error "+err.Error()) + return + } + + var in usermod.DeleteCommentCond + err = ctx.ShouldBind(&in) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset param error "+err.Error()) + return + } + err = userser.DeleteComment(&in, adminAct) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + log, _ := json.Marshal(in) + _ = operatorlgmod.RecordOperation(adminAct, constant.UserManageList, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// QueryGameCode doc +// @Summary 查询用户游戏码 +// @Description 查询用户游戏码 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param q query usermod.QueryGameCodeCond false "请求参数" +// @Success 200 object game.QueryData "成功" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/queryGameCode [get] +func QueryGameCode(ctx *gin.Context) { + var in usermod.QueryGameCodeCond + err := ctx.ShouldBind(&in) + if err != nil { + log.Error(fmt.Sprintf("web user QueryGameCode param error:%v", err)) + common.ServeJSON(ctx, stderr.ErrParamError, "web user QueryGameCode param error "+err.Error()) + return + } + data, err := userser.QueryGameCode(&in) + if err != nil { + log.Error(fmt.Sprintf("web user QueryGameCode error:%v", err)) + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, data) +} + +// UpdateLotteryTimes doc +// @Summary 修改用户抽奖次数 +// @Description 修改用户抽奖次数 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer true "用户id" +// @Param lotteryTimes formData integer true "增减抽奖次数" +// @Param mark formData string true "备注" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/update/lotteryTimes [post] +func UpdateLotteryTimes(ctx *gin.Context) { + adminAct, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset GetAdminAct error "+err.Error()) + return + } + + var in usermod.UpdateLotteryTimesCond + err = ctx.ShouldBind(&in) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset param error "+err.Error()) + return + } + err = userser.UpdateLotteryTimes(&in, adminAct) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + log, _ := json.Marshal(in) + _ = operatorlgmod.RecordOperation(adminAct, constant.UserManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// AddVIP 新增产品产生的行为 +func AddVIP(uid uint64, productID primitive.ObjectID, sys string) stderr.Code { + p, err := productmod.FindProduct(productID, sys) + if err != nil || p == nil { + return stderr.ErrParamError + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrNetWorkBusy + } + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return stderr.ErrNetWorkBusy + } + vipExpire, vipLevel, payVidDiscount, vName := checkVipRenew(u, p) + sel := usermod.UserSelector{VipExpireDate: &vipExpire, VipLevel: &vipLevel, PayVidDiscount: &payVidDiscount, VipName: &vName} + if p.GoldVideoFreeDay > 0 { + expire := time.Time{} + if u.GoldVideoFreeExpire.IsZero() || u.GoldVideoFreeExpire.Before(time.Now()) { + expire = time.Now().AddDate(0, 0, p.GoldVideoFreeDay) + } else { + expire = u.GoldVideoFreeExpire.AddDate(0, 0, p.GoldVideoFreeDay) + } + sel.GoldVideoFreeExpire = &expire + } + var plan walletmod.CreditPlan + if p.DownloadCount > 0 { + plan.DownloadCount = &p.DownloadCount + } + if p.AiUndressCount > 0 { + aiUndressFreeTimes := int64(p.AiUndressCount) + plan.AiUndressFreeTimes = &aiUndressFreeTimes + } + if p.LuckyDrawCount > 0 { + plan.LotteryTimes = &p.LuckyDrawCount + } + if p.ChatPrice > 0 { + sel.ChatPrice = &p.ChatPrice + } + if p.GiveCoin > 0 { + plan.Amount = &p.GiveCoin + } + if p.BroadcastDays > 0 { + expire := time.Time{} + if u.BroadcastExpire.IsZero() || u.BroadcastExpire.Before(time.Now()) { + expire = time.Now().AddDate(0, 0, p.BroadcastDays) + } else { + expire = u.BroadcastExpire.AddDate(0, 0, p.BroadcastDays) + } + sel.BroadcastExpire = &expire + } + if p.DramaDays > 0 { + expire := usermod.RenewDramaExpire(u.DramaExpire, time.Now(), p.DramaDays) + sel.DramaExpire = &expire + } + if err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + var err error + if err = usermod.UpdateVIP(t, uid, u.VipExpireDate, sel); err != nil { + return err + } + txnLogs := []txnmod.TransactionLog{ + { + UID: uid, + TranType: txnmod.AdminAddVIP.Key(), + TranTypeInt: int64(txnmod.AdminAddVIP), + Desc: "官方添加VIP-" + p.Name, + SysType: u.SysType, + RealAmount: w.RealAmount(), + }, + } + if p.AiUndressCount > 0 || p.GiveCoin > 0 || p.DownloadCount > 0 || p.LuckyDrawCount > 0 { + wallet, err := walletmod.Credit(t, plan, uid) + if err != nil { + return err + } + // 插入购买会员卡赠送金币流水 + if p.GiveCoin > 0 { + giveLog := txnmod.TransactionLog{ + UID: uid, + Amount: p.GiveCoin, + ActualAmount: float64(p.GiveCoin), + TranType: txnmod.VipCardGive.Key(), + TranTypeInt: int64(txnmod.VipCardGive), + Desc: "官方添加VIP-" + p.Name + "-赠送金币", + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + txnLogs = append(txnLogs, giveLog) + } + if p.AiUndressCount > 0 { + aiLog := txnmod.TransactionLog{ + UID: uid, + Amount: int64(p.AiUndressCount), + ActualAmount: float64(p.AiUndressCount), + TranType: txnmod.VipCardGiveAiUndressFreeCount.Key(), + TranTypeInt: int64(txnmod.VipCardGiveAiUndressFreeCount), + Desc: fmt.Sprintf("官方添加VIP%s-赠送AI脱衣免费次数[%v次]", p.Name, p.AiUndressCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + } + txnLogs = append(txnLogs, aiLog) + } + if p.DownloadCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: uid, + DownloadCount: p.DownloadCount, + TranType: txnmod.GiveDownload.Key(), + TranTypeInt: int64(txnmod.GiveDownload), + Desc: fmt.Sprintf("官方添加VIP-%v", p.Name) + fmt.Sprintf("-赠送视频下载[%d]次", p.DownloadCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if p.LuckyDrawCount > 0 { + txnLogs = append(txnLogs, txnmod.TransactionLog{ + UID: uid, + DownloadCount: p.LuckyDrawCount, + TranType: txnmod.GiveLotteryTimesCount.Key(), + TranTypeInt: int64(txnmod.GiveLotteryTimesCount), + Desc: fmt.Sprintf("官方添加VIP-%v", p.Name) + fmt.Sprintf("-赠送[%d]次数", p.LuckyDrawCount), + DiscDoc: u.DiscDoc, + SysType: u.SysType, + }) + } + } + if len(txnLogs) > 0 { + if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil { + log.Warn(fmt.Sprintf("productser addVIP Transaction err %s", err.Error())) + return err + } + } + return nil + }); err != nil { + log.Warn(fmt.Sprintf("productser addVIP Transaction err %s", err.Error())) + return stderr.BuyFailed //通知消息 + } + return stderr.Success +} +func checkVipRenew(u *usermod.User, p *productmod.Product) (time.Time, int, int, string) { + var ( + end time.Time + now = time.Now() + level = p.VipLevel + payVidDiscount = p.PayVidDiscount + d = time.Hour * 24 * time.Duration(p.Duration) + vipName = p.Name + ) + if u.VipExpireDate.After(now) { //renew + // 判断用户VIP等级 + if p.VipLevel > u.VipLevel { + end = now.Add(d) + } else if p.VipLevel == u.VipLevel { + end = u.VipExpireDate.Add(d) + } else if p.VipLevel < u.VipLevel { + end = u.VipExpireDate + } + + if u.VipLevel > p.VipLevel { //当前用户的vip等级比这次购买的大,使用用户的 + level = u.VipLevel + vipName = u.VipName + } + //当前用户的折扣比这次购买的大,使用用户的 + if u.PayVidDiscount < payVidDiscount && u.PayVidDiscount > 0 { + payVidDiscount = u.PayVidDiscount + } + } else { + end = now.Add(time.Duration(d)) + } + return end, level, payVidDiscount, vipName +} + +// SetaiMateBalance doc +// @Summary 设置用户AI伴侣积分 +// @Description 设置用户AI伴侣积分 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer true "用户id" +// @Param amount formData integer true "增减金额" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/credit/aiMateBalance [post] +func SetAiMateBalance(ctx *gin.Context) { + adminID, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset GetAdminAct error "+err.Error()) + return + } + var p struct { + UID uint64 `json:"uid"` + AiMateBalance float64 `json:"aiMateBalance"` //ai伴侣余额 + Reason string `json:"reason"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset param error "+err.Error()) + return + } + if p.AiMateBalance < 0 { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err = userser.SetAiMateBalance(p.UID, p.AiMateBalance, p.Reason); err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(adminID, constant.UserManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// PrivateZone doc +// @Summary 批量使用户开启私密圈权限 +// @Description 批量使用户开启私密圈权限 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uidList formData array true "uid数组" +// @Param enable formData bool true "ture:开启私密圈权限,false:关闭私密圈权限" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/privateZone [post] +func PrivateZone(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + UIDList []uint64 `form:"uidList" json:"uidList" binding:"required"` + Enable *bool `form:"enable" json:"enable" binding:"required"` + } + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web user Banned arg error "+err.Error()) + return + } + doc := usermod.UserSelector{HasPrivateZone: arg.Enable} + + if _, err = usermod.UpdateMany(arg.UIDList, doc); err != nil { + common.ServeJSON(c, stderr.ErrServerUnavailable, err) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.UserManageList, constant.Modify, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} + +// CreditIntegral doc +// @Summary 修改用户积分 +// @Description 修改用户积分 +// @Tags web-用户管理 +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData integer true "用户id" +// @Param integral formData integer true "增减积分" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/admin/user/integral [post] +func CreditIntegral(ctx *gin.Context) { + adminID, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset GetAdminAct error "+err.Error()) + return + } + var p struct { + UID uint64 `json:"uid"` + Integral int64 `json:"integral"` + Reason string `json:"reason"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, "web user DevIDReset param error "+err.Error()) + return + } + if err = userser.CreditIntegral(p.UID, p.Integral, adminID, p.Reason); err != nil { + if err.Error() == "Insufficient balance" { + common.ServeJSON(ctx, stderr.InsufficientBalance, "web user DevIDReset ResetDevID error "+err.Error()) + return + } + common.ServeJSON(ctx, stderr.Failure, "web user DevIDReset ResetDevID error "+err.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(adminID, constant.UserManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/versionctrl/version.go b/web/api/versionctrl/version.go new file mode 100644 index 0000000..7d4d076 --- /dev/null +++ b/web/api/versionctrl/version.go @@ -0,0 +1,177 @@ +package versionctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/common/version" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/versionmod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Findlist doc +// @Summary 版本信息查询 +// @Description 根据字段信息条件查询 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ver/list [get] +func FindList(ctx *gin.Context) { + var arg struct { + common.StandQuery + versionmod.VersionQueryReq + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + total, data, err := versionmod.FindMany(common.StandQueryMap(arg.StandQuery, arg.VersionQueryReq)) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, map[string]interface{}{ + "total": total, + "history": data, + }) +} + +// Insert doc +// @Summary 新增 +// @Description 新增 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/ver [post] +func Insert(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + v := versionmod.Version{} + if err = ctx.ShouldBind(&v); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + cv, err := version.New(v.VersionName) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + } + v.VersionName = cv.String() + v.Code = cv.GetCode() + if v.VersionRange.Major != "" && v.VersionRange.Minor != "" { + major, _ := version.New(v.VersionRange.Major) + minor, _ := version.New(v.VersionRange.Minor) + if major.LT(minor) { + common.ServeJSON(ctx, stderr.ErrVersionRange, nil) + return + } + } + if v.SpecVersion != "" { + spc, _ := version.New(v.SpecVersion) + if spc.GTE(cv) { + common.ServeJSON(ctx, stderr.ErrVersionTooLow, nil) + return + } + } + if err = versionmod.Insert(&v); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(v) + _ = operatorlgmod.RecordOperation(manager, constant.SystemManageVersion, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Update doc +// @Summary 修改producttype +// @Description 修改producttype +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web [put] +func Update(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var q struct { + ID primitive.ObjectID `json:"id" form:"id" binding:"required"` + versionmod.VersionUpdateReq + } + if err = ctx.ShouldBind(&q); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if q.VersionRange.Major != "" && q.VersionRange.Minor != "" { + major, _ := version.New(q.VersionRange.Major) + minor, _ := version.New(q.VersionRange.Minor) + if major.LT(minor) { + common.ServeJSON(ctx, stderr.ErrVersionRange, nil) + return + } + } + if q.SpecVersion != nil { + vmod, _ := versionmod.FindOneByID(q.ID) + if vmod != nil { + vname, _ := version.New(vmod.VersionName) + spc, _ := version.New(*q.SpecVersion) + if vname.GTE(spc) { + common.ServeJSON(ctx, stderr.ErrVersionTooLow, nil) + return + } + } + } + if err = versionmod.Update(q.ID, q.VersionUpdateReq); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, "") + return + } + log, _ := json.Marshal(q) + _ = operatorlgmod.RecordOperation(manager, constant.SystemManageVersion, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Delete doc +// @Summary 删除 +// @Description 删除 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Param id formData int true "product等级"" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /web/ver [Delete] +func Delete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + ID primitive.ObjectID `json:"id" form:"id" binding:"required"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err = versionmod.Delete(p.ID); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, "") + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.SystemManageVersion, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/vidctrl/discount_area.go b/web/api/vidctrl/discount_area.go new file mode 100644 index 0000000..66890e3 --- /dev/null +++ b/web/api/vidctrl/discount_area.go @@ -0,0 +1,236 @@ +package vidctrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/discount_area_mod" + "91porn-server/web/service/discount_area_ser" + "encoding/json" + "github.com/gin-gonic/gin" +) + +// DiscountAreaList doc +// @Summary 获取折扣专区列表 +// @Description 获取折扣专区列表 +// @Tags discountArea +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} discount_area_mod.WebDiscountAreaListResp "{"msg": "操作成功"}" +// @Failure 400 {string} string "{"msg": "操作失败"}" +// @Router /api/web/admin/discount_area/list [get] +func DiscountAreaList(ctx *gin.Context) { + list, err := discount_area_mod.GetAllNoDeleteDiscountArea() + if err != nil { + return + } + common.ServeJSON(ctx, stderr.Success, &discount_area_mod.WebDiscountAreaListResp{ + List: list, + }) +} + +// DiscountAreaAdd doc +// @Summary 新增折扣专区 +// @Description 新增折扣专区 +// @Tags discountArea +// @Accept mpfd,json +// @Produce json,html +// @Param title formData string true "标题" +// @Param desc formData string true "描述" +// @Param discount formData integer true "折扣,1折 10,1.5折 15" +// @Param status formData integer true "0-未开启 1-开启" +// @Param sortCode formData integer true "排序字段" +// @Param recommendNum formData integer true "推荐展示视频个数" +// @Success 200 {object} string "{"msg": "操作成功"}" +// @Failure 400 {string} string "{"msg": "操作失败"}" +// @Router /api/web/admin/discount_area/add [post] +func DiscountAreaAdd(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := discount_area_mod.WebDiscountAreaAddReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if req.Discount < 0 || req.Discount > 100 { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + err = discount_area_ser.AddDiscountArea(&req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoDiscountArea, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// DiscountAreaUpdate doc +// @Summary 修改折扣专区 +// @Description 修改折扣专区 +// @Tags discountArea +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "id" +// @Param title formData string false "标题" +// @Param desc formData string false "描述" +// @Param discount formData integer false "折扣,1折 10,1.5折 15" +// @Param status formData integer false "0-未开启 1-开启" +// @Param sortCode formData integer false "排序字段" +// @Param recommendNum formData integer true "推荐展示视频个数" +// @Success 200 {object} string "{"msg": "操作成功"}" +// @Failure 400 {string} string "{"msg": "操作失败"}" +// @Router /api/web/admin/discount_area/update [post] +func DiscountAreaUpdate(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := discount_area_mod.WebDiscountAreaUpdateReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if req.Discount != nil && (*req.Discount < 0 || *req.Discount > 100) { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + err = discount_area_ser.UpdateDiscountArea(&req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoDiscountArea, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// DiscountAreaDelete doc +// @Summary 删除折扣专区 +// @Description 删除折扣专区 +// @Tags discountArea +// @Accept mpfd,json +// @Produce json,html +// @Param id formData integer true "id" +// @Success 200 {object} string "{"msg": "操作成功"}" +// @Failure 400 {string} string "{"msg": "操作失败"}" +// @Router /api/web/admin/discount_area/delete [post] +func DiscountAreaDelete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := discount_area_mod.WebDiscountAreaDeleteReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + err = discount_area_ser.DeleteDiscountArea(req.ID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, nil) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoDiscountArea, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// DiscountAreaVids doc +// @Summary 折扣专区视频列表 +// @Description 折扣专区视频列表 +// @Tags discountArea +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "页码数" +// @Param pageSize formData integer true "页码数" +// @Param discountAreaId formData string true "折扣专区id" +// @Param vid formData string true "帖子id" +// @Success 200 {object} discount_area_mod.WebDiscountAreaVidsResp "{"msg": "操作成功"}" +// @Failure 400 {string} string "{"msg": "操作失败"}" +// @Router /api/web/admin/discount_area/vid/list [get] +func DiscountAreaVids(ctx *gin.Context) { + req := discount_area_mod.WebDiscountAreaVidsReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + list, total, err := discount_area_ser.GetDiscountAreaVids(&req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, &discount_area_mod.WebDiscountAreaVidsResp{ + List: list, + Total: total, + }) +} + +// DiscountAreaAddVid doc +// @Summary 折扣专区新增视频 +// @Description 折扣专区新增视频 +// @Tags discountArea +// @Accept mpfd,json +// @Produce json,html +// @Param discountAreaId formData string true "折扣专区id" +// @Param vids formData []string true "帖子id" +// @Success 200 {object} string "{"msg": "操作成功"}" +// @Failure 400 {string} string "{"msg": "操作失败"}" +// @Router /api/web/admin/discount_area/vid/add [post] +func DiscountAreaAddVid(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := discount_area_mod.WebDiscountAreaAddVidReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code := discount_area_ser.DiscountAreaAddVid(&req) + if code != stderr.Success { + common.ServeJSON(ctx, code, code.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoDiscountAreaVideo, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, nil) +} + +// DiscountAreaDeleteVid doc +// @Summary 折扣专区删除视频 +// @Description 折扣专区删除视频 +// @Tags discountArea +// @Accept mpfd,json +// @Produce json,html +// @Param vids formData []string true "视频id" +// @Success 200 {object} string "{"msg": "操作成功"}" +// @Failure 400 {string} string "{"msg": "操作失败"}" +// @Router /api/web/admin/discount_area/vid/delete [post] +func DiscountAreaDeleteVid(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := discount_area_mod.WebDiscountAreaDeleteVidReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code := discount_area_ser.DiscountAreaDeleteVid(req.Ids) + if code != stderr.Success { + common.ServeJSON(ctx, code, code.Error()) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoDiscountAreaVideo, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, nil) +} diff --git a/web/api/vidctrl/mediaresource.go b/web/api/vidctrl/mediaresource.go new file mode 100644 index 0000000..acaa090 --- /dev/null +++ b/web/api/vidctrl/mediaresource.go @@ -0,0 +1,118 @@ +package vidctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/web/service/vidser" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + + "github.com/gin-gonic/gin" +) + +// GetMediaList doc +// @Summary 获取媒体资源库资源 +// @Description 获取媒体资源库资源 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Router /api/web/admin/mediaResource/list [get] +func GetMediaList(ctx *gin.Context) { + var p struct { + Title string `form:"title" json:"title" ` + TagText string `form:"tag_text" json:"tag_text"` //标签搜索 + HashId string `form:"hash_id" json:"hash_id"` + MVia int `form:"mVia" json:"mVia"` //0 (defalut) from all;1 from uploader;2 from non-uploader + VideoType int `form:"videoType" json:"videoType"` //0 长视频 1短视频 2图文 + VloggerUserId int `form:"vlogger_user_id" json:"vlogger_user_id"` //up主所在平台的uid + SyncType string `form:"syncType" json:"syncType"` //同步类型all 全量 incr + TerminalCdo string `form:"terminalCdo" json:"terminalCdo"` // 上传者 + SortType int `form:"sortType" json:"sortType"` // 0-正序 1-倒序 + StartTime time.Time `form:"start" json:"start"` // 开始时间 + EndTime time.Time `form:"end" json:"end"` // 结束时间 + MaxTotal int `json:"maxTotal" form:"maxTotal"` // 最大列表总数 + commod.Page + } + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + data, total, err := vidser.GetMediaList(ctx, int64(p.PageNumber), int64(p.PageSize), int64(p.VloggerUserId), + p.Title, p.TagText, p.HashId, p.MVia, p.VideoType, p.SyncType, p.TerminalCdo, p.StartTime, p.EndTime, p.SortType, p.MaxTotal) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "count": total, + "list": data, + }) +} + +// UseMedia doc +// @Summary 使用资源 +// @Description 使用资源媒体资源库资源 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Router /api/web/admin/mediaResource/use [post] +func UseMedia(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + return + } + var p struct { + IDs []int64 `json:"ids"` + AuthorID uint64 `json:"authorID"` + Tags []string `json:"tags"` + SectionID primitive.ObjectID `json:"sectionID"` + VideoType int `json:"videoType"` //0 长视频 1短视频 2图文 + OnlineTime time.Time `json:"onlineTime" form:"onlineTime"` // 自动上架时间 + Coins *int64 `json:"coins" form:"coins"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + hits, err := vidser.UseMedias(ctx, p.IDs, p.AuthorID, p.SectionID, p.Tags, p.VideoType, manager, p.Coins, p.OnlineTime) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + resp := gin.H{"sensitiveHits": hits} + if len(hits) > 0 { + resp["sensitiveTip"] = vidser.SensitiveForcedOfflineTip + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +// IgnoreMedia doc +// @Summary 忽略媒体资源 +// @Description 忽略媒体资源库资源 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Router /api/web/admin/mediaResource/ignore [post] +func IgnoreMedia(ctx *gin.Context) { + var p struct { + IDs []int64 `json:"ids"` + VideoType int `json:"videoType"` + } + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if err := vidser.IgnoreMedias(ctx, p.IDs, p.VideoType); err != nil { + common.ServeJSON(ctx, stderr.Failure, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/vidctrl/rejecttemplate.go b/web/api/vidctrl/rejecttemplate.go new file mode 100644 index 0000000..69dc74f --- /dev/null +++ b/web/api/vidctrl/rejecttemplate.go @@ -0,0 +1,126 @@ +package vidctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/rjctmpltmod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetRejectTemplate doc +// @Summary 获取审核拒绝模版 +// @Description 获取审核拒绝模版 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/recharge/channel/list [get] +func GetRejectTemplate(ctx *gin.Context) { + rType := ctx.Query("type") + infos, err := rjctmpltmod.FindMany(rType) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{ + "list": infos, + }) +} + +// RejectTemplateInsert doc +// @Summary 新增 +// @Description 新增 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Param reachargeDetails formData rjctmpltmod.RejectTemplate ture "product卡" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/rejectTemp/insert [post] +func RejectTemplateInsert(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + r := rjctmpltmod.RejectTemplate{} + if err = ctx.ShouldBind(&r); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = rjctmpltmod.InsertOne(r); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInputExist, err.Error()) + return + } + log, _ := json.Marshal(r) + _ = operatorlgmod.RecordOperation(manager, constant.RejectTemplate, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// RejectTemplateUpdate doc +// @Summary 编辑充值渠道 +// @Description 编辑充值渠道 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/recharge/channel/update [post] +func RejectTemplateUpdate(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + set := rjctmpltmod.Edit{} + if err = ctx.ShouldBind(&set); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = rjctmpltmod.Update(set); err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + log, _ := json.Marshal(set) + _ = operatorlgmod.RecordOperation(manager, constant.RejectTemplate, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// RejectTemplateDelete doc +// @Summary 删除 +// @Description 删除 +// @Tags recharge +// @Accept mpfd,json +// @Produce json,html +// @Param id formData int true "product等级"" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/recharge/channel/delete [delete] +func RejectTemplateDelete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var p struct { + IDs []primitive.ObjectID `json:"ids"` + } + if err = ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = rjctmpltmod.DeleteMany(p.IDs); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + log, _ := json.Marshal(p) + _ = operatorlgmod.RecordOperation(manager, constant.RejectTemplate, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} diff --git a/web/api/vidctrl/vid.go b/web/api/vidctrl/vid.go new file mode 100644 index 0000000..8c53264 --- /dev/null +++ b/web/api/vidctrl/vid.go @@ -0,0 +1,840 @@ +package vidctrl + +import ( + "91porn-server/models/v/export_task_mod" + "91porn-server/models/v/vidtimeonlinemod" + "encoding/json" + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/s/statrecordmod" + "91porn-server/models/v/operationlogmod" + "91porn-server/models/v/pushmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/vidmod" + "91porn-server/models/v/vidpopmod" + "91porn-server/web/service/vidser" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetVidList doc +// @Summary 获取视频列表 +// @Description 根据各种条件获取视频的列表 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Param status formData integer true "是否通过审核,0 未审核 1通过 2审核失败 3视为免费 4所有 5非零" +// @Param key formData string true "查询的排序条件,createdAt,playCount" +// @Param value formData integer true "正序反序,-1,1" +// @Param isFree formData integer true "是否付费视频,0, 付费,1,免费, 2, 所有" +// @Param isUserUp formData integer true "是否用户上传,0, 所有,1,是, 2,否" +// @Param chosen formData integer true "是否精选 1精选 2未精选" +// @Param title formData string true "过滤标题" +// @Param uid formData string true "上传用户uid过滤" +// @Param start formData integer true "上传时间,开始范围,时间戳" +// @Param end formData integer true "上传时间,结束范围,时间戳" +// @Param sectionID formData string false "专题ID" +// @Param isSortedUnderModule formData string false "视频在专题下是否设置了排序" +// @Param seedLinkUrl formData string false "种子下载链接" +// @Param seedSize formData int false "种子影片大小" +// @Param seedPlayTime formData int false "种子影片时长" +// @Success 200 {object} vidmod.ListResp "{"msg": "操作成功"}" +// @Failure 400 {string} string "{"msg": "操作失败"}" +// @Router /api/web/admin/vid/list [get] +func GetVidList(ctx *gin.Context) { + req := vidmod.ListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + m, sort, err := assembleConditions(req) + if err != stderr.Success { + common.ServeJSON(ctx, err, err) + return + } + code, data := vidser.GetVidList(m, sort, req.PageNumber, req.PageSize, req.IsPush) + common.ServeJSON(ctx, code, data) +} + +// GetVidList doc +// @Summary 导出视频列表 +// @Description 根据各种条件导出视频的列表 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param status formData integer true "是否通过审核,0 未审核 1通过 2审核失败 3视为免费 4所有 5非零" +// @Param key formData string true "查询的排序条件,createdAt,playCount" +// @Param value formData integer true "正序反序,-1,1" +// @Param isFree formData integer true "是否付费视频,0, 付费,1,免费, 2, 所有" +// @Param isUserUp formData integer true "是否用户上传,0, 所有,1,是, 2,否" +// @Param chosen formData integer true "是否精选 1精选 2未精选" +// @Param title formData string true "过滤标题" +// @Param uid formData string true "上传用户uid过滤" +// @Param start formData integer true "上传时间,开始范围,时间戳" +// @Param end formData integer true "上传时间,结束范围,时间戳" +// @Param sectionID formData string false "专题ID" +// @Param isSortedUnderModule formData string false "视频在专题下是否设置了排序" +// @Success 200 {string} string "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/vid/export [post] +func ExportVidList(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := vidmod.ListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + err = export_task_mod.CreateTask(export_task_mod.ExportVidTask, manager, req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrExportFileFail, err) + return + } + common.ServeJSON(ctx, stderr.Success, "success") + //m, sort, err := assembleConditions(req) + //if err != stderr.Success { + // common.ServeJSON(ctx, err, err) + // return + //} + //buff, err := vidser.ExportVidList(m, sort) + //if err != stderr.Success { + // common.ServeJSON(ctx, err, err) + // return + //} + //common.ServeFile(ctx, "video-list", common.FileExcel, &buff) +} + +func assembleConditions(req vidmod.ListReq) (m map[string]interface{}, sort vidser.SortModeList, err stderr.Code) { + sort = vidser.SortModeList{{Field: "createdAt", Value: -1}} + m = make(map[string]interface{}) + if req.ShowType != nil { + m["showType"] = *req.ShowType + } + if req.Status != 4 { + if req.Status == 7 { + m["status"] = map[string]int{"$gt": 0} + } else { + m["status"] = req.Status + } + } + if req.IsFree == 0 { + m["coins"] = map[string]int{"$gt": 0} + } + if req.IsFree == 1 { + m["coins"] = map[string]int{"$eq": 0} + } + if req.IsUserUp == 2 { + m["publisherID"] = map[string]int{"$lt": 115000} + } + if req.IsUserUp == 1 { + m["publisherID"] = map[string]int{"$gt": 115000} + } + if len(req.Title) != 0 { + m["title"] = map[string]string{"$regex": req.Title, "$options": "i"} + } + if req.UID > 0 { + m["publisherID"] = req.UID + } + if req.Chosen == 1 { + m["chosen"] = true + } + if req.Chosen == 2 { + m["chosen"] = false + } + if req.FreeArea == 1 { + m["freeArea"] = true + } + if req.FreeArea == 2 { + m["freeArea"] = false + } + if !req.End.IsZero() { + m["createdAt"] = map[string]time.Time{"$gte": req.Start, "$lt": req.End} + } + if req.ReviewAccount != "" { + m["reviewAccount"] = req.ReviewAccount + } + if len(req.Tag) != 0 { + id, err := tagmod.GetTagIDByName(req.Tag) + if err != nil { + return m, sort, stderr.ErrDbQueryError + } + m["tags"] = id + //sort = vidser.SortModeList{{Field: "tagSort." + id.Hex(), Value: -1}, {Field: "createdAt", Value: -1}} + } + if len(req.ID) != 0 { + oid, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + return m, sort, stderr.ErrParamError + } + m["_id"] = oid + } + //通过初始价格判断是否是马甲账号 + if req.IsPretendAcc == 1 { + m["coins"] = vidmod.PretendAccInitCoins + } + if req.IsPretendAcc == 2 { + //i := make(map[string]int64) + //i["$ne"] = vidmod.PretendAccInitCoins + m["coins"] = map[string]int64{"$ne": vidmod.PretendAccInitCoins} + } + m["deleteAt"] = bson.M{"$exists": false} + if req.NewsType != "" { + m["newsType"] = req.NewsType + } + + if req.LiaoBaTop != nil { + if *req.LiaoBaTop { + sort = vidser.LiaoBaTopSortModeList() + m["liaoBaTopSort"] = bson.M{"$gt": 0} + } else { + m["liaoBaTopSort"] = 0 + } + } + if req.SectionID != "" { + m["sectionID"] = req.SectionID + } + m["isSortedUnderModule"] = req.IsSortedUnderModule + if req.IsSortedUnderModule { + sort = nil + } + if req.IsRecommended != nil { + if *req.IsRecommended { + m["recoWeight"] = bson.M{"$gte": 0} + } else { + m["recoWeight"] = bson.M{"$lt": 0} + } + } + if req.Key == "likeRate" || req.Key == "purchaseRate" { + m[req.Key] = req.Value + } + if req.IsHappinessPlazaTop != nil && *req.IsHappinessPlazaTop { + if *req.IsHappinessPlazaTop { + m["happinessPlazaTop"] = bson.M{"$gt": 0} + } else { + m["happinessPlazaTop"] = bson.M{"$lte": 0} + } + } + return m, sort, stderr.Success +} + +// GetVidListByIds doc +// @Summary 根据Ids获取视频列表 +// @Description 根据各种条件获取视频的列表 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData array true "id数组" +// @Param token formData string true "token" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/vid/listDetail [POST] +func ListDetail(ctx *gin.Context) { + var args struct { + IDs []string `json:"ids" form:"ids" binding:"required"` + Token string `json:"token" form:"token" binding:"required"` + } + if err := ctx.ShouldBind(&args); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + if args.Token != "q7ydjHVIVBYiTqQ&" { + common.ServeJSON(ctx, stderr.ErrNoToken, nil) + return + } + m := make(map[string]interface{}) + i := make(map[string][]primitive.ObjectID) + i["$in"] = common.String2ObjectID(args.IDs) + m["_id"] = i + code, data := vidser.GetVidList(m, vidser.SortModeList{{Field: "createdAt", Value: 1}}, 1, 100, false) + common.ServeJSON(ctx, code, data) +} + +// UpdateVidInfo doc +// @Summary 编辑视频信息 +// @Description 编辑视频信息 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param id formData integer true "id" +// @Param uid formData integer true "用户ID" +// @Param title formData string true "视频标题" +// @Param tags formData array true "视频标签数组" +// @Param playTime formData string false "视频时长" +// @Param cover formData string false "封面图" +// @Param coverThumb formData string false "小图" +// @Param via formData string false "视频来源" +// @Param coins formData integer false "观看金币" +// @Param size formData integer false "文件大小" +// @Param mimeType formData string false "影片类型" +// @Param sourceURL formData string true "资源url" +// @Param sourceID formData string true "上传视频成功后 返回的ID" +// @Param filename formData string true "文件名" +// @Param resolution formData string false "分辨率" +// @Param md5 formData string true "文件摘要" +// @Param freeTime formData integer false "免费观影时长" +// @Param seedLinkUrl formData string false "种子下载链接" +// @Param seedSize formData integer false "种子影片大小" +// @Param seedPlayTime formData integer false "种子影片时长" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/vid/update [post] +func UpdateVidInfo(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := vidmod.EditReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := vidser.UpdateVidInfo(req, manager) + if code == stderr.Success { + _, _ = webg.Redis.Del(fmt.Sprintf(redisconst.VideoInfoKey(), req.ID)) + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, data) +} + +// DeleteVids doc +// @Summary 删除视频资源 +// @Description 删除视频资源 +// @Tags follow +// @Accept mpfd,json +// @Produce json,html +// @Param vids formData []string true "视频id数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/vid/del [delete] +func DeleteVids(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := vidmod.DeleteReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + ids, err := common.IDArray(req.IDs) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, "") + return + } + for _, id := range ids { + if err = operationlogmod.Insert(operationlogmod.OperationLog{ + ID: primitive.NewObjectID(), + OperationType: constant.Media_Del, + AfterContent: fmt.Sprintf("%v", id), + CreatedUser: manager, + }); err != nil { + common.ServeJSON(ctx, stderr.Failure, "") + return + } + } + code, data := vidser.DeleteManyByTTL(ids...) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageList, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, data) +} + +// UpdateVids doc +// @Summary 批量更新视频状态 +// @Description 批量更新视频状态 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData []string true "批量更新的视频id" +// @Param reco formData bool true "是否推荐, true:推荐,false:不推荐" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/vid/batch/reco [post] +func BatchUpdateReco(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := vidmod.BatchUpdateRecoRequest{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + if err = vidser.BatchUpdateReco(req); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertManyError, err) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// UpdateVids doc +// @Summary 批量更新视频状态 +// @Description 批量更新视频状态 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData []string true "批量更新的视频id" +// @Param field formData string true "更新的字段名" +// @Param status formData bool true "状态变更" +// @Param publisherID formData integer true "发布者id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/vid/batch/update [post] +func UpdateVids(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := vidmod.EditManyReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := vidser.UpdateVids(req) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, data) +} + +// PassAndEditVids doc +// @Summary 批量更新视频状态 +// @Description 批量更新视频状态 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData []string true "批量更新的视频id" +// @Param field formData string true "更新的字段名" +// @Param status formData bool true "状态变更" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/vid/batch/edit [post] +func PassAndEditVids(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := vidmod.PassManyReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := vidser.PassAndEditVids(req, manager) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, data) +} + +// PassVid doc +// @Summary 视频通过审核 +// @Description 视频通过审核 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData []string true "通过审核的视频id" +// @Param pass formData integer true "是否通过审核" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/vid/batch/pass [post] +func PassVid(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := vidmod.BatchReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := vidser.PassVid(req, manager) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, data) +} + +// ViewVid doc +// @Summary 查看视频状态 +// @Description 查看视频状态 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param id formData string true "查看视频id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/vid/view/:id [get] +func ViewVid(ctx *gin.Context) { + id := ctx.Param("id") + if id == "" { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := vidser.ViewVideoFromFs(id) + common.ServeJSON(ctx, code, data) +} + +// PushVid doc +// @Summary 视频推送 +// @Description 视频推送 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData []string true "通过审核的视频id" +// @Param pass formData integer true "是否通过审核" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/vid/push [post] +func PushVid(ctx *gin.Context) { + req := pushmod.AddReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + //判断视频是否审核通过 + status, err := vidmod.GetVidStatus(req.VideoID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + if status != vidmod.CheckPass { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := vidser.PushVid(req) + common.ServeJSON(ctx, code, data) +} + +// PushVidList doc +// @Summary 视频推送列表 +// @Description 视频推送列表 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData []string true "通过审核的视频id" +// @Param pass formData integer true "是否通过审核" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/vid/push/list [get] +func PushVidList(ctx *gin.Context) { + req := pushmod.ListReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := vidser.PushVidList(req) + common.ServeJSON(ctx, code, data) +} + +// DeletePushVid doc +// @Summary 视频推送列表 +// @Description 视频推送列表 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData []string true "通过审核的视频id" +// @Param pass formData integer true "是否通过审核" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/vid/push/delete [delete] +func DeletePushVid(ctx *gin.Context) { + req := pushmod.DeleteReq{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := vidser.DeletePushVid(req.IDs) + common.ServeJSON(ctx, code, data) +} + +// ConfigPopularity doc +// @Summary 配置视频热度计算方式 +// @Description 配置视频热度计算方式 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param request body vidpopmod.VideoPopularityConfig true "request" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/vid/popularity/config [POST] +func ConfigPopularity(ctx *gin.Context) { + req := vidpopmod.VideoPopularityConfig{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + for i := range req.PlayTimePercentage { + req.PlayTimePercentage[i].Max *= 60 + req.PlayTimePercentage[i].Min *= 60 + } + if err := vidpopmod.InsertOne(req); err != nil { + common.ServeJSON(ctx, stderr.ErrDbInsertError, err) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// GetPopularityConfig doc +// @Summary 获取视频热度计算方式 +// @Description 获取视频热度计算方式 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {object} vidpopmod.VideoPopularityConfig "Success" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/vid/popularity/config [get] +func GetPopularityConfig(ctx *gin.Context) { + resp, err := vidpopmod.FindOne() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +func Reindex(ctx *gin.Context) { + if err := statrecordmod.DeleteStatRecordByName(statrecordmod.ElasticSyncVideoJob); err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err) + return + } + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Search doc +// @Summary 按照app搜索逻辑搜索视频 +// @Description 按照app搜索逻辑搜索视频 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param request body vidmod.WebElasticSearchRequest true "request" +// @Success 200 {object} vidmod.WebElasticSearchResponse "Success" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/vid/search [post] +func Search(ctx *gin.Context) { + req := vidmod.WebElasticSearchRequest{} + if err := ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + res, err := vidmod.SearchWithTotal(req.Keyword, int64((req.PageNumber-1)*(req.PageSize)), int64(req.PageSize+1)) + if err != nil { + common.ServeJSON(ctx, stderr.ErrInterServerError, err) + return + } + hasNext := false + if uint64(len(res.Hits)) > req.PageSize { + res.Hits = res.Hits[:req.PageSize] + hasNext = true + } + list := make([]vidmod.ESVideo, len(res.Hits)) + for i := range res.Hits { + list[i] = res.Hits[i].Source + } + resp := vidmod.WebElasticSearchResponse{List: list, HasNext: hasNext, Total: res.Total.Value} + common.ServeJSON(ctx, stderr.Success, resp) +} + +// BatchAudit doc +// @Summary 批量自动上架 +// @Description 批量自动上架 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param vids formData []string true "帖子id" +// @Param sectionID formData string false "专题id" +// @Param onlineTime formData string false "上架时间" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/vid/timeOnline/batchAudit [post] +func BatchAudit(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := vidtimeonlinemod.BatchAuditReq{} + err = ctx.ShouldBind(&req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + code, data := vidser.BatchAuditVidTimeOnlineInfo(req, -1, manager) + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, code, data) +} + +// GetTimeOnlineVidList doc +// @Summary 获取定时上线视频列表 +// @Description 根据各种条件获取定时上线视频列表 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param uid formData string true "上传用户uid过滤" +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {object} vidmod.ListResp "{"msg": "操作成功"}" +// @Failure 400 {string} string "{"msg": "操作失败"}" +// @Router /api/web/admin/vid/timeOnline/list [get] +func GetTimeOnlineVidList(ctx *gin.Context) { + var req = vidtimeonlinemod.ListReq{} + err := ctx.ShouldBind(&req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + code, data := vidser.GetTimeOnlineVidList(&req) + common.ServeJSON(ctx, code, data) +} + +// DeleteVids doc +// @Summary 删除准备自动上架视频 +// @Description 删除准备自动上架视频 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData []string true "记录的id数组" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/vid/timeOnline/del [post] +func DelVidTimeOnlines(ctx *gin.Context) { + req := vidtimeonlinemod.DeleteReq{} + err := ctx.ShouldBind(&req) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + ids, err := common.IDArray(req.IDs) + if err != nil { + common.ServeJSON(ctx, stderr.Failure, "") + return + } + code := vidser.DeleteTimeOnlineVid(ids) + common.ServeJSON(ctx, code, nil) +} + +// EditVidTimeOnlineInfo doc +// @Summary 修改自动上架视频信息 +// @Description 修改自动上架视频信息 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param vidId formData string true "帖子id" +// @Param price formData int true "价格" +// @Param sectionID formData string false "专题id" +// @Param onlineTime formData string false "上架时间" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/vid/timeOnline/edit [post] +func EditVidTimeOnlineInfo(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := vidtimeonlinemod.EditReq{} + err1 := ctx.ShouldBind(&req) + if err1 != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code, data := vidser.BatchAuditVidTimeOnlineInfo(vidtimeonlinemod.BatchAuditReq{ + Vids: []primitive.ObjectID{req.VidId}, + SectionID: req.SectionID, + OnlineTime: req.OnlineTime, + }, req.Price, manager) + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + common.ServeJSON(ctx, code, data) +} + +// BatchUpdateVidNewsType doc +// @Summary 批量修改帖子类型 +// @Description 批量修改帖子类型 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param q body vidmod.BatchUpdateVidNewsTypeReq false "请求参数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/vid/batch/newsType/update [post] +func BatchUpdateVidNewsType(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := vidmod.BatchUpdateVidNewsTypeReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + + code := vidser.UpdateVidNewsType(req) + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, code.Msg()) +} + +// UpdateVidTags doc +// @Summary 批量修改视频标签 +// @Description 批量修改视频标签 +// @Tags vid +// @Accept mpfd,json +// @Produce json,html +// @Param ids formData []string true "批量修改的帖子id" +// @Param tags formData []string true "标签名称" +// @Param tagSort formData []string true "标签排序" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/vid/batch/update/tags [post] +func UpdateVidTags(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := vidmod.EditVideTagsReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, nil) + return + } + code := vidser.UpdateVidTags(req) + if code != stderr.Success { + common.ServeJSON(ctx, code, nil) + return + } + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.VideoManageList, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, code, code.Msg()) +} diff --git a/web/api/video_activity_ctrl/video_activity.go b/web/api/video_activity_ctrl/video_activity.go new file mode 100644 index 0000000..cdf9355 --- /dev/null +++ b/web/api/video_activity_ctrl/video_activity.go @@ -0,0 +1,67 @@ +package video_activity_ctrl + +import ( + "fmt" + + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/web/service/video_activity_service" + + "github.com/gin-gonic/gin" +) + +// Submit 提交 +func Submit(c *gin.Context) { + var in *video_activity_service.SubmitCond + if err := c.ShouldBind(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web video_activity Submit arg error "+err.Error()) + return + } + if code := video_activity_service.Submit(in); code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, stderr.Success, "success") +} + +// Edit 修改 +func Edit(c *gin.Context) { + var in *video_activity_service.EditCond + if err := c.ShouldBind(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web video_activity Edit arg error "+err.Error()) + return + } + if code := video_activity_service.Edit(in); code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, stderr.Success, "success") +} + +// QueryAll 查询 +func QueryAll(c *gin.Context) { + var in *video_activity_service.QueryAllCond + log.Info("进入这里") + if err := c.ShouldBindQuery(&in); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "web video_activity QueryAll arg error "+err.Error()) + return + } + data, code := video_activity_service.QueryAll(in) + if code != stderr.Success { + log.Info(fmt.Sprintf("%v\n%d", data, code)) + common.ServeJSON(c, code, nil) + return + } + log.Info(fmt.Sprintf("%v\n%d", data, code)) + common.ServeJSON(c, code, data) +} + +func Box(c *gin.Context) { + data, code := video_activity_service.Box() + if code != stderr.Success { + common.ServeJSON(c, code, nil) + return + } + common.ServeJSON(c, code, data) +} diff --git a/web/api/video_gold_coin_ctrl/video_gold_coin.go b/web/api/video_gold_coin_ctrl/video_gold_coin.go new file mode 100644 index 0000000..c474db1 --- /dev/null +++ b/web/api/video_gold_coin_ctrl/video_gold_coin.go @@ -0,0 +1,146 @@ +package video_gold_coin_ctrl + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/videogoldcoinmod" + "91porn-server/web/service/video_gold_coin_ser" + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" +) + +// QueryAll doc +// @Summary 查询金币视频帖子 +// @Description 查询金币视频帖子 +// @Tags 金币视频配置 +// @Accept mpfd,json +// @Produce json,html +// @Param ID formData primitive.ObjectID true "文档id" +// @Param Status formData integer true "上架状态" +// @Param pageNumber formData integer true "查询页码" +// @Param pageSize formData integer true "页码大小" +// @Success 200 {object} videogoldcoinmod.QueryAllRes +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/video_gold_coin [get] +func QueryAll(c *gin.Context) { + var in videogoldcoinmod.QueryAllCond + if err := c.ShouldBindQuery(&in); err != nil { + log.Error(fmt.Sprintf("Query all video_gold_coin_ser param err:%v\n", err)) + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + + data, err := video_gold_coin_ser.QueryAll(&in) + if err != nil { + log.Error(fmt.Sprintf("Query all video_gold_coin_ser err:%v\n%d", data, err)) + common.ServeJSON(c, stderr.Failure, nil) + return + } + common.ServeJSON(c, stderr.Success, data) +} + +// Update doc +// @Summary 修改金币视频帖子 +// @Description 修改金币视频帖子 +// @Tags 金币视频配置 +// @Accept mpfd,json +// @Produce json,html +// @Param ID formData primitive.ObjectID true "文档id" +// @Param Status formData int false "状态 1 上架 2 下架" +// @Param sortCode formData int false "排序号" +// @Param Reason formData string false "拒绝理由" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/video_gold_coin [put] +func Update(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var in videogoldcoinmod.EditCond + if err = c.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("Update video_gold_coin_ser param err:%v\n", err)) + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + + if err = video_gold_coin_ser.Edit(&in, manager); err != nil { + log.Error(fmt.Sprintf("Update video_gold_coin_ser err:%v\n", err)) + common.ServeJSON(c, stderr.Failure, nil) + return + } + updateLog, _ := json.Marshal(in) + _ = operatorlgmod.RecordOperation(manager, constant.VideoGoldCoinList, constant.Modify, string(updateLog), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, stderr.Success.Msg()) +} + +// Add doc +// @Summary 新增金币视频帖子 +// @Description 新增金币视频帖子 +// @Tags 金币视频配置 +// @Accept mpfd,json +// @Produce json,html +// @Param videoId formData string true "帖子ID" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/video_gold_coin [post] +func Add(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var in videogoldcoinmod.AddCond + if err = c.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("Add video_gold_coin_ser param err:%v\n", err)) + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + + if code := video_gold_coin_ser.Add(&in, manager); code != stderr.Success { + log.Error(fmt.Sprintf("Add video_gold_coin_ser err:%v\n", code)) + common.ServeJSON(c, code, nil) + return + } + updateLog, _ := json.Marshal(in) + _ = operatorlgmod.RecordOperation(manager, constant.VideoGoldCoinList, constant.Add, string(updateLog), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, stderr.Success.Msg()) +} + +// Delete doc +// @Summary 删除金币视频 +// @Description 删除金币视频 +// @Tags 金币视频配置 +// @Accept mpfd,json +// @Produce json,html +// @Param id body string true "id" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/video_gold_coin [delete] +func Delete(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var in videogoldcoinmod.DeleteCond + if err = c.ShouldBind(&in); err != nil { + log.Error(fmt.Sprintf("Delete video_gold_coin_ser param err:%v\n", err)) + common.ServeJSON(c, stderr.ErrParamError, nil) + return + } + + if err := video_gold_coin_ser.Delete(&in); err != nil { + common.ServeJSON(c, stderr.ErrDbDeleteError, err.Error()) + return + } + + log, _ := json.Marshal(in) + operatorlgmod.RecordOperation(manager, constant.VideoGoldCoinList, constant.Delete, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} diff --git a/web/api/vipcardexperimentctrl/experiment.go b/web/api/vipcardexperimentctrl/experiment.go new file mode 100644 index 0000000..730dd06 --- /dev/null +++ b/web/api/vipcardexperimentctrl/experiment.go @@ -0,0 +1,77 @@ +package vipcardexperimentctrl + +import ( + "encoding/json" + "strings" + "time" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/vipcardexperimentmod" + "91porn-server/web/service/vipcardexperimentser" + + "github.com/gin-gonic/gin" +) + +func Current(ctx *gin.Context) { + experiment, err := vipcardexperimentser.Current(time.Now()) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + if experiment == nil { + common.ServeJSON(ctx, stderr.Success, gin.H{}) + return + } + common.ServeJSON(ctx, stderr.Success, experiment) +} + +func Publish(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + experiment := vipcardexperimentmod.Experiment{} + if err = ctx.ShouldBindJSON(&experiment); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = vipcardexperimentser.Publish(&experiment, manager); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + body, _ := json.Marshal(experiment) + _ = operatorlgmod.RecordOperation(manager, "VIP卡片A/B实验", "发布", string(body), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, experiment) +} + +func Disable(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + request := vipcardexperimentser.DisableRequest{} + if err = ctx.ShouldBindJSON(&request); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = vipcardexperimentser.Disable(request.ExperimentID, manager); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + _ = operatorlgmod.RecordOperation(manager, "VIP卡片A/B实验", "停用", request.ExperimentID, ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, "") +} + +func Statistics(ctx *gin.Context) { + experimentID := strings.TrimSpace(ctx.Query("experimentId")) + response, err := vipcardexperimentser.Statistics(experimentID) + if err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, response) +} diff --git a/web/api/web_im/im_sync.go b/web/api/web_im/im_sync.go new file mode 100644 index 0000000..d019de0 --- /dev/null +++ b/web/api/web_im/im_sync.go @@ -0,0 +1,136 @@ +package web_im + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/skd/job" + "91porn-server/services/srv_im" + + "github.com/gin-gonic/gin" +) + +type syncIMUsersResp struct { + Accepted bool `json:"accepted"` + Running bool `json:"running"` + Message string `json:"message"` + Task job.SyncIMUsersTaskStatus `json:"task"` +} + +func SyncIMUsers(ctx *gin.Context) { + accepted, status := job.RequestSyncIMUsers() + resp := syncIMUsersResp{ + Accepted: accepted, + Running: status.Running, + Task: status, + } + if accepted { + resp.Message = "sync task queued" + } else if status.Running { + resp.Message = "sync task already in progress" + } else { + resp.Message = "sync task already queued" + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +func SyncIMUsersStatus(ctx *gin.Context) { + common.ServeJSON(ctx, stderr.Success, job.GetSyncIMUsersStatus()) +} + +func ListIMUsers(ctx *gin.Context) { + var req srv_im.UserListReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, code, msg := srv_im.ListUsers(req) + if code != stderr.Success { + common.ServeJSON(ctx, code, msg) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +func SyncIMUsersByIDs(ctx *gin.Context) { + var req srv_im.SyncUsersReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, code, msg := srv_im.SyncUsers(req) + if code != stderr.Success { + common.ServeJSON(ctx, code, msg) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +func AddIMFriendDirect(ctx *gin.Context) { + var req srv_im.FriendAddDirectReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + code, msg := srv_im.AddFriendDirect(req) + if code != stderr.Success { + common.ServeJSON(ctx, code, msg) + return + } + common.ServeJSON(ctx, stderr.Success, gin.H{"ok": true}) +} + +func BatchIMOnlineStatus(ctx *gin.Context) { + var req srv_im.OnlineStatusReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, code, msg := srv_im.BatchOnlineStatus(req) + if code != stderr.Success { + common.ServeJSON(ctx, code, msg) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +func SendIMPassthrough(ctx *gin.Context) { + var req srv_im.PassthroughReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, code, msg := srv_im.SendPassthrough(req) + if code != stderr.Success { + common.ServeJSON(ctx, code, msg) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +func SendIMAdNotify(ctx *gin.Context) { + var req srv_im.AdNotifyReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, code, msg := srv_im.SendAdNotify(req) + if code != stderr.Success { + common.ServeJSON(ctx, code, msg) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} + +func SendIMMessage(ctx *gin.Context) { + var req srv_im.SendMessageReq + if err := ctx.ShouldBindJSON(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + resp, code, msg := srv_im.SendMessage(req) + if code != stderr.Success { + common.ServeJSON(ctx, code, msg) + return + } + common.ServeJSON(ctx, stderr.Success, resp) +} diff --git a/web/api/withdrawctrl/goldfish.go b/web/api/withdrawctrl/goldfish.go new file mode 100644 index 0000000..ed50650 --- /dev/null +++ b/web/api/withdrawctrl/goldfish.go @@ -0,0 +1,57 @@ +package withdrawctrl + +import ( + "bytes" + "errors" + "fmt" + "net/http" + "strconv" + + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/web/service/withdrawser" + + "github.com/gin-gonic/gin" +) + +// GoldFishCallBack 金鱼结构回调函数 +func GoldFishCallBack(ctx *gin.Context) { + if err := func() error { + exchg := rchgutil.GoldFishExchangeRes{} + e := rchgutil.GoldFishExchange{} + if err := ctx.ShouldBindJSON(&exchg); err != nil { + log.Error(fmt.Sprintf("GoldFish callback parameter bind fail error:%+v:", err)) + return err + } + log.Info(fmt.Sprintf("GoldFish callback parameter data:%+v:", exchg)) + buf := bytes.Buffer{} + buf.WriteString(strconv.Itoa(exchg.Code)) + buf.WriteString(exchg.Mark) + buf.WriteString(exchg.MercID) + buf.WriteString(exchg.Money) + buf.WriteString(exchg.OID) + buf.WriteString(exchg.Status) + buf.WriteString(exchg.TradeNo) + buf.WriteString(e.GetAppSecret()) + if !rchgutil.VerifySign(exchg.Sign, buf.String()) { + log.Error("GoldFish callback sign verify fail") + return errors.New("check sign fail") + } + moneyf, err := strconv.ParseFloat(exchg.Money, 64) + if err != nil { + log.Error(fmt.Sprintf("GoldFish callback ParseFloat fail error:%+v:", err)) + return err + } + moneyf = moneyf * 100 + money := int64(moneyf) + if err = withdrawser.ExchgCallBack(exchg.TradeNo, exchg.OID, money, exchg.Status, exchg.Mark); err != nil { + log.Error(fmt.Sprintf("GoldFish RechargeCallBack fail error:%+v:", err)) + return err + } + return nil + }(); err != nil { + ctx.String(http.StatusBadRequest, "fail") + return + } + ctx.String(http.StatusOK, "success") +} diff --git a/web/api/withdrawctrl/txnact.go b/web/api/withdrawctrl/txnact.go new file mode 100644 index 0000000..65aa7fb --- /dev/null +++ b/web/api/withdrawctrl/txnact.go @@ -0,0 +1,58 @@ +package withdrawctrl + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/v/txnactmod" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func FindAct(ctx *gin.Context) { + q := txnactmod.Query{} + if err := ctx.ShouldBind(&q); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + infos, err := txnactmod.FindMany(q) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, infos) +} + +func EditAct(ctx *gin.Context) { + var p struct { + ID primitive.ObjectID `json:"id"` + txnactmod.TransactionActSelector + } + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + + return + } + c, err := txnactmod.Update(p.ID, &p.TransactionActSelector) + if err != nil || c < 0 { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, "") +} + +func DelAct(ctx *gin.Context) { + var p struct { + ID primitive.ObjectID `json:"id"` + } + if err := ctx.ShouldBind(&p); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + c, err := txnactmod.DeleteByID(p.ID) + if err != nil || c < 0 { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, "") +} diff --git a/web/api/withdrawctrl/withdraw.go b/web/api/withdrawctrl/withdraw.go new file mode 100644 index 0000000..087dd8a --- /dev/null +++ b/web/api/withdrawctrl/withdraw.go @@ -0,0 +1,244 @@ +package withdrawctrl + +import ( + "encoding/json" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/wdordmod" + "91porn-server/web/service/withdrawser" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" +) + +// Refund 手动对特定状态的提现订单执行退款 +func Refund(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + ID string `json:"id" binding:"required"` // 订单id + Status int `json:"status" binding:"required"` // 当前订单状态 + Desc string `json:"desc" binding:"required"` // 原因说明 + } + if err = ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = withdrawser.ExchangeRefuseByHand(arg.ID, arg.Status, arg.Desc); err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err.Error()) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.TradeManageWithdrawOrder, constant.Modify, string(log), + ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Refuse doc +// @Summary 拒绝提现 +// @Description 拒绝提现 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /withdraw/Refuse [post] +func Refuse(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + ID string `json:"id"` //订单id + StatusDesc string `json:"statusDesc"` + } + if err = ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if err = withdrawser.ExchangeRefuse(arg.ID, arg.StatusDesc, manager); err != nil { + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err.Error()) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.TradeManageWithdrawOrder, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// Check doc +// @Summary 通过提现 +// @Description 通过提现 +// @Tags 钱包 +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /withdraw/check [post] +func Check(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + ID string `json:"id"` //订单id + Type int `json:"type"` //0 拒绝 1通过 + StatusDesc string `json:"statusDesc"` + UsdtRate float64 `json:"usdtRate"` //仅订单类型为usdt时必传 + } + if err = ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + if arg.Type == 0 { //拒绝 + err = withdrawser.ExchangeRefuse(arg.ID, arg.StatusDesc, manager) + } else if arg.Type == 1 { //通过 + err = withdrawser.ExchangeAllow(arg.ID, arg.UsdtRate, manager) + } else { + common.ServeJSON(ctx, stderr.ErrParamError, arg.Type) + return + } + if err != nil { + if err.Error() == "unknown" { + common.ServeJSON(ctx, stderr.UnkonwErr, err.Error()) + return + } + common.ServeJSON(ctx, stderr.ErrServerUnavailable, err.Error()) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.TradeManageWithdrawOrder, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// ExchgOrder doc +// @Summary 充值订单 +// @Description 充值订单 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /business/order/list [get] +func ExchgOrder(ctx *gin.Context) { + var arg struct { + common.StandQuery + wdordmod.WithdrawWebQueryReq + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + cond, opt := common.StandQueryMap(arg.StandQuery, arg.WithdrawWebQueryReq) + //兼容以前没有productType的订单 + if arg.ProductType != nil && *arg.ProductType == 0 { + cond["productType"] = bson.M{"$in": bson.A{0, nil}} + } + total, data, err := withdrawser.GetAllOrders(cond, opt) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, map[string]interface{}{ + "total": total, + "orders": data, + }) +} + +// ExchgLog doc +// @Summary 充值订单 +// @Description 充值订单 +// @Tags product +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /business/order/list [get] +func ExchgLog(ctx *gin.Context) { + var arg struct { + common.StandQuery + wdordmod.WithdrawQueryReq + } + if err := ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err) + return + } + total, data, err := wdordmod.FindOrders(common.StandQueryMap(arg.StandQuery, arg.WithdrawQueryReq)) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, "") + return + } + common.ServeJSON(ctx, stderr.Success, map[string]interface{}{ + "total": total, + "orders": data, + }) +} + +// RefundList doc +// @Summary 提现退款列表 +// @Description 提现退款列表 +// @Tags withdraw +// @Accept mpfd,json +// @Produce json,html +// @Param id query string false "ID" +// @Param districtCode query string false "商区码" +// @Param uid query integer false "uid" +// @Param pageNumber query integer true "当前页" +// @Param pageSize query integer true "每页条数" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/withdraw/refund/list [get] +func RefundList(c *gin.Context) { + var arg struct { + withdrawser.HumanRefundQuery + commod.Page + } + if err := c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "districtctrl RefundList arg error "+err.Error()) + return + } + page := withdrawser.HumanRefundPage(int64(arg.Skip()), int64(arg.Limit()), arg.HumanRefundQuery) + common.ServeJSON(c, stderr.Success, page) +} + +// RefundDealWith doc +// @Summary 提现退款处理 +// @Description 提现退款处理 +// @Tags withdraw +// @Accept mpfd,json +// @Produce json,html +// @Param id query string true "ID" +// @Param remark query string false "备注" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/admin/withdraw/refund/dealwith [post] +func RefundDealWith(c *gin.Context) { + manager, err := common.GetAdminAct(c) + if err != nil { + common.ServeJSON(c, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + ID string `form:"id" json:"id" binding:"required"` + Remark string `form:"remark" json:"remark" binding:"required"` + } + if err = c.ShouldBind(&arg); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "districtctrl RefundDealWith arg error "+err.Error()) + return + } + if err = withdrawser.DealWithUnkownOrder(arg.ID, arg.Remark); err != nil { + common.ServeJSON(c, stderr.Failure, "districtctrl RefundDealWith error: "+err.Error()) + return + } + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.TradeManageWithdrawRefund, constant.Modify, string(log), c.Request.URL.RequestURI()) + common.ServeJSON(c, stderr.Success, "") +} diff --git a/web/api/withdrawctrl/withdrawCfg.go b/web/api/withdrawctrl/withdrawCfg.go new file mode 100644 index 0000000..fda6844 --- /dev/null +++ b/web/api/withdrawctrl/withdrawCfg.go @@ -0,0 +1,198 @@ +package withdrawctrl + +import ( + "encoding/json" + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/l/operatorlgmod" + "91porn-server/models/v/wdchannmod" + "91porn-server/models/v/wdtaxmod" + "91porn-server/web/service/withdrawser" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetChannel doc +// @Summary 获取提现渠道 +// @Description 获取提现渠道 +// @Tags withdraw +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/withdraw/channel/list [get] +func GetChannel(ctx *gin.Context) { + infos, err := withdrawser.WithdrawType() + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.ServeJSON(ctx, stderr.Success, infos) +} + +// ChannelInsert doc +// @Summary 新增 +// @Description 新增 +// @Tags withdraw +// @Accept mpfd,json +// @Produce json,html +// @Param reachargeDetails formData wdchannmod.ChannelReq ture "product卡" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/withdraw/channel/add [post] +func ChannelInsert(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := wdchannmod.ChannelReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + pc := wdchannmod.WithdrawChannel{ + ChannelName: req.ChannelName, + CID: req.CID, + PayType: req.PayType, + MinMoney: req.MinMoney, + QpMinMoney: req.QpMinMoney, + MaxMoney: req.MaxMoney, + Weight: req.Weight, + UpdatedAt: time.Now(), + CreatedAt: time.Now(), + } + if err = wdchannmod.InsertWithdrawChannel(&pc); err != nil { + common.ServeJSON(ctx, stderr.ErrDbQueryError, err.Error()) + return + } + common.Go(func() { + _, _ = webg.Redis.Del(redisconst.WithdrawCfgCacheKey()) + }) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.TradeManageWithdrawType, constant.Add, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, nil) +} + +// ChannelUpdate doc +// @Summary 编辑提现渠道 +// @Description 编辑提现渠道 +// @Tags withdraw +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/withdraw/channel/update [post] +func ChannelUpdate(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + var arg struct { + ID string `json:"id"` + wdchannmod.WithdrawChannelSelector + } + if err = ctx.ShouldBind(&arg); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + cnt, err := wdchannmod.UpdateWithdrawChannel(arg.ID, &arg.WithdrawChannelSelector) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + common.Go(func() { + _, _ = webg.Redis.Del(redisconst.WithdrawCfgCacheKey()) + }) + log, _ := json.Marshal(arg) + _ = operatorlgmod.RecordOperation(manager, constant.TradeManageWithdrawType, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, wdchannmod.OpeResp{Count: cnt}) +} + +// ChannelDelete doc +// @Summary 删除 +// @Description 删除 +// @Tags withdraw +// @Accept mpfd,json +// @Produce json,html +// @Param id formData int true "product等级"" +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/withdraw/channel/delete [delete] +func ChannelDelete(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + req := wdchannmod.DelReq{} + if err = ctx.ShouldBind(&req); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + oids := make([]primitive.ObjectID, 0, len(req.IDs)) + for _, id := range req.IDs { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Error(fmt.Sprintf("exchange string(%s) to objectID error.", id)) + continue + } + oids = append(oids, oid) + } + cond := bson.M{"_id": bson.M{"$in": oids}} + cnt, err := wdchannmod.DeleteWithdrawChannel(cond) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbDeleteError, err.Error()) + return + } + common.Go(func() { + _, _ = webg.Redis.Del(redisconst.WithdrawCfgCacheKey()) + }) + log, _ := json.Marshal(req) + _ = operatorlgmod.RecordOperation(manager, constant.TradeManageWithdrawType, constant.Delete, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, wdchannmod.OpeResp{Count: cnt}) +} + +// ChannelBatch doc +// @Summary 编辑提现税率 +// @Description 编辑提现税率 +// @Tags withdraw +// @Accept mpfd,json +// @Produce json,html +// @Success 200 {string} json "{"msg": "操作成功"}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /api/web/withdraw/channel/batch [post] +func ChannelBatch(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + return + } + set := wdtaxmod.WithdrawTariffSelector{} + if err = ctx.ShouldBind(&set); err != nil { + common.ServeJSON(ctx, stderr.ErrParamError, err.Error()) + return + } + cnt, err := wdtaxmod.UpdateWithDrawTariff(set) + if err != nil { + common.ServeJSON(ctx, stderr.ErrDbUpdateError, err.Error()) + return + } + common.Go(func() { + _, _ = webg.Redis.Del(redisconst.WithdrawTariffCacheKey()) + }) + + log, _ := json.Marshal(set) + _ = operatorlgmod.RecordOperation(manager, constant.TradeManageWithdrawType, constant.Modify, string(log), ctx.Request.URL.RequestURI()) + common.ServeJSON(ctx, stderr.Success, wdchannmod.OpeResp{Count: cnt}) +} diff --git a/web/api/withdrawctrl/yinse.go b/web/api/withdrawctrl/yinse.go new file mode 100644 index 0000000..21963f1 --- /dev/null +++ b/web/api/withdrawctrl/yinse.go @@ -0,0 +1,34 @@ +package withdrawctrl + +import ( + "fmt" + "net/http" + + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/common/stderr" + "91porn-server/web/service/withdrawser" + + "github.com/gin-gonic/gin" +) + +// GoldFishCallBack 金鱼结构回调函数 +func YinseCallBack(ctx *gin.Context) { + ctx.JSON(http.StatusOK, func() (msg rchgutil.ExchgBackMsg) { + exchg := rchgutil.ExchgBack{} + msg.Code = int(stderr.Failure) + if err := ctx.ShouldBindJSON(&exchg); err != nil { + msg.Err = fmt.Sprintf("GoldFish callback parameter bind fail error:%+v:", err) + log.Error(msg.Err) + return + } + log.Info(fmt.Sprintf("GoldFish callback parameter data:%+v:", exchg)) + if err := withdrawser.ExchgCallBack(exchg.TradeNo, exchg.OID, exchg.Money, exchg.Status, exchg.Mark); err != nil { + msg.Err = fmt.Sprintf("GoldFish RechargeCallBack fail error:%+v:", err) + log.Error(msg.Err) + return + } + msg.Code = int(stderr.Success) + return + }()) +} diff --git a/web/main.go b/web/main.go new file mode 100644 index 0000000..78a637d --- /dev/null +++ b/web/main.go @@ -0,0 +1,173 @@ +package main + +import ( + "91porn-server/common/cachev2" + "91porn-server/common/laosiji" + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "91porn-server/common" + "91porn-server/common/conf" + "91porn-server/common/constant" + "91porn-server/common/db" + "91porn-server/common/httputil" + "91porn-server/common/laosiji_app" + "91porn-server/common/log" + "91porn-server/common/sms" + "91porn-server/common/tg" + "91porn-server/models" + "91porn-server/models/Init" + _ "91porn-server/models/commod" // swagger依赖 + _ "91porn-server/models/v/ldycfgmod" // swagger依赖 + _ "91porn-server/models/v/moduleconfmod" // swagger依赖 + _ "91porn-server/models/v/modulesectionmod" // swagger依赖 + _ "91porn-server/models/v/operationlogmod" // swagger依赖 + _ "91porn-server/models/v/rchgamegoldmod" // swagger依赖 + _ "91porn-server/models/v/rchgamtmod" // swagger依赖 + _ "91porn-server/models/v/rchgchanmod" // swagger依赖 + _ "91porn-server/models/v/tagconfmod" // swagger依赖 + _ "91porn-server/models/v/wdchannmod" // swagger依赖 + + // _ "91porn-server/web/docs" + "91porn-server/web/router" + "91porn-server/web/service/activityser" + officialwebsiteser "91porn-server/web/service/officialWebsiteser" + "91porn-server/web/service/prefetch" + "91porn-server/web/webg" +) + +var httpServer *http.Server + +func main() { + time.Local, _ = time.LoadLocation("Asia/Shanghai") + common.Go(startUp) + waitQuit() +} + +func init() { + cfg := webg.GlobalConfig{} + //加载配置 etcd配置 + static := webg.StaticSource{} + if err := conf.LoadJSON("config/static.json", &static); err != nil { + fmt.Printf("startUp load static error: %v\n", err) + } + if err := conf.LoadJSON("config/web.json", &cfg); err != nil { + fmt.Printf("startUp get config error: %v\n", err) + os.Exit(1) + } + fmt.Println("use local config successfully") + webg.Conf = &cfg + webg.Static = &static + sms.Init(sms.Config{ + FengNiao: sms.FengNiaoConfig{ + Url: cfg.SMS.FengNiao.Url, + ChinaMercId: cfg.SMS.FengNiao.ChinaMercId, + ChinaSecret: cfg.SMS.FengNiao.ChinaSecret, + ChinaTmplId: cfg.SMS.FengNiao.ChinaTmplId, + InterMercId: cfg.SMS.FengNiao.InterMercId, + InterSecret: cfg.SMS.FengNiao.InterSecret, + InterTmplId: cfg.SMS.FengNiao.InterTmplId, + }, + }) + laosiji_app.Init(webg.Conf.LaoSiJiAiMate) + log.Init(log.Options{Level: webg.Conf.Log.Level, DisableStack: webg.Conf.Log.DisableStacktrace}) + if cfg.Base.Env == constant.ProdEnv { + webg.Bot = tg.New(tg.TianTian_Bot, fmt.Sprintf("短视频[env:%s]", webg.Conf.Base.Env)) + common.SetPanicHandler(func(s string) { _ = webg.Bot.Send(tg.Msg{ChatId: tg.ChatIdServerTeam, Text: "[Web-Server]" + s}) }) + } +} + +func startUp() { + ds := db.InitDS(InitMongodbParam(models.RegisterPool)) + if len(ds) > 0 { + for k, v := range ds { + switch k { + case models.VideoDb: + webg.VideoDB = v + case models.LogDb: + webg.LogDB = v + case models.StatDb: + webg.StatDB = v + } + } + } + // 初始化Redis + webg.InitRedis() + // 初始化缓存操作中间件 + cachev2.Init(webg.Redis, log.ZapLog) + // 初始化ES + webg.InitElastic() + webg.ProxyCfg = &httputil.ProxyCfg{ + Host: webg.Conf.URL.HttpProxy, + IsActive: false, + } + Init.InitMongo() + laosiji.Init(*webg.Conf) + + //载入活动定时任务 + if err := activityser.LoadingJob(); err != nil { + os.Exit(1) + return + } + // 官网 seoSlug 回填定时任务:扫描各表缺失 seoSlug 的记录按标题补齐 + officialwebsiteser.StartSeoSlugBackfillCron() + port := webg.Conf.Base.Port + httpServer = &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: router.SetupGin(), + } + log.Info("Web Start Listen ...", log.Any("port", port)) + if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatal("server listen err", log.E(err)) + } + //Block Here + log.Info("httpServer quit...") +} + +func waitQuit() { + //graceful exit + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT) + <-ch + log.Info("Recv Signal, Shutdown Server Start...") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if httpServer != nil { + if err := httpServer.Shutdown(ctx); err != nil { + log.Error(fmt.Sprintf("Server Shutdown:%+v", err)) + } + } + db.CloseDS() + prefetch.StopSubmitPrefetch() + if err := webg.RedisClose(); err != nil { + log.Error("redis close error", log.E(err)) + return + } + log.Warn("Shutdown Server OK...") +} + +func InitMongodbParam(registerPool []models.Register) ([]db.DBmap, []db.Register) { + pri := db.DBmap{ + Key: models.VideoDb, + URL: webg.Conf.Mongo.VideoDbUrl, + } + sta := db.DBmap{ + Key: models.StatDb, + URL: webg.Conf.Mongo.StatDbUrl, + } + log := db.DBmap{ + Key: models.LogDb, + URL: webg.Conf.Mongo.LogDbUrl, + } + dbgister := make([]db.Register, len(registerPool)) + for i, v := range registerPool { + dbgister[i] = db.Register{Key: v.Key, Table: v.Table} + } + dbmaps := []db.DBmap{pri, sta, log} + return dbmaps, dbgister +} diff --git a/web/middleware/authweb/authweb.go b/web/middleware/authweb/authweb.go new file mode 100644 index 0000000..b820f17 --- /dev/null +++ b/web/middleware/authweb/authweb.go @@ -0,0 +1,350 @@ +package authweb + +import ( + "errors" + "net/http" + "strings" + "time" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/crypt" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/adminmod" + "91porn-server/models/v/ipwhitemod" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" +) + +var ( + // 白名单,不需要验证token的api + whitelist = map[string]bool{ + "/swagger": true, + "/api/web/admin/vid/pullFileInfoFromAws": true, + "/api/web/admin/vid/listDetail": true, + "/api/web/admin/vid/uploadStaticBatch": true, + "/api/web/channel/admin/login": true, + "/api/web/channel/admin/sms/captcha": true, + "/api/web/admin/cdn/sync": true, + "/api/web/admin/extend/syncServerFile": true, + "/api/web/admin/login": true, + "/api/web/admin/slogin": true, + "/api/web/admin/verify": true, + "/api/web/admin/vid/sec": true, + "/api/web/admin/vid/pms/sec": true, + "/api/web/admin/vid/pms/mt_sec": true, + "/api/web/admin/vid/sp/sec": true, + "/api/web/admin/vid/sp/lsjsec": true, + "/api/web/admin/vid/sp/m3u8sec": true, + "/api/web/admin/export/rechargeOrder": true, + "/api/web/admin/export/withdrawOrder": true, + "/api/web/admin/export/goldTurnover": true, + "/api/web/admin/export/videoIncome": true, + "/api/web/admin/export/orderStat": true, + "/api/web/admin/export/exchangecode": true, + "/api/web/district/agent/captcha": true, + "/api/web/district/agent/login": true, + "/api/web/admin/vid/syncNewVideo": true, + "/api/web/admin/newactivity/importModels": true, + "/api/web/admin/vid/spiderSyncSubmit": true, + "/api/web/admin/stats/export": true, + "/api/web/admin/ai/undress/callback": true, + "/api/web/admin/ai/changeface/callback": true, + "/api/web/admin/ai/change_face_img/callback": true, + "/api/web/admin/ai/image_to_video/callback": true, + "/api/web/admin/ai/text_to_image/callback": true, + "/api/web/admin/ai/text_to_novel/callback": true, + "/api/web/laosiji/post/search": true, + "/api/web/laosiji/post/detail": true, + "/api/web/laosiji/comics/search": true, + "/api/web/laosiji/comics/detail": true, + "/api/web/laosiji/novel/search": true, + "/api/web/laosiji/novel/detail": true, + } + InvalidTokenErr = errors.New(" invalid token err") + AccessForbidErr = errors.New(stderr.ErrAccessForbid.Msg()) + noTokenMsg = gin.H{ + "code": stderr.ErrNoToken, + "msg": stderr.ErrNoToken.Msg(), + } +) + +const WarnTokenExpire = 10 * time.Minute + +type ActType = string + +const ( + Admin ActType = "admin" + Channel ActType = "channel" + District ActType = "district" +) + +type AdminRole = string + +const ( + JuShang AdminRole = "聚商" + NudeChatMerchant AdminRole = "裸聊商家" +) + +func GetTokenSecret() string { + return webg.Conf.Base.JwtKey +} + +type Claims struct { + Type ActType + Act string `json:"act"` + Role string `json:"role"` + CID string `json:"cid"` +} + +type claimsWithExp struct { + Claims + Exp int64 `json:"exp"` +} + +func genToken(claims *Claims) (string, error) { + c := claimsWithExp{Claims: *claims, Exp: time.Now().Add(redisconst.WebTokenExpire).Unix()} + secret := GetTokenSecret() + args, _ := common.JSONStruct2Map(c) + token, err := crypt.CreateToken(secret, args) + if err != nil { + log.Error("auth web GenToken error", log.Any("claims", claims), log.E(err)) + } + return token, err +} + +func GenAndSaveToken(claims *Claims) (string, error) { + token, err := genToken(claims) + if err != nil { + return "", err + } + if err = saveToken(claims.Type, claims.Act, token); err != nil { + return "", err + } + return token, nil +} + +func ParseToken(token string) (*claimsWithExp, error) { + secret := GetTokenSecret() + claims, err := crypt.ParseToken(secret, token) + if err != nil { + return nil, err + } + c := claimsWithExp{} + return &c, common.Map2JSONStruct(&c, claims) +} + +func tokenRedisKey(typ ActType, act string) string { + return redisconst.WebTokenKey(typ, act) +} + +func saveToken(typ ActType, act string, token string) error { + key := tokenRedisKey(typ, act) + return webg.Redis.Set(key, token, redisconst.WebTokenExpire) +} + +func RevokeDistrictToken(acts ...string) { + keys := make([]string, len(acts)) + for i, act := range acts { + keys[i] = tokenRedisKey(District, act) + } + _, _ = webg.Redis.Del(keys...) +} + +func RevokeChannelToken(acts ...string) { + keys := make([]string, len(acts)) + for i, act := range acts { + keys[i] = tokenRedisKey(Channel, act) + } + _, _ = webg.Redis.Del(keys...) +} + +func RevokeAdminToken(acts ...string) { + keys := make([]string, len(acts)) + for i, act := range acts { + keys[i] = tokenRedisKey(Admin, act) + } + _, _ = webg.Redis.Del(keys...) +} + +func auth(token string) (string, AdminRole, ActType, string, bool, error) { + claims, err := ParseToken(token) + if err != nil { + return "", "", "", "", false, InvalidTokenErr + } + act := claims.Act + typ := claims.Type + role := claims.Role + cid := claims.CID + exp := time.Unix(claims.Exp, 0) + redisKey := tokenRedisKey(typ, act) + redisToken, err := webg.Redis.Get(redisKey) + if err != nil || redisToken == nil { + return "", "", "", "", false, InvalidTokenErr + } + if token != *redisToken { //Redis token过期,或者错误 + return "", "", "", "", false, InvalidTokenErr + } + now := time.Now() + if now.After(exp) { //token过期 + return "", "", "", "", false, InvalidTokenErr + } + warn := exp.Sub(now) < WarnTokenExpire + return cid, role, typ, act, warn, nil +} + +var accessForbidMsg = gin.H{ + "code": stderr.ErrAccessForbid, + "msg": stderr.ErrAccessForbid.Msg(), +} + +func Auth(ctx *gin.Context) { + for url, ok := range whitelist { + if ok && strings.HasPrefix(ctx.Request.URL.Path, url) { + return + } + } + //检查ip是否在白名单中 + // ipFlag, err := webg.Redis.SISMember(constant.IPWhiteRedisKey, constant.CtxIP) + // if webg.Conf.EnableIPWhite.IsEnable && (err != nil || !ipFlag) { + if webg.Conf.EnableIPWhite.IsEnable { + iPWhite, err := ipwhitemod.FindOneByIp(ctx.ClientIP()) + if err != nil || iPWhite.IP == "" { + ctx.AbortWithStatusJSON(http.StatusOK, accessForbidMsg) + return + } + } + var token string + t1 := ctx.Request.Header.Get("Authorization") + t2 := ctx.Query("token") //为兼容m3u8 + if t1 != "" { + token = t1 + } + if t2 != "" { + token = t2 + } + if token == "" { + ctx.AbortWithStatusJSON(http.StatusOK, noTokenMsg) + return + } + cid, role, typ, act, warn, err := auth(token) + if err != nil { + ctx.Writer.Header().Set("Refresh-Authorization", "false") + ctx.AbortWithStatusJSON(http.StatusOK, gin.H{ + "code": stderr.ErrAuthInvalid, + "msg": err.Error(), + }) + return + } + // 检查管理员账号是否被删除或禁用 + adm, err := adminmod.FindOneByName(act) + if err != nil || adm.ID.IsZero() || adm.HasLocked { + RevokeAdminToken(act) + ctx.Writer.Header().Set("Refresh-Authorization", "false") + ctx.AbortWithStatusJSON(http.StatusOK, gin.H{ + "code": stderr.ErrAuthInvalid, + "msg": "账号已被禁用或删除", + }) + return + } + + if warn { + ctx.Writer.Header().Set("Refresh-Authorization", "true") + } + ctx.Set(constant.CtxAdminRole, role) + switch typ { + case District: + ctx.Set(constant.CtxDistrictName, act) + default: + ctx.Set(constant.CtxAdminAct, act) + switch role { + case JuShang: + if cid == "" { + ctx.AbortWithStatusJSON(http.StatusOK, gin.H{ + "code": stderr.ErrAuthInvalid, + "msg": err.Error(), + }) + } + ctx.Set(constant.CtxJuShangCID, cid) + if !strings.Contains(ctx.Request.URL.Path, "/jushang") && !strings.Contains(ctx.Request.URL.Path, "/api/web/admin/refresh") { + ctx.AbortWithStatusJSON(http.StatusOK, + gin.H{ + "code": stderr.ErrAccessForbid, + "msg": "权限出错", + }) + } + case NudeChatMerchant: + if cid == "" { + ctx.AbortWithStatusJSON(http.StatusOK, gin.H{ + "code": stderr.ErrAuthInvalid, + "msg": err.Error(), + }) + } + ctx.Set(constant.CtxNudeChatMerchant, cid) + } + } +} + +func tokenAppRedisKey(uid uint64) string { + return redisconst.UserTokenKey(uid) +} + +// RevokeTokenCache 吊销redis用户token +func RevokeTokenCache(uids ...uint64) { + keys := make([]string, len(uids)) + for i, uid := range uids { + keys[i] = tokenAppRedisKey(uid) + } + _, _ = webg.Redis.Del(keys...) +} + +func getWebTokenSecret() string { + return webg.Conf.Base.WebJwtKey +} + +// https://tools.ietf.org/html/rfc7519#section-4.1 +// See examples for how to use this with your own claim types +type WebClaims struct { + //用户UID + UID uint64 `json:"uid,omitempty"` + + //The "exp" (expiration time) claim identifies the expiration time on + //or after which the JWT MUST NOT be accepted for processing. The + //processing of the "exp" claim requires that the current date/time + //MUST be before the expiration date/time listed in the "exp" claim. + //Implementers MAY provide for some small leeway, usually no more than + //a few minutes, to account for clock skew. Its value MUST be a number + //containing a NumericDate value. Use of this claim is OPTIONAL. + ExpiresAt int64 `json:"exp,omitempty"` + + //The "iat" (issued at) claim identifies the time at which the JWT was + //issued. This claim can be used to determine the age of the JWT. Its + //value MUST be a number containing a NumericDate value. Use of this + //claim is OPTIONAL. + IssuedAt int64 `json:"iat,omitempty"` +} + +func GenWebToken(claims WebClaims) (string, error) { + secret := getWebTokenSecret() + m, _ := common.JSONStruct2Map(claims) + token, err := crypt.CreateToken(secret, m) + if err != nil { + log.Error("GenWebToken error", log.Any("claims", claims), log.E(err)) + } + return token, err +} + +// ParseWebClaims +// 如果token过期会返回error +func ParseWebClaims(token string) (WebClaims, error) { + secret := getWebTokenSecret() + claims, err := crypt.ParseToken(secret, token) + if err != nil { + return WebClaims{}, err + } + webClaims := WebClaims{} + return webClaims, common.Map2JSONStruct(&webClaims, claims) +} diff --git a/web/middleware/checkPermission/checkPermission.go b/web/middleware/checkPermission/checkPermission.go new file mode 100644 index 0000000..dda328c --- /dev/null +++ b/web/middleware/checkPermission/checkPermission.go @@ -0,0 +1,51 @@ +package checkPermission + +import ( + "net/http" + "strings" + "time" + + "91porn-server/common" + + "github.com/gin-gonic/gin" + "github.com/storyicon/grbac" +) + +// 白名单,不需要验证token的api +var whitelist = map[string]bool{ + "/swagger": true, +} + +func QueryRolesByHeaders(c *gin.Context) (roles []string, err error) { + role, _ := common.GetAdminRole(c) + roles = append(roles, role) + return roles, err +} + +var rbac *grbac.Controller + +func init() { + var err error + rbac, err = grbac.New(grbac.WithJSON("config/rules.json", 10*time.Minute)) + if err != nil { + panic(err) + } +} + +func CheckPermission(c *gin.Context) { + for url, ok := range whitelist { + if ok && strings.HasPrefix(c.Request.URL.Path, url) { + return + } + } + + roles, _ := QueryRolesByHeaders(c) + state, _ := rbac.IsRequestGranted(c.Request, roles) + if !state.IsGranted() { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ + "code": http.StatusUnauthorized, + "msg": "没有操作权限", + }) + return + } +} diff --git a/web/middleware/limitHandler/limit.go b/web/middleware/limitHandler/limit.go new file mode 100644 index 0000000..95298c6 --- /dev/null +++ b/web/middleware/limitHandler/limit.go @@ -0,0 +1,29 @@ +package limitHandler + +import ( + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/web/webg" + "fmt" + "github.com/gin-gonic/gin" + "time" +) + +// FilterRequestByExport 限制用户短时间内的导出请求次数,limit决定限制时间 +func FilterRequestByExport() gin.HandlerFunc { + return func(ctx *gin.Context) { + manager, err := common.GetAdminAct(ctx) + if err != nil { + common.ServeJSON(ctx, stderr.AdminIDErr, err.Error()) + ctx.Abort() + return + } + key := fmt.Sprintf("export_excel:%v", manager) + if webg.Redis.IsExist(key) { + common.ServeJSON(ctx, stderr.VisitLimit, err) + ctx.Abort() + return + } + go func() { _ = webg.Redis.Set(key, "-", time.Second*3) }() + } +} diff --git a/web/middleware/limitHandler/login.go b/web/middleware/limitHandler/login.go new file mode 100644 index 0000000..9af1cc2 --- /dev/null +++ b/web/middleware/limitHandler/login.go @@ -0,0 +1,105 @@ +package limitHandler + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/httputil" + "91porn-server/common/stderr" + "91porn-server/web/webg" + + "github.com/gin-gonic/gin" + "github.com/patrickmn/go-cache" +) + +var limitURI = []string{ + "/api/web/admin/login", + "/api/web/admin/slogin", + "/api/web/admin/verify", +} +var ( + tgBotToken = "2013237388:AAFwvKvEmQfEvcI7wt_64Zkp9KPfgHo8sow" + tgGroupId = "-1003735177636" +) +var ( + limitDuration = 3 * time.Minute + limitCount = 5 +) + +var memCache = cache.New(1*time.Minute, 10*time.Minute) + +// LoginLimit 登陆频率限制 +func LoginLimit(ctx *gin.Context) { + path := ctx.Request.URL.Path + // 检查是否需要限制频率 + check := false + for _, p := range limitURI { + if strings.HasPrefix(path, p) { + check = true + } + } + if !check { + return + } + // 测试环境不限制 + if webg.Conf.Base.Env != constant.ProdEnv { + return + } + // 获取KEY:只按 path + 管理员账号(name),不含 IP + // (对方会伪造 IP,若 key 含 IP 则每次换 IP 就重置计数,导致限流被绕过一直重试) + ip := ctx.ClientIP() // ip 仅用于超限预警展示,不参与限流 key + key := fmt.Sprintf("web-limit:%s", path) + // 检查参数值 + var params map[string]interface{} + if ctx.Request.Method == http.MethodPost { + raw, _ := ctx.GetRawData() + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(raw)) + if err := json.Unmarshal(raw, ¶ms); err == nil { + if a, ok := params["name"]; ok { // 做登陆频率限制只检查管理员账号 + key += ":" + a.(string) + } + } + } + + // 检查频率 + aCount, ok := memCache.Get(key) + if !ok { + memCache.Set(key, 1, limitDuration) + return + } + count, ok2 := aCount.(int) + if !ok2 { + memCache.Set(key, 1, limitDuration) + return + } + count++ + defer func() { memCache.Set(key, count, limitDuration) }() + if count > limitCount { + // 超出限制 + go func() { + // 预警 + message := fmt.Sprintf("[91Porn]\n登陆请求频率过高!\n时间: %s\n请求地址: %s\n请求参数: %+v\nIP: %s\n错误次数: %d", time.Now().Format("2006-01-02 15:04:05"), path, params, ip, count) + // 请求地址/参数为不可信输入,去掉非法 UTF-8 字节;不用 parse_mode,避免 Markdown 把内容解析成实体后报错 + message = strings.ToValidUTF8(message, "") + url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", tgBotToken) + req := map[string]interface{}{ + "chat_id": tgGroupId, + "text": message, + } + resp, _ := httputil.DefaultClientPostJson(url, nil, req) + _ = resp.Body.Close() + }() + ctx.Abort() + common.ServeJSON(ctx, stderr.ErrReqForbidden, nil) + return + } + + ctx.Next() +} diff --git a/web/proto/ai.go b/web/proto/ai.go new file mode 100644 index 0000000..e572c99 --- /dev/null +++ b/web/proto/ai.go @@ -0,0 +1,25 @@ +package proto + +type AiUndressOrderReq struct { + AppId int `json:"appId"` + FileUrl []string `json:"fileUrl"` //base64存的文件服地址 + UserId string `json:"userId"` //用户id + AppOrderNum string `json:"appOrderNum"` //app中生成的订单号 + NotifyUrl string `json:"notifyUrl"` //回调产品地址 +} + +type AiUndressOrderResp struct { + Code int `json:"code"` +} + +type AiChangeFaceReq struct { + AppId int `json:"app_id"` + OrderNumAPP string `json:"order_num_app"` //app中生成的订单号 + Title string `json:"title"` //模版标题 + SourceImageUrl string `json:"source_image_url"` //源脸图片地址 + NotifyUrl string `json:"notify_url"` //订单的回调地址 +} + +type AiChangeFaceResp struct { + Code int `json:"code"` +} diff --git a/web/router/activity.go b/web/router/activity.go new file mode 100644 index 0000000..9542fed --- /dev/null +++ b/web/router/activity.go @@ -0,0 +1,35 @@ +package router + +import ( + "91porn-server/web/api/activityctrl" + + "github.com/gin-gonic/gin" +) + +func activityRouter(router *gin.RouterGroup) { + group := router.Group("/activity") + { + group.POST("", activityctrl.Add) + group.PUT("", activityctrl.Modify) + group.GET("", activityctrl.QueryAll) + group.GET("/box", activityctrl.QueryDropDownBox) + } + prize := group.Group("/prize") + { + prize.POST("", activityctrl.AddPrize) + prize.PUT("", activityctrl.ModifyPrize) + prize.GET("", activityctrl.QueryAllPrize) + prize.GET("/vipBox", activityctrl.QueryVipDropDownBox) + prize.GET("/box", activityctrl.QueryPrizeDropDownBox) + } + prizePool := group.Group("/prizePool") + { + prizePool.POST("", activityctrl.AddPrizePool) + prizePool.PUT("", activityctrl.ModifyPrizePool) + prizePool.GET("", activityctrl.QueryAllPrizePool) + } + record := group.Group("/record") + { + record.GET("", activityctrl.QueryAllRecord) + } +} diff --git a/web/router/actv.go b/web/router/actv.go new file mode 100644 index 0000000..5bcffe9 --- /dev/null +++ b/web/router/actv.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/web/api/actvctrl" + + "github.com/gin-gonic/gin" +) + +func activeRouter(router *gin.RouterGroup) { + group := router.Group("/actv") + { + group.GET("/list", actvctrl.GetActivities) // 活动列表 + group.POST("/update", actvctrl.UpdateActivity) // 编辑活动 + group.POST("/add", actvctrl.AddActivity) // 新增活动 + } +} diff --git a/web/router/addWare.go b/web/router/addWare.go new file mode 100644 index 0000000..394fac6 --- /dev/null +++ b/web/router/addWare.go @@ -0,0 +1,14 @@ +package router + +import ( + "91porn-server/web/api/addwlefarectrl" + + "github.com/gin-gonic/gin" +) + +func addWelfareBackRouter(router *gin.RouterGroup) { + g := router.Group("/third") + { + g.POST("/api/notify/addWelfare", addwlefarectrl.AddWelfare) + } +} diff --git a/web/router/adgroup.go b/web/router/adgroup.go new file mode 100644 index 0000000..1f613a3 --- /dev/null +++ b/web/router/adgroup.go @@ -0,0 +1,14 @@ +package router + +import ( + "91porn-server/web/api/adv_group_ctrl" + + "github.com/gin-gonic/gin" +) + +func adGroupRouter(router *gin.RouterGroup) { + adGroup := router.Group("/adgroup") + { + adGroup.POST("/list", adv_group_ctrl.GetAdGroupStatList) + } +} diff --git a/web/router/admin.go b/web/router/admin.go new file mode 100644 index 0000000..9f8cc91 --- /dev/null +++ b/web/router/admin.go @@ -0,0 +1,24 @@ +package router + +import ( + "91porn-server/web/api/adminctrl" + + "github.com/gin-gonic/gin" +) + +func adminRouter(router *gin.RouterGroup) { + { + router.POST("/login", adminctrl.Login) + router.POST("/slogin", adminctrl.SLogin) + router.POST("/verify", adminctrl.Verify) + router.POST("/logout", adminctrl.Logout) + router.GET("/refresh", adminctrl.Refresh) + } + manage := router.Group("/manage") + { + manage.GET("/list", adminctrl.List) + manage.POST("/add", adminctrl.Add) + manage.DELETE("/del", adminctrl.Delete) + manage.POST("/update", adminctrl.Update) + } +} diff --git a/web/router/ads.go b/web/router/ads.go new file mode 100644 index 0000000..b11a9ed --- /dev/null +++ b/web/router/ads.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/adsctrl" + + "github.com/gin-gonic/gin" +) + +func adsRouter(router *gin.RouterGroup) { + ads := router.Group("/ads") + { + ads.GET("/list", adsctrl.List) + ads.POST("/add", adsctrl.Add) + ads.POST("/update", adsctrl.Update) + ads.DELETE("/del", adsctrl.Delete) + } +} diff --git a/web/router/advance_config.go b/web/router/advance_config.go new file mode 100644 index 0000000..92d67ba --- /dev/null +++ b/web/router/advance_config.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/web/api/advance_config_ctrl" + "github.com/gin-gonic/gin" +) + +func advanceConfigRouter(router *gin.RouterGroup) { + group := router.Group("/advance_config") + { + group.GET("/list", advance_config_ctrl.QueryAll) + group.POST("/create", advance_config_ctrl.Create) + group.POST("/update", advance_config_ctrl.Update) + group.POST("/delete", advance_config_ctrl.Delete) + } +} diff --git a/web/router/advance_order.go b/web/router/advance_order.go new file mode 100644 index 0000000..cc0a261 --- /dev/null +++ b/web/router/advance_order.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/web/api/advance_order_ctrl" + "91porn-server/web/middleware/limitHandler" + + "github.com/gin-gonic/gin" +) + +func advanceOrderRouter(router *gin.RouterGroup) { + group := router.Group("/advance_order") + { + group.GET("", advance_order_ctrl.QueryAll) + group.POST("/export", limitHandler.FilterRequestByExport(), advance_order_ctrl.Export) + } +} diff --git a/web/router/ai_changeface.go b/web/router/ai_changeface.go new file mode 100644 index 0000000..533a160 --- /dev/null +++ b/web/router/ai_changeface.go @@ -0,0 +1,29 @@ +package router + +import ( + "91porn-server/web/api/ai_changeface_ctrl" + + "github.com/gin-gonic/gin" +) + +func aiChangeFaceRouter(router *gin.RouterGroup) { + group := router.Group("/ai/changeface") + { + group.GET("/list", ai_changeface_ctrl.List) // ai换脸订单列表 + group.POST("/update", ai_changeface_ctrl.Update) // ai换脸订单编辑 + group.POST("/callback", ai_changeface_ctrl.Callback) // ai换脸回调 + } + //modGroup := group.Group("/mod") + //{ + // modGroup.GET("/list", ai_changeface_ctrl.ModList) // ai换脸模版列表 + // modGroup.POST("/update", ai_changeface_ctrl.ModUpdate) // ai换脸模版编辑 + // modGroup.POST("/add", ai_changeface_ctrl.ModAdd) // ai换脸模版添加 + // modGroup.GET("/getById", ai_changeface_ctrl.GetById) // 根据id获取模版详情 + //} + imgGroup := router.Group("/ai/change_face_img") + { + imgGroup.GET("/list", ai_changeface_ctrl.AIChangeFaceImgList) + imgGroup.POST("/update", ai_changeface_ctrl.AIChangeFaceImgUpdate) + imgGroup.POST("/callback", ai_changeface_ctrl.CallbackByImg) //ai换脸订单回调 + } +} diff --git a/web/router/ai_mate.go b/web/router/ai_mate.go new file mode 100644 index 0000000..58fd358 --- /dev/null +++ b/web/router/ai_mate.go @@ -0,0 +1,14 @@ +package router + +import ( + ai_mate_ctrl "91porn-server/web/api/ai_mate_ctr" + + "github.com/gin-gonic/gin" +) + +func aiMateRouter(router *gin.RouterGroup) { + aimate := router.Group("/aimate") + { + aimate.GET("/trans_list", ai_mate_ctrl.List) + } +} diff --git a/web/router/ai_mate_v2.go b/web/router/ai_mate_v2.go new file mode 100644 index 0000000..2978132 --- /dev/null +++ b/web/router/ai_mate_v2.go @@ -0,0 +1,12 @@ +package router + +import ( + "91porn-server/web/api/ai_mate_v2_ctrl" + + "github.com/gin-gonic/gin" +) + +func aiMateV2Router(router *gin.RouterGroup) { + aimateV2 := router.Group("/aimatev2") + aimateV2.GET("/order_logs", ai_mate_v2_ctrl.OrderLogs) +} diff --git a/web/router/ai_mod.go b/web/router/ai_mod.go new file mode 100644 index 0000000..5204008 --- /dev/null +++ b/web/router/ai_mod.go @@ -0,0 +1,18 @@ +package router + +import ( + "91porn-server/web/api/ai_changeface_ctrl" + "github.com/gin-gonic/gin" +) + +func aiModRouter(router *gin.RouterGroup) { + group := router.Group("/ai/changeface/mod") + { + group.GET("/list", ai_changeface_ctrl.ModList) // ai换脸模版列表 + group.POST("/update", ai_changeface_ctrl.ModUpdate) // ai换脸模版编辑 + group.POST("/batch/update", ai_changeface_ctrl.BatchModUpdate) // ai换脸模版编辑 + group.POST("/add", ai_changeface_ctrl.ModAdd) // ai换脸模版添加 + group.GET("/getById", ai_changeface_ctrl.GetById) // 根据id获取模版详情 + group.GET("/all/scene", ai_changeface_ctrl.AllSceneList) // ai换脸模版列表 + } +} diff --git a/web/router/ai_text_to_novel.go b/web/router/ai_text_to_novel.go new file mode 100755 index 0000000..fa23acf --- /dev/null +++ b/web/router/ai_text_to_novel.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/ai_text_to_novel_ctrl" + "github.com/gin-gonic/gin" +) + +// aiTextToNovelRouter AI小说列表管理 +func aiTextToNovelRouter(router *gin.RouterGroup) { + group := router.Group("/ai/text_to_novel") + { + group.GET("/list", ai_text_to_novel_ctrl.List) + group.POST("/update", ai_text_to_novel_ctrl.Update) + group.POST("/delete", ai_text_to_novel_ctrl.Delete) + group.POST("/callback", ai_text_to_novel_ctrl.Callback) + } +} diff --git a/web/router/ai_undress.go b/web/router/ai_undress.go new file mode 100644 index 0000000..f256bdf --- /dev/null +++ b/web/router/ai_undress.go @@ -0,0 +1,18 @@ +package router + +import ( + "github.com/gin-gonic/gin" + + "91porn-server/web/api/ai_undress_ctrl" +) + +func aiUndressRouter(router *gin.RouterGroup) { + group := router.Group("/ai/undress") + { + group.GET("/list", ai_undress_ctrl.List) + group.POST("/update", ai_undress_ctrl.Update) + group.POST("/auto", ai_undress_ctrl.Auto) + group.POST("/auto/batch", ai_undress_ctrl.AutoBatch) + group.POST("/callback", ai_undress_ctrl.Callback) + } +} diff --git a/web/router/aiimagetovideo.go b/web/router/aiimagetovideo.go new file mode 100755 index 0000000..2205848 --- /dev/null +++ b/web/router/aiimagetovideo.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/ai_image_to_video_ctrl" + "github.com/gin-gonic/gin" +) + +// aiImageToVideoRouter AI图生视频列表管理 +func aiImageToVideoRouter(router *gin.RouterGroup) { + group := router.Group("/ai/image_to_video") + { + group.GET("/list", ai_image_to_video_ctrl.List) + group.POST("/update", ai_image_to_video_ctrl.Update) + group.POST("/delete", ai_image_to_video_ctrl.Delete) + group.POST("/callback", ai_image_to_video_ctrl.Callback) + } +} diff --git a/web/router/aiplaza.go b/web/router/aiplaza.go new file mode 100755 index 0000000..6b04997 --- /dev/null +++ b/web/router/aiplaza.go @@ -0,0 +1,19 @@ +package router + +import ( + "91porn-server/web/api/aiplazactrl" + + "github.com/gin-gonic/gin" +) + +// aiPlazaRouter ai广场帖子管理 +func aiPlazaRouter(router *gin.RouterGroup) { + group := router.Group("/aiplaza") + { + group.GET("/list", aiplazactrl.List) + group.POST("/create", aiplazactrl.Create) + group.POST("/update", aiplazactrl.Update) + group.POST("/delete", aiplazactrl.Delete) + group.POST("/batch/update", aiplazactrl.BatchUpdate) + } +} diff --git a/web/router/aitemplatemodule.go b/web/router/aitemplatemodule.go new file mode 100755 index 0000000..89fdac9 --- /dev/null +++ b/web/router/aitemplatemodule.go @@ -0,0 +1,18 @@ +package router + +import ( + "91porn-server/web/api/ai_template_module_ctrl" + "github.com/gin-gonic/gin" +) + +// aiTemplateModuleRouter AI模版模块列表管理 +func aiTemplateModuleRouter(router *gin.RouterGroup) { + group := router.Group("/ai_template_module") + { + group.GET("/list", ai_template_module_ctrl.List) + group.GET("/all", ai_template_module_ctrl.AllList) + group.POST("/create", ai_template_module_ctrl.Create) + group.POST("/update", ai_template_module_ctrl.Update) + group.POST("/delete", ai_template_module_ctrl.Delete) + } +} diff --git a/web/router/aitexttoimage.go b/web/router/aitexttoimage.go new file mode 100755 index 0000000..e0f1a87 --- /dev/null +++ b/web/router/aitexttoimage.go @@ -0,0 +1,18 @@ +package router + +import ( + "91porn-server/web/api/ai_text_to_image_ctrl" + "github.com/gin-gonic/gin" +) + +// aiTextToImageRouter AI绘图列表管理 +func aiTextToImageRouter(router *gin.RouterGroup) { + group := router.Group("/ai/text_to_image") + { + group.GET("/list", ai_text_to_image_ctrl.List) + group.POST("/update", ai_text_to_image_ctrl.Update) + group.POST("/delete", ai_text_to_image_ctrl.Delete) + group.POST("/callback", ai_text_to_image_ctrl.Callback) + + } +} diff --git a/web/router/annou.go b/web/router/annou.go new file mode 100644 index 0000000..d83ab8f --- /dev/null +++ b/web/router/annou.go @@ -0,0 +1,24 @@ +package router + +import ( + "91porn-server/web/api/annouctrl" + + "github.com/gin-gonic/gin" +) + +func annouRouter(router *gin.RouterGroup) { + group := router.Group("/annou") + { + group.GET("/list", annouctrl.GetAnnouList) + group.POST("add", annouctrl.AddAnnou) + group.POST("/update", annouctrl.UpdateAnnou) + group.DELETE("/del", annouctrl.DeleteAnnou) + } + groupAnnounce := router.Group("/announce") + { + groupAnnounce.GET("/list", annouctrl.GetAnnounceList) + groupAnnounce.POST("add", annouctrl.AddAnnounce) + groupAnnounce.POST("/update", annouctrl.UpdateAnnounce) + groupAnnounce.DELETE("/del", annouctrl.DeleteAnnounce) + } +} diff --git a/web/router/author.go b/web/router/author.go new file mode 100644 index 0000000..96f73c6 --- /dev/null +++ b/web/router/author.go @@ -0,0 +1,22 @@ +package router + +import ( + "91porn-server/web/api/authorctrl" + "91porn-server/web/api/rolectrl" + + "github.com/gin-gonic/gin" +) + +func authorRouter(router *gin.RouterGroup) { + group := router.Group("/author") + { + group.GET("/list", authorctrl.List) + group.POST("/update", authorctrl.Update) + group.POST("/add", authorctrl.Add) + group.DELETE("/del", authorctrl.Delete) + } + role := router.Group("/role") + { + role.POST("/edit", rolectrl.Edit) + } +} diff --git a/web/router/backpack.go b/web/router/backpack.go new file mode 100644 index 0000000..5137f81 --- /dev/null +++ b/web/router/backpack.go @@ -0,0 +1,13 @@ +package router + +import ( + "91porn-server/web/api/backpack_ctrl" + "github.com/gin-gonic/gin" +) + +func userBackpackRouter(router *gin.RouterGroup) { + group := router.Group("/backpack") + { + group.GET("", backpack_ctrl.QueryAll) + } +} diff --git a/web/router/bannerjump.go b/web/router/bannerjump.go new file mode 100755 index 0000000..5ab44f5 --- /dev/null +++ b/web/router/bannerjump.go @@ -0,0 +1,18 @@ +package router + +import ( + "91porn-server/web/api/bannerjumpctrl" + + "github.com/gin-gonic/gin" +) + +// bannerJumpRouter 限时Banner活动管理 +func bannerJumpRouter(router *gin.RouterGroup) { + group := router.Group("/bannerjump") + { + group.GET("/list", bannerjumpctrl.List) + group.POST("/create", bannerjumpctrl.Create) + group.POST("/update", bannerjumpctrl.Update) + group.POST("/delete", bannerjumpctrl.Delete) + } +} diff --git a/web/router/base.go b/web/router/base.go new file mode 100644 index 0000000..acb407c --- /dev/null +++ b/web/router/base.go @@ -0,0 +1,192 @@ +package router + +import ( + "91porn-server/web/middleware/limitHandler" + "fmt" + "net/http" + "runtime/debug" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/tg" + "91porn-server/middleware/cors" + "91porn-server/middleware/ginzap" + "91porn-server/middleware/ip" + "91porn-server/web/middleware/authweb" + "91porn-server/web/middleware/checkPermission" + "91porn-server/web/service/certificateser" + "91porn-server/web/webg" + + "github.com/gin-contrib/pprof" + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +// SetupGin 初始化gin router +func SetupGin() *gin.Engine { + gin.SetMode(gin.DebugMode) + if webg.Conf.Base.Env == constant.ProdEnv { + gin.SetMode(gin.ReleaseMode) + } + router := gin.New() + router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + if webg.Conf.Base.PProf { + pprof.Register(router) // 性能分析工具 + } + router.ForwardedByClientIP = true + router.Use(ip.RealIP) + router.Use( + ginzap.Logger([]string{constant.CtxAdminAct, constant.CtxIP}), + ginzap.Recovery(true, func(err interface{}) { + stack := string(debug.Stack()) + common.Go(func() { + _ = webg.Bot.Send(tg.Msg{ChatId: tg.ChatIdServerTeam, Text: fmt.Sprintf("[Web-Server] panic:%+v stack:%s", err, stack)}) + }) + })) + router.Use(cors.Cors(&cors.Options{ + Origin: nil, // "*" + Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodHead, http.MethodDelete}, + AllowedHeaders: []string{"Content-Length", "Content-Type", "Authorization"}, + ExposedHeaders: []string{"Content-Disposition", "Refresh-Authorization"}, + OptionsSuccessStatus: http.StatusOK, + })) + router.HandleMethodNotAllowed = true + router.NoMethod(func(c *gin.Context) { + c.JSON(http.StatusMethodNotAllowed, gin.H{"result": false, "error": "Method Not Allowed"}) + }) + router.NoRoute(func(c *gin.Context) { + c.JSON(http.StatusNotFound, gin.H{"result": false, "error": "Endpoint Not Found"}) + }) + registerRouter(router) + return router +} + +func registerRouter(router *gin.Engine) { + //root Router 根组 + rootRouter := router.Group("/api/web", + authweb.Auth, + checkPermission.CheckPermission, + limitHandler.LoginLimit, + ) + { + //web后台管理 + admin := rootRouter.Group("/admin") + { + newActivityRoute(admin) + productRouter(admin) + paymentGuideRouter(admin) + sceneBannerRouter(admin) + vipCardExperimentRouter(admin) + productBenefitRouter(admin) + tagRouter(admin) + updownloadRouter(admin) + locationRoute(admin) + toneRoute(admin) + vidRouter(admin) + imageTopRouter(admin) + adsRouter(admin) + logRouter(admin) + annouRouter(admin) + userRouter(admin) + versionRouter(admin) + adminRouter(admin) + authorRouter(admin) + exportRouter(admin) + activityRouter(admin) + withdrawRouter(admin) + rechargeRouter(admin) + sourceRouter(admin) + proxyRouter(admin) + promoteUrlRouter(admin) + syncCdnRouter(admin) + filterRouter(admin) + ipblockRouter(admin) + ipwhiteRouter(admin) + infmtRoute(admin) + exchangeCodeRouter(admin) + chatRobotRouter(admin) + daichongRouter(admin) + goldCfgRouter(admin) + extendRouter(admin) + freeVidCfgRouter(admin) + ldyRouter(admin) + fictionRouter(admin) + taskRouter(admin) + currencyRouter(admin) + moduleConfRouter(admin) + operationLogRouter(admin) + videoActivityRouter(admin) + activeRouter(admin) + rejectTemp(admin) + aiUndressRouter(admin) + integralConfigRouter(admin) + officialRouter(admin) + commentRouter(admin) + statRouter(admin) + aiChangeFaceRouter(admin) + videoGoldCoinRouter(admin) + advanceOrderRouter(admin) + advanceConfigRouter(admin) + userBackpackRouter(admin) + integralExchangeRouter(admin) + quickSearchRouter(admin) + aiMateRouter(admin) + aiMateV2Router(admin) + sysConfRouter(admin) + bannerJumpRouter(admin) + mediaRouter(admin) + mediaContentRouter(admin) + mediaTagRouter(admin) + mediaTagDimensionRouter(admin) + jingangRouter(admin) + imGroupRouter(admin) + imGroupMemberRouter(admin) + imMessageRouter(admin) + nakedChatRouter(admin) + nakedChatOrderRouter(admin) + aiTextToImageRouter(admin) + aiImageToVideoRouter(admin) + aiTemplateModuleRouter(admin) + aiPlazaRouter(admin) + aiModRouter(admin) + aiTextToNovelRouter(admin) + laosijiRouter(admin) + adGroupRouter(admin) + checkinRouter(admin) + sensitiveWordRouter(admin) + newOfficialWebsiteRoute(admin) + contentReviewRouter(admin) + imRouter(admin) + } + } + // openclaw still 接口 + openclawRouter := router.Group("/customer") + { + commentAgentRouter(openclawRouter) + } + + ext := router.Group("/ext") //对H5接口的 + { + certificate := ext.Group("certificate") + { + certificate.POST("/qr", certificateser.QrCode) + } + } + thirdRouter := router.Group("/3rd") + { + defrayRouter(thirdRouter) + daichongCallBackRouter(thirdRouter) + addWelfareBackRouter(thirdRouter) + } + //代充接口 测试时环境 如此配置 ,生产环境 移动到group下 + daichongRouter := router.Group("/") + { + daichongCallBackRouter(daichongRouter) + } + //数据同步,内部数据中心 + syncDataRouter := router.Group("/services") + { + syncRouter(syncDataRouter) + } +} diff --git a/web/router/chatrobot.go b/web/router/chatrobot.go new file mode 100644 index 0000000..766da6d --- /dev/null +++ b/web/router/chatrobot.go @@ -0,0 +1,23 @@ +package router + +import ( + "91porn-server/web/api/chatrobotctrl" + "91porn-server/web/api/contentlibctrl" + + "github.com/gin-gonic/gin" +) + +func chatRobotRouter(router *gin.RouterGroup) { + group := router.Group("/robot") + { + // 机器人配置 + group.GET("/conf/list", chatrobotctrl.RobotConfList) + group.POST("/conf/add", chatrobotctrl.AddRobotConf) + group.POST("/conf/update", chatrobotctrl.UpdateRobotConf) + // 机器人聊天内容库 + group.GET("/content/list", contentlibctrl.ContentList) + group.POST("/content/add", contentlibctrl.ContentAdd) + group.DELETE("/content/del", contentlibctrl.ContentDel) + } + +} diff --git a/web/router/checkin.go b/web/router/checkin.go new file mode 100644 index 0000000..47d3fa3 --- /dev/null +++ b/web/router/checkin.go @@ -0,0 +1,27 @@ +package router + +import ( + "91porn-server/web/api/checkinctrl" + + "github.com/gin-gonic/gin" +) + +func checkinRouter(router *gin.RouterGroup) { + checkin := router.Group("/checkin") + { + // 签到配置 + checkin.POST("/conf/get", checkinctrl.GetCheckinConfig) + checkin.POST("/conf/update", checkinctrl.UpdateCheckinConfig) + // 签到历史记录 + checkin.POST("/history/list", checkinctrl.GetUserCheckins) + checkin.POST("/history/del", checkinctrl.DelOneUserCheckin) + } + checkinPrize := router.Group("/checkinprize") + { + // 签到奖品 + checkinPrize.POST("/list", checkinctrl.GetCheckinPrizeList) + checkinPrize.POST("/add", checkinctrl.AddOneCheckinPrize) + checkinPrize.POST("/update", checkinctrl.UpdateOneCheckinPrize) + checkinPrize.POST("/del", checkinctrl.DelOneCheckinPrize) + } +} diff --git a/web/router/comment.go b/web/router/comment.go new file mode 100644 index 0000000..930ab80 --- /dev/null +++ b/web/router/comment.go @@ -0,0 +1,23 @@ +package router + +import ( + "91porn-server/web/api/commentctrl" + + "github.com/gin-gonic/gin" +) + +func commentRouter(router *gin.RouterGroup) { + group := router.Group("/comment") + { + group.GET("/list", commentctrl.List) + group.GET("/info", commentctrl.Info) + + group.POST("/send", commentctrl.Send) + group.POST("/delete", commentctrl.Delete) + group.POST("/access", commentctrl.Access) + group.POST("/batch/access", commentctrl.BatchAccess) + group.POST("/update", commentctrl.Update) + group.POST("/importExcel", commentctrl.AddContentExcel) + group.POST("/importComment", commentctrl.ImportComment) + } +} diff --git a/web/router/commentAgent.go b/web/router/commentAgent.go new file mode 100644 index 0000000..9b80d1a --- /dev/null +++ b/web/router/commentAgent.go @@ -0,0 +1,22 @@ +package router + +import ( + "91porn-server/web/api/commentAgentctrl" + + "github.com/gin-gonic/gin" +) + +func commentAgentRouter(router *gin.RouterGroup) { + group := router.Group("/comments") + { + group.GET("/pull", commentAgentctrl.Pull) + group.POST("/approve", commentAgentctrl.Approve) + group.POST("/reply", commentAgentctrl.Reply) + } + + comicsTranslateGroup := router.Group("/comics/translate") + { + comicsTranslateGroup.GET("/pull", commentAgentctrl.ComicsTranslatePull) + comicsTranslateGroup.POST("/push", commentAgentctrl.ComicsTranslatePush) + } +} diff --git a/web/router/content_review.go b/web/router/content_review.go new file mode 100644 index 0000000..58151af --- /dev/null +++ b/web/router/content_review.go @@ -0,0 +1,21 @@ +package router + +import ( + "91porn-server/web/api/contentreviewctrl" + + "github.com/gin-gonic/gin" +) + +func contentReviewRouter(router *gin.RouterGroup) { + group := router.Group("/content-review") + { + group.POST("/create", contentreviewctrl.Create) + group.GET("/list", contentreviewctrl.List) + group.GET("/detail", contentreviewctrl.Detail) + group.GET("/issues", contentreviewctrl.Issues) + // 命中记录处理流程:提交即生效 / 搁置带原因 / 批量下架 + group.POST("/issue/resolve", contentreviewctrl.ResolveIssue) + group.POST("/issue/reject", contentreviewctrl.RejectIssue) + group.POST("/issue/batch-offshelf", contentreviewctrl.BatchOffShelfIssues) + } +} diff --git a/web/router/currency.go b/web/router/currency.go new file mode 100644 index 0000000..6391d68 --- /dev/null +++ b/web/router/currency.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/currencyctrl" + + "github.com/gin-gonic/gin" +) + +// 货币管理 +func currencyRouter(router *gin.RouterGroup) { + group := router.Group("/currency") + { + group.GET("", currencyctrl.QueryAll) + group.POST("", currencyctrl.Add) + group.PUT("", currencyctrl.Edit) + } +} diff --git a/web/router/daichong.go b/web/router/daichong.go new file mode 100644 index 0000000..816b10e --- /dev/null +++ b/web/router/daichong.go @@ -0,0 +1,21 @@ +package router + +import ( + "91porn-server/web/api/daichongctrl" + + "github.com/gin-gonic/gin" +) + +func daichongCallBackRouter(router *gin.RouterGroup) { + g := router.Group("/third") + { + g.POST("/api/queryPlayerInfo", daichongctrl.QueryPlayerInfo) + } +} + +func daichongRouter(router *gin.RouterGroup) { + g2 := router.Group("/daichong/order") + { + g2.POST("/handScore", daichongctrl.HandAddScore) + } +} diff --git a/web/router/defray.go b/web/router/defray.go new file mode 100644 index 0000000..6b95d74 --- /dev/null +++ b/web/router/defray.go @@ -0,0 +1,37 @@ +package router + +import ( + "91porn-server/web/api/rechargectrl" + "91porn-server/web/api/withdrawctrl" + + "github.com/gin-gonic/gin" +) + +// defrayRouter 支付回调 +func defrayRouter(router *gin.RouterGroup) { + groupDefray := router.Group("/defray/callback") + { + //大白鲨充值回调 + groupDefray.POST("/dabaisha", rechargectrl.DaBaiShaCallBack) + //鲨鱼充值回调 + groupDefray.POST("/shark", rechargectrl.SharkCallBack) + //乐支付充值回调 + groupDefray.POST("/lucky", rechargectrl.LuckyCallBack) + //金鱼充值回调 + //大金鱼充值回调 + groupDefray.POST("/goldfishPlus", rechargectrl.GoldfishPlusCallBack) + //艾支付充值回调 + groupDefray.GET("/izhifu", rechargectrl.IZhiFuCallBack) + //dx支付充值回调 + groupDefray.POST("/dx", rechargectrl.DXZhiFuCallBack) + //雷火支付充值回调 + groupDefray.POST("/leihuo", rechargectrl.LeiHuoCallBack) + //中付支付充值回调 + groupDefray.POST("/zhongfu", rechargectrl.ZhongFuCallBack) + //yilianfu支付充值回调 + groupDefray.POST("/yilianfu", rechargectrl.YiLianFuCallBack) + //音色 + groupDefray.POST("/echg/goldfish", withdrawctrl.GoldFishCallBack) + } + router.POST("/withdraw/callback", withdrawctrl.YinseCallBack) +} diff --git a/web/router/exchcode.go b/web/router/exchcode.go new file mode 100644 index 0000000..dacfb81 --- /dev/null +++ b/web/router/exchcode.go @@ -0,0 +1,21 @@ +package router + +import ( + "91porn-server/web/api/exchcodectrl" + + "github.com/gin-gonic/gin" +) + +// 兑换码路由 +func exchangeCodeRouter(router *gin.RouterGroup) { + group := router.Group("/exchange") + { + // 兑换码兑换日志 + group.GET("/log/list", exchcodectrl.LogList) + // 兑换码 + group.GET("/code/list", exchcodectrl.CodeList) + group.POST("/code/add", exchcodectrl.CodeAdd) + group.POST("/code/update", exchcodectrl.CodeUpdate) + } + router.GET("/channel/list", exchcodectrl.ChannelList) +} diff --git a/web/router/export.go b/web/router/export.go new file mode 100644 index 0000000..d773c5f --- /dev/null +++ b/web/router/export.go @@ -0,0 +1,22 @@ +package router + +import ( + "91porn-server/web/api/exportctrl" + "91porn-server/web/middleware/limitHandler" + + "github.com/gin-gonic/gin" +) + +func exportRouter(router *gin.RouterGroup, handlers ...gin.HandlerFunc) { + group := router.Group("/export", handlers...) + { + group.GET("/userList", limitHandler.FilterRequestByExport(), exportctrl.UserList) + group.GET("/rechargeOrder", exportctrl.RechargeOrder) + group.GET("/withdrawOrder", exportctrl.WithdrawOrder) + group.GET("/goldTurnover", exportctrl.GoldTurnover) + group.GET("/videoIncome", exportctrl.VideoIncome) + group.GET("/orderStat", exportctrl.OrderStat) + group.GET("/exchangecode", exportctrl.CodeList) + //group.GET("/:fileType", exportctrl.Export) + } +} diff --git a/web/router/extend.go b/web/router/extend.go new file mode 100644 index 0000000..e7afe9c --- /dev/null +++ b/web/router/extend.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/web/api/extctrl" + + "github.com/gin-gonic/gin" +) + +func extendRouter(router *gin.RouterGroup) { + g := router.Group("/extend") + { + g.GET("/viewCaptcha", extctrl.ViewCaptcha) + g.POST("/syncServerFile", extctrl.SyncServerFile) + } +} diff --git a/web/router/fiction.go b/web/router/fiction.go new file mode 100644 index 0000000..bf38f7c --- /dev/null +++ b/web/router/fiction.go @@ -0,0 +1,34 @@ +package router + +import ( + "91porn-server/web/api/audiobookctrl" + "91porn-server/web/api/errfeedbackctrl" + "91porn-server/web/api/fictionctrl" + + "github.com/gin-gonic/gin" +) + +func fictionRouter(router *gin.RouterGroup) { + fiction := router.Group("/fiction") + { + fiction.POST("/list", fictionctrl.List) + fiction.POST("/edit", fictionctrl.Update) + fiction.POST("/add", fictionctrl.Insert) + fiction.DELETE("/del", fictionctrl.Delete) + fiction.POST("/get", fictionctrl.Get) + } + audiobook := router.Group("/audiobook") + { + audiobook.POST("/list", audiobookctrl.List) + audiobook.POST("/edit", audiobookctrl.Update) + audiobook.POST("/editBatch", audiobookctrl.UpdateBatch) + audiobook.POST("/add", audiobookctrl.Insert) + audiobook.DELETE("/del", audiobookctrl.Delete) + } + errfeedback := router.Group("/errFeedback") + { + errfeedback.POST("/list", errfeedbackctrl.List) + errfeedback.POST("/edit", errfeedbackctrl.Process) + errfeedback.DELETE("/del", errfeedbackctrl.DelVerifyReport) + } +} diff --git a/web/router/filter.go b/web/router/filter.go new file mode 100644 index 0000000..3de087a --- /dev/null +++ b/web/router/filter.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/web/api/filterctrl" + + "github.com/gin-gonic/gin" +) + +func filterRouter(router *gin.RouterGroup) { + group := router.Group("/filter") + { + group.GET("/list", filterctrl.List) + group.POST("/add", filterctrl.Add) + group.DELETE("/delete", filterctrl.Delete) + } +} diff --git a/web/router/freevidcfg.go b/web/router/freevidcfg.go new file mode 100644 index 0000000..267d818 --- /dev/null +++ b/web/router/freevidcfg.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/freevidcfgctrl" + + "github.com/gin-gonic/gin" +) + +func freeVidCfgRouter(router *gin.RouterGroup) { + g := router.Group("/freeVid/cfg") + { + g.POST("/add", freevidcfgctrl.Add) + g.POST("/edit", freevidcfgctrl.Edit) + g.GET("/list", freevidcfgctrl.List) + g.DELETE("/del", freevidcfgctrl.Del) + } +} diff --git a/web/router/goldcfg.go b/web/router/goldcfg.go new file mode 100644 index 0000000..42c73d3 --- /dev/null +++ b/web/router/goldcfg.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/goldcfgctrl" + + "github.com/gin-gonic/gin" +) + +func goldCfgRouter(router *gin.RouterGroup) { + g := router.Group("/gold/cfg") + { + g.POST("/add", goldcfgctrl.Add) + g.POST("/edit", goldcfgctrl.Edit) + g.GET("/list", goldcfgctrl.List) + g.DELETE("/del", goldcfgctrl.Del) + } +} diff --git a/web/router/im.go b/web/router/im.go new file mode 100644 index 0000000..9d0f857 --- /dev/null +++ b/web/router/im.go @@ -0,0 +1,24 @@ +package router + +import ( + "91porn-server/web/api/web_im" + + "github.com/gin-gonic/gin" +) + +func imRouter(router *gin.RouterGroup) { + group := router.Group("/im") + { + group.POST("/sync-users", web_im.SyncIMUsers) + group.GET("/sync-users/status", web_im.SyncIMUsersStatus) + group.POST("/users/list", web_im.ListIMUsers) + group.POST("/users/sync", web_im.SyncIMUsersByIDs) + group.POST("/friend/add-direct", web_im.AddIMFriendDirect) + group.POST("/friends/add-direct", web_im.AddIMFriendDirect) + group.POST("/online/status", web_im.BatchIMOnlineStatus) + group.POST("/online/status/batch", web_im.BatchIMOnlineStatus) + group.POST("/passthrough/send", web_im.SendIMPassthrough) + group.POST("/ad/notify", web_im.SendIMAdNotify) + group.POST("/message/send", web_im.SendIMMessage) + } +} diff --git a/web/router/imagetop.go b/web/router/imagetop.go new file mode 100644 index 0000000..c5d943e --- /dev/null +++ b/web/router/imagetop.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/web/api/imagetopctrl" + + "github.com/gin-gonic/gin" +) + +func imageTopRouter(router *gin.RouterGroup) { + group := router.Group("/image") + { + group.POST("/list", imagetopctrl.GetImageTopList) + group.POST("/update", imagetopctrl.UpdateImageTop) + group.POST("/delete", imagetopctrl.DeleteImageTop) + } +} diff --git a/web/router/imgroup.go b/web/router/imgroup.go new file mode 100755 index 0000000..c94faaf --- /dev/null +++ b/web/router/imgroup.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/web/api/imgroupctrl" + "github.com/gin-gonic/gin" +) + +// imGroupRouter im群组管理 +func imGroupRouter(router *gin.RouterGroup) { + group := router.Group("/imgroup") + { + group.GET("/list", imgroupctrl.List) + group.POST("/create", imgroupctrl.Create) + group.POST("/update", imgroupctrl.Update) + } +} diff --git a/web/router/imgroupmember.go b/web/router/imgroupmember.go new file mode 100755 index 0000000..6ae5dd6 --- /dev/null +++ b/web/router/imgroupmember.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/web/api/imgroupmemberctrl" + "github.com/gin-gonic/gin" +) + +// imGroupMemberRouter im群组成员管理 +func imGroupMemberRouter(router *gin.RouterGroup) { + group := router.Group("/imgroupmember") + { + group.GET("/list", imgroupmemberctrl.List) + group.POST("/create", imgroupmemberctrl.Create) + group.POST("/update", imgroupmemberctrl.Update) + } +} diff --git a/web/router/immessage.go b/web/router/immessage.go new file mode 100755 index 0000000..10e9ec4 --- /dev/null +++ b/web/router/immessage.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/immessagectrl" + "github.com/gin-gonic/gin" +) + +// imMessageRouter im消息管理 +func imMessageRouter(router *gin.RouterGroup) { + group := router.Group("/immessage") + { + group.GET("/list", immessagectrl.List) + group.POST("/create", immessagectrl.Create) + group.POST("/update", immessagectrl.Update) + group.POST("/delete", immessagectrl.Delete) + } +} diff --git a/web/router/infmt.go b/web/router/infmt.go new file mode 100644 index 0000000..14c78a0 --- /dev/null +++ b/web/router/infmt.go @@ -0,0 +1,26 @@ +package router + +import ( + "91porn-server/web/api/infmtctrl" + + "github.com/gin-gonic/gin" +) + +func infmtRoute(router *gin.RouterGroup) { + infmt := router.Group("/infmt") + { + mail := infmt.Group("/mail") + { + mail.GET("/list", infmtctrl.MailList) + mail.POST("/add", infmtctrl.MailAdd) + mail.DELETE("/delete", infmtctrl.MailDel) + } + notice := infmt.Group("/notice") + { + notice.GET("/list", infmtctrl.NoticeList) + notice.POST("/update", infmtctrl.NoticeUpdate) + notice.POST("/add", infmtctrl.NoticeAdd) + notice.DELETE("/delete", infmtctrl.NoticeDel) + } + } +} diff --git a/web/router/integralConfig.go b/web/router/integralConfig.go new file mode 100644 index 0000000..e06b015 --- /dev/null +++ b/web/router/integralConfig.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/web/api/integral_config_ctrl" + "github.com/gin-gonic/gin" +) + +func integralConfigRouter(router *gin.RouterGroup) { + group := router.Group("/integral_config") + { + group.PUT("", integral_config_ctrl.Update) + group.POST("", integral_config_ctrl.Add) + group.GET("", integral_config_ctrl.QueryAll) + } +} diff --git a/web/router/integralExchange.go b/web/router/integralExchange.go new file mode 100644 index 0000000..5043026 --- /dev/null +++ b/web/router/integralExchange.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/web/api/integral_exchange_ctrl" + + "github.com/gin-gonic/gin" +) + +func integralExchangeRouter(router *gin.RouterGroup) { + group := router.Group("/integral_exchange") + { + group.PUT("", integral_exchange_ctrl.Update) + group.GET("", integral_exchange_ctrl.QueryAll) + } +} diff --git a/web/router/ipblock.go b/web/router/ipblock.go new file mode 100644 index 0000000..0afe2cc --- /dev/null +++ b/web/router/ipblock.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/ipblockctrl" + + "github.com/gin-gonic/gin" +) + +func ipblockRouter(router *gin.RouterGroup) { + group := router.Group("/ipblock") + { + group.GET("/list", ipblockctrl.IpBlockList) + group.POST("/add", ipblockctrl.IPBlockAdd) + group.DELETE("/del", ipblockctrl.IPBlockDel) + group.POST("/edit", ipblockctrl.IPBlockEdit) + } +} diff --git a/web/router/ipwhite.go b/web/router/ipwhite.go new file mode 100644 index 0000000..932db2c --- /dev/null +++ b/web/router/ipwhite.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/ipwhitectrl" + + "github.com/gin-gonic/gin" +) + +func ipwhiteRouter(router *gin.RouterGroup) { + group := router.Group("/ipwhite") + { + group.GET("/list", ipwhitectrl.IpWhiteList) + group.POST("/add", ipwhitectrl.IPWhiteAdd) + group.DELETE("/del", ipwhitectrl.IPWhiteDel) + group.POST("/edit", ipwhitectrl.IPWhiteEdit) + } +} diff --git a/web/router/jingang.go b/web/router/jingang.go new file mode 100644 index 0000000..607b401 --- /dev/null +++ b/web/router/jingang.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/web/api/jingangctrl" + "github.com/gin-gonic/gin" +) + +func jingangRouter(router *gin.RouterGroup) { + group := router.Group("/jingang") + + group.GET("/list", jingangctrl.List) + group.POST("/update", jingangctrl.Update) + group.POST("/add", jingangctrl.Add) + group.POST("/delete", jingangctrl.Delete) +} diff --git a/web/router/laosiji.go b/web/router/laosiji.go new file mode 100644 index 0000000..f17b84d --- /dev/null +++ b/web/router/laosiji.go @@ -0,0 +1,38 @@ +package router + +import ( + "91porn-server/web/api/laosijictrl" + "91porn-server/web/api/tianyuctrl" + + "github.com/gin-gonic/gin" +) + +func laosijiRouter(router *gin.RouterGroup) { + group := router.Group("/laosiji") + { + group.POST("/movie/search", laosijictrl.MovieSearch) + group.POST("/movie/detail", laosijictrl.MovieDetail) + group.POST("/movie/add", laosijictrl.MovieAdd) + } + { + group.POST("/post/search", laosijictrl.PostSearch) + group.POST("/post/detail", laosijictrl.PostDetail) + group.POST("/post/add", laosijictrl.PostAdd) + } + { + group.POST("/comics/search", laosijictrl.ComicsSearch) + group.POST("/comics/detail", laosijictrl.ComicsDetail) + group.POST("/comics/add", laosijictrl.ComicsAdd) + } + { + group.POST("/novel/search", laosijictrl.NovelSearch) + group.POST("/novel/detail", laosijictrl.NovelDetail) + group.POST("/novel/add", laosijictrl.NovelAdd) + } + + groupTianyu := router.Group("/tianyu") + { + groupTianyu.POST("/search", tianyuctrl.VideoSearch) + groupTianyu.POST("/add", tianyuctrl.AddOneMedia) + } +} diff --git a/web/router/ldycfg.go b/web/router/ldycfg.go new file mode 100644 index 0000000..c8bc2fa --- /dev/null +++ b/web/router/ldycfg.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/web/api/ldyctrl" + + "github.com/gin-gonic/gin" +) + +func ldyRouter(router *gin.RouterGroup) { + group := router.Group("/ldy") + { + group.GET("/cfg", ldyctrl.GetCfg) + group.POST("/update", ldyctrl.Update) + } +} diff --git a/web/router/location.go b/web/router/location.go new file mode 100644 index 0000000..0a5efd3 --- /dev/null +++ b/web/router/location.go @@ -0,0 +1,22 @@ +package router + +import ( + "91porn-server/web/api/locationctrl" + + "github.com/gin-gonic/gin" +) + +func locationRoute(router *gin.RouterGroup) { + group := router.Group("/vid/location/city") + { + group.GET("/list", locationctrl.CityList) + group.POST("/update", locationctrl.CityUpdate) + } + group = router.Group("/vid/location/hotCity") + { + group.GET("/list", locationctrl.HotCityList) + group.POST("/add", locationctrl.HotCityAdd) + group.POST("/update", locationctrl.HotCityUpdate) + group.DELETE("/delete", locationctrl.HotCityDelete) + } +} diff --git a/web/router/log.go b/web/router/log.go new file mode 100644 index 0000000..3b1f403 --- /dev/null +++ b/web/router/log.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/logctrl" + + "github.com/gin-gonic/gin" +) + +func logRouter(router *gin.RouterGroup) { + group := router.Group("/log") + { + group.GET("/gold", logctrl.GetRechargeGoldLog) + group.GET("/goldType", logctrl.GetRechargeGoldTranType) + group.GET("/operator", logctrl.GetOperatorLog) + group.POST("/operator/modify", logctrl.ModifyOperatorLog) + } +} diff --git a/web/router/media.go b/web/router/media.go new file mode 100755 index 0000000..0bf56b9 --- /dev/null +++ b/web/router/media.go @@ -0,0 +1,43 @@ +package router + +import ( + "91porn-server/web/api/dramatopicctrl" + "91porn-server/web/api/media_resource" + "91porn-server/web/api/mediactrl" + "github.com/gin-gonic/gin" +) + +// mediaRouter 动漫列表管理 +func mediaRouter(router *gin.RouterGroup) { + group := router.Group("/media") + { + group.GET("/list", mediactrl.List) + group.GET("/find", mediactrl.Find) + group.POST("/create", mediactrl.Create) + group.POST("/update", mediactrl.Update) + group.POST("/batch/update", mediactrl.BatchUpdate) + group.POST("/delete", mediactrl.Delete) + group.POST("/batch/delete", mediactrl.BatchDelete) + group.POST("/export", mediactrl.Export) + } + topic := router.Group("/media/drama/topic") + { + topic.GET("/list", dramatopicctrl.List) + topic.POST("/create", dramatopicctrl.Create) + topic.POST("/update", dramatopicctrl.Update) + topic.POST("/delete", dramatopicctrl.Delete) + topic.POST("/sort", dramatopicctrl.Sort) + topic.POST("/works/update", dramatopicctrl.UpdateWorks) + } + // 媒资库相关操作 + mediaR := router.Group("/mediaResource/media") + { + mediaR.GET("/list", media_resource.List) + mediaR.GET("/content_list", media_resource.ContentList) + mediaR.POST("/use", media_resource.UseMedia) + mediaR.POST("/ignore", media_resource.IgnoreMedia) + // 立马执行同步任务 + mediaR.POST("/sync", media_resource.SyncMedia) + + } +} diff --git a/web/router/mediacontent.go b/web/router/mediacontent.go new file mode 100755 index 0000000..d38616e --- /dev/null +++ b/web/router/mediacontent.go @@ -0,0 +1,19 @@ +package router + +import ( + "91porn-server/web/api/mediacontentctrl" + "github.com/gin-gonic/gin" +) + +// mediaContentRouter 动漫内容列表管理 +func mediaContentRouter(router *gin.RouterGroup) { + group := router.Group("/media_content") + { + group.GET("/list", mediacontentctrl.List) + group.POST("/create", mediacontentctrl.Create) + group.POST("/update", mediacontentctrl.Update) + group.POST("/batch/update", mediacontentctrl.BatchUpdate) + group.POST("/delete", mediacontentctrl.Delete) + group.GET("/view/:id", mediacontentctrl.ViewMedia) + } +} diff --git a/web/router/mediatag.go b/web/router/mediatag.go new file mode 100755 index 0000000..96b6f51 --- /dev/null +++ b/web/router/mediatag.go @@ -0,0 +1,18 @@ +package router + +import ( + "91porn-server/web/api/mediatagctrl" + "github.com/gin-gonic/gin" +) + +// mediaTagRouter 动漫标签管理 +func mediaTagRouter(router *gin.RouterGroup) { + group := router.Group("/media_tag") + { + group.GET("/list", mediatagctrl.List) + group.GET("/all", mediatagctrl.AllList) + group.POST("/create", mediatagctrl.Create) + group.POST("/update", mediatagctrl.Update) + group.POST("/delete", mediatagctrl.Delete) + } +} diff --git a/web/router/mediatagdimension.go b/web/router/mediatagdimension.go new file mode 100755 index 0000000..606718c --- /dev/null +++ b/web/router/mediatagdimension.go @@ -0,0 +1,18 @@ +package router + +import ( + "91porn-server/web/api/mediatagdimensionctrl" + "github.com/gin-gonic/gin" +) + +// mediaTagDimensionRouter 媒体纬度列表管理 +func mediaTagDimensionRouter(router *gin.RouterGroup) { + group := router.Group("/media_tag_dimension") + { + group.GET("/list", mediatagdimensionctrl.List) + group.GET("/all", mediatagdimensionctrl.AllList) + group.POST("/create", mediatagdimensionctrl.Create) + group.POST("/update", mediatagdimensionctrl.Update) + group.POST("/delete", mediatagdimensionctrl.Delete) + } +} diff --git a/web/router/module.go b/web/router/module.go new file mode 100644 index 0000000..628e6c6 --- /dev/null +++ b/web/router/module.go @@ -0,0 +1,41 @@ +package router + +import ( + "91porn-server/web/api/modulectrl" + "91porn-server/web/api/modulevideoctrl" + + "github.com/gin-gonic/gin" +) + +func moduleConfRouter(r *gin.RouterGroup) { + module := r.Group("/module") + { + moduleconf := module.Group("/conf") + moduleconf.POST("/add", modulectrl.Add) + moduleconf.POST("/edit", modulectrl.Edit) + moduleconf.POST("/delete", modulectrl.Delete) + moduleconf.POST("/list", modulectrl.Search) + moduleconf.POST("/marquee", modulectrl.ChangeModuleMarquee) + moduleconf.GET("/marquee", modulectrl.GetModuleMarquee) + moduleconf.GET("/cartoon/all", modulectrl.AllCartoonList) + + } + { + section := module.Group("/section") + section.POST("/add", modulectrl.AddSection) + section.POST("/edit", modulectrl.EditSection) + section.POST("/delete", modulectrl.DeleteSection) + section.POST("/list", modulectrl.SearchSection) + section.POST("/all", modulectrl.AllSections) + section.POST("/clicks", modulectrl.ClicksList) + } + { + sectionVideo := module.Group("/video") + sectionVideo.POST("/list", modulevideoctrl.List) + sectionVideo.POST("/add", modulevideoctrl.AddVideo) + sectionVideo.POST("/edit", modulevideoctrl.UpdateVideo) + sectionVideo.POST("/delete", modulevideoctrl.DeleteVideo) + sectionVideo.POST("/delete/batch", modulevideoctrl.BatchDeleteVideo) + sectionVideo.POST("/add/batch", modulevideoctrl.AddVideoBatch) + } +} diff --git a/web/router/nakedchat.go b/web/router/nakedchat.go new file mode 100755 index 0000000..c2fde28 --- /dev/null +++ b/web/router/nakedchat.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/nakedchatctrl" + "github.com/gin-gonic/gin" +) + +// nakedChatRouter 裸聊管理 +func nakedChatRouter(router *gin.RouterGroup) { + group := router.Group("/nakedchat") + { + group.GET("/list", nakedchatctrl.List) + group.POST("/create", nakedchatctrl.Create) + group.POST("/update", nakedchatctrl.Update) + group.POST("/batch/update", nakedchatctrl.BatchUpdate) + } +} diff --git a/web/router/nakedchatorder.go b/web/router/nakedchatorder.go new file mode 100755 index 0000000..987b93c --- /dev/null +++ b/web/router/nakedchatorder.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/web/api/nakedchatorderctrl" + "github.com/gin-gonic/gin" +) + +// nakedChatOrderRouter 裸聊订单管理 +func nakedChatOrderRouter(router *gin.RouterGroup) { + group := router.Group("/nakedchatorder") + { + group.GET("/list", nakedchatorderctrl.List) + group.POST("/update", nakedchatorderctrl.Update) + } +} diff --git a/web/router/newActivity.go b/web/router/newActivity.go new file mode 100644 index 0000000..dff9b4b --- /dev/null +++ b/web/router/newActivity.go @@ -0,0 +1,19 @@ +package router + +import ( + "91porn-server/web/api/newactivityctrl" + + "github.com/gin-gonic/gin" +) + +func newActivityRoute(router *gin.RouterGroup) { + g := router.Group("/newactivity") + { + g.POST("importModels", newactivityctrl.ImportModels) + g.POST("updateJoinNum", newactivityctrl.UpdateJoinNum) + g.GET("joinNum", newactivityctrl.GetJoinNum) + g.POST("buyPage", newactivityctrl.BuyPage) + g.POST("addWinRecord", newactivityctrl.AddWinRecord) + g.POST("winRecordPage", newactivityctrl.WinRecordPage) + } +} diff --git a/web/router/official.go b/web/router/official.go new file mode 100644 index 0000000..2e99bbe --- /dev/null +++ b/web/router/official.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/web/api/officialctrl" + "github.com/gin-gonic/gin" +) + +func officialRouter(router *gin.RouterGroup) { + group := router.Group("/official") + { + group.GET("/list", officialctrl.List) + group.POST("/update", officialctrl.Update) + group.POST("/add", officialctrl.Add) + group.DELETE("/delete", officialctrl.Delete) + } +} diff --git a/web/router/officialWebsite.go b/web/router/officialWebsite.go new file mode 100644 index 0000000..4150933 --- /dev/null +++ b/web/router/officialWebsite.go @@ -0,0 +1,107 @@ +package router + +import ( + "91porn-server/web/api/officialWebsitectrl" + + "github.com/gin-gonic/gin" +) + +const ( + officialWebsitePathList = "/list" + officialWebsitePathCreate = "/create" + officialWebsitePathUpdate = "/update" + officialWebsitePathDelete = "/delete" +) + +func newOfficialWebsiteRoute(router *gin.RouterGroup) { + g := router.Group("/officialWebsite") + { + g.GET("", officialWebsitectrl.GetBasicData) // 品牌站基础数据接口,包含基础静态数据和CMS动态数据 + g.POST("", officialWebsitectrl.CreateBasicData) + g.PUT("", officialWebsitectrl.UpdateBasicData) + g.DELETE("", officialWebsitectrl.DeleteBasicData) + + hero := g.Group("/hero") + { + hero.GET(officialWebsitePathList, officialWebsitectrl.ListHero) + hero.POST(officialWebsitePathCreate, officialWebsitectrl.CreateHero) + hero.PUT(officialWebsitePathUpdate, officialWebsitectrl.UpdateHero) + hero.DELETE(officialWebsitePathDelete, officialWebsitectrl.DeleteHero) + } + + album := g.Group("/album") + { + album.GET(officialWebsitePathList, officialWebsitectrl.ListAlbum) + album.POST(officialWebsitePathCreate, officialWebsitectrl.CreateAlbum) + album.PUT(officialWebsitePathUpdate, officialWebsitectrl.UpdateAlbum) + album.DELETE(officialWebsitePathDelete, officialWebsitectrl.DeleteAlbum) + } + + video := g.Group("/video") + { + video.GET(officialWebsitePathList, officialWebsitectrl.ListVideo) + video.POST(officialWebsitePathCreate, officialWebsitectrl.CreateVideo) + video.PUT(officialWebsitePathUpdate, officialWebsitectrl.UpdateVideo) + video.DELETE(officialWebsitePathDelete, officialWebsitectrl.DeleteVideo) + video.GET("/listFromVid", officialWebsitectrl.ListVideoFromVid) + video.PUT("/batch", officialWebsitectrl.BatchUpdateVideos) + video.PUT("/import", officialWebsitectrl.BatchImportVideos) + } + + photograph := g.Group("/photograph") + { + photograph.GET(officialWebsitePathList, officialWebsitectrl.ListPhotograph) + photograph.POST(officialWebsitePathCreate, officialWebsitectrl.CreatePhotograph) + photograph.PUT(officialWebsitePathUpdate, officialWebsitectrl.UpdatePhotograph) + photograph.DELETE(officialWebsitePathDelete, officialWebsitectrl.DeletePhotograph) + photograph.PUT("/batch", officialWebsitectrl.BatchUpdatePhotograph) + } + + tag := g.Group("/tag") + { + tag.GET(officialWebsitePathList, officialWebsitectrl.ListTag) + tag.POST(officialWebsitePathCreate, officialWebsitectrl.CreateTag) + tag.PUT(officialWebsitePathUpdate, officialWebsitectrl.UpdateTag) + tag.DELETE(officialWebsitePathDelete, officialWebsitectrl.DeleteTag) + } + + recruitForm := g.Group("/recruitForm") + { + recruitForm.GET(officialWebsitePathList, officialWebsitectrl.ListRecruitForm) + recruitForm.PUT(officialWebsitePathUpdate, officialWebsitectrl.UpdateRecruitForm) + recruitForm.DELETE(officialWebsitePathDelete, officialWebsitectrl.DeleteRecruitForm) + } + + jobList := g.Group("/jobList") + { + jobList.GET(officialWebsitePathList, officialWebsitectrl.ListJobList) + jobList.POST(officialWebsitePathCreate, officialWebsitectrl.CreateJobList) + jobList.PUT(officialWebsitePathUpdate, officialWebsitectrl.UpdateJobList) + jobList.DELETE(officialWebsitePathDelete, officialWebsitectrl.DeleteJobList) + } + + news := g.Group("/news") + { + news.GET(officialWebsitePathList, officialWebsitectrl.ListNews) + news.POST(officialWebsitePathCreate, officialWebsitectrl.CreateNews) + news.PUT(officialWebsitePathUpdate, officialWebsitectrl.UpdateNews) + news.DELETE(officialWebsitePathDelete, officialWebsitectrl.DeleteNews) + } + + partner := g.Group("/partner") + { + partner.GET(officialWebsitePathList, officialWebsitectrl.ListPartner) + partner.POST(officialWebsitePathCreate, officialWebsitectrl.CreatePartner) + partner.PUT(officialWebsitePathUpdate, officialWebsitectrl.UpdatePartner) + partner.DELETE(officialWebsitePathDelete, officialWebsitectrl.DeletePartner) + } + + business := g.Group("/business") + { + business.GET(officialWebsitePathList, officialWebsitectrl.ListBusiness) + business.POST(officialWebsitePathCreate, officialWebsitectrl.CreateBusiness) + business.PUT(officialWebsitePathUpdate, officialWebsitectrl.UpdateBusiness) + business.DELETE(officialWebsitePathDelete, officialWebsitectrl.DeleteBusiness) + } + } +} diff --git a/web/router/operationlog.go b/web/router/operationlog.go new file mode 100644 index 0000000..c51d548 --- /dev/null +++ b/web/router/operationlog.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/web/api/operationlogctrl" + + "github.com/gin-gonic/gin" +) + +// 帖子、用户操作日志 +func operationLogRouter(router *gin.RouterGroup) { + group := router.Group("/operation/log") + { + group.GET("list", operationlogctrl.OperationLogList) + } +} diff --git a/web/router/payment_guide.go b/web/router/payment_guide.go new file mode 100644 index 0000000..e38624f --- /dev/null +++ b/web/router/payment_guide.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/paymentguidectrl" + + "github.com/gin-gonic/gin" +) + +func paymentGuideRouter(router *gin.RouterGroup) { + group := router.Group("/payment-guide") + { + group.GET("/list", paymentguidectrl.List) + group.POST("/add", paymentguidectrl.Add) + group.POST("/edit", paymentguidectrl.Edit) + group.DELETE("/delete", paymentguidectrl.Delete) + } +} diff --git a/web/router/product.go b/web/router/product.go new file mode 100644 index 0000000..8776c7b --- /dev/null +++ b/web/router/product.go @@ -0,0 +1,33 @@ +package router + +import ( + "91porn-server/web/api/productctrl" + "91porn-server/web/middleware/limitHandler" + + "github.com/gin-gonic/gin" +) + +func productRouter(router *gin.RouterGroup) { + group := router.Group("/business") + { + //商品管理 + group.GET("/product/list", productctrl.FindVipList) + group.POST("/product/update", productctrl.Update) + group.POST("/product", productctrl.Insert) + group.DELETE("/product", productctrl.Delete) + group.GET("/getConfig", productctrl.GetConfig) //获取配置 + group.POST("/setConfig", productctrl.SetConfig) //修改配置 + group.GET("/product/checkbox", productctrl.CheckboxList) + //商品购买订单 + group.GET("/product/order", productctrl.ProductLog) + group.POST("/product/order/export", limitHandler.FilterRequestByExport(), productctrl.ProductLogExport) + position := group.Group("/productPosition") + { + position.POST("/list", productctrl.GetPosition) + position.POST("/names", productctrl.GetPositionNames) + position.POST("/add", productctrl.InsertPosition) + position.POST("/edit", productctrl.UpdatePosition) + position.POST("/del", productctrl.DeletePosition) + } + } +} diff --git a/web/router/productBenefit.go b/web/router/productBenefit.go new file mode 100644 index 0000000..ae9da83 --- /dev/null +++ b/web/router/productBenefit.go @@ -0,0 +1,19 @@ +package router + +import ( + "91porn-server/web/api/productBenefitctrl" + + "github.com/gin-gonic/gin" +) + +func productBenefitRouter(router *gin.RouterGroup) { + group := router.Group("/productBenefit") + { + //商品管理 + group.GET("/list", productBenefitctrl.GetProductBenefitList) + group.POST("/update", productBenefitctrl.UpdateProductBenefit) + group.POST("/add", productBenefitctrl.AddProductBenefit) + group.DELETE("/del", productBenefitctrl.DeleteProductBenefit) + group.GET("/all/list", productBenefitctrl.GetAllProductBenefitList) + } +} diff --git a/web/router/promoteurl.go b/web/router/promoteurl.go new file mode 100644 index 0000000..31fdda8 --- /dev/null +++ b/web/router/promoteurl.go @@ -0,0 +1,18 @@ +package router + +import ( + "91porn-server/web/api/promoteurlctrl" + + "github.com/gin-gonic/gin" +) + +func promoteUrlRouter(router *gin.RouterGroup) { + group := router.Group("/promoteUrl") + { + //商品管理 + group.GET("/list", promoteurlctrl.FindMany) + group.POST("/update", promoteurlctrl.Update) + group.POST("/insert", promoteurlctrl.Insert) + group.DELETE("/delete", promoteurlctrl.Delete) + } +} diff --git a/web/router/proxy.go b/web/router/proxy.go new file mode 100644 index 0000000..adabd07 --- /dev/null +++ b/web/router/proxy.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/proxyctrl" + + "github.com/gin-gonic/gin" +) + +func proxyRouter(router *gin.RouterGroup) { + group := router.Group("/proxy") + { + group.GET("/log", proxyctrl.ProxyLog) + group.GET("/totalIncome", proxyctrl.AllIncome) + group.GET("/income/details", proxyctrl.IncomeDetails) + group.GET("/promotion", proxyctrl.PromotionQuery) + } +} diff --git a/web/router/quicksearch.go b/web/router/quicksearch.go new file mode 100755 index 0000000..2396062 --- /dev/null +++ b/web/router/quicksearch.go @@ -0,0 +1,18 @@ +package router + +import ( + "91porn-server/web/api/quicksearchctrl" + + "github.com/gin-gonic/gin" +) + +// quickSearchRouter 快捷搜索表管理 +func quickSearchRouter(router *gin.RouterGroup) { + group := router.Group("/quickSearch") + { + group.GET("/list", quicksearchctrl.List) + group.POST("/create", quicksearchctrl.Create) + group.POST("/update", quicksearchctrl.Update) + group.POST("/delete", quicksearchctrl.Delete) + } +} diff --git a/web/router/recharge.go b/web/router/recharge.go new file mode 100644 index 0000000..18b5d03 --- /dev/null +++ b/web/router/recharge.go @@ -0,0 +1,31 @@ +package router + +import ( + "91porn-server/web/api/rechargectrl" + + "github.com/gin-gonic/gin" +) + +func rechargeRouter(router *gin.RouterGroup) { + group := router.Group("/recharge") + { + //支付渠道 + group.GET("/channel/list", rechargectrl.GetChannel) + group.POST("/channel/update", rechargectrl.ChannelUpdate) + group.POST("/channel/add", rechargectrl.ChannelInsert) + group.DELETE("/channel/delete", rechargectrl.ChannelDelete) + //金币配置 + group.GET("/gold/list", rechargectrl.GetGoldList) + group.POST("/gold/update", rechargectrl.GoldUpdate) + group.POST("/gold/add", rechargectrl.GoldInsert) + group.DELETE("/gold/delete", rechargectrl.GoldDelete) + //充值订单 + group.GET("/order/list", rechargectrl.OrderList) + group.POST("/order/update", rechargectrl.UpdateOrder) + //游戏金币配置 + group.GET("/game/gold/list", rechargectrl.GetGameGoldList) + group.POST("/game/gold/update", rechargectrl.GameGoldUpdate) + group.POST("/game/gold/add", rechargectrl.GameGoldInsert) + group.DELETE("/game/gold/delete", rechargectrl.GameGoldDelete) + } +} diff --git a/web/router/rejectTemp.go b/web/router/rejectTemp.go new file mode 100644 index 0000000..19a2971 --- /dev/null +++ b/web/router/rejectTemp.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/vidctrl" + + "github.com/gin-gonic/gin" +) + +func rejectTemp(router *gin.RouterGroup) { + r := router.Group("/rejectTemp") + { + r.POST("/insert", vidctrl.RejectTemplateInsert) + r.POST("/update", vidctrl.RejectTemplateUpdate) + r.POST("/del", vidctrl.RejectTemplateDelete) + r.GET("/list", vidctrl.GetRejectTemplate) + } +} diff --git a/web/router/scene_banner.go b/web/router/scene_banner.go new file mode 100644 index 0000000..496f767 --- /dev/null +++ b/web/router/scene_banner.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/scenebannerctrl" + + "github.com/gin-gonic/gin" +) + +func sceneBannerRouter(router *gin.RouterGroup) { + group := router.Group("/banner") + { + group.GET("/list", scenebannerctrl.List) + group.POST("/add", scenebannerctrl.Add) + group.POST("/edit", scenebannerctrl.Edit) + group.DELETE("/delete", scenebannerctrl.Delete) + } +} diff --git a/web/router/sensitive_word.go b/web/router/sensitive_word.go new file mode 100644 index 0000000..a29f55e --- /dev/null +++ b/web/router/sensitive_word.go @@ -0,0 +1,20 @@ +package router + +import ( + "91porn-server/web/api/sensitivewordctrl" + + "github.com/gin-gonic/gin" +) + +func sensitiveWordRouter(router *gin.RouterGroup) { + group := router.Group("/sensitive-word") + { + group.GET("/list", sensitivewordctrl.List) + group.POST("/add", sensitivewordctrl.Add) + group.POST("/update", sensitivewordctrl.Update) + group.POST("/delete", sensitivewordctrl.Delete) + group.POST("/import", sensitivewordctrl.Import) + group.GET("/export", sensitivewordctrl.Export) + group.GET("/template", sensitivewordctrl.Template) + } +} diff --git a/web/router/sign.go b/web/router/sign.go new file mode 100644 index 0000000..adf2c6b --- /dev/null +++ b/web/router/sign.go @@ -0,0 +1,37 @@ +package router + +import ( + "91porn-server/web/api/signctrl" + + "github.com/gin-gonic/gin" +) + +func taskRouter(router *gin.RouterGroup) { + task := router.Group("/task") + { + config := task.Group("/growth") + { + config.POST("/add", signctrl.AddConfig) + config.POST("/edit", signctrl.ModifyConfig) + config.GET("/list", signctrl.QueryAllConfig) + } + once := task.Group("/once") + { + once.GET("/list", signctrl.OnceTaskList) + once.POST("/add", signctrl.AddOnceTask) + once.POST("/edit", signctrl.EditOnceTask) + } + daily := task.Group("/daily") + { + daily.GET("/list", signctrl.DailyTaskList) + daily.POST("/add", signctrl.AddDailyTask) + daily.POST("/edit", signctrl.EditDailyTask) + } + sign := task.Group("/sign") + { + sign.POST("/add", signctrl.SignAddConfig) + sign.POST("/edit", signctrl.SignModifyConfig) + sign.GET("/list", signctrl.SignQueryAllConfig) + } + } +} diff --git a/web/router/source.go b/web/router/source.go new file mode 100644 index 0000000..0dc088b --- /dev/null +++ b/web/router/source.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/sourcectrl" + + "github.com/gin-gonic/gin" +) + +func sourceRouter(router *gin.RouterGroup) { + group := router.Group("/source") + { + group.GET("/list", sourcectrl.SourceList) + group.POST("/add", sourcectrl.SourceAdd) + group.DELETE("/del", sourcectrl.SourceDel) + group.POST("/edit", sourcectrl.SourceEdit) + } +} diff --git a/web/router/stat.go b/web/router/stat.go new file mode 100644 index 0000000..0540182 --- /dev/null +++ b/web/router/stat.go @@ -0,0 +1,27 @@ +package router + +import ( + "91porn-server/web/api/statctrl" + "github.com/gin-gonic/gin" +) + +func statRouter(router *gin.RouterGroup) { + group := router.Group("/stats") + { + group.GET("/user/daily/list", statctrl.DailyStats) //用户每日统计 + group.GET("/video/earn/list", statctrl.VideoEarnStats) + group.GET("/video/earn/aggregate", statctrl.VideoAggregateStats) + group.GET("/video/publisher/deduction", statctrl.VideoPublisherDeductionStats) + order := group.Group("/order") + { + order.GET("/list", statctrl.OrderStats) //充值和兑换订单统计 + order.POST("/sucRate", statctrl.RchSucRate) //充值和兑换订单统计 + } + export := group.Group("/export") + { + export.GET("/trans", statctrl.DataStat) //用户每日统计 + export.GET("/vidsales", statctrl.VidStat) //用户每日统计 + + } + } +} diff --git a/web/router/sync.go b/web/router/sync.go new file mode 100644 index 0000000..4a87a21 --- /dev/null +++ b/web/router/sync.go @@ -0,0 +1,32 @@ +package router + +import ( + "91porn-server/web/api/statcenterctl" + + "github.com/gin-gonic/gin" +) + +// syncRouter 数据同步 +func syncRouter(router *gin.RouterGroup) { + routerGroup := router.Group("/router") + { + // 返回所有同步接口路由 + routerGroup.GET("/all", statcenterctl.SyncAllRouterList) + } + userGroup := router.Group("/user") + { + // 新用户 + userGroup.GET("/newUser", statcenterctl.NewUserRegisterList) + // 用户访问 + userGroup.GET("/userAccess", statcenterctl.NewUserAccessList) + // 邀请 + userGroup.GET("/inviteRecord", statcenterctl.NewUserInviterList) + } + orderGroup := router.Group("/order") + { + // 充值订单,所有的 + orderGroup.GET("/newOrder", statcenterctl.NewUserRechargeList) + // 充值订单,已完成的 + orderGroup.GET("/finishOrder", statcenterctl.NewUserPaidRechargeList) + } +} diff --git a/web/router/synccdn.go b/web/router/synccdn.go new file mode 100644 index 0000000..07a5fee --- /dev/null +++ b/web/router/synccdn.go @@ -0,0 +1,15 @@ +package router + +import ( + "91porn-server/web/api/synccdnctrl" + + "github.com/gin-gonic/gin" +) + +func syncCdnRouter(router *gin.RouterGroup) { + group := router.Group("/cdn") + { + group.POST("/sync", synccdnctrl.SyncCdn) + group.POST("/syncWithOptions", synccdnctrl.SyncWithOptions) + } +} diff --git a/web/router/sysconf.go b/web/router/sysconf.go new file mode 100755 index 0000000..d214e22 --- /dev/null +++ b/web/router/sysconf.go @@ -0,0 +1,19 @@ +package router + +import ( + "91porn-server/web/api/sysconfctrl" + + "github.com/gin-gonic/gin" +) + +// sysConfRouter 系统通用配置管理 +func sysConfRouter(router *gin.RouterGroup) { + group := router.Group("/sysconf") + { + group.GET("/all", sysconfctrl.All) + group.POST("/create", sysconfctrl.Create) + group.POST("/update", sysconfctrl.Update) + group.POST("/delete", sysconfctrl.Delete) + group.POST("/update/select", sysconfctrl.UpdateSelect) + } +} diff --git a/web/router/tag.go b/web/router/tag.go new file mode 100644 index 0000000..31557d2 --- /dev/null +++ b/web/router/tag.go @@ -0,0 +1,32 @@ +package router + +import ( + "91porn-server/web/api/tagctrl" + + "github.com/gin-gonic/gin" +) + +func tagRouter(router *gin.RouterGroup) { + group := router.Group("/tag") + { + group.GET("/list", tagctrl.List) + group.GET("/pure/list", tagctrl.PureList) + group.POST("/update", tagctrl.Update) + group.POST("/add", tagctrl.Add) + group.DELETE("/delete", tagctrl.Delete) + tagConf := group.Group("/conf") + { + tagConf.GET("", tagctrl.TagConfList) + tagConf.POST("/add", tagctrl.AddTagConf) + tagConf.POST("/delete", tagctrl.DeleteTagConf) + tagConf.POST("/update", tagctrl.UpdateTagConf) + } + } + recmd := router.Group("/recmd") + { + recmd.GET("/list", tagctrl.RecmdList) + recmd.POST("/add", tagctrl.RecmdAdd) + recmd.POST("/update", tagctrl.RecmdUpdate) + recmd.DELETE("/delete", tagctrl.RecmdDelete) + } +} diff --git a/web/router/tone.go b/web/router/tone.go new file mode 100644 index 0000000..8546fb0 --- /dev/null +++ b/web/router/tone.go @@ -0,0 +1,21 @@ +package router + +import ( + "91porn-server/web/api/tonectrl" + + "github.com/gin-gonic/gin" +) + +func toneRoute(router *gin.RouterGroup) { + group := router.Group("/vid/tone") + { + group.GET("/list", tonectrl.List) + group.POST("/update", tonectrl.Update) + } + group = router.Group("/api/web/vid/tone/offi_recom") + { + group.GET("/list", tonectrl.OffiRecomList) + group.POST("/add", tonectrl.OffiRecomAdd) + group.DELETE("/delete", tonectrl.OffiRecomDelete) + } +} diff --git a/web/router/updownLoad.go b/web/router/updownLoad.go new file mode 100644 index 0000000..6b7fd5c --- /dev/null +++ b/web/router/updownLoad.go @@ -0,0 +1,31 @@ +package router + +import ( + "91porn-server/web/api/updownloadctrl" + + "github.com/gin-gonic/gin" +) + +func updownloadRouter(router *gin.RouterGroup) { + group := router.Group("/vid") + { + group.POST("/upload", updownloadctrl.Upload) + group.POST("/uploadStatic", updownloadctrl.UploadStatic) + group.POST("/uploadStaticBatch", updownloadctrl.UploadStaticBatch) + group.POST("/uploadVideo", updownloadctrl.UploadVideo) + group.POST("/uploadDotStream", updownloadctrl.UploadDotStream) + group.POST("/uploadDotJson", updownloadctrl.UploadDotJson) + group.POST("/submit", updownloadctrl.Submit) + group.POST("/pullFileInfoFromAws", updownloadctrl.PullFileInfoFromAws) + group.GET("/m3u8/*source", updownloadctrl.DownloadM3u8) + group.StaticFile("/sec", "config/enc.key") + group.StaticFile("/sp/sec", "config/enc.key") + group.StaticFile("/pms/sec", "config/enc_pms.key") + group.StaticFile("/pms/mt_sec", "config/enkeymt.key") + group.StaticFile("/sp/lsjsec", "config/lsjm3u8.key") + group.StaticFile("/sp/m3u8sec", "config/m3u8.key") + group.POST("/syncNewVideo", updownloadctrl.SyncNewVideo) + group.POST("/spiderSyncSubmit", updownloadctrl.SpiderSyncSubmit) + group.POST("/uploadSuccess", updownloadctrl.UploadVideoSuccess) + } +} diff --git a/web/router/user.go b/web/router/user.go new file mode 100644 index 0000000..3738dfb --- /dev/null +++ b/web/router/user.go @@ -0,0 +1,72 @@ +package router + +import ( + "91porn-server/web/api/chat_message_ctrl" + "91porn-server/web/api/userctrl" + + "github.com/gin-gonic/gin" +) + +func userRouter(router *gin.RouterGroup) { + group := router.Group("/user") + { + group.GET("/list", userctrl.List) + group.GET("/vipInfo", userctrl.VipInfo) + group.POST("/update", userctrl.Update) + group.POST("/banned", userctrl.Banned) + group.POST("/advertiser", userctrl.SetAdvertiser) + group.POST("/forbid", userctrl.Forbid) + group.POST("/lock", userctrl.Lock) + group.POST("/devID/reset", userctrl.DevIDReset) + //group.GET("/record/play", userctrl.PlayRecord) + group.GET("/login/list", userctrl.LoginList) + group.GET("/play/list", userctrl.PlayList) + group.GET("/report/list", userctrl.ReportList) + group.GET("/report/types/list", userctrl.ReportTypesList) + group.POST("/report/setStatus", userctrl.SetStatus) + group.GET("/reco/list", userctrl.GetUserRecoList) + group.POST("/reco/modify", userctrl.ModifyUserReco) + group.POST("/reco/add", userctrl.AddUserReco) + group.POST("/reco/del", userctrl.DelUserReco) + group.POST("/credit", userctrl.CreditAmount) //用户增减金币 + group.POST("/integral", userctrl.CreditIntegral) //用户增减积分 + group.POST("/credit/fruitCoin", userctrl.FruitCoinChange) //管理员操作用户果币变更 + group.POST("/credit/aiMateBalance", userctrl.SetAiMateBalance) //管理员操作用户ai伴侣币变更 + group.POST("/credit/gold", userctrl.CreditGold) //用户扣除金币、收益 + group.POST("qr_code", userctrl.GenerateQRCode) + group.POST("/update/freeTimes", userctrl.UpdateFreeTimes) + group.POST("/update/download", userctrl.UpdateDownload) + group.POST("/update/lotteryTimes", userctrl.UpdateLotteryTimes) + group.POST("/deleteComment", userctrl.DeleteComment) + group.GET("/queryGameCode", userctrl.QueryGameCode) + group.POST("/privateZone", userctrl.PrivateZone) + } + faqRouter := group.Group("/faq") + { + faqRouter.POST("/add", userctrl.FaqAdd) + faqRouter.GET("/list", userctrl.FaqList) + faqRouter.POST("/update", userctrl.FaqUpdate) + faqRouter.DELETE("/del", userctrl.FaqDelete) + faqRouter.GET("/cate/list", userctrl.FaqCateList) + } + feedbackRouter := group.Group("/feedback") + { + feedbackRouter.GET("/list", userctrl.FeedbackList) + feedbackRouter.POST("/update", userctrl.FeedbackUpdate) + feedbackRouter.DELETE("/del", userctrl.FeedbackDelete) + } + resourceRouter := group.Group("/resource") + { + resourceRouter.GET("/list", userctrl.GetUserResourceList) + resourceRouter.POST("/add", userctrl.AddUserResource) + resourceRouter.POST("/update", userctrl.ModifyUserResource) + resourceRouter.POST("/del", userctrl.DelUserResource) + } + chatMessageRouter := group.Group("/chatMessage") + { + chatMessageRouter.GET("/list", chat_message_ctrl.List) + chatMessageRouter.POST("/del", chat_message_ctrl.Del) + chatMessageRouter.POST("/send", chat_message_ctrl.Send) + + } +} diff --git a/web/router/version.go b/web/router/version.go new file mode 100644 index 0000000..b1f855d --- /dev/null +++ b/web/router/version.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/web/api/versionctrl" + + "github.com/gin-gonic/gin" +) + +func versionRouter(router *gin.RouterGroup) { + { + router.GET("/ver/list", versionctrl.FindList) + router.POST("/ver/update", versionctrl.Update) + router.POST("/ver", versionctrl.Insert) + router.DELETE("/ver", versionctrl.Delete) + } +} diff --git a/web/router/vid.go b/web/router/vid.go new file mode 100644 index 0000000..555d68b --- /dev/null +++ b/web/router/vid.go @@ -0,0 +1,58 @@ +package router + +import ( + "91porn-server/web/api/vidctrl" + "91porn-server/web/middleware/limitHandler" + + "github.com/gin-gonic/gin" +) + +func vidRouter(router *gin.RouterGroup) { + group := router.Group("/vid") + { + group.GET("/list", vidctrl.GetVidList) + group.POST("/export", limitHandler.FilterRequestByExport(), vidctrl.ExportVidList) + group.POST("/listDetail", vidctrl.ListDetail) + group.POST("/update", vidctrl.UpdateVidInfo) + group.DELETE("/del", vidctrl.DeleteVids) + group.POST("/batch/update", vidctrl.UpdateVids) + group.POST("/batch/pass", vidctrl.PassVid) + group.GET("/view/:id", vidctrl.ViewVid) + group.POST("/push", vidctrl.PushVid) + group.GET("/push/list", vidctrl.PushVidList) + group.POST("/push/delete", vidctrl.DeletePushVid) + group.POST("/batch/edit", vidctrl.PassAndEditVids) + group.POST("/qr_code") + group.POST("batch/reco", vidctrl.BatchUpdateReco) + group.POST("/popularity/config", vidctrl.ConfigPopularity) + group.GET("/popularity/config", vidctrl.GetPopularityConfig) + group.POST("/reindex", vidctrl.Reindex) + group.POST("/search", vidctrl.Search) + group.POST("/batch/newsType/update", vidctrl.BatchUpdateVidNewsType) + group.POST("/batch/update/tags", vidctrl.UpdateVidTags) + + // 定时上线 + group.POST("/timeOnline/batchAudit", vidctrl.BatchAudit) + group.GET("/timeOnline/list", vidctrl.GetTimeOnlineVidList) + group.POST("/timeOnline/del", vidctrl.DelVidTimeOnlines) + group.POST("/timeOnline/edit", vidctrl.EditVidTimeOnlineInfo) + + } + mediaR := router.Group("/mediaResource") + { + mediaR.GET("/list", vidctrl.GetMediaList) + mediaR.POST("/use", vidctrl.UseMedia) + mediaR.POST("/ignore", vidctrl.IgnoreMedia) + } + // 折扣专区相关 + discountArea := router.Group("/discount_area") + { + discountArea.GET("/list", vidctrl.DiscountAreaList) + discountArea.POST("/add", vidctrl.DiscountAreaAdd) + discountArea.POST("/update", vidctrl.DiscountAreaUpdate) + discountArea.POST("/delete", vidctrl.DiscountAreaDelete) + discountArea.GET("/vid/list", vidctrl.DiscountAreaVids) + discountArea.POST("/vid/add", vidctrl.DiscountAreaAddVid) + discountArea.POST("/vid/delete", vidctrl.DiscountAreaDeleteVid) + } +} diff --git a/web/router/video_activity.go b/web/router/video_activity.go new file mode 100644 index 0000000..22dd2a5 --- /dev/null +++ b/web/router/video_activity.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/video_activity_ctrl" + + "github.com/gin-gonic/gin" +) + +func videoActivityRouter(router *gin.RouterGroup) { + group := router.Group("/video_activity") + { + group.POST("", video_activity_ctrl.Submit) + group.PUT("", video_activity_ctrl.Edit) + group.GET("", video_activity_ctrl.QueryAll) + group.GET("/box", video_activity_ctrl.Box) + } +} diff --git a/web/router/video_gold_coin.go b/web/router/video_gold_coin.go new file mode 100644 index 0000000..8294ca9 --- /dev/null +++ b/web/router/video_gold_coin.go @@ -0,0 +1,16 @@ +package router + +import ( + "91porn-server/web/api/video_gold_coin_ctrl" + "github.com/gin-gonic/gin" +) + +func videoGoldCoinRouter(router *gin.RouterGroup) { + group := router.Group("/video_gold_coin") + { + group.PUT("", video_gold_coin_ctrl.Update) + group.POST("", video_gold_coin_ctrl.Add) + group.GET("", video_gold_coin_ctrl.QueryAll) + group.DELETE("", video_gold_coin_ctrl.Delete) + } +} diff --git a/web/router/vip_card_experiment.go b/web/router/vip_card_experiment.go new file mode 100644 index 0000000..72544d4 --- /dev/null +++ b/web/router/vip_card_experiment.go @@ -0,0 +1,17 @@ +package router + +import ( + "91porn-server/web/api/vipcardexperimentctrl" + + "github.com/gin-gonic/gin" +) + +func vipCardExperimentRouter(router *gin.RouterGroup) { + group := router.Group("/vip-card-experiment") + { + group.GET("/current", vipcardexperimentctrl.Current) + group.POST("/publish", vipcardexperimentctrl.Publish) + group.POST("/disable", vipcardexperimentctrl.Disable) + group.GET("/statistics", vipcardexperimentctrl.Statistics) + } +} diff --git a/web/router/withdraw.go b/web/router/withdraw.go new file mode 100644 index 0000000..007ff2e --- /dev/null +++ b/web/router/withdraw.go @@ -0,0 +1,35 @@ +package router + +import ( + "91porn-server/web/api/withdrawctrl" + + "github.com/gin-gonic/gin" +) + +func withdrawRouter(router *gin.RouterGroup) { + { + //商品管理 + router.POST("/withdraw/check", withdrawctrl.Check) + router.POST("/product/refuse", withdrawctrl.Refuse) + router.GET("/withdraw/list", withdrawctrl.ExchgOrder) + router.POST("/withdraw/refund", withdrawctrl.Refund) + + router.GET("/withdraw/refund/list", withdrawctrl.RefundList) + router.POST("/withdraw/refund/dealwith", withdrawctrl.RefundDealWith) + } + group := router.Group("/withdraw") + { + //支付渠道 + group.GET("/channel/list", withdrawctrl.GetChannel) + group.POST("/channel/update", withdrawctrl.ChannelUpdate) + group.POST("/channel/add", withdrawctrl.ChannelInsert) + group.DELETE("/channel/delete", withdrawctrl.ChannelDelete) + group.POST("/channel/batch", withdrawctrl.ChannelBatch) + } + txnact := router.Group("/txnact") + { + txnact.POST("/list", withdrawctrl.FindAct) + txnact.POST("/edit", withdrawctrl.EditAct) + txnact.POST("/del", withdrawctrl.DelAct) + } +} diff --git a/web/service/activityser/activity.go b/web/service/activityser/activity.go new file mode 100644 index 0000000..86149c2 --- /dev/null +++ b/web/service/activityser/activity.go @@ -0,0 +1,109 @@ +package activityser + +import ( + "fmt" + "time" + + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/activitymod" + "91porn-server/web/webg" +) + +// 新增活动 +func Add(in activitymod.AddCond) stderr.Code { + result, err := activitymod.Add(in) + if err != nil { + return stderr.ErrDbInsertError + } + log.Info(fmt.Sprintf("新增活动[%v]成功", result)) + return stderr.Success +} + +// 修改活动 +func Modify(in *activitymod.ModifyCond) stderr.Code { + var jobStatus bool + now := time.Now() + // 获取活动信息 + activityDetail, err := activitymod.Get(in.ID) + if err != nil { + return stderr.ErrDbQueryError + } + // 活动开启限制 + if in.Status != nil { + // 时间限制 + if *in.Status && (in.StartTime.After(now) || !in.EndTime.After(now)) { + return stderr.ActivityDateError + } + // 开启/关闭任务 + if (*in.Status && !activityDetail.Status) || (!*in.Status && activityDetail.Status) { + jobStatus = true + } + } + if err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + if err = activitymod.Modify(t, in.Cond(), in.Bson()); err != nil { + return stderr.ErrDbUpdateError + } + if jobStatus { + if err = OpenBotDraw(in.ID, *in.Status); err != nil { + return err + } + } + return nil + }); err != nil { + return stderr.ErrDbTransError + } + return stderr.Success +} + +// 查询列表 +func QueryAll(in *activitymod.QueryAllCond) (interface{}, stderr.Code) { + var data map[string]interface{} = map[string]interface{}{ + "list": []interface{}{}, + "count": 0, + } + count, err := activitymod.Count(in.Query()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + if count == 0 { + return data, stderr.Success + } + list, err := activitymod.QueryAll(in.Query(), in.Options()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + data["count"] = count + data["list"] = list + return data, stderr.Success +} + +func QueryDropDownBox() (interface{}, stderr.Code) { + list, err := activitymod.QueryDropDownBox() + if err != nil { + return nil, stderr.ErrDbQueryError + } + data := make([]map[string]interface{}, len(list)) + for i, v := range list { + data[i] = map[string]interface{}{ + "label": v.Title, + "value": v.ID.Hex(), + } + } + return data, stderr.Success +} + +func LoadingJob() error { + // 获取所有开启中活动 + activityList, err := activitymod.QueryDropDownBox() + if err != nil { + return err + } + for _, v := range activityList { + if err = OpenBotDraw(v.ID, v.Status); err != nil { + return err + } + } + return nil +} diff --git a/web/service/activityser/cron.go b/web/service/activityser/cron.go new file mode 100644 index 0000000..a6400f8 --- /dev/null +++ b/web/service/activityser/cron.go @@ -0,0 +1,131 @@ +package activityser + +import ( + "fmt" + "math/rand" + "sync" + "time" + + "91porn-server/common/log" + "91porn-server/models/v/activitymod" + "91porn-server/models/v/prizemod" + + "github.com/robfig/cron/v3" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var job JobCron + +type JobCron struct { + cron *cron.Cron + cache map[primitive.ObjectID]cron.EntryID + mutex sync.Mutex +} + +func init() { + job = JobCron{ + cron: cron.New(cron.WithSeconds()), + cache: make(map[primitive.ObjectID]cron.EntryID, 1), + } + job.cron.Start() +} + +type BotDraw struct { + ActivityID primitive.ObjectID +} + +func (b BotDraw) Run() { + var now time.Time = time.Now() + // 随机种子 + randInt := rand.New(rand.NewSource(time.Now().UnixNano())) + // 随机获取n个马甲号 + names := GetFullName(randInt) + // 获取大奖列表 + prizes, err := prizemod.GetHighPrizeListByActivityID(b.ActivityID) + if err != nil { + log.Error(fmt.Sprintf("获取大奖列表失败 [%v]", err)) + return + } + // 获取奖池信息 + prizePool, err := activitymod.GetPrizePool(b.ActivityID) + if err != nil { + log.Error(fmt.Sprintf("获取奖池信息失败 [%v]", err)) + return + } + // 增加抽奖记录 + records := make([]activitymod.UserRecord, len(names)) + for i, v := range names { + p := prizes[randInt.Intn(len(prizes))] + records[i] = activitymod.UserRecord{ + ActivityID: b.ActivityID, + VIPCardID: p.VIPCardID, + UserName: v, + Name: p.Name, + Type: p.Type, + Price: p.Price, + Count: p.Count, + Value: p.Value, + Level: p.Level, + DrawPrice: prizePool.Price, + CreateTime: now.Add(-time.Duration(randInt.Int63n(3600)) * time.Second), + } + } + // 插入中奖信息 + if err := activitymod.AddMany(records); err != nil { + log.Error(fmt.Sprintf("插入中奖信息失败 [%v]", err)) + return + } +} + +func GetFullName(randInt *rand.Rand) []string { + var lastName = []string{ + "赵", "钱", "孙", "李", "周", "吴", "郑", "王", "冯", "陈", "褚", "卫", "蒋", + "沈", "韩", "杨", "朱", "秦", "尤", "许", "何", "吕", "施", "张", "孔", "曹", "严", "华", "金", "魏", + "陶", "姜", "戚", "谢", "邹", "喻", "柏", "水", "窦", "章", "云", "苏", "潘", "葛", "奚", "范", "彭", + "郎", "鲁", "韦", "昌", "马", "苗", "凤", "花", "方", "任", "袁", "柳", "鲍", "史", "唐", "费", "薛", + "雷", "贺", "倪", "汤", "滕", "殷", "罗", "毕", "郝", "安", "常", "傅", "卞", "齐", "元", "顾", "孟", + "平", "黄", "穆", "萧", "尹", "姚", "邵", "湛", "汪", "祁", "毛", "狄", "米", "伏", "成", "戴", "谈", + "宋", "茅", "庞", "熊", "纪", "舒", "屈", "项", "祝", "董", "梁", "杜", "阮", "蓝", "闵", "季", "贾", + "路", "娄", "江", "童", "颜", "郭", "梅", "盛", "林", "钟", "徐", "邱", "骆", "高", "夏", "蔡", "田", + "樊", "胡", "凌", "霍", "虞", "万", "支", "柯", "管", "卢", "莫", "柯", "房", "裘", "缪", "解", "应", + "宗", "丁", "宣", "邓", "单", "杭", "洪", "包", "诸", "左", "石", "崔", "吉", "龚", "程", "嵇", "邢", + "裴", "陆", "荣", "翁", "荀", "于", "惠", "甄", "曲", "封", "储", "仲", "伊", "宁", "仇", "甘", "武", + "符", "刘", "景", "詹", "龙", "叶", "幸", "司", "黎", "溥", "印", "怀", "蒲", "邰", "从", "索", "赖", + "卓", "屠", "池", "乔", "胥", "闻", "莘", "党", "翟", "谭", "贡", "劳", "逄", "姬", "申", "扶", "堵", + "冉", "宰", "雍", "桑", "寿", "通", "燕", "浦", "尚", "农", "温", "别", "庄", "晏", "柴", "瞿", "阎", + "连", "习", "容", "向", "古", "易", "廖", "庾", "终", "步", "都", "耿", "满", "弘", "匡", "国", "文", + "寇", "广", "禄", "阙", "东", "欧", "利", "师", "巩", "聂", "关", "荆", "司马", "上官", "欧阳", "夏侯", + "诸葛", "闻人", "东方", "赫连", "皇甫", "尉迟", "公羊", "澹台", "公冶", "宗政", "濮阳", "淳于", "单于", + "太叔", "申屠", "公孙", "仲孙", "轩辕", "令狐", "徐离", "宇文", "长孙", "慕容", "司徒", "司空"} + var firstName = []string{ + "伟", "刚", "勇", "毅", "俊", "峰", "强", "军", "平", "保", "东", "文", "辉", "力", "明", "永", "健", "世", "广", "志", "义", + "兴", "良", "海", "山", "仁", "波", "宁", "贵", "福", "生", "龙", "元", "全", "国", "胜", "学", "祥", "才", "发", "武", "新", + "利", "清", "飞", "彬", "富", "顺", "信", "子", "杰", "涛", "昌", "成", "康", "星", "光", "天", "达", "安", "岩", "中", "茂", + "进", "林", "有", "坚", "和", "彪", "博", "诚", "先", "敬", "震", "振", "壮", "会", "思", "群", "豪", "心", "邦", "承", "乐", + "绍", "功", "松", "善", "厚", "庆", "磊", "民", "友", "裕", "河", "哲", "江", "超", "浩", "亮", "政", "谦", "亨", "奇", "固", + "之", "轮", "翰", "朗", "伯", "宏", "言", "若", "鸣", "朋", "斌", "梁", "栋", "维", "启", "克", "伦", "翔", "旭", "鹏", "泽", + "晨", "辰", "士", "以", "建", "家", "致", "树", "炎", "德", "行", "时", "泰", "盛", "雄", "琛", "钧", "冠", "策", "腾", "楠", + "榕", "风", "航", "弘", "秀", "娟", "英", "华", "慧", "巧", "美", "娜", "静", "淑", "惠", "珠", "翠", "雅", "芝", "玉", "萍", + "红", "娥", "玲", "芬", "芳", "燕", "彩", "春", "菊", "兰", "凤", "洁", "梅", "琳", "素", "云", "莲", "真", "环", "雪", "荣", + "爱", "妹", "霞", "香", "月", "莺", "媛", "艳", "瑞", "凡", "佳", "嘉", "琼", "勤", "珍", "贞", "莉", "桂", "娣", "叶", "璧", + "璐", "娅", "琦", "晶", "妍", "茜", "秋", "珊", "莎", "锦", "黛", "青", "倩", "婷", "姣", "婉", "娴", "瑾", "颖", "露", "瑶", + "怡", "婵", "雁", "蓓", "纨", "仪", "荷", "丹", "蓉", "眉", "君", "琴", "蕊", "薇", "菁", "梦", "岚", "苑", "婕", "馨", "瑗", + "琰", "韵", "融", "园", "艺", "咏", "卿", "聪", "澜", "纯", "毓", "悦", "昭", "冰", "爽", "琬", "茗", "羽", "希", "欣", "飘", + "育", "滢", "馥", "筠", "柔", "竹", "霭", "凝", "晓", "欢", "霄", "枫", "芸", "菲", "寒", "伊", "亚", "宜", "可", "姬", "舒", + "影", "荔", "枝", "丽", "阳", "妮", "宝", "贝", "初", "程", "梵", "罡", "恒", "鸿", "桦", "骅", "剑", "娇", "纪", "宽", "苛", + "灵", "玛", "媚", "琪", "晴", "容", "睿", "烁", "堂", "唯", "威", "韦", "雯", "苇", "萱", "阅", "彦", "宇", "雨", "洋", "忠", + "宗", "曼", "紫", "逸", "贤", "蝶", "菡", "绿", "蓝", "儿", "翠", "烟", "小", "轩"} + var lastNameLen = len(lastName) + var firstNameLen = len(firstName) + total := randInt.Intn(10) + names := make([]string, total+1) + // 随机产生1-10位 + for i := 0; i <= total; i++ { + var first string + for i := 0; i <= rand.Intn(2); i++ { //随机产生2位或者3位的名 + first = fmt.Sprint(firstName[rand.Intn(firstNameLen-1)]) + } + names[i] = fmt.Sprintf("%s%s", fmt.Sprint(lastName[rand.Intn(lastNameLen-1)]), first) + } + return names +} diff --git a/web/service/activityser/prize.go b/web/service/activityser/prize.go new file mode 100644 index 0000000..b37ef5e --- /dev/null +++ b/web/service/activityser/prize.go @@ -0,0 +1,81 @@ +package activityser + +import ( + "fmt" + + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/prizemod" + "91porn-server/models/v/productmod" +) + +// 新增活动 +func AddPrize(in prizemod.AddPrizeCond) stderr.Code { + result, err := prizemod.AddPrize(in) + if err != nil { + return stderr.ErrDbInsertError + } + log.Info(fmt.Sprintf("新增活动[%v]成功", result)) + return stderr.Success +} + +// 修改活动 +func ModifyPrize(in *prizemod.ModifyPrizeCond) stderr.Code { + if err := prizemod.ModifyPrize(in.Cond(), in.Bson()); err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success +} + +// 查询列表 +func QueryAllPrize(in *prizemod.QueryAllPrizeCond) (interface{}, stderr.Code) { + var data map[string]interface{} = map[string]interface{}{ + "list": []interface{}{}, + "count": 0, + } + count, err := prizemod.CountPrize(in.Query()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + if count == 0 { + return data, stderr.Success + } + list, err := prizemod.QueryAllPrize(in.Query(), in.Options()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + data["count"] = count + data["list"] = list + return data, stderr.Success +} + +func QueryVipDropDownBox() (interface{}, stderr.Code) { + list, err := productmod.FindByProductType(commod.VIP) + if err != nil { + return nil, stderr.ErrDbQueryError + } + data := make([]map[string]interface{}, len(list)) + for i, v := range list { + data[i] = map[string]interface{}{ + "label": v.Name, + "value": v.ID.Hex(), + } + } + return data, stderr.Success +} + +func QueryPrizeDropDownBox() (interface{}, stderr.Code) { + list, err := prizemod.QueryDropDownBox() + if err != nil { + return nil, stderr.ErrDbQueryError + } + data := make([]map[string]interface{}, len(list)) + for i, v := range list { + data[i] = map[string]interface{}{ + "label": v.Name + fmt.Sprintf("[数量:%d]", v.Count), + "value": v.ID.Hex(), + } + } + return data, stderr.Success +} diff --git a/web/service/activityser/prize_pool.go b/web/service/activityser/prize_pool.go new file mode 100644 index 0000000..bc8a884 --- /dev/null +++ b/web/service/activityser/prize_pool.go @@ -0,0 +1,49 @@ +package activityser + +import ( + "fmt" + + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/activitymod" +) + +// 新增活动奖池 +func AddPrizePool(in activitymod.AddPrizePoolCond) stderr.Code { + result, err := activitymod.AddPrizePool(in) + if err != nil { + return stderr.ErrDbInsertError + } + log.Info(fmt.Sprintf("新增活动奖池[%v]成功", result)) + return stderr.Success +} + +// 修改活动奖池 +func ModifyPrizePool(in *activitymod.ModifyPrizePoolCond) stderr.Code { + if err := activitymod.ModifyPrizePool(in.Cond(), in.Bson()); err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success +} + +// 查询活动奖池列表 +func QueryAllPrizePool(in *activitymod.QueryAllPrizePoolCond) (interface{}, stderr.Code) { + var data map[string]interface{} = map[string]interface{}{ + "list": []interface{}{}, + "count": 0, + } + count, err := activitymod.CountPrizePool(in.Query()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + if count == 0 { + return data, stderr.Success + } + list, err := activitymod.QueryAllPrizePool(in.Query(), in.Options()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + data["count"] = count + data["list"] = list + return data, stderr.Success +} diff --git a/web/service/activityser/user_record.go b/web/service/activityser/user_record.go new file mode 100644 index 0000000..b5dcda2 --- /dev/null +++ b/web/service/activityser/user_record.go @@ -0,0 +1,55 @@ +package activityser + +import ( + "errors" + + "91porn-server/common/stderr" + "91porn-server/models/v/activitymod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func QueryAllRecord(in *activitymod.QueryAllRecordCond) (interface{}, stderr.Code) { + var data map[string]interface{} = map[string]interface{}{ + "list": []interface{}{}, + "count": 0, + } + count, err := activitymod.CountRecord(in.Query()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + if count == 0 { + return data, stderr.Success + } + list, err := activitymod.QueryAllRecord(in.Query(), in.Options()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + data["count"] = count + data["list"] = list + return data, stderr.Success +} + +// 开启机器人抽奖 +func OpenBotDraw(activityID primitive.ObjectID, status bool) error { + job.mutex.Lock() + defer job.mutex.Unlock() + if entryID, ok := job.cache[activityID]; ok { + if !status { + // 移除任务 + job.cron.Remove(entryID) + delete(job.cache, activityID) + return nil + } + return errors.New("任务已开启") + } + if status { + // 加入任务(每小时执行一次) + entryID, err := job.cron.AddJob("@hourly", BotDraw{ActivityID: activityID}) + if err != nil { + return err + } + job.cache[activityID] = entryID + } + return nil +} diff --git a/web/service/actvser/actv.go b/web/service/actvser/actv.go new file mode 100644 index 0000000..d775338 --- /dev/null +++ b/web/service/actvser/actv.go @@ -0,0 +1,99 @@ +package actvser + +import ( + "time" + + "91porn-server/models/v/actmod" + + "go.mongodb.org/mongo-driver/bson" +) + +func GetActities(req GetActitiesRequest) (int64, []actmod.Activity, bool, error) { + skip := (req.PageNum - 1) * req.PageSize + count, err := actmod.GetActivitiesCount() + if err != nil { + return 0, nil, false, err + } + if uint64(count) <= skip { + return 0, nil, false, err + } + limit := req.PageSize + 1 + actvs, err := actmod.GetActivities(skip, limit) + if err != nil { + return count, nil, false, err + } + hasNext := false + if uint64(len(actvs)) > req.PageSize { + hasNext = true + actvs = actvs[:req.PageSize] + } + return count, actvs, hasNext, nil +} + +func AddActivity(req AddActivityRequest) error { + now := time.Now() + act := actmod.Activity{ + CreatedAt: now, + UpdatedAt: now, + } + if req.ActivityName != nil { + act.ActivityName = *req.ActivityName + } + if req.Desc != nil { + act.Desc = *req.Desc + } + if req.Img != nil { + act.Img = *req.Img + } + if req.Content != nil { + act.Content = *req.Content + } + if req.Link != nil { + act.Link = *req.Link + } + if req.Sort != nil { + act.Sort = *req.Sort + } + if req.Status != nil { + act.Status = *req.Status + } + if req.ExpiredIn != nil { + act.ExpiredIn = *req.ExpiredIn + } + return actmod.AddACtivity(act) +} + +func UpdateActivity(req UpdateActivityRequest) error { + b := make(bson.M) + if req.ActivityName != nil { + b["activityName"] = *req.ActivityName + } + if req.Desc != nil { + b["desc"] = *req.Desc + } + if req.Img != nil { + b["img"] = *req.Img + } + if req.Content != nil { + b["content"] = *req.Content + } + if req.Link != nil { + b["link"] = *req.Link + } + if req.Sort != nil { + b["sort"] = *req.Sort + } + if req.Status != nil { + b["status"] = *req.Status + } + if req.ExpiredIn != nil { + b["expiredIn"] = *req.ExpiredIn + } + if len(b) == 0 { + return nil + } + now := time.Now() + b["createdAt"] = now + b["updatedAt"] = now + return actmod.EidtActivity(req.ActvID, b) +} diff --git a/web/service/actvser/type.go b/web/service/actvser/type.go new file mode 100644 index 0000000..fe5843b --- /dev/null +++ b/web/service/actvser/type.go @@ -0,0 +1,35 @@ +package actvser + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type GetActitiesRequest struct { + PageNum uint64 `form:"pageNum" json:"pageNum" binding:"required,min=1"` // 页码 + PageSize uint64 `form:"pageSize" json:"pageSize" binding:"required,min=1,max=20"` // 每页展示数 +} + +type UpdateActivityRequest struct { + ActvID primitive.ObjectID `json:"activeID"` // 被编辑的活动id + ActivityName *string `json:"activityName"` // 活动名 + Desc *string `json:"desc"` // 活动说明 + Img *string `json:"img"` // 活动图片 + Content *string `json:"content"` // 富文本内容 + Link *string `json:"link"` // 链接地址 + Sort *int `json:"sort"` // 活动排序 + Status *int `json:"status"` // 状态 1 有效 0 无效 + ExpiredIn *time.Time `json:"expiredIn"` // 过期时间 +} + +type AddActivityRequest struct { + ActivityName *string `json:"activityName"` // 活动名 + Desc *string `json:"desc"` // 活动说明 + Img *string `json:"img"` // 活动图片 + Content *string `json:"content"` // 富文本内容 + Link *string `json:"link"` // 链接地址 + Sort *int `json:"sort"` // 活动排序 + Status *int `json:"status"` // 状态 1 有效 0 无效 + ExpiredIn *time.Time `json:"expiredIn"` // 过期时间 +} diff --git a/web/service/addwelfareser/addwelfare.go b/web/service/addwelfareser/addwelfare.go new file mode 100644 index 0000000..d4b44ca --- /dev/null +++ b/web/service/addwelfareser/addwelfare.go @@ -0,0 +1,42 @@ +package addwelfareser + +import ( + "strconv" + "strings" + "time" + + "91porn-server/common/stderr" + "91porn-server/models/l/welfarelgmod" + "91porn-server/models/v/usermod" + "91porn-server/web/service/userser" + + "github.com/jinzhu/now" +) + +func AddWelfare(userId string) (stderr.Code, error) { + uid, err := strconv.ParseUint(userId, 10, 64) + if err != nil { + return stderr.Failure, err + } + user, err := usermod.FindUserByUIDForNoCache(uid) + if err != nil || user == nil { + return stderr.Failure, err + } + if err = welfarelgmod.InsertWelFareLog(welfarelgmod.WelfareLog{ + Uid: uid, + FareType: welfarelgmod.Fare_Vip, + FareNum: 2, + DailyDate: now.BeginningOfDay(), + CreatedAt: time.Now(), + }); err != nil { + if strings.Contains(err.Error(), "E11000 duplicate key error") { + return stderr.ErrDbInputExist, err + } + return stderr.Failure, err + } + vipExpire, vipLevel := userser.CheckVipRenew(user, 3, 0) + if _, err := usermod.Update(user.UID, usermod.UserSelector{VipExpireDate: &vipExpire, VipLevel: &vipLevel}); err != nil { + return stderr.Failure, err + } + return stderr.Success, nil +} diff --git a/web/service/adminser/web.go b/web/service/adminser/web.go new file mode 100644 index 0000000..d6390d9 --- /dev/null +++ b/web/service/adminser/web.go @@ -0,0 +1,97 @@ +package adminser + +import ( + "time" + + "91porn-server/common/pageopt" + "91porn-server/models/v/adminmod" + "91porn-server/models/v/authoritymod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type M = bson.M + +// Record Record +type Record struct { + ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + Name *string `json:"name,omitempty" bson:"name,omitempty"` //账号 + Password *string `json:"password,omitempty" bson:"password,omitempty"` //密码 + Nickname *string `json:"nickname,omitempty" bson:"nickname,omitempty"` //昵称 + Email *string `json:"email,omitempty" bson:"email,omitempty"` //邮箱 + PrivilegeID *primitive.ObjectID `json:"privilegeID,omitempty" bson:"privilegeID,omitempty"` //权限ID + Role *string `json:"role,omitempty" bson:"role,omitempty"` //角色 + HasLocked *bool `json:"hasLocked,omitempty" bson:"hasLocked,omitempty"` //已禁止登陆 + LockReason *string `json:"lockReason,omitempty" bson:"lockReason,omitempty"` //禁止登陆的原因 + CreatedAt *time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` + LastLoginAt *time.Time `json:"lastLoginAt,omitempty" bson:"lastLoginAt,omitempty"` +} + +type Page struct { + Total int64 `json:"total" bson:"total"` + List []Record `json:"list" bson:"list"` +} + +// NameMatch +type NameMatch struct { + Name *string +} + +func (n *NameMatch) New() pageopt.Matcher { + return pageopt.NewAssignMatch("name", n.Name) +} + +// NotSuperAdminMatch +type NotSuperAdminMatch struct { +} + +func (n *NotSuperAdminMatch) New() pageopt.Matcher { + return pageopt.NewAssignMatch("name", M{"$ne": "superAdmin"}) +} + +// AdminPages AdminPages +func AdminPages(skip, limit int64, name *string) (Page, error) { + matList := []pageopt.Matcher{(&NotSuperAdminMatch{}).New()} + if name != nil { + matList = []pageopt.Matcher{(&NameMatch{name}).New()} + } + adminList, err := adminmod.List(skip, limit, matList...) + if err != nil { + return Page{}, err + } + roles := make([]string, len(adminList)) + for i, admin := range adminList { + roles[i] = admin.Role + } + roleIDMap, err := authoritymod.IDMapByRoles(roles) + if err != nil { + return Page{}, err + } + recordList := make([]Record, len(adminList)) + for i, admin := range adminList { + _admin := admin + privilegeID := roleIDMap[_admin.Role] + recordList[i] = Record{ + ID: _admin.ID, + Name: &_admin.Name, + Password: &_admin.Password, + Nickname: &_admin.Nickname, + Email: &_admin.Email, + PrivilegeID: &privilegeID, + Role: &_admin.Role, + HasLocked: &_admin.HasLocked, + LockReason: &_admin.LockReason, + CreatedAt: &_admin.CreatedAt, + LastLoginAt: &_admin.LastLoginAt, + } + } + count, err := adminmod.Count(matList...) + if err != nil { + return Page{}, err + } + return Page{ + Total: count, + List: recordList, + }, nil +} diff --git a/web/service/adser/ads.go b/web/service/adser/ads.go new file mode 100644 index 0000000..fda3329 --- /dev/null +++ b/web/service/adser/ads.go @@ -0,0 +1,111 @@ +package adser + +import ( + "time" + + "91porn-server/common" + "91porn-server/models/v/adsmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AdRecord struct { + ID primitive.ObjectID `json:"id"` //广告id + AdsType adsmod.AdsType `json:"adsType"` //广告类型 + Cover string `json:"cover"` //广告封面图 + Size int `json:"size"` //尺寸 B + Height int `json:"height"` //高 + Width int `json:"width"` //宽 + Title string `json:"title"` //广告标题 + HrefType adsmod.URLJumpType `json:"hrefType"` //广告链接类型 + Href string `json:"href"` //广告跳转地址 + Position adsmod.AdPosition `json:"position"` //广告位置 + SortCode int `json:"sortCode"` //排序号 + Active bool `json:"active"` //激活状态 true:激活 + Click int `json:"click"` //点击次数 + Remark string `json:"remark"` //备注 + ReviewID primitive.ObjectID `json:"reviewID,omitempty"` //审核id + DistrictCode string `json:"districtCode"` //商区码 + CreatedAt time.Time `json:"createdAt"` //创建时间 + Start time.Time `json:"start"` //开始时间 + End time.Time `json:"end"` //结束时间 +} + +type AdPage struct { + List []AdRecord `json:"list"` + Total int64 `json:"total"` +} + +// AdPages 获取广告列表 +func AdPages(skip, limit int64, idMatch adsmod.IDMatch, adsType adsmod.AdsTypeMatch, adsPositionMatch adsmod.AdPositionMatch, districtCodeMatch adsmod.DistrictCodeMatch) (AdPage, error) { + mats := []adsmod.Matcher{ + idMatch.New(), + adsType.New(), + adsPositionMatch.New(), + districtCodeMatch.New(), + } + list, _ := adsmod.List(adsmod.Sort_createdAt_N1, &skip, &limit, mats...) + recordList := make([]AdRecord, len(list)) + for i, v := range list { + recordList[i] = AdRecord{ + ID: v.ID, + AdsType: v.AdsType, + Cover: v.Cover, + Size: v.Size, + Height: v.Height, + Width: v.Width, + Title: v.Title, + HrefType: v.HrefType, + Href: v.Href, + Position: v.Position, + SortCode: v.SortCode, + Active: v.Active, + Click: v.Click, + Remark: v.Remark, + ReviewID: v.ReviewID, + DistrictCode: v.DistrictCode, + CreatedAt: v.CreatedAt, + Start: v.Start, + End: v.End, + } + } + count, _ := adsmod.Count(mats...) + return AdPage{ + recordList, + count, + }, nil +} + +func IsValidAdPicture(pos adsmod.AdPosition, pictureProp common.PictureProp) error { + switch pos { + case adsmod.StartupPage: //启动页广告 + if pictureProp.Height > 1560 || pictureProp.Width > 720 { + return ErrImgScale{} + } + case adsmod.HomeRecoSmall: //首页-推荐小广告 + if pictureProp.Height > 620 || pictureProp.Width > 560 { + return ErrImgScale{} + } + case adsmod.HomePopupAnnou: //首页-弹窗公告 + if pictureProp.Height > 1300 || pictureProp.Width > 656 { + return ErrImgScale{} + } + case adsmod.NotifyPage: //消息-界面广告 + if pictureProp.Height > 260 || pictureProp.Width > 720 { + return ErrImgScale{} + } + case adsmod.LiaoCarousel: //撩吧-轮播广告 + if pictureProp.Height > 500 || pictureProp.Width > 720 { + return ErrImgScale{} + } + case adsmod.WalletPage: //钱包-轮播广告 + if pictureProp.Height > 120 || pictureProp.Width > 720 { + return ErrImgScale{} + } + case adsmod.SearchPage: //搜索-轮播广告 + if pictureProp.Height > 360 || pictureProp.Width > 720 { + return ErrImgScale{} + } + } + return nil +} diff --git a/web/service/adser/error.go b/web/service/adser/error.go new file mode 100644 index 0000000..05e66df --- /dev/null +++ b/web/service/adser/error.go @@ -0,0 +1,29 @@ +package adser + +type ErrImgURLInvalid struct { +} + +func (ErrImgURLInvalid) Error() string { + return "picture url is invalid!" +} + +type ErrImgTooBig struct { +} + +func (ErrImgTooBig) Error() string { + return "picture is too big!" +} + +type ErrImgScale struct { +} + +func (ErrImgScale) Error() string { + return "picture cale is invalid!" +} + +type ErrImgContent struct { +} + +func (ErrImgContent) Error() string { + return "picture is invalid!" +} diff --git a/web/service/adser/review.go b/web/service/adser/review.go new file mode 100644 index 0000000..41df638 --- /dev/null +++ b/web/service/adser/review.go @@ -0,0 +1,166 @@ +package adser + +import ( + "time" + + "91porn-server/common/db" + "91porn-server/models/v/adreviewmod" + "91porn-server/models/v/adsmod" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ReviewRecord struct { + ID primitive.ObjectID `json:"id"` //id + DistrictCode string `json:"districtCode"` //商区码 + OptType adreviewmod.OptType `json:"optType"` //操作类型 + ApplyAt time.Time `json:"applyAt"` //申请时间 + PassAt time.Time `json:"passAt"` //通过时间 + RefussAt time.Time `json:"refussAt"` //拒绝时间 + Reviewer string `json:"reviewer"` //审核人 + ReviewStatus adreviewmod.ReviewStatus `json:"reviewStatus"` //审核状态 + Remark string `json:"remark"` //备注 + ReceiptInfo string `json:"receiptInfo"` //回执信息 + ADID primitive.ObjectID `json:"adID"` //广告id + Title string `json:"title"` //广告标题 + Cover string `json:"cover"` //广告封面图 + Size int `json:"size"` //尺寸 单位:B + Height int `json:"height"` + Width int `json:"width"` + HrefType adsmod.URLJumpType `json:"hrefType"` //广告链接类型 + Href string `json:"href"` //广告跳转地址 + Position adsmod.AdPosition `json:"position"` //广告位置 + SortCode int `json:"sortCode"` //排序号 +} + +type ReviewPage struct { + List []ReviewRecord `json:"list"` + Total int64 `json:"total"` +} + +// ReviewPages 获取广告列表 +func ReviewPages(skip, limit int64, idMatch adreviewmod.IDMatch, reviewStatusMatch adreviewmod.ReviewStatusMatch, districtCodeMatch adreviewmod.DistrictCodeMatch) (ReviewPage, error) { + mats := []adreviewmod.Matcher{ + idMatch.New(), + reviewStatusMatch.New(), + districtCodeMatch.New(), + } + list, err := adreviewmod.List(adreviewmod.Sort_createdAt_N1, &skip, &limit, mats...) + if err != nil { + return ReviewPage{}, nil + } + recordList := make([]ReviewRecord, len(list)) + for i, v := range list { + recordList[i] = ReviewRecord{ + ID: v.ID, + DistrictCode: v.DistrictCode, + OptType: v.OptType, + PassAt: v.PassAt, + RefussAt: v.RefussAt, + Reviewer: v.Reviewer, + ReviewStatus: v.ReviewStatus, + ADID: v.ADID, + Cover: v.Cover, + Size: v.Size, + Height: v.Height, + Width: v.Width, + Title: v.Title, + HrefType: v.HrefType, + Href: v.Href, + Position: adsmod.AdPosition(v.Position), + SortCode: v.SortCode, + Remark: v.Remark, + ReceiptInfo: v.ReceiptInfo, + ApplyAt: v.ApplyAt, + } + } + count, err := adreviewmod.Count(mats...) + if err != nil { + return ReviewPage{}, nil + } + return ReviewPage{ + recordList, + count, + }, nil +} + +func CheckRefuse(id primitive.ObjectID, reviewer string, receiptInfo string) error { + return adreviewmod.UpdateReviewByID(id, adreviewmod.Processong, adreviewmod.Refuse, nil, reviewer, receiptInfo, time.Now()) +} + +func CheckPass(id primitive.ObjectID, adsType adsmod.AdsType, reviewer string, receiptInfo string, startTime, endTime time.Time) error { + adR, err := adreviewmod.FindOneByID(id) + if err != nil { + return err + } + now := time.Now() + position := adsmod.AdPosition(adR.Position) + reviewInfo := adsmod.ReviewInfo{ + ReviewID: adR.ID, + ReviewAt: &now, + Reviewer: &reviewer, + } + insDoc := adsmod.InsertDoc{ + AdsType: adsType, + Cover: adR.Cover, + Title: adR.Title, + HrefType: adR.HrefType, + Href: adR.Href, + Position: position, + SortCode: adR.SortCode, + Active: true, + Remark: receiptInfo, + DistrictCode: adR.DistrictCode, + ReviewInfo: reviewInfo, + CreatedAt: now, + CoverProp: adR.CoverProp, + Start: startTime, + End: endTime, + } + t := true + upDoc := adsmod.UpdateDoc{ + Title: &adR.Title, + Cover: &adR.Cover, + HrefType: &adR.HrefType, + Href: &adR.Href, + Position: &position, + SortCode: &adR.SortCode, + Active: &t, + Remark: &receiptInfo, + ReviewInfo: reviewInfo, + UpdatedAt: now, + CoverProp: adR.CoverProp, + Start: &startTime, + End: &endTime, + } + return webg.VideoDB.Trans(func(t *db.MongoTool) error { + if adR.ADID.IsZero() { + adID, err := adsmod.InsertOne(t, insDoc) + if err != nil { + return err + } + return adreviewmod.UpdateReviewByID(id, adreviewmod.Processong, adreviewmod.Pass, &adID, reviewer, receiptInfo, now) + } + if err := adsmod.UpdateOneByID(t, adR.ADID, upDoc); err != nil { + return err + } + return adreviewmod.UpdateReviewByID(id, adreviewmod.Processong, adreviewmod.Pass, nil, reviewer, receiptInfo, now) + }) +} + +func DeleteAd(adID, arID *primitive.ObjectID) error { + return webg.VideoDB.Trans(func(t *db.MongoTool) error { + if adID != nil { + if err := adsmod.DeleteOne(t, *adID); err != nil { + return err + } + } + if arID != nil { + if err := adreviewmod.DeleteOne(t, *arID); err != nil { + return err + } + } + return nil + }) +} diff --git a/web/service/advance_config_ser/advance_config.go b/web/service/advance_config_ser/advance_config.go new file mode 100644 index 0000000..16220dd --- /dev/null +++ b/web/service/advance_config_ser/advance_config.go @@ -0,0 +1,50 @@ +package advance_config_ser + +import ( + "91porn-server/common/stderr" + "91porn-server/models/v/advanceconfigmod" +) + +func QueryAll(in *advanceconfigmod.QueryAllCond) (*advanceconfigmod.QueryAllRes, error) { + var out = &advanceconfigmod.QueryAllRes{} + count, err := advanceconfigmod.CountDocument(in.Filter()) + if err != nil { + return nil, err + } + if count == 0 { + out.List = []*advanceconfigmod.AdvanceConfig{} + out.Total = 0 + return out, nil + } + out.Total = count + out.List, err = advanceconfigmod.QueryAllDocument(in.Filter(), in.Options()) + if err != nil { + return nil, err + } + return out, nil +} + +func Create(in *advanceconfigmod.WebCreateReq, manager string) stderr.Code { + err := advanceconfigmod.InsertOne(nil, in.Create(manager)) + if err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success +} + +func Update(in *advanceconfigmod.WebUpdateReq, manager string) stderr.Code { + err := advanceconfigmod.Update(nil, in.Filter(), in.Update(manager)) + if err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success + +} + +func Del(in *advanceconfigmod.WebDeleteReq, manager string) stderr.Code { + err := advanceconfigmod.Update(nil, in.Filter(), in.Del(manager)) + if err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success +} diff --git a/web/service/advance_order_ser/advance_order.go b/web/service/advance_order_ser/advance_order.go new file mode 100644 index 0000000..0a484d6 --- /dev/null +++ b/web/service/advance_order_ser/advance_order.go @@ -0,0 +1,24 @@ +package advance_order_ser + +import ( + "91porn-server/models/v/advanceordermod" +) + +func QueryAll(in *advanceordermod.QueryAllCond) (*advanceordermod.QueryAllRes, error) { + var out = &advanceordermod.QueryAllRes{} + count, err := advanceordermod.CountDocument(in.Filter()) + if err != nil { + return nil, err + } + if count == 0 { + out.List = []*advanceordermod.AdvanceOrder{} + out.Total = 0 + return out, nil + } + out.Total = count + out.List, err = advanceordermod.QueryAllDocument(in.Filter(), in.Options()) + if err != nil { + return nil, err + } + return out, nil +} diff --git a/web/service/advgroupser/advgroupser.go b/web/service/advgroupser/advgroupser.go new file mode 100644 index 0000000..a1db2be --- /dev/null +++ b/web/service/advgroupser/advgroupser.go @@ -0,0 +1,270 @@ +package advgroupser + +import ( + "91porn-server/common" + "91porn-server/models/commod" + "91porn-server/models/s/dailyretentionmod" + "context" + "errors" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type AdStatType int + +const ( + AdStatTypeUserRetained AdStatType = 1 // 用户留存数 + AdStatTypeUserRates AdStatType = 2 // 用户留存率 + AdStatTypeAdClickAcc AdStatType = 3 // 广告点击累计 + AdStatTypePayTotalAcc AdStatType = 4 // 充值总额累计 + AdStatTypePayAndAdClickTotalAcc AdStatType = 5 // 充值总额+广告点击 +) + +type QueryReq struct { + StartDate *time.Time `json:"startDate" form:"startDate"` // 开始日期 + EndDate *time.Time `json:"endDate" form:"endDate"` // 结束日期 + commod.Page +} + +type AdGroupStatReq struct { + Type AdStatType `json:"type" form:"type"` // 类型 1-用户留存数 2-用户留存率 3-广告点击累计 4-充值总额累计 5-充值总额+广告点击 + QueryReq +} + +type QueryRes struct { + Total int64 `json:"total"` // 总数 + List []Item `json:"list"` // 列表 +} + +type Item struct { + ID primitive.ObjectID `json:"id" bson:"id"` // ID + Date time.Time `json:"date" bson:"date"` // 日期 + GroupA []string `json:"groupA" bson:"groupA"` // A组数据(0-30天) + GroupB []string `json:"groupB" bson:"groupB"` // B组数据(0-30天) + GroupC []string `json:"groupC" bson:"groupC"` // C组数据(0-30天) +} + +func GetAdGroupStatList(ctx context.Context, req *AdGroupStatReq) (*QueryRes, error) { + var err error + var res *QueryRes + switch req.Type { + case AdStatTypeUserRetained: + res, err = QueryUserRetained(ctx, &req.QueryReq) + case AdStatTypeUserRates: + res, err = QueryUserRates(ctx, &req.QueryReq) + case AdStatTypeAdClickAcc: + res, err = QueryTotalClickAcc(ctx, &req.QueryReq) + case AdStatTypePayTotalAcc: + res, err = QueryPayTotalAcc(ctx, &req.QueryReq) + case AdStatTypePayAndAdClickTotalAcc: + res, err = QueryTotalPayAndClickAcc(ctx, &req.QueryReq) + default: + return nil, errors.New("未知的类型") + } + if err != nil { + return nil, err + } + + //res, _ = GetAdGroupStatListMose(ctx, req) // 使用模拟数据替代真实查询 + + return res, nil +} + +type dataExtractor func(item *dailyretentionmod.DailyRetention) []string + +// queryRetention 通用查询函数 +func queryRetention(ctx context.Context, req *QueryReq, extractor dataExtractor) (*QueryRes, error) { + startDate := time.Time{} + endDate := time.Now() + if req.StartDate != nil { + startDate = time.Date(req.StartDate.Year(), req.StartDate.Month(), req.StartDate.Day(), 0, 0, 0, 0, req.StartDate.Location()) + } + if req.EndDate != nil { + endDate = time.Date(req.EndDate.Year(), req.EndDate.Month(), req.EndDate.Day(), 0, 0, 0, 0, req.EndDate.Location()) + } + + filter := bson.M{ + "date": bson.M{ + "$gte": startDate, + "$lte": endDate, + }, + } + + // 查询总数(按日期去重) + total, err := countDistinctDates(ctx, filter) + if err != nil { + return nil, fmt.Errorf("count failed: %w", err) + } + + // 分页查询 + skip := int64((req.PageNumber - 1) * req.PageSize) + limit := int64(req.PageSize) + + // 先查询日期范围内的所有记录 + opts := options.Find().SetSort(bson.D{{Key: "date", Value: -1}}) + items, err := dailyretentionmod.QueryAllList(filter, opts) + if err != nil { + return nil, fmt.Errorf("query failed: %w", err) + } + + // 按日期分组 + dateMap := make(map[time.Time]map[commod.AdGroup]*dailyretentionmod.DailyRetention) + for _, item := range items { + if dateMap[item.Date] == nil { + dateMap[item.Date] = make(map[commod.AdGroup]*dailyretentionmod.DailyRetention) + } + dateMap[item.Date][item.AdGroup] = item + } + + // 收集所有日期并排序 + dates := make([]time.Time, 0, len(dateMap)) + for date := range dateMap { + dates = append(dates, date) + } + sortDatesDesc(dates) + + // 分页处理 + start := int(skip) + end := start + int(limit) + if start > len(dates) { + start = len(dates) + } + if end > len(dates) { + end = len(dates) + } + pagedDates := dates[start:end] + + // 构建结果 + list := make([]Item, 0, len(pagedDates)) + for _, date := range pagedDates { + groupMap := dateMap[date] + item := Item{ + Date: common.NormalizeDate(date), + GroupA: extractGroupData(groupMap[commod.AdGroupA], extractor), + GroupB: extractGroupData(groupMap[commod.AdGroupB], extractor), + GroupC: extractGroupData(groupMap[commod.AdGroupC], extractor), + } + list = append(list, item) + } + + return &QueryRes{ + Total: total, + List: list, + }, nil +} + +// countDistinctDates 统计日期数量 +func countDistinctDates(ctx context.Context, filter bson.M) (int64, error) { + dates, err := dailyretentionmod.Distinct(filter, "date") + if err != nil { + return 0, err + } + return int64(len(dates)), nil +} + +// sortDatesDesc 日期降序排序 +func sortDatesDesc(dates []time.Time) { + for i := 0; i < len(dates)-1; i++ { + for j := i + 1; j < len(dates); j++ { + if dates[i].Before(dates[j]) { + dates[i], dates[j] = dates[j], dates[i] + } + } + } +} + +// extractGroupData 提取单个广告组的数据 +func extractGroupData(item *dailyretentionmod.DailyRetention, extractor dataExtractor) []string { + if item == nil { + // 返回 31 个空字符串 + result := make([]string, dailyretentionmod.MaxRetentionDays+1) + for i := range result { + result[i] = "" + } + return result + } + return extractor(item) +} + +// QueryUserRetained 查询用户留存人数 +func QueryUserRetained(ctx context.Context, req *QueryReq) (*QueryRes, error) { + return queryRetention(ctx, req, func(item *dailyretentionmod.DailyRetention) []string { + result := make([]string, dailyretentionmod.MaxRetentionDays+1) + for i := 0; i <= dailyretentionmod.MaxRetentionDays; i++ { + if item.UserRetained != nil { + result[i] = fmt.Sprintf("%d", item.UserRetained[i]) + } else { + result[i] = "0" + } + } + return result + }) +} + +// QueryUserRates 查询用户留存率 +func QueryUserRates(ctx context.Context, req *QueryReq) (*QueryRes, error) { + return queryRetention(ctx, req, func(item *dailyretentionmod.DailyRetention) []string { + result := make([]string, dailyretentionmod.MaxRetentionDays+1) + for i := 0; i <= dailyretentionmod.MaxRetentionDays; i++ { + if item.UserRates != nil { + result[i] = formatRatePercentage(item.UserRates[i]) + } else { + result[i] = "100%" + } + } + return result + }) +} + +// QueryPayTotalAcc 查询充值累计金额 +func QueryPayTotalAcc(ctx context.Context, req *QueryReq) (*QueryRes, error) { + return queryRetention(ctx, req, func(item *dailyretentionmod.DailyRetention) []string { + result := make([]string, dailyretentionmod.MaxRetentionDays+1) + for i := 0; i <= dailyretentionmod.MaxRetentionDays; i++ { + if item.PayTotalAcc != nil { + result[i] = fmt.Sprintf("%d", item.PayTotalAcc[i]/100) // 金额以分为单位,转换为元 + } else { + result[i] = "0" + } + } + return result + }) +} + +// QueryTotalClickAcc 查询点击累计次数 +func QueryTotalClickAcc(ctx context.Context, req *QueryReq) (*QueryRes, error) { + return queryRetention(ctx, req, func(item *dailyretentionmod.DailyRetention) []string { + result := make([]string, dailyretentionmod.MaxRetentionDays+1) + for i := 0; i <= dailyretentionmod.MaxRetentionDays; i++ { + if item.TotalClickAcc != nil { + result[i] = fmt.Sprintf("%d", item.TotalClickAcc[i]) + } else { + result[i] = "0" + } + } + return result + }) +} + +// QueryTotalPayAndClickAcc 查询点击累计次数 +func QueryTotalPayAndClickAcc(ctx context.Context, req *QueryReq) (*QueryRes, error) { + return queryRetention(ctx, req, func(item *dailyretentionmod.DailyRetention) []string { + result := make([]string, dailyretentionmod.MaxRetentionDays+1) + for i := 0; i <= dailyretentionmod.MaxRetentionDays; i++ { + if item.TotalClickAcc != nil { + result[i] = fmt.Sprintf("%d", item.TotalClickAcc[i]+item.PayTotalAcc[i]/100) + } else { + result[i] = "0" + } + } + return result + }) +} + +func formatRatePercentage(rate float64) string { + return fmt.Sprintf("%.2f%%", rate*100) +} diff --git a/web/service/advgroupser/advgroupser_test.go b/web/service/advgroupser/advgroupser_test.go new file mode 100644 index 0000000..91e80a3 --- /dev/null +++ b/web/service/advgroupser/advgroupser_test.go @@ -0,0 +1,21 @@ +package advgroupser + +import "testing" + +func TestFormatRatePercentage(t *testing.T) { + t.Run("formats decimal rate as percentage", func(t *testing.T) { + got := formatRatePercentage(0.1234) + want := "12.340%" + if got != want { + t.Fatalf("expected %s, got %s", want, got) + } + }) + + t.Run("formats zero as zero percentage", func(t *testing.T) { + got := formatRatePercentage(0) + want := "0.000%" + if got != want { + t.Fatalf("expected %s, got %s", want, got) + } + }) +} diff --git a/web/service/ai_changeface_img_ser/ai_changface_img.go b/web/service/ai_changeface_img_ser/ai_changface_img.go new file mode 100644 index 0000000..21f1203 --- /dev/null +++ b/web/service/ai_changeface_img_ser/ai_changface_img.go @@ -0,0 +1,468 @@ +package ai_changeface_img_ser + +import ( + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/aichangefaceimgmod" + "91porn-server/models/v/aichangefacevidmod" + "91porn-server/models/v/aiplazamod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/walletmod" + "91porn-server/web/proto" + "91porn-server/web/webg" + "fmt" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func List(req *aichangefaceimgmod.WebListRequest) (interface{}, stderr.Code) { + var data = map[string]interface{}{ + "list": []interface{}{}, + "count": 0, + } + count, err := aichangefaceimgmod.CountDocument(req.Filter()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + if count == 0 { + return data, stderr.Success + } + list, err := aichangefaceimgmod.QueryAllDocument(req.Filter(), req.Options()) + if err != nil { + return data, stderr.ErrDbQueryError + } + data["count"] = count + data["list"] = list + return data, stderr.Success +} + +func Update(act string, req *aichangefaceimgmod.EditCond) stderr.Code { + // 获取Ai记录 + data, err := aichangefaceimgmod.FindDataByID(req.ID) + if err != nil { + return stderr.ErrDbQueryError + } + + if data.ID.IsZero() { + return stderr.CodeEmptyData + } + if (*req.Status == aichangefaceimgmod.FAILURE || *req.Status == aichangefaceimgmod.REFUND) && + (data.Status == aichangefaceimgmod.FAILURE || data.Status == aichangefaceimgmod.REFUND) { + return stderr.Success + } + // 根据状态判断 + switch *req.Status { + case aichangefaceimgmod.Processing: + if data.Status != aichangefaceimgmod.SUCCESS { + return stderr.AiChangeFaceImgStatusIsErr + } + err = aichangefaceimgmod.Edit(nil, req.Filter(), req.Update(act)) + if err != nil { + return stderr.ErrDbUpdateError + } + case aichangefaceimgmod.SUCCESS: + if err = aichangefaceimgmod.Edit(nil, req.Filter(), req.Update(act)); err != nil { + return stderr.ErrDbUpdateError + } + case aichangefaceimgmod.FAILURE, aichangefaceimgmod.REFUND: + // 失败处理 退金币 + deAmt := -data.DebitAmountCoin + deIncome := -data.DebitIncomeCoin + uid := data.UID + if err = webg.VideoDB.Trans(func(tool *db.MongoTool) error { + updateSet, _ := req.Update(act)["$set"].(bson.M) + transitioned, transitionErr := aichangefaceimgmod.TransitionStatus(tool, data.ID, data.Status, *req.Status, updateSet) + if transitionErr != nil { + return transitionErr + } + if !transitioned { + return nil + } + var tl []txnmod.TransactionLog + // 根据是否是免费次数退回 + if data.Count > 0 || data.IsFreeTimes { + deCount := data.Count + if data.Count == 0 { + deCount = 1 + } + // 加钱包余额 + wallet, wErr := walletmod.DebitAiFreeTimes(tool, -deCount, uid) + if wErr != nil { + log.Error(fmt.Sprintf("Handle AiChangeFaceImg Edit walletmod.DebitAiFreeTimes error:%+v:", wErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: deCount, + ActualAmount: float64(deCount), + TranType: txnmod.AiChangeFaceImgDebitFreeTimesReturn.Key(), + TranTypeInt: int64(txnmod.AiChangeFaceImgDebitFreeTimesReturn), + Desc: fmt.Sprintf("AI图片换脸免费次数退返[%d次]", deCount), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + + if data.DebitAmountCoin > 0 || data.DebitIncomeCoin > 0 { + // 加钱包余额 + wallet, wErr := walletmod.ReturnAmountAndIncome(tool, deAmt, deIncome, uid) + if wErr != nil { + log.Error(fmt.Sprintf("Handle AiChangeFaceImg Edit walletmod.DebitAmount error:%+v:", wErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + if data.DebitIncomeCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -deIncome, + ActualAmount: float64(-deIncome), + TranType: txnmod.AiChangeFaceImgReturnIncomeGold.Key(), + TranTypeInt: int64(txnmod.AiChangeFaceImgReturnIncomeGold), + Desc: fmt.Sprintf("AI图片换脸收益金币退返-%d", -deIncome), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if data.DebitAmountCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -deAmt, + ActualAmount: float64(-deAmt), + TranType: txnmod.AiChangeFaceImgReturnGold.Key(), + TranTypeInt: int64(txnmod.AiChangeFaceImgReturnGold), + Desc: fmt.Sprintf("AI图片换脸金币退返-%d", -deAmt), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + } + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(tool, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle AiChangeFaceImg Edit txnmod.InsertManyTransactionLog error:%+v:", txnErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + } + + return nil + }); err != nil { + log.Error(fmt.Sprintf("Handle AiChangeFaceImg Edit Trans error:%+v;uid:%v;", err, uid)) + return stderr.ErrDbUpdateError + } + } + return stderr.Success +} + +func UpdateNew(act string, req *aichangefaceimgmod.EditCond) stderr.Code { + // 获取Ai记录 + data, err := aichangefaceimgmod.FindDataByID(req.ID) + if err != nil { + return stderr.ErrDbQueryError + } + if data.ID.IsZero() { + return stderr.CodeEmptyData + } + if *req.Status == aichangefaceimgmod.StatusSubmit && data.Status == aichangefaceimgmod.StatusSubmit { + return stderr.Success + } + if (*req.Status == aichangefaceimgmod.FAILURE || *req.Status == aichangefaceimgmod.REFUND) && + (data.Status == aichangefaceimgmod.FAILURE || data.Status == aichangefaceimgmod.REFUND) { + return stderr.Success + } + // 根据状态判断 + switch *req.Status { + case aichangefaceimgmod.Processing: + err = aichangefaceimgmod.Edit(nil, req.Filter(), req.Update(act)) + if err != nil { + return stderr.ErrDbUpdateError + } + case aichangefaceimgmod.StatusSubmit: + //提交订单到第三方 + notifyURL, notifyErr := webg.AICallbackURL("/api/web/admin/ai/change_face_img/callback") + if notifyErr != nil { + log.Error("AI图片换脸回调地址配置异常", log.E(notifyErr)) + return stderr.Failure + } + httpReq := proto.AiChangeFaceReq{ + AppId: int(commod.KFK_APPID), + OrderNumAPP: data.ID.Hex(), + Title: data.ModTitle, + SourceImageUrl: data.OriginPic, + NotifyUrl: notifyURL, + } + url := webg.Conf.URL.AiServer + "/api/changeface/prd/changeFaceByVideo" + //第三方请求地址 + var resp proto.AiChangeFaceResp + codeJ, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, httpReq) + if err != nil { + log.Error("CreateChangeFace httputil.DefaultClientPostJsonWithResp err", log.E(err)) + return stderr.Failure + } + if codeJ != 200 { + log.Error("CreateChangeFace req http code error", log.Any("code", codeJ)) + return stderr.Failure + } + if resp.Code == 40000 { + return stderr.ChangeFaceNoModule + } + if err = aichangefaceimgmod.Edit(nil, req.Filter(), req.Update(act)); err != nil { + return stderr.ErrDbUpdateError + } + case aichangefaceimgmod.FAILURE, aichangefaceimgmod.REFUND: + // 失败处理 退金币 + deAmt := -data.DebitAmountCoin + deIncome := -data.DebitIncomeCoin + uid := data.UID + if err = webg.VideoDB.Trans(func(tool *db.MongoTool) error { + updateSet, _ := req.Update(act)["$set"].(bson.M) + transitioned, transitionErr := aichangefaceimgmod.TransitionStatus(tool, data.ID, data.Status, *req.Status, updateSet) + if transitionErr != nil { + return transitionErr + } + if !transitioned { + return nil + } + var tl []txnmod.TransactionLog + // 根据是否是免费次数退回 + if data.Count > 0 || data.IsFreeTimes { + deCount := data.Count + if data.Count == 0 { + deCount = 1 + } + // 加钱包余额 + wallet, wErr := walletmod.DebitAiFreeTimes(tool, -deCount, uid) + if wErr != nil { + log.Error(fmt.Sprintf("Handle AiChangeFaceImg Edit walletmod.DebitAiFreeTimes error:%+v:", wErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: deCount, + ActualAmount: float64(deCount), + TranType: txnmod.AiChangeFaceImgDebitFreeTimesReturn.Key(), + TranTypeInt: int64(txnmod.AiChangeFaceImgDebitFreeTimesReturn), + Desc: fmt.Sprintf("AI图片换脸免费次数退返[%d次]", deCount), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + + if data.DebitAmountCoin > 0 || data.DebitIncomeCoin > 0 { + // 加钱包余额 + wallet, wErr := walletmod.ReturnAmountAndIncome(tool, deAmt, deIncome, uid) + if wErr != nil { + log.Error(fmt.Sprintf("Handle AiChangeFaceImg Edit walletmod.DebitAmount error:%+v:", wErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + if data.DebitIncomeCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -deIncome, + ActualAmount: float64(-deIncome), + TranType: txnmod.AiChangeFaceImgReturnIncomeGold.Key(), + TranTypeInt: int64(txnmod.AiChangeFaceImgReturnIncomeGold), + Desc: fmt.Sprintf("AI图片换脸收益金币退返-%d", -deIncome), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if data.DebitAmountCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -deAmt, + ActualAmount: float64(-deAmt), + TranType: txnmod.AiChangeFaceImgReturnGold.Key(), + TranTypeInt: int64(txnmod.AiChangeFaceImgReturnGold), + Desc: fmt.Sprintf("AI图片换脸金币退返-%d", -deAmt), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + } + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(tool, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle AiChangeFaceImg Edit txnmod.InsertManyTransactionLog error:%+v:", txnErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + } + + return nil + }); err != nil { + log.Error(fmt.Sprintf("Handle AiChangeFaceImg Edit Trans error:%+v;uid:%v;", err, uid)) + return stderr.ErrDbUpdateError + } + } + return stderr.Success +} + +func CallBackOrder(req *aichangefacevidmod.CallbackReq) stderr.Code { + //根据订单号更新ai换脸订单 + id, _ := primitive.ObjectIDFromHex(req.AppOrderNum) + //查询该笔订单是否存在 + aichangeFaceInfo, err := aichangefaceimgmod.FindByID(nil, id) + if aichangeFaceInfo == nil || err != nil { + return stderr.ErrDbQueryError + } + oldOrderTime := time.Date(2025, 9, 15, 0, 0, 0, 0, time.Local) + if aichangeFaceInfo.CreatedAt.Before(oldOrderTime) && + (aichangeFaceInfo.Status == aichangefaceimgmod.SUCCESS || aichangeFaceInfo.Status == aichangefaceimgmod.FAILURE || aichangeFaceInfo.Status == aichangefaceimgmod.REFUND) { + // 以前的单子如果处理过了就新下资源和状态 + set := bson.M{"newPic": []string{strings.TrimSpace(req.ImgUrl)}} + if req.Msg == "操作成功" { + set["status"] = aichangefaceimgmod.SUCCESS + } else { + set["remark"] = req.Msg + set["status"] = aichangefaceimgmod.REFUND + } + _ = aichangefaceimgmod.Update(nil, id, set) + + return stderr.Success + } + + if aichangeFaceInfo.Status != aichangefaceimgmod.StatusSubmit { + log.Error("AI图片换脸-重复回调或者状态不正确", log.Any("id", aichangeFaceInfo.ID.Hex()), log.Any("status", aichangeFaceInfo.Status)) + return stderr.Success + } + + var status = aichangefaceimgmod.SUCCESS + //处理金币退款以及后续操作 + set := bson.M{} + if req.ImgUrl != "" { + set["newPic"] = append([]string{}, strings.TrimSpace(req.ImgUrl)) + } + if req.Msg != "操作成功" { + set["remark"] = req.Msg + status = aichangefaceimgmod.REFUND + } + set["status"] = status + switch status { + case aichangefaceimgmod.SUCCESS: + transitioned, updateErr := aichangefaceimgmod.TransitionStatus(nil, id, aichangefaceimgmod.StatusSubmit, aichangefaceimgmod.SUCCESS, set) + if updateErr != nil { + return stderr.ErrDbUpdateError + } + if !transitioned { + return stderr.Success + } + common.Go(func() { + if aichangeFaceInfo.ShareStatus != 1 { + return + } + // 新增一个分享 + alPlaza := aiplazamod.AiPlaza{ + Type: 1, + OrderId: aichangeFaceInfo.ID, + Template: aichangeFaceInfo.MId, + Uid: aichangeFaceInfo.UID, + Title: aichangeFaceInfo.ShareTitle, + OriginalImage: aichangeFaceInfo.ModPic, + OriginalVideo: "", + OriginContent: "", + GenerateImage: req.ImgUrl, + GenerateVideo: "", + Status: aiplazamod.DefaultStatus, + } + _, err = aiplazamod.Insert(nil, alPlaza) + if err != nil { + log.Error("新增ai广场帖子失败", log.Any("alPlaza", alPlaza), log.E(err)) + return + } + }) + return stderr.Success + case aichangefaceimgmod.REFUND: + //换脸失败,进行退款操作 + // 失败处理 退金币 + deAmt := -aichangeFaceInfo.DebitAmountCoin + deIncome := -aichangeFaceInfo.DebitIncomeCoin + uid := aichangeFaceInfo.UID + if err = webg.VideoDB.Trans(func(tool *db.MongoTool) error { + transitioned, transitionErr := aichangefaceimgmod.TransitionStatus(tool, id, aichangefaceimgmod.StatusSubmit, aichangefaceimgmod.REFUND, set) + if transitionErr != nil { + return transitionErr + } + if !transitioned { + return nil + } + var tl []txnmod.TransactionLog + // 根据是否是免费次数退回 + if aichangeFaceInfo.Count > 0 || aichangeFaceInfo.IsFreeTimes { + deCount := aichangeFaceInfo.Count + if aichangeFaceInfo.Count == 0 { + deCount = 1 + } + // 加钱包余额 + wallet, wErr := walletmod.DebitAiFreeTimes(tool, -deCount, uid) + if wErr != nil { + log.Error(fmt.Sprintf("Handle AiChangeFaceImg Edit walletmod.DebitAiFreeTimes error:%+v:", wErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: deCount, + ActualAmount: float64(deCount), + TranType: txnmod.AiChangeFaceImgDebitFreeTimesReturn.Key(), + TranTypeInt: int64(txnmod.AiChangeFaceImgDebitFreeTimesReturn), + Desc: fmt.Sprintf("AI图片换脸免费次数退返[%d次]", deCount), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + + if aichangeFaceInfo.DebitAmountCoin > 0 || aichangeFaceInfo.DebitIncomeCoin > 0 { + // 加钱包余额 + wallet, wErr := walletmod.ReturnAmountAndIncome(tool, deAmt, deIncome, uid) + if wErr != nil { + log.Error(fmt.Sprintf("Handle AiChangeFaceImg Edit walletmod.DebitAmount error:%+v:", wErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + if aichangeFaceInfo.DebitIncomeCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -deIncome, + ActualAmount: float64(-deIncome), + TranType: txnmod.AiChangeFaceImgReturnIncomeGold.Key(), + TranTypeInt: int64(txnmod.AiChangeFaceImgReturnIncomeGold), + Desc: fmt.Sprintf("AI图片换脸收益金币退返-%d", -deIncome), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if aichangeFaceInfo.DebitAmountCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -deAmt, + ActualAmount: float64(-deAmt), + TranType: txnmod.AiChangeFaceImgReturnGold.Key(), + TranTypeInt: int64(txnmod.AiChangeFaceImgReturnGold), + Desc: fmt.Sprintf("AI图片换脸金币退返-%d", -deAmt), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + } + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(tool, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle AiChangeFaceImg Edit txnmod.InsertManyTransactionLog error:%+v:", txnErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + } + + return nil + }); err != nil { + log.Error(fmt.Sprintf("Handle AiChangeFaceImg Edit Trans error:%+v;uid:%v;", err, uid)) + return stderr.ErrDbUpdateError + } + } + return stderr.Success +} diff --git a/web/service/ai_changeface_ser/ai_changeface.go b/web/service/ai_changeface_ser/ai_changeface.go new file mode 100644 index 0000000..8f027d2 --- /dev/null +++ b/web/service/ai_changeface_ser/ai_changeface.go @@ -0,0 +1,540 @@ +package ai_changeface_ser + +import ( + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/aichangefacemod" + "91porn-server/models/v/aichangefacevidmod" + "91porn-server/models/v/aiplazamod" + "91porn-server/models/v/backpackmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/walletmod" + "91porn-server/web/proto" + "91porn-server/web/webg" + "fmt" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func List(id *primitive.ObjectID, req *aichangefacemod.ListRequest) ([]aichangefacemod.AiChangeFaceWebResp, int64, bool, error) { + limit := req.Limit() + acfs, count, err := aichangefacemod.WebList(id, req.Uid, req.Status, req.VidID, int(req.Skip()), int(limit+1)) + if err != nil { + return nil, 0, false, err + } + hasNext := false + if len(acfs) > int(limit) { + acfs = acfs[:limit] + hasNext = true + } + acfsLen := len(acfs) + acfwrs := make([]aichangefacemod.AiChangeFaceWebResp, acfsLen) + for i, v := range acfs { + acfwrs[i] = aichangefacemod.AiChangeFaceWebResp{ + ID: v.ID, + Uid: v.Uid, + Pic: v.Pic, + ModId: v.VidID, + ModUrl: v.ModVideo, + ModTitle: v.ModTitle, + Coin: v.Coin, + IncomeCoin: v.IncomeCoin, + Status: v.Status, + Cover: v.Cover, + Url: v.Url, + Remark: v.Remark, + Operator: v.Operator, + CreatedAt: v.CreatedAt, + UpdatedAt: v.UpdatedAt, + } + } + return acfwrs, count, hasNext, nil +} + +func Update(act string, req *aichangefacemod.EditAiChangeFace) stderr.Code { + // 获取Ai记录 + data, err := aichangefacemod.FindByID(nil, req.ID) + if err != nil { + return stderr.ErrDbQueryError + } + + // 如果已经退款,直接返回,避免重复扣款 + if *req.Status == aichangefacemod.StatusRefund && data.Status == aichangefacemod.StatusRefund { + return stderr.Success + } + + switch *req.Status { + case aichangefacemod.StatusGenning: // 未完成或完成 + if err = aichangefacemod.Update(nil, req.ID, req.Update(act)); err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success + case aichangefacemod.StatusComplete: + if req.Url == nil || *req.Url == "" { + return stderr.ChangeFaceSuccessWithoutUrl + } + if err = aichangefacemod.Update(nil, req.ID, req.Update(act)); err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success + case aichangefacemod.StatusRefund: // 退款 + if err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + var tl []txnmod.TransactionLog + updateSet, _ := req.Update(act)["$set"].(bson.M) + transitioned, transitionErr := aichangefacemod.TransitionStatus(t, req.ID, data.Status, aichangefacemod.StatusRefund, updateSet) + if transitionErr != nil { + return transitionErr + } + if !transitioned { + return nil + } + + // 折扣券返还 + if len(data.Discount) > 0 { + if _, err = backpackmod.UnuseManyGoods(t, data.Discount); err != nil { + return err + } + } + + if data.Coin > 0 || data.IncomeCoin > 0 { + // 退款进入钱包余额 + wallet, err := walletmod.ReturnAmountAndIncome(t, -data.Coin, -data.IncomeCoin, data.Uid) + if err != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Edit walletmod.DebitAmount error:%+v:", err), log.Any("uid", data.Uid)) + return err + } + + if data.Coin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: data.Uid, + Amount: data.Coin, + ActualAmount: float64(data.Coin), + TranType: txnmod.AiChangefaceDebitGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiChangefaceDebitGoldReturn), + Desc: fmt.Sprintf("AI换脸金币退返-%d", data.Coin), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + + if data.IncomeCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: data.Uid, + Amount: data.IncomeCoin, + ActualAmount: float64(data.IncomeCoin), + TranType: txnmod.AiChangefaceDebitIncomeGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiChangefaceDebitIncomeGoldReturn), + Desc: fmt.Sprintf("AI换收益金币退返-%d", data.IncomeCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + } + + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(t, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle AiChangeface Generate txnmod.InsertManyTransactionLog error:%+v:", txnErr), log.Any("uid", data.Uid)) + return stderr.ErrDbQueryError + } + } + return nil + }); err != nil { + if e, ok := err.(stderr.Code); ok { + return e + } + return stderr.ErrDbQueryError + } + return stderr.Success + default: + return stderr.ErrParamError + } +} + +func UpdateNew(act string, req *aichangefacemod.EditAiChangeFace) stderr.Code { + // 获取Ai记录 + data, err := aichangefacemod.FindByID(nil, req.ID) + if err != nil { + return stderr.ErrDbQueryError + } + if *req.Status == aichangefacemod.StatusSubmit && data.Status == aichangefacemod.StatusSubmit { + return stderr.Success + } + if *req.Status == aichangefacemod.StatusRefund && data.Status == aichangefacemod.StatusRefund { + return stderr.Success + } + set := bson.M{} + set["status"] = req.Status + if req.Cover != nil { + set["cover"] = strings.TrimSpace(*req.Cover) + } + if req.Url != nil { + set["url"] = strings.TrimSpace(*req.Url) + } + if req.Remark != nil { + set["remark"] = *req.Remark + } + if len(set) == 0 { // 没有做任何修改 + return stderr.Success + } + set["operator"] = act // 记录操作员 + set["updatedAt"] = time.Now() + switch *req.Status { + case aichangefacemod.StatusGenning: // 未完成或完成 + if err = aichangefacemod.Update(nil, req.ID, bson.M{"$set": set}); err != nil { + return stderr.ErrDbUpdateError + } + case aichangefacemod.StatusSubmit: + //提交订单到第三方 + notifyURL, notifyErr := webg.AICallbackURL("/api/web/admin/ai/changeface/callback") + if notifyErr != nil { + log.Error("AI视频换脸回调地址配置异常", log.E(notifyErr)) + return stderr.Failure + } + httpReq := proto.AiChangeFaceReq{ + AppId: int(commod.KFK_APPID), + OrderNumAPP: data.ID.Hex(), + Title: data.ModTitle, + SourceImageUrl: data.Pic[0], + NotifyUrl: notifyURL, + } + url := webg.Conf.URL.AiServer + "/api/changeface/prd/changeFaceByVideo" + //第三方请求地址 + var resp proto.AiChangeFaceResp + codeJ, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, httpReq) + if err != nil { + log.Error("CreateChangeFace httputil.DefaultClientPostJsonWithResp err", log.E(err)) + return stderr.Failure + } + if codeJ != 200 { + log.Error("CreateChangeFace req http code error", log.Any("code", codeJ)) + return stderr.Failure + } + if resp.Code == 40000 { + return stderr.ChangeFaceNoModule + } + if err = aichangefacemod.Update(nil, req.ID, bson.M{"$set": set}); err != nil { + return stderr.ErrDbUpdateError + } + case aichangefacemod.StatusRefund: // 退款 + if err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + transitioned, transitionErr := aichangefacemod.TransitionStatus(t, req.ID, data.Status, aichangefacemod.StatusRefund, set) + if transitionErr != nil { + return transitionErr + } + if !transitioned { + return nil + } + // 折扣券返还 + if len(data.Discount) > 0 { + if _, err = backpackmod.UnuseManyGoods(t, data.Discount); err != nil { + return err + } + } + // 退款 + if data.Coin == 0 && data.IncomeCoin == 0 { // 总金额为0则不需要退款 + return nil + } + // 退款进入钱包余额 + wallet, err := walletmod.ReturnAmountAndIncome(t, -data.Coin, -data.IncomeCoin, data.Uid) + if err != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Edit walletmod.DebitAmount error:%+v:", err), log.Any("uid", data.Uid)) + return err + } + // 添加退款记录 + totalCoin := data.Coin + data.IncomeCoin + if err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: data.Uid, + Amount: totalCoin, + ActualAmount: float64(totalCoin), + TranType: txnmod.AiChangefaceDebitGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiChangefaceDebitGoldReturn), + Desc: fmt.Sprintf("AI换脸金币退返-%d", totalCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }); err != nil { + log.Error(fmt.Sprintf("Handle AiChangeface Edit txnmod.InsertTransactionLog error:%+v:", err), log.Any("uid", data.Uid)) + return err + } + return nil + }); err != nil { + if e, ok := err.(stderr.Code); ok { + return e + } + return stderr.ErrDbQueryError + } + default: + return stderr.ErrParamError + } + return stderr.Success +} + +func AddMod(acfv aichangefacevidmod.AddAiChangeFaceVidReq) error { + now := time.Now() + return aichangefacevidmod.AddAiChangeFaceVid(aichangefacevidmod.AiChangeFaceVidMod{ + Title: acfv.Title, + SourceURL: acfv.SourceURL, + Status: acfv.Status, + PlayTime: acfv.PlayTime, + Cover: acfv.Cover, + Type: acfv.Type, + NewUrl: acfv.NewUrl, + Coin: acfv.Coin, + VipCoin: acfv.VipCoin, + StyleType: acfv.StyleType, + ModuleType: acfv.ModuleType, + HotMark: acfv.HotMark, + HotValue: acfv.HotValue, + Sort: acfv.Sort, + SceneType: acfv.SceneType, + CreatedAt: now, + UpdatedAt: now, + }) +} + +func UpdateMod(eacfv aichangefacevidmod.EditAiChangeFaceVid) error { + set := bson.M{} + if eacfv.Title != nil { + set["title"] = *eacfv.Title + } + if eacfv.SourceURL != nil { + set["sourceURL"] = *eacfv.SourceURL + } + if eacfv.Status != nil { + set["status"] = *eacfv.Status + } + if eacfv.NewUrl != nil { + set["newUrl"] = *eacfv.NewUrl + } + if eacfv.PlayTime != nil { + set["playTime"] = *eacfv.PlayTime + } + if eacfv.Cover != nil { + set["cover"] = *eacfv.Cover + } + if eacfv.Type != nil { + set["type"] = *eacfv.Type + } + if eacfv.Coin != nil { + set["coin"] = *eacfv.Coin + } + if eacfv.VipCoin != nil { + set["vipCoin"] = *eacfv.VipCoin + } + if eacfv.ModuleType != nil { + set["moduleType"] = *eacfv.ModuleType + } + if eacfv.HotValue != nil { + set["hotValue"] = *eacfv.HotValue + } + if eacfv.HotMark != nil { + set["hotMark"] = *eacfv.HotMark + } + if eacfv.Sort != nil { + set["sort"] = *eacfv.Sort + } + if eacfv.SceneType != nil { + set["sceneType"] = *eacfv.SceneType + } + if len(set) == 0 { + return nil + } + set["updatedAt"] = time.Now() + return aichangefacevidmod.UpdateAiChangeFaceVid(eacfv.ID, bson.M{"$set": set}) +} + +func ModList(req *aichangefacevidmod.WebListRequest) (interface{}, stderr.Code) { + var ( + data = map[string]interface{}{ + "list": []interface{}{}, + "count": 0, + } + err error + ) + + if err != nil { + return nil, stderr.ErrParamError + } + count, err := aichangefacevidmod.CountDocument(req.Filter()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + if count == 0 { + return data, stderr.Success + } + list, err := aichangefacevidmod.QueryAllDocument(req.Filter(), req.Options()) + if err != nil { + return data, stderr.ErrDbQueryError + } + data["count"] = count + data["list"] = list + return data, stderr.Success +} + +func CallBackOrder(req *aichangefacevidmod.CallbackReq) stderr.Code { + //根据订单号更新ai换脸订单 + id, _ := primitive.ObjectIDFromHex(req.AppOrderNum) + //查询该笔订单是否存在 + aichangeFaceInfo, err := aichangefacemod.FindByID(nil, id) + if aichangeFaceInfo == nil || err != nil { + return stderr.ErrDbQueryError + } + oldOrderTime := time.Date(2025, 9, 15, 0, 0, 0, 0, time.Local) + if aichangeFaceInfo.CreatedAt.Before(oldOrderTime) && + (aichangeFaceInfo.Status == aichangefacemod.StatusComplete || aichangeFaceInfo.Status == aichangefacemod.StatusRefund) { + // 以前的单子如果处理过了就新下资源和状态 + set := bson.M{"url": strings.TrimSpace(req.ImgUrl)} + if req.Msg == "操作成功" { + set["status"] = aichangefacemod.StatusComplete + } else { + set["remark"] = req.Msg + set["status"] = aichangefacemod.StatusRefund + } + _ = aichangefacemod.Update(nil, id, bson.M{"$set": set}) + + return stderr.Success + } + if aichangeFaceInfo.Status != aichangefacemod.StatusSubmit { + log.Error("AI视频换脸-重复回调或者状态不正确", log.Any("id", aichangeFaceInfo.ID.Hex()), log.Any("status", aichangeFaceInfo.Status)) + return stderr.Success + } + + var status = aichangefacemod.StatusComplete + //处理金币退款以及后续操作 + set := bson.M{} + if req.ImgUrl != "" { + set["url"] = strings.TrimSpace(req.ImgUrl) + } + if req.Msg != "操作成功" { + set["remark"] = req.Msg + status = aichangefacemod.StatusRefund + } + set["status"] = status + set["updatedAt"] = time.Now() + switch status { + case aichangefacemod.StatusComplete: + transitioned, updateErr := aichangefacemod.TransitionStatus(nil, id, aichangefacemod.StatusSubmit, aichangefacemod.StatusComplete, set) + if updateErr != nil { + return stderr.ErrDbUpdateError + } + if !transitioned { + return stderr.Success + } + common.Go(func() { + if aichangeFaceInfo.ShareStatus != 1 { + return + } + // 新增一个分享 + alPlaza := aiplazamod.AiPlaza{ + Type: 2, + OrderId: aichangeFaceInfo.ID, + Template: aichangeFaceInfo.VidID.Hex(), + Uid: aichangeFaceInfo.Uid, + Title: aichangeFaceInfo.ShareTitle, + OriginalImage: "", + OriginalVideo: aichangeFaceInfo.ModVideo, + OriginalVideoCover: aichangeFaceInfo.ModCover, + OriginContent: "", + GenerateVideo: req.ImgUrl, + GenerateVideoCover: aichangeFaceInfo.Pic[0], + Status: aiplazamod.DefaultStatus, + } + if len(aichangeFaceInfo.Pic) > 0 { + alPlaza.GenerateVideoCover = aichangeFaceInfo.Pic[0] + } + _, err = aiplazamod.Insert(nil, alPlaza) + if err != nil { + log.Error("新增ai广场帖子失败", log.Any("alPlaza", alPlaza), log.E(err)) + return + } + }) + return stderr.Success + case aichangefacemod.StatusRefund: // 退款 + if err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + transitioned, transitionErr := aichangefacemod.TransitionStatus(t, id, aichangefacemod.StatusSubmit, aichangefacemod.StatusRefund, set) + if transitionErr != nil { + return transitionErr + } + if !transitioned { + return nil + } + // 折扣券返还 + if len(aichangeFaceInfo.Discount) > 0 { + if _, err = backpackmod.UnuseManyGoods(t, aichangeFaceInfo.Discount); err != nil { + return err + } + } + // 退款 + if aichangeFaceInfo.Coin == 0 && aichangeFaceInfo.IncomeCoin == 0 { // 总金额为0则不需要退款 + return nil + } + // 退款进入钱包余额 + wallet, err := walletmod.ReturnAmountAndIncome(t, -aichangeFaceInfo.Coin, -aichangeFaceInfo.IncomeCoin, aichangeFaceInfo.Uid) + if err != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Edit walletmod.DebitAmount error:%+v:", err), log.Any("uid", aichangeFaceInfo.Uid)) + return err + } + // 添加退款记录 + totalCoin := aichangeFaceInfo.Coin + aichangeFaceInfo.IncomeCoin + if err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: aichangeFaceInfo.Uid, + Amount: totalCoin, + ActualAmount: float64(totalCoin), + TranType: txnmod.AiChangefaceDebitGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiChangefaceDebitGoldReturn), + Desc: fmt.Sprintf("AI换脸金币退返-%d", totalCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }); err != nil { + log.Error(fmt.Sprintf("Handle AiChangeface Edit txnmod.InsertTransactionLog error:%+v:", err), log.Any("uid", aichangeFaceInfo.Uid)) + return err + } + return nil + }); err != nil { + if e, ok := err.(stderr.Code); ok { + return e + } + return stderr.ErrDbQueryError + } + return stderr.Success + default: + return stderr.ErrParamError + } +} + +type BatchUpdateReq struct { + IDS []string `json:"ids" bson:"ids"` // AI模版IDS + MID string `json:"mid" bson:"mid"` // 模块ID +} + +func (q *BatchUpdateReq) Filter() primitive.M { + filter := bson.M{} + var ids []primitive.ObjectID + for _, id := range q.IDS { + objectID, _ := primitive.ObjectIDFromHex(id) + ids = append(ids, objectID) + } + + if len(ids) > 0 { + filter["_id"] = bson.M{"$in": ids} + } + return filter +} + +func (q *BatchUpdateReq) UpdateCond() primitive.M { + cond := bson.M{} + cond["mid"] = q.MID + cond["updatedAt"] = time.Now() + return cond +} + +func (q *BatchUpdateReq) BatchUpdateMod() error { + return aichangefacevidmod.UpdateManyByCond(q.Filter(), bson.M{"$set": q.UpdateCond()}) +} diff --git a/web/service/ai_changeface_ser/scene.go b/web/service/ai_changeface_ser/scene.go new file mode 100644 index 0000000..ca18378 --- /dev/null +++ b/web/service/ai_changeface_ser/scene.go @@ -0,0 +1,41 @@ +package ai_changeface_ser + +import "91porn-server/models/v/aiimagetovideomod" + +type AllSceneListReq struct { +} + +type AllSceneResp struct { + Name string `json:"name" bson:"name"` // 名称 + Value int `json:"value" bson:"value"` // 值 +} + +func (in *AllSceneListReq) All() (data []AllSceneResp, err error) { + var ret []AllSceneResp + ret = append(ret, AllSceneResp{ + Name: aiimagetovideomod.SceneTagLiftShirt.String(), + Value: int(aiimagetovideomod.SceneTagLiftShirt), + }, AllSceneResp{ + Name: aiimagetovideomod.SceneTagTearClothes.String(), + Value: int(aiimagetovideomod.SceneTagTearClothes), + }, AllSceneResp{ + Name: aiimagetovideomod.SceneTagCumOnFace.String(), + Value: int(aiimagetovideomod.SceneTagCumOnFace), + }, AllSceneResp{ + Name: aiimagetovideomod.SceneOFace.String(), + Value: int(aiimagetovideomod.SceneOFace), + }, AllSceneResp{ + Name: aiimagetovideomod.SceneTagKiss.String(), + Value: int(aiimagetovideomod.SceneTagKiss), + }, AllSceneResp{ + Name: aiimagetovideomod.SceneTagSuckBreasts.String(), + Value: int(aiimagetovideomod.SceneTagSuckBreasts), + }, AllSceneResp{ + Name: aiimagetovideomod.SceneRevealed.String(), + Value: int(aiimagetovideomod.SceneRevealed), + }, AllSceneResp{ + Name: aiimagetovideomod.SceneLargerChest.String(), + Value: int(aiimagetovideomod.SceneLargerChest), + }) + return ret, nil +} diff --git a/web/service/ai_image_to_video_ser/ai_image_to_video.go b/web/service/ai_image_to_video_ser/ai_image_to_video.go new file mode 100755 index 0000000..bb7f1d5 --- /dev/null +++ b/web/service/ai_image_to_video_ser/ai_image_to_video.go @@ -0,0 +1,311 @@ +package ai_image_to_video_ser + +import ( + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/aiimagetovideomod" + "91porn-server/models/v/aiplazamod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/walletmod" + "91porn-server/web/webg" + "errors" + "fmt" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + + "strings" + "time" +) + +type WebListReq struct { + ID *string `json:"id" form:"id"` // ID + UID *uint64 `json:"uid" form:"uid"` // 用户ID + Status *int `json:"status" form:"status"` // 状态 + SceneType *int `json:"sceneType" form:"sceneType"` // 模版场景类型 + commod.Page +} + +func (q *WebListReq) Filter() primitive.M { + filter := bson.M{} + if q.ID != nil { + id, _ := primitive.ObjectIDFromHex(*q.ID) + filter["_id"] = id + } + if q.Status != nil { + filter["status"] = q.Status + } + if q.UID != nil { + filter["uid"] = q.UID + } + if q.SceneType != nil { + filter["sceneType"] = q.SceneType + } + return filter +} + +type WebListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []aiimagetovideomod.AiImageToVideo `json:"list"` +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res WebListRes, err error) { + res.List, res.Total, res.HasNext, err = aiimagetovideomod.GetList(q.Filter(), int64(q.Skip()), int64(q.Limit()), q.GetSort()) + + return +} + +type WebUpdateReq struct { + ID string `json:"id" binding:"required"` // ID + Status *int `json:"status"` // 状态 + Remark *string `json:"remark"` // 备注 +} + +func (p *WebUpdateReq) UpdateCond() primitive.M { + data := make(map[string]interface{}) + if p.Status != nil { + data["status"] = *p.Status + } + if p.Remark != nil { + data["remark"] = *p.Remark + } + data["updatedAt"] = time.Now() + return data +} + +// Update 更新数据 +func (p *WebUpdateReq) Update() error { + id, _ := primitive.ObjectIDFromHex(p.ID) + data, err := aiimagetovideomod.GetInfo(id) + if err != nil { + return err + } + + if data.ID.IsZero() { + return errors.New("data is null") + } + + if p.Status != nil { + if data.Status == int(aiimagetovideomod.StatusRefunded) || data.Status == int(aiimagetovideomod.StatusGenerationFailed) { + return errors.New("Please do not refund repeatedly.") + } + + switch aiimagetovideomod.AiImageToVideoStatus(*p.Status) { + case aiimagetovideomod.StatusRefunded: + if err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + if _, err = aiimagetovideomod.UpdateByID(t, id, p.UpdateCond()); err != nil { + return err + } + + // 退款 + if data.DebitAmountCoin == 0 && data.DebitIncomeCoin == 0 { // 总金额为0则不需要退款 + return nil + } + + // 退款进入钱包余额 + wallet, err := walletmod.ReturnAmountAndIncome(t, -data.DebitAmountCoin, -data.DebitIncomeCoin, data.UID) + if err != nil { + log.Error(fmt.Sprintf("Handle Edit walletmod.DebitAmount error:%+v:", err), log.Any("uid", data.UID)) + return err + } + + var tl []txnmod.TransactionLog + if data.DebitAmountCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: data.UID, + Amount: -data.DebitAmountCoin, + ActualAmount: float64(-data.DebitAmountCoin), + TranType: txnmod.AiImageToVideoDebitGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiImageToVideoDebitGoldReturn), + Desc: fmt.Sprintf("退还AI图生视频金币-%d", data.DebitAmountCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if data.DebitIncomeCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: data.UID, + Amount: -data.DebitIncomeCoin, + ActualAmount: float64(-data.DebitIncomeCoin), + TranType: txnmod.AiImageToVideoDebitIncomeGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiImageToVideoDebitIncomeGoldReturn), + Desc: fmt.Sprintf("退还AI图生视频收益金币-%d", data.DebitIncomeCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(t, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle imagetovideo Edit txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", data.UID)) + return stderr.ErrDbQueryError + } + } + return nil + }); err != nil { + if e, ok := err.(stderr.Code); ok { + return e + } + return stderr.ErrDbQueryError + } + default: + } + } + if _, err = aiimagetovideomod.UpdateByID(nil, id, p.UpdateCond()); err != nil { + return err + } + return nil +} + +type WebDeleteReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` +} + +// Delete 删除数据 +func (p *WebDeleteReq) Delete() error { + if err := aiimagetovideomod.DeleteByID(nil, p.ID); err != nil { + return err + } + + return nil +} + +// CallbackReq 换脸订单回调请求 +type CallbackReq struct { + ImgUrl string `json:"imgUrl"` // AI图生视频图片地址 + AppOrderNum string `json:"appOrderNum"` // app订单号 + Status int `json:"status"` // 订单状态 1:成功,2:失败 + Msg string `json:"msg"` // 消息 +} + +func (p *CallbackReq) Callback() error { + id, _ := primitive.ObjectIDFromHex(p.AppOrderNum) + // 查询该笔订单是否存在 + data, err := aiimagetovideomod.GetInfo(id) + if err != nil { + return stderr.ErrDbQueryError + } + + if data.ID.IsZero() { + return errors.New("ai imagetovideo data is null") + } + + if data.Status != int(aiimagetovideomod.StatusSubmitted) { + log.Error("AI图生视频-重复回调或者状态不正确", log.Any("id", data.ID.Hex()), log.Any("status", data.Status)) + return nil + } + + var status = aiimagetovideomod.StatusGenerationSuccess + + // 处理金币退款以及后续操作 + set := bson.M{} + if p.ImgUrl != "" { + set["newImgUrl"] = strings.TrimSpace(p.ImgUrl) + } + if p.Status != 1 { + set["remark"] = p.Msg + set["newImgUrl"] = "" + status = aiimagetovideomod.StatusGenerationFailed + } + set["status"] = status + set["updatedAt"] = time.Now() + + switch status { + case aiimagetovideomod.StatusGenerationSuccess: + if _, err = aiimagetovideomod.UpdateByID(nil, id, set); err != nil { + return stderr.ErrDbUpdateError + } + if data.ShareStatus == 1 { + common.Go(func() { + // 新增一个分享 + alPlaza := aiplazamod.AiPlaza{ + Type: 4, + OrderId: data.ID, + Template: "", + Uid: data.UID, + Title: data.ShareTitle, + OriginalImage: data.ImgUrl, + OriginalVideo: "", + OriginContent: "", + GenerateImage: p.ImgUrl, + GenerateVideo: "", + Status: aiplazamod.DefaultStatus, + } + _, err = aiplazamod.Insert(nil, alPlaza) + if err != nil { + log.Error("新增ai广场帖子失败", log.Any("alPlaza", alPlaza), log.E(err)) + return + } + }) + } + + return nil + case aiimagetovideomod.StatusGenerationFailed: // 退款 + if err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + if _, err = aiimagetovideomod.UpdateByID(t, id, set); err != nil { + return err + } + + // 退款 + if data.DebitAmountCoin == 0 && data.DebitIncomeCoin == 0 { // 总金额为0则不需要退款 + return nil + } + + // 退款进入钱包余额 + wallet, err := walletmod.ReturnAmountAndIncome(t, -data.DebitAmountCoin, -data.DebitIncomeCoin, data.UID) + if err != nil { + log.Error(fmt.Sprintf("Handle Callback Edit walletmod.DebitAmount error:%+v:", err), log.Any("uid", data.UID)) + return err + } + + var tl []txnmod.TransactionLog + if data.DebitAmountCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: data.UID, + Amount: -data.DebitAmountCoin, + ActualAmount: float64(-data.DebitAmountCoin), + TranType: txnmod.AiImageToVideoDebitGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiImageToVideoDebitGoldReturn), + Desc: fmt.Sprintf("退还AI图生视频金币-%d", data.DebitAmountCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if data.DebitIncomeCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: data.UID, + Amount: -data.DebitIncomeCoin, + ActualAmount: float64(-data.DebitIncomeCoin), + TranType: txnmod.AiImageToVideoDebitIncomeGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiImageToVideoDebitIncomeGoldReturn), + Desc: fmt.Sprintf("退还AI图生视频收益金币-%d", data.DebitIncomeCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(t, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle imagetovideo Callback txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", data.UID)) + return stderr.ErrDbQueryError + } + } + return nil + }); err != nil { + if e, ok := err.(stderr.Code); ok { + return e + } + return stderr.ErrDbQueryError + } + return nil + default: + } + return nil +} diff --git a/web/service/ai_mate_ser/ai_mate.go b/web/service/ai_mate_ser/ai_mate.go new file mode 100644 index 0000000..e3089b1 --- /dev/null +++ b/web/service/ai_mate_ser/ai_mate.go @@ -0,0 +1,45 @@ +package ai_mate_ser + +import ( + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/txnmod" + "fmt" + "go.mongodb.org/mongo-driver/bson" +) + +type AIMateTransactionInfo struct { + OrderId string `json:"orderId"` // 订单Id + Credit_delta string `json:"credit_delta"` // 此次操作积分数 + Post_credit string `json:"post_credit"` // 操作后剩余积分数 + Created_at string `json:"created_at"` // 操作时间 + Operation_type string `json:"operation_type"` // 操作类型 +} + +// GetBalance 获取AI伴侣用户交易记录 +func GetUserTransactionsList(uid uint64, pageNumber, pageSize uint64) (code stderr.Code, data map[string]interface{}) { + code = stderr.Success + + filter := bson.M{"tranTypeInt": bson.M{"$in": []txnmod.TransType{txnmod.AiMateChat, txnmod.AdminAiMateSet, txnmod.AiMateCurrencyExchange}}, "uid": uid} + + list, _, total, err := txnmod.GetCoinLogs(pageNumber, pageSize, filter) + if err != nil { + log.Error("ai_mate_ser GetCoinLogs", log.Any("err", err), log.Any("uid", uid)) + return + } + rsl := make([]AIMateTransactionInfo, 0) + for _, v := range list { + info := AIMateTransactionInfo{ + OrderId: v.ID.Hex(), + Credit_delta: fmt.Sprintf("%v", v.AiMatePoint), + Post_credit: fmt.Sprintf("%v", v.RealAiMatePoint), + Created_at: v.CreatedAt.String(), + Operation_type: v.TranType, + } + rsl = append(rsl, info) + } + data = map[string]interface{}{} + data["count"] = total + data["list"] = rsl + return +} diff --git a/web/service/ai_mate_v2_ser/ai_mate.go b/web/service/ai_mate_v2_ser/ai_mate.go new file mode 100644 index 0000000..4d29ec4 --- /dev/null +++ b/web/service/ai_mate_v2_ser/ai_mate.go @@ -0,0 +1,39 @@ +package ai_mate_v2_ser + +import ( + "context" + "strconv" + "time" + + "91porn-server/common/laosiji_app" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/web/webg" +) + +// GetUserOrderLogs 查询老司机AI女友V2第三方消费流水。 +func GetUserOrderLogs(ctx context.Context, uid, pageNumber, pageSize uint64, startTime, endTime time.Time) (code stderr.Code, data map[string]interface{}) { + resp, err := laosiji_app.GetAiMateOrderLogs(ctx, webg.Conf.Base.Env, laosiji_app.AiMateOrderLogsReq{ + AppID: commod.KFK_APPID, + UID: uid, + Page: int(pageNumber), + PageSize: int(pageSize), + StartTime: startTime, + EndTime: endTime, + }) + if err != nil { + log.Error("ai_mate_v2_ser GetUserOrderLogs", log.Any("err", err), log.Any("uid", uid)) + return stderr.Failure, nil + } + + total, _ := strconv.ParseInt(resp.Total, 10, 64) + items := resp.Items + if items == nil { + items = make([]laosiji_app.AiMateOrderLog, 0) + } + return stderr.Success, map[string]interface{}{ + "count": total, + "list": items, + } +} diff --git a/web/service/ai_template_module_ser/aitemplatemodule.go b/web/service/ai_template_module_ser/aitemplatemodule.go new file mode 100755 index 0000000..03e1765 --- /dev/null +++ b/web/service/ai_template_module_ser/aitemplatemodule.go @@ -0,0 +1,155 @@ +package ai_template_module_ser + +import ( + "time" + + "91porn-server/models/commod" + "91porn-server/models/v/aitemplatemodulemod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + Type *int `json:"type" form:"type"` // 模块类型 + Status *int `json:"status" form:"status"` // 状态 + commod.Page +} + +func (q *WebListReq) Filter() primitive.M { + filter := bson.M{} + if q.Status != nil { + filter["status"] = q.Status + } + if q.Type != nil { + filter["type"] = q.Type + } + return filter +} + +type WebListRes struct { + Total int64 `json:"total"` + List []aitemplatemodulemod.AiTemplateModule `json:"list"` +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res WebListRes, err error) { + sort := bson.D{{"sortNum", -1}} + res.List, res.Total, _, err = aitemplatemodulemod.GetList(q.Filter(), int64(q.Skip()), int64(q.Limit()), sort) + return +} + +type WebAllListReq struct { +} + +func (q *WebAllListReq) Filter() primitive.M { + filter := bson.M{} + + filter["status"] = 1 + filter["isDelete"] = false + return filter +} + +type WebAllListRes struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + Type int `json:"type" bson:"type"` // 模块类型 1:AI视频换脸,2:AI图片换脸 + Name string `json:"name" bson:"name"` // 模块名称 +} + +// GetList 获取列表 +func (q *WebAllListReq) GetList() (res []WebAllListRes, err error) { + var ret []WebAllListRes + sort := bson.D{{"sortNum", -1}} + data, err := aitemplatemodulemod.GetAll(q.Filter(), sort) + if err != nil { + return ret, err + } + + if data != nil && len(data) > 0 { + for _, d := range data { + ret = append(ret, WebAllListRes{ + ID: d.ID, + Name: d.Name, + Type: d.Type, + }) + } + } + return ret, nil +} + +type WebCreateReq struct { + Type int `json:"type" form:"type" binding:"required"` // 模块类型 + Name string `json:"name" form:"name" binding:"required"` // 模块名称 + Status int `json:"status" form:"status" binding:"required"` // 状态 + SortNum int `json:"sortNum" form:"sortNum" binding:"required"` // 模块排序 +} + +// Create 发布数据 +func (p *WebCreateReq) Create() error { + data := aitemplatemodulemod.AiTemplateModule{ + Type: p.Type, + Name: p.Name, + Status: p.Status, + SortNum: p.SortNum, + IsDelete: false, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // 创建数据 + if _, err := aitemplatemodulemod.Insert(nil, data); err != nil { + return err + } + + return nil +} + +type WebUpdateReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` + Type *int `json:"type"` // 模块类型 + Name *string `json:"name"` // 模块名称 + Status *int `json:"status"` // 状态 + SortNum *int `json:"sortNum"` // 模块排序 +} + +// Update 更新数据 +func (p *WebUpdateReq) Update() error { + _, err := aitemplatemodulemod.GetInfo(p.ID) + if err != nil { + return err + } + + data := make(map[string]interface{}) + if p.Type != nil { + data["type"] = *p.Type + } + if p.Name != nil { + data["name"] = *p.Name + } + if p.Status != nil { + data["status"] = *p.Status + } + if p.SortNum != nil { + data["sortNum"] = *p.SortNum + } + + data["updatedAt"] = time.Now() + + if _, err = aitemplatemodulemod.UpdateByID(nil, p.ID, data); err != nil { + return err + } + + return nil +} + +type WebDeleteReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` +} + +// Delete 删除数据 +func (p *WebDeleteReq) Delete() error { + if err := aitemplatemodulemod.DeleteByID(nil, p.ID); err != nil { + return err + } + + return nil +} diff --git a/web/service/ai_text_to_image_ser/aitexttoimage.go b/web/service/ai_text_to_image_ser/aitexttoimage.go new file mode 100755 index 0000000..70ce9ff --- /dev/null +++ b/web/service/ai_text_to_image_ser/aitexttoimage.go @@ -0,0 +1,325 @@ +package ai_text_to_image_ser + +import ( + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/aiimagetovideomod" + "91porn-server/models/v/aiplazamod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/walletmod" + "91porn-server/web/webg" + "errors" + "fmt" + "strings" + "time" + + "91porn-server/models/cache/aitexttoimagedata" + "91porn-server/models/commod" + "91porn-server/models/v/aitexttoimagemod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + ID *string `json:"id" form:"id"` // ID + UID *uint64 `json:"uid" form:"uid"` // 用户ID + Status *int `json:"status" form:"status"` // 状态 + commod.Page +} + +func (q *WebListReq) Filter() primitive.M { + filter := bson.M{} + if q.ID != nil { + id, _ := primitive.ObjectIDFromHex(*q.ID) + filter["_id"] = id + } + if q.Status != nil { + filter["status"] = q.Status + } + if q.UID != nil { + filter["uid"] = q.UID + } + return filter +} + +type WebListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []aitexttoimagemod.AiTextToImage `json:"list"` +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res WebListRes, err error) { + res.List, res.Total, res.HasNext, err = aitexttoimagemod.GetList(q.Filter(), int64(q.Skip()), int64(q.Limit()), q.GetSort()) + + return +} + +type WebUpdateReq struct { + ID string `json:"id" binding:"required"` // ID + Status *int `json:"status"` // 状态 + Remark *string `json:"remark"` // 备注 +} + +func (p *WebUpdateReq) UpdateCond() primitive.M { + data := make(map[string]interface{}) + if p.Status != nil { + data["status"] = *p.Status + } + if p.Remark != nil { + data["remark"] = *p.Remark + } + data["updatedAt"] = time.Now() + return data +} + +// Update 更新数据 +func (p *WebUpdateReq) Update() error { + id, _ := primitive.ObjectIDFromHex(p.ID) + data, err := aitexttoimagemod.GetInfo(id) + if err != nil { + return err + } + + if data.ID.IsZero() { + return errors.New("data is null") + } + + if p.Status != nil { + if data.Status == int(aitexttoimagemod.StatusRefunded) || data.Status == int(aiimagetovideomod.StatusGenerationFailed) { + return errors.New("Please do not refund repeatedly.") + } + + switch aitexttoimagemod.AiTextToImageStatus(*p.Status) { + case aitexttoimagemod.StatusRefunded: + if err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + if _, err = aitexttoimagemod.UpdateByID(t, id, p.UpdateCond()); err != nil { + return err + } + + // 退款 + if data.DebitAmountCoin == 0 && data.DebitIncomeCoin == 0 { // 总金额为0则不需要退款 + return nil + } + + // 退款进入钱包余额 + wallet, err := walletmod.ReturnAmountAndIncome(t, -data.DebitAmountCoin, -data.DebitIncomeCoin, data.UID) + if err != nil { + log.Error(fmt.Sprintf("Handle Edit walletmod.DebitAmount error:%+v:", err), log.Any("uid", data.UID)) + return err + } + + var tl []txnmod.TransactionLog + if data.DebitAmountCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: data.UID, + Amount: -data.DebitAmountCoin, + ActualAmount: float64(-data.DebitAmountCoin), + TranType: txnmod.AiTextToImageDebitGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiTextToImageDebitGoldReturn), + Desc: fmt.Sprintf("退还AI绘图金币-%d", data.DebitAmountCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if data.DebitIncomeCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: data.UID, + Amount: -data.DebitIncomeCoin, + ActualAmount: float64(-data.DebitIncomeCoin), + TranType: txnmod.AiTextToImageDebitIncomeGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiTextToImageDebitIncomeGoldReturn), + Desc: fmt.Sprintf("退还AI绘图收益金币-%d", data.DebitIncomeCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(t, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle imagetovideo Edit txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", data.UID)) + return stderr.ErrDbQueryError + } + } + return nil + }); err != nil { + if e, ok := err.(stderr.Code); ok { + return e + } + return stderr.ErrDbQueryError + } + default: + } + } + if _, err = aitexttoimagemod.UpdateByID(nil, id, p.UpdateCond()); err != nil { + return err + } + return nil +} + +type WebDeleteReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` +} + +// Delete 删除数据 +func (p *WebDeleteReq) Delete() error { + data, err := aitexttoimagemod.GetInfo(p.ID) + if err != nil { + return err + } + if err = validateDeleteStatus(data.Status); err != nil { + return err + } + + if err := aitexttoimagedata.DeleteData(nil, p.ID); err != nil { + return err + } + + return nil +} + +func validateDeleteStatus(status int) error { + if status == int(aitexttoimagemod.StatusOrderSuccess) || + status == int(aitexttoimagemod.StatusSubmitted) { + return stderr.AiGenningDelForbidden + } + return nil +} + +// CallbackReq 换脸订单回调请求 +type CallbackReq struct { + ImgUrl string `json:"imgUrl"` // AI图生视频图片地址 + AppOrderNum string `json:"appOrderNum"` // app订单号 + Status int `json:"status"` // 订单状态 1:成功,2:失败 + Msg string `json:"msg"` // 消息 +} + +func (p *CallbackReq) Callback() error { + id, _ := primitive.ObjectIDFromHex(p.AppOrderNum) + // 查询该笔订单是否存在 + data, err := aitexttoimagemod.GetInfo(id) + if err != nil { + return stderr.ErrDbQueryError + } + + if data.ID.IsZero() { + return errors.New("ai imagetovideo data is null") + } + + if data.Status != int(aitexttoimagemod.StatusSubmitted) { + log.Error("AI文生图-重复回调或者状态不正确", log.Any("id", data.ID.Hex()), log.Any("status", data.Status)) + return nil + } + + var status = aitexttoimagemod.StatusGenerationSuccess + + // 处理金币退款以及后续操作 + set := bson.M{} + if p.ImgUrl != "" { + set["newImgUrl"] = strings.TrimSpace(p.ImgUrl) + } + if p.Status != 1 { + set["remark"] = p.Msg + set["newImgUrl"] = "" + status = aitexttoimagemod.StatusGenerationFailed + } + set["status"] = status + set["updatedAt"] = time.Now() + + switch status { + case aitexttoimagemod.StatusGenerationSuccess: + if _, err = aitexttoimagemod.UpdateByID(nil, id, set); err != nil { + return stderr.ErrDbUpdateError + } + common.Go(func() { + if data.ShareStatus != 1 { + return + } + // 新增一个分享 + alPlaza := aiplazamod.AiPlaza{ + Type: 5, + OrderId: data.ID, + Template: "", + Uid: data.UID, + Title: data.ShareTitle, + OriginalImage: "", + OriginalVideo: "", + OriginContent: data.Text, + GenerateImage: p.ImgUrl, + GenerateVideo: "", + Status: aiplazamod.DefaultStatus, + } + _, err = aiplazamod.Insert(nil, alPlaza) + if err != nil { + log.Error("新增ai广场帖子失败", log.Any("alPlaza", alPlaza), log.E(err)) + return + } + }) + return nil + case aitexttoimagemod.StatusGenerationFailed: // 退款 + if err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + if _, err = aitexttoimagemod.UpdateByID(t, id, set); err != nil { + return err + } + + // 退款 + if data.DebitAmountCoin == 0 && data.DebitIncomeCoin == 0 { // 总金额为0则不需要退款 + return nil + } + + // 退款进入钱包余额 + wallet, err := walletmod.ReturnAmountAndIncome(t, -data.DebitAmountCoin, -data.DebitIncomeCoin, data.UID) + if err != nil { + log.Error(fmt.Sprintf("Handle Callback Edit walletmod.DebitAmount error:%+v:", err), log.Any("uid", data.UID)) + return err + } + + var tl []txnmod.TransactionLog + if data.DebitAmountCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: data.UID, + Amount: -data.DebitAmountCoin, + ActualAmount: float64(-data.DebitAmountCoin), + TranType: txnmod.AiTextToImageDebitGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiTextToImageDebitGoldReturn), + Desc: fmt.Sprintf("退还AI绘图金币-%d", data.DebitAmountCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if data.DebitIncomeCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: data.UID, + Amount: -data.DebitIncomeCoin, + ActualAmount: float64(-data.DebitIncomeCoin), + TranType: txnmod.AiTextToImageDebitIncomeGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiTextToImageDebitIncomeGoldReturn), + Desc: fmt.Sprintf("退还AI绘图收益金币-%d", data.DebitIncomeCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(t, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle ai text to image Callback txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", data.UID)) + return stderr.ErrDbQueryError + } + } + return nil + }); err != nil { + if e, ok := err.(stderr.Code); ok { + return e + } + return stderr.ErrDbQueryError + } + return nil + default: + } + return nil +} diff --git a/web/service/ai_text_to_novel_ser/aitexttonovel.go b/web/service/ai_text_to_novel_ser/aitexttonovel.go new file mode 100755 index 0000000..28ab3c9 --- /dev/null +++ b/web/service/ai_text_to_novel_ser/aitexttonovel.go @@ -0,0 +1,342 @@ +package ai_text_to_novel_ser + +import ( + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/cache/aitexttonoveldata" + "91porn-server/models/commod" + "91porn-server/models/v/aitexttonovelmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/walletmod" + "91porn-server/web/webg" + "errors" + "fmt" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + ID *string `json:"id" form:"id"` // ID + UID *uint64 `json:"uid" form:"uid"` // 用户ID + Status *int `json:"status" form:"status"` // 状态 + commod.Page +} + +func (q *WebListReq) Filter() primitive.M { + filter := bson.M{} + if q.ID != nil { + id, _ := primitive.ObjectIDFromHex(*q.ID) + filter["_id"] = id + } + if q.Status != nil { + filter["status"] = q.Status + } + if q.UID != nil { + filter["uid"] = q.UID + } + return filter +} + +func (q *WebListReq) Sort() primitive.D { + return bson.D{{Key: "createdAt", Value: -1}} +} + +type WebListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []aitexttonovelmod.AiTextToNovel `json:"list"` +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res WebListRes, err error) { + res.List, res.Total, res.HasNext, err = aitexttonovelmod.GetList(q.Filter(), int64(q.Skip()), int64(q.Limit()), q.Sort()) + return +} + +type WebUpdateReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` + UID *uint64 `json:"uid"` // 用户ID + Background *string `json:"background"` // 小说人物设定/故事背景等 + Scene *string `json:"scene"` // 地点场景 + Text *string `json:"text"` // 故事情节描述 + Other *string `json:"other"` // 小说其他要求 + ModelType *string `json:"modelType"` // AI小说模型 1,2 + Content *string `json:"content"` // AI生成小说正文 + Status *int `json:"status"` // 状态 + Coin *int64 `json:"coin"` // AI小说金币个数 + DebitAmountCoin *int64 `json:"debitAmountCoin"` // 此次AI小说扣除金币个数 + DebitIncomeCoin *int64 `json:"debitIncomeCoin"` // 此次AI小说扣除收益金币个数 + IsFreeTimes *bool `json:"isFreeTimes"` // 是否使用免费次数 + IsHide *bool `json:"isHide"` // 是否被用户隐藏 + Remark *string `json:"remark"` // 备注 + IsDelete *bool `json:"isDelete"` // 是否删除 +} + +// Update 更新数据 +func (p *WebUpdateReq) Update() error { + info, err := aitexttonovelmod.GetInfo(p.ID) + if err != nil { + return err + } + + if info.ID.IsZero() { + return errors.New("data is null") + } + + data := make(map[string]interface{}) + if p.UID != nil { + data["uid"] = *p.UID + } + if p.Background != nil { + data["background"] = *p.Background + } + if p.Scene != nil { + data["scene"] = *p.Scene + } + if p.Text != nil { + data["text"] = *p.Text + } + if p.Other != nil { + data["other"] = *p.Other + } + if p.ModelType != nil { + data["modelType"] = *p.ModelType + } + if p.Content != nil { + data["content"] = *p.Content + } + if p.Status != nil { + data["status"] = *p.Status + } + if p.Coin != nil { + data["coin"] = *p.Coin + } + if p.DebitAmountCoin != nil { + data["debitAmountCoin"] = *p.DebitAmountCoin + } + if p.DebitIncomeCoin != nil { + data["debitIncomeCoin"] = *p.DebitIncomeCoin + } + if p.IsFreeTimes != nil { + data["isFreeTimes"] = *p.IsFreeTimes + } + if p.IsHide != nil { + data["isHide"] = *p.IsHide + } + if p.Remark != nil { + data["remark"] = *p.Remark + } + if p.IsDelete != nil { + data["isDelete"] = *p.IsDelete + } + data["updatedAt"] = time.Now() + if p.Status != nil && *p.Status == 5 { + if info.Status == int(aitexttonovelmod.StatusRefunded) || info.Status == int(aitexttonovelmod.StatusGenerationFailed) { + return errors.New("Please do not refund repeatedly.") + } + + if err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + if _, err = aitexttonovelmod.UpdateByID(t, info.ID, data); err != nil { + return err + } + + // 退款 + if info.DebitAmountCoin == 0 && info.DebitIncomeCoin == 0 { // 总金额为0则不需要退款 + return nil + } + + // 退款进入钱包余额 + wallet, err := walletmod.ReturnAmountAndIncome(t, -info.DebitAmountCoin, -info.DebitIncomeCoin, info.UID) + if err != nil { + log.Error(fmt.Sprintf("Handle text to novel Edit walletmod.DebitAmount error:%+v:", err), log.Any("uid", info.UID)) + return err + } + var tl []txnmod.TransactionLog + if info.DebitAmountCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: info.UID, + Amount: info.DebitAmountCoin, + ActualAmount: float64(info.DebitAmountCoin), + TranType: txnmod.AiTextToNovelDebitGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiTextToNovelDebitGoldReturn), + Desc: fmt.Sprintf("退还AI小说金币-%d", info.DebitAmountCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if info.DebitIncomeCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: info.UID, + Amount: info.DebitIncomeCoin, + ActualAmount: float64(info.DebitIncomeCoin), + TranType: txnmod.AiTextToNovelDebitIncomeGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiTextToNovelDebitIncomeGoldReturn), + Desc: fmt.Sprintf("退还AI小说收益金币-%d", info.DebitIncomeCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(t, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle ai text to novel txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", info.UID)) + return stderr.ErrDbQueryError + } + } + return nil + }); err != nil { + if e, ok := err.(stderr.Code); ok { + return e + } + return stderr.ErrDbQueryError + } + return nil + } else { + if _, err = aitexttonoveldata.UpdateData(nil, p.ID.Hex(), data); err != nil { + } + return err + } +} + +type WebDeleteReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` +} + +// Delete 删除数据 +func (p *WebDeleteReq) Delete() error { + data, err := aitexttonovelmod.GetInfo(p.ID) + if err != nil { + return err + } + if err = validateDeleteStatus(data.Status); err != nil { + return err + } + + if err := aitexttonoveldata.DeleteData(nil, p.ID); err != nil { + return err + } + + return nil +} + +func validateDeleteStatus(status int) error { + if status == int(aitexttonovelmod.StatusOrderSuccess) || + status == int(aitexttonovelmod.StatusSubmitted) { + return stderr.AiGenningDelForbidden + } + return nil +} + +// CallbackReq 换脸订单回调请求 +type CallbackReq struct { + Content string `json:"content"` // AI小说内容 + AppOrderNum string `json:"appOrderNum"` // app订单号 + Status int `json:"status"` // 订单状态 1:成功,2:失败 + Msg string `json:"msg"` // 消息 +} + +func (p *CallbackReq) Callback() error { + id, _ := primitive.ObjectIDFromHex(p.AppOrderNum) + // 查询该笔订单是否存在 + data, err := aitexttonovelmod.GetInfo(id) + if err != nil { + return stderr.ErrDbQueryError + } + + if data.ID.IsZero() { + return errors.New("ai text to novel data is null") + } + + if data.Status != int(aitexttonovelmod.StatusSubmitted) { + log.Error("AI小说-重复回调或者状态不正确", log.Any("id", data.ID.Hex()), log.Any("status", data.Status)) + return nil + } + + var status = aitexttonovelmod.StatusGenerationSuccess + + // 处理金币退款以及后续操作 + set := bson.M{} + if p.Content != "" { + set["content"] = strings.TrimSpace(p.Content) + } + if p.Status != 1 { + set["remark"] = p.Msg + set["content"] = "" + status = aitexttonovelmod.StatusGenerationFailed + } + set["status"] = status + set["updatedAt"] = time.Now() + + switch status { + case aitexttonovelmod.StatusGenerationSuccess: + if _, err = aitexttonovelmod.UpdateByID(nil, id, set); err != nil { + return stderr.ErrDbUpdateError + } + return nil + case aitexttonovelmod.StatusGenerationFailed: // 退款 + if err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + if _, err = aitexttonovelmod.UpdateByID(t, id, set); err != nil { + return err + } + + // 退款 + if data.DebitAmountCoin == 0 && data.DebitIncomeCoin == 0 { // 总金额为0则不需要退款 + return nil + } + + // 退款进入钱包余额 + wallet, err := walletmod.ReturnAmountAndIncome(t, -data.DebitAmountCoin, -data.DebitIncomeCoin, data.UID) + if err != nil { + log.Error(fmt.Sprintf("Handle Callback Edit walletmod.DebitAmount error:%+v:", err), log.Any("uid", data.UID)) + return err + } + var tl []txnmod.TransactionLog + if data.DebitAmountCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: data.UID, + Amount: data.DebitAmountCoin, + ActualAmount: float64(data.DebitAmountCoin), + TranType: txnmod.AiTextToNovelDebitGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiTextToNovelDebitGoldReturn), + Desc: fmt.Sprintf("退还AI小说金币-%d", data.DebitAmountCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if data.DebitIncomeCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: data.UID, + Amount: data.DebitIncomeCoin, + ActualAmount: float64(data.DebitIncomeCoin), + TranType: txnmod.AiTextToNovelDebitIncomeGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiTextToNovelDebitIncomeGoldReturn), + Desc: fmt.Sprintf("退还AI小说收益金币-%d", data.DebitIncomeCoin), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(t, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle ai text to image Callback txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", data.UID)) + return stderr.ErrDbQueryError + } + } + return nil + }); err != nil { + if e, ok := err.(stderr.Code); ok { + return e + } + return stderr.ErrDbQueryError + } + return nil + default: + } + return nil +} diff --git a/web/service/ai_undress_service/ai_undress.go b/web/service/ai_undress_service/ai_undress.go new file mode 100644 index 0000000..11372e9 --- /dev/null +++ b/web/service/ai_undress_service/ai_undress.go @@ -0,0 +1,362 @@ +package ai_undress_service + +import ( + "91porn-server/common" + "91porn-server/models/commod" + "91porn-server/models/v/aiplazamod" + "91porn-server/models/v/walletmod" + "fmt" + "strconv" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + + "91porn-server/common/db" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/aiUnDressmod" + "91porn-server/models/v/txnmod" + "91porn-server/web/webg" +) + +func List(req *aiUnDressmod.WebListRequest) (interface{}, stderr.Code) { + var data = map[string]interface{}{ + "list": []interface{}{}, + "count": 0, + } + count, err := aiUnDressmod.CountDocument(req.Filter()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + if count == 0 { + return data, stderr.Success + } + list, err := aiUnDressmod.QueryAllDocument(req.Filter(), req.Options()) + if err != nil { + return data, stderr.ErrDbQueryError + } + data["count"] = count + data["list"] = list + return data, stderr.Success +} + +func Update(act string, req *aiUnDressmod.EditCond) stderr.Code { + // 获取Ai记录 + data, err := aiUnDressmod.FindDataByID(req.ID) + if err != nil { + return stderr.ErrDbQueryError + } + + if (*req.Status == aiUnDressmod.FAILURE || *req.Status == aiUnDressmod.REFUND) && + (data.Status == aiUnDressmod.FAILURE || data.Status == aiUnDressmod.REFUND) { + return stderr.Success + } + + // 根据状态判断 + switch *req.Status { + case aiUnDressmod.Processing: + if data.Status != aiUnDressmod.SUCCESS && data.Status != aiUnDressmod.SubmitOrder { + return stderr.AiUnDressStatusIsErr + } + err = aiUnDressmod.Edit(nil, req.Filter(), req.Update(act)) + if err != nil { + return stderr.ErrDbUpdateError + } + case aiUnDressmod.SUCCESS: + err = aiUnDressmod.Edit(nil, req.Filter(), req.Update(act)) + if err != nil { + return stderr.ErrDbUpdateError + } + case aiUnDressmod.FAILURE, aiUnDressmod.REFUND: + // 失败处理 退金币 + deAmt := -data.DebitAmountCoin + deIncome := -data.DebitIncomeCoin + uid := data.UID + oldVersion := false + err = webg.VideoDB.Trans(func(tool *db.MongoTool) error { + updateSet, _ := req.Update(act)["$set"].(bson.M) + transitioned, transitionErr := aiUnDressmod.TransitionStatus(tool, data.ID, data.Status, *req.Status, updateSet) + if transitionErr != nil { + return transitionErr + } + if !transitioned { + return nil + } + var tl []txnmod.TransactionLog + + // 根据是否是免费次数退回 + if data.Count > 0 || data.IsFreeTimes { + deCount := data.Count + if data.Count == 0 { + deCount = 1 + } + // 加钱包余额 + wallet, wErr := walletmod.DebitAiFreeTimes(tool, -deCount, uid) + if wErr != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Edit walletmod.DebitAiFreeTimes error:%+v:", wErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: deCount, + ActualAmount: float64(deCount), + TranType: txnmod.AiUndressDebitFreeTimesReturn.Key(), + TranTypeInt: int64(txnmod.AiUndressDebitFreeTimesReturn), + Desc: fmt.Sprintf("AI脱衣免费次数退返[%d次]", deCount), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + + if !data.IsFreeTimes && data.DebitAmountCoin == 0 && data.DebitIncomeCoin == 0 { + deAmt = -data.Coin + oldVersion = true + } + if data.DebitAmountCoin > 0 || data.DebitIncomeCoin > 0 || oldVersion { + // 加钱包余额 + wallet, wErr := walletmod.ReturnAmountAndIncome(tool, deAmt, deIncome, uid) + if wErr != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Edit walletmod.DebitAmount error:%+v:", wErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + if data.DebitIncomeCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -deIncome, + ActualAmount: float64(-deIncome), + TranType: txnmod.AiUndressDebitIncomeGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiUndressDebitIncomeGoldReturn), + Desc: fmt.Sprintf("AI脱衣收益金币退返-%d", -deIncome), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if data.DebitAmountCoin > 0 || oldVersion { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -deAmt, + ActualAmount: float64(-deAmt), + TranType: txnmod.AiUndressDebitGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiUndressDebitGoldReturn), + Desc: fmt.Sprintf("AI脱衣金币退返-%d", -deAmt), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + } + + txnErr := txnmod.InsertManyTransactionLog(tool, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Edit txnmod.InsertManyTransactionLog error:%+v:", txnErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + + return nil + }) + if err != nil { + log.Error(fmt.Sprintf("Handle AiUnDress Edit Trans error:%+v;uid:%v;", err, uid)) + return stderr.ErrDbUpdateError + } + } + return stderr.Success +} + +// Auto 提交AI自动化服务 +func Auto(act string, req *aiUnDressmod.AutoCond) stderr.Code { + // 获取Ai记录 + data, err := aiUnDressmod.FindDataByID(req.ID) + if err != nil { + return stderr.ErrDbQueryError + } + if data.Status == aiUnDressmod.SubmitOrder { + return stderr.Success + } + //提交订单到第三方 + notifyURL, notifyErr := webg.AICallbackURL("/api/web/admin/ai/undress/callback") + if notifyErr != nil { + log.Error("AI脱衣回调地址配置异常", log.E(notifyErr)) + return stderr.Failure + } + httpReq := aiUnDressmod.AiUndressOrderReq{ + AppId: int(commod.KFK_APPID), + FileUrl: data.OriginPics, + UserId: strconv.FormatUint(data.UID, 10), + AppOrderNum: data.ID.Hex(), + NotifyUrl: notifyURL, + } + var resp aiUnDressmod.AiUndressOrderResp + url := webg.Conf.URL.AiUndressServer + "/api/undress/prd/create_job" + codeJ, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, httpReq) + if err != nil { + log.Error("CreateUnderss httputil.DefaultClientPostWithResp err", log.E(err)) + return stderr.Failure + } + if codeJ != 200 { + log.Error("CreateUnderss req http code error", log.Any("code", codeJ)) + return stderr.Failure + } + //更新ai脱衣订单状态 + if err := aiUnDressmod.SubmitStatus(req.ID, aiUnDressmod.SubmitOrder, "已提交"); err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success +} + +// CallBackOrder AI脱衣回调 +func CallBackOrder(req *aiUnDressmod.CallBackReq) stderr.Code { + // 获取Ai记录 + data, err := aiUnDressmod.FindDataByID(req.AppOrderNum) + if err != nil { + return stderr.ErrDbQueryError + } + + if data.Status != aiUnDressmod.SubmitOrder { + log.Error("AI脱衣-重复回调或者状态不正确", log.Any("id", data.ID.Hex()), log.Any("status", data.Status)) + return stderr.Success + } + + var ( + set = bson.M{"remark": req.Msg} + // 返还次数、返还金币、返还收益金币 + refoundCount, deAmt, deIncome int64 + // 订单状态 + status int + oldVersion = false + successCount = len(req.ImgUrl) + uid = data.UID + _id = data.ID + isFreeTimes = false + ) + if req.ImgUrl != nil && len(req.ImgUrl) > 0 { + set["newPic"] = req.ImgUrl + // 同步内存中的 data.NewPic,否则下方 goroutine 读取 data.NewPic[0] 会因未更新而越界 panic + data.NewPic = req.ImgUrl + } + if successCount > 0 { // 成功 + status = aiUnDressmod.SUCCESS + refoundCount = 0 + deAmt = 0 + deIncome = 0 + } else { // 失败 + status = aiUnDressmod.REFUND + refoundCount = data.Count + // 失败处理 退金币 + deAmt = -data.DebitAmountCoin + deIncome = -data.DebitIncomeCoin + isFreeTimes = data.IsFreeTimes + } + set["status"] = status + handled := false + if err := webg.VideoDB.Trans(func(tool *db.MongoTool) error { + transitioned, transitionErr := aiUnDressmod.TransitionStatus(tool, _id, aiUnDressmod.SubmitOrder, status, set) + if transitionErr != nil { + return transitionErr + } + if !transitioned { + return nil + } + handled = true + var tl []txnmod.TransactionLog + if refoundCount > 0 || isFreeTimes { + if refoundCount == 0 { + refoundCount = 1 + } + // 加免费次数,负负为正 + wallet, wErr := walletmod.DebitAiFreeTimes(tool, -refoundCount, uid) + if wErr != nil { + log.Error(fmt.Sprintf("Handle AiUnDress CallBackOrder walletmod.DebitAiFreeTimes error:%+v:", wErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: refoundCount, + ActualAmount: float64(refoundCount), + TranType: txnmod.AiUndressDebitFreeTimesReturn.Key(), + TranTypeInt: int64(txnmod.AiUndressDebitFreeTimesReturn), + Desc: fmt.Sprintf("AI脱衣免费次数退返[%d次]", refoundCount), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if !data.IsFreeTimes && data.DebitAmountCoin == 0 && data.DebitIncomeCoin == 0 { + deAmt = -data.Coin + oldVersion = true + } + if deAmt < 0 || deIncome < 0 || oldVersion { + // 加钱包余额,负负为正 + wallet, wErr := walletmod.ReturnAmountAndIncome(tool, deAmt, deIncome, uid) + if wErr != nil { + log.Error(fmt.Sprintf("Handle AiUnDress CallBackOrder walletmod.DebitAmount error:%+v:", wErr), log.Any("uid", uid)) + return stderr.ErrDbUpdateError + } + if data.DebitIncomeCoin > 0 { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -deIncome, + ActualAmount: float64(-deIncome), + TranType: txnmod.AiUndressDebitIncomeGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiUndressDebitIncomeGoldReturn), + Desc: fmt.Sprintf("AI脱衣收益金币退返-%d", -deIncome), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + if data.DebitAmountCoin > 0 || oldVersion { + tl = append(tl, txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -deAmt, + ActualAmount: float64(-deAmt), + TranType: txnmod.AiUndressDebitGoldReturn.Key(), + TranTypeInt: int64(txnmod.AiUndressDebitGoldReturn), + Desc: fmt.Sprintf("AI脱衣金币退返-%d", -deAmt), + RealAmount: walletmod.GetRealAmount(wallet), + }) + } + } + if len(tl) > 0 { + txnErr := txnmod.InsertManyTransactionLog(tool, tl) + if txnErr != nil { + log.Error(fmt.Sprintf("Handle AiUnDress CallBackOrder txnmod.InsertManyTransactionLog error:%+v:", txnErr), log.Any("uid", uid)) + return stderr.ErrDbQueryError + } + } + return nil + }); err != nil { + log.Error(fmt.Sprintf("Handle AiUnDress CallBackOrder Trans error:%+v;uid:%v;", err, uid)) + return stderr.ErrDbUpdateError + } + if !handled { + return stderr.Success + } + if successCount > 0 { + common.Go(func() { + if data.ShareStatus != 1 { + return + } + // 新增一个分享 + alPlaza := aiplazamod.AiPlaza{ + Type: 3, + OrderId: data.ID, + Template: "", + Uid: data.UID, + Title: data.ShareTitle, + OriginalImage: data.OriginPic, + OriginalVideo: "", + OriginContent: "", + GenerateImage: data.NewPic[0], + GenerateVideo: "", + Status: aiplazamod.DefaultStatus, + } + _, err = aiplazamod.Insert(nil, alPlaza) + if err != nil { + log.Error("新增ai广场帖子失败", log.Any("alPlaza", alPlaza), log.E(err)) + return + } + }) + } + + return stderr.Success +} diff --git a/web/service/aiplazaser/aiplaza.go b/web/service/aiplazaser/aiplaza.go new file mode 100755 index 0000000..b68cebd --- /dev/null +++ b/web/service/aiplazaser/aiplaza.go @@ -0,0 +1,223 @@ +package aiplazaser + +import ( + "strings" + "time" + + "91porn-server/models/cache/aiplazadata" + "91porn-server/models/commod" + "91porn-server/models/v/aiplazamod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + OrderId *string `form:"orderId"` // 关联AI订单id + Type *int `form:"type"` // 1-ai图片换脸 2-ai视频换脸 3-ai脱衣 4-ai图生视频 5-ai绘画 6-ai小说 + Status *int `form:"status"` // 0-待审核 1-审核通过 2-拒绝 3-下架 + Uid *int64 `form:"uid"` // uid + Title *string `form:"title"` // 标题 + commod.Page +} +type WebListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []aiplazamod.AiPlaza `json:"list"` +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res WebListRes, err error) { + filter := bson.M{} + if q.OrderId != nil { + oid, _ := primitive.ObjectIDFromHex(strings.TrimSpace(*q.OrderId)) + filter["orderId"] = oid + } + if q.Status != nil { + //0-待审核 1-审核通过 2-拒绝 3-下架 + filter["status"] = *q.Status + } + if q.Title != nil && len(*q.Title) > 0 { + filter["title"] = bson.M{"$regex": *q.Title, "$options": "i"} + } + if q.Type != nil { + // 1-ai图片换脸 2-ai视频换脸 3-ai脱衣 4-ai图生视频 5-ai绘画 + filter["type"] = *q.Type + } + if q.Uid != nil { + filter["uid"] = *q.Uid + } + sort := bson.D{{"reviewAt", -1}} + res.List, res.Total, res.HasNext, err = aiplazamod.GetList(filter, int64(q.Skip()), int64(q.Limit()), sort) + + return +} + +type WebCreateReq struct { + Type int `json:"type" form:"type" binding:"required"` // 1-ai图片换脸 2-ai视频换脸 3-ai脱衣 4-ai图生视频 5-ai绘画 + Template string `json:"template" form:"template"` // 关联模版id + Uid uint64 `json:"uid" form:"uid" binding:"required"` // + Title string `json:"title" form:"title"` // 标题 + OriginalImage string `json:"originalImage" form:"originalImage"` // 原图 + OriginalVideo string `json:"originalVideo" form:"originalVideo"` // 原视频 + OriginalVideoCover string `json:"originalVideoCover" form:"originalVideoCover"` // 原视频封面 + OriginContent string `json:"originContent" form:"originContent"` // 输入的原文 + GenerateImage string `json:"generateImage" form:"generateImage"` // 生成的图 + GenerateVideo string `json:"generateVideo" form:"generateVideo"` // 生成的视频 + GenerateVideoCover string `json:"generateVideoCover" form:"generateVideoCover"` // 生成的视频封面 + Status int `json:"status" form:"status"` // 0-待审核 1-审核通过 2-拒绝 3-下架 + Reason string `json:"reason" form:"reason"` // 原因 + SortCode int `json:"sortCode" form:"sortCode"` // 置顶排序号,大于0就是置顶 + ReviewAt time.Time `json:"reviewAt" form:"reviewAt"` // 审核时间 +} + +// Create 发布数据 +func (p *WebCreateReq) Create() error { + data := aiplazamod.AiPlaza{ + Type: p.Type, + Template: p.Template, + Uid: p.Uid, + Title: p.Title, + OriginalImage: p.OriginalImage, + OriginalVideo: p.OriginalVideo, + OriginContent: p.OriginContent, + GenerateImage: p.GenerateImage, + GenerateVideo: p.GenerateVideo, + GenerateVideoCover: p.GenerateVideoCover, + Status: p.Status, + Reason: p.Reason, + SortCode: p.SortCode, + ReviewAt: p.ReviewAt, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // 创建数据 + if _, err := aiplazadata.InsertData(nil, data); err != nil { + return err + } + + return nil +} + +type WebUpdateReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` + Type *int `json:"type"` // + Template *string `json:"template" form:"template"` // 关联模版id + Uid *uint64 `json:"uid"` // + Title *string `json:"title"` // 标题 + OriginalImage *string `json:"originalImage"` // 原图 + OriginalVideo *string `json:"originalVideo"` // 原视频 + OriginalVideoCover *string `json:"originalVideoCover" form:"originalVideoCover"` // 原视频封面 + OriginContent *string `json:"originContent"` // 输入的原文 + GenerateImage *string `json:"generateImage"` // 生成的图 + GenerateVideo *string `json:"generateVideo"` // 生成的视频 + GenerateVideoCover *string `json:"generateVideoCover" form:"generateVideoCover"` // 生成的视频封面 + Status *int `json:"status"` // 0-待审核 1-审核通过 2-拒绝 3-下架 + Reason *string `json:"reason"` // 原因 + SortCode *int `json:"sortCode"` // 置顶排序号,大于0就是置顶 + ReviewAt *time.Time `json:"reviewAt"` // 审核时间 +} + +// Update 更新数据 +func (p *WebUpdateReq) Update() error { + post, err := aiplazamod.GetInfo(p.ID) + if err != nil { + return err + } + + data := make(map[string]interface{}) + if p.Type != nil { + data["type"] = *p.Type + } + if p.Template != nil { + data["template"] = *p.Template + } + if p.Uid != nil { + data["uid"] = *p.Uid + } + if p.Title != nil { + data["title"] = *p.Title + } + if p.OriginalImage != nil { + data["originalImage"] = *p.OriginalImage + } + if p.OriginalVideo != nil { + data["originalVideo"] = *p.OriginalVideo + } + if p.OriginalVideoCover != nil { + data["originalVideoCover"] = *p.OriginalVideoCover + } + if p.OriginContent != nil { + data["originContent"] = *p.OriginContent + } + if p.GenerateImage != nil { + data["generateImage"] = *p.GenerateImage + } + if p.GenerateVideo != nil { + data["generateVideo"] = *p.GenerateVideo + } + if p.GenerateVideoCover != nil { + data["generateVideoCover"] = *p.GenerateVideoCover + } + if p.Status != nil { + data["status"] = *p.Status + // 审核后上线时间为当前时间 + if post.Status != *p.Status && *p.Status == 1 { + data["reviewAt"] = time.Now() + } + } + if p.Reason != nil { + data["reason"] = *p.Reason + } + if p.SortCode != nil { + data["sortCode"] = *p.SortCode + } + data["updatedAt"] = time.Now() + + if _, err = aiplazadata.UpdateData(nil, p.ID.Hex(), data); err != nil { + return err + } + + return nil +} + +type WebDeleteReq struct { + ID []primitive.ObjectID `json:"ids" binding:"required"` +} + +// Delete 删除数据 +func (p *WebDeleteReq) Delete() error { + if err := aiplazadata.DeleteData(nil, p.ID...); err != nil { + return err + } + return nil +} + +type WebBatchUpdateReq struct { + Ids []primitive.ObjectID `json:"ids" binding:"required"` + Status *int `json:"status"` // 0-待审核 1-审核通过 2-拒绝 3-下架 + Reason *string `json:"reason"` // 原因 +} + +// Update 更新数据 +func (p *WebBatchUpdateReq) Update() error { + data := make(map[string]interface{}) + + if p.Status != nil { + data["status"] = *p.Status + if *p.Status == 1 { + data["reviewAt"] = time.Now() + } + } + if p.Reason != nil { + data["reason"] = *p.Reason + } + data["updatedAt"] = time.Now() + + if _, err := aiplazamod.UpdateByIds(nil, p.Ids, data); err != nil { + return err + } + + return nil +} diff --git a/web/service/annouser/annou.go b/web/service/annouser/annou.go new file mode 100644 index 0000000..901fc84 --- /dev/null +++ b/web/service/annouser/annou.go @@ -0,0 +1,80 @@ +package annouser + +import ( + "fmt" + "time" + + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/annoumod" + "91porn-server/web/vidhelp" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetAnnouList 获取公告列表 +func GetAnnouList(req annoumod.ListReq) (code stderr.Code, data interface{}) { + if len(req.Sort) == 0 { + req.Sort = "createdAt" + } + desc := -1 + if req.Desc == 1 { + desc = req.Desc + } + aInfos, _, total, err := annoumod.GetAnnou(req.PageNumber, req.PageSize, req.Sort, desc) + if err != nil { + return stderr.ErrDbQueryError, nil + } + return stderr.Success, annoumod.ListResp{AInfos: aInfos, Total: total} +} + +// UpdateAnnou 更新公告信息 +func UpdateAnnou(req annoumod.EditReq) (code stderr.Code, data interface{}) { + oid, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + log.Error(fmt.Sprintf("exchange string(%s) to objectID error.", req.ID)) + return stderr.ErrParamError, nil + } + m := vidhelp.Struct2Map(req.EditInfo) + cnt, err := annoumod.UpdateAnnou(oid, m) + if err != nil { + return stderr.ErrDbUpdateError, nil + } + return stderr.Success, annoumod.OpeResp{Count: cnt} +} + +// DeleteAnnou 删除公告 +func DeleteAnnou(ids []string) (code stderr.Code, data interface{}) { + oids := make([]primitive.ObjectID, 0, len(ids)) + for _, id := range ids { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Error(fmt.Sprintf("exchange string(%s) to objectID error.", id)) + continue + } + oids = append(oids, oid) + } + cnt, err := annoumod.DeleteAnnou(oids) + if err != nil { + return stderr.ErrDbUpdateError, nil + } + return stderr.Success, annoumod.OpeResp{Count: cnt} +} + +// AddAnnou 添加公告 +func AddAnnou(req annoumod.AddReq) (code stderr.Code, data interface{}) { + infos := []annoumod.Annou{annoumod.Annou{ + Title: req.Title, + Content: req.Content, + Cover: req.Cover, + Type: req.Type, + Href: req.Href, + Active: req.Active, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }} + if err := annoumod.AddAnnou(infos); err != nil { + return stderr.ErrDbInsertError, "" + } + return stderr.Success, nil +} diff --git a/web/service/annouser/announce.go b/web/service/annouser/announce.go new file mode 100644 index 0000000..27f055e --- /dev/null +++ b/web/service/annouser/announce.go @@ -0,0 +1,88 @@ +package annouser + +import ( + "fmt" + "time" + + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/annoumod" + "91porn-server/web/vidhelp" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +// GetAnnounceList 获取会员中心跑马灯列表 +func GetAnnounceListApi() (code stderr.Code, announce string) { + aInfos, err := annoumod.GetAnnounceApi("createdAt", -1) + if err != nil && err != mongo.ErrNoDocuments { + return stderr.ErrDbQueryError, "" + } + if err == mongo.ErrNoDocuments || len(aInfos) <= 0 { + return stderr.Success, "" + } + return stderr.Success, aInfos[0].Content +} + +// GetAnnounceList 获取会员中心跑马灯列表 +func GetAnnounceList() (code stderr.Code, data []annoumod.Announce) { + aInfos, err := annoumod.GetAnnounce("createdAt", -1) + if err != nil && err != mongo.ErrNoDocuments { + return stderr.ErrDbQueryError, nil + } + if err == mongo.ErrNoDocuments || len(aInfos) <= 0 { + return stderr.Success, nil + } + return stderr.Success, aInfos +} + +// UpdateAnnounce 更新会员中心跑马灯信息 +func UpdateAnnounce(req annoumod.EditAnnounceReq) (code stderr.Code, data interface{}) { + oid, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + log.Error(fmt.Sprintf("exchange string(%s) to objectID error.", req.ID)) + return stderr.ErrParamError, nil + } + m := vidhelp.Struct2Map(req.EditAnnounceInfo) + cnt, err := annoumod.UpdateAnnounce(oid, m) + if err != nil { + return stderr.ErrDbUpdateError, nil + } + return stderr.Success, annoumod.OpeResp{Count: cnt} +} + +// DeleteAnnounce 删除会员中心跑马灯 +func DeleteAnnounce(ids []string) (code stderr.Code, data interface{}) { + oids := make([]primitive.ObjectID, 0, len(ids)) + for _, id := range ids { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Error(fmt.Sprintf("exchange string(%s) to objectID error.", id)) + continue + } + oids = append(oids, oid) + } + cnt, err := annoumod.DeleteAnnounce(oids) + if err != nil { + return stderr.ErrDbUpdateError, nil + } + return stderr.Success, annoumod.OpeResp{Count: cnt} +} + +// AddAnnounce 添加会员中心跑马灯 +func AddAnnounce(req annoumod.AddAnnounceReq) (code stderr.Code) { + infos := []annoumod.Announce{annoumod.Announce{ + ID: primitive.NewObjectID(), + Content: req.Content, + Url: req.Url, + Type: annoumod.UserCenter, + Active: req.Active, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }} + if err := annoumod.AddAnnounce(infos); err != nil { + return stderr.ErrDbInsertError + } + return stderr.Success +} diff --git a/web/service/authorser/author.go b/web/service/authorser/author.go new file mode 100644 index 0000000..b07f5ca --- /dev/null +++ b/web/service/authorser/author.go @@ -0,0 +1,57 @@ +package authorser + +import ( + "fmt" + + "91porn-server/models/v/adminmod" + "91porn-server/models/v/authoritymod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ObjectID ObjectID +type ObjectID = primitive.ObjectID + +// SetAuthority SetAuthority +func SetAuthority(adminID ObjectID, authID ObjectID) error { + existed, err := adminmod.ExistsByID(adminID) + if err != nil { + return fmt.Errorf("service SetAuthority adminID Exists error: " + err.Error()) + } + if !existed { + return fmt.Errorf("service SetAuthority adminID is not exist") + } + existed, err = authoritymod.ExistsByID(authID) + if err != nil { + return fmt.Errorf("service SetAuthority authID Exists error: " + err.Error()) + } + if !existed { + return fmt.Errorf("service SetAuthority authID is not exist") + } + authority, err := authoritymod.FindOne(authoritymod.AuthorityDoc{ + ID: authID, + }) + if err != nil { + return fmt.Errorf("service SetAuthority authID is not existed" + err.Error()) + } + if err = adminmod.UpdateByID(adminID, adminmod.AdminDoc{ + Role: &authority.Role, + }); err != nil { + return fmt.Errorf("service SetAuthority faild " + err.Error()) + } + return nil +} + +func UnsetAuthority(adminID ObjectID) error { + existed, err := adminmod.ExistsByID(adminID) + if err != nil { + return fmt.Errorf("author UnsetAuthority adminID Exists error: " + err.Error()) + } + if !existed { + return fmt.Errorf("service UnsetAuthority adminID is not exist") + } + if err = adminmod.UnsetPrivilege(adminID); err != nil { + return fmt.Errorf("service UnsetAuthority faild " + err.Error()) + } + return nil +} diff --git a/web/service/backpack_service/backpack.go b/web/service/backpack_service/backpack.go new file mode 100644 index 0000000..4a9bfbc --- /dev/null +++ b/web/service/backpack_service/backpack.go @@ -0,0 +1,22 @@ +package backpack_service + +import "91porn-server/models/v/backpackmod" + +func QueryAll(in *backpackmod.QueryAllWebCond) (*backpackmod.QueryAllRes, error) { + var out = &backpackmod.QueryAllRes{} + count, err := backpackmod.QueryAllCount(in.Filter()) + if err != nil { + return nil, err + } + if count == 0 { + out.List = []*backpackmod.Backpack{} + out.Total = 0 + return out, nil + } + out.Total = count + out.List, err = backpackmod.QueryAllList(in.Filter(), in.Options()) + if err != nil { + return nil, err + } + return out, nil +} diff --git a/web/service/bannerjumpser/bannerjump.go b/web/service/bannerjumpser/bannerjump.go new file mode 100755 index 0000000..779dad7 --- /dev/null +++ b/web/service/bannerjumpser/bannerjump.go @@ -0,0 +1,129 @@ +package bannerjumpser + +import ( + "time" + + "91porn-server/models/cache/bannerjumpdata" + "91porn-server/models/commod" + "91porn-server/models/v/bannerjumpmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + commod.Page +} +type WebListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []bannerjumpmod.BannerJump `json:"list"` +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res WebListRes, err error) { + filter := bson.M{} + res.List, res.Total, res.HasNext, err = bannerjumpmod.GetList(filter, int64(q.Skip()), int64(q.Limit()), q.GetSort()) + + return +} + +type WebCreateReq struct { + Position int `json:"position" form:"position" binding:"required"` // 位置 1-视频播放器下方 2-收益页 3-首页浮窗 + Banner string `json:"banner" form:"banner" binding:"required"` // BANNER图片 + Title string `json:"title" form:"title" binding:"required"` // 标题 + Url string `json:"url" form:"url" binding:"required"` // 跳转地址 + StartAt time.Time `json:"startAt" form:"startAt" binding:"required"` // 开始时间 + EndAt time.Time `json:"endAt" form:"endAt" binding:"required"` // 结束时间 + Weight int `json:"weight" form:"weight" binding:"required"` // 权重 + Enable bool `json:"enable" form:"enable" binding:"required"` // 是否启用 +} + +// Create 发布数据 +func (p *WebCreateReq) Create() error { + data := bannerjumpmod.BannerJump{ + Position: p.Position, + Banner: p.Banner, + Title: p.Title, + Url: p.Url, + StartAt: p.StartAt, + EndAt: p.EndAt, + Weight: p.Weight, + Enable: p.Enable, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // 创建数据 + if _, err := bannerjumpdata.InsertData(nil, data); err != nil { + return err + } + + return nil +} + +type WebUpdateReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` + Position *int `json:"position"` // 位置 1-视频播放器下方 2-收益页 3-首页浮窗 + Banner *string `json:"banner"` // BANNER图片 + Title *string `json:"title"` // 标题 + Url *string `json:"url"` // 跳转地址 + StartAt *time.Time `json:"startAt"` // 开始时间 + EndAt *time.Time `json:"endAt"` // 结束时间 + Weight *int `json:"weight"` // 权重 + Enable *bool `json:"enable"` // 是否启用 +} + +// Update 更新数据 +func (p *WebUpdateReq) Update() error { + _, err := bannerjumpmod.GetInfo(p.ID) + if err != nil { + return err + } + + data := make(map[string]interface{}) + if p.Position != nil { + data["position"] = *p.Position + } + if p.Banner != nil { + data["banner"] = *p.Banner + } + if p.Title != nil { + data["title"] = *p.Title + } + if p.Url != nil { + data["url"] = *p.Url + } + if p.StartAt != nil { + data["startAt"] = *p.StartAt + } + if p.EndAt != nil { + data["endAt"] = *p.EndAt + } + if p.Weight != nil { + data["weight"] = *p.Weight + } + if p.Enable != nil { + data["enable"] = *p.Enable + } + data["updatedAt"] = time.Now() + + if _, err = bannerjumpdata.UpdateData(nil, p.ID.Hex(), data); err != nil { + return err + } + + return nil +} + +type WebDeleteReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` +} + +// Delete 删除数据 +func (p *WebDeleteReq) Delete() error { + if err := bannerjumpdata.DeleteData(nil, p.ID); err != nil { + return err + } + + return nil +} diff --git a/web/service/certificateser/certificate.go b/web/service/certificateser/certificate.go new file mode 100644 index 0000000..eef98bf --- /dev/null +++ b/web/service/certificateser/certificate.go @@ -0,0 +1,59 @@ +package certificateser + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/ysqr" + "91porn-server/models/v/usermod" + "91porn-server/web/middleware/authweb" + + "github.com/gin-gonic/gin" +) + +// QrCode doc +// @Summary 二维码 +// @Description 二维码 +// @Tags 凭证 +// @Accept mpfd,json +// @Produce json,html +// @Param token formData string true "令牌" +// @Success 200 {string} json "{"msg": "操作成功", "content": "", user:{}}" +// @Failure 400 {string} json "{"msg": "操作失败"}" +// @Router /ext/certificate/qr [post] +func QrCode(c *gin.Context) { + var args struct { + Token string `form:"token" json:"token" binding:"required"` //token + } + if err := c.ShouldBind(&args); err != nil { + common.ServeJSON(c, stderr.ErrParamError, "certificateser QrCode arg error "+err.Error()) + return + } + claims, err := authweb.ParseWebClaims(args.Token) + if err != nil { + log.Error("activity InvitInfo ParseWebClaims error", log.Any("token", args.Token), log.E(err)) + common.ServeJSON(c, stderr.InvalidToken, "") + return + } + user, _ := usermod.FindUserByUID(claims.UID) + if user == nil { + common.ServeJSON(c, stderr.AdminIsNotExist, "") + return + } + content := ysqr.Content{ + UID: user.UID, + T: ysqr.Login, + LoginClaims: ysqr.LoginClaims{ + DevID: user.DevID, + }, + } + u := gin.H{ + "uid": user.UID, + "name": user.Name, + "portrait": user.Portrait, + } + common.ServeJSON(c, stderr.Success, gin.H{ + "content": content.String(authweb.GetTokenSecret()), + "user": u, + }) +} diff --git a/web/service/chat_message_ser/chat_message.go b/web/service/chat_message_ser/chat_message.go new file mode 100644 index 0000000..739a770 --- /dev/null +++ b/web/service/chat_message_ser/chat_message.go @@ -0,0 +1,219 @@ +package chat_message_ser + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/messagemod" + "91porn-server/models/v/sessionmod" + "91porn-server/models/v/usermod" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "time" +) + +func List(req *messagemod.WebListRequest) (interface{}, stderr.Code) { + var data = map[string]interface{}{ + "list": []interface{}{}, + "count": 0, + } + count, err := messagemod.CountDocument(req.Filter()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + if count == 0 { + return data, stderr.Success + } + list, err := messagemod.QueryAllDocument(req.Filter(), req.Options()) + if err != nil { + return data, stderr.ErrDbQueryError + } + data["count"] = count + data["list"] = list + return data, stderr.Success +} + +func Del(req *messagemod.WebDelReq) stderr.Code { + data, err := messagemod.FindOneByFilter(req.Filter()) + if err != nil { + return stderr.ErrDbQueryError + } + + if data.ID.IsZero() { + return stderr.CodeEmptyData + } + + err = messagemod.RemoveDocument(req.Filter()) + if err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success +} + +// AddPrivateLetter 新增私信消息 +func AddPrivateLetter(req *messagemod.SendMsg) stderr.Code { + if req.Sender == 0 || req.Receiver == 0 { + return stderr.ErrParamError + } + data, err := usermod.FindUserByUID(req.Sender) + if err != nil || data == nil { + log.Warn(fmt.Sprintf("messageser Sender is null"), log.Any("Sender", req.Sender)) + return stderr.CommentUserInfoErr + } + + if req.Sender == req.Receiver { + log.Warn(fmt.Sprintf("messageser req.Sender == req.Receiver"), log.Any("Sender", req.Sender), log.Any("Receiver", req.Receiver)) + return stderr.NoSelfAddMsg + } + + //查询被私信的用户是否存在 + takeUser, err := usermod.FindUserByUID(req.Receiver) + if err != nil { + return stderr.CommentUserInfoErr + } + if takeUser == nil { + log.Warn(fmt.Sprintf("messageser Receiver is null"), log.Any("Receiver", req.Receiver)) + return stderr.CommentUserInfoErr + } + //会话id + sessionId := "" + //新增私聊信息之前, 先查询是否有会话信息 + cond := bson.M{"$or": []bson.M{bson.M{"sendUid": req.Sender, "takeUid": req.Receiver}, bson.M{"sendUid": req.Receiver, "takeUid": req.Sender}}} + session, err := sessionmod.FindOneByFilter(cond) + if err != nil { + log.Warn(fmt.Sprintf("messageser Add sessionmod.FindOneByFilter error:%+v:", err), log.Any("Sender", req.Sender), log.Any("Receiver", req.Receiver)) + return stderr.ErrDbQueryError + } + t := time.Now() + if session.ID.IsZero() { + sessionId = common.UUID() + //新增session + s := &sessionmod.Session{ + SendUid: req.Sender, + TakeUid: req.Receiver, + SessionId: sessionId, + CreatedAt: t, + UpdatedAt: t, + } + err := sessionmod.InsertOne(nil, s) + if err != nil { + log.Warn(fmt.Sprintf("messageser Add sessionmod.InsertOne error:%+v:", err), log.Any("session", s), log.Any("Sender", req.Sender), log.Any("Receiver", req.Receiver)) + return stderr.ErrDbInsertError + } + } else { + sessionId = session.SessionId + } + + code := PublicAddMsg(messagemod.PublicAddMsg{ + SendUid: req.Sender, + TakeUid: req.Receiver, + SessionId: sessionId, + Content: req.Content, + //ImgUrl: req.ImgUrl,// 暂时不支持图片 + MsgType: req.MsgType, + ObjId: req.ObjId, + ObjName: req.ObjName, + }) + if code != stderr.Success { + return code + } + common.Go(func() { + //更新会话预览消息 + content := req.Content + // 暂时不支持图片 + //if len(req.ImgUrl) > 0 && content == "" { + // content = "发送了一张图片" + //} + err := sessionmod.UpdOneByFilter(nil, bson.M{"sessionId": sessionId}, bson.M{"$set": bson.M{"preContent": content, "updatedAt": t}}) + if err != nil { + log.Warn(fmt.Sprintf("messageser Add messagemod.UpdOneByFilter error:%+v:", err), log.Any("sessionId", sessionId), log.Any("preContent", req.Content), log.Any("Sender", req.Sender), log.Any("Receiver", req.Receiver)) + } + }) + return stderr.Success +} + +// PublicAddMsg 添加消息公共方法 +func PublicAddMsg(prams messagemod.PublicAddMsg) stderr.Code { + //查询用户信息 + user, err := usermod.FindUserByUID(prams.SendUid) + if err != nil { + return stderr.ErrDbQueryError + } + if user == nil || user.HasLocked { + return stderr.UserIsNotExists + } + now := time.Now() + //新增消息 + msg := &messagemod.Message{ + SendUid: prams.SendUid, + TakeUid: prams.TakeUid, + MsgType: prams.MsgType, + Content: prams.Content, + ImgUrl: prams.ImgUrl, + IsRead: false, + CreatedAt: now, + UpdatedAt: now, + ObjId: prams.ObjId, + ObjName: prams.ObjName, + } + // 是否加入未读提醒 + needTip := false + switch prams.MsgType { + case messagemod.PrivateLetterMsg: + //私信 + msg.SessionId = prams.SessionId + needTip = true + case messagemod.OfficialPrivateLetterMsg: + //官方私信 + msg.SessionId = prams.SessionId + msg.ContentType = prams.ContentType + needTip = true + case messagemod.FollowMsg: + //关注 + msg.Content = "关注了你" + needTip = true + case messagemod.LikeMsg: + //点赞 + msg.Content = "点赞了你的帖子" + msg.ObjId = prams.ObjId + msg.ObjName = prams.ObjName + needTip = true + case messagemod.ForwardMsg: //转发 + msg.Content = "转发了你的帖子" + msg.ObjId = prams.ObjId + msg.ObjName = prams.ObjName + case messagemod.CommentMsg: //评论 + msg.Content = "评论了你的帖子" + msg.ObjId = prams.ObjId + msg.ObjName = prams.ObjName + needTip = true + case messagemod.DesireMsg: + msg.Content = "回复了你的工单" + msg.ObjId = prams.ObjId + msg.ObjName = prams.ObjName + needTip = true + case messagemod.LikeCommentMsg: // 点赞评论 + msg.Content = "点赞了你的评论" + msg.ObjId = prams.ObjId + msg.ObjName = prams.ObjName + case messagemod.ReplyCommentMsg: // 回复评论 + msg.Content = "回复了你的评论" + msg.ObjId = prams.ObjId + msg.ObjName = prams.ObjName + needTip = true + } + _, err = messagemod.InsertOne(nil, msg) + if err != nil { + log.Warn(fmt.Sprintf("messageser Add messagemod.InsertOne error:%+v:", err), log.Any("message", msg)) + return stderr.ErrDbQueryError + } + go func() { + err = usermod.IncDynamic(prams.TakeUid) + }() + // 更新提示状态 + if needTip { + //_ = message.SetTip(prams.TakeUid) + } + + return stderr.Success +} diff --git a/web/service/chatrobotser/chatrobot.go b/web/service/chatrobotser/chatrobot.go new file mode 100644 index 0000000..b68686b --- /dev/null +++ b/web/service/chatrobotser/chatrobot.go @@ -0,0 +1,119 @@ +package chatrobotser + +import ( + "time" + + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/chatrobotmod" + + "github.com/jinzhu/copier" +) + +// 获取机器人配置列表 +func GetRobotConfList(param chatrobotmod.ReqConfList) (code stderr.Code, data interface{}, err error) { + var skip = int64((param.PageNumber - 1) * param.PageSize) + var limit = int64(param.PageSize) + total, cList, err := chatrobotmod.GetRobotConf(param.Type, skip, limit) + if err != nil { + code = stderr.Failure + return + } + if cList == nil { + data = commod.ListResp{List: []chatrobotmod.ChatRobot{}} + } else { + list := make([]chatrobotmod.RespList, len(cList)) + for i, v := range cList { + tmp := chatrobotmod.RespList{} + if err = copier.Copy(&tmp, &v); err != nil { + code = stderr.Failure + return + } + list[i] = tmp + } + data = commod.ListResp{List: list, Total: total} + } + code = stderr.Success + return +} + +// 添加配置 +func AddRobotConf(param chatrobotmod.ReqAdd) (code stderr.Code, data interface{}, err error) { + if code, err = addCheck(param.Type); err != nil || code != stderr.Success { + return + } + switch param.Type { + case chatrobotmod.Comment: // 评论机器人 + return addCmtRobotConf(param.Type, param.ReqAddCmt) + default: + code = stderr.RobotTypeInvalid + return + } +} + +func addCheck(rType chatrobotmod.EnumRobotType) (stderr.Code, error) { + isExists, err := chatrobotmod.ConfExists(rType) + if err != nil { + return stderr.Failure, err + } + if isExists { + return stderr.RobotTypeExisted, nil + } + return stderr.Success, nil +} + +// 新增评论机器人配置 +func addCmtRobotConf(rType chatrobotmod.EnumRobotType, param chatrobotmod.ReqAddCmt) (code stderr.Code, data interface{}, err error) { + var now = time.Now() + var doc = chatrobotmod.ChatRobot{ + Type: rType, + IsActive: param.IsActive, + CreatedAt: now, + UpdatedAt: now, + CommentRobot: chatrobotmod.CommentRobot{ + From: param.From, + To: param.To, + TotalLimit: param.TotalLimit, + PureLimit: param.PureLimit, + Frequency: param.Frequency, + MaxCommentNum: param.MaxCommentNum, + MaxFakeLikes: param.MaxFakeLikes, + MinFakeLikes: param.MinFakeLikes, + MaxFakePlayCount: param.MaxFakePlayCount, + MinFakePlayCount: param.MinFakePlayCount, + }, + } + if err = chatrobotmod.Insert(doc); err != nil { + code = stderr.Failure + return + } + code = stderr.Success + return +} + +// 更新机器人配置 +func UpdateRobotConf(param chatrobotmod.ReqUpdate) (code stderr.Code, data interface{}, err error) { + switch param.Type { + case chatrobotmod.Comment: // 评论机器人 + return updateCmtRobotConf(param.Type, param.ReqUpdateCmt) + default: + code = stderr.RobotTypeInvalid + return + } +} + +// 更新评论机器人配置 +func updateCmtRobotConf(rType chatrobotmod.EnumRobotType, param chatrobotmod.ReqUpdateCmt) (code stderr.Code, data interface{}, err error) { + doc := chatrobotmod.CmtUpdateDoc{} + if err = copier.Copy(&doc, ¶m); err != nil { + code = stderr.Failure + return + } + doc.UpdatedAt = time.Now() + if err = chatrobotmod.UpdateCmtRobotConf(rType, doc); err != nil { + code = stderr.Failure + return + } + code = stderr.Success + return +} diff --git a/web/service/commentser/comment.go b/web/service/commentser/comment.go new file mode 100644 index 0000000..95f835e --- /dev/null +++ b/web/service/commentser/comment.go @@ -0,0 +1,424 @@ +package commentser + +import ( + "91porn-server/app/service/taskser" + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/dailytaskmod" + "91porn-server/models/v/locmod" + "91porn-server/models/v/messagemod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "91porn-server/models/v/walletmod" + "91porn-server/web/service/chat_message_ser" + "91porn-server/web/vidhelp" + "91porn-server/web/webg" + "errors" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetCommentList 后台获取评论列表 +func GetCommentList(vidID string, cUID uint64, content string, isRobot, isAdvertiser *bool, status *int, page commod.Page) (data interface{}, err error) { + cList, count, err := cmtmod.CmtFindCommentsAndCount(vidID, cUID, content, isRobot, isAdvertiser, status, page) + if err != nil { + log.Error("web service comment GetCommentList FindUserByUID error", log.E(err), log.Any("vidID", vidID), log.Any("cUID", cUID), log.Any("content", content)) + return + } + var cmtList []cmtmod.WebCmtRespList + var uids []uint64 + for _, v := range cList { + // 获取评论的用户是否已禁言 + usr, err := usermod.FindUserByUID(v.UserID) + if err != nil || usr == nil { + log.Error("web service comment GetCommentList FindUserByUID error", log.E(err), log.Any("uid", v.UserID)) + continue // 避免测试服脏数据造成无数据返回 + } + tmp := cmtmod.WebCmtRespList{ + ID: v.ID, + ObjID: v.ObjID, + UserID: v.UserID, + LikeCount: v.LikeCount, + Content: v.Content, + Level: v.Level, + Status: v.Status, + IsDelete: v.IsDelete, + LinkStr: v.LinkStr, + IsBaned: usr.HasBanned, + IsGodComment: v.IsGodComment, + Portrait: usr.Portrait, + Name: usr.Name, + CreatedAt: v.CreatedAt, + IsRobot: v.IsRobot, + HasLocked: usr.HasLocked, + IsAdvertiser: usr.IsAdvertiser, + Image: v.Image, + } + cmtList = append(cmtList, tmp) + uids = append(uids, v.UserID) + } + if cmtList == nil { + cmtList = []cmtmod.WebCmtRespList{} + } + + resp := make(map[string]interface{}) + resp["list"] = cmtList + resp["total"] = count + resp["uids"] = uids + data = resp + return +} + +// 后台评论管理屏蔽评论 +func DeleteCommentsByID(ids []primitive.ObjectID, uid uint64, content string) (err error) { + var cmtList []cmtmod.Comment + if uid != 0 { + // 获取评论 + if cmtList, err = cmtmod.GetCommentsByUID(uid); err != nil { + return + } + if err = cmtmod.DelCommentsByUID(uid); err != nil { + return + } + } else if content != "" { + if cmtList, err = cmtmod.GetCommentsByContent(content); err != nil { + return + } + if err = cmtmod.DelCommentsByContent(content); err != nil { + return + } + } else { + if ids == nil || len(ids) == 0 { + return + } + if cmtList, err = cmtmod.GetCommentsByIDS(ids); err != nil { + return + } + if err = cmtmod.DelCommentsByIDS(ids); err != nil { + return + } + } + // 评论数更新 + for _, v := range cmtList { + // 视频评论数-1 + _ = vidhelp.DecCommentCount(v.ObjID) + // 删除子评论 + if v.Level == 1 { + var childIDS []primitive.ObjectID + // 获取子评论 + childCmtList, _ := cmtmod.GetCommentsByCID(v.ObjID, v.ID) + for _, v1 := range childCmtList { + childIDS = append(childIDS, v1.ID) + // 视频评论数-1 + _ = vidhelp.DecCommentCount(v1.ObjID) + } + if childIDS == nil || len(childIDS) == 0 { + continue + } + // 删除评论 + _ = cmtmod.DelCommentsByIDS(childIDS) + } + // 评论数-1 + if v.Level == 2 && !v.CID.IsZero() { + _ = cmtmod.CmtDecreaseCommentCountByID(v.CID) + } + } + return +} + +// 后台获取评论详情 +func GetCommentInfo(id primitive.ObjectID) (data interface{}, err error) { + return cmtmod.CmtFindOneByID(id) +} + +// AccessComment 后台评论通过审核 +func AccessComment(id primitive.ObjectID) (err error) { + cmt, err := cmtmod.CmtFindOneByID(id) + if err != nil || cmt.ID.IsZero() { + log.Error("web service comment AccessComment CmtFindOneByID error", log.E(err), log.Any("id", id)) + return + } + err = cmtmod.CmtUpdateCommentStatusByID(id, 1) + if err != nil { + log.Error("web service comment GetCommentList CmtUpdateCommentStatusByID error", log.E(err), log.Any("uid", id)) + return + } + // 审核通过增加评论数 + err = vidmod.IncCommentCount(cmt.ObjID) + if err != nil { + log.Error("web service comment GetCommentList IncCommentCount error", log.E(err), log.Any("uid", id)) + return + } + return +} + +// BatchAccessComment 批量后台评论通过审核 +func BatchAccessComment(ids []primitive.ObjectID, status int) (err error) { + cmt, err := cmtmod.CmtFindOneByIds(ids) + if err != nil || len(cmt) <= 0 { + log.Error("web service comment AccessComment CmtFindOneByIds error", log.E(err), log.Any("ids", ids)) + return + } + err = cmtmod.PassCommentStatusByIds(ids, status) + if err != nil { + log.Error("web service BatchAccessComment PassCommentStatusByIds error", log.E(err)) + return + } + return +} + +// PublishComment 发表评论 统一以作者身份回复 +func PublishComment(param cmtmod.PublishReqInfo) (code stderr.Code, err error) { + videoInfo, err := vidmod.GetVideoInfo(param.ObjID.Hex()) + if err != nil || videoInfo.ID.IsZero() { + code = stderr.CommentErrSendFail + code.SetMsg("current video is not exist or find error") + return code, err + } + var cmtMod cmtmod.Comment + isAuthor := false + if videoInfo.PublisherID == param.ToUserID { + isAuthor = true + } + if code, cmtMod, err = getTrackInfo(videoInfo, "", isAuthor, param); code != stderr.Success || err != nil { + return code, err + } + // 保存到数据库 + id, err := cmtmod.CmtInsertOne(&cmtMod) + if err != nil { + code = stderr.CommentErrSendFail + return code, err + } + if err = setGid(param.CID, param.RID, id); err != nil { + code = stderr.CommentErrSendFail + return code, err + } + // 增加评论数 + incCommentCount(param.CID, param.ObjID) + // 获取该评论 + return +} + +// 获取相关信息 +func getTrackInfo(videoInfo vidmod.VideoModel, ip string, isAuthor bool, param cmtmod.PublishReqInfo) (code stderr.Code, data cmtmod.Comment, err error) { + var cid primitive.ObjectID + var publiserID = videoInfo.PublisherID + if param.CID != "" { + if cid, err = primitive.ObjectIDFromHex(param.CID); err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + } + + loc, _ := locmod.GetLocationInfoByID(videoInfo.Location) + now := time.Now() + data = cmtmod.Comment{ + ObjID: param.ObjID, + CID: cid, + PublisherID: publiserID, + SortCnt: 0, + ToUserID: param.ToUserID, + Content: param.Content, + OrgContent: param.Content, + UserID: param.ToUserID, + Status: 1, + Level: param.Level, + AuthorMark: isAuthor, + IsAuthor: isAuthor, + IsDelete: false, + CommentCount: 0, + LikeCount: 0, + IPAddr: ip, + City: loc.City, + IsRobot: true, + CreatedAt: now, + } + + if param.ToUserID != 0 { + if param.Level == 1 { + user, err := usermod.FindUserByUID(param.ToUserID) + if err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + data.UserID = user.UID + } else { + rCmt, err := cmtmod.CmtFindOneByID(cid) + if err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + user, err := usermod.FindUserByUID(rCmt.UserID) + if err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + data.UserID = param.ToUserID + data.ToUserID = rCmt.UserID + data.ToUserName = user.Name + } + } + if param.RID != "" { + rid, err := primitive.ObjectIDFromHex(param.RID) + if err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + rCmt, err := cmtmod.CmtFindOneByID(rid) + if err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + user, err := usermod.FindUserByUID(rCmt.UserID) + if err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + if rCmt.Level != 1 { + // 如果回复的不是1级,则需要写入rid + data.RID = rid + } + data.GID = rCmt.GID + data.AuthorMark = rCmt.AuthorMark + data.SortCnt = rCmt.SortCnt + data.Type = cmtmod.CmtTypeReply + data.PublisherID = rCmt.UserID + data.ToUserName = user.Name + data.ToUserID = rCmt.UserID + if !rCmt.CID.IsZero() { + data.CID = rCmt.CID + } + } else if param.RID == "" && !cid.IsZero() { + data.Type = cmtmod.CmtTypeComment + cCmt, err := cmtmod.CmtFindOneByID(cid) + if err != nil { + code = stderr.CommentErrSendFail + return code, data, err + } + data.PublisherID = cCmt.UserID + } else { + data.PublisherID = videoInfo.PublisherID + data.Type = cmtmod.CmtTypeVideo + } + code = stderr.Success + return +} + +// 设置分组id +func setGid(cid string, rid string, id primitive.ObjectID) (err error) { + if cid != "" && rid == "" { + return cmtmod.SetGid(id) + } + return +} + +// 增加评论数 +func incCommentCount(cid string, objID primitive.ObjectID) { + if cid != "" { + var objCid primitive.ObjectID + objCid, err := primitive.ObjectIDFromHex(cid) + if err != nil { + return + } + // 根据cid获取评论信息 + if toComment, err := cmtmod.CmtFindOneByID(objCid); err != nil || (toComment.Level != 1 && toComment.Level != 2) { + return + } + if err := cmtmod.CmtIncreaseCommentCountByID(objCid); err != nil { + return + } + } + // 增加视频评论数 + if err := vidhelp.IncCommentCount(objID); err != nil { + return + } +} + +// UpdateComment 后台评论设置 +func UpdateComment(in *cmtmod.EditCond) (err error) { + cmt, err := cmtmod.CmtFindOneByID(in.ID) + if err != nil || cmt.ID.IsZero() { + log.Error("web service comment AccessComment CmtFindOneByID error", log.E(err), log.Any("id", in.ID)) + return errors.New("comment is err or null") + } + if in.LinkStr == nil && cmt.Level != 1 && in.LikeCount == nil { + return errors.New("comment level is not 1") + } + + if in.LinkStr == nil && cmt.IsAuthor && in.LikeCount == nil { + return errors.New("author not allow godComment") + } + + if in.LinkStr == nil && in.IsGodComment == nil && in.LikeCount == nil { + return errors.New("update params is null") + } + // 如果设置为神评论 + // 判断是否已经发送过该评论的神评论奖励(避免修改为普通评论后又修改为神评论,奖励多次发放的情况) + if in.IsGodComment != nil && *in.IsGodComment == true && !cmt.GodCommentAward { + in.GodCommentAward = true + } + err = cmtmod.Update(in.Filter(), in.Update()) + if err != nil { + log.Error("web service comment GetCommentList CmtUpdateCommentStatusByID error", log.E(err), log.Any("id", in.ID)) + return + } + /********************* 以下是神评相关逻辑 **********************/ + // 完成任务,评论被设置为神评 + if in.IsGodComment != nil && *in.IsGodComment == true { + // 完成评论被设置为神评 + common.Go(func() { + err = taskser.CompleteDailyTask(nil, cmt.UserID, dailytaskmod.CommentSetGod) + if err != nil { + log.Error("web 设置神评,完成每日任务【评论被设置为神评】失败", log.E(err), log.Any("id", in.ID)) + return + } + }) + } + // 获取神评论奖励积分配置 + if webg.Conf.GodCommentAward <= 0 { + return stderr.Success + } + + // 查询出评论的帖子信息 + video, err := vidmod.GetVideoByID(cmt.ObjID) + if err != nil || video == nil { + return + } + objName := fmt.Sprintf("[%v]", video.Title) + content := fmt.Sprintf("恭喜您在%v中评论获得神评,获得%v积分", objName, webg.Conf.GodCommentAward) + // 发放积分 + walletInfo, err := walletmod.CreditIntegral(nil, webg.Conf.GodCommentAward, cmt.UserID) + if err != nil { + return err + } + + log := &txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: cmt.UserID, + Integral: webg.Conf.GodCommentAward, + ActualIntegral: float64(webg.Conf.GodCommentAward), + TranType: txnmod.GodCommentAward.Key(), + TranTypeInt: int64(txnmod.GodCommentAward), + Desc: fmt.Sprintf("%s-积分[%d个]", txnmod.GodCommentAward.Key(), webg.Conf.GodCommentAward), + RealIntegral: walletInfo.RealIntegral(), + } + txnmod.InsertTransactionLog(nil, log) + + // 发放私信告诉用户获得了积分 + code := chat_message_ser.AddPrivateLetter(&messagemod.SendMsg{ + Sender: 100001, + Receiver: cmt.UserID, + Content: content, + MsgType: messagemod.OfficialPrivateLetterMsg, + ObjId: video.ID, + ObjName: objName, + }) + + return code +} diff --git a/web/service/commentser/commentAgent.go b/web/service/commentser/commentAgent.go new file mode 100644 index 0000000..1a0ed6b --- /dev/null +++ b/web/service/commentser/commentAgent.go @@ -0,0 +1,417 @@ +package commentser + +import ( + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "fmt" + "strconv" + "strings" + "sync" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// 缓存随机用户列表,避免每次都查询数据库 +var ( + cachedUsers []*usermod.User + cachedUsersMu sync.RWMutex + cachedUsersTime time.Time + cachedUsersTTL = 10 * time.Minute +) + +func getReplyUsers() ([]*usermod.User, error) { + cachedUsersMu.RLock() + if len(cachedUsers) > 0 && time.Since(cachedUsersTime) < cachedUsersTTL { + users := cachedUsers + cachedUsersMu.RUnlock() + return users, nil + } + cachedUsersMu.RUnlock() + + cachedUsersMu.Lock() + defer cachedUsersMu.Unlock() + // double check + if len(cachedUsers) > 0 && time.Since(cachedUsersTime) < cachedUsersTTL { + return cachedUsers, nil + } + + users, _, err := usermod.FetchList(bson.M{"uid": bson.M{"$lte": 300000}}, options.Find().SetLimit(1000)) + if err != nil { + return nil, err + } + cachedUsers = users + cachedUsersTime = time.Now() + return users, nil +} + +func PullCommentList(status string, limit int) (cmtmod.PullResp, error) { + var resp cmtmod.PullResp + filter := bson.M{} + if limit <= 0 { + limit = 20 + } + if limit > 100 { + limit = 100 + } + // 判断评论状态,抓取未审核的评论 + if status == "pending" { + filter["status"] = 2 + } + // 拉取已经审核成功且无子评论的一级评论 + if status == "pending_reply" { + filter["status"] = 1 + filter["level"] = 1 + } + // 查询评论列表 + opts := options.FindOptions{} + opts.SetSort(bson.D{{Key: "createdAt", Value: 1}}).SetLimit(int64(limit)) + commentList, err := cmtmod.GetList(filter, &opts) + if err != nil { + log.Error("pull comment list fail", log.E(err)) + return resp, err + } + resp = cmtmod.PullResp{ + Code: 200, + Msg: "success", + Data: nil, + } + for _, comment := range commentList { + pullComment := cmtmod.PullComment{ + Id: comment.ID.Hex(), + UserId: strconv.FormatUint(comment.UserID, 10), + Nickname: "", + Content: comment.Content, + CreatedAt: comment.CreatedAt.Format("2006-01-02 15:04:05"), + Ip: "", + Context: cmtmod.Context{ + ContentId: comment.ObjID.Hex(), + ContentTitle: comment.Title, + }, + } + switch comment.ObjType { + case cmtmod.OTypeVideo: + pullComment.Context.ContentType = 1 + case cmtmod.OTypeCartoon: + pullComment.Context.ContentType = 3 + default: + pullComment.Context.ContentType = 2 + } + resp.Data = append(resp.Data, pullComment) + } + log.Info("pull comment list success", log.Any("resp", resp)) + return resp, nil +} + +func CommentApprove(comments cmtmod.ApproveReq) (err error) { + var passCommentIDs []primitive.ObjectID + var reviewCommentIDs []primitive.ObjectID + // 遍历评论列表 + for _, comment := range comments.Reviews { + oid, parseErr := primitive.ObjectIDFromHex(comment.CommentId) + if parseErr != nil || oid.IsZero() { + continue + } + reviewCommentIDs = append(reviewCommentIDs, oid) + // 审核通过评论 + if comment.Action == "approve" { + passCommentIDs = append(passCommentIDs, oid) + } else if comment.Action == "reject" { + // 评论没有通过,更新评论状态,以及评论没有通过原因 + err = cmtmod.Update(bson.M{"_id": oid}, bson.M{"$set": bson.M{"status": 4, "reason": comment.Reason, "desc": "Agent system review reject"}}) + if err != nil { + log.Error("web service comment CommentApprove cmtmod.Update fail", log.E(err), log.Any("id", oid)) + return + } + } + } + + if len(passCommentIDs) > 0 { + err = cmtmod.UpdateMany(bson.M{"_id": bson.M{"$in": passCommentIDs}}, bson.M{"$set": bson.M{"status": 1, "desc": "Agent system review"}}) + if err != nil { + log.Error("web service comment CommentApprove cmtmod.UpdateMany fail", log.E(err), log.Any("_id", passCommentIDs)) + return + } + } + + if len(reviewCommentIDs) == 0 { + return nil + } + + commentList, err := cmtmod.GetCommentsByIDS(reviewCommentIDs) + if err != nil { + log.Error("web service comment CommentApprove cmtmod.GetCommentsByIDS fail", log.E(err), log.Any("ids", reviewCommentIDs)) + return err + } + go rebuildVideoAndMediaCommentCount(commentList) + return +} + +func ReplyComment(comments cmtmod.RepliesReq) (cmtmod.ReplyResp, error) { + now := time.Now() + // 从缓存获取随机用户列表 + userInfos, err := getReplyUsers() + if err != nil { + log.Error("web service comment ReplyComment getReplyUsers fail", log.E(err)) + return cmtmod.ReplyResp{Code: 0, Msg: "web service comment ReplyComment Error"}, err + } + if len(userInfos) == 0 { + return cmtmod.ReplyResp{Code: 0, Msg: "web service comment ReplyComment Error"}, fmt.Errorf("no available users for auto reply") + } + + // 先收集所有有效的评论ID + oidMap := make(map[primitive.ObjectID]string, len(comments.Replies)) // oid -> reply content + oids := make([]primitive.ObjectID, 0, len(comments.Replies)) + for _, reply := range comments.Replies { + oid, parseErr := primitive.ObjectIDFromHex(reply.CommentId) + if parseErr != nil || oid.IsZero() { + continue + } + oids = append(oids, oid) + oidMap[oid] = reply.Content + } + + // 批量查询评论信息 + cmtInfos, err := cmtmod.CmtFindOneByIds(oids) + if err != nil { + log.Error("web service comment ReplyComment cmtmod.CmtFindOneByIds fail", log.E(err)) + return cmtmod.ReplyResp{Code: 0, Msg: "web service comment ReplyComment Error"}, err + } + + affectedComments := make([]cmtmod.Comment, 0, len(cmtInfos)) + insertDocs := make([]cmtmod.Comment, 0, len(cmtInfos)) + for _, cmtInfo := range cmtInfos { + if cmtInfo.ID.IsZero() { + continue + } + content, ok := oidMap[cmtInfo.ID] + if !ok { + continue + } + rand := common.RandInt(0, len(userInfos)) + insertDocs = append(insertDocs, cmtmod.Comment{ + ID: primitive.NewObjectID(), + ObjType: cmtInfo.ObjType, + Title: cmtInfo.Title, + ObjID: cmtInfo.ObjID, + CID: cmtInfo.ID, + RID: cmtmod.ObjectID{}, + GID: cmtmod.ObjectID{}, + Type: cmtmod.CmtTypeReply, + PublisherID: 0, + AuthorMark: false, + SortCnt: 0, + UserID: userInfos[rand].UID, + ToUserID: 0, + ToUserName: "", + LikeCount: 0, + CommentCount: 0, + IsGodComment: false, + IsAuthor: false, + IsRobot: false, + Content: content, + OrgContent: "", + Level: 2, + IPAddr: "", + Status: 1, + Reason: "", + Desc: "Agent system reply", + Image: "", + City: "", + LinkStr: "", + QuoteType: "", + QuoteID: primitive.ObjectID{}, + QuoteImg: "", + QuoteTitle: "", + IsDelete: false, + IsAdvertiser: false, + CreatedAt: now, + DeletedAt: time.Time{}, + }) + affectedComments = append(affectedComments, cmtInfo) + } + + if len(insertDocs) > 0 { + _, err = cmtmod.CmtInsertMany(insertDocs) + if err != nil { + log.Error("web service comment ReplyComment cmtmod.CmtInsertMany fail", log.E(err)) + return cmtmod.ReplyResp{Code: 0, Msg: "web service comment ReplyComment Error"}, err + } + } + + rebuildVideoAndMediaCommentCount(affectedComments) + return cmtmod.ReplyResp{ + Code: 200, + Msg: "success", + }, nil +} + +func rebuildVideoAndMediaCommentCount(comments []cmtmod.Comment) { + if len(comments) == 0 { + return + } + type objInfo struct { + objType cmtmod.OType + objID primitive.ObjectID + } + objMap := make(map[string]objInfo) + for _, cmt := range comments { + if cmt.ObjID.IsZero() { + continue + } + if cmt.ObjType != cmtmod.OTypeVideo && cmt.ObjType != cmtmod.OTypeCartoon { + continue + } + key := string(cmt.ObjType) + "_" + cmt.ObjID.Hex() + objMap[key] = objInfo{ + objType: cmt.ObjType, + objID: cmt.ObjID, + } + } + + var wg sync.WaitGroup + for _, v := range objMap { + obj := v + wg.Add(1) + common.Go(func() { + defer wg.Done() + rebuildSingleObjCommentCount(obj.objType, obj.objID) + }) + } + wg.Wait() +} + +func rebuildSingleObjCommentCount(objType cmtmod.OType, objID primitive.ObjectID) { + cond := bson.M{ + "objType": objType, + "objID": objID, + "status": 1, + "isAdvertiser": false, + "isDelete": false, + } + count, err := cmtmod.CmtFindCountByCond(cond) + if err != nil { + log.Error("web service comment rebuildSingleObjCommentCount cmtmod.CmtFindCountByCond fail", + log.E(err), log.Any("objType", objType), log.Any("objID", objID)) + return + } + + switch objType { + case cmtmod.OTypeVideo: + fakeDelta := 0 + videoInfo, err := vidmod.GetVideoInfo(objID.Hex()) + if err == nil && !videoInfo.ID.IsZero() { + fakeDelta = videoInfo.FakeCommentCount - videoInfo.CommentCount + if fakeDelta < 0 { + fakeDelta = 0 + } + } + _, err = vidmod.UpdateOneByID(objID, bson.M{ + "commentCount": int(count), + "fakeCommentCount": int(count) + fakeDelta, + }) + if err != nil { + log.Error("web service comment rebuildSingleObjCommentCount vidmod.UpdateOneByID fail", + log.E(err), log.Any("objID", objID), log.Any("count", count)) + } + case cmtmod.OTypeCartoon: + _, err = mediacontentmod.UpdateOneByID(objID, bson.M{"countComment": count}) + if err != nil { + log.Error("web service comment rebuildSingleObjCommentCount mediacontentmod.UpdateOneByID fail", + log.E(err), log.Any("objID", objID), log.Any("count", count)) + } + _, err = mediamod.UpdateByID(nil, objID, map[string]interface{}{"countComment": count}) + if err != nil { + log.Error("web service comment rebuildSingleObjCommentCount mediamod.UpdateByID fail", + log.E(err), log.Any("objID", objID), log.Any("count", count)) + } + } +} + +func ComicsTranslatePull(req *cmtmod.ComicsTitlePullReq) (cmtmod.ComicsTitlePullResp, error) { + resp := cmtmod.ComicsTitlePullResp{List: make([]cmtmod.ComicsTitleInfo, 0)} + if req == nil { + return resp, nil + } + + opts := options.Find() + pageNum := req.PageNum + if pageNum <= 0 { + pageNum = 1 + } + pageSize := req.PageSize + if pageSize <= 0 { + pageSize = 20 + } + opts.SetSkip((pageNum - 1) * pageSize) + opts.SetLimit(pageSize) + opts.SetSort(bson.D{{Key: "_id", Value: -1}}) + + filter := bson.M{} + if strings.TrimSpace(req.TagId) != "" { + tagID, err := primitive.ObjectIDFromHex(strings.TrimSpace(req.TagId)) + if err != nil { + return resp, err + } + filter["tags"] = tagID + } + + list, err := mediamod.QueryAllList(filter, opts) + if err != nil { + return resp, err + } + for _, one := range list { + resp.List = append(resp.List, cmtmod.ComicsTitleInfo{Id: one.ID.Hex(), Title: one.Title}) + } + return resp, nil +} + +func ComicsTranslatePush(req *cmtmod.ComicsTitlePushReq) (cmtmod.ComicsTitlePushRes, error) { + resp := cmtmod.ComicsTitlePushRes{} + if req == nil { + return resp, nil + } + + var tagID primitive.ObjectID + var hasTag bool + if strings.TrimSpace(req.TagId) != "" { + parsedTagID, err := primitive.ObjectIDFromHex(strings.TrimSpace(req.TagId)) + if err != nil { + return resp, err + } + tagID = parsedTagID + hasTag = true + } + + for _, one := range req.List { + mediaID, err := primitive.ObjectIDFromHex(strings.TrimSpace(one.Id)) + if err != nil { + return resp, err + } + update := bson.M{"title": strings.TrimSpace(one.Title), "updateTime": time.Now()} + if hasTag { + mediaInfo, err := mediamod.GetInfo(mediaID) + if err != nil { + return resp, err + } + newTags := make([]primitive.ObjectID, 0, len(mediaInfo.Tags)) + for _, tag := range mediaInfo.Tags { + if tag != tagID { + newTags = append(newTags, tag) + } + } + update["tags"] = newTags + } + if _, err = mediamod.UpdateByID(nil, mediaID, update); err != nil { + return resp, err + } + } + return resp, nil +} diff --git a/web/service/contentlibser/contentlib.go b/web/service/contentlibser/contentlib.go new file mode 100644 index 0000000..c343126 --- /dev/null +++ b/web/service/contentlibser/contentlib.go @@ -0,0 +1,104 @@ +package contentlibser + +import ( + "time" + + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/chatrobotmod" + "91porn-server/models/v/contentlibmod" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +const RobotContentSet = "robot_content" // 机器人聊天内容集合 + +// 获取内容列表 +func GetContentList(param contentlibmod.ReqContentList) (code stderr.Code, data interface{}, err error) { + var skip = int64((param.PageNumber - 1) * param.PageSize) + var limit = int64(param.PageSize) + total, cList, err := contentlibmod.GetContentList(param.RobotType, skip, limit) + if err != nil { + code = stderr.Failure + return + } + if cList == nil { + data = commod.ListResp{List: []contentlibmod.RespList{}} + } else { + list := make([]contentlibmod.RespList, len(cList)) + for i, v := range cList { + list[i] = contentlibmod.RespList(v) + } + data = commod.ListResp{List: list, Total: total} + } + code = stderr.Success + return +} + +// 增加机器人聊天内容 +func AddChatContent(param contentlibmod.ReqContentAdd) (code stderr.Code, data interface{}, err error) { + // 检查类型是否有效 + switch param.RobotType { + case chatrobotmod.Comment: + default: + code = stderr.RobotTypeInvalid + return + } + // 内容不能为空 + if param.Content == "" { + code = stderr.RobotContentIsNull + return + } + var doc = contentlibmod.ContentLibrary{ + RobotType: param.RobotType, + Content: param.Content, + CreatedAt: time.Now(), + } + // 添加到redis + if _, err = webg.Redis.SAdd(RobotContentSet, param.Content); err != nil { + code = stderr.Failure + return + } + if err = contentlibmod.Insert(doc); err != nil { + code = stderr.Failure + return + } + code = stderr.Success + return +} + +// 删除内容 +func DeleteChatContent(param contentlibmod.ReqContentDel) (code stderr.Code, data interface{}, err error) { + if param.IDS == nil || len(param.IDS) == 0 { + code = stderr.RobotContentDelParamIsNull + return + } + // 获取内容 + cList, err := contentlibmod.GetContentListByIDS(param.IDS) + if err != nil { + code = stderr.Failure + return + } + if len(cList) == 0 { + code = stderr.Failure + return + } + contents := make([]string, len(cList)) + for i, v := range cList { + contents[i] = v.Content + } + // 从redis中移除 + if _, err = webg.Redis.SRem(RobotContentSet, contents); err != nil { + code = stderr.Failure + return + } + if err = contentlibmod.DeleteByIDS(param.IDS); err != nil { + code = stderr.Failure + return + } + code = stderr.Success + return +} diff --git a/web/service/contentreviewser/applier.go b/web/service/contentreviewser/applier.go new file mode 100644 index 0000000..806c164 --- /dev/null +++ b/web/service/contentreviewser/applier.go @@ -0,0 +1,80 @@ +package contentreviewser + +import ( + "fmt" + + "91porn-server/models/v/contentreviewmod" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ApplyResolution 把命中记录里管理员提交的修改写回到对应数据源 +// 按 TargetType 分发: +// +// VIDEO → vidmod.UpdateForReview(title, content, richText) +// ACG_MEDIA → mediamod.UpdateForReview(title, summary) +// ACG_CONTENT → mediacontentmod.UpdateForReview(name, text) +// +// 该函数不修改 issue 自身的 resolveStatus;调用方在写回成功后再调 ApproveIssue +func ApplyResolution(issue *contentreviewmod.ReviewIssue) error { + switch issue.TargetType { + case contentreviewmod.IssueTargetVideo: + return vidmod.UpdateForReview( + issue.TargetID, + issue.SubmittedTitle, + issue.SubmittedContent, + issue.SubmittedRichText, + ) + case contentreviewmod.IssueTargetACGMedia: + return mediamod.UpdateForReview( + issue.TargetID, + issue.SubmittedTitle, + issue.SubmittedContent, + ) + case contentreviewmod.IssueTargetACGContent: + return mediacontentmod.UpdateForReview( + issue.TargetID, + issue.SubmittedTitle, + issue.SubmittedContent, + ) + default: + return fmt.Errorf("unsupported targetType: %s", issue.TargetType) + } +} + +// ApplyOffShelfBatch 按 TargetType 分组把命中目标批量下架: +// +// VIDEO → vidmod.OffShelfManyForReview(TargetIDs) 视频置为 OffShelf(5) +// ACG_MEDIA → mediamod.OffShelfManyForReview(TargetIDs) Media 置为 status=0 +// ACG_CONTENT → mediacontentmod.OffShelfManyForReview(TargetIDs) 章节置为 isActive=false +// +// 每个类型一次 UpdateMany;遇到未知 TargetType 直接返回错误,整批回滚交给调用方语义处理。 +// 该函数不修改 issue 自身 resolveStatus;调用方下架成功后再调 BatchOffShelfResolveIssues +func ApplyOffShelfBatch(issues []*contentreviewmod.ReviewIssue) error { + var videoIDs, mediaIDs, contentIDs []primitive.ObjectID + for _, iss := range issues { + switch iss.TargetType { + case contentreviewmod.IssueTargetVideo: + videoIDs = append(videoIDs, iss.TargetID) + case contentreviewmod.IssueTargetACGMedia: + mediaIDs = append(mediaIDs, iss.TargetID) + case contentreviewmod.IssueTargetACGContent: + contentIDs = append(contentIDs, iss.TargetID) + default: + return fmt.Errorf("unsupported targetType: %s (issueId=%s)", iss.TargetType, iss.ID.Hex()) + } + } + if err := vidmod.OffShelfManyForReview(videoIDs); err != nil { + return fmt.Errorf("offshelf videos fail: %w", err) + } + if err := mediamod.OffShelfManyForReview(mediaIDs); err != nil { + return fmt.Errorf("offshelf media fail: %w", err) + } + if err := mediacontentmod.OffShelfManyForReview(contentIDs); err != nil { + return fmt.Errorf("offshelf media content fail: %w", err) + } + return nil +} diff --git a/web/service/currencyser/currency.go b/web/service/currencyser/currency.go new file mode 100644 index 0000000..e40f3ef --- /dev/null +++ b/web/service/currencyser/currency.go @@ -0,0 +1,49 @@ +package currencyser + +import ( + "91porn-server/common/stderr" + "91porn-server/models/v/currencymod" +) + +// Add 新增裸聊资料 +func Add(in *AddCond) stderr.Code { + // 插入数据库 + n, err := in.Document() + if err != nil { + return stderr.ErrReqForbidden + } + if err := currencymod.Add(n); err != nil { + return stderr.ErrDbInsertError + } + return stderr.Success +} + +// Edit 编辑裸聊资料 +func Edit(in *EditCond) stderr.Code { + if err := currencymod.Edit(in.Cond(), in.Update()); err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success +} + +// QueryAll 查询裸聊列表 +func QueryAll(in *QueryAllCond) (interface{}, stderr.Code) { + var data = map[string]interface{}{ + "data": []interface{}{}, + "total": 0, + } + count, err := currencymod.QueryCount(in.Query()) + if err != nil { + return data, stderr.ErrDbQueryError + } + if count == 0 { + return data, stderr.Success + } + data["total"] = count + list, err := currencymod.QueryAll(in.Query(), in.Options()) + if err != nil { + return data, stderr.ErrDbQueryError + } + data["data"] = list + return data, stderr.Success +} diff --git a/web/service/currencyser/type.go b/web/service/currencyser/type.go new file mode 100644 index 0000000..f4d20ae --- /dev/null +++ b/web/service/currencyser/type.go @@ -0,0 +1,135 @@ +package currencyser + +import ( + "encoding/json" + "fmt" + "time" + + "91porn-server/common/log" + "91porn-server/models/commod" + "91porn-server/models/v/currencymod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type AddCond struct { + Name string `json:"name"` //货币名称 + Type commod.CurrencyType `json:"type"` //货币类型 + Coins int64 `json:"coins"` //购买货币数 + Price int64 `json:"price"` //价格 单位:元 + CouponDesc string `json:"couponDesc"` //优惠描述 + GiveVipDays int `json:"giveVipDays"` //赠送vip天数 + LouFengUnlockTimes int `json:"louFengUnlockTimes"` //赠送楼凤解锁次数 + GiveGameCoin int64 `json:"giveGameCoin"` //赠送游戏金币 + GiveFruitCoin int64 `json:"giveFruitCoin"` //赠送果币 + GiveGold int64 `json:"giveGold"` //赠送金币 + FirstGiveVipDays int `json:"firstGiveVipDays"` //首次赠送vip天数 + FirstGiveGameCoin int64 `json:"firstGiveGameCoin"` //首次赠送游戏金币 + NotFirst bool `json:"notFirst"` //非首充依然赠送 + Active bool `json:"active"` //是否激活 +} + +func (cond *AddCond) Document() (interface{}, error) { + data, err := json.Marshal(&cond) + if err != nil { + log.Error(fmt.Sprintf("新增裸聊json序列化异常[%v]", err)) + return nil, err + } + var n currencymod.Currency + if err = json.Unmarshal(data, &n); err != nil { + log.Error(fmt.Sprintf("新增裸聊json反序列化异常[%v]", err)) + return nil, err + } + n.CreatedAt = time.Now() + n.UpdatedAt = time.Now() + return n, nil +} + +type EditCond struct { + ID primitive.ObjectID `json:"id" binding:"required"` //裸聊ID + Name *string `json:"name"` //货币名称 + Type *commod.CurrencyType `json:"type"` //货币类型 + Coins *int64 `json:"coins"` //购买货币数 + Price *int64 `json:"price"` //价格 单位:元 + CouponDesc *string `json:"couponDesc"` //优惠描述 + GiveVipDays *int `json:"giveVipDays"` //赠送vip天数 + LouFengUnlockTimes *int `json:"louFengUnlockTimes"` //赠送楼凤解锁次数 + GiveGold *int64 `json:"giveGold"` //赠送金币 + GiveGameCoin *int64 `json:"giveGameCoin"` //赠送游戏金币 + GiveFruitCoin *int64 `json:"giveFruitCoin"` //赠送果币 + NotFirst *bool `json:"notFirst"` //非首充依然赠送 + IsActive *bool `json:"isActive"` //是否激活 + FirstGiveVipDays *int `json:"firstGiveVipDays" bson:"firstGiveVipDays"` //首次赠送vip天数 + FirstGiveGameCoin *int64 `json:"firstGiveGameCoin" bson:"firstGiveGameCoin"` //首次赠送游戏金币 +} + +func (cond *EditCond) Cond() primitive.M { + return bson.M{"_id": cond.ID} +} + +func (cond *EditCond) Update() primitive.M { + var update = bson.M{} + if cond.Name != nil { + update["name"] = cond.Name + } + if cond.Type != nil { + update["type"] = cond.Type + } + if cond.Coins != nil { + update["coins"] = cond.Coins + } + if cond.Price != nil { + update["price"] = cond.Price + } + if cond.CouponDesc != nil { + update["couponDesc"] = cond.CouponDesc + } + if cond.GiveVipDays != nil { + update["giveVipDays"] = cond.GiveVipDays + } + if cond.LouFengUnlockTimes != nil { + update["louFengUnlockTimes"] = cond.LouFengUnlockTimes + } + if cond.GiveGold != nil { + update["giveGold"] = cond.GiveGold + } + if cond.GiveGameCoin != nil { + update["giveGameCoin"] = cond.GiveGameCoin + } + if cond.GiveFruitCoin != nil { + update["giveFruitCoin"] = cond.GiveFruitCoin + } + if cond.NotFirst != nil { + update["notFirst"] = cond.NotFirst + } + if cond.IsActive != nil { + update["isActive"] = cond.IsActive + } + if cond.FirstGiveVipDays != nil { + update["firstGiveVipDays"] = cond.FirstGiveVipDays + } + if cond.FirstGiveGameCoin != nil { + update["firstGiveGameCoin"] = cond.FirstGiveGameCoin + } + return bson.M{"$set": update} +} + +type QueryAllCond struct { + IsActive *bool `form:"isActive"` //是否激活 + Type commod.CurrencyType `form:"type" binding:"required"` //货币类型 + Page commod.Page +} + +func (cond *QueryAllCond) Query() primitive.M { + var query = bson.M{"type": cond.Type} + if cond.IsActive != nil { + query["isActive"] = cond.IsActive + } + return query +} + +func (cond *QueryAllCond) Options() *options.FindOptions { + return options.Find().SetLimit(int64(cond.Page.Limit())).SetSkip(int64(cond.Page.Skip())).SetSort(bson.D{{Key: "coins", Value: 1}}) +} diff --git a/web/service/daichongser/daichong.go b/web/service/daichongser/daichong.go new file mode 100644 index 0000000..7a08819 --- /dev/null +++ b/web/service/daichongser/daichong.go @@ -0,0 +1,72 @@ +package daichongser + +import ( + "context" + "time" + + "91porn-server/common/daichong" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/rchgordmod" + "91porn-server/web/webg" +) + +const ( + rate_yinse_coin = 10 //金额转换成金币的单位 1元=10音色币 + rate_yuan_feng = 100 //将元转换为分 + retries = 7 //通知7次 +) + +var interval = []int64{0, 60, 5 * 60, 10 * 60, 30 * 60, 60 * 60, 2 * 60 * 60} //通知时间间隔 + +// 上分通知 将订单的处理结果通知给代充平台 +func OrderCallBack(ctx context.Context, req daichong.OrderCallSign) (commod.Resp, stderr.Code) { + sign, err := daichong.Convert2Sign(req, webg.Conf.DaiChong.AppSecret) + if err != nil { + log.ErrorX(ctx, "create sign data wrong", log.Any("warn", err)) + return commod.Resp{}, stderr.Failure + } + orderReq := daichong.CommonReq{ + AppId: webg.Conf.DaiChong.AppID, + Data: sign, + } + resp, err := daichong.OrderCallBack(ctx, orderReq, webg.Conf.DaiChong.Domain) + if err != nil { + log.ErrorX(ctx, "GetOrderInfo send http wrong", log.Any("warn", err)) + return resp, stderr.Failure + } + return resp, resp.Code +} + +// 通知第三方回调的结果 按照时间间隔通知 +func NotifyThirdOrderCallBack(ctx context.Context, oid string, callBackTime int64, orderStatus int) { + var cnt = 0 + selector := rchgordmod.ReplaceEditSelector{} + for cnt < retries { + order, _ := rchgordmod.FindRechargeOrderByThirdOrderId(oid) + if order.NotifyStatus == rchgordmod.Notify_SUCCESS { + return + } + time.Sleep(time.Duration(interval[cnt]) * time.Second) + resp, code := OrderCallBack(ctx, daichong.OrderCallSign{ + OrderId: oid, + Time: callBackTime, + Status: orderStatus, + }) + if code == stderr.Success { + notifySuccess := rchgordmod.Notify_SUCCESS + selector.NotifyStatus = ¬ifySuccess + _ = rchgordmod.UpdateBaseOnOidAndStatus(nil, oid, order.Status, selector) + log.Info("daichong order call back send http request successfully", log.Any("msg", resp.Msg), log.Any("orderstatus", orderStatus), + log.Any("orderId", oid), + log.Any("time", time.Now().Format(time.RFC3339))) + return + } + notifyFailure := rchgordmod.Notify_FAILURE + selector.NotifyStatus = ¬ifyFailure + _ = rchgordmod.UpdateBaseOnOidAndStatus(nil, oid, order.Status, selector) + log.Warn("daichong order call back send http request failed", log.Any("responseCode", code), log.Any("msg", resp.Msg)) + cnt++ + } +} diff --git a/web/service/deduction/deduction.go b/web/service/deduction/deduction.go new file mode 100644 index 0000000..7a2e430 --- /dev/null +++ b/web/service/deduction/deduction.go @@ -0,0 +1,44 @@ +package deduction + +import ( + "91porn-server/models/l/payvidlgmod" + "91porn-server/models/v/usermod" +) + +func StatPublisherDeduction(arg *Query) (*PublisherDeduction, error) { + res, err := payvidlgmod.PublisherVideoDeductionRange(*arg.UID, *arg.StartTime, *arg.EndTime) + if err != nil { + return nil, err + } + publisher, err := usermod.FindUserByUIDForNoCache(*arg.UID) + if err != nil { + return nil, err + } + // 避免脏数据引起的空指针 + if publisher == nil { + publisher = &usermod.User{UID: *arg.UID} + } + deduction, deductionCount, totalAmount, totalCount := int64(0), int64(0), int64(0), int64(0) + for _, stat := range res { + if stat.IsVideoDeduction { + deduction = stat.TotalAmount + deductionCount = stat.VideoCount + } + totalAmount += stat.TotalAmount + totalCount += stat.VideoCount + } + ret := PublisherDeduction{ + PublisherInfo: PublisherInfo{ + UID: publisher.UID, + Name: publisher.Name, + VideoDeduction: publisher.VideoDeduction, + VideoDeducitonCount: publisher.VideoDeductionCount, + VideoDeductionPayCount: publisher.VideoDeductionPayCount, + }, + AfterDeduction: totalAmount - deduction, + BeforeDeduction: totalAmount, + DeductionCount: deductionCount, + NonDeductionCount: totalCount - deductionCount, + } + return &ret, err +} diff --git a/web/service/deduction/type.go b/web/service/deduction/type.go new file mode 100644 index 0000000..ca8ce02 --- /dev/null +++ b/web/service/deduction/type.go @@ -0,0 +1,27 @@ +package deduction + +import "time" + +type Query struct { + StartTime *time.Time `form:"startTime" json:"startTime,omitempty" binding:"required"` //开始时间 + EndTime *time.Time `form:"endTime" json:"endTime,omitempty" binding:"required"` //结束时间 + UID *uint64 `form:"uid" json:"uid" binding:"required"` //uid +} + +// 视频博主信息 +type PublisherInfo struct { + UID uint64 `json:"uid"` // 视频博主id + Name string `json:"name"` // 博主姓名 + VideoDeduction float64 `json:"videoDeduction"` // 视频扣量 + VideoDeducitonCount int `json:"videoDeductionCount"` // 已扣量视频数 + VideoDeductionPayCount int `json:"videoDeductionPayCount"` // 视频售卖总次数 +} + +// 视频扣量统计 +type PublisherDeduction struct { + PublisherInfo `json:",inline"` + BeforeDeduction int64 `json:"beforeDeduction"` // 扣量前总售卖(金币) + AfterDeduction int64 `json:"afterDeduction"` // 扣量后总售卖(金币) + DeductionCount int64 `json:"deductionCount"` // 被扣量视频总数 + NonDeductionCount int64 `json:"nonDedcutionCount"` // 未扣量视频总数 +} diff --git a/web/service/discount_area_ser/discountArea.go b/web/service/discount_area_ser/discountArea.go new file mode 100644 index 0000000..8acdf57 --- /dev/null +++ b/web/service/discount_area_ser/discountArea.go @@ -0,0 +1,236 @@ +package discount_area_ser + +import ( + "91porn-server/common" + "91porn-server/common/cachev2" + "91porn-server/common/constant/redisconst" + sli "91porn-server/common/slice" + "91porn-server/common/stderr" + "91porn-server/models/v/discount_area_mod" + "91porn-server/models/v/discount_area_video_mod" + "91porn-server/models/v/vidmod" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +func AddDiscountArea(req *discount_area_mod.WebDiscountAreaAddReq) (err error) { + now := time.Now() + model := &discount_area_mod.DiscountArea{ + Title: req.Title, + Desc: req.Desc, + Discount: int(req.Discount), + Status: int(req.Status), + SortCode: int(req.SortCode), + RecommendNum: int(req.RecommendNum), + UpdatedAt: now, + CreatedAt: now, + } + //最多10个 + if model.RecommendNum > 10 { + model.RecommendNum = 10 + } + err = discount_area_mod.AddDiscountArea(model) + if err != nil { + return + } + // 删除缓存 + cachev2.Classes().AutoListKey(redisconst.DiscountArea).AutoClear(true, nil) + return +} + +func UpdateDiscountArea(req *discount_area_mod.WebDiscountAreaUpdateReq) (err error) { + var maxRecomment uint = 10 + if req.RecommendNum != nil && *req.RecommendNum > 10 { + req.RecommendNum = &maxRecomment + } + objId, err := primitive.ObjectIDFromHex(req.Id) + if err != nil { + return + } + update, err := common.ToBsonM(req.WebDiscountAreaUpdateData) + if err != nil { + return + } + update["updateAt"] = time.Now() + err = discount_area_mod.UpdateDiscountArea(objId, update) + if err != nil { + return + } + // 删除缓存 + cachev2.Classes().AutoListKey(redisconst.DiscountArea).AutoClear(true, nil) + return +} + +// DeleteDiscountArea 删除折扣专区 +func DeleteDiscountArea(id string) (err error) { + objId, err := primitive.ObjectIDFromHex(id) + if err != nil { + return + } + update := bson.M{ + "status": 2, + "updateAt": time.Now(), + } + err = discount_area_mod.UpdateDiscountArea(objId, update) + if err != nil { + return + } + // 删除缓存 + cachev2.Classes().AutoListKey(redisconst.DiscountArea).AutoClear(true, nil) + return +} + +// GetDiscountAreaVids 获取专区下的视频 +func GetDiscountAreaVids(req *discount_area_mod.WebDiscountAreaVidsReq) (list []discount_area_mod.WebDiscountAreaVideo, total int64, err error) { + filter := bson.M{} + if req.DiscountAreaId != "" { + discountAreaId, _ := primitive.ObjectIDFromHex(req.DiscountAreaId) + filter["discountAreaId"] = discountAreaId + } + if req.Vid != "" { + videoID, _ := primitive.ObjectIDFromHex(req.Vid) + filter["videoID"] = videoID + } + opts := options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}}).SetSkip(int64(req.Skip())).SetLimit(int64(req.Limit())) + discountAreaVideoList, total, err := discount_area_video_mod.GetListByCond(filter, opts) + if err != nil { + return + } + vids := []primitive.ObjectID{} + discountAreaIds := []primitive.ObjectID{} + // 获取视频信息 + for _, v := range discountAreaVideoList { + vids = append(vids, v.VideoID) + discountAreaIds = append(discountAreaIds, v.DiscountAreaId) + } + + videoList, err := vidmod.GetVideoListByIDsNoStatus(sli.RemoveRepObjectID(vids)) + if err != nil { + return + } + videoMap := make(map[primitive.ObjectID]vidmod.VideoModel) + for _, video := range videoList { + videoMap[video.ID] = *video + } + discountAreaList, err := discount_area_mod.GetNoStatusDiscountAreaByIds(sli.RemoveRepObjectID(discountAreaIds)) + if err != nil { + return + } + discountAreaMap := make(map[primitive.ObjectID]discount_area_mod.DiscountArea) + for _, discountArea := range discountAreaList { + discountAreaMap[discountArea.ID] = discountArea + } + + for _, v := range discountAreaVideoList { + discountAreaVideo := discount_area_mod.WebDiscountAreaVideo{ + Id: v.ID, + VideoID: v.VideoID, + DiscountAreaId: v.DiscountAreaId, + CreatedAt: v.CreatedAt, + UpdatedAt: v.UpdatedAt, + } + if video, ok := videoMap[v.VideoID]; ok { + discountAreaVideo.Title = video.Title + discountAreaVideo.Cover = video.Cover + discountAreaVideo.PublisherID = video.PublisherID + discountAreaVideo.OriginalPrice = video.Coins + } + if discountArea, ok := discountAreaMap[v.DiscountAreaId]; ok { + discountAreaVideo.DiscountAreaTitle = discountArea.Title + discountAreaVideo.DiscountedPrice = decimal.NewFromInt(int64(discountArea.Discount)).Shift(-2). + Mul(decimal.NewFromInt(discountAreaVideo.OriginalPrice)).Round(0).IntPart() + } + list = append(list, discountAreaVideo) + } + + return +} + +// DiscountAreaAddVid 折扣区新增视频 +func DiscountAreaAddVid(req *discount_area_mod.WebDiscountAreaAddVidReq) (code stderr.Code) { + if req.DiscountAreaId.IsZero() || len(req.VIds) == 0 { + return stderr.ErrParamError + } + // 判断是否是视频 + videoList, err := vidmod.GetVideoListByIDs(req.VIds) + if err != nil { + return stderr.ErrDbQueryError + } + vids := []primitive.ObjectID{} + for _, v := range videoList { + // 不是视频,直接返回错误 + if v.NewsType != vidmod.SP { + return stderr.NotAVideo + } + vids = append(vids, v.ID) + } + if len(vids) == 0 { + return + } + // 查询是否已经在其他折扣区,是的话则直接报错 + list, err := discount_area_video_mod.GetDiscountAreaVideoByVids(vids) + if err != nil { + return stderr.ErrDbQueryError + } + if len(list) > 0 { + // 不能一个视频同时在两个折扣区 + return stderr.VideoInDiscountArea + } + now := time.Now() + batchAddVids := []discount_area_video_mod.DiscountAreaVideo{} + for _, vid := range vids { + batchAddVids = append(batchAddVids, discount_area_video_mod.DiscountAreaVideo{ + DiscountAreaId: req.DiscountAreaId, + VideoID: vid, + CreatedAt: now, + UpdatedAt: now, + }) + } + // 写入折扣区 + err = discount_area_video_mod.BatchAdd(batchAddVids) + if err != nil { + return stderr.ErrDbInsertManyError + } + update := bson.M{} + update["discountAreaId"] = req.DiscountAreaId + update["updatedAt"] = time.Now() + _, err = vidmod.UpdateManyVideo(vids, update) + if err != nil { + return stderr.ErrDbUpdateError + } + + return stderr.Success +} + +// DiscountAreaDeleteVid 移除折扣区视频 +func DiscountAreaDeleteVid(ids []primitive.ObjectID) (code stderr.Code) { + if len(ids) == 0 { + return stderr.ErrParamError + } + + list, err := discount_area_video_mod.GetDiscountAreaVideoByIds(ids) + if err != nil { + return stderr.ErrDbQueryError + } + vids := []primitive.ObjectID{} + for _, v := range list { + vids = append(vids, v.VideoID) + } + // 删除视频 + err = discount_area_video_mod.DeleteDiscountAreaVideoByIds(ids) + if err != nil { + return stderr.ErrDbDeleteError + } + // 移除 + update := bson.M{} + update["discountAreaId"] = nil + update["updatedAt"] = time.Now() + _, err = vidmod.UpdateManyVideo(vids, update) + if err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success +} diff --git a/web/service/dramatopicser/topic.go b/web/service/dramatopicser/topic.go new file mode 100644 index 0000000..56ae663 --- /dev/null +++ b/web/service/dramatopicser/topic.go @@ -0,0 +1,492 @@ +package dramatopicser + +import ( + "errors" + "fmt" + "sort" + "strconv" + "strings" + "time" + "unicode/utf8" + + "91porn-server/common/db" + "91porn-server/common/dramatopic" + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/commod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/modulesectionmod" + "91porn-server/models/v/sysconfmod" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListRequest struct { + Name string `form:"name" json:"name"` + TopicType string `form:"topicType" json:"topicType" binding:"omitempty,oneof=SYSTEM CUSTOM"` + Status *int `form:"status" json:"status" binding:"omitempty,oneof=0 1"` + commod.Page +} + +type Topic struct { + TopicID string `json:"topicId"` + Name string `json:"name"` + TopicType string `json:"topicType"` + SystemKey string `json:"systemKey"` + Status int `json:"status"` + Sort int `json:"sort"` + WorkCount int64 `json:"workCount"` + Editable bool `json:"editable"` + WorksEditable bool `json:"worksEditable"` + CreatedAt *time.Time `json:"createdAt"` + UpdatedAt *time.Time `json:"updatedAt"` + systemOrder int +} + +type ListResponse struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []Topic `json:"list"` +} + +func (req ListRequest) List(now time.Time) (ListResponse, error) { + moduleID, ok, err := dramatopic.ModuleID(now) + if err != nil { + return ListResponse{}, err + } + sections := make([]modulesectionmod.Section, 0) + if ok { + sections, err = modulesectionmod.ListBySubModule(moduleID, 1000) + if err != nil { + return ListResponse{}, err + } + } + sectionIDs := make([]primitive.ObjectID, 0, len(sections)) + for _, section := range sections { + sectionIDs = append(sectionIDs, section.ID) + } + counts, err := mediamod.CountActiveDramaBySectionIDs(sectionIDs) + if err != nil { + return ListResponse{}, err + } + activeTotal, err := mediamod.QueryAllCount(bson.M{"mediaType": mediamod.MediaTypeDrama, "status": 1, "isDelete": false}) + if err != nil { + return ListResponse{}, err + } + all := make([]Topic, 0, 3+len(sections)) + if req.TopicType == "" || req.TopicType == dramatopic.TypeSystem { + for _, system := range dramatopic.SystemTopics() { + if !matches(req, system.Name, 1) { + continue + } + all = append(all, Topic{ + TopicID: system.ID, Name: system.Name, TopicType: dramatopic.TypeSystem, + SystemKey: system.SystemKey, Status: 1, Sort: system.Sort, WorkCount: activeTotal, + Editable: false, WorksEditable: false, systemOrder: system.TieOrder, + }) + } + } + if req.TopicType == "" || req.TopicType == dramatopic.TypeCustom { + for _, section := range sections { + status := int(valueOrZeroUint8(section.Status)) + if !matches(req, section.SectionName, status) { + continue + } + createdAt := section.CreatedAt + updatedAt := section.UpdatedAt + all = append(all, Topic{ + TopicID: section.ID.Hex(), Name: section.SectionName, TopicType: dramatopic.TypeCustom, + Status: status, Sort: valueOrZeroInt(section.Sort), WorkCount: counts[section.ID], + Editable: true, WorksEditable: true, CreatedAt: &createdAt, UpdatedAt: &updatedAt, + }) + } + } + sort.SliceStable(all, func(i, j int) bool { + if all[i].Sort != all[j].Sort { + return all[i].Sort > all[j].Sort + } + if all[i].systemOrder != all[j].systemOrder { + return all[i].systemOrder > all[j].systemOrder + } + if all[i].UpdatedAt == nil { + return false + } + if all[j].UpdatedAt == nil { + return true + } + return all[i].UpdatedAt.After(*all[j].UpdatedAt) + }) + response := ListResponse{Total: int64(len(all)), List: []Topic{}} + start := int(req.Skip64()) + if start >= len(all) { + return response, nil + } + end := start + int(req.PageSize) + if end > len(all) { + end = len(all) + } + response.HasNext = end < len(all) + response.List = all[start:end] + return response, nil +} + +func matches(req ListRequest, name string, status int) bool { + if req.Status != nil && *req.Status != status { + return false + } + return req.Name == "" || strings.Contains(strings.ToLower(name), strings.ToLower(strings.TrimSpace(req.Name))) +} + +type CreateRequest struct { + Name string `json:"name" binding:"required"` + Status *int `json:"status" binding:"omitempty,oneof=0 1"` + Sort *int `json:"sort" binding:"omitempty,min=0"` +} + +func (req CreateRequest) Create(now time.Time) (primitive.ObjectID, error) { + name, err := validateName(req.Name) + if err != nil { + return primitive.NilObjectID, err + } + moduleID, ok, err := dramatopic.ModuleID(now) + if err != nil || !ok { + if err == nil { + err = errors.New("短剧专题模块未配置") + } + return primitive.NilObjectID, err + } + status := uint8(1) + if req.Status != nil { + status = uint8(*req.Status) + } + topicSort := 0 + if req.Sort != nil { + topicSort = *req.Sort + } + id := primitive.NewObjectID() + err = modulesectionmod.InsertOne(modulesectionmod.Section{ + ID: id, SectionName: name, SubModuleID: moduleID, Status: &status, Sort: &topicSort, + }) + return id, err +} + +type UpdateRequest struct { + TopicID string `json:"topicId" binding:"required"` + Name *string `json:"name"` + Status *int `json:"status" binding:"omitempty,oneof=0 1"` + Sort *int `json:"sort" binding:"omitempty,min=0"` +} + +func (req UpdateRequest) Update(now time.Time) error { + topicID, moduleID, _, err := customTopic(req.TopicID, now) + if err != nil { + return err + } + fields := bson.M{} + if req.Name != nil { + name, nameErr := validateName(*req.Name) + if nameErr != nil { + return nameErr + } + fields["sectionName"] = name + } + if req.Status != nil { + fields["status"] = uint8(*req.Status) + } + if req.Sort != nil { + fields["sort"] = *req.Sort + } + if len(fields) == 0 { + return errors.New("至少需要修改一个字段") + } + return webg.VideoDB.Trans(func(t *db.MongoTool) error { + matched, updateErr := modulesectionmod.UpdateTopicFields(t, topicID, moduleID, fields) + if updateErr != nil { + return updateErr + } + if matched == 0 { + return errors.New("专题不存在") + } + if name, renamed := fields["sectionName"]; renamed { + if _, updateErr = mediamod.UpdateMany(t, bson.M{"sId": topicID}, bson.M{"$set": bson.M{"sectionName": name}}); updateErr != nil { + return updateErr + } + } + return nil + }) +} + +type DeleteRequest struct { + TopicID string `json:"topicId" binding:"required"` +} + +func (req DeleteRequest) Delete(now time.Time) error { + topicID, moduleID, _, err := customTopic(req.TopicID, now) + if err != nil { + return err + } + return webg.VideoDB.Trans(func(t *db.MongoTool) error { + if _, updateErr := mediamod.UpdateMany(t, bson.M{"sId": topicID}, bson.M{"$set": bson.M{ + "sId": primitive.NilObjectID, "sectionName": "", "sectionSort": 0, + }}); updateErr != nil { + return updateErr + } + deleted, deleteErr := modulesectionmod.DeleteTopic(t, topicID, moduleID) + if deleteErr != nil { + return deleteErr + } + if deleted != 1 { + return errors.New("自定义专题不存在") + } + return nil + }) +} + +type SortRequest struct { + Items []SortRequestItem `json:"items" binding:"required,min=1,max=1000,dive"` +} + +type SortRequestItem struct { + TopicID string `json:"topicId" binding:"required"` + Sort int `json:"sort" binding:"min=0"` +} + +func (req SortRequest) Update(now time.Time) error { + moduleID, moduleOK, err := dramatopic.ModuleID(now) + if err != nil { + return err + } + type target struct { + item SortRequestItem + system *dramatopic.SystemTopic + custom primitive.ObjectID + } + targets := make([]target, 0, len(req.Items)) + seen := make(map[string]struct{}, len(req.Items)) + systemTopics := make(map[string]dramatopic.SystemTopic, 3) + for _, system := range dramatopic.SystemTopics() { + systemTopics[system.ID] = system + } + for _, item := range req.Items { + if _, exists := seen[item.TopicID]; exists { + return errors.New("topicId不能重复") + } + seen[item.TopicID] = struct{}{} + if system, found := systemTopics[item.TopicID]; found { + topic := system + targets = append(targets, target{item: item, system: &topic}) + continue + } + if !moduleOK { + return errors.New("短剧专题模块未配置") + } + id, parseErr := primitive.ObjectIDFromHex(item.TopicID) + if parseErr != nil { + return errors.New("无效的专题ID") + } + section, findErr := modulesectionmod.GetBySectionByID(id) + if findErr != nil || section.SubModuleID != moduleID || section.DeletedAt != nil { + return errors.New("自定义专题不存在") + } + targets = append(targets, target{item: item, custom: id}) + } + sysconfmod.EnsureInitData() + for _, target := range targets { + if target.system != nil { + config, configErr := sysconfmod.GetByVCode(target.system.VCode) + if configErr != nil { + return configErr + } + if _, configErr = sysconfdata.UpdateData(nil, config.ID.Hex(), map[string]interface{}{ + "value": strconv.Itoa(target.item.Sort), "updatedAt": time.Now(), + }); configErr != nil { + return configErr + } + continue + } + if _, updateErr := modulesectionmod.UpdateTopicFields(nil, target.custom, moduleID, bson.M{"sort": target.item.Sort}); updateErr != nil { + return updateErr + } + } + return nil +} + +type UpdateWorksRequest struct { + TopicID string `json:"topicId" binding:"required"` + WorkIDs []string `json:"workIds" binding:"max=500,dive,required"` +} + +type UpdateWorksResponse struct { + TopicID string `json:"topicId"` + WorkCount int `json:"workCount"` +} + +type WorkConflict struct { + MediaID string `json:"mediaId"` + TopicID string `json:"topicId"` + TopicName string `json:"topicName"` +} + +type WorksConflictError struct { + Message string `json:"message"` + Conflicts []WorkConflict `json:"conflicts"` +} + +func (e *WorksConflictError) Error() string { return e.Message } + +type topicWorkAssignment struct { + ID primitive.ObjectID + Sort int +} + +func appendTopicWorkAssignments( + topicID primitive.ObjectID, + ids []primitive.ObjectID, + mediaMap map[primitive.ObjectID]mediamod.Media, + currentCount int64, +) ([]topicWorkAssignment, int) { + newIDs := make([]primitive.ObjectID, 0, len(ids)) + for _, id := range ids { + if mediaMap[id].SID != topicID { + newIDs = append(newIDs, id) + } + } + total := int(currentCount) + len(newIDs) + assignments := make([]topicWorkAssignment, 0, len(newIDs)) + for i, id := range newIDs { + assignments = append(assignments, topicWorkAssignment{ID: id, Sort: total - i}) + } + return assignments, total +} + +func (req UpdateWorksRequest) Update(now time.Time) (UpdateWorksResponse, error) { + topicID, _, section, err := customTopic(req.TopicID, now) + if err != nil { + return UpdateWorksResponse{}, err + } + ids := make([]primitive.ObjectID, 0, len(req.WorkIDs)) + seen := make(map[primitive.ObjectID]struct{}, len(req.WorkIDs)) + for _, raw := range req.WorkIDs { + id, parseErr := primitive.ObjectIDFromHex(raw) + if parseErr != nil { + return UpdateWorksResponse{}, errors.New("workIds包含无效ID") + } + if _, exists := seen[id]; exists { + return UpdateWorksResponse{}, errors.New("workIds不能重复") + } + seen[id] = struct{}{} + ids = append(ids, id) + } + medias, mediaMap, err := mediamod.GetListByIds(ids) + if err != nil { + return UpdateWorksResponse{}, err + } + if len(medias) != len(ids) { + return UpdateWorksResponse{}, errors.New("部分短剧不存在") + } + conflictTopicIDs := make([]primitive.ObjectID, 0) + for _, id := range ids { + media := mediaMap[id] + if media.MediaType != mediamod.MediaTypeDrama || media.IsDelete { + return UpdateWorksResponse{}, fmt.Errorf("作品%s不是有效短剧", id.Hex()) + } + if !media.SID.IsZero() && media.SID != topicID { + conflictTopicIDs = append(conflictTopicIDs, media.SID) + } + } + if len(conflictTopicIDs) > 0 { + sections, _ := modulesectionmod.GetBySectionByIDs(conflictTopicIDs) + names := make(map[primitive.ObjectID]string, len(sections)) + for _, item := range sections { + names[item.ID] = item.SectionName + } + conflict := &WorksConflictError{Message: "部分短剧已配置到其他专题", Conflicts: []WorkConflict{}} + for _, id := range ids { + media := mediaMap[id] + if !media.SID.IsZero() && media.SID != topicID { + conflict.Conflicts = append(conflict.Conflicts, WorkConflict{MediaID: id.Hex(), TopicID: media.SID.Hex(), TopicName: names[media.SID]}) + } + } + return UpdateWorksResponse{}, conflict + } + currentCount, err := mediamod.QueryAllCount(bson.M{ + "sId": topicID, "mediaType": mediamod.MediaTypeDrama, "isDelete": false, + }) + if err != nil { + return UpdateWorksResponse{}, err + } + // 作品配置是增量追加;已在当前专题内的作品保持原顺序,避免单个追加覆盖整批历史配置。 + assignments, total := appendTopicWorkAssignments(topicID, ids, mediaMap, currentCount) + if len(assignments) == 0 { + return UpdateWorksResponse{TopicID: req.TopicID, WorkCount: total}, nil + } + err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + for _, assignment := range assignments { + matched, updateErr := mediamod.UpdateMany(t, bson.M{ + "_id": assignment.ID, "mediaType": mediamod.MediaTypeDrama, "isDelete": false, + "$or": []bson.M{{"sId": primitive.NilObjectID}, {"sId": bson.M{"$exists": false}}}, + }, bson.M{"$set": bson.M{ + "sId": topicID, "sectionName": section.SectionName, "sectionSort": assignment.Sort, + }}) + if updateErr != nil { + return updateErr + } + if matched != 1 { + return errors.New("专题作品并发冲突,请刷新后重试") + } + } + return nil + }) + if err != nil { + return UpdateWorksResponse{}, err + } + return UpdateWorksResponse{TopicID: req.TopicID, WorkCount: total}, nil +} + +func customTopic(rawID string, now time.Time) (primitive.ObjectID, primitive.ObjectID, modulesectionmod.Section, error) { + if _, system := dramatopic.FindSystem(rawID); system { + return primitive.NilObjectID, primitive.NilObjectID, modulesectionmod.Section{}, errors.New("系统专题不允许此操作") + } + id, err := primitive.ObjectIDFromHex(rawID) + if err != nil { + return primitive.NilObjectID, primitive.NilObjectID, modulesectionmod.Section{}, errors.New("无效的专题ID") + } + moduleID, ok, err := dramatopic.ModuleID(now) + if err != nil || !ok { + if err == nil { + err = errors.New("短剧专题模块未配置") + } + return primitive.NilObjectID, primitive.NilObjectID, modulesectionmod.Section{}, err + } + section, err := modulesectionmod.GetBySectionByID(id) + if err != nil || section.SubModuleID != moduleID || section.DeletedAt != nil { + if err == nil { + err = errors.New("自定义专题不存在") + } + return primitive.NilObjectID, primitive.NilObjectID, modulesectionmod.Section{}, err + } + return id, moduleID, section, nil +} + +func validateName(raw string) (string, error) { + name := strings.TrimSpace(raw) + if count := utf8.RuneCountInString(name); count < 1 || count > 20 { + return "", errors.New("专题名称长度必须为1到20个字符") + } + return name, nil +} + +func valueOrZeroInt(value *int) int { + if value == nil { + return 0 + } + return *value +} + +func valueOrZeroUint8(value *uint8) uint8 { + if value == nil { + return 0 + } + return *value +} diff --git a/web/service/exchcodeser/exchcode.go b/web/service/exchcodeser/exchcode.go new file mode 100644 index 0000000..0b3a573 --- /dev/null +++ b/web/service/exchcodeser/exchcode.go @@ -0,0 +1,258 @@ +package exchcodeser + +import ( + "fmt" + "strconv" + "strings" + "time" + + "91porn-server/common" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/adminmod" + "91porn-server/models/v/exchcodemod" +) + +const SuperAdmin = "superAdmin" + +// 获取兑换码列表 +func GetExchangeCodeList(param exchcodemod.ListReqParam, manager string) (code stderr.Code, data interface{}, err error) { + var filterParams = exchcodemod.FilterDoc{ + BatchNum: param.BatchNum, + Channel: param.Channel, + Code: param.Code, + App: param.App, + Authority: param.Authority, + } + // 获取管理员角色 + admin, err := adminmod.FindOneByName(manager) + if err != nil || admin.ID.IsZero() { + code = stderr.AdminIDErr + return + } + var isSuperAdmin bool + if admin.Role == SuperAdmin { + isSuperAdmin = true + } + total, cList, err := exchcodemod.GetCodeList(filterParams, param.Status, param.Page, isSuperAdmin) + if err != nil { + code = stderr.Failure + return + } + list := make([]exchcodemod.RespList, len(cList)) + for i, v := range cList { + tmp := exchcodemod.RespList(v) + if v.InvalidAt.Before(time.Now()) { + tmp.Status = exchcodemod.StatusExpired + } + list[i] = tmp + } + if len(list) == 0 { + data = commod.ListResp{Total: 0, List: []exchcodemod.RespList{}} + } else { + data = commod.ListResp{Total: total, List: list} + } + code = stderr.Success + return +} + +// ExchangeCodes 获取兑换码列表,导出 +func ExchangeCodes(param exchcodemod.ListReqParam) []exchcodemod.RespList { + var filterParams = exchcodemod.FilterDoc{ + BatchNum: param.BatchNum, + Channel: param.Channel, + Code: param.Code, + App: param.App, + Authority: param.Authority, + } + var isSuperAdmin bool + if param.Role != nil && *param.Role == SuperAdmin { + isSuperAdmin = true + } + _, cList, err := exchcodemod.GetCodeList(filterParams, param.Status, param.Page, isSuperAdmin) + if err != nil { + return nil + } + list := make([]exchcodemod.RespList, len(cList)) + for i, v := range cList { + var tmp = exchcodemod.RespList(v) + if v.InvalidAt.Before(time.Now()) { + tmp.Status = exchcodemod.StatusExpired + } + list[i] = tmp + } + return list +} + +// 增加兑换码 +func AddExchangeCode(param exchcodemod.AddReqParam, manager string) (code stderr.Code, err error) { + // 检查app类型 + switch param.App { + case exchcodemod.AppTypeYSVideo: + case exchcodemod.AppTypePFVideo: + case exchcodemod.AppTypeLTVpn: + default: + code = stderr.ExchangeCodeInvalidApp + return code, err + } + // 检查权限 + switch param.Authority { + case exchcodemod.AuthorityShortVideoVip: + case exchcodemod.AuthorityFilmVip: + case exchcodemod.AuthoritySuper: + case exchcodemod.AuthorityVideoCoupon: + case exchcodemod.Authority3dPermanentVIP: + param.Channels = []string{"XUA01"} + case exchcodemod.AuthorityGold: + // 获取管理员角色 + admin, err := adminmod.FindOneByName(manager) + if err != nil || admin.ID.IsZero() { + code = stderr.AdminIDErr + return code, err + } + if admin.Role != SuperAdmin { + code = stderr.ExchangeCodeOperateDenied + return code, err + } + default: + code = stderr.ExchangeCodeInvalidAuthority + return code, err + } + // 检查渠道 + if param.Channels == nil || len(param.Channels) == 0 { + param.Channels = []string{"system"} + } + // 获取批次号 + eMod, err := exchcodemod.GetLatestBatchNumber(param.App) + if err != nil { + code = stderr.Failure + return + } + var batchNum = produceBatchNumber(param.App, eMod.BatchNum) + if batchNum == "" { + code = stderr.ExchangeCodeInvalidBatchNumber + return + } + var docs []exchcodemod.ExchCode + for i := 0; i < len(param.Channels); i++ { + for i2 := 0; int64(i2) < param.Count; i2++ { + var exchangeCode = common.InvitePromotionCodeGenera() + if exchangeCode == "" { + code = stderr.ExchangeCodeProduceCodeFailed + return + } + var now = time.Now() + var doc = exchcodemod.ExchCode{ + BatchNum: batchNum, + App: param.App, + Channel: param.Channels[i], + Code: exchangeCode, + Authority: param.Authority, + Status: exchcodemod.StatusUnused, + Reward: param.Reward, + RewardCount: param.RewardCount, + UsableNum: param.UsableNum, + Operator: manager, + Remark: param.Remark, + EffectiveAt: param.EffectiveAt, + InvalidAt: param.InvalidAt, + CreatedAt: now, + UpdatedAt: now, + } + docs = append(docs, doc) + } + } + if err = exchcodemod.BulkWrite(docs); err != nil { + code = stderr.Failure + return + } + code = stderr.Success + return +} + +// 生成兑换码 +func ProduceExchangeCode(n int) string { + if n >= 5 { + return "" + } + var length = 12 + c := common.RandBase32Upper(length) + if mod, err := exchcodemod.GetExchangeCodeByCode(c); err != nil || mod != nil { + c = ProduceExchangeCode(n + 1) + } + return c +} + +// 根据兑换类型和批次号删除兑换码你 +func UpdateExchangeCode(param exchcodemod.UpdateReqParam, manager string) (code stderr.Code, err error) { + // 获取管理员角色 + admin, err := adminmod.FindOneByName(manager) + if err != nil || admin.ID.IsZero() { + code = stderr.AdminIDErr + return + } + if param.Authority != nil && *param.Authority == exchcodemod.AuthorityGold && admin.Role != SuperAdmin { + code = stderr.ExchangeCodeOperateDenied + return + } + // 获取兑换码状态 + cMode, err := exchcodemod.GetExchangeCodeByID(param.ID) + if err != nil || cMode == nil { + code = stderr.ExchangeCodeInvalidCode + return + } + if cMode.InvalidAt.Before(time.Now()) || cMode.Status == exchcodemod.StatusUsed { + code = stderr.ExchangeCodeForbidUpdate + return + } + var update = exchcodemod.UpdateDoc{ + Status: param.Status, + Authority: param.Authority, + Channel: param.Channel, + Reward: param.Reward, + EffectiveAt: param.EffectiveAt, + InvalidAt: param.InvalidAt, + Remark: param.Remark, + UsableNum: param.UsableNum, + } + if err = exchcodemod.UpdateExchangeCode(param.ID, update); err != nil { + code = stderr.Failure + return + } + code = stderr.Success + return +} + +// 生成批次号 +func produceBatchNumber(app exchcodemod.AppType, lastBatchNum string) string { + var suffix string + switch app { + case exchcodemod.AppTypeYSVideo: + suffix = "YS-" + case exchcodemod.AppTypePFVideo: + suffix = "PF-" + case exchcodemod.AppTypeLTVpn: + suffix = "LT-" + } + if lastBatchNum == "" { + return suffix + "0001" + } + s := strings.Split(lastBatchNum, "-") + if len(s) < 2 { + return "" + } + n := s[1] + num, _ := strconv.ParseInt(n, 10, 64) + num++ + var batchNum string + if num >= 1000 { + batchNum = suffix + strconv.FormatInt(num, 10) + } else if num >= 100 && num < 1000 { + batchNum = suffix + fmt.Sprintf("0%d", num) + } else if num >= 10 && num < 100 { + batchNum = suffix + fmt.Sprintf("00%d", num) + } else { + batchNum = suffix + fmt.Sprintf("000%d", num) + } + return batchNum +} diff --git a/web/service/exchcodeser/exchlog.go b/web/service/exchcodeser/exchlog.go new file mode 100644 index 0000000..816968b --- /dev/null +++ b/web/service/exchcodeser/exchlog.go @@ -0,0 +1,49 @@ +package exchcodeser + +import ( + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/l/exchlogmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +// 获取兑换码日志列表 +func GetExchangeTypeList(param exchlogmod.ListReqParam) (code stderr.Code, data interface{}, err error) { + var filterParam = exchlogmod.FilterDoc{ + Code: param.Code, + UserID: param.UserID, + Channel: param.Channel, + Authority: param.Authority, + } + total, tList, err := exchlogmod.GetExchangeLogList(filterParam, param.Page) + if err != nil { + code = stderr.Failure + return + } + list := make([]exchlogmod.ListResp, len(tList)) + for i, v := range tList { + // 兼容之前的数据 + if v.RewardCount == 0 { + v.RewardCount = 1 + } + list[i] = exchlogmod.ListResp{ + Code: v.Code, + UserID: v.UserID, + Channel: v.Channel, + Authority: v.Authority, + Reward: v.Reward, + RewardCount: v.RewardCount, + CreatedAt: v.CreatedAt, + } + } + if tList == nil { + data = commod.ListResp{Total: 0, List: []exchlogmod.ExchangeLog{}} + } else { + data = commod.ListResp{Total: total, List: list} + } + code = stderr.Success + return +} diff --git a/web/service/exportser/export.go b/web/service/exportser/export.go new file mode 100644 index 0000000..52d49fe --- /dev/null +++ b/web/service/exportser/export.go @@ -0,0 +1,431 @@ +package exportser + +import ( + "container/list" + "errors" + "fmt" + "path/filepath" + "reflect" + "strconv" + "time" + + "91porn-server/common/constant" + "91porn-server/common/file" + "91porn-server/common/log" + "91porn-server/models/s/statordermod" + "91porn-server/models/s/statvidmod" + "91porn-server/models/v/exchcodemod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/wdordmod" + + "github.com/tealeg/xlsx" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Import 读取文件 +func Import(inFile string) (data *list.List, errr error) { + // 打开文件 + xlFile, err := xlsx.OpenFile(inFile) + if err != nil { + return nil, err + } + l := list.New() + // 遍历sheet页读取 + for _, sheet := range xlFile.Sheets { + fmt.Println("sheet name: ", sheet.Name) + //遍历行读取 + for _, row := range sheet.Rows { + var rowCell []string + // 遍历每行的列读取 + for _, cell := range row.Cells { + text := cell.String() + rowCell = append(rowCell, text) + } + l.PushBack(rowCell) + } + } + return l, nil +} + +type Student struct { + Name string + age int + Phone string + Gender string + Mail string +} + +// Export 导出xlsx文件 +func Export(typeFile int64) (string, error) { + switch typeFile { + case constant.ChannelLogTypeFile: + return ExampleExport() + default: + return "", errors.New("not this type of excel") + } +} + +func ExampleExport() (string, error) { + var fileName = "导出excel案列文件.xlsx" + f := xlsx.NewFile() + sheet, err := f.AddSheet("导出excel案列") + if err != nil { + fmt.Println(err.Error()) + return "", err + } + data := getStudents() + //add data + for _, stu := range data { + row := sheet.AddRow() + nameCell := row.AddCell() + nameCell.Value = stu.Name + ageCell := row.AddCell() + ageCell.Value = strconv.Itoa(stu.age) + phoneCell := row.AddCell() + phoneCell.Value = stu.Phone + genderCell := row.AddCell() + genderCell.Value = stu.Gender + mailCell := row.AddCell() + mailCell.Value = stu.Mail + } + var path, _ = filepath.Abs("temp") + if err = file.MakeDir(path); err != nil { + log.Warn("can't make fs dir", log.E(err)) + } + fpath := path + "/" + fileName + fmt.Println(fpath) + if err = f.Save(fpath); err != nil { + fmt.Println(err.Error()) + return "", err + } + fmt.Println("\n\nexport success") + return fpath, nil +} + +func StructToTitle(s *xlsx.Sheet, obj interface{}) { + t := reflect.TypeOf(obj) + row := s.AddRow() + for i := 0; i < t.NumField(); i++ { + k, _ := t.Field(i).Tag.Lookup("json") + c := row.AddCell() + c.Value = k + } +} + +func StructToTitle2(s *xlsx.Sheet, obj interface{}, lableMap map[string]string) { + t := reflect.TypeOf(obj) + v := reflect.ValueOf(obj) + row := s.AddRow() + for i := 0; i < t.NumField(); i++ { + vv := v.Field(i) + fv := vv.Type() + switch fv.Kind() { + case reflect.Struct: + } + k, _ := t.Field(i).Tag.Lookup("json") + lable, ok := lableMap[k] + if !ok { + lable = k + } + row.AddCell().Value = lable + } +} + +func getValue(kind reflect.Kind, value reflect.Value) string { + switch kind { + case reflect.String: + return value.String() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(value.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return strconv.FormatUint(value.Uint(), 10) + case reflect.Bool: + return strconv.FormatBool(value.Bool()) + case reflect.Float32, reflect.Float64: + return strconv.FormatFloat(value.Float(), 'f', 2, 64) + case reflect.Struct: + timeV, ok := value.Interface().(time.Time) + if ok { + return timeV.String() + } + if oid, ok := value.Interface().(primitive.ObjectID); ok { + return oid.Hex() + } + return "" + case reflect.Interface: + if timeV, ok := value.Interface().(time.Time); ok { + return timeV.String() + } + return "" + case reflect.Ptr: + if value.IsNil() { + return "" + } + value = value.Elem() + if value.Interface() != nil { + if timeV, ok := value.Interface().(time.Time); ok { + return timeV.String() + } + } else { + return "" + } + case reflect.Array: + if id, ok := value.Interface().(primitive.ObjectID); ok { + if id.IsZero() { + return "" + } else { + return id.Hex() + } + } + } + return "未知" +} + +func StructToRow(row *xlsx.Row, obj interface{}) error { + t := reflect.TypeOf(obj) + v := reflect.ValueOf(obj) + for i := 0; i < t.NumField(); i++ { + fv := v.Field(i).Type() + vv := v.Field(i) + c := row.AddCell() + c.Value = getValue(fv.Kind(), vv) + } + return nil +} + +func UserListExport(path string, index int, users []usermod.ExportUser) (string, error) { + var fileName = fmt.Sprintf("用户列表_%v.xlsx", index) + f := xlsx.NewFile() + sheet, err := f.AddSheet("用户列表") + if err != nil { + fmt.Println(err.Error()) + return "", err + } + StructToTitle(sheet, usermod.ExportUser{}) + //add data + for _, u := range users { + row := sheet.AddRow() + _ = StructToRow(row, u) + } + if err = file.MakeDir(path); err != nil { + log.Warn("can't make fs dir", log.E(err)) + } + fpath := path + "/" + fileName + fmt.Println(fpath) + err = f.Save(fpath) + if err != nil { + fmt.Println(err.Error()) + return "", err + } + fmt.Println("\n\nexport success") + return fpath, nil +} + +func getStudents() []Student { + students := make([]Student, 10) + for i := 0; i < 10; i++ { + stu := Student{} + stu.Name = "name" + strconv.Itoa(i+1) + stu.Mail = stu.Name + "@chairis.cn" + stu.Phone = "1380013800" + strconv.Itoa(i) + stu.age = 20 + stu.Gender = "男" + students[i] = stu + } + return students +} + +// RechargeOrderExport 充值订单存xlsx +func RechargeOrderExport(orders []*rchgordmod.RchargeOrderExport) (string, error) { + var fileName = "充值订单.xlsx" + f := xlsx.NewFile() + sheet, err := f.AddSheet("充值订单") + if err != nil { + fmt.Println(err.Error()) + return "", err + } + StructToTitle(sheet, rchgordmod.RchargeOrderExport{}) + //add data + for _, o := range orders { + row := sheet.AddRow() + _ = StructToRow(row, *o) + } + var path, _ = filepath.Abs("temp") + if err = file.MakeDir(path); err != nil { + log.Warn("can't make fs dir", log.E(err)) + } + fpath := path + "/" + fileName + if err = f.Save(fpath); err != nil { + return "", err + } + return fpath, nil +} + +// WithdrawOrderExport 提现订单存xlsx +func WithdrawOrderExport(orders []*wdordmod.WithdrawOrder) (string, error) { + var fileName = "提现订单.xlsx" + f := xlsx.NewFile() + sheet, err := f.AddSheet("提现订单") + if err != nil { + fmt.Println(err.Error()) + return "", err + } + StructToTitle(sheet, wdordmod.WithdrawOrder{}) + //add data + for _, o := range orders { + row := sheet.AddRow() + _ = StructToRow(row, *o) + } + var path, _ = filepath.Abs("temp") + if err = file.MakeDir(path); err != nil { + log.Warn("can't make fs dir", log.E(err)) + } + fpath := path + "/" + fileName + if err = f.Save(fpath); err != nil { + return "", err + } + return fpath, nil +} + +// GoldTurnoverExport 金币流水存xlsx +func GoldTurnoverExport(turnover []*txnmod.TransactionLog) (string, error) { + var fileName = "金币流水.xlsx" + f := xlsx.NewFile() + sheet, err := f.AddSheet("金币流水") + if err != nil { + fmt.Println(err.Error()) + return "", err + } + StructToTitle(sheet, txnmod.TransactionLog{}) + //add data + for _, t := range turnover { + row := sheet.AddRow() + _ = StructToRow(row, *t) + } + var path, _ = filepath.Abs("temp") + if err = file.MakeDir(path); err != nil { + log.Warn("can't make fs dir", log.E(err)) + } + fpath := path + "/" + fileName + if err = f.Save(fpath); err != nil { + return "", err + } + return fpath, nil +} + +func changeVideoStatRecord(incomes []statvidmod.VideoStatRecord) []statvidmod.VideoStatExport { + infos := make([]statvidmod.VideoStatExport, len(incomes)) + for i, v := range incomes { + vs := statvidmod.VideoStatExport{} + if v.SumDate != nil { + vs.SumDate = *v.SumDate + } + if v.Video.ID != nil { + vs.ID = *v.Video.ID + } + if v.Video.Title != nil { + vs.Title = *v.Video.Title + } + if v.Video.PublisherID != nil { + vs.PublisherID = *v.Video.PublisherID + } + if v.Video.Coins != nil { + vs.Coins = *v.Video.Coins + } + if v.Income != nil { + vs.Income = *v.Income + } + if v.Tax != nil { + vs.Tax = *v.Tax + } + if v.PayCount != nil { + vs.PayCount = *v.PayCount + } + if v.CreatedAt != nil { + vs.CreatedAt = *v.CreatedAt + } + infos[i] = vs + } + return infos +} + +// VideoIncomeExport 视频收入存xlsx +func VideoIncomeExport(incomes []statvidmod.VideoStatRecord) (string, error) { + infos := changeVideoStatRecord(incomes) + var fileName = "视频收入.xlsx" + f := xlsx.NewFile() + sheet, err := f.AddSheet("视频收入") + if err != nil { + fmt.Println(err.Error()) + return "", err + } + StructToTitle(sheet, statvidmod.VideoStatExport{}) + //add data + for _, i := range infos { + row := sheet.AddRow() + _ = StructToRow(row, i) + } + var path, _ = filepath.Abs("temp") + if err = file.MakeDir(path); err != nil { + log.Warn("can't make fs dir", log.E(err)) + } + fpath := path + "/" + fileName + if err = f.Save(fpath); err != nil { + return "", err + } + return fpath, nil +} + +// OrderStatExport 订单统计存xlsx +func OrderStatExport(orders []statordermod.OrderState) (string, error) { + var fileName = "订单统计.xlsx" + f := xlsx.NewFile() + sheet, err := f.AddSheet("订单统计") + if err != nil { + fmt.Println(err.Error()) + return "", err + } + StructToTitle(sheet, statordermod.OrderState{}) + //add data + for _, o := range orders { + row := sheet.AddRow() + _ = StructToRow(row, o) + } + var path, _ = filepath.Abs("temp") + if err = file.MakeDir(path); err != nil { + log.Warn("can't make fs dir", log.E(err)) + } + fpath := path + "/" + fileName + if err = f.Save(fpath); err != nil { + return "", err + } + return fpath, nil +} + +// ExchangeCodeExport 兑换码导出 +func ExchangeCodeExport(codes []exchcodemod.RespList) (string, error) { + var fileName = "兑换码.xlsx" + f := xlsx.NewFile() + sheet, err := f.AddSheet("兑换码") + if err != nil { + fmt.Println(err.Error()) + return "", err + } + StructToTitle(sheet, exchcodemod.RespList{}) + //add data + for _, c := range codes { + row := sheet.AddRow() + _ = StructToRow(row, c) + } + var path, _ = filepath.Abs("temp") + if err = file.MakeDir(path); err != nil { + log.Warn("can't make fs dir", log.E(err)) + } + fpath := path + "/" + fileName + if err = f.Save(fpath); err != nil { + return "", err + } + return fpath, nil +} diff --git a/web/service/ffser/ff.go b/web/service/ffser/ff.go new file mode 100644 index 0000000..942ac56 --- /dev/null +++ b/web/service/ffser/ff.go @@ -0,0 +1,55 @@ +package ffser + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math" + "os/exec" + "strconv" + "strings" + + "91porn-server/common/crypt" +) + +// AvFormat 调用ffmpeg 获取信息 +func AvFormat(fpath string) (resloution string, playTime uint, err error) { + args := makeArgs(fpath, []string{"width", "height", "duration"}...) + fmt.Printf("%v", args) + command := exec.CommandContext(context.Background(), "ffprobe", args...) + out, err := command.CombinedOutput() + if err != nil { + fmt.Printf("ffprobe convert to ts wrong %+v", err) + return + } + if strings.Contains(string(out), "Invalid argument") { + err = errors.New("ffmpeg convert err") + return + } + result, _ := crypt.JSON2Map(string(out)) + if v, ok := result["streams"].([]interface{}); ok { + if v2, ok := v[0].(map[string]interface{}); ok { + width, _ := json.Marshal(v2["width"]) + height, _ := json.Marshal(v2["height"]) + duration, _ := v2["duration"].(string) + resloution = string(width) + "*" + string(height) + pt, _ := strconv.ParseFloat(string(duration), 32) + playTime = uint(math.Ceil(pt)) + fmt.Println(playTime) + return + } + } + return +} + +func makeArgs(path string, command ...string) []string { + com := strings.Join(command, ",") + return []string{ + "-v", "quiet", + "-v", "quiet", + "-select_streams", "v", "-show_entries", + "stream=" + com, + "-i", path, + } +} diff --git a/web/service/filterser/filter.go b/web/service/filterser/filter.go new file mode 100644 index 0000000..08d0730 --- /dev/null +++ b/web/service/filterser/filter.go @@ -0,0 +1,67 @@ +package filterser + +import ( + "time" + + "91porn-server/common/filter" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/filtermod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// 获取敏感词列表 +func GetFilterWordList(content *string, page commod.Page) (code stderr.Code, data interface{}, err error) { + total, wordList, err := filtermod.GetFilterWordList(content, page) + if err != nil { + code = stderr.Failure + return + } + list := make([]filtermod.WordListResp, len(wordList)) + for i, v := range wordList { + list[i] = filtermod.WordListResp(v) + } + data = commod.ListResp{Total: total, List: list} + code = stderr.Success + return +} + +// 添加过滤词 +func AddFilterWord(words []string) (code stderr.Code, err error) { + docs := make([]filtermod.WordsFilter, len(words)) + for i, v := range words { + docs[i] = filtermod.WordsFilter{ + Word: v, + CreatedAt: time.Now(), + } + } + if len(docs) == 0 { + code = stderr.Success + return + } + if err = filtermod.InsertMany(docs); err != nil { + code = stderr.Failure + return + } + if err = filter.UpdateFilterWords(); err != nil { + code = stderr.Failure + return + } + code = stderr.Success + return +} + +// 删除过滤词 +func DeleteFilterWords(ids []primitive.ObjectID) (code stderr.Code, err error) { + if err = filtermod.DeleteFilterWords(ids); err != nil { + code = stderr.Failure + return + } + if err = filter.UpdateFilterWords(); err != nil { + code = stderr.Failure + return + } + code = stderr.Success + return +} diff --git a/web/service/imagetopser/imagetop.go b/web/service/imagetopser/imagetop.go new file mode 100644 index 0000000..e27d489 --- /dev/null +++ b/web/service/imagetopser/imagetop.go @@ -0,0 +1,129 @@ +package imagetopser + +import ( + "fmt" + "time" + + "91porn-server/common" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/imagetopmod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func FindImageTopList(req imagetopmod.ImageTopListReq) (code stderr.Code, data interface{}) { + resp := imagetopmod.ImageTopListRes{ + Total: 0, + List: make([]imagetopmod.ImageTopResp, 0), + } + //查询帖子列表 + skip := (req.PageNumber - 1) * req.PageSize + var filter = bson.M{} + switch req.TopType { + case 1: //推荐 + filter = bson.M{"recommendSort": bson.M{"$gt": 0}} + case 2: //最新 + filter = bson.M{"newSort": bson.M{"$gt": 0}} + } + sort := bson.D{{Key: "updatedAt", Value: -1}} + opts := options.Find().SetSort(sort).SetSkip(int64(skip)).SetLimit(int64(req.PageSize)) + imageTopList, err := imagetopmod.GetImageTopList(filter, opts) + if err != nil { + log.Error(fmt.Sprintf("获取视频列表 发生错误:%v", err), log.Any("params", req)) + return stderr.ErrDbQueryError, nil + } + //获取置顶视频数量 + total, _ := imagetopmod.GetImageTopCount(filter) + resp.Total = total + //根据视频ID获取置顶视频信息 + resp.List = make([]imagetopmod.ImageTopResp, len(imageTopList)) + for i, v := range imageTopList { + vidInfo, _ := vidmod.GetVideoByID(v.ImageId) + resp.List[i] = imagetopmod.ImageTopResp{ + ID: vidInfo.ID, + NewsType: vidInfo.NewsType, + PubliserID: vidInfo.PublisherID, + Title: vidInfo.Title, + SourceID: vidInfo.SourceID, + SourceURL: vidInfo.SourceURL, + FileName: vidInfo.Filename, + PlayTime: vidInfo.PlayTime, + Status: vidInfo.Status, + Cover: vidInfo.Cover, + CoverThumb: vidInfo.CoverThumb, + SeriesCover: vidInfo.SeriesCover, + PlayCount: vidInfo.PlayCount, + LikeCount: vidInfo.LikeCount, + CommentCount: vidInfo.CommentCount, + ShareCount: vidInfo.ShareCount, + FakeLikeCount: vidInfo.FakeLikeCount, + FakeCommentCount: vidInfo.FakeCommentCount, + FakeShareCount: vidInfo.FakeShareCount, + FakePlayCount: vidInfo.FakePlayCount, + Coins: vidInfo.Coins, + Resolution: vidInfo.Resolution, + FreeTime: vidInfo.FreeTime, + Chosen: vidInfo.Chosen, + FreeArea: vidInfo.FreeArea, + Size: vidInfo.Size, + CreatedAt: vidInfo.CreatedAt, + UpdatedAt: vidInfo.UpdatedAt, + Reason: vidInfo.Reason, + IsTopping: vidInfo.IsTopping, + IsRecommend: vidInfo.IsRecommend, + IsChoosen: vidInfo.IsChoosen, + RecoWeight: vidInfo.RecoWeight, + ReviewAccount: vidInfo.ReviewAccount, + LinkUrl: vidInfo.LinkUrl, + SortCode: vidInfo.SortCode, + Rewarded: vidInfo.Rewarded, + FakeRewarded: vidInfo.FakeRewarded, + LiaoBaTop: vidInfo.LiaoBaTop, + LiaoBaTopSort: vidInfo.LiaoBaTopSort, + WorksSort: vidInfo.WorksSort, + PageViewCount: vidInfo.PageViewCount, + ActivityID: vidInfo.ActivityID, + NewSort: v.NewSort, + NewEndTime: *v.NewEndTime, + RecommendSort: v.RecommendSort, + RecommendEndTime: *v.RecommendEndTime, + } + } + code = stderr.Success + data = resp + return +} + +func UpdateImageTop(req imagetopmod.EditReq) (code stderr.Code, data interface{}) { + doc := imagetopmod.ImageTopModel{ + ImageId: req.ID, + NewSort: req.NewSort, + RecommendSort: req.RecommendSort, + NewEndTime: &req.NewEndTime, + RecommendEndTime: &req.RecommendEndTime, + UpdatedAt: time.Now(), + } + update, _ := common.ToBsonM(doc) + if _, err := imagetopmod.UpdateImageTop(req.ID, update); err != nil { + log.Error(fmt.Sprintf("更新置顶帖子 发生错误:%v", err), log.Any("params", req)) + return stderr.ErrDbUpdateError, nil + } + return stderr.Success, nil +} + +func DeleteImageTop(req imagetopmod.DeleteReq) (code stderr.Code, data interface{}) { + if err := imagetopmod.DeleteOne(req.Id); err != nil { + log.Error(fmt.Sprintf("删除置顶贴子 发生错误:%v", err), log.Any("params", req)) + return stderr.ErrDbDeleteError, nil + } + set := bson.M{"happinessPlazaTop": 0} + //兼容老版本,将幸福广场参数置为0 + if _, err := vidmod.UpdateOneByID(req.Id, set); err != nil { + log.Error(fmt.Sprintf("更新视频信息 发生错误:%v", err), log.Any("params", req)) + return stderr.ErrDbUpdateError, nil + } + return stderr.Success, nil +} diff --git a/web/service/imgroupmemberser/imgroupmember.go b/web/service/imgroupmemberser/imgroupmember.go new file mode 100755 index 0000000..5cd8e0b --- /dev/null +++ b/web/service/imgroupmemberser/imgroupmember.go @@ -0,0 +1,73 @@ +package imgroupmemberser + +import ( + "time" + + "91porn-server/models/cache/imgroupmemberdata" + "91porn-server/models/commod" + "91porn-server/models/v/imgroupmembermod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + commod.Page +} +type WebListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []imgroupmembermod.ImGroupMember `json:"list"` +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res WebListRes, err error) { + filter := bson.M{} + res.List, res.Total, res.HasNext, err = imgroupmembermod.GetList(filter, int64(q.Skip()), int64(q.Limit()), q.GetSort()) + + return +} + +type WebCreateReq struct { + GroupId int64 `json:"groupId" bson:"groupId"` // 群组id + Uid uint64 `json:"uid" form:"uid" binding:"required"` // uid +} + +// Create 发布数据 +func (p *WebCreateReq) Create() error { + data := imgroupmembermod.ImGroupMember{ + GroupId: p.GroupId, + Uid: p.Uid, + Status: 1, + CreatedAt: time.Now(), + } + + // 创建数据 + if _, err := imgroupmemberdata.InsertData(nil, data); err != nil { + return err + } + + return nil +} + +type WebUpdateReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` + Status *int `json:"status" bson:"status"` // 0-禁言 1-可用 2-已移除 +} + +// Update 更新数据 +func (p *WebUpdateReq) Update() error { + _, err := imgroupmembermod.GetInfo(p.ID) + if err != nil { + return err + } + + data := make(map[string]interface{}) + if p.Status != nil { + data["status"] = *p.Status + } + if _, err = imgroupmemberdata.UpdateData(nil, p.ID.Hex(), data); err != nil { + return err + } + + return nil +} diff --git a/web/service/imgroupser/imgroup.go b/web/service/imgroupser/imgroup.go new file mode 100755 index 0000000..e056a7a --- /dev/null +++ b/web/service/imgroupser/imgroup.go @@ -0,0 +1,106 @@ +package imgroupser + +import ( + "math/rand" + "time" + + "91porn-server/models/cache/imgroupdata" + "91porn-server/models/commod" + "91porn-server/models/v/imgroupmod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + commod.Page +} +type WebListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []imgroupmod.ImGroup `json:"list"` +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res WebListRes, err error) { + filter := bson.M{} + res.List, res.Total, res.HasNext, err = imgroupmod.GetList(filter, int64(q.Skip()), int64(q.Limit()), q.GetSort()) + + return +} + +type WebCreateReq struct { + Name string `json:"name" form:"name" binding:"required"` // 群组名 + Cover string `json:"cover" form:"cover" binding:"required"` // 封面 + Summary string `json:"summary" form:"summary" binding:"required"` // 简介 + FakeMemberNum int64 `json:"fakeMemberNum" form:"fakeMemberNum"` // 成员数量(假的) + Price int64 `json:"price" form:"price"` // 加入群聊价格 0-免费 + Status int `json:"status" form:"status"` // 0-不可用 1-可用 + +} + +// Create 发布数据 +func (p *WebCreateReq) Create() error { + data := imgroupmod.ImGroup{ + GroupId: 10000000 + rand.Int63n(89999999), // 随机一个 + Name: p.Name, + Cover: p.Cover, + Summary: p.Summary, + FakeMemberNum: p.FakeMemberNum, + Price: p.Price, + Status: p.Status, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // 创建数据 + if _, err := imgroupdata.InsertData(nil, data); err != nil { + return err + } + + return nil +} + +type WebUpdateReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` + Name *string `json:"name"` // 群组名 + Cover *string `json:"cover"` // 封面 + Summary *string `json:"summary"` // 简介 + FakeMemberNum *int64 `json:"fakeMemberNum"` // 成员数量(假的) + Price *int64 `json:"price"` // 加入群聊价格 0-免费 + Status *int `json:"status" form:"status"` // 0-不可用 1-可用 +} + +// Update 更新数据 +func (p *WebUpdateReq) Update() error { + _, err := imgroupmod.GetInfo(p.ID) + if err != nil { + return err + } + + data := make(map[string]interface{}) + if p.Name != nil { + data["name"] = *p.Name + } + if p.Cover != nil { + data["cover"] = *p.Cover + } + if p.Summary != nil { + data["summary"] = *p.Summary + } + if p.FakeMemberNum != nil { + data["fakeMemberNum"] = *p.FakeMemberNum + } + if p.Price != nil { + data["price"] = *p.Price + } + if p.Status != nil { + data["status"] = *p.Status + } + data["updatedAt"] = time.Now() + + if _, err = imgroupdata.UpdateData(nil, p.ID.Hex(), data); err != nil { + return err + } + + return nil +} diff --git a/web/service/immessageser/immessage.go b/web/service/immessageser/immessage.go new file mode 100755 index 0000000..546e928 --- /dev/null +++ b/web/service/immessageser/immessage.go @@ -0,0 +1,106 @@ +package immessageser + +import ( + "time" + + "91porn-server/models/cache/immessagedata" + "91porn-server/models/commod" + "91porn-server/models/v/immessagemod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + GroupId *int64 `json:"groupId" form:"groupId"` + commod.Page +} +type WebListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []immessagemod.ImMessage `json:"list"` +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res WebListRes, err error) { + filter := bson.M{} + if q.GroupId != nil { + filter["groupId"] = *q.GroupId + } + res.List, res.Total, res.HasNext, err = immessagemod.GetList(filter, int64(q.Skip()), int64(q.Limit()), q.GetSort()) + + return +} + +type WebCreateReq struct { + GroupId int64 `json:"groupId" form:"groupId" binding:"required"` // 群组id + Uid uint64 `json:"uid" form:"uid" binding:"required"` // 用户id + Content string `json:"content" form:"content"` // 消息内容 + Image string `json:"image" form:"image"` // 图片 +} + +// Create 发布数据 +func (p *WebCreateReq) Create() error { + data := immessagemod.ImMessage{ + GroupId: p.GroupId, + Uid: p.Uid, + Content: p.Content, + Image: p.Image, + CreatedAt: time.Now(), + } + + // 创建数据 + if _, err := immessagedata.InsertData(nil, data); err != nil { + return err + } + + return nil +} + +type WebUpdateReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` + GroupId *int64 `json:"groupId"` // 群组id + Uid *int64 `json:"uid"` // 用户id + Content *string `json:"content"` // 消息内容 + Image *string `json:"image"` // 图片 +} + +// Update 更新数据 +func (p *WebUpdateReq) Update() error { + _, err := immessagemod.GetInfo(p.ID) + if err != nil { + return err + } + + data := make(map[string]interface{}) + if p.GroupId != nil { + data["groupId"] = *p.GroupId + } + if p.Uid != nil { + data["uid"] = *p.Uid + } + if p.Content != nil { + data["content"] = *p.Content + } + if p.Image != nil { + data["image"] = *p.Image + } + + if _, err = immessagedata.UpdateData(nil, p.ID.Hex(), data); err != nil { + return err + } + + return nil +} + +type WebDeleteReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` +} + +// Delete 删除数据 +func (p *WebDeleteReq) Delete() error { + if err := immessagedata.DeleteData(nil, p.ID); err != nil { + return err + } + + return nil +} diff --git a/web/service/infmtser/notice.go b/web/service/infmtser/notice.go new file mode 100644 index 0000000..a3ecb14 --- /dev/null +++ b/web/service/infmtser/notice.go @@ -0,0 +1,197 @@ +package infmtser + +import ( + "time" + + "91porn-server/common/pageopt" + "91porn-server/models/v/noticefmtmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ObjectID = primitive.ObjectID + +type Sender = noticefmtmod.Sender + +type Receiver = noticefmtmod.Receiver + +type UIDSlice = noticefmtmod.UIDSlice + +type NoticeType = noticefmtmod.NoticeType + +type RegularNotice = noticefmtmod.RegularNotice + +type PlanNotice = noticefmtmod.PlanNotice + +type SpecifyNotice = noticefmtmod.SpecifyNotice + +type AfterRegistNotice = noticefmtmod.AfterRegistNotice + +type MailNotice = noticefmtmod.MailNotice + +type TimeUnit = noticefmtmod.TimeUnit + +type TimeSlice = noticefmtmod.TimeSlice + +type Record struct { + ID ObjectID `json:"_id,omitempty"` //_id + NoticeCode string `json:"noticeCode"` //通知码 唯一标记不同的通知计划,可代替_id,方便复制数据 + Title string `json:"title"` //标题 + Sender Sender `json:"sender"` //发送者 + Receiver Receiver `json:"receiver"` //接受者 + Content string `json:"content"` //内容 + Remark string `json:"remark"` //备注 + NoticeType NoticeType `json:"noticeType"` //通知类型 + Enable bool `json:"enable"` //消息开关 true:允许通知Receiver,否则不允许通知Receiver + UIDList UIDSlice `json:"uidList"` //用户列表 + Rule string `json:"rule"` //描述 + + StartAt *time.Time `json:"startAt"` //缺省字段 + EndAt *time.Time `json:"endAt"` //缺省字段 + TimeUnit *TimeUnit `json:"timeUnit"` //缺省字段 + TimeCount *int64 `json:"timeCount"` //缺省字段 + PlanCount *int64 `json:"planCount"` //缺省字段 + SpecifyTimeList TimeSlice `json:"specifyTimeList"` //缺省字段 +} + +type NoticePage struct { + Total int64 `json:"total" bson:"total"` + List []Record `json:"list" bson:"list"` +} + +type TitleMatch = noticefmtmod.TitleMatch +type NoticeCodeMatch = noticefmtmod.NoticeCodeMatch +type NoticeTypeMatch = noticefmtmod.NoticeTypeMatch +type SenderMatch = noticefmtmod.SenderMatch +type ReceiverMatch = noticefmtmod.ReceiverMatch +type EnableMatch = noticefmtmod.EnableMatch + +func NoticePages(skip, limit int64, + titleMatch TitleMatch, + noticeCodeMatch NoticeCodeMatch, + noticeTypeMatch NoticeTypeMatch, + senderMatch SenderMatch, + receiverMatch ReceiverMatch, + enableMatch EnableMatch, +) (NoticePage, error) { + mats := []pageopt.Matcher{ + titleMatch.New(), + noticeCodeMatch.New(), + noticeTypeMatch.New(), + senderMatch.New(), + receiverMatch.New(), + enableMatch.New(), + } + sort := bson.D{{Key: "createdAt", Value: -1}} + list, err := noticefmtmod.List(sort, skip, limit, mats...) + if err != nil { + return NoticePage{}, err + } + recordList := make([]Record, len(list)) + for i, v := range list { + record := Record{ + ID: v.ID, + NoticeCode: v.NoticeCode, + Title: v.Title, + Sender: v.Sender, + Receiver: v.Receiver, + Content: v.Content, + Remark: v.Remark, + NoticeType: v.NoticeType, + Enable: v.Enable, + UIDList: v.UIDList, + } + switch v.NoticeType { + case noticefmtmod.RegularNoticeType: + record.Rule = v.RegularNotice.Rule() + record.StartAt = v.RegularNotice.StartAt + record.TimeUnit = v.RegularNotice.TimeUnit + record.TimeCount = v.RegularNotice.TimeCount + case noticefmtmod.PlanNoticeType: + record.Rule = v.PlanNotice.Rule() + record.StartAt = v.PlanNotice.StartAt + record.EndAt = v.PlanNotice.EndAt + record.PlanCount = v.PlanNotice.PlanCount + case noticefmtmod.SpecifyNoticeType: + record.Rule = v.SpecifyNotice.Rule() + record.SpecifyTimeList = v.SpecifyNotice.SpecifyTimeList + case noticefmtmod.AfterRegistNoticeType: + record.Rule = v.AfterRegistNotice.Rule() + record.TimeUnit = v.AfterRegistNotice.TimeUnit + record.TimeCount = v.AfterRegistNotice.TimeCount + } + recordList[i] = record + } + count, err := noticefmtmod.Count(mats...) + if err != nil { + return NoticePage{}, err + } + return NoticePage{ + count, + recordList, + }, nil +} + +type Mail struct { + ID ObjectID `json:"_id,omitempty"` //_id + NoticeCode string `json:"noticeCode"` //通知码 唯一标记不同的通知计划,可代替_id,方便复制数据 + Title string `json:"title"` //标题 + Receiver Receiver `json:"receiver"` //接受者 + Content string `json:"content"` //内容 + Remark string `json:"remark"` //备注 + Enable bool `json:"-"` //消息开关 true:允许通知Receiver,否则不允许通知Receiver + UIDList UIDSlice `json:"uidList"` //用户列表 + SendAt time.Time `json:"sendAt"` //发送时间 +} + +type MailPage struct { + Total int64 `json:"total" bson:"total"` + List []Mail `json:"list" bson:"list"` +} + +func MailPages(skip, limit int64, + titleMatch TitleMatch, + noticeCodeMatch NoticeCodeMatch, + receiverMatch ReceiverMatch, +) (MailPage, error) { + noticeType := noticefmtmod.MailNoticeType + mats := []pageopt.Matcher{ + (&NoticeTypeMatch{NoticeType: ¬iceType}).New(), + titleMatch.New(), + noticeCodeMatch.New(), + receiverMatch.New(), + } + sort := bson.D{{Key: "createdAt", Value: -1}} + list, err := noticefmtmod.List(sort, skip, limit, mats...) + if err != nil { + return MailPage{}, err + } + recordList := make([]Mail, len(list)) + for i, v := range list { + record := Mail{ + ID: v.ID, + NoticeCode: v.NoticeCode, + Title: v.Title, + Receiver: v.Receiver, + Content: v.Content, + Remark: v.Remark, + Enable: v.Enable, + UIDList: v.UIDList, + } + if v.NoticeType == noticefmtmod.MailNoticeType { + if v.MailNotice.SendAt != nil { + record.SendAt = *v.MailNotice.SendAt + } + } + recordList[i] = record + } + count, err := noticefmtmod.Count(mats...) + if err != nil { + return MailPage{}, err + } + return MailPage{ + count, + recordList, + }, nil +} diff --git a/web/service/integarl_exchange_ser/integral_exchange.go b/web/service/integarl_exchange_ser/integral_exchange.go new file mode 100644 index 0000000..974efea --- /dev/null +++ b/web/service/integarl_exchange_ser/integral_exchange.go @@ -0,0 +1,52 @@ +package integarl_exchange_ser + +import ( + "91porn-server/common/log" + "91porn-server/models/v/integralexcangemod" + "errors" + "fmt" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func Edit(in *integralexcangemod.EditCond, manager string) error { + oid, _ := primitive.ObjectIDFromHex(in.ID) + vp, err := integralexcangemod.FindOneById(oid) + if err != nil { + return err + } + + if vp.ID.IsZero() { + return errors.New("integral config is null") + } + if *in.Status == integralexcangemod.REFUSE { + // 退还积分 + + } + + if err = integralexcangemod.Edit(in.Filter(), in.Update(manager)); err != nil { + log.Error(fmt.Sprintf("integralconfigmod update error:%+v:", err)) + return err + } + return nil +} + +func QueryAll(in *integralexcangemod.WebListRequest) (*integralexcangemod.QueryAllRes, error) { + var out = &integralexcangemod.QueryAllRes{} + count, err := integralexcangemod.QueryAllCount(in.Filter()) + if err != nil { + return nil, err + } + if count == 0 { + out.List = []*integralexcangemod.IntegralExchange{} + out.Total = 0 + return out, nil + } + out.Total = count + out.List, err = integralexcangemod.QueryAllList(in.Filter(), in.Options()) + if err != nil { + log.Error(fmt.Sprintf("integralexcangemod queryAllList error:%+v:", err)) + return nil, err + } + return out, nil +} diff --git a/web/service/integralconfigser/integralconfig.go b/web/service/integralconfigser/integralconfig.go new file mode 100644 index 0000000..af8d227 --- /dev/null +++ b/web/service/integralconfigser/integralconfig.go @@ -0,0 +1,54 @@ +package integralconfigser + +import ( + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/integralconfigmod" + "errors" + "fmt" +) + +func Add(in *integralconfigmod.AddCond, manager string) stderr.Code { + err := integralconfigmod.InsertOne(nil, in.Generate(manager)) + if err != nil { + return stderr.ErrDbInsertError + } + return stderr.Success +} + +func Edit(in *integralconfigmod.EditCond, manager string) error { + vp, err := integralconfigmod.FindOneById(in.ID) + if err != nil { + return err + } + + if vp.ID.IsZero() { + return errors.New("integral config is null") + } + + if err = integralconfigmod.Edit(in.Filter(), in.Update(manager)); err != nil { + log.Error(fmt.Sprintf("integralconfigmod update error:%+v:", err)) + return err + } + return nil +} + +func QueryAll(in *integralconfigmod.QueryAllCond) (*integralconfigmod.QueryAllRes, error) { + var out = &integralconfigmod.QueryAllRes{} + count, err := integralconfigmod.QueryAllCount(in.Filter()) + if err != nil { + return nil, err + } + if count == 0 { + out.List = []*integralconfigmod.IntegralConfig{} + out.Total = 0 + return out, nil + } + out.Total = count + out.List, err = integralconfigmod.QueryAllList(in.Filter(), in.Options()) + if err != nil { + log.Error(fmt.Sprintf("integralconfigmod queryAllList error:%+v:", err)) + return nil, err + } + return out, nil +} diff --git a/web/service/jingangser/jingang.go b/web/service/jingangser/jingang.go new file mode 100644 index 0000000..5759aab --- /dev/null +++ b/web/service/jingangser/jingang.go @@ -0,0 +1,76 @@ +package jingangser + +import ( + "91porn-server/models/v/jingangmod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +func UpdateJingang(r UpdateJingangReq) error { + cond := make(bson.M) + if r.Name != nil { + cond["name"] = *r.Name + } + if r.Img != nil { + cond["img"] = *r.Img + } + if r.Desc != nil { + cond["desc"] = *r.Desc + } + if r.LinkType != nil { + cond["link_type"] = *r.LinkType + } + if r.LinkUrl != nil { + cond["link_url"] = *r.LinkUrl + } + if r.Sort != nil { + cond["sort"] = *r.Sort + } + if r.Status != nil { + cond["status"] = *r.Status + } + if !r.Mid.IsZero() { + cond["mid"] = r.Mid + } + if len(cond) == 0 { + return nil + } + cond["updatedAt"] = time.Now() + return jingangmod.EditJingang(nil, r.ID, cond) +} + +func AddJingang(r AddJingangReq) (*primitive.ObjectID, error) { + now := time.Now() + j := jingangmod.JingangModel{ + Name: r.Name, + CreatedAt: now, + UpdatedAt: now, + } + if r.Img != nil { + j.Img = *r.Img + } + if r.Desc != nil { + j.Desc = *r.Desc + } + if r.LinkType != nil { + j.LinkType = *r.LinkType + } + if r.LinkUrl != nil { + j.LinkUrl = *r.LinkUrl + } + if r.Sort != nil { + j.Sort = *r.Sort + } + if r.Status != nil { + j.Status = *r.Status + } + if !r.Mid.IsZero() { + j.Mid = r.Mid + } + return jingangmod.AddJingang(nil, j) +} + +func DeleteJingang(r DeleteJingangReq) error { + return jingangmod.DeleteJingang(nil, r.ID) +} diff --git a/web/service/jingangser/type.go b/web/service/jingangser/type.go new file mode 100644 index 0000000..e159785 --- /dev/null +++ b/web/service/jingangser/type.go @@ -0,0 +1,35 @@ +package jingangser + +import "go.mongodb.org/mongo-driver/bson/primitive" + +type JingangListRequest struct { + PageNum uint64 `form:"pageNum" json:"pageNum" binding:"required,min=1"` // 页码 + PageSize uint64 `form:"pageSize" json:"pageSize" binding:"required,min=1"` // 每页展示数 +} + +type UpdateJingangReq struct { + ID primitive.ObjectID `json:"id" form:"id" binding:"required"` // 金刚id + Mid primitive.ObjectID `json:"mid" bson:"mid"` // 所属模块 + Name *string `json:"name" form:"name"` // 名称 + Img *string `json:"img" form:"img"` // 图片地址 + Desc *string `json:"desc" form:"desc"` // 说明 + LinkType *int64 `json:"link_type" form:"link_type"` // 1 内部链接; 2 外部连接 + LinkUrl *string `json:"link_url" form:"link_url"` // 链接地址 + Sort *uint64 `json:"sort" form:"sort"` // 排序 + Status *uint64 `json:"status" form:"status"` // 1 正常; 2 禁用 +} + +type AddJingangReq struct { + Mid primitive.ObjectID `json:"mid" bson:"mid"` // 所属模块 + Name string `json:"name" form:"name" binding:"required"` // 名称 + Img *string `json:"img" form:"img"` // 图片地址 + Desc *string `json:"desc" form:"desc"` // 说明 + LinkType *int64 `json:"link_type" form:"link_type"` // 1 内部链接; 2 外部链接 + LinkUrl *string `json:"link_url" form:"link_url"` // 链接地址 + Sort *uint64 `json:"sort" form:"sort"` // 排序 + Status *uint64 `json:"status" form:"status"` // 1 正常; 2 禁用 +} + +type DeleteJingangReq struct { + ID primitive.ObjectID `json:"id" form:"id" binding:"required"` +} diff --git a/web/service/laosijiser/comics.go b/web/service/laosijiser/comics.go new file mode 100644 index 0000000..239d588 --- /dev/null +++ b/web/service/laosijiser/comics.go @@ -0,0 +1,266 @@ +package laosijiser + +import ( + "91porn-server/common" + "91porn-server/common/laosiji" + "91porn-server/common/log" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + "91porn-server/web/service/vidser" + "context" + "errors" + "fmt" + "strconv" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func ComicsSearch(ctx context.Context, param laosiji.ComicsSearchListReq) (resp laosiji.ComicsSearchListResp, err error) { + comicsResp, err := laosiji.ComicsSearch(ctx, param) + if err != nil { + log.Error("MovieSearch", log.E(err)) + return + } + + //是否导入 + lsjIds := make([]string, 0) + for _, v := range comicsResp.Data { + lsjIds = append(lsjIds, v.Id) + } + comicsList, _ := mediamod.GetListByLsjIds(lsjIds) + + // 处理域名 + resp = laosiji.ComicsSearchListResp{ + Data: make([]laosiji.ComicsSearchInfo, 0), + Total: comicsResp.Total, + Current_page: comicsResp.Current_page, + Page_size: comicsResp.Page_size, + Last_page: comicsResp.Last_page, + } + for _, comicsInfo := range comicsResp.Data { + //comicsInfo.Img = common.JoinUrlPath(laosiji.IMAGEYUAN, comicsInfo.Img) + comicsInfo.Img = common.JoinUrlPath("laosiji", comicsInfo.Img) + + for _, comicsIn := range comicsList { + if comicsIn.LsjId == comicsInfo.Id { + comicsInfo.IsAdd = true + continue + } + } + + resp.Data = append(resp.Data, comicsInfo) + } + return resp, nil +} + +func ComicsDetail(ctx context.Context, param laosiji.ComicsDetailReq) (resp laosiji.ComicsDetailResp, serr error) { + resp, serr = laosiji.ComicsDetail(ctx, param) + if serr != nil { + log.Error("MovieSearch", log.E(serr)) + return + } + + // 处理域名 + //resp.Img = common.JoinUrlPath(laosiji.IMAGEYUAN, resp.Img) + resp.Img = common.JoinUrlPath("laosiji", resp.Img) + if resp.Chapter != nil && len(resp.Chapter) > 0 { + for i, cha := range resp.Chapter { + if len(cha.Img) > 0 { + //resp.Chapter[i].Img = common.JoinUrlPath(laosiji.IMAGEYUAN, resp.Chapter[i].Img) + resp.Chapter[i].Img = common.JoinUrlPath("laosiji", resp.Chapter[i].Img) + } + if cha.Content != nil && len(cha.Content) > 0 { + for y, content := range cha.Content { + //resp.Chapter[i].Content[y].F = common.JoinUrlPath(laosiji.IMAGEYUAN, content.F) + resp.Chapter[i].Content[y].F = common.JoinUrlPath("laosiji", content.F) + } + } + } + } + return +} + +func ComicsAddList(ctx context.Context, param laosiji.ComicsSyncReq, manager string) (hits []vidser.SensitiveHit, err error) { + if param.Ids == nil || len(param.Ids) <= 0 { + return + } + // 批量查询所有详情 + comicsDetails := make([]laosiji.ComicsDetailResp, 0) + for _, id := range param.Ids { + var comicsDetail laosiji.ComicsDetailResp + req := laosiji.ComicsDetailReq{ + Id: id, + } + comicsDetail, err = laosiji.ComicsDetail(ctx, req) + if err != nil { + log.Warn("ComicsAddList laosiji.ComicsDetail failed", log.Any("id", id), log.E(err)) + continue + } + // 处理域名 + //comicsDetail.Img = common.JoinUrlPath(laosiji.IMAGEYUAN, comicsDetail.Img) + comicsDetail.Img = common.JoinUrlPath("laosiji", comicsDetail.Img) + if comicsDetail.Chapter != nil && len(comicsDetail.Chapter) > 0 { + for i, cha := range comicsDetail.Chapter { + if len(cha.Img) > 0 { + comicsDetail.Chapter[i].Img = common.JoinUrlPath("laosiji", comicsDetail.Chapter[i].Img) + } + if cha.Content != nil && len(cha.Content) > 0 { + for y, content := range cha.Content { + comicsDetail.Chapter[i].Content[y].F = common.JoinUrlPath("laosiji", content.F) + } + } + } + } + comicsDetails = append(comicsDetails, comicsDetail) + } + if len(comicsDetails) <= 0 { + log.Warn("ComicsAddList laosiji.ComicsDetailResp len is 0") + err = errors.New("laosiji.ComicsDetailResp len is 0") + return + } + // 批量处理标签 + tags := make(map[string]struct{}) + for _, comicsDetail := range comicsDetails { + if comicsDetail.Tags != nil && len(comicsDetail.Tags) > 0 { + for _, tag := range comicsDetail.Tags { + tags[tag.Name] = struct{}{} + } + } + } + tagMaps := make(map[string]primitive.ObjectID) + if len(tags) > 0 { + tagList := make([]string, 0) + for k, _ := range tags { + tagList = append(tagList, k) + } + tagMaps, err = checkInsertMediaTag(tagList, 2) + if err != nil { + log.Error("ComicsAdd checkInsertMediaTag failed", log.E(err)) + return + } + } + terms := vidser.LoadEnabledSensitiveTerms() + hits = make([]vidser.SensitiveHit, 0) + failedIds := make([]string, 0) + for _, comicsDetail := range comicsDetails { + hit, oneErr := syncOneComics(comicsDetail, tagMaps, manager, terms) + if oneErr != nil { + log.Warn("ComicsAdd syncOneComics failed", log.Any("id", comicsDetail.Id), log.E(oneErr)) + failedIds = append(failedIds, comicsDetail.Id) + continue + } + if hit != nil { + hits = append(hits, *hit) + } + } + if len(failedIds) > 0 { + err = errors.New(fmt.Sprintf("需要同步%d部漫画,失败%d,失败Id[%s]", len(param.Ids), len(failedIds), strings.Join(failedIds, ","))) + } + return +} + +func syncOneComics(comicsDetail laosiji.ComicsDetailResp, tagMaps map[string]primitive.ObjectID, manager string, terms []string) (*vidser.SensitiveHit, error) { + now := time.Now() + mediaTemp, err := mediamod.GetListByLsjId(comicsDetail.Id) + if err != nil { + log.Warn("syncOneComics GetListByLsjId failed", log.Any("id", comicsDetail.Id), log.Any("err", err)) + return nil, err + } + if !mediaTemp.ID.IsZero() { + return nil, nil + } + mediaTemp = mediamod.Media{ + ID: primitive.NewObjectID(), + LsjId: comicsDetail.Id, + Title: comicsDetail.Name, + HorizontalCover: comicsDetail.Img, + VerticalCover: comicsDetail.Img, + Tags: make([]primitive.ObjectID, 0), + TagDetails: make([]mediamod.TagDetail, 0), + Summary: comicsDetail.Description, + FreeEpisode: 0, + CurrentEpisode: 0, + MediaType: mediamod.MediaTypeImage, + Kind: 5, + PermissionIconHide: false, + Permission: 2, + Price: 0, + ContentsPrice: 0, + Status: 1, + CreatedAt: now, + UpdateTime: now, + UpdatedAct: manager, + } + mediaTemp.TotalEpisode, _ = strconv.Atoi(comicsDetail.Chapter_count) + mediaTemp.UpdateStatus, _ = strconv.Atoi(comicsDetail.Update_status) + mediaTemp.Price, _ = strconv.ParseInt(comicsDetail.Money, 10, 64) + if mediaTemp.Price > 0 { + mediaTemp.Permission = 1 + } + //if mediaTemp.UpdateStatus == 1 { + // mediaTemp.UpdatedDesc = fmt.Sprintf("已完结%d话", mediaTemp.TotalEpisode) + //} else { + // mediaTemp.UpdatedDesc = fmt.Sprintf("更新至%d话", mediaTemp.TotalEpisode) + //} + for _, tag := range comicsDetail.Tags { + if v, ok := tagMaps[tag.Name]; ok { + mediaTemp.TagDetails = append(mediaTemp.TagDetails, mediamod.TagDetail{ + Id: v, + Name: tag.Name, + }) + mediaTemp.Tags = append(mediaTemp.Tags, v) + } + } + contents := make([]mediacontentmod.MediaContent, 0) + if len(comicsDetail.Chapter) > 0 { + for i, chapter := range comicsDetail.Chapter { + mediaContent := mediacontentmod.MediaContent{ + ID: primitive.NewObjectID(), + MediaID: mediaTemp.ID, + EpisodeNumber: i + 1, + ListenPermission: mediaTemp.Permission, + Price: mediaTemp.Price, + Name: fmt.Sprintf("第%d话", i+1), + Text: "", + Md5: "", + VideoUrl: "", + UrlSet: make([]string, 0), + Status: 1, + IsActive: true, + UpdatedAct: manager, + CreatedAt: now, + UpdateTime: now, + HashId: chapter.Id, + } + mediaContent.Height, _ = strconv.Atoi(chapter.Content[0].H) + mediaContent.Weight, _ = strconv.Atoi(chapter.Content[0].W) + if chapter.Content != nil && len(chapter.Content) > 0 { + for _, content := range chapter.Content { + mediaContent.UrlSet = append(mediaContent.UrlSet, content.F) + } + } + contents = append(contents, mediaContent) + } + } + mediaTemp.CurrentEpisode = len(contents) + // 敏感词命中 → 强制下架(Status=0) + hit := vidser.CheckTextHits(terms, comicsDetail.Id, mediaTemp.Title, mediaTemp.Summary, "") + if hit != nil { + mediaTemp.Status = 0 + log.Warn("syncOneComics sensitive hit, forced offline", + log.Any("sourceId", hit.SourceID), + log.Any("title", hit.Title), + log.Any("detail", hit.Detail)) + } + _, err = mediamod.Insert(nil, mediaTemp) + if err != nil { + log.Warn("syncOneComics InsertMedia failed", log.Any("media", mediaTemp.ID), log.E(err)) + return nil, err + } + if len(contents) > 0 { + _ = mediacontentmod.InsetBatch(nil, contents) + } + return hit, nil +} diff --git a/web/service/laosijiser/media.go b/web/service/laosijiser/media.go new file mode 100644 index 0000000..08c0625 --- /dev/null +++ b/web/service/laosijiser/media.go @@ -0,0 +1,207 @@ +package laosijiser + +import ( + "91porn-server/common/laosiji" + "91porn-server/common/log" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/mediatagmod" + "91porn-server/web/service/vidser" + "errors" + "fmt" + "strconv" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func checkInsertMediaTag(names []string, typ int) (data map[string]primitive.ObjectID, err error) { + data, err = mediatagmod.FindOneTagByNames(names, typ) + if err != nil { + log.Error("checkInsertMediaTag FindOneTagByiNames failed", log.Any("tags", names), log.E(err)) + return + } + insertTags := make([]mediatagmod.MediaTag, 0) + for _, tag := range names { + if _, ok := data[tag]; !ok { + insertTag := mediatagmod.MediaTag{ + ID: primitive.NewObjectID(), + Type: typ, + Name: tag, + Active: true, + Sort: 0, + IsDiscovery: false, + CreatedAt: time.Now(), + } + insertTags = append(insertTags, insertTag) + data[tag] = insertTag.ID + } + } + if len(insertTags) > 0 { + err = mediatagmod.InsertMany(nil, insertTags) + if err != nil { + log.Warn("checkInsertMediaTag InsertMany failed", log.Any("tags", insertTags), log.E(err)) + return + } + } + return +} + +func syncCartoonList(movieDetails []laosiji.MovieDetailResp, tags map[string]struct{}, manager string, terms []string) ([]vidser.SensitiveHit, error) { + hits := make([]vidser.SensitiveHit, 0) + tagMaps := make(map[string]primitive.ObjectID) + var err error + if len(tags) > 0 { + tagList := make([]string, 0) + for k, _ := range tags { + tagList = append(tagList, k) + } + tagMaps, err = checkInsertMediaTag(tagList, 1) + if err != nil { + log.Error("syncCartoonList checkInsertVidTags failed", log.E(err)) + return hits, err + } + } + failedIds := make([]string, 0) + for _, movieDetail := range movieDetails { + hit, oneErr := syncOneCartoon(movieDetail, tagMaps, manager, terms) + if oneErr != nil { + log.Warn("syncCartoonList syncOneCartoon failed", log.Any("id", movieDetail.Id), log.E(oneErr)) + failedIds = append(failedIds, movieDetail.Id) + continue + } + if hit != nil { + hits = append(hits, *hit) + } + } + if len(failedIds) > 0 { + return hits, errors.New(fmt.Sprintf("需要同步%d部动漫,失败%d,失败Id[%s]", len(movieDetails), len(failedIds), strings.Join(failedIds, ","))) + } + return hits, nil +} + +func syncOneCartoon(movieDetail laosiji.MovieDetailResp, tagMaps map[string]primitive.ObjectID, manager string, terms []string) (*vidser.SensitiveHit, error) { + now := time.Now() + mediaTemp, err := mediamod.GetListByLsjId(movieDetail.Id) + if err != nil { + log.Warn("syncOneCartoon GetListByLsjId failed", log.Any("id", movieDetail.Id), log.Any("err", err)) + return nil, err + } + if !mediaTemp.ID.IsZero() { + return nil, syncExistingCartoonH265(mediaTemp.ID, movieDetail.Links, manager) + } + mediaTemp = mediamod.Media{ + ID: primitive.NewObjectID(), + LsjId: movieDetail.Id, + Title: movieDetail.Name, + HorizontalCover: movieDetail.Img_x, + VerticalCover: movieDetail.Img_y, + Tags: make([]primitive.ObjectID, 0), + TagDetails: make([]mediamod.TagDetail, 0), + Summary: movieDetail.Description, + MediaType: mediamod.MediaTypeVideo, + Kind: 1, + PermissionIconHide: false, + Permission: 2, + Status: 1, + CreatedAt: now, + UpdateTime: now, + UpdatedAct: manager, + Anchor: movieDetail.Director, + } + + mediaTemp.UpdateStatus, _ = strconv.Atoi(movieDetail.Update_status) + + //if mediaTemp.UpdateStatus == 1 { + // mediaTemp.UpdatedDesc = fmt.Sprintf("已完结%d话", mediaTemp.TotalEpisode) + //} else { + // mediaTemp.UpdatedDesc = fmt.Sprintf("更新至%d话", mediaTemp.TotalEpisode) + //} + for _, tag := range movieDetail.Tags { + if v, ok := tagMaps[tag.Name]; ok { + mediaTemp.TagDetails = append(mediaTemp.TagDetails, mediamod.TagDetail{ + Id: v, + Name: tag.Name, + }) + mediaTemp.Tags = append(mediaTemp.Tags, v) + } + } + contents := make([]mediacontentmod.MediaContent, 0) + if len(movieDetail.Links) > 0 { + for i, chapter := range movieDetail.Links { + mediaContent := mediacontentmod.MediaContent{ + ID: primitive.NewObjectID(), + MediaID: mediaTemp.ID, + EpisodeNumber: i + 1, + ListenPermission: mediaTemp.Permission, + Price: mediaTemp.Price, + Name: chapter.Name, + VideoUrl: chapter.M3u8_url, + H265Url: strings.TrimSpace(chapter.Hevc_m3u8_url), + UrlSet: make([]string, 0), + Status: 1, + IsActive: true, + UpdatedAct: manager, + CreatedAt: now, + UpdateTime: now, + HashId: chapter.Id, + } + contents = append(contents, mediaContent) + } + } + + mediaTemp.CurrentEpisode = len(contents) + mediaTemp.TotalEpisode = mediaTemp.CurrentEpisode + + // 敏感词命中 → 强制下架(Status=0) + hit := vidser.CheckTextHits(terms, movieDetail.Id, mediaTemp.Title, mediaTemp.Summary, "") + if hit != nil { + mediaTemp.Status = 0 + log.Warn("syncOneCartoon sensitive hit, forced offline", + log.Any("sourceId", hit.SourceID), + log.Any("title", hit.Title), + log.Any("detail", hit.Detail)) + } + + _, err = mediamod.Insert(nil, mediaTemp) + if err != nil { + log.Warn("syncOneCartoon InsertMedia failed", log.Any("media", mediaTemp.ID), log.E(err)) + return nil, err + } + if len(contents) > 0 { + _ = mediacontentmod.InsetBatch(nil, contents) + } + return hit, nil +} + +// syncExistingCartoonH265 允许重复导入老司机动漫时回填历史章节的 H.265 地址。 +// 只接受上游非空地址,不改动现有 H.264 地址,也不会用空值覆盖已入库资源。 +func syncExistingCartoonH265(mediaID primitive.ObjectID, links []laosiji.LinkInfo, manager string) error { + updatedIDs := make([]primitive.ObjectID, 0) + for _, chapter := range links { + h265URL := strings.TrimSpace(chapter.Hevc_m3u8_url) + if h265URL == "" || strings.TrimSpace(chapter.Id) == "" { + continue + } + content, err := mediacontentmod.GetInfoByHashId(mediaID, chapter.Id) + if err == mediacontentmod.MediaContentNotFound { + continue + } + if err != nil { + return err + } + if strings.TrimSpace(content.H265Url) == h265URL { + continue + } + if _, err = mediacontentmod.UpdateByID(nil, content.ID, map[string]interface{}{ + "h265Url": h265URL, + "updatedAct": manager, + "updateTime": time.Now(), + }); err != nil { + return err + } + updatedIDs = append(updatedIDs, content.ID) + } + return nil +} diff --git a/web/service/laosijiser/novel.go b/web/service/laosijiser/novel.go new file mode 100644 index 0000000..5ec4cd2 --- /dev/null +++ b/web/service/laosijiser/novel.go @@ -0,0 +1,314 @@ +package laosijiser + +import ( + "91porn-server/common" + "91porn-server/common/crypt" + "91porn-server/common/httputil" + "91porn-server/common/laosiji" + "91porn-server/common/log" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + "91porn-server/web/webg" + "context" + "errors" + "fmt" + "io/ioutil" + "strconv" + "strings" + "sync" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func NovelSearch(ctx context.Context, param laosiji.NovelSearchListReq) (resp laosiji.NovelSearchListResp, err error) { + novelResp, err := laosiji.NovelSearch(ctx, param) + if err != nil { + log.Error("MovieSearch", log.E(err)) + return + } + + resp = laosiji.NovelSearchListResp{ + Data: make([]laosiji.NovelSearchInfo, 0), + Total: novelResp.Total, + Current_page: novelResp.Current_page, + Page_size: novelResp.Page_size, + Last_page: novelResp.Last_page, + } + //是否导入 + taskIds := make([]string, 0) + for _, v := range novelResp.Data { + taskIds = append(taskIds, v.Id) + } + novelList, _ := mediamod.GetListByLsjIds(taskIds) + + // 处理域名 + for _, novelInfo := range novelResp.Data { + //novelInfo.Img = common.JoinUrlPath(laosiji.IMAGEYUAN, novelInfo.Img) + novelInfo.Img = common.JoinUrlPath("laosiji", novelInfo.Img) + + for _, novelIn := range novelList { + if novelIn.LsjId == novelInfo.Id { + novelInfo.IsAdd = true + continue + } + } + + resp.Data = append(resp.Data, novelInfo) + } + return +} + +func NovelDetail(ctx context.Context, param laosiji.NovelDetailReq) (resp laosiji.NovelDetailResp, err error) { + resp, err = laosiji.NovelDetail(ctx, param) + if err != nil { + log.Error("MovieSearch", log.E(err)) + return + } + + // 处理域名 + //resp.Img = common.JoinUrlPath(laosiji.IMAGEYUAN, resp.Img) + resp.Img = common.JoinUrlPath("laosiji", resp.Img) + if resp.Chapter != nil && len(resp.Chapter) > 0 { + for i, cha := range resp.Chapter { + if len(cha.Img) > 0 { + //resp.Chapter[i].Img = common.JoinUrlPath(laosiji.IMAGEYUAN, resp.Chapter[i].Img) + resp.Chapter[i].Img = common.JoinUrlPath("laosiji", resp.Chapter[i].Img) + } + if len(cha.Content) > 0 { + if cha.Is_audio == "y" { + resp.Chapter[i].Content = common.JoinUrlPath("laosiji/m3m", resp.Chapter[i].Content) + } else { + resp.Chapter[i].Content = common.JoinUrlPath("laosiji", resp.Chapter[i].Content) + } + } + } + } + return +} + +func NovelAddList(ctx context.Context, param laosiji.NovelAddListReq, manager string) (err error) { + if param.Ids == nil || len(param.Ids) <= 0 { + return + } + novelDetails := make([]laosiji.NovelDetailResp, 0) + for _, id := range param.Ids { + var novelDetail laosiji.NovelDetailResp + req := laosiji.NovelDetailReq{ + Id: id, + } + novelDetail, err = laosiji.NovelDetail(ctx, req) + if err != nil { + log.Warn("NovelAddList laosiji.NovelDetail failed", log.Any("id", id), log.E(err)) + continue + } + // 处理域名 + //novelDetail.Img = common.JoinUrlPath(laosiji.IMAGEYUAN, novelDetail.Img) + novelDetail.Img = common.JoinUrlPath("laosiji/", novelDetail.Img) + if novelDetail.Chapter != nil && len(novelDetail.Chapter) > 0 { + for i, cha := range novelDetail.Chapter { + if len(cha.Img) > 0 { + novelDetail.Chapter[i].Img = common.JoinUrlPath("laosiji/", novelDetail.Chapter[i].Img) + } + if len(cha.Content) > 0 { + if cha.Is_audio == "y" { + return errors.New("暂不支持有声小说!!") + //novelDetail.Chapter[i].Content = common.JoinUrlPath("laosiji/m3m", novelDetail.Chapter[i].Content) + } else { + novelDetail.Chapter[i].Content = common.JoinUrlPath("laosiji", novelDetail.Chapter[i].Content) + } + } + } + } + novelDetails = append(novelDetails, novelDetail) + } + if len(novelDetails) <= 0 { + log.Warn("NovelAddList laosiji.NovelDetailResp len is 0") + err = errors.New("laosiji.NovelDetailResp len is 0") + return + } + // 批量处理标签 + tagMaps := make(map[string]primitive.ObjectID) + tagList := make([]string, 0) // 减少重复循环 + tagList2 := make([]string, 0) // 减少重复循环 + tagTmpMap := map[int]map[string]any{ // 区分类别和去重 + 3: make(map[string]any), // 文本 + 4: make(map[string]any), // 有声 + } + for _, comicsDetail := range novelDetails { + for _, tag := range comicsDetail.Tags { + if len(comicsDetail.Chapter) > 0 && comicsDetail.Chapter[0].Is_audio == "y" { + if _, ok := tagTmpMap[4][tag.Name]; !ok { + tagTmpMap[4][tag.Name] = nil + tagList = append(tagList, tag.Name) + } + } else { + if _, ok := tagTmpMap[3][tag.Name]; !ok { + tagTmpMap[3][tag.Name] = nil + tagList2 = append(tagList, tag.Name) + } + } + } + } + + if len(tagTmpMap[3]) > 0 { + tagMaps, err = checkInsertMediaTag(tagList, 3) + if err != nil { + log.Error("NovelAddList checkInsertMediaTag failed", log.E(err)) + return err + } + } + if len(tagTmpMap[4]) > 0 { + tagMaps2, err := checkInsertMediaTag(tagList2, 4) + if err != nil { + log.Error("NovelAddList checkInsertMediaTag failed", log.E(err)) + return err + } + for k, id := range tagMaps2 { + tagMaps[k] = id + } + } + + failedIds := make([]string, 0) + wg := sync.WaitGroup{} + for index, _ := range novelDetails { + wg.Add(1) + common.GoParam(index, func(i int) { + defer wg.Done() + comicsDetail := novelDetails[i] + err = syncOneNovel(comicsDetail, tagMaps, manager) + if err != nil { + log.Warn("NovelAddList syncOneNovel failed", log.Any("id", comicsDetail.Id), log.E(err)) + failedIds = append(failedIds, comicsDetail.Id) + } + }) + } + wg.Wait() + if len(failedIds) > 0 { + err = errors.New(fmt.Sprintf("需要同步%d部小说,失败%d,失败Id[%s]", len(param.Ids), len(failedIds), strings.Join(failedIds, ","))) + } + return + +} + +func syncOneNovel(novelDetail laosiji.NovelDetailResp, tagMaps map[string]primitive.ObjectID, manager string) error { + now := time.Now() + mediaTemp, err := mediamod.GetListByLsjId(novelDetail.Id) + if err != nil { + log.Warn("syncOneNovel GetListByLsjId failed", log.Any("id", novelDetail.Id), log.Any("err", err)) + return err + } + if !mediaTemp.ID.IsZero() { + return nil + } + mediaTemp = mediamod.Media{ + ID: primitive.NewObjectID(), + LsjId: novelDetail.Id, + Title: novelDetail.Name, + HorizontalCover: novelDetail.Img, + VerticalCover: novelDetail.Img, + Tags: make([]primitive.ObjectID, 0), + TagDetails: make([]mediamod.TagDetail, 0), + Summary: novelDetail.Description, + FreeEpisode: 0, + CurrentEpisode: 0, + MediaType: mediamod.MediaTypeText, + Kind: 7, + Anchor: novelDetail.Author, + PermissionIconHide: false, + Permission: 2, + Price: 0, + ContentsPrice: 0, + Status: 1, + CreatedAt: now, + UpdateTime: now, + UpdatedAct: manager, + } + isAudo := false + mediaTemp.TotalEpisode, _ = strconv.Atoi(novelDetail.Chapter_count) + mediaTemp.UpdateStatus, _ = strconv.Atoi(novelDetail.Update_status) + mediaTemp.Price, _ = strconv.ParseInt(novelDetail.Money, 10, 64) + if mediaTemp.Price > 0 { + mediaTemp.Permission = 1 + } + //if mediaTemp.UpdateStatus == 1 { + // mediaTemp.UpdatedDesc = fmt.Sprintf("已完结%d话", mediaTemp.TotalEpisode) + //} else { + // mediaTemp.UpdatedDesc = fmt.Sprintf("更新至%d话", mediaTemp.TotalEpisode) + //} + for _, tag := range novelDetail.Tags { + if v, ok := tagMaps[tag.Name]; ok { + mediaTemp.TagDetails = append(mediaTemp.TagDetails, mediamod.TagDetail{ + Id: v, + Name: tag.Name, + }) + mediaTemp.Tags = append(mediaTemp.Tags, v) + } + } + contents := make([]mediacontentmod.MediaContent, 0) + if len(novelDetail.Chapter) > 0 { + for i, chapter := range novelDetail.Chapter { + mediaContent := mediacontentmod.MediaContent{ + ID: primitive.NewObjectID(), + MediaID: mediaTemp.ID, + EpisodeNumber: i + 1, + ListenPermission: mediaTemp.Permission, + Price: mediaTemp.Price, + Name: fmt.Sprintf("第%d章", i+1), + UrlSet: make([]string, 0), + Status: 1, + IsActive: true, + Author: mediaTemp.Anchor, + IsDelete: false, + UpdatedAct: manager, + CreatedAt: now, + UpdateTime: now, + HashId: chapter.Id, + } + if chapter.Is_audio == "y" { + mediaContent.AudioUrl = chapter.Content + isAudo = true + } else { + content, err := textNovelContent(chapter.Content) + if err != nil { + log.Warn("syncOneNovel textNovelContent failed", log.Any("err", err)) + return err + } + mediaContent.Text = content + } + contents = append(contents, mediaContent) + } + } + + if isAudo { + mediaTemp.MediaSubType = 1 + } + _, err = mediamod.Insert(nil, mediaTemp) + if err != nil { + log.Warn("syncOneNovel InsertMedia failed", log.Any("media", mediaTemp.ID), log.E(err)) + return err + } + + if len(contents) > 0 { + _ = mediacontentmod.InsetBatch(nil, contents) + } + return nil +} + +func textNovelContent(url string) (string, error) { + decKey := "525202f9149e061d" + path := fmt.Sprintf("%s/%s", webg.Conf.URL.LaoSiJiTSCdn, url) + resp, err := httputil.DefaultClientGet(path, make(map[string]string)) + if err != nil { + log.Warn("textNovelContent http client get failed", log.Any("url", path), log.Any("err", err)) + return "", err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Warn("textNovelContent read response body failed", log.Any("err", err)) + return "", err + } + b := crypt.AESECBDecrypt(body, []byte(decKey)) + return string(b), nil +} diff --git a/web/service/laosijiser/novel_test.go b/web/service/laosijiser/novel_test.go new file mode 100644 index 0000000..9e7a0b4 --- /dev/null +++ b/web/service/laosijiser/novel_test.go @@ -0,0 +1,34 @@ +package laosijiser + +import ( + "91porn-server/common/crypt" + "91porn-server/common/httputil" + "91porn-server/common/log" + "fmt" + "io/ioutil" + "testing" +) + +func TestNovelDetail(t *testing.T) { + url := "laosiji/media/novel/cbd/cbd5b05ea5677797958895694211582c.bnc" + key := "525202f9149e061d" + domain := "https://tssdaa.hlvwcn.cn" + //req, err := http.NewRequest("GET", url, nil) + //if err != nil { + // t.Fatal(err) + // return + //} + resp, err := httputil.ClientGet(30, fmt.Sprintf("%s/%s", domain, url), make(map[string]string)) + if err != nil { + log.Warn("http client get failed", log.Any("err", err)) + return + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Warn("read response body failed", log.Any("err", err)) + return + } + b := crypt.AESECBDecrypt(body, []byte(key)) + log.Info(string(b)) +} diff --git a/web/service/laosijiser/post.go b/web/service/laosijiser/post.go new file mode 100644 index 0000000..d26bb8f --- /dev/null +++ b/web/service/laosijiser/post.go @@ -0,0 +1,229 @@ +package laosijiser + +import ( + "91porn-server/common" + "91porn-server/common/laosiji" + "91porn-server/common/log" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "91porn-server/web/service/vidser" + "context" + "errors" + "fmt" + "math/rand" + "strconv" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func PostSearch(ctx context.Context, param laosiji.PostSearchListReq) (resp laosiji.PostSearchListResp, err error) { + postResp, err := laosiji.PostSearch(ctx, param) + if err != nil { + log.Error("MovieSearch", log.E(err)) + return + } + resp = laosiji.PostSearchListResp{ + Data: make([]laosiji.PostSearchInfo, 0), + Total: postResp.Total, + Current_page: postResp.Current_page, + Page_size: postResp.Page_size, + Last_page: postResp.Last_page, + } + + //是否导入 + taskIds := make([]string, 0) + for _, v := range postResp.Data { + taskIds = append(taskIds, v.Id) + } + postList, _ := vidmod.GetList(bson.M{"sourceID": bson.M{"$in": taskIds}}) + + // 处理域名 + for _, postInfo := range postResp.Data { + //postInfo.Img = common.JoinUrlPath(laosiji.IMAGEYUAN, postInfo.Img) + postInfo.Img = common.JoinUrlPath("laosiji", postInfo.Img) + + for _, postIn := range postList { + if postIn.SourceID == postInfo.Id { + postInfo.IsAdd = true + continue + } + } + + resp.Data = append(resp.Data, postInfo) + } + return resp, nil +} +func PostDetail(ctx context.Context, param laosiji.PostDetailReq) (resp laosiji.PostDetailResp, err error) { + resp, err = laosiji.PostDetail(ctx, param) + if err != nil { + log.Error("MovieSearch", log.E(err)) + return + } + + // 处理域名 + //resp.Img = common.JoinUrlPath(laosiji.IMAGEYUAN, resp.Img) + resp.Img = common.JoinUrlPath("laosiji", resp.Img) + if resp.Files != nil && len(resp.Files) > 0 { + for i, file := range resp.Files { + if len(file.Image) > 0 { + //resp.Files[i].Image = common.JoinUrlPath(laosiji.IMAGEYUAN, file.Image) + resp.Files[i].Image = common.JoinUrlPath("laosiji", file.Image) + } + if len(file.Video_link) > 0 { + resp.Files[i].Video_link = common.JoinUrlPath(laosiji.APIUrl+"/m3m", file.Video_link) + } + } + } + return +} + +func PostAddList(ctx context.Context, param laosiji.PostAddListReq, manager string) (hits []vidser.SensitiveHit, err error) { + if param.Ids == nil || len(param.Ids) <= 0 { + return + } + // 批量查询所有详情 + postDetails := make([]laosiji.PostDetailResp, 0) + for _, id := range param.Ids { + var postDetail laosiji.PostDetailResp + req := laosiji.PostDetailReq{ + Id: id, + } + postDetail, err = laosiji.PostDetail(ctx, req) + if err != nil { + log.Warn("PostAddList laosiji.PostDetail failed", log.Any("id", id), log.E(err)) + continue + } + //postDetail.Img = common.JoinUrlPath(laosiji.IMAGEYUAN, postDetail.Img) + postDetail.Img = common.JoinUrlPath("laosiji", postDetail.Img) + if postDetail.Files != nil && len(postDetail.Files) > 0 { + for i, file := range postDetail.Files { + if len(file.Image) > 0 { + postDetail.Files[i].Image = common.JoinUrlPath("laosiji", file.Image) + } + if len(file.Video_link) > 0 { + postDetail.Files[i].Video_link = common.JoinUrlPath("laosiji/m3m/", file.Video_link) + } + } + } + postDetails = append(postDetails, postDetail) + } + if len(postDetails) <= 0 { + log.Warn("PostAddList laosiji.PostDetailResp len is 0") + err = errors.New("laosiji.PostDetailResp len is 0") + return + } + + users, err := usermod.FindMany(bson.M{"uid": bson.M{"$gt": 300000}}, &options.FindOptions{}, 0, 300) + if err != nil { + log.Warn("PostAddList FindManyUser failed", log.E(err)) + return + } + terms := vidser.LoadEnabledSensitiveTerms() + hits = make([]vidser.SensitiveHit, 0) + failedIds := make([]string, 0) + for _, postDetail := range postDetails { + hit, oneErr := addOnePost(postDetail, nil, users, manager, terms) + if oneErr != nil { + log.Warn("PostAddList addOnePost failed", log.Any("id", postDetail.Id), log.E(oneErr)) + failedIds = append(failedIds, postDetail.Id) + continue + } + if hit != nil { + hits = append(hits, *hit) + } + } + if len(failedIds) > 0 { + err = errors.New(fmt.Sprintf("需要同步%d个帖子,失败%d,失败Id[%s]", len(param.Ids), len(failedIds), strings.Join(failedIds, ","))) + } + return +} + +func addOnePost(postDetail laosiji.PostDetailResp, tagMaps map[string]primitive.ObjectID, upUsers []*usermod.User, manager string, terms []string) (*vidser.SensitiveHit, error) { + // + // 有的帖子是图文节点 前端格式不支持 这里屏蔽 + // + if len(postDetail.Files) == 0 { + return nil, nil + } + + now := time.Now() + postList, err := vidmod.GetList(bson.M{"sourceID": bson.M{"$in": []string{postDetail.Id}}}) + if err != nil { + log.Warn("addOnePost GetList failed", log.Any("id", postDetail.Id), log.E(err)) + return nil, err + } + if postList != nil && len(postList) > 0 { + return nil, nil + } + postInfo := vidmod.VideoModel{ + ID: primitive.NewObjectID(), + NewsType: vidmod.COVER, + Title: postDetail.Title, + Content: postDetail.Content, + Tags: make([]primitive.ObjectID, 0), + MimeType: "video/mp4", + Cover: postDetail.Img, + CoverThumb: postDetail.Img, + SeriesCover: make([]string, 0), + Via: "laosiji", + Status: vidmod.CheckPass, + Location: primitive.ObjectID{}, + NewUpdatedAt: "laosiji", + ReviewAt: now, + CreatedAt: now, + UpdatedAt: now, + ReviewAccount: manager, + SourceID: postDetail.Id, + VerticalCover: postDetail.Img, + } + postInfo.SeriesNum, _ = strconv.Atoi(postDetail.Img_count) + postInfo.Coins, _ = strconv.ParseInt(postDetail.Money, 10, 64) + uIndex := rand.Intn(len(upUsers)) + u := upUsers[uIndex] + postInfo.PublisherID = u.UID + //if postInfo.Coins > 0 { + // postInfo.Permission = 1 + //} else { + // postInfo.Permission = 2 + // postInfo.FreeArea = true + // postInfo.FreeAreaDate = now + //} + //节点 处理 + if postDetail.Files != nil && len(postDetail.Files) > 0 { + for _, file := range postDetail.Files { + if file.Type == "image" { + if len(file.Image) > 0 { + postInfo.SeriesCover = append(postInfo.SeriesCover, file.Image) + } + } + if file.Type == "video" { + postInfo.SourceURL = file.Video_link + postInfo.PlayTime = 1 + postInfo.Filename = file.Tips + postInfo.SourceID = postDetail.Id + } + } + } + if postDetail.Rich_content != nil && len(postDetail.Rich_content) > 0 { + for _, file := range postDetail.Rich_content { + postInfo.RichText += fmt.Sprintf("%s\n", file) + } + } + // 敏感词命中 → 强制为待审核(WaitingCheck=0),写库后由后台逐条告警 + hit := vidser.CheckTextHits(terms, postDetail.Id, postInfo.Title, postInfo.Content, postInfo.RichText) + if hit != nil { + postInfo.Status = vidmod.WaitingCheck + log.Warn("addOnePost sensitive hit, forced offline", + log.Any("sourceId", hit.SourceID), + log.Any("title", hit.Title), + log.Any("detail", hit.Detail)) + } + if _, err = vidmod.InsertBase(postInfo); err != nil { + return nil, err + } + return hit, nil +} diff --git a/web/service/laosijiser/video.go b/web/service/laosijiser/video.go new file mode 100644 index 0000000..fdd3106 --- /dev/null +++ b/web/service/laosijiser/video.go @@ -0,0 +1,338 @@ +package laosijiser + +import ( + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/laosiji" + "91porn-server/common/log" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "91porn-server/web/service/vidser" + "91porn-server/web/webg" + "context" + "errors" + "fmt" + "math/rand" + "strconv" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func MovieSearch(ctx context.Context, param laosiji.MovieSearchReq) (resp laosiji.MovieSearchResp, err error) { + var yuanresp laosiji.MovieSearchResp + yuanresp, err = laosiji.MovieSearch(ctx, param) + if err != nil { + log.Error("MovieSearch", log.E(err)) + return + } + + //是否导入 + taskIds := make([]string, 0) + for _, v := range yuanresp.Data { + taskIds = append(taskIds, v.Id) + } + resp = laosiji.MovieSearchResp{ + Data: make([]laosiji.MovieInfo, 0), + Total: yuanresp.Total, + Current_page: yuanresp.Current_page, + Page_size: yuanresp.Page_size, + Last_page: yuanresp.Last_page, + } + if param.Position == "guochan" || param.Position == "av" || param.Position == "douyin" || param.Position == "dark" { + videoList, _ := vidmod.GetList(bson.M{"sourceID": bson.M{"$in": taskIds}}) + // 处理域名 + for _, movieInfo := range yuanresp.Data { + //movieInfo.Img_x = common.JoinUrlPath(laosiji.IMAGEYUAN, movieInfo.Img_x) + //movieInfo.Img_y = common.JoinUrlPath(laosiji.IMAGEYUAN, movieInfo.Img_y) + movieInfo.Img_x = common.JoinUrlPath("laosiji", movieInfo.Img_x) + movieInfo.Img_y = common.JoinUrlPath("laosiji", movieInfo.Img_y) + + for _, videoInfo := range videoList { + if videoInfo.SourceID == movieInfo.Id { + movieInfo.IsAdd = true + continue + } + } + + resp.Data = append(resp.Data, movieInfo) + } + } else if param.Position == "cartoon" { + mediaList, _ := mediamod.GetListByLsjIds(taskIds) + for _, movieInfo := range yuanresp.Data { + //movieInfo.Img_x = common.JoinUrlPath(laosiji.IMAGEYUAN, movieInfo.Img_x) + //movieInfo.Img_y = common.JoinUrlPath(laosiji.IMAGEYUAN, movieInfo.Img_y) + movieInfo.Img_x = common.JoinUrlPath("laosiji", movieInfo.Img_x) + movieInfo.Img_y = common.JoinUrlPath("laosiji", movieInfo.Img_y) + for _, videoInfo := range mediaList { + if videoInfo.LsjId == movieInfo.Id { + movieInfo.IsAdd = true + continue + } + } + resp.Data = append(resp.Data, movieInfo) + } + + } else { + err = errors.New("该资源暂不支持") + } + return resp, nil +} + +func MovieDetail(ctx context.Context, param laosiji.MovieDetailReq) (resp laosiji.MovieDetailResp, err error) { + resp, err = laosiji.MovieDetail(ctx, param) + if err != nil { + log.Error("MovieSearch", log.E(err)) + return + } + + // 处理域名 + //resp.Img_x = common.JoinUrlPath(laosiji.IMAGEYUAN, resp.Img_x) + //resp.Img_y = common.JoinUrlPath(laosiji.IMAGEYUAN, resp.Img_y) + resp.Img_x = common.JoinUrlPath("laosiji", resp.Img_x) + resp.Img_y = common.JoinUrlPath("laosiji", resp.Img_y) + for key, value := range resp.Preview_images { + //resp.Preview_images[key] = common.JoinUrlPath(laosiji.IMAGEYUAN, value) + resp.Preview_images[key] = common.JoinUrlPath("laosiji", value) + } + for key, value := range resp.Links { + resp.Links[key].M3u8_url = laosiji.MovieM3u8OriginURL(value.M3u8_url) + resp.Links[key].Preview_m3u8_url = laosiji.MovieM3u8OriginURL(value.Preview_m3u8_url) + resp.Links[key].Hevc_m3u8_url = laosiji.MovieM3u8OriginURL(value.Hevc_m3u8_url) + } + return resp, nil +} + +func MovieAddList(ctx context.Context, param laosiji.MovieAddListReq, manager string) ([]vidser.SensitiveHit, error) { + if param.Ids == nil || len(param.Ids) <= 0 { + return nil, nil + } + movieDetails := make([]laosiji.MovieDetailResp, 0) + for _, id := range param.Ids { + var movieDetail laosiji.MovieDetailResp + req := laosiji.MovieDetailReq{ + Id: id, + } + movieDetail, err := laosiji.MovieDetail(ctx, req) + if err != nil { + log.Warn("MovieAddList laosiji.MovieDetail failed", log.Any("id", id), log.E(err)) + continue + } + for i, linkInfo := range movieDetail.Links { + linkInfo.M3u8_url = laosiji.MovieM3u8SourcePath(linkInfo.M3u8_url) + linkInfo.Preview_m3u8_url = laosiji.MovieM3u8SourcePath(linkInfo.Preview_m3u8_url) + linkInfo.Hevc_m3u8_url = laosiji.MovieM3u8SourcePath(linkInfo.Hevc_m3u8_url) + movieDetail.Links[i] = linkInfo + } + if len(movieDetail.Img_y) > 0 { + movieDetail.Img_y = common.JoinUrlPath("laosiji/", movieDetail.Img_y) + } + if len(movieDetail.Img_x) > 0 { + movieDetail.Img_x = common.JoinUrlPath("laosiji/", movieDetail.Img_x) + } + movieDetails = append(movieDetails, movieDetail) + } + if len(movieDetails) <= 0 { + log.Warn("MovieAddList laosiji.MovieDetailResp len is 0") + return nil, errors.New("laosiji.MovieDetailResp len is 0") + } + // 批量处理标签 + tags := make(map[string]struct{}) + for _, comicsDetail := range movieDetails { + if comicsDetail.Tags != nil && len(comicsDetail.Tags) > 0 { + for _, tag := range comicsDetail.Tags { + tags[tag.Name] = struct{}{} + } + } + } + terms := vidser.LoadEnabledSensitiveTerms() + hits := make([]vidser.SensitiveHit, 0) + if param.Position == "cartoon" { + return syncCartoonList(movieDetails, tags, manager, terms) + } else if param.Position == "av" || param.Position == "guochan" || param.Position == "douyin" || param.Position == "dark" { + tagMaps := make(map[string]primitive.ObjectID) + var err error + if len(tags) > 0 { + tagList := make([]string, 0) + for k, _ := range tags { + tagList = append(tagList, k) + } + tagMaps, err = checkInsertVidTags(tagList) + if err != nil { + log.Error("MovieAddList checkInsertVidTags failed", log.E(err)) + return hits, err + } + } + users, err := usermod.FindMany(bson.M{"uid": bson.M{"$gt": 300000}}, &options.FindOptions{}, 0, 300) + if err != nil { + log.Warn("PostAddList FindManyUser failed", log.E(err)) + return hits, err + } + failedIds := make([]string, 0) + for _, comicsDetail := range movieDetails { + hit, oneErr := syncOneMovie(comicsDetail, tagMaps, users, manager, terms) + if oneErr != nil { + log.Warn("MovieAddList syncOneMovie failed", log.Any("id", comicsDetail.Id), log.E(oneErr)) + failedIds = append(failedIds, comicsDetail.Id) + continue + } + if hit != nil { + hits = append(hits, *hit) + } + } + if len(failedIds) > 0 { + return hits, errors.New(fmt.Sprintf("需要同步%d部资源,失败%d,失败Id[%s]", len(param.Ids), len(failedIds), strings.Join(failedIds, ","))) + } + return hits, nil + } + return hits, errors.New("该资源暂不支持") +} + +func syncOneMovie(movieDetail laosiji.MovieDetailResp, tags map[string]primitive.ObjectID, upUsers []*usermod.User, manager string, terms []string) (*vidser.SensitiveHit, error) { + now := time.Now() + postList, err := vidmod.GetList(bson.M{"sourceID": bson.M{"$in": []string{movieDetail.Id}}}) + if err != nil { + log.Warn("syncOneMovie GetList failed", log.Any("id", movieDetail.Id), log.E(err)) + return nil, err + } + if postList != nil && len(postList) > 0 { + h265URL := movieH265SourceURL(movieDetail.Links) + if h265URL == "" { + return nil, nil + } + video := postList[0] + if err = vidmod.MarkH265Success(video.ID, h265URL); err != nil { + return nil, err + } + if webg.Redis != nil { + cacheKey := fmt.Sprintf(redisconst.VideoInfoKey(), video.ID.Hex()) + if _, err = webg.Redis.Del(cacheKey); err != nil { + log.Warn("syncOneMovie clear video cache failed", + log.Any("id", video.ID), + log.Any("cacheKey", cacheKey), + log.E(err)) + return nil, err + } + } + return nil, nil + } + vidInfo := vidmod.VideoModel{ + ID: primitive.NewObjectID(), + Title: movieDetail.Name, + Content: movieDetail.Description, + Tags: make([]primitive.ObjectID, 0), + MimeType: "video/mp4", + SeriesCover: make([]string, 0), + Via: "laosiji", + Status: vidmod.CheckPass, + Location: primitive.ObjectID{}, + Actor: movieDetail.Actor, + NewUpdatedAt: "laosiji", + ReviewAt: now, + CreatedAt: now, + UpdatedAt: now, + ReviewAccount: manager, + VerticalCover: movieDetail.Img_y, + } + + if movieDetail.Img_type == "short" && len(movieDetail.Img_y) > 0 { + vidInfo.NewsType = vidmod.SHORT + vidInfo.Cover = movieDetail.Img_y + vidInfo.CoverThumb = movieDetail.Img_y + vidInfo.Direction = "vertical" + } else { + vidInfo.NewsType = vidmod.SP + vidInfo.Cover = movieDetail.Img_x + vidInfo.CoverThumb = movieDetail.Img_x + vidInfo.Direction = "horizontal" + } + playTime, _ := strconv.Atoi(movieDetail.Duration) + vidInfo.PlayTime = uint(playTime) + uIndex := rand.Intn(len(upUsers)) + u := upUsers[uIndex] + vidInfo.PublisherID = u.UID + if movieDetail.Links != nil && len(movieDetail.Links) > 0 { + vidInfo.SourceURL = movieDetail.Links[0].M3u8_url + vidInfo.Filename = movieDetail.Links[0].Name + vidInfo.PreviewURL = movieDetail.Links[0].Preview_m3u8_url + vidInfo.H265Url = movieH265SourceURL(movieDetail.Links) + if vidInfo.H265Url != "" { + vidInfo.H265Status = vidmod.H265StatusSuccess + } + } + vidInfo.SourceID = movieDetail.Id + + if movieDetail.Tags != nil && len(movieDetail.Tags) > 0 { + for _, tag := range movieDetail.Tags { + if k, ok := tags[tag.Name]; ok { + vidInfo.Tags = append(vidInfo.Tags, k) + } + } + } + + // 敏感词命中 → 强制为待审核(WaitingCheck=0) + hit := vidser.CheckTextHits(terms, movieDetail.Id, vidInfo.Title, vidInfo.Content, "") + if hit != nil { + vidInfo.Status = vidmod.WaitingCheck + log.Warn("syncOneMovie sensitive hit, forced offline", + log.Any("sourceId", hit.SourceID), + log.Any("title", hit.Title), + log.Any("detail", hit.Detail)) + } + if vidInfo.H265Url == "" && vidInfo.SourceURL != "" && vidInfo.Status == vidmod.CheckPass && vidInfo.NewsType == vidmod.SP { + vidInfo.H265Status = vidmod.H265StatusQueued + vidInfo.H265QueuedAt = now + } + if _, err = vidmod.InsertBase(vidInfo); err != nil { + return nil, err + } + return hit, nil +} + +// movieH265SourceURL 返回首个播放链接对应的 H.265 地址。 +// syncOneMovie 的 H.264 也固定使用 links[0],不能拿后续分集的 H.265 与首集混用。 +func movieH265SourceURL(links []laosiji.LinkInfo) string { + if len(links) == 0 { + return "" + } + return laosiji.MovieM3u8SourcePath(links[0].Hevc_m3u8_url) +} + +func checkInsertVidTags(names []string) (data map[string]vidmod.ObjectID, err error) { + data = make(map[string]vidmod.ObjectID) + tags, err := tagmod.FindOneTagByNames(names) + if err != nil { + log.Warn("checkInsertVidTags tagmod.FindOneTagByNames", log.E(err), log.Any("name", names)) + return + } + for _, tag := range tags { + data[tag.TagName] = tag.ID + } + insertTags := make([]tagmod.Tag, 0) + for _, tag := range tags { + if _, ok := data[tag.TagName]; !ok { + insertTag := tagmod.Tag{ + ID: primitive.NewObjectID(), + TagName: tag.TagName, + IsActive: true, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + insertTags = append(insertTags, insertTag) + data[tag.TagName] = insertTag.ID + } + } + if len(insertTags) > 0 { + err = tagmod.InsertMany(insertTags) + if err != nil { + log.Warn("checkInsertVidTags InsertMany failed", log.Any("tags", insertTags), log.E(err)) + return + } + } + return +} diff --git a/web/service/laosijiser/video_test.go b/web/service/laosijiser/video_test.go new file mode 100644 index 0000000..231c053 --- /dev/null +++ b/web/service/laosijiser/video_test.go @@ -0,0 +1,53 @@ +package laosijiser + +import ( + "testing" + + "91porn-server/common/laosiji" +) + +func TestMovieH265SourceURL(t *testing.T) { + tests := []struct { + name string + links []laosiji.LinkInfo + want string + }{ + { + name: "empty", + links: []laosiji.LinkInfo{ + {Hevc_m3u8_url: " "}, + }, + }, + { + name: "does not mix a later episode with first H264 link", + links: []laosiji.LinkInfo{ + {Hevc_m3u8_url: ""}, + {Hevc_m3u8_url: "https://cdn.example.com/m3m/movie/hevc.m3u8?token=ignored"}, + {Hevc_m3u8_url: "other.m3u8"}, + }, + }, + { + name: "first link normalized", + links: []laosiji.LinkInfo{ + {Hevc_m3u8_url: "https://cdn.example.com/m3m/movie/hevc.m3u8?token=ignored"}, + {Hevc_m3u8_url: "other.m3u8"}, + }, + want: "laosiji/m3m/movie/hevc.m3u8", + }, + { + name: "already normalized", + links: []laosiji.LinkInfo{ + {Hevc_m3u8_url: "laosiji/rk130/hevc/movie/index.m3u8"}, + }, + want: "laosiji/rk130/hevc/movie/index.m3u8", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := movieH265SourceURL(test.links); got != test.want { + t.Fatalf("movieH265SourceURL() = %q, want %q", got, test.want) + } + }) + } +} diff --git a/web/service/locationser/city.go b/web/service/locationser/city.go new file mode 100644 index 0000000..c4b9bd9 --- /dev/null +++ b/web/service/locationser/city.go @@ -0,0 +1,43 @@ +package locationser + +import ( + "91porn-server/models/v/locmod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// CityPages CityPages +func CityPages(city string, page, size uint64) (locmod.CityPageResp, error) { + var resp locmod.CityPageResp + lModel, total, err := locmod.GetLocationList(city, page, size) + if err != nil { + return resp, err + } + lmodelLen := len(lModel) + tids := make([]primitive.ObjectID, lmodelLen) + for i, l := range lModel { + tids[i] = l.ID + } + mPlayCnt, err := vidmod.GetCityPlayCount2Map(tids) + if err != nil { + return resp, err + } + mVideosCnt, _ := vidmod.GetCityPublishedVideoCount2Map(tids) + resp.List = make([]locmod.LocInfo, lmodelLen) + for i, l := range lModel { + resp.List[i] = locmod.LocInfo{ + ID: l.ID.Hex(), + City: l.City, + Cover: l.Cover, + Visit: l.Visit, + FakeVisit: l.FakeVisit, + SortKey: l.SortKey, + CreatedAt: l.CreatedAt, + VideoCount: mVideosCnt[l.ID], + PlayCount: mPlayCnt[l.ID], + } + } + resp.Total = int(total) + return resp, nil +} diff --git a/web/service/logser/log.go b/web/service/logser/log.go new file mode 100644 index 0000000..238353b --- /dev/null +++ b/web/service/logser/log.go @@ -0,0 +1,22 @@ +package logser + +import ( + "91porn-server/common/stderr" + "91porn-server/models/v/txnmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// MAP 别名 +type MAP map[string]interface{} + +type ObjectID = primitive.ObjectID + +// GetRechargeGoldLog 获取充值金币日志 +func GetRechargeGoldLog(cond MAP, page, size uint64) (code stderr.Code, data interface{}) { + infos, _, total, err := txnmod.GetCoinLogs(page, size, cond) + if err != nil { + return stderr.ErrDbQueryError, nil + } + return stderr.Success, txnmod.CoinLogResp{Logs: infos, Total: total} +} diff --git a/web/service/mediacontentser/drama_web_test.go b/web/service/mediacontentser/drama_web_test.go new file mode 100644 index 0000000..7172067 --- /dev/null +++ b/web/service/mediacontentser/drama_web_test.go @@ -0,0 +1,121 @@ +package mediacontentser + +import ( + "testing" + + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/vidmod" +) + +func TestValidateDramaEpisodeState(t *testing.T) { + tests := []struct { + name string + episodeNumber int + listenPermission int + price int64 + videoURL string + active bool + status int + wantErr bool + }{ + {name: "free", episodeNumber: 1, listenPermission: 2, videoURL: "1.m3u8", active: true, status: 1}, + {name: "coin", episodeNumber: 2, listenPermission: 1, price: 30, videoURL: "2.m3u8", active: true, status: 1}, + {name: "free with price", episodeNumber: 1, listenPermission: 2, price: 30, wantErr: true}, + {name: "coin without price", episodeNumber: 2, listenPermission: 1, wantErr: true}, + {name: "active while transcoding", episodeNumber: 1, listenPermission: 2, videoURL: "1.m3u8", active: true, status: 0, wantErr: true}, + {name: "active without resource", episodeNumber: 1, listenPermission: 2, active: true, status: 1, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateDramaEpisodeState(tt.episodeNumber, tt.listenPermission, tt.price, "第1集", tt.videoURL, "", tt.active, tt.status) + if gotErr := err != nil; gotErr != tt.wantErr { + t.Fatalf("validateDramaEpisodeState() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestValidateDramaEpisodeStateAllowsNonContinuousFreeEpisodes(t *testing.T) { + episodes := []struct { + episodeNumber int + listenPermission int + price int64 + }{ + {episodeNumber: 2, listenPermission: 1, price: 30}, + {episodeNumber: 3, listenPermission: 2, price: 0}, + } + for _, episode := range episodes { + if err := validateDramaEpisodeState( + episode.episodeNumber, + episode.listenPermission, + episode.price, + "剧集", + "episode.m3u8", + "", + true, + 1, + ); err != nil { + t.Fatalf("episode %d validation failed: %v", episode.episodeNumber, err) + } + } +} + +func TestFileStatusToEpisodeStatus(t *testing.T) { + tests := map[string]int{ + vidmod.Converting: 0, + vidmod.ConvertCompleted: 1, + vidmod.Completed: 1, + vidmod.ConvertError: 2, + vidmod.MergeError: 2, + } + for input, want := range tests { + if got := fileStatusToEpisodeStatus(input); got != want { + t.Fatalf("fileStatusToEpisodeStatus(%q) = %d, want %d", input, got, want) + } + } +} + +func TestWebBatchUpdateReqValidatedIDs(t *testing.T) { + id := "507f1f77bcf86cd799439011" + price := int64(30) + permission := 1 + freeEpisode := 1 + req := WebBatchUpdateReq{ + IDS: []string{id, id}, + Price: &price, + Permission: &permission, + FreeEpisode: &freeEpisode, + } + ids, err := req.ValidatedIDs() + if err != nil { + t.Fatalf("ValidatedIDs() error: %v", err) + } + if len(ids) != 1 { + t.Fatalf("ValidatedIDs() length = %d, want 1", len(ids)) + } + if _, err = (&WebBatchUpdateReq{IDS: []string{id}}).ValidatedIDs(); err == nil { + t.Fatal("request without update fields unexpectedly accepted") + } + req.IDS = []string{"bad-id"} + if _, err = req.ValidatedIDs(); err == nil { + t.Fatal("invalid ObjectID unexpectedly accepted") + } +} + +func TestWebBatchUpdateReqAppliesPricingWithoutIsActive(t *testing.T) { + price := int64(30) + permission := 1 + req := WebBatchUpdateReq{Price: &price, Permission: &permission} + content := mediacontentmod.MediaContent{IsActive: true, Price: 10, ListenPermission: 2} + + next := req.applyContentFields(content) + if !next.IsActive { + t.Fatal("isActive changed when omitted") + } + if next.Price != price { + t.Fatalf("price = %d, want %d", next.Price, price) + } + if next.ListenPermission != permission { + t.Fatalf("listenPermission = %d, want %d", next.ListenPermission, permission) + } +} diff --git a/web/service/mediacontentser/mediacontent.go b/web/service/mediacontentser/mediacontent.go new file mode 100644 index 0000000..0b5204b --- /dev/null +++ b/web/service/mediacontentser/mediacontent.go @@ -0,0 +1,723 @@ +package mediacontentser + +import ( + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/mediamod" + "91porn-server/models/v/vidmod" + "91porn-server/web/service/updownloadser" + "errors" + "fmt" + "path/filepath" + "strings" + "time" + + "go.mongodb.org/mongo-driver/mongo/options" + + "91porn-server/models/v/mediacontentmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + ID *string `form:"id" bson:"id,omitempty"` // ID + Name *string `form:"name" bson:"name,omitempty"` // 名称 + Type *int `form:"type" bson:"type,omitempty"` // 类型 1:动漫,2:漫画 + Status *int `form:"status" bson:"status,omitempty"` // 状态 + IsActive *bool `form:"isActive" json:"isActive"` // 是否激活 + MID *string `form:"mId" json:"mId" bson:"mId" binding:"required"` // 媒体资源ID + PageNumber uint64 `form:"pageNumber" json:"pageNumber" binding:"required,min=1"` // 当前页 + PageSize uint64 `form:"pageSize" json:"pageSize" binding:"required,min=1,max=100"` // 每页条数 +} +type WebListRes struct { + Total int64 `json:"total"` + List []*mediacontentmod.MediaContent `json:"list"` +} + +func (q *WebListReq) Filter() bson.M { + var query = bson.M{} + if q.Name != nil { + query["name"] = primitive.Regex{Pattern: *q.Name, Options: "i"} + } + if q.Status != nil { + query["status"] = *q.Status + } + if q.MID != nil { + mid, _ := primitive.ObjectIDFromHex(*q.MID) + query["mediaId"] = mid + } + if q.Type != nil { + query["type"] = *q.Type + } + if q.IsActive != nil { + query["isActive"] = *q.IsActive + } + if q.ID != nil { + id, _ := primitive.ObjectIDFromHex(*q.ID) + query["_id"] = id + } + query["isDelete"] = false + return query +} +func (q *WebListReq) Options() *options.FindOptions { + return options.Find().SetSkip(int64((q.PageNumber - 1) * q.PageSize)).SetLimit(int64(q.PageSize)).SetSort(bson.M{"episodeNumber": 1}) +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res *WebListRes, err error) { + var out = &WebListRes{} + count, err := mediacontentmod.QueryAllCount(q.Filter()) + if err != nil { + return nil, err + } + if count == 0 { + out.List = []*mediacontentmod.MediaContent{} + out.Total = 0 + return out, nil + } + out.Total = count + out.List, err = mediacontentmod.QueryAllList(q.Filter(), q.Options()) + if err != nil { + log.Error(fmt.Sprintf("mediacontentmod queryAllList error:%+v:", err)) + return nil, err + } + return out, nil +} + +type WebCreateReq struct { + MediaID primitive.ObjectID `json:"mediaId" form:"mediaId" binding:"required"` // 媒体资源ID + MediaType string `json:"mediaType" form:"mediaType" binding:"required"` // 媒体类型 + EpisodeNumber uint `json:"episodeNumber" form:"episodeNumber" ` // 第几集 + ListenPermission int `json:"listenPermission" form:"listenPermission" ` // 收听权限 0:会员 1:金币购买 2:免费 + Price int64 `json:"price" form:"price" ` // 购买价格 + Cover string `json:"cover" form:"cover"` //封面 + Name string `json:"name" form:"name" ` // 章节名 + Text string `json:"text" form:"text" ` // 内容(小说专用) + Md5 string `json:"md5" form:"md5" ` // 媒体md5 + SourceID string `json:"sourceID" form:"sourceID"` // 视频唯一id,对应文件服 + VideoUrl string `json:"videoUrl" form:"videoUrl" ` // 视频地址 + AudioUrl string `json:"audioUrl" form:"audioUrl"` // 有声小说地址 + UrlSet []string `json:"urlSet" form:"urlSet"` // 地址集(多个资源地址用 例如漫画) + Filename string `json:"filename" form:"filename"` // 文件名称 + IsActive bool `json:"isActive" form:"isActive" ` // 是否激活 + PreviewEnabled bool `json:"previewEnabled" form:"previewEnabled"` // 是否允许试看 + PreviewStart int `json:"previewStart" form:"previewStart"` // 试看起始时间(秒) + PreviewSeconds int `json:"previewSeconds" form:"previewSeconds"` // 试看时长(秒) +} + +const defaultDramaPreviewSeconds = 60 + +func normalizeDramaPreview(enabled bool, start, seconds int, playTime uint) (bool, int, int, error) { + if !enabled { + return false, 0, 0, nil + } + if start < 0 { + return false, 0, 0, errors.New("试看起始时间不能小于0") + } + if seconds < 0 { + return false, 0, 0, errors.New("试看时长不能小于0") + } + if seconds == 0 { + seconds = defaultDramaPreviewSeconds + } + if playTime > 0 && uint64(start)+uint64(seconds) > uint64(playTime) { + return false, 0, 0, errors.New("试看起始时间与时长之和不能超过视频总时长") + } + return true, start, seconds, nil +} + +func validateDramaEpisodeState(episodeNumber int, listenPermission int, price int64, name, videoURL, h265URL string, active bool, status int) error { + if episodeNumber < 1 { + return errors.New("剧集编号必须大于0") + } + if strings.TrimSpace(name) == "" { + return errors.New("剧集名称不能为空") + } + if listenPermission != 1 && listenPermission != 2 { + return errors.New("短剧剧集listenPermission只支持1或2") + } + if listenPermission == 2 && price != 0 { + return errors.New("免费剧集价格必须为0") + } + if listenPermission == 1 && price <= 0 { + return errors.New("金币剧集价格必须大于0") + } + if active { + if status != 1 { + return errors.New("视频仍在转码,暂不能上架") + } + if strings.TrimSpace(videoURL) == "" && strings.TrimSpace(h265URL) == "" { + return errors.New("剧集没有播放资源,暂不能上架") + } + } + return nil +} + +func ensureEpisodeNumberUnique(mediaID primitive.ObjectID, episodeNumber int, excludeID primitive.ObjectID) error { + filter := bson.M{ + "mediaId": mediaID, + "episodeNumber": episodeNumber, + "isDelete": false, + } + if !excludeID.IsZero() { + filter["_id"] = bson.M{"$ne": excludeID} + } + count, err := mediacontentmod.QueryAllCount(filter) + if err != nil { + return err + } + if count > 0 { + return errors.New("剧集编号已存在") + } + return nil +} + +func fileStatusToEpisodeStatus(status string) int { + switch status { + case vidmod.ConvertCompleted, vidmod.Completed: + return 1 + case vidmod.FileUploadError, vidmod.MergeError, vidmod.ConvertError: + return 2 + default: + return 0 + } +} + +func mediaMetadataFromFs(id string) (vidmod.AwsPullResp, int, error) { + resp, err := updownloadser.PullFileInfo(id) + if err != nil { + return vidmod.AwsPullResp{}, 0, err + } + status := fileStatusToEpisodeStatus(resp.Data.Status) + return resp.Data, status, nil +} + +// Create 发布数据 +func (p *WebCreateReq) Create(manager string) (err error, vid primitive.ObjectID) { + now := time.Now() + media, err := mediamod.QueryMediaByID(bson.M{"_id": p.MediaID}) + if err != nil { + return err, primitive.ObjectID{} + } + if media == nil || media.ID.IsZero() { + return errors.New("media info is nll"), primitive.ObjectID{} + } + if media.IsDelete || p.MediaType != media.MediaType { + return errors.New("剧集mediaType与父媒体不一致"), primitive.ObjectID{} + } + p.Name = strings.TrimSpace(p.Name) + + if p.MediaType == mediamod.MediaTypeVideo || p.MediaType == mediamod.MediaTypeDrama { + vid, err = primitive.ObjectIDFromHex(p.SourceID) + if err != nil || vid.IsZero() { + return errors.New("sourceID不是有效的资源ID"), primitive.ObjectID{} + } + } else { + vid = primitive.NewObjectID() + } + if err = ensureEpisodeNumberUnique(media.ID, int(p.EpisodeNumber), primitive.NilObjectID); err != nil { + return err, primitive.ObjectID{} + } + if media.MediaType == mediamod.MediaTypeDrama { + if err = validateDramaEpisodeState(int(p.EpisodeNumber), p.ListenPermission, p.Price, p.Name, p.VideoUrl, "", false, 0); err != nil { + return err, primitive.ObjectID{} + } + if strings.TrimSpace(p.VideoUrl) == "" { + return errors.New("短剧剧集必须提供videoUrl"), primitive.ObjectID{} + } + } + previewEnabled, previewStart, previewSeconds := false, 0, 0 + if media.MediaType == mediamod.MediaTypeDrama { + previewEnabled, previewStart, previewSeconds, err = normalizeDramaPreview(p.PreviewEnabled, p.PreviewStart, p.PreviewSeconds, 0) + if err != nil { + return err, primitive.ObjectID{} + } + } + mc := mediacontentmod.MediaContent{ + ID: vid, + MediaID: p.MediaID, + MediaType: media.MediaType, + Cover: p.Cover, + EpisodeNumber: int(p.EpisodeNumber), + ListenPermission: p.ListenPermission, + Price: p.Price, + Name: p.Name, + IsActive: p.IsActive, + IsDelete: false, + UpdatedAct: manager, + UpdateTime: now, + CreatedAt: now, + PreviewEnabled: previewEnabled, + PreviewStart: previewStart, + PreviewSeconds: previewSeconds, + } + // 视频和短剧都使用视频资源。 + if media.MediaType == mediamod.MediaTypeVideo || media.MediaType == mediamod.MediaTypeDrama { + mc.Md5 = p.Md5 + mc.VideoUrl = strings.TrimSpace(p.VideoUrl) + mc.Status = 0 + if p.IsActive { + metadata, status, metadataErr := mediaMetadataFromFs(vid.Hex()) + if metadataErr != nil { + return errors.New("文件服务连接失败,暂不能上架剧集"), primitive.ObjectID{} + } + mc.Status = status + mc.PlayTime = metadata.PlayTime + mc.Weight = metadata.Width + mc.Height = metadata.Height + mc.MediaSize = int64(metadata.Size) + if metadata.CheckSum != "" { + mc.Md5 = metadata.CheckSum + } + mc.Ratio = metadata.Ratio + if mc.Ratio == 0 && mc.Height > 0 { + mc.Ratio = float64(mc.Weight) / float64(mc.Height) + } + if media.MediaType == mediamod.MediaTypeDrama { + mc.PreviewEnabled, mc.PreviewStart, mc.PreviewSeconds, err = normalizeDramaPreview(mc.PreviewEnabled, mc.PreviewStart, mc.PreviewSeconds, mc.PlayTime) + if err != nil { + return err, primitive.ObjectID{} + } + if err = validateDramaEpisodeState(mc.EpisodeNumber, mc.ListenPermission, mc.Price, mc.Name, mc.VideoUrl, mc.H265Url, true, mc.Status); err != nil { + return err, primitive.ObjectID{} + } + } + } + } + // 图集 + if media.MediaType == mediamod.MediaTypeImage { + mc.UrlSet = p.UrlSet + } + // 小说 + if media.MediaType == mediamod.MediaTypeText { + mc.AudioUrl = p.AudioUrl + } + + // 创建数据 + if _, err := mediacontentmod.Insert(nil, mc); err != nil { + return err, primitive.ObjectID{} + } + + err = OnMediaContentChange(media.ID, media.MediaType, true, mc.IsActive) + if err != nil { + return err, primitive.ObjectID{} + } + return nil, vid +} + +type WebUpdateReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` + EpisodeNumber *int `json:"episodeNumber"` // 第几集 + ListenPermission *int `json:"listenPermission"` // 收听权限 0:会员 1:金币购买 2:免费 + Price *int64 `json:"price"` // 购买价格 + Name *string `json:"name"` // 章节名 + Cover *string `json:"cover"` // 封面 + Text *string `json:"text"` // 内容(小说专用) + Md5 *string `json:"md5"` // 媒体md5 + VideoUrl *string `json:"videoUrl"` // 视频地址 + H265Url *string `json:"h265Url"` // H.265视频地址 + AudioUrl *string `json:"audioUrl"` // 有声小说地址 + UrlSet *[]string `json:"urlSet"` // 地址集(多个资源地址用 例如漫画) + IsActive *bool `json:"isActive"` // 是否激活 + PreviewEnabled *bool `json:"previewEnabled"` // 是否允许试看 + PreviewStart *int `json:"previewStart"` // 试看起始时间(秒) + PreviewSeconds *int `json:"previewSeconds"` // 试看时长(秒) +} + +// Update 更新数据 +func (p *WebUpdateReq) Update(manager string) error { + mediaContent, err := mediacontentmod.GetInfo(p.ID) + if err != nil { + return err + } + + if mediaContent.ID.IsZero() { + return errors.New("media content is null") + } + media, err := mediamod.GetInfo(mediaContent.MediaID) + if err != nil { + return err + } + next := mediaContent + if p.EpisodeNumber != nil { + next.EpisodeNumber = *p.EpisodeNumber + } + if p.ListenPermission != nil { + next.ListenPermission = *p.ListenPermission + } + if p.Price != nil { + next.Price = *p.Price + } + if p.Name != nil { + next.Name = strings.TrimSpace(*p.Name) + } + if p.VideoUrl != nil { + next.VideoUrl = strings.TrimSpace(*p.VideoUrl) + } + if p.H265Url != nil { + next.H265Url = strings.TrimSpace(*p.H265Url) + } + if p.IsActive != nil { + next.IsActive = *p.IsActive + } + if p.PreviewEnabled != nil { + next.PreviewEnabled = *p.PreviewEnabled + } + if p.PreviewStart != nil { + next.PreviewStart = *p.PreviewStart + } + if p.PreviewSeconds != nil { + next.PreviewSeconds = *p.PreviewSeconds + } + var metadata *vidmod.AwsPullResp + if next.IsActive && (media.MediaType == mediamod.MediaTypeVideo || media.MediaType == mediamod.MediaTypeDrama) && next.Status != 1 { + loaded, status, metadataErr := mediaMetadataFromFs(mediaContent.ID.Hex()) + if metadataErr != nil { + return errors.New("文件服务连接失败,暂不能上架剧集") + } + metadata = &loaded + next.Status = status + next.PlayTime = loaded.PlayTime + next.Weight = loaded.Width + next.Height = loaded.Height + next.MediaSize = int64(loaded.Size) + next.Ratio = loaded.Ratio + if next.Ratio == 0 && next.Height > 0 { + next.Ratio = float64(next.Weight) / float64(next.Height) + } + } + if media.MediaType == mediamod.MediaTypeDrama { + next.PreviewEnabled, next.PreviewStart, next.PreviewSeconds, err = normalizeDramaPreview(next.PreviewEnabled, next.PreviewStart, next.PreviewSeconds, next.PlayTime) + if err != nil { + return err + } + if err = validateDramaEpisodeState(next.EpisodeNumber, next.ListenPermission, next.Price, next.Name, next.VideoUrl, next.H265Url, next.IsActive, next.Status); err != nil { + return err + } + if err = ensureEpisodeNumberUnique(media.ID, next.EpisodeNumber, mediaContent.ID); err != nil { + return err + } + } + data := make(map[string]interface{}) + if p.EpisodeNumber != nil { + data["episodeNumber"] = *p.EpisodeNumber + } + if p.ListenPermission != nil { + data["listenPermission"] = *p.ListenPermission + } + if p.Price != nil { + data["price"] = *p.Price + } + if p.Name != nil { + data["name"] = next.Name + } + if p.Cover != nil { + data["cover"] = *p.Cover + } + if p.Text != nil { + data["text"] = *p.Text + } + if p.Md5 != nil { + data["md5"] = *p.Md5 + } + if p.VideoUrl != nil { + data["videoUrl"] = next.VideoUrl + } + if p.H265Url != nil { + data["h265Url"] = next.H265Url + } + if p.AudioUrl != nil { + data["audioUrl"] = *p.AudioUrl + } + if p.UrlSet != nil { + data["urlSet"] = *p.UrlSet + } + + if p.IsActive != nil { + data["isActive"] = *p.IsActive + } + if media.MediaType == mediamod.MediaTypeDrama && (p.PreviewEnabled != nil || p.PreviewStart != nil || p.PreviewSeconds != nil) { + data["previewEnabled"] = next.PreviewEnabled + data["previewStart"] = next.PreviewStart + data["previewSeconds"] = next.PreviewSeconds + } + if metadata != nil { + data["playTime"] = metadata.PlayTime + data["weight"] = metadata.Width + data["height"] = metadata.Height + data["mediaSize"] = metadata.Size + data["status"] = next.Status + data["ratio"] = next.Ratio + if metadata.CheckSum != "" { + data["md5"] = metadata.CheckSum + } + } + + data["updatedAct"] = manager + data["updateTime"] = time.Now() + if _, err = mediacontentmod.UpdateByID(nil, p.ID, data); err != nil { + return err + } + published := p.IsActive != nil && *p.IsActive && !mediaContent.IsActive + err = OnMediaContentChange(media.ID, media.MediaType, false, published) + if err != nil { + return err + } + return nil +} + +type WebBatchUpdateReq struct { + IDS []string `json:"ids" binding:"required"` + IsActive *bool `json:"isActive"` // 是否激活 + Price *int64 `json:"price"` // 价格 + Permission *int `json:"permission"` // 收听权限 0:会员 1:金币购买 2:免费 + FreeEpisode *int `json:"freeEpisode"` // 前几集免费 +} + +func (p *WebBatchUpdateReq) Filter() []primitive.ObjectID { + ids, _ := p.ValidatedIDs() + return ids +} + +func (p *WebBatchUpdateReq) ValidatedIDs() ([]primitive.ObjectID, error) { + if len(p.IDS) == 0 || len(p.IDS) > 100 { + return nil, errors.New("ids数量必须在1到100之间") + } + if p.IsActive == nil && p.Price == nil && p.Permission == nil && p.FreeEpisode == nil { + return nil, errors.New("至少需要一个批量更新字段") + } + if p.FreeEpisode != nil && *p.FreeEpisode < 0 { + return nil, errors.New("freeEpisode不能小于0") + } + ids := make([]primitive.ObjectID, 0, len(p.IDS)) + seen := make(map[primitive.ObjectID]struct{}, len(p.IDS)) + for _, raw := range p.IDS { + id, err := primitive.ObjectIDFromHex(raw) + if err != nil || id.IsZero() { + return nil, errors.New("ids包含无效的剧集ID") + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + ids = append(ids, id) + } + return ids, nil +} + +func (p *WebBatchUpdateReq) applyContentFields(content mediacontentmod.MediaContent) mediacontentmod.MediaContent { + if p.IsActive != nil { + content.IsActive = *p.IsActive + } + if p.Price != nil { + content.Price = *p.Price + } + if p.Permission != nil { + content.ListenPermission = *p.Permission + } + return content +} + +// Update 更新数据 +func (p *WebBatchUpdateReq) Update(manager string) error { + ids, err := p.ValidatedIDs() + if err != nil { + return err + } + contents, err := mediacontentmod.QueryAllList(bson.M{ + "_id": bson.M{"$in": ids}, + "isDelete": false, + }, options.Find().SetLimit(int64(len(ids)))) + if err != nil { + return err + } + if len(contents) != len(ids) { + return errors.New("部分剧集不存在或已删除") + } + mediaID := contents[0].MediaID + mediaType := contents[0].MediaType + published := false + for _, content := range contents { + if content.MediaID != mediaID { + return errors.New("批量操作的剧集必须属于同一部短剧") + } + next := p.applyContentFields(*content) + if p.IsActive != nil && *p.IsActive && !content.IsActive { + published = true + } + validateState := p.Price != nil || p.Permission != nil || (p.IsActive != nil && *p.IsActive) + if validateState && mediaType == mediamod.MediaTypeDrama { + if err = validateDramaEpisodeState(next.EpisodeNumber, next.ListenPermission, next.Price, next.Name, next.VideoUrl, next.H265Url, next.IsActive, next.Status); err != nil { + return fmt.Errorf("第%d集:%w", content.EpisodeNumber, err) + } + } + } + data := make(map[string]interface{}) + if p.IsActive != nil { + data["isActive"] = *p.IsActive + } + if p.Price != nil { + data["price"] = *p.Price + } + if p.Permission != nil { + data["listenPermission"] = *p.Permission + } + data["updatedAct"] = manager + data["updateTime"] = time.Now() + + contentChanged := p.IsActive != nil || p.Price != nil || p.Permission != nil + if contentChanged { + if _, err := mediacontentmod.UpdateByIDS(nil, ids, data); err != nil { + return err + } + if err = OnMediaContentChange(mediaID, mediaType, false, published); err != nil { + return err + } + } + if p.FreeEpisode != nil { + if _, err = mediamod.UpdateByID(nil, mediaID, map[string]interface{}{ + "freeEpisode": *p.FreeEpisode, + "updatedAct": manager, + "updateTime": time.Now(), + }); err != nil { + return err + } + } + return nil +} + +type WebDeleteReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` +} + +// Delete 删除数据 +func (p *WebDeleteReq) Delete() error { + mediaContent, err := mediacontentmod.GetInfo(p.ID) + if err != nil { + return err + } + + if err := mediacontentmod.DeleteByID(nil, p.ID); err != nil { + return err + } + + err = OnMediaContentChange(mediaContent.MediaID, mediaContent.MediaType, false, false) + if err != nil { + return err + } + return nil +} + +func OnMediaContentChange(mediaId primitive.ObjectID, mediaType string, contentAdded, published bool) (err error) { + // 获取当前最小的章节,重新update + defaultContent := mediamod.DefaultContent{} + if mediaType == mediamod.MediaTypeImage { + contentList, _, _, err := mediacontentmod.GetList(bson.M{"mediaId": mediaId, "isActive": true, "isDelete": false}, 0, 1, bson.D{{Key: "episodeNumber", Value: 1}, {Key: "createdAt", Value: 1}}) + if err != nil { + return err + } + if len(contentList) > 0 { + defaultContent.Id = contentList[0].ID + defaultContent.Name = contentList[0].Name + defaultContent.UrlSet = contentList[0].UrlSet + defaultContent.Cover = contentList[0].Cover + } + } + // 子集总数 + count, err := mediacontentmod.QueryAllCount(bson.M{"mediaId": mediaId, "isActive": true, "isDelete": false}) + if err != nil { + return err + } + // 获取子集免费章节总数 + freeCount, err := mediacontentmod.QueryAllCount(bson.M{"mediaId": mediaId, "listenPermission": 2, "isActive": true, "isDelete": false}) + if err != nil { + return err + } + + contentsPrice, err := mediacontentmod.QueryContentsPrice(mediaId) + if err != nil { + return + } + updateData := bson.M{ + "currentEpisode": count, + "contentsPrice": contentsPrice, + "freeEpisode": freeCount, + "updateTime": time.Now(), + "defaultContent": defaultContent, // 默认展示的子集内容,冗余一个字段,减少一次查询 + } + now := time.Now() + if contentAdded { + updateData["contentUpdateTime"] = now + } + if published { + updateData["latestPublishedAt"] = now + } + //修改当前父级的记录 + _, err = mediamod.UpdateByID(nil, mediaId, updateData) + + if err != nil { + return err + } + return nil +} + +// ViewMediaFromFs 查看视频转码状态 +func ViewMediaFromFs(id string) (code stderr.Code, data interface{}) { + resp, err := updownloadser.PullFileInfo(id) + if err != nil { + return stderr.ErrConnectToFs, nil + } + status := fileStatusToEpisodeStatus(resp.Data.Status) + ratio := resp.Data.Ratio + if ratio == 0 && resp.Data.Height > 0 { + ratio = float64(resp.Data.Width) / float64(resp.Data.Height) + } + set := map[string]interface{}{ + "status": status, + "playTime": resp.Data.PlayTime, + "weight": resp.Data.Width, + "height": resp.Data.Height, + "mediaSize": int64(resp.Data.Size), + "ratio": ratio, + "updateTime": time.Now(), + } + if resp.Data.CheckSum != "" { + set["md5"] = resp.Data.CheckSum + } + objId, _ := primitive.ObjectIDFromHex(id) + if _, err = mediacontentmod.UpdateByID(nil, objId, set); err != nil { + return stderr.ErrDbUpdateError, nil + } + back := make(map[string]interface{}, 10) + back["playTime"] = resp.Data.PlayTime + back["size"] = resp.Data.Size + back["width"] = resp.Data.Width + back["height"] = resp.Data.Height + back["status"] = resp.Data.Status + back["ratio"] = ratio + fileName := resp.Data.Filename + fieldNameFs := resp.Data.FieldNameFs + back["fileName"] = filepath.Join(filepath.Dir(fieldNameFs), fileName) + switch resp.Data.Status { + case vidmod.UploadLoadingToFs: + back["statusDesc"] = "正在上传中" + case vidmod.UploadCompleted: + back["statusDesc"] = "上传完成" + case vidmod.FileUploadError: + back["statusDesc"] = "上传错误" + case vidmod.Merging: + back["statusDesc"] = "正在合并中" + case vidmod.MergeCompleted: + back["statusDesc"] = "合并完成" + case vidmod.MergeError: + back["statusDesc"] = "合并失败" + case vidmod.Converting: + back["statusDesc"] = "正在转码中" + case vidmod.ConvertError: + back["statusDesc"] = "转码失败" + case vidmod.ConvertCompleted, vidmod.Completed: + back["statusDesc"] = "完成状态" + } + return stderr.Success, back +} diff --git a/web/service/mediaresourceser/media.go b/web/service/mediaresourceser/media.go new file mode 100644 index 0000000..9f5858b --- /dev/null +++ b/web/service/mediaresourceser/media.go @@ -0,0 +1,215 @@ +package mediaresourceser + +import ( + "91porn-server/common" + "91porn-server/common/log" + mediaService "91porn-server/common/media" + "91porn-server/common/mediaCenter" + "91porn-server/web/webg" + "errors" + "go.mongodb.org/mongo-driver/bson/primitive" + "strings" + "sync" +) + +type MediaListReq struct { + MediaType int `json:"media_type" form:"media_type"` // 1-动画 2-漫画 3-文本小说 4-有声小说 5-短剧 + MediaCenterId *uint `json:"media_center_id" form:"media_center_id"` // 媒资中心的中资源唯一id + VloggerId *uint `json:"vlogger_id" form:"vlogger_id"` // up主id + IsUp *int `json:"is_up" form:"is_up"` // 是否是up主 0-不是 1-是 不传则是所有 + Title *string `json:"title" form:"title"` // 标题 + Tag *string `json:"tag" form:"tag"` // 标签 + TerminalCdo *string `json:"terminal_cdo" form:"terminal_cdo"` // 上传者 + SyncType int `json:"sync_type" form:"sync_type"` // 同步类型 1-全量同步 2-增量同步 + Page uint `json:"page" form:"page"` + Size uint `json:"size" form:"size"` +} + +type MediaListResp struct { + List []*mediaCenter.Media `json:"list"` + Total uint `json:"total"` +} + +func (p *MediaListReq) Validate() error { + if p.MediaType < 1 || p.MediaType > 5 { + return errors.New("media_type只支持1到5") + } + if p.Page < 1 || p.Size < 1 || p.Size > 100 { + return errors.New("page必须大于0且size必须在1到100之间") + } + return nil +} + +func (p *MediaListReq) GetList() (res MediaListResp, err error) { + ms := mediaCenter.NewMediaCenterService(mediaCenter.Config{ + Url: webg.Conf.MediaResourceDBCfg.BaseUrl, + ProId: webg.Conf.MediaResourceDBCfg.Merchat, + Secret: webg.Conf.MediaResourceDBCfg.Secret, + }) + data, err := ms.MediaList(&mediaCenter.MediaListReq{ + MediaType: p.MediaType, + Id: p.MediaCenterId, + VloggerId: p.VloggerId, + IsUp: p.IsUp, + Title: p.Title, + Tag: p.Tag, + TerminalCdo: p.TerminalCdo, + SyncType: p.SyncType, + Page: p.Page, + Size: p.Size, + }) + if err != nil { + return + } + res.List = data.Data + res.Total = data.Pagination.Count + return +} + +type MediaContentListReq struct { + MediaId int `json:"media_id" form:"media_id"` + Page uint `json:"page" form:"page"` + Size uint `json:"size" form:"size"` +} + +type MediaContentListResp struct { + List []*mediaCenter.MediaContent `json:"list"` + Total uint `json:"total"` +} + +func (p *MediaContentListReq) Validate() error { + if p.MediaId <= 0 { + return errors.New("media_id必须大于0") + } + if p.Page < 1 || p.Size < 1 || p.Size > 100 { + return errors.New("page必须大于0且size必须在1到100之间") + } + return nil +} + +func (p *MediaContentListReq) GetList() (res MediaContentListResp, err error) { + ms := mediaCenter.NewMediaCenterService(mediaCenter.Config{ + Url: webg.Conf.MediaResourceDBCfg.BaseUrl, + ProId: webg.Conf.MediaResourceDBCfg.Merchat, + Secret: webg.Conf.MediaResourceDBCfg.Secret, + }) + data, err := ms.MediaContentList(&mediaCenter.MediaContentListReq{ + MediaId: p.MediaId, + Page: p.Page, + Size: p.Size, + }) + if err != nil { + return + } + res.List = data.Data + res.Total = data.Pagination.Count + return +} + +type MediaUseReq struct { + Ids []uint `json:"ids" form:"ids"` + Kind int `json:"kind" form:"kind" binding:"required"` //种类(1、哩番,2、3D,3、同人动画,4、单行本,5、连载漫画,6、韩漫 7、小说 8剧场 + Type int `json:"type" form:"type" binding:"required"` //维度筛选 1-动漫 2-漫画 + Dimension string `json:"dimension" form:"dimension" binding:"required"` //维度 + SectionId primitive.ObjectID `json:"section_id" form:"section_id"` //专题id +} + +func (p *MediaUseReq) Validate() error { + if len(p.Ids) == 0 || len(p.Ids) > 100 { + return errors.New("ids数量必须在1到100之间") + } + if strings.EqualFold(strings.TrimSpace(p.Dimension), "drama") { + if p.Kind != 8 || p.Type != 1 { + return errors.New("短剧导入时kind必须为8且type必须为1") + } + } + return nil +} + +func (p *MediaUseReq) Use(admin string) (err error) { + if err = p.Validate(); err != nil { + return err + } + workerCount := 5 + if len(p.Ids) < workerCount { + workerCount = len(p.Ids) + } + jobs := make(chan uint) + errs := make(chan error, len(p.Ids)) + var wg sync.WaitGroup + for i := 0; i < workerCount; i++ { + wg.Add(1) + common.Go(func() { + defer wg.Done() + for id := range jobs { + if useErr := use(id, admin, p.Dimension, p.Type, p.Kind, p.SectionId); useErr != nil { + log.Error("MediaUse Fail", log.Any("id", id), log.E(useErr)) + errs <- useErr + } + } + }) + } + for _, id := range p.Ids { + jobs <- id + } + close(jobs) + wg.Wait() + close(errs) + for useErr := range errs { + if useErr != nil { + return useErr + } + } + return +} + +func use(id uint, admin string, dimension string, dimensionType, kind int, sectionId primitive.ObjectID) (err error) { + mediaService := mediaService.NewMediaSync(webg.Redis, mediaCenter.NewMediaCenterService(mediaCenter.Config{ + Url: webg.Conf.MediaResourceDBCfg.BaseUrl, + ProId: webg.Conf.MediaResourceDBCfg.Merchat, + Secret: webg.Conf.MediaResourceDBCfg.Secret, + })) + // 调用use接口,在媒资库打上标记,避免还出现在增量数据里 + err = mediaService.Ms.MediaUse(&mediaCenter.MediaUseReq{ + Ids: []uint{id}, + }) + if err != nil { + return + } + // 因为需要同步的东西特别多,所以需要异步处理 + mediaService.Worker.Exec(func() { + mediaService.UseMediaData(admin, id, dimension, dimensionType, kind, sectionId) + }) + return +} + +type MediaIgnoreReq struct { + Ids []uint `json:"ids" form:"ids"` +} + +func (p *MediaIgnoreReq) Ignore() (err error) { + ms := mediaCenter.NewMediaCenterService(mediaCenter.Config{ + Url: webg.Conf.MediaResourceDBCfg.BaseUrl, + ProId: webg.Conf.MediaResourceDBCfg.Merchat, + Secret: webg.Conf.MediaResourceDBCfg.Secret, + }) + // 在媒资库打上标记,避免还出现在增量数据里 + return ms.MediaIgnore(&mediaCenter.MediaIgnoreReq{ + Ids: p.Ids, + }) +} + +type MediaSyncReq struct { +} + +// 执行同步的请求 +func (p *MediaSyncReq) Sync(admin string) { + common.Go(func() { + mediaService := mediaService.NewMediaSync(webg.Redis, mediaCenter.NewMediaCenterService(mediaCenter.Config{ + Url: webg.Conf.MediaResourceDBCfg.BaseUrl, + ProId: webg.Conf.MediaResourceDBCfg.Merchat, + Secret: webg.Conf.MediaResourceDBCfg.Secret, + })) + mediaService.SyncAll(admin) + }) +} diff --git a/web/service/mediaresourceser/media_test.go b/web/service/mediaresourceser/media_test.go new file mode 100644 index 0000000..2df1d1d --- /dev/null +++ b/web/service/mediaresourceser/media_test.go @@ -0,0 +1,32 @@ +package mediaresourceser + +import "testing" + +func TestMediaListReqValidate(t *testing.T) { + valid := MediaListReq{MediaType: 5, Page: 1, Size: 20} + if err := valid.Validate(); err != nil { + t.Fatalf("valid request rejected: %v", err) + } + invalidType := valid + invalidType.MediaType = 6 + if err := invalidType.Validate(); err == nil { + t.Fatal("invalid media_type unexpectedly accepted") + } + invalidPage := valid + invalidPage.Page = 0 + if err := invalidPage.Validate(); err == nil { + t.Fatal("page 0 unexpectedly accepted") + } +} + +func TestMediaUseReqValidateDrama(t *testing.T) { + valid := MediaUseReq{Ids: []uint{1}, Dimension: "drama", Kind: 8, Type: 1} + if err := valid.Validate(); err != nil { + t.Fatalf("valid drama import rejected: %v", err) + } + invalid := valid + invalid.Kind = 1 + if err := invalid.Validate(); err == nil { + t.Fatal("invalid drama kind unexpectedly accepted") + } +} diff --git a/web/service/mediaser/drama_web_test.go b/web/service/mediaser/drama_web_test.go new file mode 100644 index 0000000..eaf1fdc --- /dev/null +++ b/web/service/mediaser/drama_web_test.go @@ -0,0 +1,106 @@ +package mediaser + +import ( + "testing" + + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediamod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func validDramaCreateReq() WebCreateReq { + return WebCreateReq{ + Title: "测试短剧", + HorizontalCover: "https://example.com/h.jpg", + VerticalCover: "https://example.com/v.jpg", + TotalEpisode: 10, + UpdateStatus: mediamod.Update, + MediaType: mediamod.MediaTypeDrama, + Kind: mediamod.Theater, + Permission: 1, + } +} + +func TestWebCreateReqValidateDrama(t *testing.T) { + tests := []struct { + name string + mutate func(*WebCreateReq) + wantErr bool + }{ + {name: "valid"}, + {name: "missing cover", mutate: func(req *WebCreateReq) { req.VerticalCover = "" }, wantErr: true}, + {name: "published before first episode", mutate: func(req *WebCreateReq) { req.Status = 1 }, wantErr: true}, + {name: "wrong kind", mutate: func(req *WebCreateReq) { req.Kind = 1 }, wantErr: true}, + {name: "wrong permission", mutate: func(req *WebCreateReq) { req.Permission = 2 }, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := validDramaCreateReq() + if tt.mutate != nil { + tt.mutate(&req) + } + if gotErr := req.Validate() != nil; gotErr != tt.wantErr { + t.Fatalf("Validate() error = %v, wantErr %v", gotErr, tt.wantErr) + } + }) + } +} + +func TestValidateDramaMediaForPublish(t *testing.T) { + media := mediamod.Media{ + ID: primitive.NewObjectID(), + MediaType: mediamod.MediaTypeDrama, + Title: "测试短剧", + HorizontalCover: "h.jpg", + VerticalCover: "v.jpg", + TotalEpisode: 10, + UpdateStatus: mediamod.Update, + Kind: mediamod.Theater, + Permission: 1, + } + first := mediacontentmod.MediaContent{ + ID: primitive.NewObjectID(), + MediaID: media.ID, + MediaType: mediamod.MediaTypeDrama, + EpisodeNumber: 1, + Name: "第1集", + Status: 1, + IsActive: true, + VideoUrl: "https://example.com/1.m3u8", + } + if err := validateDramaMediaForPublish(media, first); err != nil { + t.Fatalf("valid drama cannot publish: %v", err) + } + first.Status = 0 + if err := validateDramaMediaForPublish(media, first); err == nil { + t.Fatal("transcoding first episode unexpectedly allowed to publish") + } + first.Status = 1 + first.VideoUrl = "" + if err := validateDramaMediaForPublish(media, first); err == nil { + t.Fatal("first episode without play URL unexpectedly allowed to publish") + } +} + +func TestWebListReqFilterUsesScalarValues(t *testing.T) { + status, kind, mediaCenterID := 1, mediamod.Theater, uint(123) + req := WebListReq{Status: &status, Kind: &kind, MediaCenterID: &mediaCenterID} + filter := req.Filter() + if filter["status"] != status || filter["kind"] != kind || filter["mediaCenterId"] != mediaCenterID { + t.Fatalf("unexpected filter: %#v", filter) + } +} + +func TestWebBatchUpdateReqRejectsDramaParentPricing(t *testing.T) { + price := int64(30) + req := WebBatchUpdateReq{Price: &price} + medias := []*mediamod.Media{{MediaType: mediamod.MediaTypeDrama}} + if err := req.validateDramaBatchFields(medias); err == nil { + t.Fatal("drama parent pricing unexpectedly accepted") + } + req.Price = nil + if err := req.validateDramaBatchFields(medias); err != nil { + t.Fatalf("drama batch without parent pricing rejected: %v", err) + } +} diff --git a/web/service/mediaser/media.go b/web/service/mediaser/media.go new file mode 100755 index 0000000..930fc4a --- /dev/null +++ b/web/service/mediaser/media.go @@ -0,0 +1,1095 @@ +package mediaser + +import ( + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/db" + "91porn-server/common/file" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/common/redis" + sli "91porn-server/common/slice" + "91porn-server/common/stderr" + topser "91porn-server/common/top" + topasist "91porn-server/common/top/asistant" + "91porn-server/models/commod" + "91porn-server/models/v/mediacontentmod" + "91porn-server/models/v/mediatagmod" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/modulesectionmod" + "91porn-server/models/v/vidmod" + "91porn-server/web/webg" + "bytes" + "errors" + "fmt" + "strings" + "time" + + "go.mongodb.org/mongo-driver/mongo/options" + + "91porn-server/models/v/mediamod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + ID *string `form:"id" bson:"id,omitempty"` // ID + Title *string `form:"title" json:"title"` // 帖子标题 + Kind *int `form:"kind" bson:"kind,omitempty"` // 帖子种类 1:动漫,2:漫画 + UpdateStatus *int `form:"updateStatus" bson:"updateStatus,omitempty"` // 连载状态 + Type *string `form:"type" bson:"type,omitempty"` // 类型 "video":视频,"image":图片 + Status *int `form:"status" bson:"status,omitempty"` // 是否开启 + Permission *int `form:"permission" json:"permission"` // 观看权限 + Start *time.Time `form:"start" json:"start"` // 开始时间 + End *time.Time `form:"end" json:"end"` // 结束时间 + Tag *string `form:"tag" json:"tag"` // 标签名称 + SID *string `form:"sId" json:"sId"` // 专题ID + MID *string `form:"mId" json:"mId"` // 模块ID + MediaType *string `form:"mediaType" json:"mediaType"` // 媒体类型 + MediaSubType *int `form:"mediaSubType" json:"mediaSubType"` // 媒体子类型(小说使用)0文本小说 1-有声小说 + MediaCenterID *uint `form:"mediaCenterId" json:"mediaCenterId"` // 媒资库资源ID + PageNumber uint64 `form:"pageNumber" json:"pageNumber" binding:"required,min=1"` // 当前页 + PageSize uint64 `form:"pageSize" json:"pageSize" binding:"required,min=1,max=100"` // 每页条数 +} +type WebListRes struct { + Total int64 `json:"total"` + List []*mediamod.Media `json:"list"` +} + +func (q *WebListReq) Filter() bson.M { + var query = bson.M{} + if q.Title != nil { + i := make(map[string]string) + i["$regex"] = *q.Title + i["$options"] = "i" + query["title"] = i + } + if q.Status != nil { + query["status"] = *q.Status + } + if q.Type != nil { + query["mediaType"] = *q.Type + } + if q.Permission != nil { + query["permission"] = *q.Permission + } + if q.Tag != nil { + //tag, _ := mediatagmod.FindOneTagByName(*q.Tag) + //query["tags"] = tag.ID + tagIds, _ := mediatagmod.FindTagIdsByName(*q.Tag) + if len(tagIds) > 0 { + query["tags"] = bson.M{"$in": tagIds} + } + } + if q.Start != nil && q.End != nil { + i := make(map[string]time.Time) + i["$gte"] = *q.Start + i["$lt"] = *q.End + query["createdAt"] = i + } + if q.ID != nil { + id, _ := primitive.ObjectIDFromHex(*q.ID) + query["_id"] = id + } + if q.SID != nil { + id, _ := primitive.ObjectIDFromHex(*q.SID) + query["sId"] = id + } + if q.MID != nil { + id, _ := primitive.ObjectIDFromHex(*q.MID) + query["mId"] = id + } + if q.MediaType != nil { + query["mediaType"] = *q.MediaType + } + if q.MediaSubType != nil { + query["mediaSubType"] = *q.MediaSubType + } + if q.Kind != nil { + query["kind"] = *q.Kind + } + if q.UpdateStatus != nil && *q.UpdateStatus > 0 { + query["updateStatus"] = *q.UpdateStatus + } + if q.MediaCenterID != nil { + query["mediaCenterId"] = *q.MediaCenterID + } + query["isDelete"] = false + return query +} + +func (q *WebListReq) Options() *options.FindOptions { + sort := bson.M{"createdAt": -1} + if q.SID != nil { + sort = bson.M{"sectionSort": -1} + } + return options.Find().SetSkip(int64((q.PageNumber - 1) * q.PageSize)).SetLimit(int64(q.PageSize)).SetSort(sort) +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res *WebListRes, err error) { + var out = &WebListRes{} + count, err := mediamod.QueryAllCount(q.Filter()) + if err != nil { + return nil, err + } + if count == 0 { + out.List = []*mediamod.Media{} + out.Total = 0 + return out, nil + } + out.Total = count + out.List, err = mediamod.QueryAllList(q.Filter(), q.Options()) + if err != nil { + log.Error(fmt.Sprintf("mediamod queryAllList error:%+v:", err)) + return nil, err + } + + if len(out.List) > 0 { + var tags []primitive.ObjectID + for _, m := range out.List { + if len(m.Tags) > 0 { + tags = append(tags, m.Tags...) + } + } + + if len(tags) > 0 { + toMap, err := mediatagmod.FindFilterToMap(bson.M{"_id": bson.M{"$in": tags}}) + if err != nil { + return out, stderr.ErrDbQueryError + } + + if toMap != nil { + for i, v := range out.List { + var tagObjs []mediamod.TagDetail + if len(v.Tags) > 0 { + for _, id := range v.Tags { + if toMap[id] != nil { + tagObjs = append(tagObjs, mediamod.TagDetail{ + Id: id, + Name: toMap[id].Name, + }) + } + } + } + out.List[i].TagDetails = tagObjs + out.List[i].CountPurchasesRate = float32(out.List[i].CountBrowse) / float32(out.List[i].CountView+1) + } + } + } + + } + + return out, nil +} + +// Export 导出 +func (q *WebListReq) Export() (buff bytes.Buffer, code error) { + count, err := mediamod.QueryAllCount(q.Filter()) + if err != nil { + return + } + if count == 0 { + return + } + + step := 900 + totalPages := int(count)/step + 1 + list := []*mediamod.WebExportMedia{} + for page := 1; page <= totalPages; page++ { + q.PageSize = uint64(step) + q.PageNumber = uint64(page) + res, err := mediamod.QueryAllList(q.Filter(), q.Options()) + if err != nil { + log.Error(fmt.Sprintf("collectionmod queryAllList error:%+v:", err)) + continue + } + // 没有查到数据直接停止 + if len(res) == 0 { + break + } + + var tags []primitive.ObjectID + for _, m := range res { + if len(m.Tags) > 0 { + tags = append(tags, m.Tags...) + } + } + tags = sli.RemoveRepObjectID(tags) + toMap, err := mediatagmod.FindFilterToMap(bson.M{"_id": bson.M{"$in": tags}}) + if err != nil { + return + } + for _, v := range res { + item := &mediamod.WebExportMedia{ + ID: v.ID, + Title: v.Title, + Kind: mediamod.KindName(v.Kind), + Direction: mediamod.DirectionName(v.Direction), + TotalEpisode: v.TotalEpisode, + UpdateStatus: mediamod.UpdateStatusName(v.UpdateStatus), + Status: mediamod.StatusName(v.Status), + ModuleName: v.ModuleName, + SectionName: v.SectionName, + MediaType: mediamod.MediaTypeName(v.MediaType), + FreeTime: v.FreeTime, + Price: v.Price, + CountBrowse: v.CountBrowse, + CountCollect: v.CountCollect, + CountComment: v.CountComment, + CountLike: v.CountLike, + CountPurchases: v.CountPurchases, + CountView: v.CountView, + Permission: mediamod.PermissionName(v.Permission), + PermissionIconHide: v.PermissionIconHide, + Summary: v.Summary, + UpdatedAct: v.UpdatedAct, + CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"), + UpdateTime: v.UpdateTime.Format("2006-01-02 15:04:05"), + } + tagNameList := []string{} + for _, tagId := range v.Tags { + tag, ok := toMap[tagId] + if !ok { + continue + } + tagNameList = append(tagNameList, tag.Name) + } + item.Tags = strings.Join(tagNameList, ",") + list = append(list, item) + } + fmt.Println(len(list)) + } + + excelFile := file.WriteStruct2Xlsx("动漫列表", list) + defer excelFile.Close() + _ = excelFile.DeleteSheet("Sheet1") + _, _ = excelFile.WriteTo(&buff) + return buff, nil +} + +type WebFindMediaReq struct { + ID string `form:"id" bson:"id,omitempty"` // ID +} +type WebFindMediaRes struct { + MediaTitle string `json:"mediaTitle" bson:"mediaTitle"` // 媒体标题 + MediaType string `json:"mediaType" bson:"mediaType"` // 媒体类型 + IsExist bool `json:"isExist" bson:"isExist"` // 是否存在 +} + +func (q *WebFindMediaReq) Filter() bson.M { + var query = bson.M{} + id, _ := primitive.ObjectIDFromHex(q.ID) + query["_id"] = id + query["isDelete"] = false + return query +} + +// GetMediaByID 获取列表 +func (q *WebFindMediaReq) GetMediaByID() (res *WebFindMediaRes, err error) { + var out = &WebFindMediaRes{} + data, err := mediamod.QueryMediaByID(q.Filter()) + if err != nil { + log.Error(fmt.Sprintf("mediamod QueryMediaByID error:%+v:", err)) + return nil, err + } + + if data == nil || data.ID.IsZero() { + return nil, errors.New("media data is null") + } + if !data.ID.IsZero() { + out.IsExist = true + out.MediaTitle = data.Title + out.MediaType = data.MediaType + } else { + out.IsExist = false + } + + return out, nil +} + +type WebCreateReq struct { + SortCode int `json:"sortCode" form:"sortCode"` // 排序 + Status int `json:"status" form:"status"` // 是否开启 + Title string `json:"title" form:"title"` // 标题 + HorizontalCover string `json:"horizontalCover" form:"horizontalCover"` // 横版封面 + VerticalCover string `json:"verticalCover" form:"verticalCover"` // 竖版封面 + Tags []primitive.ObjectID `json:"tags" form:"tags"` // 标签 + Summary string `json:"summary" form:"summary"` // 简介 + TotalEpisode int `json:"totalEpisode" form:"totalEpisode"` // 总集数 + UpdateStatus int `json:"updateStatus" bson:"updateStatus"` // 更新状态 + MediaType string `json:"mediaType" form:"mediaType"` // 媒体类型 + MediaSubType mediamod.MediaSubType `json:"mediaSubType" form:"mediaSubType"` // 子类型 ,暂时只有小说使用 0-默认文本小说 1-有声小说 + Kind int `json:"kind" form:"kind"` // 种类 (1、哩番,2、3D,3、同人动画,4、单行本,5、连载漫画,6、韩漫 ) + PermissionIconHide bool `json:"permissionIconHide" form:"permissionIconHide"` // 售卖类型标识隐藏 + Permission int `json:"permission" form:"permission"` // 收听权限 0:会员 1:金币购买 2:免费 + Price int64 `json:"price" form:"price"` // 价格 + Direction int64 `json:"direction" form:"direction"` // 排版方向 0横 1竖(冗余字段) + FreeTime int `json:"freeTime" form:"freeTime"` // 免费试看时长 单位 s + FreeEpisode int `json:"freeEpisode" form:"freeEpisode"` // 免费集数(前几集免费) + Choice primitive.ObjectID `json:"choice" form:"choice"` // 精选 + ChoiceSort int `json:"choiceSort" form:"choiceSort"` // 精选专题排序 + IsActive bool `json:"isActive" form:"isActive"` // 是否激活 + CountComment int64 `json:"countComment" form:"countComment"` // 评论数 + CountPurchases int64 `json:"countPurchases" form:"countPurchases"` // 购买数 + Style int `json:"style" form:"style"` // 对应五种风格背景颜色 + CountBrowse int64 `json:"countBrowse" form:"countBrowse"` // 浏览数 + CountCollect int64 `json:"countCollect" form:"countCollect"` // 收藏数 + CountLike int64 `json:"countLike" form:"countLike"` // 喜欢数 + CountView int64 `json:"countView" form:"countView"` // 展现数 + Number int64 `json:"number" form:"number"` // 编号 + MediaSize int `form:"mediaSize" json:"mediaSize"` // 媒体大小 + SourceID string `form:"sourceID" json:"sourceID"` // 资源ID + SourceURL string `form:"sourceURL" json:"sourceURL"` // 资源链接 + AudioUrl string `json:"audioUrl" bson:"audioUrl"` // 有声小说地址 + Author string `json:"author" bson:"author"` // 作者 + Md5 string `form:"md5" json:"md5"` // md5 + EpisodeNumber int `json:"episodeNumber" bson:"episodeNumber"` // 第几集 + EpisodeNumberName string `form:"episodeNumberName" json:"episodeNumberName"` // 章节名称 + ListenPermission int `json:"listenPermission" bson:"listenPermission"` // 收听权限 0:会员 1:金币购买 2:免费 + SeriesCover []string `form:"seriesCover" json:"seriesCover"` // 封面 + Filename string `form:"filename" json:"filename"` // 文件名称 + PlayTime uint `form:"playTime" json:"playTime"` // 时长 + Cover string `form:"cover" json:"cover"` // 子集的封面 +} + +func (p *WebCreateReq) Validate() error { + if p.MediaType != mediamod.MediaTypeDrama { + return nil + } + p.Title = strings.TrimSpace(p.Title) + p.HorizontalCover = strings.TrimSpace(p.HorizontalCover) + p.VerticalCover = strings.TrimSpace(p.VerticalCover) + if p.Title == "" || p.HorizontalCover == "" || p.VerticalCover == "" { + return errors.New("短剧标题、横版封面和竖版封面不能为空") + } + if p.TotalEpisode < 1 { + return errors.New("短剧总集数必须大于0") + } + if p.UpdateStatus != mediamod.Update && p.UpdateStatus != mediamod.Finish { + return errors.New("短剧更新状态只支持1或2") + } + if p.Status != 0 { + return errors.New("短剧创建后请先添加有效首集再上架") + } + if p.Kind != mediamod.Theater { + return errors.New("短剧kind必须为8") + } + if p.Permission != 1 { + return errors.New("短剧主记录permission必须为1") + } + if p.SortCode < 0 { + return errors.New("短剧推荐排序不能小于0") + } + return nil +} + +// Create 发布数据 +func (p *WebCreateReq) Create(manager string) (code stderr.Code, vid primitive.ObjectID) { + now := time.Now() + if p.MediaType == mediamod.MediaTypeVideo { + vid, _ = primitive.ObjectIDFromHex(p.SourceID) + } else { + vid = primitive.NewObjectID() + } + if p.UpdateStatus == 0 { + p.UpdateStatus = 2 + } + data := mediamod.Media{ + SortCode: p.SortCode, + Status: p.Status, + Title: p.Title, + HorizontalCover: p.HorizontalCover, + VerticalCover: p.VerticalCover, + Tags: p.Tags, + Summary: p.Summary, + TotalEpisode: p.TotalEpisode, + UpdateStatus: p.UpdateStatus, + MediaType: p.MediaType, + MediaSubType: p.MediaSubType, + Kind: p.Kind, + PermissionIconHide: p.PermissionIconHide, + Permission: p.Permission, + Price: p.Price, + Direction: p.Direction, + Style: p.Style, + FreeTime: p.FreeTime, + FreeEpisode: p.FreeEpisode, + Choice: p.Choice, + ChoiceSort: p.ChoiceSort, + CountComment: p.CountComment, + CountPurchases: 0, + CountBrowse: 0, + CountCollect: 0, + CountLike: 0, + CountView: 0, + Number: 0, + IsDelete: false, + UpdatedAct: manager, + UpdateTime: now, + CreatedAt: now, + } + if p.MediaType == mediamod.MediaTypeImage { + data.DefaultContent = mediamod.DefaultContent{ + Id: vid, + Cover: p.Cover, + Name: p.EpisodeNumberName, + UrlSet: p.SeriesCover, + } + } + resultID := vid + err := webg.VideoDB.Trans(func(t *db.MongoTool) error { + // 创建数据 + mediaId, err := mediamod.Insert(t, data) + if err != nil { + log.Error(fmt.Sprintf("SubmitBase mediamod.InsertMedia err:%v", err)) + return stderr.ErrDbInsertError + } + if p.MediaType == mediamod.MediaTypeDrama { + resultID = mediaId + return nil + } + + mc := mediacontentmod.MediaContent{ + ID: vid, + MediaID: mediaId, + Cover: p.Cover, + MediaType: p.MediaType, + EpisodeNumber: p.EpisodeNumber, + Name: p.EpisodeNumberName, + ListenPermission: p.ListenPermission, + Price: p.Price, + IsActive: p.IsActive, + } + // 视频 + if p.MediaType == mediamod.MediaTypeVideo { + mc.Md5 = p.Md5 + mc.VideoUrl = p.SourceURL + mc.Status = 0 + } + // 音频 + if p.MediaType == mediamod.MediaTypeText { + mc.Md5 = p.Md5 + mc.AudioUrl = p.AudioUrl + mc.Author = p.Author + mc.Status = 1 + } + // 图集 + if p.MediaType == mediamod.MediaTypeImage { + mc.UrlSet = p.SeriesCover + } + + // 媒体内容新增 + if _, err := mediacontentmod.Insert(t, mc); err != nil { + log.Error(fmt.Sprintf("SubmitBase mediacontentmod Insert err:%v", err)) + return stderr.ErrDbInsertError + } + return nil + }) + if err != nil { + log.Error(fmt.Sprintf("Media SubmitBase Trans fail error:%+v:", err)) + return stderr.Failure, vid + } + + return stderr.Success, resultID +} + +// DeleteManyByTTL 删除视频 被删除的视屏归属为officialUID,未审核或审核未通过的视屏通过TTL删除 +func DeleteManyByTTL(ids ...primitive.ObjectID) (code stderr.Code, data interface{}) { + delSetDoc := (&vidmod.SetDoc{}).SetPublisherID(constant.DeleteUID).SetDeleteAt(time.Now()).SetStatus(vidmod.CheckFailure) + _, err := vidmod.ModifyMany(ids, *delSetDoc) + if err != nil { + return stderr.ErrDbUpdateError, nil + } + return stderr.Success, nil +} + +// SendBaseInfo2AWs 上传基本文件信息到aws +func SendBaseInfo2AWs(req mediamod.SendBaseInfo2AWsReq) (code stderr.Code) { + tags := []string{} + if len(req.Tags) > 0 { + for _, t := range req.Tags { + tags = append(tags, t.Hex()) + } + } + info := vidmod.BaseVid4Aws{ + ID: req.SourceID, + Tags: tags, + CheckSum: req.Md5, + Title: req.Title, + Actors: []string{}, + AddedTime: time.Now().Format("2006-01-02"), + PlayTime: uint64(req.PlayTime), + Size: req.MediaSize, + Filename: req.Filename, + Via: "q1", + Type: "sp", + } + params, _ := common.ToJsonM(info) + respBody := commod.Resp{} + if _, err := httputil.DefaultClientPostJsonWithResp(&respBody, webg.Conf.URL.FileInfoUrl, nil, params); err != nil { + log.Error("SendBaseInfo2AWs error", log.Any("req", req), log.Any("err", err)) + return stderr.Failure + } + + if respBody.Code != stderr.Success { + log.Error("SendBaseInfo2AWs status error", log.Any("req", req), log.Any("respBody.Code", respBody.Code)) + return stderr.Failure + } + return stderr.Success +} + +type WebUpdateReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` + SortCode *int `json:"sortCode"` // 排序 + MediaSubType *mediamod.MediaSubType `json:"mediaSubType" form:"mediaSubType"` // 子类型 ,暂时只有小说使用 0-默认文本小说 1-有声小说 + Status *int `json:"status"` // 是否开启 1:上架,2:下架 + Title *string `json:"title"` // 标题 + HorizontalCover *string `json:"horizontalCover"` // 横版封面 + VerticalCover *string `json:"verticalCover"` // 竖版封面 + Tags *[]primitive.ObjectID `json:"tags"` // 标签 + Summary *string `json:"summary"` // 简介 + TotalEpisode *int `json:"totalEpisode"` // 总集数 + UpdateStatus *int `json:"updateStatus" bson:"updateStatus"` // 更新状态 + Kind *int `json:"kind"` // 种类(1、哩番,2、3D,3、同人动画,4、单行本,5、连载漫画,6、韩漫) + Style *int `json:"style" form:"style"` // 对应五种风格背景颜色 + PermissionIconHide *bool `json:"permissionIconHide"` // 售卖类型标识隐藏 + Permission *int `json:"permission"` // 收听权限 0:会员 1:金币购买 2:免费 + Price *int64 `json:"price"` // 价格 + Direction *int64 `json:"direction"` // 排版方向 0横 1竖(冗余字段) + SellType *int `json:"sellType"` // 商品售卖类型(0单集售卖 1整部售卖) + FreeTime *int `json:"freeTime"` // 免费试看时长 单位 s + FreeEpisode *int `json:"freeEpisode" form:"freeEpisode"` // 免费集数(前几集免费) + SectionSort *int `json:"sectionSort" bson:"sectionSort"` // 专题排序 + Choice *primitive.ObjectID `json:"choice"` // 精选 + ChoiceSort *int `json:"choiceSort"` // 精选专题排序 + IsActive *bool `json:"isActive"` // 是否激活 + IsRemove *bool `json:"isRemove"` // 是否移出专题 + CountComment *int64 `json:"countComment"` // 评论数 + CountPurchases *int64 `json:"countPurchases"` // 购买数 + CountBrowse *int64 `json:"countBrowse"` // 浏览数 + CountCollect *int64 `json:"countCollect"` // 收藏数 + CountLike *int64 `json:"countLike"` // 喜欢数 + CountView *int64 `json:"countView"` // 展现数 + Number *int64 `json:"number"` // 编号 +} + +func (p *WebUpdateReq) mergedDramaMedia(current mediamod.Media) mediamod.Media { + next := current + if p.SortCode != nil { + next.SortCode = *p.SortCode + } + if p.Status != nil { + next.Status = *p.Status + } + if p.Title != nil { + next.Title = strings.TrimSpace(*p.Title) + } + if p.HorizontalCover != nil { + next.HorizontalCover = strings.TrimSpace(*p.HorizontalCover) + } + if p.VerticalCover != nil { + next.VerticalCover = strings.TrimSpace(*p.VerticalCover) + } + if p.TotalEpisode != nil { + next.TotalEpisode = *p.TotalEpisode + } + if p.UpdateStatus != nil { + next.UpdateStatus = *p.UpdateStatus + } + if p.Kind != nil { + next.Kind = *p.Kind + } + if p.Permission != nil { + next.Permission = *p.Permission + } + return next +} + +func validateDramaMediaFields(media mediamod.Media, requireHorizontalCover bool) error { + if strings.TrimSpace(media.Title) == "" || strings.TrimSpace(media.VerticalCover) == "" { + return errors.New("短剧标题和竖版封面不能为空") + } + if requireHorizontalCover && strings.TrimSpace(media.HorizontalCover) == "" { + return errors.New("短剧横版封面不能为空") + } + if media.TotalEpisode < 1 { + return errors.New("短剧总集数必须大于0") + } + if media.UpdateStatus != mediamod.Update && media.UpdateStatus != mediamod.Finish { + return errors.New("短剧更新状态只支持1或2") + } + if media.Status != 0 && media.Status != 1 { + return errors.New("短剧状态只支持0或1") + } + if media.Kind != mediamod.Theater { + return errors.New("短剧kind必须为8") + } + if media.Permission != 1 { + return errors.New("短剧主记录permission必须为1") + } + if media.SortCode < 0 { + return errors.New("短剧推荐排序不能小于0") + } + return nil +} + +func validateDramaMediaForPublish(media mediamod.Media, first mediacontentmod.MediaContent) error { + if err := validateDramaMediaFields(media, true); err != nil { + return err + } + return validateDramaFirstEpisodeForPublish(media, first) +} + +func validateDramaFirstEpisodeForPublish(media mediamod.Media, first mediacontentmod.MediaContent) error { + if first.ID.IsZero() || first.MediaID != media.ID || first.MediaType != mediamod.MediaTypeDrama || first.EpisodeNumber != 1 || first.IsDelete { + return errors.New("短剧第1集不存在,不能上架整部短剧") + } + if !first.IsActive { + return errors.New("短剧第1集未上架,不能上架整部短剧") + } + if first.Status != 1 { + return errors.New("短剧第1集仍在转码,不能上架整部短剧") + } + if strings.TrimSpace(first.VideoUrl) == "" && strings.TrimSpace(first.H265Url) == "" { + return errors.New("短剧第1集没有播放资源,不能上架整部短剧") + } + return nil +} + +func (p *WebUpdateReq) validateDramaUpdate(current mediamod.Media) error { + if current.MediaType != mediamod.MediaTypeDrama { + return nil + } + next := p.mergedDramaMedia(current) + if err := validateDramaMediaFields(next, false); err != nil { + return err + } + if next.Status != 1 { + return nil + } + first, err := mediacontentmod.GetInfoByCond(bson.M{ + "mediaId": current.ID, + "mediaType": mediamod.MediaTypeDrama, + "episodeNumber": 1, + "isDelete": false, + }) + if err != nil { + return errors.New("短剧第1集不存在,不能上架整部短剧") + } + return validateDramaFirstEpisodeForPublish(next, first) +} + +// 钻石榜 +const ( + diamondRanking = "DiamondRanking:%v" +) + +// Update 更新数据 +func (p *WebUpdateReq) Update(manager string) error { + media, err := mediamod.GetInfo(p.ID) + if err != nil { + return err + } + if err = p.validateDramaUpdate(media); err != nil { + return err + } + + data := make(map[string]interface{}) + if p.MediaSubType != nil { + data["mediaSubType"] = *p.MediaSubType + } + if p.SortCode != nil { + data["sortCode"] = *p.SortCode + } + if p.Status != nil { + data["status"] = *p.Status + if media.Status != 1 && *p.Status == 1 { + data["latestPublishedAt"] = time.Now() + } + } + if p.UpdateStatus != nil { + data["updateStatus"] = *p.UpdateStatus + if *p.UpdateStatus == 0 { + data["updateStatus"] = 2 + } + } + if p.Title != nil { + data["title"] = strings.TrimSpace(*p.Title) + } + if p.HorizontalCover != nil { + data["horizontalCover"] = strings.TrimSpace(*p.HorizontalCover) + } + if p.VerticalCover != nil { + data["verticalCover"] = strings.TrimSpace(*p.VerticalCover) + } + if p.Tags != nil { + data["tags"] = *p.Tags + } + if p.SectionSort != nil { + data["sectionSort"] = *p.SectionSort + } + if p.Summary != nil { + data["summary"] = *p.Summary + } + if p.TotalEpisode != nil { + data["totalEpisode"] = *p.TotalEpisode + } + if p.Kind != nil { + data["kind"] = *p.Kind + } + if p.PermissionIconHide != nil { + data["permissionIconHide"] = *p.PermissionIconHide + } + if p.Style != nil { + data["style"] = *p.Style + } + if p.Permission != nil { + data["permission"] = *p.Permission + } + if p.Price != nil { + data["price"] = *p.Price + } + if p.Direction != nil { + data["direction"] = *p.Direction + } + if p.SellType != nil { + data["sellType"] = *p.SellType + } + if p.FreeTime != nil { + data["freeTime"] = *p.FreeTime + } + if p.FreeEpisode != nil { + data["freeEpisode"] = *p.FreeEpisode + } + if p.Choice != nil { + data["choice"] = *p.Choice + } + if p.ChoiceSort != nil { + data["choiceSort"] = *p.ChoiceSort + } + if p.CountComment != nil { + data["countComment"] = *p.CountComment + } + if p.CountPurchases != nil { + data["countPurchases"] = *p.CountPurchases + } + if p.CountBrowse != nil { + data["countBrowse"] = *p.CountBrowse + } + if p.CountCollect != nil { + data["countCollect"] = *p.CountCollect + } + if p.CountLike != nil { + data["countLike"] = *p.CountLike + } + if p.CountView != nil { + data["countView"] = *p.CountView + } + if p.Number != nil { + data["number"] = *p.Number + } + + if p.IsRemove != nil { + data["mId"] = primitive.ObjectID{} + data["sId"] = primitive.ObjectID{} + } + data["updatedAct"] = manager + data["updateTime"] = time.Now() + + if _, err = mediamod.UpdateByID(nil, p.ID, data); err != nil { + return err + } + if p.Status != nil && *p.Status != 1 { + // 将它从排行榜上移除 + topasist.Remove(topser.TypeMedia(media.MediaType), media.ID.Hex()) + key := fmt.Sprintf(diamondRanking, media.MediaType) + redis.Handler.ZRem(key, media.ID.Hex()) + } + return nil +} + +type WebBatchUpdateReq struct { + IDS []string `json:"ids" binding:"required"` // IDS + SID *primitive.ObjectID `json:"sId" bson:"sId,omitempty"` // 专题ID + IsActive *bool `json:"isActive" bson:"isActive,omitempty"` // 上下架 + Price *int64 `json:"price" bson:"price"` // 价格 + Permission *int `json:"permission" bson:"permission"` // 收听权限 0:会员 1:金币购买 2:免费 + Tags *[]primitive.ObjectID `json:"tags"` // 标签 + FreeEpisode *int `json:"freeEpisode" form:"freeEpisode"` // 免费集数(前几集免费) +} + +func (p *WebBatchUpdateReq) Filter() []primitive.ObjectID { + ids, _ := p.ValidatedIDs() + return ids +} + +func (p *WebBatchUpdateReq) ValidatedIDs() ([]primitive.ObjectID, error) { + if len(p.IDS) == 0 || len(p.IDS) > 100 { + return nil, errors.New("ids数量必须在1到100之间") + } + ids := make([]primitive.ObjectID, 0, len(p.IDS)) + seen := make(map[primitive.ObjectID]struct{}, len(p.IDS)) + for _, raw := range p.IDS { + id, err := primitive.ObjectIDFromHex(raw) + if err != nil || id.IsZero() { + return nil, errors.New("ids包含无效的媒体ID") + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + ids = append(ids, id) + } + return ids, nil +} + +func loadBatchMedia(ids []primitive.ObjectID) ([]*mediamod.Media, error) { + list, err := mediamod.QueryAllList(bson.M{ + "_id": bson.M{"$in": ids}, + "isDelete": false, + }, options.Find().SetLimit(int64(len(ids)))) + if err != nil { + return nil, err + } + if len(list) != len(ids) { + return nil, errors.New("部分媒体不存在或已删除") + } + return list, nil + +} + +func validateDramaBatchPublish(medias []*mediamod.Media) error { + dramaIDs := make([]primitive.ObjectID, 0, len(medias)) + dramaByID := make(map[primitive.ObjectID]mediamod.Media) + for _, media := range medias { + if media.MediaType != mediamod.MediaTypeDrama { + continue + } + dramaIDs = append(dramaIDs, media.ID) + dramaByID[media.ID] = *media + } + if len(dramaIDs) == 0 { + return nil + } + firstList, err := mediacontentmod.QueryAllList(bson.M{ + "mediaId": bson.M{"$in": dramaIDs}, + "mediaType": mediamod.MediaTypeDrama, + "episodeNumber": 1, + "isDelete": false, + }, options.Find().SetLimit(int64(len(dramaIDs)))) + if err != nil { + return err + } + firstByMediaID := make(map[primitive.ObjectID]mediacontentmod.MediaContent, len(firstList)) + for _, first := range firstList { + firstByMediaID[first.MediaID] = *first + } + for _, id := range dramaIDs { + media := dramaByID[id] + media.Status = 1 + if err := validateDramaMediaForPublish(media, firstByMediaID[id]); err != nil { + return fmt.Errorf("短剧%s:%w", media.Title, err) + } + } + return nil +} + +func (p *WebBatchUpdateReq) validateDramaBatchFields(medias []*mediamod.Media) error { + if p.Price == nil && p.Permission == nil && p.FreeEpisode == nil { + return nil + } + for _, media := range medias { + if media.MediaType == mediamod.MediaTypeDrama { + return errors.New("短剧价格、权限和免费集数必须在剧集管理中配置") + } + } + return nil +} + +// Update 更新数据 +func (p *WebBatchUpdateReq) Update(manager string) error { + ids, err := p.ValidatedIDs() + if err != nil { + return err + } + medias, err := loadBatchMedia(ids) + if err != nil { + return err + } + if err = p.validateDramaBatchFields(medias); err != nil { + return err + } + if p.IsActive != nil && *p.IsActive { + if err = validateDramaBatchPublish(medias); err != nil { + return err + } + } + nonDramaIDs := make([]primitive.ObjectID, 0, len(medias)) + for _, media := range medias { + if media.MediaType != mediamod.MediaTypeDrama { + nonDramaIDs = append(nonDramaIDs, media.ID) + } + } + data := make(map[string]interface{}) + + if p.SID != nil { + section, err := modulesectionmod.GetBySectionByID(*p.SID) + if err != nil { + return err + } + if section.ID.IsZero() { + return errors.New("section is null") + } + moduleConf, err := moduleconfmod.FindOneById(section.SubModuleID) + if err != nil { + return err + } + if moduleConf.ID.IsZero() { + return errors.New("module conf is null") + } + + data["mId"] = section.SubModuleID + data["sId"] = section.ID + data["sectionName"] = section.SectionName + data["moduleName"] = moduleConf.ModuleName + } + if p.Price != nil { + data["price"] = *p.Price + } + if p.Permission != nil { + data["permission"] = *p.Permission + } + if p.Tags != nil { + data["tags"] = *p.Tags + } + if p.FreeEpisode != nil { + data["freeEpisode"] = *p.FreeEpisode + } + data["updatedAct"] = manager + data["updateTime"] = time.Now() + + err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + if p.IsActive != nil { + // 短剧的剧集上下架由 media_content/batch/update 单独控制; + // 其他媒体保持历史整部上下架行为。 + if len(nonDramaIDs) > 0 { + filter := bson.M{"mediaId": bson.M{"$in": nonDramaIDs}} + updateData := make(map[string]interface{}) + updateData["isActive"] = *p.IsActive + updateData["updatedAct"] = manager + updateData["updateTime"] = time.Now() + _, err := mediacontentmod.UpdateByCond(t, filter, updateData) + if err != nil { + log.Error(fmt.Sprintf("mediacontentmod UpdateByCond error:%+v:", err)) + return err + } + } + if *p.IsActive { + data["status"] = 1 + data["latestPublishedAt"] = time.Now() + } else { + data["status"] = 0 + } + } + // 更新数据 + if _, err := mediamod.UpdateByIDS(t, ids, data); err != nil { + log.Error(fmt.Sprintf("mediamod UpdateByIDS error:%+v:", err)) + return err + } + return nil + }) + if err != nil { + log.Error(fmt.Sprintf("Media update Trans fail error:%+v:", err)) + return err + } + if p.IsActive != nil && *p.IsActive == false { + // 批量下架 + common.Go(func() { + for _, id := range p.IDS { + objId, _ := primitive.ObjectIDFromHex(id) + media, err := mediamod.GetInfo(objId) + if err != nil { + continue + } + // 将它从排行榜上移除 + topasist.Remove(topser.TypeMedia(media.MediaType), media.ID.Hex()) + key := fmt.Sprintf(diamondRanking, media.MediaType) + redis.Handler.ZRem(key, media.ID.Hex()) + } + }) + } + return nil +} + +type WebDeleteReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` +} + +// Delete 删除数据 +func (p *WebDeleteReq) Delete() error { + media, err := mediamod.GetInfo(p.ID) + if err != nil { + return err + } + err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + if err := mediamod.DeleteByID(t, p.ID); err != nil { + return err + } + if media.MediaType == mediamod.MediaTypeDrama { + return mediacontentmod.DeleteByMediaId(t, p.ID) + } + return nil + }) + if err != nil { + return err + } + // 将它从排行榜上移除 + topasist.Remove(topser.TypeMedia(media.MediaType), media.ID.Hex()) + key := fmt.Sprintf(diamondRanking, media.MediaType) + redis.Handler.ZRem(key, media.ID.Hex()) + return nil +} + +type WebBatchDeleteReq struct { + Ids []primitive.ObjectID `json:"ids" binding:"required"` +} + +// Delete 删除数据 +func (p *WebBatchDeleteReq) Delete() error { + if len(p.Ids) == 0 || len(p.Ids) > 100 { + return errors.New("ids数量必须在1到100之间") + } + medias := make([]mediamod.Media, 0, len(p.Ids)) + for _, id := range p.Ids { + media, err := mediamod.GetInfo(id) + if err != nil { + return err + } + medias = append(medias, media) + } + err := webg.VideoDB.Trans(func(t *db.MongoTool) error { + for _, media := range medias { + if err := mediamod.DeleteByID(t, media.ID); err != nil { + return err + } + if media.MediaType == mediamod.MediaTypeDrama { + if err := mediacontentmod.DeleteByMediaId(t, media.ID); err != nil { + return err + } + } + } + return nil + }) + if err != nil { + return err + } + for _, media := range medias { + // 将它从排行榜上移除 + topasist.Remove(topser.TypeMedia(media.MediaType), media.ID.Hex()) + key := fmt.Sprintf(diamondRanking, media.MediaType) + redis.Handler.ZRem(key, media.ID.Hex()) + } + return nil +} diff --git a/web/service/mediaser/sensitive_check.go b/web/service/mediaser/sensitive_check.go new file mode 100644 index 0000000..8bd4487 --- /dev/null +++ b/web/service/mediaser/sensitive_check.go @@ -0,0 +1,100 @@ +package mediaser + +import ( + "fmt" + "strings" + + "91porn-server/models/v/mediamod" + "91porn-server/models/v/sensitivewordmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// CheckSensitive 单条媒资编辑的上架敏感词校验,规则对齐 vid/update: +// +// 1. status 被切到上架(1) —— 上架转换 +// 2. 当前已上架(status==1) 且本次更新涉及 Title/Summary —— 已上架内容改动 +// +// 校验文本以请求字段为准(提供则用新值,否则用 DB 现值)。 +// 返回 (命中详情, true) 时调用方应拒绝本次更新;未命中或与上架无关返回 ("", false)。 +func (p *WebUpdateReq) CheckSensitive() (string, bool) { + media, err := mediamod.GetInfo(p.ID) + if err != nil { + // 查不到交给后续 Update 流程报错,这里不拦 + return "", false + } + // mediamod.Media.Status: 1 上架 / 0 下架 + const statusUp = 1 + shelvingTransition := p.Status != nil && *p.Status == statusUp + currentlyShelved := media.Status == statusUp + touchesText := p.Title != nil || p.Summary != nil + if !shelvingTransition && !(currentlyShelved && touchesText) { + return "", false + } + + title := media.Title + summary := media.Summary + if p.Title != nil { + title = *p.Title + } + if p.Summary != nil { + summary = *p.Summary + } + + terms := sensitivewordmod.LoadEnabledTerms() + if len(terms) == 0 { + return "", false + } + titleHits := sensitivewordmod.MatchHits(title, terms) + summaryHits := sensitivewordmod.MatchHits(summary, terms) + if len(titleHits) == 0 && len(summaryHits) == 0 { + return "", false + } + return formatMediaHitLine(p.ID, title, titleHits, summaryHits), true +} + +// CheckSensitive 批量媒资更新的上架敏感词校验。 +// 批量请求体不含文本字段,因此仅当 IsActive=true(批量上架)时, +// 对每个 id 的 DB 现值 Title/Summary 做命中检测。任一命中即拒绝,多条按行聚合。 +func (p *WebBatchUpdateReq) CheckSensitive() (string, bool) { + if p.IsActive == nil || !*p.IsActive { + return "", false + } + terms := sensitivewordmod.LoadEnabledTerms() + if len(terms) == 0 { + return "", false + } + lines := make([]string, 0) + for _, id := range p.Filter() { + if id.IsZero() { + continue + } + media, err := mediamod.GetInfo(id) + if err != nil { + continue + } + titleHits := sensitivewordmod.MatchHits(media.Title, terms) + summaryHits := sensitivewordmod.MatchHits(media.Summary, terms) + if len(titleHits) == 0 && len(summaryHits) == 0 { + continue + } + lines = append(lines, formatMediaHitLine(media.ID, media.Title, titleHits, summaryHits)) + } + if len(lines) == 0 { + return "", false + } + return strings.Join(lines, "\n"), true +} + +// formatMediaHitLine 媒资命中描述: +// "id:xxxx 对象标题 XXXXXX。 标题命中 XX 简介命中 YY,不予上架成功" +func formatMediaHitLine(id primitive.ObjectID, title string, titleHits, summaryHits []string) string { + parts := make([]string, 0, 2) + if len(titleHits) > 0 { + parts = append(parts, "标题命中 "+strings.Join(titleHits, "、")) + } + if len(summaryHits) > 0 { + parts = append(parts, "简介命中 "+strings.Join(summaryHits, "、")) + } + return fmt.Sprintf("id:%s 对象标题 %s。 %s,不予上架成功", id.Hex(), title, strings.Join(parts, " ")) +} diff --git a/web/service/mediatagdimensionser/mediatagdimension.go b/web/service/mediatagdimensionser/mediatagdimension.go new file mode 100755 index 0000000..18eb273 --- /dev/null +++ b/web/service/mediatagdimensionser/mediatagdimension.go @@ -0,0 +1,201 @@ +package mediatagdimensionser + +import ( + "91porn-server/common/log" + "errors" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/mongo/options" + + "91porn-server/models/cache/mediatagdimensiondata" + "91porn-server/models/v/mediatagdimensionmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + ID *string `form:"id" bson:"id,omitempty"` // ID + Name *string `form:"name" bson:"name,omitempty"` // 名称 + Type *int `form:"type" bson:"type,omitempty"` // 类型 1:动漫,2:漫画 + Status *int `form:"status" bson:"status,omitempty"` // 是否开启 + PageNumber uint64 `form:"pageNumber" json:"pageNumber" binding:"required,min=1"` // 当前页 + PageSize uint64 `form:"pageSize" json:"pageSize" binding:"required,min=1,max=100"` // 每页条数 +} + +type WebListRes struct { + Total int64 `json:"total"` + List []*mediatagdimensionmod.MediaTagDimension `json:"list"` +} + +func (q *WebListReq) Filter() bson.M { + var query = bson.M{} + if q.Name != nil { + query["name"] = q.Name + } + if q.Status != nil { + query["status"] = q.Status + } + if q.Type != nil { + query["type"] = q.Type + } + if q.ID != nil { + id, _ := primitive.ObjectIDFromHex(*q.ID) + query["_id"] = id + } + query["isDelete"] = false + return query +} +func (q *WebListReq) Options() *options.FindOptions { + return options.Find().SetSkip(int64((q.PageNumber - 1) * q.PageSize)).SetLimit(int64(q.PageSize)).SetSort(bson.M{"sortCode": -1}) +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res *WebListRes, err error) { + var out = &WebListRes{} + count, err := mediatagdimensionmod.QueryAllCount(q.Filter()) + if err != nil { + return nil, err + } + if count == 0 { + out.List = []*mediatagdimensionmod.MediaTagDimension{} + out.Total = 0 + return out, nil + } + out.Total = count + out.List, err = mediatagdimensionmod.QueryAllList(q.Filter(), q.Options()) + if err != nil { + log.Error(fmt.Sprintf("mediatagdimensionmod queryAllList error:%+v:", err)) + return nil, err + } + return out, nil +} + +type WebAllListReq struct { + Type *int `form:"type" bson:"type,omitempty"` // 类型 1:动漫,2:漫画 +} + +type WebAllListRes struct { + List []*mediatagdimensionmod.WebMediaTagDimension `json:"list"` +} + +func (q *WebAllListReq) Filter() bson.M { + var query = bson.M{} + if q.Type != nil { + query["type"] = q.Type + } + query["status"] = 1 + query["isDelete"] = false + return query +} +func (q *WebAllListReq) Options() *options.FindOptions { + return options.Find().SetSort(bson.M{"sortCode": -1}) +} + +// GetAllList 获取所有列表 +func (q *WebAllListReq) GetAllList() (res *WebAllListRes, err error) { + var out = &WebAllListRes{} + data, err := mediatagdimensionmod.QueryAllList(q.Filter(), q.Options()) + if err != nil { + log.Error(fmt.Sprintf("mediatagdimensionmod queryAllList error:%+v:", err)) + return nil, err + } + + if len(data) > 0 { + for _, d := range data { + mm := &mediatagdimensionmod.WebMediaTagDimension{ + ID: d.ID, + Name: d.Name, + Type: d.Type, + } + out.List = append(out.List, mm) + } + } + + return out, nil +} + +type WebCreateReq struct { + Status int `json:"status" form:"status" binding:"required"` // 是否开启 + Type int `json:"type" form:"type" binding:"required"` // 类型 1:动漫,2:漫画 + Name string `json:"name" form:"name" binding:"required"` // 名称 + SortCode int `json:"sortCode" form:"sortCode"` // 排序 +} + +// Create 发布数据 +func (p *WebCreateReq) Create(manager string) error { + now := time.Now() + data := mediatagdimensionmod.MediaTagDimension{ + SortCode: p.SortCode, + Status: p.Status, + Type: p.Type, + Name: p.Name, + IsDelete: false, + UpdatedAct: manager, + UpdateTime: now, + CreatedAt: now, + } + + // 创建数据 + if _, err := mediatagdimensiondata.InsertData(nil, data); err != nil { + return err + } + + return nil +} + +type WebUpdateReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` + SortCode *int `json:"sortCode"` // 排序 + Status *int `json:"status"` // 是否开启 + Type *int `json:"type"` // 类型 + Name *string `json:"name"` // 名称 +} + +// Update 更新数据 +func (p *WebUpdateReq) Update(manager string) error { + coll, err := mediatagdimensionmod.GetInfo(p.ID) + if err != nil { + return err + } + + if coll.ID.IsZero() { + return errors.New("media tag dimension is null") + } + + data := make(map[string]interface{}) + if p.SortCode != nil { + data["sortCode"] = *p.SortCode + } + if p.Status != nil { + data["status"] = *p.Status + } + if p.Type != nil { + data["type"] = *p.Type + } + if p.Name != nil { + data["name"] = *p.Name + } + data["updatedAct"] = manager + data["updateTime"] = time.Now() + + if _, err = mediatagdimensiondata.UpdateData(nil, p.ID.Hex(), data); err != nil { + return err + } + + return nil +} + +type WebDeleteReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` +} + +// Delete 删除数据 +func (p *WebDeleteReq) Delete() error { + if err := mediatagdimensiondata.DeleteData(nil, p.ID); err != nil { + return err + } + + return nil +} diff --git a/web/service/mediatagser/mediatag.go b/web/service/mediatagser/mediatag.go new file mode 100755 index 0000000..80a15ec --- /dev/null +++ b/web/service/mediatagser/mediatag.go @@ -0,0 +1,221 @@ +package mediatagser + +import ( + "91porn-server/common/log" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/mongo/options" + + "91porn-server/models/cache/mediatagdata" + "91porn-server/models/commod" + "91porn-server/models/v/mediatagmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + ID *string `form:"id" bson:"id,omitempty"` // ID + Name *string `form:"name" bson:"name,omitempty"` // 名称 + Type *int `form:"type" bson:"type,omitempty"` // 类型 + Kind *int `form:"kind" bson:"kind,omitempty"` // 种类 + Dimension *string `form:"dimension" bson:"dimension"` // 维度 + Status *int `form:"status" bson:"status,omitempty"` // 是否开启 + commod.Page +} +type WebListRes struct { + Total int64 `json:"total"` + List []*mediatagmod.MediaTag `json:"list"` +} + +func (q *WebListReq) Filter() bson.M { + var query = bson.M{} + if q.Name != nil { + query["name"] = q.Name + } + if q.Status != nil { + query["status"] = q.Status + } + if q.Kind != nil { + query["kind"] = q.Kind + } + if q.Type != nil { + query["type"] = q.Type + } + if q.Dimension != nil { + query["dimension"] = q.Dimension + } + if q.ID != nil { + id, _ := primitive.ObjectIDFromHex(*q.ID) + query["_id"] = id + } + query["isDelete"] = false + return query +} +func (q *WebListReq) Options() *options.FindOptions { + return options.Find().SetSkip(int64((q.PageNumber - 1) * q.PageSize)).SetLimit(int64(q.PageSize)).SetSort(bson.M{"sortCode": -1}) +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res *WebListRes, err error) { + var out = &WebListRes{} + count, err := mediatagmod.QueryAllCount(q.Filter()) + if err != nil { + return nil, err + } + if count == 0 { + out.List = []*mediatagmod.MediaTag{} + out.Total = 0 + return out, nil + } + out.Total = count + out.List, err = mediatagmod.QueryAllList(q.Filter(), q.Options()) + if err != nil { + log.Error(fmt.Sprintf("mediatagmod queryAllList error:%+v:", err)) + return nil, err + } + return out, nil +} + +type WebAllListReq struct { + Kind *int `form:"kind" bson:"kind,omitempty"` // 纬度分类 (1:动漫,2:漫画) + Dimension *string `form:"dimension" bson:"dimension"` // 维度名称 +} +type WebAllListRes struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 文档id + Name string `json:"name" bson:"name"` // 标签名称 +} + +func (q *WebAllListReq) Filter() bson.M { + var query = bson.M{} + if q.Kind != nil { + query["kind"] = q.Kind + } + if q.Dimension != nil { + query["dimension"] = q.Dimension + } + query["isDelete"] = false + return query +} +func (q *WebAllListReq) Options() *options.FindOptions { + return options.Find().SetSort(bson.M{"sortCode": -1}) +} + +// GetALLList 获取列表 +func (q *WebAllListReq) GetALLList() (res []*WebAllListRes, err error) { + var out []*WebAllListRes + data, err := mediatagmod.QueryAllList(q.Filter(), q.Options()) + if err != nil { + log.Error(fmt.Sprintf("mediatagmod GetALLList error:%+v:", err)) + return nil, err + } + if len(data) > 0 { + for _, d := range data { + out = append(out, &WebAllListRes{ + ID: d.ID, + Name: d.Name, + }) + } + } + return out, nil +} + +type WebCreateReq struct { + Kind int `json:"kind" form:"kind" binding:"required"` // 种类(1:哩番,2:3D,3:同人动画,4:单行本,5:连载漫画,6:韩漫) + Type int `json:"type" form:"type" binding:"required"` // 类型(1:动漫,2:漫画 ) + Dimension string `json:"dimension" form:"dimension" binding:"required"` // 维度 + Name string `json:"name" form:"name" binding:"required"` // 标签名称 + Active bool `json:"active" form:"active" binding:"required"` // 是都激活 + IsDiscovery bool `json:"isDiscovery" bson:"isDiscovery"` // 是否推荐发现 + Sort int `json:"sort" form:"sort"` // 排序码 +} + +// Create 发布数据 +func (p *WebCreateReq) Create(manager string) error { + now := time.Now() + data := mediatagmod.MediaTag{ + Kind: p.Kind, + Type: p.Type, + Dimension: p.Dimension, + Name: p.Name, + Active: p.Active, + Sort: p.Sort, + IsDiscovery: p.IsDiscovery, + IsDelete: false, + UpdatedAct: manager, + UpdateTime: now, + CreatedAt: now, + } + + // 创建数据 + if _, err := mediatagdata.InsertData(nil, data); err != nil { + return err + } + + return nil +} + +type WebUpdateReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` // ID + Kind *int `json:"kind"` // 种类(1:哩番,2:3D,3:同人动画,4:单行本,5:连载漫画,6:韩漫) + Type *int `json:"type"` // 类型(1:动漫,2:漫画 ) + Dimension *string `json:"dimension"` // 维度 + Name *string `json:"name"` // 标签名称 + Active *bool `json:"active"` // 是否激活 + IsDiscovery *bool `json:"isDiscovery" bson:"isDiscovery"` // 是否推荐发现 + Sort *int `json:"sort"` // 排序码 +} + +// Update 更新数据 +func (p *WebUpdateReq) Update(manager string) error { + _, err := mediatagmod.GetInfo(p.ID) + if err != nil { + return err + } + + data := make(map[string]interface{}) + if p.Kind != nil { + data["kind"] = *p.Kind + } + if p.Type != nil { + data["type"] = *p.Type + } + if p.Dimension != nil { + data["dimension"] = *p.Dimension + } + if p.Name != nil { + data["name"] = *p.Name + } + if p.Active != nil { + data["active"] = *p.Active + } + if p.IsDiscovery != nil { + data["isDiscovery"] = *p.IsDiscovery + } + if p.Sort != nil { + data["sort"] = *p.Sort + } + + data["updatedAct"] = manager + data["updateTime"] = time.Now() + + if _, err = mediatagdata.UpdateData(nil, p.ID.Hex(), data); err != nil { + return err + } + + return nil +} + +type WebDeleteReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` // 文档ID +} + +// Delete 删除数据 +func (p *WebDeleteReq) Delete() error { + if err := mediatagdata.DeleteData(nil, p.ID); err != nil { + return err + } + + return nil +} diff --git a/web/service/moduleser/drama_module_test.go b/web/service/moduleser/drama_module_test.go new file mode 100644 index 0000000..fd20acc --- /dev/null +++ b/web/service/moduleser/drama_module_test.go @@ -0,0 +1,21 @@ +package moduleser + +import ( + "testing" + + "91porn-server/models/v/moduleconfmod" +) + +func TestValidateDramaModuleName(t *testing.T) { + for _, name := range []string{"AI短剧", "热门短剧", "Drama Hot"} { + if err := validateDramaModuleName(moduleconfmod.Drama, name); err != nil { + t.Fatalf("valid module name %q rejected: %v", name, err) + } + } + if err := validateDramaModuleName(moduleconfmod.Drama, "短剧"); err == nil { + t.Fatal("ambiguous drama module name unexpectedly accepted") + } + if err := validateDramaModuleName(1, "任意名称"); err != nil { + t.Fatalf("non-drama module was affected: %v", err) + } +} diff --git a/web/service/moduleser/module.go b/web/service/moduleser/module.go new file mode 100644 index 0000000..45327d4 --- /dev/null +++ b/web/service/moduleser/module.go @@ -0,0 +1,355 @@ +package moduleser + +import ( + "91porn-server/common/log" + "91porn-server/models/v/tagmod" + "errors" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" + "strings" + "time" + + sli "91porn-server/common/slice" + "91porn-server/common/stderr" + "91porn-server/models/commod" + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/modulesectionmod" + "91porn-server/models/v/modulevidmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// AddModule 添加模块专题配置 +func AddModule(mconf moduleconfmod.ModuleConf) error { + if err := validateDramaModuleName(mconf.Type, mconf.ModuleName); err != nil { + return err + } + return moduleconfmod.InsertOne(&mconf) +} + +func validateDramaModuleName(moduleType int, moduleName string) error { + if moduleType != moduleconfmod.Drama { + return nil + } + name := strings.TrimSpace(moduleName) + lowerName := strings.ToLower(name) + if strings.Contains(lowerName, "ai") || strings.Contains(name, "热门") || strings.Contains(lowerName, "hot") { + return nil + } + return errors.New("短剧模块名称必须包含AI、热门或hot") +} + +func AddSection(section modulesectionmod.Section) error { + return modulesectionmod.InsertOne(section) +} + +// UpdateModule 更新模块专题配置 +func UpdateModule(editSelector moduleconfmod.EditSelector) error { + current, err := moduleconfmod.GetByID(editSelector.ID) + if err != nil { + return err + } + moduleType := current.Type + moduleName := current.ModuleName + if editSelector.Type != nil { + moduleType = *editSelector.Type + } + if editSelector.ModuleName != nil { + moduleName = *editSelector.ModuleName + } + if err = validateDramaModuleName(moduleType, moduleName); err != nil { + return err + } + return moduleconfmod.UpdateOne(editSelector) +} + +func UpdateSection(editSelector modulesectionmod.EditSelector) error { + var tagIds []primitive.ObjectID + if editSelector.Tags != nil && len(*editSelector.Tags) > 0 { + tags, err := tagmod.FindOneTagByNames(*editSelector.Tags) + if err != nil { + return err + } + if len(tags) <= 0 { + return errors.New("tag is null") + } + for _, t := range tags { + tagIds = append(tagIds, t.ID) + } + } + editSelector.TagIds = tagIds + return modulesectionmod.UpdateOne(editSelector) +} + +func DeleteSection(id primitive.ObjectID) error { + return modulesectionmod.DeleteById(id) +} + +// GetModuleList 获取专题下视频列表 +func GetModuleList(query modulevidmod.QuerySelector, page commod.Page) (resp ModuleVideoList, err error) { + list, hasNext, total, err := modulevidmod.Search(&query, page) + if err != nil { + return + } + listLen := len(list) + sectionIDs := make([]primitive.ObjectID, listLen) + videoIDs := make([]primitive.ObjectID, listLen) + for i, sec := range list { + sectionIDs[i] = sec.SectionID + videoIDs[i] = sec.VideoID + } + sections, err := modulesectionmod.GetByIDs(sli.RemoveRepObjectID(sectionIDs)) + if err != nil { + return + } + sectionModuleMap := make(map[primitive.ObjectID]modulesectionmod.SectionModule) + for _, v := range sections { + sectionModuleMap[v.ID] = v + } + videos, err := vidmod.GetVideoListByIDsNoStatus(videoIDs) + if err != nil { + return + } + videoMap := make(map[primitive.ObjectID]vidmod.VideoModel) + for _, video := range videos { + videoMap[video.ID] = *video + } + resp.List = make([]ModuleVideo, listLen) + for i, sec := range list { + secVideo := ModuleVideo{ + ID: sec.ID, + SectionID: sec.SectionID, + CreatedAt: sec.CreatedAt, + SortCode: sec.SortCode, + } + if module, ok := sectionModuleMap[sec.SectionID]; ok { + secVideo.SectionName = module.SectionName + if len(module.SectionModule) > 0 { + secVideo.ModuleName = module.SectionModule[0].ModuleName + secVideo.SubModuleName = module.SectionModule[0].SubModuleName + } + } + secVideo.VideoInfo = VideoInfo{ + VideoID: videoMap[sec.VideoID].ID, + Title: videoMap[sec.VideoID].Title, + PublisherID: videoMap[sec.VideoID].PublisherID, + Cover: videoMap[sec.VideoID].Cover, + } + resp.List[i] = secVideo + } + resp.Total = total + resp.HasNext = hasNext + return +} + +// AddVideoBatch 专题下批量添加视频 +func AddVideoBatch(req AddVideoBatchReq) error { + section, err := modulesectionmod.GetBySectionByID(req.SectionID) + if err != nil { + return stderr.ErrDbQueryError + } + + if section.ID.IsZero() { + return stderr.CodeEmptyData + } + mId := section.SubModuleID + + videoIdStrs := strings.Split(req.VideoIDs, ",") + vids := make([]primitive.ObjectID, len(videoIdStrs)) + for i, vidIDStr := range videoIdStrs { + videoID, err := primitive.ObjectIDFromHex(vidIDStr) + if err != nil { + return stderr.ErrParamError + } + vids[i] = videoID + } + list, err := vidmod.GetVideoListByIDs(vids) + if err != nil { + return stderr.ErrNetWorkBusy + } + records := make([]modulevidmod.SectionVideo, len(list)) + var vIds []primitive.ObjectID + var ids []primitive.ObjectID + for i := range list { + records[i] = modulevidmod.SectionVideo{ + SectionID: req.SectionID, + VideoID: list[i].ID, + VideoReviewedAt: list[i].ReviewAt, + } + vIds = append(vIds, list[i].ID) + } + sectionVideos, err := modulevidmod.GetSectionsByVideoIds(vIds) + if err != nil { + return err + } + + if len(sectionVideos) > 0 { + for i := range sectionVideos { + ids = append(ids, sectionVideos[i].ID) + } + } + if len(ids) > 0 { + err := modulevidmod.DeleteManyVideo(ids) + if err != nil { + return err + } + } + + // 批量更新视频信息 + update := bson.M{} + update["mId"] = mId.Hex() + update["updatedAt"] = time.Now() + + result, err := vidmod.UpdateManyVideo(vIds, update) + if err != nil { + return err + } + if result == 0 { + return errors.New("video update failed") + } + + return modulevidmod.InsertMany(records) +} + +// AddVideo 专题下添加一条视频 +func AddVideo(sectionVid modulevidmod.SectionVideo) error { + videoID := sectionVid.VideoID + video, err := vidmod.GetVideoInfo(videoID.Hex()) + if err != nil { + return err + } + sectionVid.VideoReviewedAt = video.ReviewAt + sectionVid.NewsType = video.NewsType + return modulevidmod.InsertOne(§ionVid) +} + +// AllSections 获取所有专题 +func AllSections() (data []modulesectionmod.ModuleSection, err error) { + modules, err := moduleconfmod.GetAllModule() + if err != nil { + return + } + sections, err := modulesectionmod.AllSections() + if err != nil { + return + } + sectionMap := make(map[primitive.ObjectID][]modulesectionmod.Section) + for _, s := range sections { + sectionMap[s.SubModuleID] = append(sectionMap[s.SubModuleID], s) + } + data = make([]modulesectionmod.ModuleSection, len(modules)) + for i, m := range modules { + data[i] = modulesectionmod.ModuleSection{ + ModuleConf: m, + Sections: sectionMap[m.ID], + } + } + return +} + +func SectionSearch(q modulesectionmod.QuerySelector, p commod.Page) (resp modulesectionmod.ListResp, err error) { + sections, hasNext, total, err := modulesectionmod.Search(q, p) + if err != nil { + return + } + resp.HasNext = hasNext + resp.Total = total + sectionsLen := len(sections) + originalUserIDs := make([]uint64, 0, sectionsLen) + subModuleIDs := make([]primitive.ObjectID, 0, sectionsLen) + for _, section := range sections { + if section.OriginalUserID != nil && *section.OriginalUserID != 0 { + originalUserIDs = append(originalUserIDs, *section.OriginalUserID) + } + if !section.SubModuleID.IsZero() { + subModuleIDs = append(subModuleIDs, section.SubModuleID) + } + } + users, err := usermod.FindUsersByUID(originalUserIDs) + if err != nil { + return + } + subModules, err := moduleconfmod.FindByIDs(subModuleIDs) + if err != nil { + return + } + originalBloggerInfo := make(map[uint64]usermod.User) + subModuleInfo := make(map[primitive.ObjectID]moduleconfmod.ModuleConf) + for _, user := range users { + originalBloggerInfo[user.UID] = *user + } + for _, module := range subModules { + subModuleInfo[module.ID] = module + } + resp.List = make([]modulesectionmod.SectionDetail, sectionsLen) + for i, section := range sections { + detail := modulesectionmod.SectionDetail{ + Section: section, + } + if section.OriginalUserID != nil && *section.OriginalUserID != 0 { + detail.OriginalBloggerInfo = modulesectionmod.OriginalBloggerInfo{ + Name: originalBloggerInfo[*section.OriginalUserID].Name, + Portrait: originalBloggerInfo[*section.OriginalUserID].Portrait, + OfficialCert: originalBloggerInfo[*section.OriginalUserID].OfficialCert, + } + } + if !section.SubModuleID.IsZero() { + detail.ModuleInfo = modulesectionmod.ModuleInfo{ + ModuleName: subModuleInfo[section.SubModuleID].ModuleName, + SubModuleName: subModuleInfo[section.SubModuleID].SubModuleName, + SectionLimit: subModuleInfo[detail.SubModuleID].SectionLimit, + } + } + resp.List[i] = detail + } + return +} + +// QueryAllCartoonList 查询动漫模块专题配置 +func QueryAllCartoonList() ([]*modulesectionmod.AllSectionConf, error) { + var out []*modulesectionmod.AllSectionConf + filter := bson.M{"type": bson.M{"$in": []int{moduleconfmod.Novel, moduleconfmod.Comics, moduleconfmod.Cartoon}}, "status": 1, "deletedAt": bson.M{"$exists": false}} + op := options.Find().SetSort(bson.M{"sortNum": -1}) + list, err := moduleconfmod.QueryAllList(filter, op) + if err != nil { + log.Error(fmt.Sprintf("moduleconfmod QueryAllList error:%+v:", err)) + return nil, err + } + var subIds []primitive.ObjectID + if len(list) > 0 { + for _, m := range list { + subIds = append(subIds, m.ID) + } + } + sf := bson.M{"subModuleID": bson.M{"$in": subIds}, "status": 1, "deletedAt": bson.M{"$exists": false}} + sop := options.Find().SetSort(bson.M{"sort": -1}) + sections, err := modulesectionmod.QueryAllList(sf, sop) + if err != nil { + log.Error(fmt.Sprintf("modulesectionmod QueryAllList error:%+v:", err)) + return nil, err + } + if len(sections) > 0 { + for _, m := range list { + mes := &modulesectionmod.AllSectionConf{ + ID: m.ID, + ModuleName: m.ModuleName, + } + var allSection []modulesectionmod.SectionConf + for _, s := range sections { + if s.SubModuleID.Hex() == m.ID.Hex() { + nes := modulesectionmod.SectionConf{ + ID: s.ID, + SectionName: s.SectionName, + } + allSection = append(allSection, nes) + } + } + mes.AllSection = allSection + out = append(out, mes) + } + } + return out, nil +} diff --git a/web/service/moduleser/type.go b/web/service/moduleser/type.go new file mode 100644 index 0000000..18e6b41 --- /dev/null +++ b/web/service/moduleser/type.go @@ -0,0 +1,36 @@ +package moduleser + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ModuleVideoList struct { + HasNext bool `json:"hasNext"` // 是否还有下一页 + List []ModuleVideo `json:"list"` // 列表 + Total int64 `json:"total"` // 总数 +} + +type ModuleVideo struct { + ID primitive.ObjectID `json:"id"` // ID + SectionID primitive.ObjectID `json:"sectionID"` // 专题id + SectionName string `json:"sectionName"` // 专题名称 + ModuleName string `json:"moduleName"` // 模块名称 + SubModuleName string `json:"subModuleName"` // 亚模块名称 + SortCode int `json:"sortCode"` // 专题内排序 + CreatedAt time.Time `json:"createdAt"` // 创建时间 + VideoInfo // 视频信息 +} + +type VideoInfo struct { + VideoID primitive.ObjectID `json:"videoID"` // 视频ID + Title string `json:"title"` // 标题 + PublisherID uint64 `json:"publisherID"` // 发布用户ID + Cover string `json:"cover"` // 视频封面图 +} + +type AddVideoBatchReq struct { + SectionID primitive.ObjectID `json:"sectionID" bson:"sectionID" binding:"required"` // 专题id + VideoIDs string `json:"videoIDs" binding:"required"` // 视频id列表,各个id以','分隔 +} diff --git a/web/service/nakedchatorderser/nakedchatorder.go b/web/service/nakedchatorderser/nakedchatorder.go new file mode 100755 index 0000000..e8ef438 --- /dev/null +++ b/web/service/nakedchatorderser/nakedchatorder.go @@ -0,0 +1,68 @@ +package nakedchatorderser + +import ( + "time" + + "91porn-server/models/cache/nakedchatorderdata" + "91porn-server/models/commod" + "91porn-server/models/v/nakedchatordermod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + Uid *uint64 `json:"uid" form:"uid"` // 用户id + OrderId *string `json:"orderId" form:"orderId"` // 订单id + Nid *string `json:"nid" bson:"nid"` // 裸聊id + + commod.Page +} +type WebListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []nakedchatordermod.NakedChatOrder `json:"list"` +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res WebListRes, err error) { + filter := bson.M{} + if q.OrderId != nil { + orderId, _ := primitive.ObjectIDFromHex(*q.OrderId) + filter["_id"] = orderId + } + if q.Uid != nil { + filter["uid"] = *q.Uid + } + if q.Nid != nil { + nid, _ := primitive.ObjectIDFromHex(*q.Nid) + filter["nid"] = nid + } + res.List, res.Total, res.HasNext, err = nakedchatordermod.GetList(filter, int64(q.Skip()), int64(q.Limit()), q.GetSort()) + + return +} + +type WebUpdateReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` + Status *int `json:"status"` // 0-已下单 1-已完成 +} + +// Update 更新数据 +func (p *WebUpdateReq) Update() error { + _, err := nakedchatordermod.GetInfo(p.ID) + if err != nil { + return err + } + + data := make(map[string]interface{}) + if p.Status != nil { + data["status"] = *p.Status + } + data["updatedAt"] = time.Now() + + if _, err = nakedchatorderdata.UpdateData(nil, p.ID.Hex(), data); err != nil { + return err + } + + return nil +} diff --git a/web/service/nakedchatser/nakedchat.go b/web/service/nakedchatser/nakedchat.go new file mode 100755 index 0000000..700560f --- /dev/null +++ b/web/service/nakedchatser/nakedchat.go @@ -0,0 +1,201 @@ +package nakedchatser + +import ( + "time" + + "91porn-server/models/cache/nakedchatdata" + "91porn-server/models/commod" + "91porn-server/models/v/nakedchatmod" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + Title *string `json:"title" form:"title"` + Status *int `json:"status" form:"status"` // 0-下架 1-上架 + Mid *string `json:"mid" form:"mid"` // 模块id + commod.Page +} +type WebListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []nakedchatmod.NakedChat `json:"list"` +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res WebListRes, err error) { + filter := bson.M{} + if q.Title != nil { + filter["title"] = bson.M{ + "$regex": *q.Title, + } + } + if q.Status != nil { + filter["status"] = *q.Status + } + if q.Mid != nil { + mid, _ := primitive.ObjectIDFromHex(*q.Mid) + filter["mid"] = mid + } + res.List, res.Total, res.HasNext, err = nakedchatmod.GetList(filter, int64(q.Skip()), int64(q.Limit()), q.GetSort()) + + return +} + +type WebCreateReq struct { + Title string `json:"title" form:"title" binding:"required"` // 标题 + Cover string `json:"cover" form:"cover" binding:"required"` // 封面 + Price uint64 `json:"price" form:"price" binding:"required"` // 价格 n金币/分钟 + Options []uint64 `json:"options" form:"options" binding:"required"` // 可供购买选项 ,多少分钟 + Images []string `json:"images" form:"images"` // 图片列表 + Video string `json:"video" form:"video"` // 展示的视频 + Contact string `json:"contact" form:"contact" binding:"required"` // 联系方式 + Age int `json:"age" form:"age" binding:"required"` // 年龄 单位 岁 + Weight int `json:"weight" form:"weight" binding:"required"` // 体重 单位 kg + Height int `json:"height" form:"height" binding:"required"` // 身高 单位 cm + Cup string `json:"cup" form:"cup" binding:"required"` // 罩杯 + FakeSaleNum int64 `json:"fakeSaleNum" form:"fakeSaleNum" binding:"required"` // 销售数量(假) + BusinessHours string `json:"businessHours" form:"businessHours" binding:"required"` // 连线时间 + Summary string `json:"summary" form:"summary"` // 简介 + Status int `json:"status" form:"status"` // 0-下架 1-上架 + SortCode int64 `json:"sortCode" bson:"sortCode"` // 排序号 + +} + +// Create 发布数据 +func (p *WebCreateReq) Create() error { + data := nakedchatmod.NakedChat{ + Title: p.Title, + Cover: p.Cover, + Price: p.Price, + Options: p.Options, + Images: p.Images, + Video: p.Video, + Contact: p.Contact, + Age: p.Age, + Weight: p.Weight, + Height: p.Height, + Cup: p.Cup, + FakeSaleNum: p.FakeSaleNum, + BusinessHours: p.BusinessHours, + Summary: p.Summary, + Status: p.Status, + SortCode: p.SortCode, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // 创建数据 + if _, err := nakedchatdata.InsertData(nil, data); err != nil { + return err + } + + return nil +} + +type WebUpdateReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` + Title *string `json:"title"` // 标题 + Cover *string `json:"cover"` // 封面 + Price *int64 `json:"price"` // 价格 n金币/分钟 + Options *[]int64 `json:"options"` // 可供购买选项 ,多少分钟 + Images *[]string `json:"images"` // 图片列表 + Video *string `json:"video"` // 展示的视频 + Contact *string `json:"contact"` // 联系方式 + Age *int `json:"age"` // 年龄 单位 岁 + Weight *int `json:"weight"` // 体重 单位 kg + Height *int `json:"height"` // 身高 单位 cm + Cup *string `json:"cup"` // 罩杯 + FakeSaleNum *int64 `json:"fakeSaleNum"` // 销售数量(假) + BusinessHours *string `json:"businessHours"` // 连线时间 + Summary *string `json:"summary"` // 简介 + Status *int `json:"status" bson:"status"` // 0-下架 1-上架 + SortCode *int64 `json:"sortCode" bson:"sortCode"` // 排序号 + +} + +// Update 更新数据 +func (p *WebUpdateReq) Update() error { + _, err := nakedchatmod.GetInfo(p.ID) + if err != nil { + return err + } + + data := make(map[string]interface{}) + if p.Title != nil { + data["title"] = *p.Title + } + if p.Cover != nil { + data["cover"] = *p.Cover + } + if p.Price != nil { + data["price"] = *p.Price + } + if p.Options != nil { + data["options"] = *p.Options + } + if p.Images != nil { + data["images"] = *p.Images + } + if p.Video != nil { + data["video"] = *p.Video + } + if p.Contact != nil { + data["contact"] = *p.Contact + } + if p.Age != nil { + data["age"] = *p.Age + } + if p.Weight != nil { + data["weight"] = *p.Weight + } + if p.Height != nil { + data["height"] = *p.Height + } + if p.Cup != nil { + data["cup"] = *p.Cup + } + if p.FakeSaleNum != nil { + data["fakeSaleNum"] = *p.FakeSaleNum + } + if p.BusinessHours != nil { + data["businessHours"] = *p.BusinessHours + } + if p.Summary != nil { + data["summary"] = *p.Summary + } + if p.Status != nil { + data["status"] = *p.Status + } + if p.SortCode != nil { + data["sortCode"] = *p.SortCode + } + data["updatedAt"] = time.Now() + + if _, err = nakedchatdata.UpdateData(nil, p.ID.Hex(), data); err != nil { + return err + } + + return nil +} + +type WebBatchUpdateReq struct { + Ids []primitive.ObjectID `json:"ids" binding:"required"` + Mid *primitive.ObjectID `json:"mid" form:"mid"` // 模块id + +} + +// Update 更新数据 +func (p *WebBatchUpdateReq) Update() error { + data := make(map[string]interface{}) + if p.Mid != nil { + data["mid"] = *p.Mid + } + data["updatedAt"] = time.Now() + for _, id := range p.Ids { + if _, err := nakedchatdata.UpdateData(nil, id.Hex(), data); err != nil { + return err + } + } + return nil +} diff --git a/web/service/officialWebsiteser/album.go b/web/service/officialWebsiteser/album.go new file mode 100644 index 0000000..62dfbf8 --- /dev/null +++ b/web/service/officialWebsiteser/album.go @@ -0,0 +1,284 @@ +package officialwebsiteser + +import ( + "time" + + "91porn-server/common/stderr" + officialwebsitemod "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ───────────────────────────────────────── +// Service functions +// ───────────────────────────────────────── + +func ListAlbum(req *AlbumListReq) (int64, []officialwebsitemod.Album, error) { + filter, err := req.Query() + if err != nil { + return 0, nil, err + } + mod := &officialwebsitemod.Album{} + total, err := mod.Count(filter) + if err != nil { + return 0, nil, err + } + list, err := mod.FindMany(filter, req.FindOptions()) + if err != nil { + return 0, nil, err + } + return total, list, nil +} + +// resolveAlbumSeoSlug 处理 seoSlug:手填则校验格式并查重(冲突报错);为空则由标题生成雏形并追加后缀去重。 +func resolveAlbumSeoSlug(provided, title string, excludeID primitive.ObjectID) (string, error) { + return ResolveSeoSlug(provided, title, func(slug string) (bool, error) { + filter := officialwebsitemod.M{"seoSlug": slug} + if !excludeID.IsZero() { + filter["_id"] = officialwebsitemod.M{"$ne": excludeID} + } + n, err := (&officialwebsitemod.Album{}).Count(filter) + return n > 0, err + }) +} + +func CreateAlbum(req *ModifyAlbumReq) (officialwebsitemod.Album, error) { + data, err := req.toMod() + if err != nil { + return officialwebsitemod.Album{}, err + } + if data.SeoSlug, err = resolveAlbumSeoSlug(data.SeoSlug, data.Title, primitive.NilObjectID); err != nil { + return officialwebsitemod.Album{}, err + } + if err := data.Create(); err != nil { + return officialwebsitemod.Album{}, err + } + return data, nil +} + +func UpdateAlbum(req *ModifyAlbumReq) (resp CountResp, err error) { + if req == nil || req.ID == "" { + return + } + id, parseErr := primitive.ObjectIDFromHex(req.ID) + if parseErr != nil || isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.Album{} + err = mod.FindOne(officialwebsitemod.M{"_id": id}) + if err != nil { + return + } + if isNilObjectID(mod.ID) { + err = stderr.ErrParamError + return + } + // 若提交了 seoSlug,先校验/去重(排除自身),回写后由 toM 落库 + if req.SeoSlug != nil { + var slug string + if slug, err = resolveAlbumSeoSlug(*req.SeoSlug, mod.Title, id); err != nil { + return + } + *req.SeoSlug = slug + } + var update officialwebsitemod.M + if update, err = req.toM(); err != nil || update == nil { + return + } + resp.Count, err = mod.Update(officialwebsitemod.M{"_id": id}, update) + return +} + +func DeleteAlbum(req *DeleteAlbumReq) (data DeleteAlbumResp, err error) { + if req == nil { + err = stderr.CodeEmptyData + return + } + var id primitive.ObjectID + if id, err = primitive.ObjectIDFromHex(req.ID); err != nil { + return + } + if isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.Album{} + data.Count, err = mod.Delete(officialwebsitemod.M{"_id": id}) + return +} + +// ───────────────────────────────────────── +// Mutation (Modify) types +// ───────────────────────────────────────── + +type ModifyAlbumReq struct { + ID string `json:"id" form:"id"` + HeroID *string `json:"heroID" form:"heroID"` + Title *string `json:"title"` + SeoSlug *string `json:"seoSlug"` + Description *string `json:"description"` + Cover *string `json:"cover"` + IsHot *bool `json:"isHot"` + Tags *[]officialwebsitemod.Tag `json:"tags"` + Sort *int64 `json:"sort"` + IsActive *bool `json:"isActive"` +} + +// ───────────────────────────────────────── +// Delete types +// ───────────────────────────────────────── + +type DeleteAlbumReq struct { + ID string `json:"id" form:"id"` +} + +type DeleteAlbumResp struct { + Count int64 `json:"count"` +} + +// ───────────────────────────────────────── +// toMod — ModifyAlbumReq → model struct +// ───────────────────────────────────────── + +func (req *ModifyAlbumReq) toMod() (officialwebsitemod.Album, error) { + if req == nil { + return officialwebsitemod.Album{}, nil + } + data := officialwebsitemod.Album{ + Title: stringOrZero(req.Title), + Description: stringOrZero(req.Description), + Cover: stringOrZero(req.Cover), + IsHot: boolOrZero(req.IsHot), + SortModel: officialwebsitemod.SortModel{ + Sort: int64OrZero(req.Sort), + IsActive: boolOrZero(req.IsActive), + }, + } + if req.HeroID != nil { + if *req.HeroID != "" { + heroID, err := primitive.ObjectIDFromHex(*req.HeroID) + if err != nil || isNilObjectID(heroID) { + return officialwebsitemod.Album{}, stderr.ErrParamError + } + data.HeroID = heroID + } + } + if req.Tags != nil { + data.Tags = *req.Tags + } + if req.SeoSlug != nil { + data.SeoSlug = *req.SeoSlug + } + return data, nil +} + +// ───────────────────────────────────────── +// toM — ModifyAlbumReq → officialwebsitemod.M +// ───────────────────────────────────────── + +func (req *ModifyAlbumReq) toM() (officialwebsitemod.M, error) { + if req == nil { + return nil, stderr.CodeEmptyData + } + set := officialwebsitemod.M{} + if req.HeroID != nil { + if *req.HeroID != "" { + heroID, err := primitive.ObjectIDFromHex(*req.HeroID) + if err != nil || isNilObjectID(heroID) { + return nil, stderr.ErrParamError + } + set["heroID"] = heroID + } + } + if req.Title != nil { + set["title"] = *req.Title + } + if req.SeoSlug != nil && *req.SeoSlug != "" { + set["seoSlug"] = *req.SeoSlug + } + if req.Description != nil { + set["description"] = *req.Description + } + if req.Cover != nil { + set["cover"] = *req.Cover + } + if req.IsHot != nil { + set["isHot"] = *req.IsHot + } + if req.Tags != nil { + set["tags"] = *req.Tags + } + if req.Sort != nil { + set["sort"] = *req.Sort + } + if req.IsActive != nil { + set["isActive"] = *req.IsActive + } + if len(set) == 0 { + return nil, nil + } + set["updatedAt"] = time.Now().UTC() + return officialwebsitemod.M{"$set": set}, nil +} + +// ───────────────────────────────────────── +// List types +// ───────────────────────────────────────── + +type AlbumListReq struct { + PageReq + ID *string `form:"id"` + HeroID *string `form:"heroID"` + Title *string `form:"title"` + Description *string `form:"description"` + Cover *string `form:"cover"` + IsHot *bool `form:"isHot"` + Tags *string `form:"tags"` + Sort *int64 `form:"sort"` + IsActive *bool `form:"isActive"` + CreatedAt *time.Time `form:"createdAt"` + UpdatedAt *time.Time `form:"updatedAt"` + DeletedAt *time.Time `form:"deletedAt"` +} + +func (r *AlbumListReq) Query() (officialwebsitemod.M, error) { + filter := officialwebsitemod.M{} + if err := parseObjectIDFilter(filter, "_id", r.ID); err != nil { + return nil, err + } + if err := parseObjectIDFilter(filter, "heroID", r.HeroID); err != nil { + return nil, err + } + if r.Title != nil { + filter["title"] = *r.Title + } + if r.Description != nil { + filter["description"] = *r.Description + } + if r.Cover != nil { + filter["cover"] = *r.Cover + } + if r.IsHot != nil { + filter["isHot"] = *r.IsHot + } + if err := parseJSONFilter(filter, "tags", r.Tags, &[]officialwebsitemod.Tag{}); err != nil { + return nil, err + } + if r.Sort != nil { + filter["sort"] = *r.Sort + } + if r.IsActive != nil { + filter["isActive"] = *r.IsActive + } + if r.CreatedAt != nil { + filter["createdAt"] = *r.CreatedAt + } + if r.UpdatedAt != nil { + filter["updatedAt"] = *r.UpdatedAt + } + if r.DeletedAt != nil { + filter["deletedAt"] = *r.DeletedAt + } + return filter, nil +} diff --git a/web/service/officialWebsiteser/backfill.go b/web/service/officialWebsiteser/backfill.go new file mode 100644 index 0000000..8c3d19a --- /dev/null +++ b/web/service/officialWebsiteser/backfill.go @@ -0,0 +1,141 @@ +package officialwebsiteser + +import ( + "91porn-server/common/log" + officialwebsitemod "91porn-server/models/v/officialWebsitemod" + + "github.com/robfig/cron/v3" +) + +var seoSlugBackfillCron *cron.Cron + +// StartSeoSlugBackfillCron 启动定时任务:扫描官网各表(Video/Hero/Album/News/Photograph) +// 中缺失 seoSlug 的记录,按标题自动生成并回填。 +// 启动后异步先跑一次(补存量),之后每小时扫一次(兜底导入/批量遗漏的记录)。 +func StartSeoSlugBackfillCron() { + go backfillAllSeoSlug() // 首次异步执行,避免阻塞服务启动 + seoSlugBackfillCron = cron.New() + if _, err := seoSlugBackfillCron.AddFunc("@every 1h", backfillAllSeoSlug); err != nil { + log.Error("StartSeoSlugBackfillCron AddFunc fail", log.E(err)) + return + } + seoSlugBackfillCron.Start() +} + +func backfillAllSeoSlug() { + backfillVideoSeoSlug() + backfillHeroSeoSlug() + backfillAlbumSeoSlug() + backfillNewsSeoSlug() + backfillPhotographSeoSlug() +} + +// seoSlugMissingFilter 匹配 seoSlug 字段缺失或为空的记录。 +func seoSlugMissingFilter() officialwebsitemod.M { + return officialwebsitemod.M{"$or": []officialwebsitemod.M{ + {"seoSlug": officialwebsitemod.M{"$exists": false}}, + {"seoSlug": ""}, + }} +} + +// setSeoSlug 回填单条记录的 seoSlug。 +func setSeoSlug(update func(officialwebsitemod.M, officialwebsitemod.M) (int64, error), id interface{}, slug string) { + if _, err := update(officialwebsitemod.M{"_id": id}, officialwebsitemod.M{"$set": officialwebsitemod.M{"seoSlug": slug}}); err != nil { + log.Error("backfill seoSlug update fail", log.E(err), log.Any("id", id)) + } +} + +func backfillVideoSeoSlug() { + mod := &officialwebsitemod.Video{} + list, err := mod.FindMany(seoSlugMissingFilter()) + if err != nil { + log.Error("backfill video seoSlug find fail", log.E(err)) + return + } + for i := range list { + if list[i].Title == "" { + continue + } + slug, e := resolveVideoSeoSlug("", list[i].Title, list[i].ID) + if e != nil || slug == "" { + continue + } + setSeoSlug(mod.Update, list[i].ID, slug) + } +} + +func backfillHeroSeoSlug() { + mod := &officialwebsitemod.Hero{} + list, err := mod.FindMany(seoSlugMissingFilter()) + if err != nil { + log.Error("backfill hero seoSlug find fail", log.E(err)) + return + } + for i := range list { + if list[i].Name == "" { + continue + } + slug, e := resolveHeroSeoSlug("", list[i].Name, list[i].ID) + if e != nil || slug == "" { + continue + } + setSeoSlug(mod.Update, list[i].ID, slug) + } +} + +func backfillAlbumSeoSlug() { + mod := &officialwebsitemod.Album{} + list, err := mod.FindMany(seoSlugMissingFilter()) + if err != nil { + log.Error("backfill album seoSlug find fail", log.E(err)) + return + } + for i := range list { + if list[i].Title == "" { + continue + } + slug, e := resolveAlbumSeoSlug("", list[i].Title, list[i].ID) + if e != nil || slug == "" { + continue + } + setSeoSlug(mod.Update, list[i].ID, slug) + } +} + +func backfillNewsSeoSlug() { + mod := &officialwebsitemod.News{} + list, err := mod.FindMany(seoSlugMissingFilter()) + if err != nil { + log.Error("backfill news seoSlug find fail", log.E(err)) + return + } + for i := range list { + if list[i].Title == "" { + continue + } + slug, e := resolveNewsSeoSlug("", list[i].Title, list[i].ID) + if e != nil || slug == "" { + continue + } + setSeoSlug(mod.Update, list[i].ID, slug) + } +} + +func backfillPhotographSeoSlug() { + mod := &officialwebsitemod.Photograph{} + list, err := mod.FindMany(seoSlugMissingFilter()) + if err != nil { + log.Error("backfill photograph seoSlug find fail", log.E(err)) + return + } + for i := range list { + if list[i].Name == "" { + continue + } + slug, e := resolvePhotographSeoSlug("", list[i].Name, list[i].ID) + if e != nil || slug == "" { + continue + } + setSeoSlug(mod.Update, list[i].ID, slug) + } +} diff --git a/web/service/officialWebsiteser/basic.go b/web/service/officialWebsiteser/basic.go new file mode 100644 index 0000000..bd0b077 --- /dev/null +++ b/web/service/officialWebsiteser/basic.go @@ -0,0 +1,779 @@ +package officialwebsiteser + +import ( + "91porn-server/common" + "91porn-server/common/cachev2" + "91porn-server/common/constant/redisconst" + "time" + + "91porn-server/common/stderr" + officialwebsitemod "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ───────────────────────────────────────── +// Service functions +// ───────────────────────────────────────── + +func GetBasicData() (data GetBasicDataResp, err error) { + mod := &officialwebsitemod.BasicData{} + if err = mod.FindOne(); err != nil { + return + } + err = data.transfer(mod) + return +} + +func CreateBasicData(req *ModifyBasicDataReq) (data officialwebsitemod.BasicData, err error) { + data, err = req.toMod() + if err != nil { + return + } + if err = data.Create(); err != nil { + return officialwebsitemod.BasicData{}, err + } + return +} + +func UpdateBasicData(req *ModifyBasicDataReq) (resp UpdateBasicDataResp, err error) { + if req == nil || req.ID == "" { + return + } + id, parseErr := primitive.ObjectIDFromHex(req.ID) + if parseErr != nil || isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.BasicData{} + if err = mod.FindOne(); err != nil { + return + } + var update officialwebsitemod.M + if update, err = req.toM(); err != nil || update == nil { + return + } + resp.Count, err = mod.Update(officialwebsitemod.M{"_id": id}, update) + + common.Go(func() { + // 删除缓存 + cachev2.Classes(). + CacheTime(redisconst.OfficialWebsiteBasicDataCacheExpire). + AutoListKey(redisconst.OfficialWebsiteBasicDataCacheKey). + DeleteCurrent() + }) + + return +} + +func DeleteBasicData(req *DeleteBasicDataReq) (data DeleteBasicDataResp, err error) { + if req == nil { + err = stderr.CodeEmptyData + return + } + var id primitive.ObjectID + if id, err = primitive.ObjectIDFromHex(req.ID); err != nil { + return + } + if isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.BasicData{} + data.Count, err = mod.Delete(officialwebsitemod.M{"_id": id}) + return +} + +// ───────────────────────────────────────── +// Query (BasicData) types +// ───────────────────────────────────────── + +type GetBasicDataReq struct{} + +type GetBasicDataResp struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` + Description string `json:"description" bson:"description"` + Banner []GBDBanner `json:"banner" bson:"banner"` + HeroBanner []GBDHeroBanner `json:"heroBanner" bson:"heroBanner"` + Masterpiece []GBDMasterpiece `json:"masterpiece" bson:"masterpiece"` + Business []GBDBusiness `json:"business" bson:"business"` + BannersDescription string `json:"bannersDescription" bson:"bannersDescription"` + BannersDuration int `json:"bannersDuration" bson:"bannersDuration"` + AboutUs GBDAboutUs `json:"aboutUs" bson:"aboutUs"` + FAQ []GBDFAQ `json:"faq" bson:"faq"` + HomePageCMS []GBDCMSData `json:"homePageCMS" bson:"homePageCMS"` + HomePageCMSDescription string `json:"homePageCMSDescription" bson:"homePageCMSDescription"` + RecruitCMS []GBDCMSData `json:"recruitCMS" bson:"recruitCMS"` + Tags []officialwebsitemod.Tag `json:"tags" bson:"tags"` +} + +type GBDBanner struct { + Name string `json:"name" bson:"name"` + Description string `json:"description" bson:"description"` + Image string `json:"image" bson:"image"` + PhoneImage string `json:"phoneImage" bson:"phoneImage"` + Thumbnail string `json:"thumbnail" bson:"thumbnail"` + URL string `json:"url" bson:"url"` +} + +type GBDHeroBanner struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + HeroID string `json:"heroId" bson:"heroId"` + Name string `json:"name" bson:"name"` + Description string `json:"description" bson:"description"` + Image string `json:"image" bson:"image"` + URL string `json:"url" bson:"url"` + Duration int `json:"duration" bson:"duration"` +} + +type GBDMasterpiece struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Description string `json:"description" bson:"description"` + Cover string `json:"cover" bson:"cover"` + Type string `json:"type" bson:"type"` +} + +type GBDBusiness struct { + Name string `json:"name" bson:"name"` + EnglishName string `json:"englishName" bson:"englishName"` + Description string `json:"description" bson:"description"` + Cover string `json:"cover" bson:"cover"` + Detail []GBDBusinessDetail `json:"detail" bson:"detail"` +} + +type GBDBusinessDetail struct { + Cover string `json:"cover" bson:"cover"` + Description string `json:"description" bson:"description"` +} + +type GBDAboutUs struct { + Description string `json:"description" bson:"description"` + Companies []GBDCompanyInfo `json:"companies" bson:"companies"` + Values string `json:"values" bson:"values"` + ValuesImg string `json:"valuesImg" bson:"valuesImg"` +} + +type GBDCompanyInfo struct { + Name string `json:"name" bson:"name"` + Address string `json:"address" bson:"address"` + Description string `json:"description" bson:"description"` + Contact []GBDContactInfo `json:"contact" bson:"contact"` +} + +type GBDContactInfo struct { + Type string `json:"type" bson:"type"` + Value string `json:"value" bson:"value"` +} + +type GBDFAQ struct { + Question string `json:"question" bson:"question"` + Answer string `json:"answer" bson:"answer"` +} + +type GBDCMSData struct { + Description string `json:"description" bson:"description"` + Value string `json:"value" bson:"value"` +} + +func (data *GetBasicDataResp) transfer(mod *officialwebsitemod.BasicData) error { + if mod == nil { + return stderr.CodeEmptyData + } + data.ID = mod.ID + data.Title = mod.Title + data.Description = mod.Description + data.Tags = mod.Tags + data.BannersDescription = mod.BannersDescription + data.BannersDuration = mod.BannersDuration + data.HomePageCMSDescription = mod.HomePageCMSDescription + data.AboutUs.Description = mod.AboutUs.Description + data.AboutUs.Values = mod.AboutUs.Values + data.AboutUs.ValuesImg = mod.AboutUs.ValuesImg + + data.Banner = make([]GBDBanner, 0, len(mod.Banner)) + for _, v := range mod.Banner { + data.Banner = append(data.Banner, GBDBanner{ + Name: v.Name, Description: v.Description, Image: v.Image, PhoneImage: v.PhoneImage, Thumbnail: v.Thumbnail, URL: v.URL, + }) + } + data.HeroBanner = make([]GBDHeroBanner, 0, len(mod.HeroBanner)) + for _, v := range mod.HeroBanner { + data.HeroBanner = append(data.HeroBanner, GBDHeroBanner{ + ID: v.ID, HeroID: v.HeroID.Hex(), Name: v.Name, Description: v.Description, Image: v.Image, URL: v.URL, Duration: v.Duration, + }) + } + data.Masterpiece = make([]GBDMasterpiece, 0, len(mod.Masterpiece)) + for _, v := range mod.Masterpiece { + data.Masterpiece = append(data.Masterpiece, GBDMasterpiece{ + ID: v.ID, Name: v.Name, Description: v.Description, Cover: v.Cover, Type: v.Type, + }) + } + data.Business = make([]GBDBusiness, 0, len(mod.Business)) + for _, v := range mod.Business { + business := GBDBusiness{ + Name: v.Name, EnglishName: v.EnglishName, Description: v.Description, Cover: v.Cover, + Detail: make([]GBDBusinessDetail, 0, len(v.Detail)), + } + for _, d := range v.Detail { + business.Detail = append(business.Detail, GBDBusinessDetail{ + Cover: d.Cover, Description: d.Description, + }) + } + data.Business = append(data.Business, business) + } + data.AboutUs.Companies = make([]GBDCompanyInfo, 0, len(mod.AboutUs.Companies)) + for _, v := range mod.AboutUs.Companies { + company := GBDCompanyInfo{ + Name: v.Name, Address: v.Address, Description: v.Description, + Contact: make([]GBDContactInfo, 0, len(v.Contact)), + } + for _, c := range v.Contact { + company.Contact = append(company.Contact, GBDContactInfo{Type: c.Type, Value: c.Value}) + } + data.AboutUs.Companies = append(data.AboutUs.Companies, company) + } + data.FAQ = make([]GBDFAQ, 0, len(mod.FAQ)) + for _, v := range mod.FAQ { + data.FAQ = append(data.FAQ, GBDFAQ{Question: v.Question, Answer: v.Answer}) + } + data.HomePageCMS = make([]GBDCMSData, 0, len(mod.HomePageCMS)) + for _, v := range mod.HomePageCMS { + data.HomePageCMS = append(data.HomePageCMS, GBDCMSData{Description: v.Description, Value: v.Value}) + } + data.RecruitCMS = make([]GBDCMSData, 0, len(mod.RecruitCMS)) + for _, v := range mod.RecruitCMS { + data.RecruitCMS = append(data.RecruitCMS, GBDCMSData{Description: v.Description, Value: v.Value}) + } + return nil +} + +// ───────────────────────────────────────── +// Mutation (Modify) types +// ───────────────────────────────────────── + +type ModifyBasicDataReq struct { + ID string `json:"id" form:"id"` + Title *string `json:"title" bson:"title"` + Description *string `json:"description" bson:"description"` + Banner *[]ModifyBanner `json:"banner" bson:"banner"` + HeroBanner *[]ModifyHeroBanner `json:"heroBanner" bson:"heroBanner"` + Masterpiece *[]ModifyMasterpiece `json:"masterpiece" bson:"masterpiece"` + Business *[]ModifyBusiness `json:"business" bson:"business"` + BannersDescription *string `json:"bannersDescription" bson:"bannersDescription"` + BannersDuration *int `json:"bannersDuration" bson:"bannersDuration"` + AboutUs *ModifyAboutUs `json:"aboutUs" bson:"aboutUs"` + FAQ *[]ModifyFAQ `json:"faq" bson:"faq"` + HomePageCMS *[]ModifyCMSData `json:"homePageCMS" bson:"homePageCMS"` + HomePageCMSDescription *string `json:"homePageCMSDescription" bson:"homePageCMSDescription"` + RecruitCMS *[]ModifyCMSData `json:"recruitCMS" bson:"recruitCMS"` + Tags *[]officialwebsitemod.Tag `json:"tags" bson:"tags"` +} + +type ModifyBanner struct { + Name *string `json:"name" bson:"name"` + Description *string `json:"description" bson:"description"` + Image *string `json:"image" bson:"image"` + PhoneImage *string `json:"phoneImage" bson:"phoneImage"` + Thumbnail *string `json:"thumbnail" bson:"thumbnail"` + URL *string `json:"url" bson:"url"` +} + +type ModifyHeroBanner struct { + ID *primitive.ObjectID `json:"id" bson:"_id,omitempty"` + HeroID *primitive.ObjectID `json:"heroId" bson:"heroId"` + Name *string `json:"name" bson:"name"` + Description *string `json:"description" bson:"description"` + Image *string `json:"image" bson:"image"` + URL *string `json:"url" bson:"url"` + Duration *int `json:"duration" bson:"duration"` +} + +type ModifyMasterpiece struct { + ID *primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Name *string `json:"name" bson:"name"` + Description *string `json:"description" bson:"description"` + Cover *string `json:"cover" bson:"cover"` + Type *string `json:"type" bson:"type"` +} + +type ModifyBusiness struct { + Name *string `json:"name" bson:"name"` + EnglishName *string `json:"englishName" bson:"englishName"` + Description *string `json:"description" bson:"description"` + Cover *string `json:"cover" bson:"cover"` + Detail *[]ModifyBusinessDetail `json:"detail" bson:"detail"` +} + +type ModifyBusinessDetail struct { + Cover *string `json:"cover" bson:"cover"` + Description *string `json:"description" bson:"description"` +} + +type ModifyAboutUs struct { + Description *string `json:"description" bson:"description"` + Companies *[]ModifyCompanyInfo `json:"companies" bson:"companies"` + Values *string `json:"values" bson:"values"` + ValuesImg *string `json:"valuesImg" bson:"valuesImg"` +} + +type ModifyCompanyInfo struct { + Name *string `json:"name" bson:"name"` + Address *string `json:"address" bson:"address"` + Description *string `json:"description" bson:"description"` + Contact *[]ModifyContactInfo `json:"contact" bson:"contact"` +} + +type ModifyContactInfo struct { + Type *string `json:"type" bson:"type"` + Value *string `json:"value" bson:"value"` +} + +type ModifyFAQ struct { + Question *string `json:"question" bson:"question"` + Answer *string `json:"answer" bson:"answer"` +} + +type ModifyCMSData struct { + Description *string `json:"description" bson:"description"` + Value *string `json:"value" bson:"value"` +} + +type UpdateBasicDataResp struct { + Count int64 `json:"count"` +} + +// ───────────────────────────────────────── +// Delete types +// ───────────────────────────────────────── + +type DeleteBasicDataReq struct { + ID string `json:"id"` +} + +type DeleteBasicDataResp struct { + Count int64 `json:"count"` +} + +// ───────────────────────────────────────── +// toMod — Modify* → model struct +// ───────────────────────────────────────── + +func (v ModifyBanner) toMod() officialwebsitemod.Banner { + return officialwebsitemod.Banner{ + Name: stringOrZero(v.Name), Description: stringOrZero(v.Description), + Image: stringOrZero(v.Image), Thumbnail: stringOrZero(v.Thumbnail), + PhoneImage: stringOrZero(v.PhoneImage), URL: stringOrZero(v.URL), + } +} + +func (v ModifyHeroBanner) toMod() officialwebsitemod.HeroBanner { + return officialwebsitemod.HeroBanner{ + HeroID: objectIDOrZero(v.HeroID), Name: stringOrZero(v.Name), Description: stringOrZero(v.Description), + Image: stringOrZero(v.Image), URL: stringOrZero(v.URL), Duration: intOrZero(v.Duration), + } +} + +func (v ModifyMasterpiece) toMod() officialwebsitemod.Masterpiece { + return officialwebsitemod.Masterpiece{ + ID: objectIDOrZero(v.ID), Name: stringOrZero(v.Name), Description: stringOrZero(v.Description), + Cover: stringOrZero(v.Cover), Type: stringOrZero(v.Type), + } +} + +func (v ModifyBusiness) toMod() officialwebsitemod.Business { + out := officialwebsitemod.Business{ + Name: stringOrZero(v.Name), EnglishName: stringOrZero(v.EnglishName), + Description: stringOrZero(v.Description), Cover: stringOrZero(v.Cover), + } + if v.Detail != nil { + out.Detail = make([]officialwebsitemod.BusinessDetail, 0, len(*v.Detail)) + for _, d := range *v.Detail { + out.Detail = append(out.Detail, d.toMod()) + } + } + return out +} + +func (v ModifyBusinessDetail) toMod() officialwebsitemod.BusinessDetail { + return officialwebsitemod.BusinessDetail{ + Cover: stringOrZero(v.Cover), Description: stringOrZero(v.Description), + } +} + +func (v ModifyContactInfo) toMod() officialwebsitemod.ContactInfo { + return officialwebsitemod.ContactInfo{Type: stringOrZero(v.Type), Value: stringOrZero(v.Value)} +} + +func (v ModifyCompanyInfo) toMod() officialwebsitemod.CompanyInfo { + out := officialwebsitemod.CompanyInfo{ + Name: stringOrZero(v.Name), Address: stringOrZero(v.Address), Description: stringOrZero(v.Description), + } + if v.Contact != nil { + out.Contact = make([]officialwebsitemod.ContactInfo, 0, len(*v.Contact)) + for _, c := range *v.Contact { + out.Contact = append(out.Contact, c.toMod()) + } + } + return out +} + +func (v *ModifyAboutUs) toMod() officialwebsitemod.AboutUs { + if v == nil { + return officialwebsitemod.AboutUs{} + } + out := officialwebsitemod.AboutUs{ + Description: stringOrZero(v.Description), Values: stringOrZero(v.Values), ValuesImg: stringOrZero(v.ValuesImg), + } + if v.Companies != nil { + out.Companies = make([]officialwebsitemod.CompanyInfo, 0, len(*v.Companies)) + for _, c := range *v.Companies { + out.Companies = append(out.Companies, c.toMod()) + } + } + return out +} + +func (v ModifyFAQ) toMod() officialwebsitemod.FAQ { + return officialwebsitemod.FAQ{Question: stringOrZero(v.Question), Answer: stringOrZero(v.Answer)} +} + +func (v ModifyCMSData) toMod() officialwebsitemod.CMSData { + return officialwebsitemod.CMSData{Description: stringOrZero(v.Description), Value: stringOrZero(v.Value)} +} + +// toMod 将请求结构体转换为完整的 BasicData model +func (req *ModifyBasicDataReq) toMod() (officialwebsitemod.BasicData, error) { + if req == nil { + return officialwebsitemod.BasicData{}, stderr.CodeEmptyData + } + data := officialwebsitemod.BasicData{ + Title: stringOrZero(req.Title), + Description: stringOrZero(req.Description), + BannersDescription: stringOrZero(req.BannersDescription), + BannersDuration: intOrZero(req.BannersDuration), + HomePageCMSDescription: stringOrZero(req.HomePageCMSDescription), + } + if req.AboutUs != nil { + data.AboutUs = req.AboutUs.toMod() + } + if req.Banner != nil { + data.Banner = make([]officialwebsitemod.Banner, 0, len(*req.Banner)) + for _, b := range *req.Banner { + data.Banner = append(data.Banner, b.toMod()) + } + } + if req.HeroBanner != nil { + data.HeroBanner = make([]officialwebsitemod.HeroBanner, 0, len(*req.HeroBanner)) + for _, h := range *req.HeroBanner { + data.HeroBanner = append(data.HeroBanner, h.toMod()) + } + } + if req.Masterpiece != nil { + data.Masterpiece = make([]officialwebsitemod.Masterpiece, 0, len(*req.Masterpiece)) + for _, m := range *req.Masterpiece { + data.Masterpiece = append(data.Masterpiece, m.toMod()) + } + } + if req.Business != nil { + data.Business = make([]officialwebsitemod.Business, 0, len(*req.Business)) + for _, b := range *req.Business { + data.Business = append(data.Business, b.toMod()) + } + } + if req.FAQ != nil { + data.FAQ = make([]officialwebsitemod.FAQ, 0, len(*req.FAQ)) + for _, f := range *req.FAQ { + data.FAQ = append(data.FAQ, f.toMod()) + } + } + if req.HomePageCMS != nil { + data.HomePageCMS = make([]officialwebsitemod.CMSData, 0, len(*req.HomePageCMS)) + for _, h := range *req.HomePageCMS { + data.HomePageCMS = append(data.HomePageCMS, h.toMod()) + } + } + if req.RecruitCMS != nil { + data.RecruitCMS = make([]officialwebsitemod.CMSData, 0, len(*req.RecruitCMS)) + for _, r := range *req.RecruitCMS { + data.RecruitCMS = append(data.RecruitCMS, r.toMod()) + } + } + if req.Tags != nil { + data.Tags = *req.Tags + } + return data, nil +} + +// ───────────────────────────────────────── +// toM — Modify* → officialwebsitemod.M (Mongo update doc) +// ───────────────────────────────────────── + +func (v ModifyBanner) toM() officialwebsitemod.M { + out := officialwebsitemod.M{} + if v.Name != nil { + out["name"] = *v.Name + } + if v.Description != nil { + out["description"] = *v.Description + } + if v.Image != nil { + out["image"] = *v.Image + } + if v.Thumbnail != nil { + out["thumbnail"] = *v.Thumbnail + } + if v.PhoneImage != nil { + out["phoneImage"] = *v.PhoneImage + } + if v.URL != nil { + out["url"] = *v.URL + } + return out +} + +func (v ModifyHeroBanner) toM() officialwebsitemod.M { + out := officialwebsitemod.M{} + if v.ID != nil { + out["_id"] = *v.ID + } + if v.HeroID != nil { + out["heroId"] = *v.HeroID + } + if v.Name != nil { + out["name"] = *v.Name + } + if v.Description != nil { + out["description"] = *v.Description + } + if v.Image != nil { + out["image"] = *v.Image + } + if v.URL != nil { + out["url"] = *v.URL + } + if v.Duration != nil { + out["duration"] = *v.Duration + } + return out +} + +func (v ModifyMasterpiece) toM() officialwebsitemod.M { + out := officialwebsitemod.M{} + if v.ID != nil { + out["_id"] = *v.ID + } + if v.Name != nil { + out["name"] = *v.Name + } + if v.Description != nil { + out["description"] = *v.Description + } + if v.Cover != nil { + out["cover"] = *v.Cover + } + if v.Type != nil { + out["type"] = *v.Type + } + return out +} + +func (v ModifyBusiness) toM() officialwebsitemod.M { + out := officialwebsitemod.M{} + if v.Name != nil { + out["name"] = *v.Name + } + if v.EnglishName != nil { + out["englishName"] = *v.EnglishName + } + if v.Description != nil { + out["description"] = *v.Description + } + if v.Cover != nil { + out["cover"] = *v.Cover + } + if v.Detail != nil { + detail := make([]officialwebsitemod.M, 0, len(*v.Detail)) + for _, d := range *v.Detail { + detail = append(detail, d.toM()) + } + out["detail"] = detail + } + return out +} + +func (v ModifyBusinessDetail) toM() officialwebsitemod.M { + out := officialwebsitemod.M{} + if v.Cover != nil { + out["cover"] = *v.Cover + } + if v.Description != nil { + out["description"] = *v.Description + } + return out +} + +func (v ModifyContactInfo) toM() officialwebsitemod.M { + out := officialwebsitemod.M{} + if v.Type != nil { + out["type"] = *v.Type + } + if v.Value != nil { + out["value"] = *v.Value + } + return out +} + +func (v ModifyCompanyInfo) toM() officialwebsitemod.M { + out := officialwebsitemod.M{} + if v.Name != nil { + out["name"] = *v.Name + } + if v.Address != nil { + out["address"] = *v.Address + } + if v.Description != nil { + out["description"] = *v.Description + } + if v.Contact != nil { + contacts := make([]officialwebsitemod.M, 0, len(*v.Contact)) + for _, c := range *v.Contact { + contacts = append(contacts, c.toM()) + } + out["contact"] = contacts + } + return out +} + +func (v *ModifyAboutUs) toM() officialwebsitemod.M { + if v == nil { + return nil + } + out := officialwebsitemod.M{} + if v.Description != nil { + out["description"] = *v.Description + } + if v.Values != nil { + out["values"] = *v.Values + } + if v.ValuesImg != nil { + out["valuesImg"] = *v.ValuesImg + } + if v.Companies != nil { + companies := make([]officialwebsitemod.M, 0, len(*v.Companies)) + for _, c := range *v.Companies { + companies = append(companies, c.toM()) + } + out["companies"] = companies + } + return out +} + +func (v ModifyFAQ) toM() officialwebsitemod.M { + out := officialwebsitemod.M{} + if v.Question != nil { + out["question"] = *v.Question + } + if v.Answer != nil { + out["answer"] = *v.Answer + } + return out +} + +func (v ModifyCMSData) toM() officialwebsitemod.M { + out := officialwebsitemod.M{} + if v.Description != nil { + out["description"] = *v.Description + } + if v.Value != nil { + out["value"] = *v.Value + } + return out +} + +// toM 将请求中的非 nil 字段构造为 Mongo $set 更新文档 +func (req *ModifyBasicDataReq) toM() (officialwebsitemod.M, error) { + if req == nil { + return nil, stderr.CodeEmptyData + } + set := officialwebsitemod.M{} + if req.Title != nil { + set["title"] = *req.Title + } + if req.Description != nil { + set["description"] = *req.Description + } + if req.BannersDescription != nil { + set["bannersDescription"] = *req.BannersDescription + } + if req.BannersDuration != nil { + set["bannersDuration"] = *req.BannersDuration + } + if req.HomePageCMSDescription != nil { + set["homePageCMSDescription"] = *req.HomePageCMSDescription + } + if req.Banner != nil { + banners := make([]officialwebsitemod.M, 0, len(*req.Banner)) + for _, b := range *req.Banner { + banners = append(banners, b.toM()) + } + set["banner"] = banners + } + if req.HeroBanner != nil { + heroBanners := make([]officialwebsitemod.M, 0, len(*req.HeroBanner)) + for _, h := range *req.HeroBanner { + heroBanners = append(heroBanners, h.toM()) + } + set["heroBanner"] = heroBanners + } + if req.Masterpiece != nil { + masterpiece := make([]officialwebsitemod.M, 0, len(*req.Masterpiece)) + for _, m := range *req.Masterpiece { + masterpiece = append(masterpiece, m.toM()) + } + set["masterpiece"] = masterpiece + } + if req.Business != nil { + businesses := make([]officialwebsitemod.M, 0, len(*req.Business)) + for _, b := range *req.Business { + businesses = append(businesses, b.toM()) + } + set["business"] = businesses + } + if req.AboutUs != nil { + aboutUsM := req.AboutUs.toM() + if len(aboutUsM) > 0 { + set["aboutUs"] = aboutUsM + } + } + if req.FAQ != nil { + faqs := make([]officialwebsitemod.M, 0, len(*req.FAQ)) + for _, f := range *req.FAQ { + faqs = append(faqs, f.toM()) + } + set["faq"] = faqs + } + if req.HomePageCMS != nil { + cms := make([]officialwebsitemod.M, 0, len(*req.HomePageCMS)) + for _, h := range *req.HomePageCMS { + cms = append(cms, h.toM()) + } + set["homePageCMS"] = cms + } + if req.RecruitCMS != nil { + rcms := make([]officialwebsitemod.M, 0, len(*req.RecruitCMS)) + for _, r := range *req.RecruitCMS { + rcms = append(rcms, r.toM()) + } + set["recruitCMS"] = rcms + } + if req.Tags != nil { + set["tags"] = *req.Tags + } + if len(set) == 0 { + return nil, nil + } + set["updatedAt"] = time.Now().UTC() + return officialwebsitemod.M{"$set": set}, nil +} diff --git a/web/service/officialWebsiteser/business.go b/web/service/officialWebsiteser/business.go new file mode 100644 index 0000000..1f8ab13 --- /dev/null +++ b/web/service/officialWebsiteser/business.go @@ -0,0 +1,209 @@ +package officialwebsiteser + +import ( + "time" + + "91porn-server/common/stderr" + officialwebsitemod "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ───────────────────────────────────────── +// Service functions +// ───────────────────────────────────────── + +func ListBusiness(req *BusinessListReq) (int64, []officialwebsitemod.BusinessData, error) { + filter, err := req.Query() + if err != nil { + return 0, nil, err + } + mod := &officialwebsitemod.BusinessData{} + total, err := mod.Count(filter) + if err != nil { + return 0, nil, err + } + list, err := mod.FindMany(filter, req.FindOptions()) + if err != nil { + return 0, nil, err + } + return total, list, nil +} + +func CreateBusiness(req *ModifyBusinessReq) (officialwebsitemod.BusinessData, error) { + data := req.toMod() + if err := data.Create(); err != nil { + return officialwebsitemod.BusinessData{}, err + } + return data, nil +} + +func UpdateBusiness(req *ModifyBusinessReq) (resp CountResp, err error) { + if req == nil || req.ID == "" { + return + } + id, parseErr := primitive.ObjectIDFromHex(req.ID) + if parseErr != nil || isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.BusinessData{} + if err = mod.FindOne(officialwebsitemod.M{"_id": id}); err != nil { + return + } + var update officialwebsitemod.M + if update, err = req.toM(); err != nil || update == nil { + return + } + resp.Count, err = mod.Update(officialwebsitemod.M{"_id": id}, update) + return +} + +func DeleteBusiness(req *DeleteBusinessReq) (data DeleteBusinessResp, err error) { + if req == nil { + err = stderr.CodeEmptyData + return + } + var id primitive.ObjectID + if id, err = primitive.ObjectIDFromHex(req.ID); err != nil { + return + } + if isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.BusinessData{} + data.Count, err = mod.Delete(officialwebsitemod.M{"_id": id}) + return +} + +// ───────────────────────────────────────── +// Mutation (Modify) types +// ───────────────────────────────────────── + +type ModifyBusinessReq struct { + ID string `json:"id" form:"id"` + Title *string `json:"title"` + Description *string `json:"description"` + Type *string `json:"type"` + ServiceOverview *string `json:"serviceOverview"` + CollaborationProcess *string `json:"collaborationProcess"` + CaseStudies *string `json:"caseStudies"` +} + +// ───────────────────────────────────────── +// Delete types +// ───────────────────────────────────────── + +type DeleteBusinessReq struct { + ID string `json:"id" form:"id"` +} + +type DeleteBusinessResp struct { + Count int64 `json:"count"` +} + +// ───────────────────────────────────────── +// toMod — ModifyBusinessReq → model struct +// ───────────────────────────────────────── + +func (req *ModifyBusinessReq) toMod() officialwebsitemod.BusinessData { + if req == nil { + return officialwebsitemod.BusinessData{} + } + return officialwebsitemod.BusinessData{ + Title: stringOrZero(req.Title), + Description: stringOrZero(req.Description), + Type: stringOrZero(req.Type), + ServiceOverview: stringOrZero(req.ServiceOverview), + CollaborationProcess: stringOrZero(req.CollaborationProcess), + CaseStudies: stringOrZero(req.CaseStudies), + } +} + +// ───────────────────────────────────────── +// toM — ModifyBusinessReq → officialwebsitemod.M +// ───────────────────────────────────────── + +func (req *ModifyBusinessReq) toM() (officialwebsitemod.M, error) { + if req == nil { + return nil, stderr.CodeEmptyData + } + set := officialwebsitemod.M{} + if req.Title != nil { + set["title"] = *req.Title + } + if req.Description != nil { + set["description"] = *req.Description + } + if req.Type != nil { + set["type"] = *req.Type + } + if req.ServiceOverview != nil { + set["serviceOverview"] = *req.ServiceOverview + } + if req.CollaborationProcess != nil { + set["collaborationProcess"] = *req.CollaborationProcess + } + if req.CaseStudies != nil { + set["caseStudies"] = *req.CaseStudies + } + if len(set) == 0 { + return nil, nil + } + set["updatedAt"] = time.Now().UTC() + return officialwebsitemod.M{"$set": set}, nil +} + +// ───────────────────────────────────────── +// List types +// ───────────────────────────────────────── + +type BusinessListReq struct { + PageReq + ID *string `form:"id"` + Title *string `form:"title"` + Description *string `form:"description"` + Type *string `form:"type"` + ServiceOverview *string `form:"serviceOverview"` + CollaborationProcess *string `form:"collaborationProcess"` + CaseStudies *string `form:"caseStudies"` + CreatedAt *time.Time `form:"createdAt"` + UpdatedAt *time.Time `form:"updatedAt"` + DeletedAt *time.Time `form:"deletedAt"` +} + +func (r *BusinessListReq) Query() (officialwebsitemod.M, error) { + filter := officialwebsitemod.M{} + if err := parseObjectIDFilter(filter, "_id", r.ID); err != nil { + return nil, err + } + if r.Title != nil { + filter["title"] = *r.Title + } + if r.Description != nil { + filter["description"] = *r.Description + } + if r.Type != nil { + filter["type"] = *r.Type + } + if r.ServiceOverview != nil { + filter["serviceOverview"] = *r.ServiceOverview + } + if r.CollaborationProcess != nil { + filter["collaborationProcess"] = *r.CollaborationProcess + } + if r.CaseStudies != nil { + filter["caseStudies"] = *r.CaseStudies + } + if r.CreatedAt != nil { + filter["createdAt"] = *r.CreatedAt + } + if r.UpdatedAt != nil { + filter["updatedAt"] = *r.UpdatedAt + } + if r.DeletedAt != nil { + filter["deletedAt"] = *r.DeletedAt + } + return filter, nil +} diff --git a/web/service/officialWebsiteser/helper.go b/web/service/officialWebsiteser/helper.go new file mode 100644 index 0000000..452aafe --- /dev/null +++ b/web/service/officialWebsiteser/helper.go @@ -0,0 +1,286 @@ +package officialwebsiteser + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + "time" + + officialwebsitemod "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// ───────────────────────────────────────── +// List Helper Types and Functions +// ───────────────────────────────────────── + +// CountResp 通用操作返回 +type CountResp struct { + Count int64 `json:"count"` +} + +// PageReq 分页请求 +type PageReq struct { + Page int64 `form:"page"` + PageSize int64 `form:"pageSize"` +} + +func (r *PageReq) FindOptions() *options.FindOptions { + page := r.Page + if page <= 0 { + page = 1 + } + pageSize := r.PageSize + if pageSize <= 0 { + pageSize = 20 + } + skip := (page - 1) * pageSize + return options.Find().SetSkip(skip).SetLimit(pageSize).SetSort(officialwebsitemod.M{"createdAt": -1}) +} + +func parseObjectIDFilter(filter officialwebsitemod.M, key string, raw *string) error { + if raw == nil || *raw == "" { + return nil + } + id, err := primitive.ObjectIDFromHex(*raw) + if err != nil { + return fmt.Errorf("invalid %s: %w", key, err) + } + filter[key] = id + return nil +} + +func parseJSONFilter(filter officialwebsitemod.M, key string, raw *string, out interface{}) error { + if raw == nil || *raw == "" { + return nil + } + if err := json.Unmarshal([]byte(*raw), out); err != nil { + return fmt.Errorf("invalid %s json: %w", key, err) + } + v := reflect.ValueOf(out) + if v.Kind() == reflect.Ptr { + filter[key] = v.Elem().Interface() + return nil + } + filter[key] = out + return nil +} + +// ─────���─────────────────────────────────── +// Update Helper Functions +// ───────────────────────────────────────── + +func modelToMap(doc interface{}) (officialwebsitemod.M, error) { + raw, err := bson.Marshal(doc) + if err != nil { + return nil, err + } + + data := officialwebsitemod.M{} + if err := bson.Unmarshal(raw, &data); err != nil { + return nil, err + } + delete(data, "_id") + delete(data, "createdAt") + delete(data, "updatedAt") + delete(data, "deletedAt") + return data, nil +} + +func modelChanged(oldDoc interface{}, newDoc interface{}) (bool, error) { + oldMap, err := modelToMap(oldDoc) + if err != nil { + return false, err + } + newMap, err := modelToMap(newDoc) + if err != nil { + return false, err + } + return !reflect.DeepEqual(oldMap, newMap), nil +} + +func buildModelUpdateDoc(doc interface{}) (officialwebsitemod.M, error) { + data, err := modelToMap(doc) + if err != nil { + return nil, err + } + data["updatedAt"] = time.Now().UTC() + return officialwebsitemod.M{"$set": data}, nil +} + +func buildPointerUpdateDoc(oldDoc interface{}, updateDoc interface{}) (officialwebsitemod.M, error) { + oldVal, err := structValueOf(oldDoc, "old doc") + if err != nil { + return nil, err + } + newVal, err := structValueOf(updateDoc, "update doc") + if err != nil { + return nil, err + } + set := officialwebsitemod.M{} + collectPointerChanges(set, oldVal, newVal) + + if len(set) == 0 { + return nil, nil + } + set["updatedAt"] = time.Now().UTC() + return officialwebsitemod.M{"$set": set}, nil +} + +func structValueOf(doc interface{}, label string) (reflect.Value, error) { + val := reflect.ValueOf(doc) + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return reflect.Value{}, errors.New(label + " is nil") + } + val = val.Elem() + } + if val.Kind() != reflect.Struct { + return reflect.Value{}, errors.New(label + " must be struct") + } + return val, nil +} + +func collectPointerChanges(set officialwebsitemod.M, oldVal reflect.Value, newVal reflect.Value) { + newType := newVal.Type() + for i := 0; i < newVal.NumField(); i++ { + addPointerFieldChange(set, oldVal, newVal.Field(i), newType.Field(i)) + } +} + +func addPointerFieldChange(set officialwebsitemod.M, oldVal reflect.Value, newField reflect.Value, fieldInfo reflect.StructField) { + if fieldInfo.Name == "ID" { + return + } + if !newField.IsValid() || newField.Kind() != reflect.Ptr || newField.IsNil() { + return + } + + oldField := findStructFieldByName(oldVal, fieldInfo.Name) + if !oldField.IsValid() { + return + } + + newValue := derefValue(newField) + oldValue := derefValue(oldField) + if !newValue.IsValid() || !oldValue.IsValid() { + return + } + if reflect.DeepEqual(oldValue.Interface(), newValue.Interface()) { + return + } + + key := bsonFieldName(fieldInfo) + if key == "" { + return + } + set[key] = newValue.Interface() +} + +func findStructFieldByName(val reflect.Value, name string) reflect.Value { + if !val.IsValid() { + return reflect.Value{} + } + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return reflect.Value{} + } + val = val.Elem() + } + if val.Kind() != reflect.Struct { + return reflect.Value{} + } + + if field := val.FieldByName(name); field.IsValid() { + return field + } + + valType := val.Type() + for i := 0; i < val.NumField(); i++ { + fieldType := valType.Field(i) + if !fieldType.Anonymous { + continue + } + field := findStructFieldByName(val.Field(i), name) + if field.IsValid() { + return field + } + } + + return reflect.Value{} +} + +func derefValue(v reflect.Value) reflect.Value { + for v.IsValid() && v.Kind() == reflect.Ptr { + if v.IsNil() { + return reflect.Value{} + } + v = v.Elem() + } + return v +} + +func bsonFieldName(field reflect.StructField) string { + if tag := field.Tag.Get("bson"); tag != "" { + name := strings.Split(tag, ",")[0] + if name != "" && name != "-" { + return name + } + } + if tag := field.Tag.Get("json"); tag != "" { + name := strings.Split(tag, ",")[0] + if name != "" && name != "-" { + return name + } + } + return field.Name +} + +// ───────────────────────────────────────── +// Pointer helpers +// ───────────────────────────────────────── + +func stringOrZero(v *string) string { + if v == nil { + return "" + } + return *v +} + +func intOrZero(v *int) int { + if v == nil { + return 0 + } + return *v +} + +func objectIDOrZero(v *primitive.ObjectID) primitive.ObjectID { + if v == nil { + return primitive.NilObjectID + } + return *v +} + +func int64OrZero(v *int64) int64 { + if v == nil { + return 0 + } + return *v +} + +func boolOrZero(v *bool) bool { + if v == nil { + return false + } + return *v +} + +func isNilObjectID(id primitive.ObjectID) bool { + return id == primitive.NilObjectID +} + diff --git a/web/service/officialWebsiteser/hero.go b/web/service/officialWebsiteser/hero.go new file mode 100644 index 0000000..b7d300d --- /dev/null +++ b/web/service/officialWebsiteser/hero.go @@ -0,0 +1,300 @@ +package officialwebsiteser + +import ( + "time" + + "91porn-server/common/stderr" + officialwebsitemod "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ───────────────────────────────────────── +// Service functions +// ───────────────────────────────────────── + +func ListHero(req *HeroListReq) (int64, []officialwebsitemod.Hero, error) { + filter, err := req.Query() + if err != nil { + return 0, nil, err + } + mod := &officialwebsitemod.Hero{} + total, err := mod.Count(filter) + if err != nil { + return 0, nil, err + } + list, err := mod.FindMany(filter, req.FindOptions()) + if err != nil { + return 0, nil, err + } + return total, list, nil +} + +func CreateHero(req *ModifyHeroReq) (officialwebsitemod.Hero, error) { + data := req.toMod() + // SEO slug:手填优先(校验唯一),为空则由名称生成雏形并去重 + var err error + if data.SeoSlug, err = resolveHeroSeoSlug(data.SeoSlug, data.Name, primitive.NilObjectID); err != nil { + return officialwebsitemod.Hero{}, err + } + if err := data.Create(); err != nil { + return officialwebsitemod.Hero{}, err + } + return data, nil +} + +// resolveHeroSeoSlug 处理 seoSlug:手填则校验格式并查重(冲突报错);为空则由名称生成雏形并追加后缀去重。 +func resolveHeroSeoSlug(provided, name string, excludeID primitive.ObjectID) (string, error) { + return ResolveSeoSlug(provided, name, func(slug string) (bool, error) { + filter := officialwebsitemod.M{"seoSlug": slug} + if !excludeID.IsZero() { + filter["_id"] = officialwebsitemod.M{"$ne": excludeID} + } + n, err := (&officialwebsitemod.Hero{}).Count(filter) + return n > 0, err + }) +} + +func UpdateHero(req *ModifyHeroReq) (resp CountResp, err error) { + if req == nil || req.ID == "" { + return + } + id, parseErr := primitive.ObjectIDFromHex(req.ID) + if parseErr != nil || isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.Hero{} + if err = mod.FindOne(officialwebsitemod.M{"_id": id}); err != nil { + return + } + // 若提交了 seoSlug,先校验/去重(排除自身),回写后由 toM 落库 + if req.SeoSlug != nil { + var slug string + if slug, err = resolveHeroSeoSlug(*req.SeoSlug, mod.Name, id); err != nil { + return + } + *req.SeoSlug = slug + } + var update officialwebsitemod.M + if update, err = req.toM(); err != nil || update == nil { + return + } + resp.Count, err = mod.Update(officialwebsitemod.M{"_id": id}, update) + return +} + +func DeleteHero(req *DeleteHeroReq) (data DeleteHeroResp, err error) { + if req == nil { + err = stderr.CodeEmptyData + return + } + var id primitive.ObjectID + if id, err = primitive.ObjectIDFromHex(req.ID); err != nil { + return + } + if isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.Hero{} + data.Count, err = mod.Delete(officialwebsitemod.M{"_id": id}) + return +} + +// ───────────────────────────────────────── +// Mutation (Modify) types +// ───────────────────────────────────────── + +type ModifyHeroReq struct { + ID string `json:"id" form:"id"` + Name *string `json:"name"` + SeoSlug *string `json:"seoSlug"` + Avatar *string `json:"avatar"` + Description *string `json:"description"` + FansCount *int64 `json:"fansCount"` + Height *int64 `json:"height"` + Bust *int64 `json:"bust"` + Waist *int64 `json:"waist"` + Hip *int64 `json:"hip"` + Cover *string `json:"cover"` + Partners *[]primitive.ObjectID `json:"partners"` + Sort *int64 `json:"sort"` + IsActive *bool `json:"isActive"` +} + +// ───────────────────────────────────────── +// Delete types +// ───────────────────────────────────────── + +type DeleteHeroReq struct { + ID string `json:"id" form:"id"` +} + +type DeleteHeroResp struct { + Count int64 `json:"count"` +} + +// ───────────────────────────────────────── +// toMod — ModifyHeroReq → model struct +// ───────────────────────────────────────── + +func (req *ModifyHeroReq) toMod() officialwebsitemod.Hero { + if req == nil { + return officialwebsitemod.Hero{} + } + data := officialwebsitemod.Hero{ + Name: stringOrZero(req.Name), + Avatar: stringOrZero(req.Avatar), + Description: stringOrZero(req.Description), + FansCount: int64OrZero(req.FansCount), + Height: int64OrZero(req.Height), + Bust: int64OrZero(req.Bust), + Waist: int64OrZero(req.Waist), + Hip: int64OrZero(req.Hip), + Cover: stringOrZero(req.Cover), + SortModel: officialwebsitemod.SortModel{ + Sort: int64OrZero(req.Sort), + IsActive: boolOrZero(req.IsActive), + }, + } + if req.Partners != nil { + data.Partners = *req.Partners + } + if req.SeoSlug != nil { + data.SeoSlug = *req.SeoSlug + } + return data +} + +// ───────────────────────────────────────── +// toM — ModifyHeroReq → officialwebsitemod.M +// ───────────────────────────────────────── + +func (req *ModifyHeroReq) toM() (officialwebsitemod.M, error) { + if req == nil { + return nil, stderr.CodeEmptyData + } + set := officialwebsitemod.M{} + if req.Name != nil { + set["name"] = *req.Name + } + if req.SeoSlug != nil && *req.SeoSlug != "" { + set["seoSlug"] = *req.SeoSlug + } + if req.Avatar != nil { + set["avatar"] = *req.Avatar + } + if req.Description != nil { + set["description"] = *req.Description + } + if req.FansCount != nil { + set["fansCount"] = *req.FansCount + } + if req.Height != nil { + set["height"] = *req.Height + } + if req.Bust != nil { + set["bust"] = *req.Bust + } + if req.Waist != nil { + set["waist"] = *req.Waist + } + if req.Hip != nil { + set["hip"] = *req.Hip + } + if req.Cover != nil { + set["cover"] = *req.Cover + } + if req.Partners != nil { + set["partners"] = *req.Partners + } + if req.Sort != nil { + set["sort"] = *req.Sort + } + if req.IsActive != nil { + set["isActive"] = *req.IsActive + } + if len(set) == 0 { + return nil, nil + } + set["updatedAt"] = time.Now().UTC() + return officialwebsitemod.M{"$set": set}, nil +} + +// ───────────────────────────────────────── +// List types +// ───────────────────────────────────────── + +type HeroListReq struct { + PageReq + ID *string `form:"id"` + Name *string `form:"name"` + Avatar *string `form:"avatar"` + Height *int64 `form:"height"` + Bust *int64 `form:"bust"` + Waist *int64 `form:"waist"` + Hip *int64 `form:"hip"` + Description *string `form:"description"` + FansCount *int64 `form:"fansCount"` + Cover *string `form:"cover"` + Partners *string `form:"partners"` + Sort *int64 `form:"sort"` + IsActive *bool `form:"isActive"` + CreatedAt *time.Time `form:"createdAt"` + UpdatedAt *time.Time `form:"updatedAt"` + DeletedAt *time.Time `form:"deletedAt"` +} + +func (r *HeroListReq) Query() (officialwebsitemod.M, error) { + filter := officialwebsitemod.M{} + if err := parseObjectIDFilter(filter, "_id", r.ID); err != nil { + return nil, err + } + if r.Name != nil { + filter["name"] = *r.Name + } + if r.Avatar != nil { + filter["avatar"] = *r.Avatar + } + if r.Height != nil { + filter["height"] = *r.Height + } + if r.Bust != nil { + filter["bust"] = *r.Bust + } + if r.Waist != nil { + filter["waist"] = *r.Waist + } + if r.Hip != nil { + filter["hip"] = *r.Hip + } + if r.Description != nil { + filter["description"] = *r.Description + } + if r.FansCount != nil { + filter["fansCount"] = *r.FansCount + } + if r.Cover != nil { + filter["cover"] = *r.Cover + } + if err := parseJSONFilter(filter, "partners", r.Partners, &[]primitive.ObjectID{}); err != nil { + return nil, err + } + if r.Sort != nil { + filter["sort"] = *r.Sort + } + if r.IsActive != nil { + filter["isActive"] = *r.IsActive + } + if r.CreatedAt != nil { + filter["createdAt"] = *r.CreatedAt + } + if r.UpdatedAt != nil { + filter["updatedAt"] = *r.UpdatedAt + } + if r.DeletedAt != nil { + filter["deletedAt"] = *r.DeletedAt + } + return filter, nil +} diff --git a/web/service/officialWebsiteser/job.go b/web/service/officialWebsiteser/job.go new file mode 100644 index 0000000..50e8065 --- /dev/null +++ b/web/service/officialWebsiteser/job.go @@ -0,0 +1,220 @@ +package officialwebsiteser + +import ( + "time" + + "91porn-server/common/stderr" + officialwebsitemod "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ───────────────────────────────────────── +// Service functions +// ───────────────────────────────────────── + +func ListJobList(req *JobListReq) (int64, []officialwebsitemod.Job, error) { + filter, err := req.Query() + if err != nil { + return 0, nil, err + } + mod := &officialwebsitemod.Job{} + total, err := mod.Count(filter) + if err != nil { + return 0, nil, err + } + list, err := mod.FindMany(filter, req.FindOptions()) + if err != nil { + return 0, nil, err + } + return total, list, nil +} + +func CreateJobList(req *ModifyJobReq) (officialwebsitemod.Job, error) { + data := req.toMod() + if err := data.Create(); err != nil { + return officialwebsitemod.Job{}, err + } + return data, nil +} + +func UpdateJobList(req *ModifyJobReq) (resp CountResp, err error) { + if req == nil || req.ID == "" { + return + } + id, parseErr := primitive.ObjectIDFromHex(req.ID) + if parseErr != nil || isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.Job{} + if err = mod.FindOne(officialwebsitemod.M{"_id": id}); err != nil { + return + } + var update officialwebsitemod.M + if update, err = req.toM(); err != nil || update == nil { + return + } + resp.Count, err = mod.Update(officialwebsitemod.M{"_id": id}, update) + return +} + +func DeleteJobList(req *DeleteJobReq) (data DeleteJobResp, err error) { + if req == nil { + err = stderr.CodeEmptyData + return + } + var id primitive.ObjectID + if id, err = primitive.ObjectIDFromHex(req.ID); err != nil { + return + } + if isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.Job{} + data.Count, err = mod.Delete(officialwebsitemod.M{"_id": id}) + return +} + +// ───────────────────────────────────────── +// Mutation (Modify) types +// ───────────────────────────────────────── + +type ModifyJobReq struct { + ID string `json:"id" form:"id"` + Title *string `json:"title"` + Description *string `json:"description"` + Requirements *string `json:"requirements"` + Benefits *string `json:"benefits"` + JDUrl *string `json:"jdUrl"` + Sort *int64 `json:"sort"` + IsActive *bool `json:"isActive"` +} + +// ───────────────────────────────────────── +// Delete types +// ───────────────────────────────────────── + +type DeleteJobReq struct { + ID string `json:"id" form:"id"` +} + +type DeleteJobResp struct { + Count int64 `json:"count"` +} + +// ───────────────────────────────────────── +// toMod — ModifyJobReq → model struct +// ───────────────────────────────────────── + +func (req *ModifyJobReq) toMod() officialwebsitemod.Job { + if req == nil { + return officialwebsitemod.Job{} + } + return officialwebsitemod.Job{ + Title: stringOrZero(req.Title), + Description: stringOrZero(req.Description), + Requirements: stringOrZero(req.Requirements), + Benefits: stringOrZero(req.Benefits), + JDUrl: stringOrZero(req.JDUrl), + SortModel: officialwebsitemod.SortModel{ + Sort: int64OrZero(req.Sort), + IsActive: boolOrZero(req.IsActive), + }, + } +} + +// ───────────────────────────────────────── +// toM — ModifyJobReq → officialwebsitemod.M +// ───────────────────────────────────────── + +func (req *ModifyJobReq) toM() (officialwebsitemod.M, error) { + if req == nil { + return nil, stderr.CodeEmptyData + } + set := officialwebsitemod.M{} + if req.Title != nil { + set["title"] = *req.Title + } + if req.Description != nil { + set["description"] = *req.Description + } + if req.Requirements != nil { + set["requirements"] = *req.Requirements + } + if req.Benefits != nil { + set["benefits"] = *req.Benefits + } + if req.JDUrl != nil { + set["jdUrl"] = *req.JDUrl + } + if req.Sort != nil { + set["sort"] = *req.Sort + } + if req.IsActive != nil { + set["isActive"] = *req.IsActive + } + if len(set) == 0 { + return nil, nil + } + set["updatedAt"] = time.Now().UTC() + return officialwebsitemod.M{"$set": set}, nil +} + +// ───────────────────────────────────────── +// List types +// ───────────────────────────────────────── + +type JobListReq struct { + PageReq + ID *string `form:"id"` + Title *string `form:"title"` + Description *string `form:"description"` + Requirements *string `form:"requirements"` + Benefits *string `form:"benefits"` + JDUrl *string `form:"jdUrl"` + Sort *int64 `form:"sort"` + IsActive *bool `form:"isActive"` + CreatedAt *time.Time `form:"createdAt"` + UpdatedAt *time.Time `form:"updatedAt"` + DeletedAt *time.Time `form:"deletedAt"` +} + +func (r *JobListReq) Query() (officialwebsitemod.M, error) { + filter := officialwebsitemod.M{} + if err := parseObjectIDFilter(filter, "_id", r.ID); err != nil { + return nil, err + } + if r.Title != nil { + filter["title"] = *r.Title + } + if r.Description != nil { + filter["description"] = *r.Description + } + if r.Requirements != nil { + filter["requirements"] = *r.Requirements + } + if r.Benefits != nil { + filter["benefits"] = *r.Benefits + } + if r.JDUrl != nil { + filter["jdUrl"] = *r.JDUrl + } + if r.Sort != nil { + filter["sort"] = *r.Sort + } + if r.IsActive != nil { + filter["isActive"] = *r.IsActive + } + if r.CreatedAt != nil { + filter["createdAt"] = *r.CreatedAt + } + if r.UpdatedAt != nil { + filter["updatedAt"] = *r.UpdatedAt + } + if r.DeletedAt != nil { + filter["deletedAt"] = *r.DeletedAt + } + return filter, nil +} diff --git a/web/service/officialWebsiteser/news.go b/web/service/officialWebsiteser/news.go new file mode 100644 index 0000000..483d407 --- /dev/null +++ b/web/service/officialWebsiteser/news.go @@ -0,0 +1,261 @@ +package officialwebsiteser + +import ( + "time" + + "91porn-server/common/stderr" + officialwebsitemod "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ───────────────────────────────────────── +// Service functions +// ───────────────────────────────────────── + +func ListNews(req *NewsListReq) (int64, []officialwebsitemod.News, error) { + filter, err := req.Query() + if err != nil { + return 0, nil, err + } + mod := &officialwebsitemod.News{} + total, err := mod.Count(filter) + if err != nil { + return 0, nil, err + } + list, err := mod.FindMany(filter, req.FindOptions()) + if err != nil { + return 0, nil, err + } + return total, list, nil +} + +// resolveNewsSeoSlug 处理 seoSlug:手填则校验格式并查重(冲突报错);为空则由标题生成雏形并追加后缀去重。 +func resolveNewsSeoSlug(provided, title string, excludeID primitive.ObjectID) (string, error) { + return ResolveSeoSlug(provided, title, func(slug string) (bool, error) { + filter := officialwebsitemod.M{"seoSlug": slug} + if !excludeID.IsZero() { + filter["_id"] = officialwebsitemod.M{"$ne": excludeID} + } + n, err := (&officialwebsitemod.News{}).Count(filter) + return n > 0, err + }) +} + +func CreateNews(req *ModifyNewsReq) (officialwebsitemod.News, error) { + data := req.toMod() + // SEO slug:手填优先(校验唯一),为空则由标题生成雏形并去重 + slug, err := resolveNewsSeoSlug(data.SeoSlug, data.Title, primitive.NilObjectID) + if err != nil { + return officialwebsitemod.News{}, err + } + data.SeoSlug = slug + if err := data.Create(); err != nil { + return officialwebsitemod.News{}, err + } + return data, nil +} + +func UpdateNews(req *ModifyNewsReq) (resp CountResp, err error) { + if req == nil || req.ID == "" { + return + } + id, parseErr := primitive.ObjectIDFromHex(req.ID) + if parseErr != nil || isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.News{} + if err = mod.FindOne(officialwebsitemod.M{"_id": id}); err != nil { + return + } + // 若提交了 seoSlug,先校验/去重(排除自身),回写后由 toM 落库 + if req.SeoSlug != nil { + var slug string + if slug, err = resolveNewsSeoSlug(*req.SeoSlug, mod.Title, id); err != nil { + return + } + *req.SeoSlug = slug + } + var update officialwebsitemod.M + if update, err = req.toM(); err != nil || update == nil { + return + } + resp.Count, err = mod.Update(officialwebsitemod.M{"_id": id}, update) + return +} + +func DeleteNews(req *DeleteNewsReq) (data DeleteNewsResp, err error) { + if req == nil { + err = stderr.CodeEmptyData + return + } + var id primitive.ObjectID + if id, err = primitive.ObjectIDFromHex(req.ID); err != nil { + return + } + if isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.News{} + data.Count, err = mod.Delete(officialwebsitemod.M{"_id": id}) + return +} + +// ───────────────────────────────────────── +// Mutation (Modify) types +// ───────────────────────────────────────── + +type ModifyNewsReq struct { + ID string `json:"id" form:"id"` + Title *string `json:"title"` + SeoSlug *string `json:"seoSlug"` + Description *string `json:"description"` + Cover *string `json:"cover"` + Url *string `json:"url"` + Tags *[]officialwebsitemod.Tag `json:"tags"` + Detail *string `json:"detail"` + Sort *int64 `json:"sort"` + IsActive *bool `json:"isActive"` +} + +// ───────────────────────────────────────── +// Delete types +// ───────────────────────────────────────── + +type DeleteNewsReq struct { + ID string `json:"id" form:"id"` +} + +type DeleteNewsResp struct { + Count int64 `json:"count"` +} + +// ───────────────────────────────────────── +// toMod — ModifyNewsReq → model struct +// ───────────────────────────────────────── + +func (req *ModifyNewsReq) toMod() officialwebsitemod.News { + if req == nil { + return officialwebsitemod.News{} + } + data := officialwebsitemod.News{ + Title: stringOrZero(req.Title), + Description: stringOrZero(req.Description), + Cover: stringOrZero(req.Cover), + Url: stringOrZero(req.Url), + Detail: stringOrZero(req.Detail), + SortModel: officialwebsitemod.SortModel{ + Sort: int64OrZero(req.Sort), + IsActive: boolOrZero(req.IsActive), + }, + } + if req.Tags != nil { + data.Tags = *req.Tags + } + if req.SeoSlug != nil { + data.SeoSlug = *req.SeoSlug + } + return data +} + +// ───────────────────────────────────────── +// toM — ModifyNewsReq → officialwebsitemod.M +// ───────────────────────────────────────── + +func (req *ModifyNewsReq) toM() (officialwebsitemod.M, error) { + if req == nil { + return nil, stderr.CodeEmptyData + } + set := officialwebsitemod.M{} + if req.Title != nil { + set["title"] = *req.Title + } + if req.SeoSlug != nil && *req.SeoSlug != "" { + set["seoSlug"] = *req.SeoSlug + } + if req.Description != nil { + set["description"] = *req.Description + } + if req.Cover != nil { + set["cover"] = *req.Cover + } + if req.Url != nil { + set["url"] = *req.Url + } + if req.Tags != nil { + set["tags"] = *req.Tags + } + if req.Detail != nil { + set["detail"] = *req.Detail + } + if req.Sort != nil { + set["sort"] = *req.Sort + } + if req.IsActive != nil { + set["isActive"] = *req.IsActive + } + if len(set) == 0 { + return nil, nil + } + set["updatedAt"] = time.Now().UTC() + return officialwebsitemod.M{"$set": set}, nil +} + +// ───────────────────────────────────────── +// List types +// ───────────────────────────────────────── + +type NewsListReq struct { + PageReq + ID *string `form:"id"` + Title *string `form:"title"` + Description *string `form:"description"` + Cover *string `form:"cover"` + Url *string `form:"url"` + Tags *string `form:"tags"` + Sort *int64 `form:"sort"` + IsActive *bool `form:"isActive"` + CreatedAt *time.Time `form:"createdAt"` + UpdatedAt *time.Time `form:"updatedAt"` + DeletedAt *time.Time `form:"deletedAt"` +} + +func (r *NewsListReq) Query() (officialwebsitemod.M, error) { + filter := officialwebsitemod.M{} + if err := parseObjectIDFilter(filter, "_id", r.ID); err != nil { + return nil, err + } + if r.Title != nil { + filter["title"] = *r.Title + } + if r.Description != nil { + filter["description"] = *r.Description + } + if r.Cover != nil { + filter["cover"] = *r.Cover + } + if r.Url != nil { + filter["url"] = *r.Url + } + if err := parseJSONFilter(filter, "tags", r.Tags, &[]officialwebsitemod.Tag{}); err != nil { + return nil, err + } + if r.Sort != nil { + filter["sort"] = *r.Sort + } + if r.IsActive != nil { + filter["isActive"] = *r.IsActive + } + if r.CreatedAt != nil { + filter["createdAt"] = *r.CreatedAt + } + if r.UpdatedAt != nil { + filter["updatedAt"] = *r.UpdatedAt + } + if r.DeletedAt != nil { + filter["deletedAt"] = *r.DeletedAt + } + return filter, nil +} diff --git a/web/service/officialWebsiteser/partner.go b/web/service/officialWebsiteser/partner.go new file mode 100644 index 0000000..69b2d1f --- /dev/null +++ b/web/service/officialWebsiteser/partner.go @@ -0,0 +1,211 @@ +package officialwebsiteser + +import ( + "time" + + "91porn-server/common/stderr" + officialwebsitemod "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ───────────────────────────────────────── +// Service functions +// ───────────────────────────────────────── + +func ListPartner(req *PartnerListReq) (int64, []officialwebsitemod.Partner, error) { + filter, err := req.Query() + if err != nil { + return 0, nil, err + } + mod := &officialwebsitemod.Partner{} + total, err := mod.Count(filter) + if err != nil { + return 0, nil, err + } + list, err := mod.FindMany(filter, req.FindOptions()) + if err != nil { + return 0, nil, err + } + return total, list, nil +} + +func CreatePartner(req *ModifyPartnerReq) (officialwebsitemod.Partner, error) { + data := req.toMod() + if err := data.Create(); err != nil { + return officialwebsitemod.Partner{}, err + } + return data, nil +} + +func UpdatePartner(req *ModifyPartnerReq) (resp CountResp, err error) { + if req == nil || req.ID == "" { + return + } + id, parseErr := primitive.ObjectIDFromHex(req.ID) + if parseErr != nil || isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.Partner{} + if err = mod.FindOne(officialwebsitemod.M{"_id": id}); err != nil { + return + } + var update officialwebsitemod.M + if update, err = req.toM(); err != nil || update == nil { + return + } + resp.Count, err = mod.Update(officialwebsitemod.M{"_id": id}, update) + return +} + +func DeletePartner(req *DeletePartnerReq) (data DeletePartnerResp, err error) { + if req == nil { + err = stderr.CodeEmptyData + return + } + var id primitive.ObjectID + if id, err = primitive.ObjectIDFromHex(req.ID); err != nil { + return + } + if isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.Partner{} + data.Count, err = mod.Delete(officialwebsitemod.M{"_id": id}) + return +} + +// ───────────────────────────────────────── +// Mutation (Modify) types +// ───────────────────────────────────────── + +type ModifyPartnerReq struct { + ID string `json:"id" form:"id"` + Name *string `json:"name"` + Description *string `json:"description"` + Logo *string `json:"logo"` + Url *string `json:"url"` + Sort *int64 `json:"sort"` + IsActive *bool `json:"isActive"` +} + +// ───────────────────────────────────────── +// Delete types +// ───────────────────────────────────────── + +type DeletePartnerReq struct { + ID string `json:"id" form:"id"` +} + +type DeletePartnerResp struct { + Count int64 `json:"count"` +} + +// ───────────────────────────────────────── +// toMod — ModifyPartnerReq → model struct +// ───────────────────────────────────────── + +func (req *ModifyPartnerReq) toMod() officialwebsitemod.Partner { + if req == nil { + return officialwebsitemod.Partner{} + } + return officialwebsitemod.Partner{ + Name: stringOrZero(req.Name), + Description: stringOrZero(req.Description), + Logo: stringOrZero(req.Logo), + Url: stringOrZero(req.Url), + SortModel: officialwebsitemod.SortModel{ + Sort: int64OrZero(req.Sort), + IsActive: boolOrZero(req.IsActive), + }, + } +} + +// ───────────────────────────────────────── +// toM — ModifyPartnerReq → officialwebsitemod.M +// ───────────────────────────────────────── + +func (req *ModifyPartnerReq) toM() (officialwebsitemod.M, error) { + if req == nil { + return nil, stderr.CodeEmptyData + } + set := officialwebsitemod.M{} + if req.Name != nil { + set["name"] = *req.Name + } + if req.Description != nil { + set["description"] = *req.Description + } + if req.Logo != nil { + set["logo"] = *req.Logo + } + if req.Url != nil { + set["url"] = *req.Url + } + if req.Sort != nil { + set["sort"] = *req.Sort + } + if req.IsActive != nil { + set["isActive"] = *req.IsActive + } + if len(set) == 0 { + return nil, nil + } + set["updatedAt"] = time.Now().UTC() + return officialwebsitemod.M{"$set": set}, nil +} + +// ───────────────────────────────────────── +// List types +// ───────────────────────────────────────── + +type PartnerListReq struct { + PageReq + ID *string `form:"id"` + Name *string `form:"name"` + Description *string `form:"description"` + Logo *string `form:"logo"` + Url *string `form:"url"` + Sort *int64 `form:"sort"` + IsActive *bool `form:"isActive"` + CreatedAt *time.Time `form:"createdAt"` + UpdatedAt *time.Time `form:"updatedAt"` + DeletedAt *time.Time `form:"deletedAt"` +} + +func (r *PartnerListReq) Query() (officialwebsitemod.M, error) { + filter := officialwebsitemod.M{} + if err := parseObjectIDFilter(filter, "_id", r.ID); err != nil { + return nil, err + } + if r.Name != nil { + filter["name"] = *r.Name + } + if r.Description != nil { + filter["description"] = *r.Description + } + if r.Logo != nil { + filter["logo"] = *r.Logo + } + if r.Url != nil { + filter["url"] = *r.Url + } + if r.Sort != nil { + filter["sort"] = *r.Sort + } + if r.IsActive != nil { + filter["isActive"] = *r.IsActive + } + if r.CreatedAt != nil { + filter["createdAt"] = *r.CreatedAt + } + if r.UpdatedAt != nil { + filter["updatedAt"] = *r.UpdatedAt + } + if r.DeletedAt != nil { + filter["deletedAt"] = *r.DeletedAt + } + return filter, nil +} diff --git a/web/service/officialWebsiteser/photograph.go b/web/service/officialWebsiteser/photograph.go new file mode 100644 index 0000000..2785d3c --- /dev/null +++ b/web/service/officialWebsiteser/photograph.go @@ -0,0 +1,331 @@ +package officialwebsiteser + +import ( + "time" + + "91porn-server/common/stderr" + officialwebsitemod "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ───────────────────────────────────────── +// Service functions +// ───────────────────────────────────────── + +func ListPhotograph(req *ListPhotographReq) (int64, []officialwebsitemod.Photograph, error) { + filter, err := req.Query() + if err != nil { + return 0, nil, err + } + mod := &officialwebsitemod.Photograph{} + total, err := mod.Count(filter) + if err != nil { + return 0, nil, err + } + list, err := mod.FindMany(filter, req.FindOptions()) + if err != nil { + return 0, nil, err + } + return total, list, nil +} + +// resolvePhotographSeoSlug 处理 seoSlug:手填则校验格式并查重(冲突报错);为空则由标题生成雏形并追加后缀去重。 +func resolvePhotographSeoSlug(provided, name string, excludeID primitive.ObjectID) (string, error) { + return ResolveSeoSlug(provided, name, func(slug string) (bool, error) { + filter := officialwebsitemod.M{"seoSlug": slug} + if !excludeID.IsZero() { + filter["_id"] = officialwebsitemod.M{"$ne": excludeID} + } + n, err := (&officialwebsitemod.Photograph{}).Count(filter) + return n > 0, err + }) +} + +func CreatePhotograph(req *ModifyPhotographReq) (officialwebsitemod.Photograph, error) { + data, err := req.toMod() + if err != nil { + return officialwebsitemod.Photograph{}, err + } + // SEO slug:手填优先(校验唯一),为空则由标题生成雏形并去重 + if data.SeoSlug, err = resolvePhotographSeoSlug(data.SeoSlug, data.Name, primitive.NilObjectID); err != nil { + return officialwebsitemod.Photograph{}, err + } + if err := data.Create(); err != nil { + return officialwebsitemod.Photograph{}, err + } + return data, nil +} + +func UpdatePhotograph(req *ModifyPhotographReq) (resp CountResp, err error) { + if req == nil || req.ID == "" { + return + } + id, parseErr := primitive.ObjectIDFromHex(req.ID) + if parseErr != nil || isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.Photograph{} + if err = mod.FindOne(officialwebsitemod.M{"_id": id}); err != nil { + return + } + if isNilObjectID(mod.ID) { + err = stderr.ErrParamError + return + } + // 若提交了 seoSlug,先校验/去重(排除自身),回写后由 toM 落库 + if req.SeoSlug != nil { + var slug string + if slug, err = resolvePhotographSeoSlug(*req.SeoSlug, mod.Name, id); err != nil { + return + } + *req.SeoSlug = slug + } + var update officialwebsitemod.M + if update, err = req.toM(); err != nil || update == nil { + return + } + resp.Count, err = mod.Update(officialwebsitemod.M{"_id": id}, update) + return +} + +func DeletePhotograph(req *DeletePhotographReq) (data DeletePhotographResp, err error) { + if req == nil { + err = stderr.CodeEmptyData + return + } + var id primitive.ObjectID + if id, err = primitive.ObjectIDFromHex(req.ID); err != nil { + return + } + if isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.Photograph{} + data.Count, err = mod.Delete(officialwebsitemod.M{"_id": id}) + return +} + +func BatchUpdatePhotographOwner(req *BatchUpdatePhotographOwnerReq) (data BatchUpdatePhotographOwnerResp, err error) { + if req == nil { + err = stderr.ErrParamError + return + } + + // Check owner is exist + var ownerID primitive.ObjectID + ownerID, err = primitive.ObjectIDFromHex(req.OwnerID) + if err != nil { + return + } + var heroMod = &officialwebsitemod.Hero{} + err = heroMod.FindOne(officialwebsitemod.M{"_id": ownerID}) + if err != nil { + return + } + if isNilObjectID(heroMod.ID) { + err = stderr.OfficialWebsiteHeroExist + return + } + + var photographIDs []primitive.ObjectID + for i := range req.PhotographIDs { + photographID, parseErr := primitive.ObjectIDFromHex(req.PhotographIDs[i]) + err = parseErr + if err != nil { + return + } + photographIDs = append(photographIDs, photographID) + } + var photographMod = &officialwebsitemod.Photograph{} + filter := bson.M{"_id": bson.M{"$in": photographIDs}} + update := bson.M{"$set": bson.M{"heroId": ownerID}} + data.Count, err = photographMod.Update(filter, update) + return +} + +// ───────────────────────────────────────── +// Batch (Modify) types +// ───────────────────────────────────────── + +type BatchUpdatePhotographOwnerReq struct { + PhotographIDs []string `json:"photograph_ids"` + OwnerID string `json:"owner_id"` +} + +type BatchUpdatePhotographOwnerResp struct { + Count int64 `json:"count"` +} + +// ───────────────────────────────────────── +// Mutation (Modify) types +// ───────────────────────────────────────── + +type ModifyPhotographReq struct { + ID string `json:"id" form:"id"` + HeroID *string `json:"heroID"` + Name *string `json:"name"` + SeoSlug *string `json:"seoSlug"` + Description *string `json:"description"` + Cover *string `json:"cover"` + Photos *[]string `json:"photos"` + Tags *[]officialwebsitemod.Tag `json:"tags"` + Sort *int64 `json:"sort"` + IsActive *bool `json:"isActive"` +} + +// ───────────────────────────────────────── +// Delete types +// ───────────────────────────────────────── + +type DeletePhotographReq struct { + ID string `json:"id" form:"id"` +} + +type DeletePhotographResp struct { + Count int64 `json:"count"` +} + +// ───────────────────────────────────────── +// toMod — ModifyPhotographReq → model struct +// ───────────────────────────────────────── + +func (req *ModifyPhotographReq) toMod() (data officialwebsitemod.Photograph, err error) { + if req == nil { + return + } + data = officialwebsitemod.Photograph{ + Name: stringOrZero(req.Name), + Description: stringOrZero(req.Description), + Cover: stringOrZero(req.Cover), + SortModel: officialwebsitemod.SortModel{ + Sort: int64OrZero(req.Sort), + IsActive: boolOrZero(req.IsActive), + }, + } + if req.HeroID != nil { + data.HeroID, err = primitive.ObjectIDFromHex(stringOrZero(req.HeroID)) + if err != nil { + return + } + } + if req.Photos != nil { + data.Photos = *req.Photos + } + if req.Tags != nil { + data.Tags = *req.Tags + } + if req.SeoSlug != nil { + data.SeoSlug = *req.SeoSlug + } + return +} + +// ───────────────────────────────────────── +// toM — ModifyPhotographReq → officialwebsitemod.M +// ───────────────────────────────────────── + +func (req *ModifyPhotographReq) toM() (officialwebsitemod.M, error) { + if req == nil { + return nil, stderr.CodeEmptyData + } + set := officialwebsitemod.M{} + if req.HeroID != nil { + if *req.HeroID != "" { + heroID, err := primitive.ObjectIDFromHex(*req.HeroID) + if err != nil || isNilObjectID(heroID) { + return nil, stderr.ErrParamError + } + set["heroId"] = heroID + } + } + if req.Name != nil { + set["name"] = *req.Name + } + if req.SeoSlug != nil && *req.SeoSlug != "" { + set["seoSlug"] = *req.SeoSlug + } + if req.Description != nil { + set["description"] = *req.Description + } + if req.Cover != nil { + set["cover"] = *req.Cover + } + if req.Photos != nil { + set["photos"] = *req.Photos + } + if req.Tags != nil { + set["tags"] = *req.Tags + } + if req.Sort != nil { + set["sort"] = *req.Sort + } + if req.IsActive != nil { + set["isActive"] = *req.IsActive + } + if len(set) == 0 { + return nil, nil + } + set["updatedAt"] = time.Now().UTC() + return officialwebsitemod.M{"$set": set}, nil +} + +// ───────────────────────────────────────── +// List types +// ───────────────────────────────────────── + +type ListPhotographReq struct { + PageReq + ID *string `form:"id"` + HeroID *string `form:"heroID"` + Name *string `form:"name"` + Description *string `form:"description"` + Cover *string `form:"cover"` + Photos *string `form:"photos"` + Sort *int64 `form:"sort"` + IsActive *bool `form:"isActive"` + CreatedAt *time.Time `form:"createdAt"` + UpdatedAt *time.Time `form:"updatedAt"` + DeletedAt *time.Time `form:"deletedAt"` +} + +func (l *ListPhotographReq) Query() (filter officialwebsitemod.M, err error) { + filter = officialwebsitemod.M{} + if err = parseObjectIDFilter(filter, "_id", l.ID); err != nil { + return nil, err + } + if err = parseObjectIDFilter(filter, "heroId", l.HeroID); err != nil { + return nil, err + } + if l.Name != nil { + filter["name"] = *l.Name + } + if l.Description != nil { + filter["description"] = *l.Description + } + if l.Cover != nil { + filter["cover"] = *l.Cover + } + if err = parseJSONFilter(filter, "photos", l.Photos, &[]string{}); err != nil { + return nil, err + } + if l.Sort != nil { + filter["sort"] = *l.Sort + } + if l.IsActive != nil { + filter["isActive"] = *l.IsActive + } + if l.CreatedAt != nil { + filter["createdAt"] = *l.CreatedAt + } + if l.UpdatedAt != nil { + filter["updatedAt"] = *l.UpdatedAt + } + if l.DeletedAt != nil { + filter["deletedAt"] = *l.DeletedAt + } + return filter, nil +} diff --git a/web/service/officialWebsiteser/recruit_form.go b/web/service/officialWebsiteser/recruit_form.go new file mode 100644 index 0000000..d7f766a --- /dev/null +++ b/web/service/officialWebsiteser/recruit_form.go @@ -0,0 +1,221 @@ +package officialwebsiteser + +import ( + "time" + + "91porn-server/common/stderr" + officialwebsitemod "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ───────────────────────────────────────── +// Service functions +// ───────────────────────────────────────── + +func ListRecruitForm(req *RecruitFormListReq) (int64, []officialwebsitemod.RecruitForm, error) { + filter, err := req.Query() + if err != nil { + return 0, nil, err + } + mod := &officialwebsitemod.RecruitForm{} + total, err := mod.Count(filter) + if err != nil { + return 0, nil, err + } + list, err := mod.FindMany(filter, req.FindOptions()) + if err != nil { + return 0, nil, err + } + return total, list, nil +} + +func CreateRecruitForm(req *ModifyRecruitFormReq) (officialwebsitemod.RecruitForm, error) { + data := req.toMod() + if err := data.Create(); err != nil { + return officialwebsitemod.RecruitForm{}, err + } + return data, nil +} + +func UpdateRecruitForm(req *ModifyRecruitFormReq) (resp CountResp, err error) { + if req == nil || req.ID == "" { + return + } + id, parseErr := primitive.ObjectIDFromHex(req.ID) + if parseErr != nil || isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.RecruitForm{} + if err = mod.FindOne(officialwebsitemod.M{"_id": id}); err != nil { + return + } + var update officialwebsitemod.M + if update, err = req.toM(); err != nil || update == nil { + return + } + resp.Count, err = mod.Update(officialwebsitemod.M{"_id": id}, update) + return +} + +func DeleteRecruitForm(req *DeleteRecruitFormReq) (data DeleteRecruitFormResp, err error) { + if req == nil { + err = stderr.CodeEmptyData + return + } + var id primitive.ObjectID + if id, err = primitive.ObjectIDFromHex(req.ID); err != nil { + return + } + if isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.RecruitForm{} + data.Count, err = mod.Delete(officialwebsitemod.M{"_id": id}) + return +} + +// ───────────────────────────────────────── +// Mutation (Modify) types +// ───────────────────────────────────────── + +type ModifyRecruitFormReq struct { + ID string `json:"id" form:"id"` + Name *string `json:"name"` + Sex *int `json:"sex"` + Age *int `json:"age"` + Country *string `json:"country"` + Address *string `json:"address"` + Contact *[]officialwebsitemod.ContactInfo `json:"contact"` + Description *string `json:"description"` +} + +// ───────────────────────────────────────── +// Delete types +// ───────────────────────────────────────── + +type DeleteRecruitFormReq struct { + ID string `json:"id" form:"id"` +} + +type DeleteRecruitFormResp struct { + Count int64 `json:"count"` +} + +// ───────────────────────────────────────── +// toMod — ModifyRecruitFormReq → model struct +// ───────────────────────────────────────── + +func (req *ModifyRecruitFormReq) toMod() officialwebsitemod.RecruitForm { + if req == nil { + return officialwebsitemod.RecruitForm{} + } + data := officialwebsitemod.RecruitForm{ + Name: stringOrZero(req.Name), + Sex: intOrZero(req.Sex), + Age: intOrZero(req.Age), + Country: stringOrZero(req.Country), + Address: stringOrZero(req.Address), + Description: stringOrZero(req.Description), + } + if req.Contact != nil { + data.Contact = *req.Contact + } + return data +} + +// ───────────────────────────────────────── +// toM — ModifyRecruitFormReq → officialwebsitemod.M +// ───────────────────────────────────────── + +func (req *ModifyRecruitFormReq) toM() (officialwebsitemod.M, error) { + if req == nil { + return nil, stderr.CodeEmptyData + } + set := officialwebsitemod.M{} + if req.Name != nil { + set["name"] = *req.Name + } + if req.Sex != nil { + set["sex"] = *req.Sex + } + if req.Age != nil { + set["age"] = *req.Age + } + if req.Country != nil { + set["country"] = *req.Country + } + if req.Address != nil { + set["address"] = *req.Address + } + if req.Contact != nil { + set["contact"] = *req.Contact + } + if req.Description != nil { + set["description"] = *req.Description + } + if len(set) == 0 { + return nil, nil + } + set["updatedAt"] = time.Now().UTC() + return officialwebsitemod.M{"$set": set}, nil +} + +// ───────────────────────────────────────── +// List types +// ───────────────────────────────────────── + +type RecruitFormListReq struct { + PageReq + ID *string `form:"id"` + Name *string `form:"name"` + Sex *int `form:"sex"` + Age *int `form:"age"` + Country *string `form:"country"` + Address *string `form:"address"` + Contact *string `form:"contact"` + Description *string `form:"description"` + CreatedAt *time.Time `form:"createdAt"` + UpdatedAt *time.Time `form:"updatedAt"` + DeletedAt *time.Time `form:"deletedAt"` +} + +func (r *RecruitFormListReq) Query() (officialwebsitemod.M, error) { + filter := officialwebsitemod.M{} + if err := parseObjectIDFilter(filter, "_id", r.ID); err != nil { + return nil, err + } + if r.Name != nil { + filter["name"] = *r.Name + } + if r.Sex != nil { + filter["sex"] = *r.Sex + } + if r.Age != nil { + filter["age"] = *r.Age + } + if r.Country != nil { + filter["country"] = *r.Country + } + if r.Address != nil { + filter["address"] = *r.Address + } + if err := parseJSONFilter(filter, "contact", r.Contact, &[]officialwebsitemod.ContactInfo{}); err != nil { + return nil, err + } + if r.Description != nil { + filter["description"] = *r.Description + } + if r.CreatedAt != nil { + filter["createdAt"] = *r.CreatedAt + } + if r.UpdatedAt != nil { + filter["updatedAt"] = *r.UpdatedAt + } + if r.DeletedAt != nil { + filter["deletedAt"] = *r.DeletedAt + } + return filter, nil +} diff --git a/web/service/officialWebsiteser/slug.go b/web/service/officialWebsiteser/slug.go new file mode 100644 index 0000000..bddbab3 --- /dev/null +++ b/web/service/officialWebsiteser/slug.go @@ -0,0 +1,113 @@ +package officialwebsiteser + +import ( + "regexp" + "strings" + + "91porn-server/common/stderr" + + "github.com/mozillazg/go-pinyin" +) + +var ( + seoSlugValidRe = regexp.MustCompile(`^[a-z]+$`) // 纯小写英文字母 + seoSlugStripRe = regexp.MustCompile(`[^a-z]`) // 非 a-z 一律剔除 +) + +// seoSlugMaxLen 自动生成 slug 的最大长度(按拼音音节边界截断,避免长标题生成超长 slug) +const seoSlugMaxLen = 20 + +// GenerateSeoSlug 从标题生成纯小写字母 slug:中文转拼音、英文保留、其余字符剔除, +// 并按音节边界累加到 seoSlugMaxLen 为止(不从音节中间截断)。 +// 例: "优雅Summer 2023" -> "youyasummer" +func GenerateSeoSlug(title string) string { + if title == "" { + return "" + } + args := pinyin.NewArgs() + // 非汉字(英文/数字/符号)原样返回,后续再统一剔除非 a-z + args.Fallback = func(r rune, a pinyin.Args) []string { + return []string{string(r)} + } + rows := pinyin.Pinyin(title, args) + var b strings.Builder + for _, row := range rows { + if len(row) == 0 { + continue + } + // 单个音节先剔除非 a-z、转小写 + syl := seoSlugStripRe.ReplaceAllString(strings.ToLower(row[0]), "") + if syl == "" { + continue + } + // 到长度上限则在音节边界停止,避免 slug 过长 + if b.Len()+len(syl) > seoSlugMaxLen { + break + } + b.WriteString(syl) + } + return b.String() +} + +// ValidateSeoSlug 校验 slug 格式:非空且纯小写英文字母。 +func ValidateSeoSlug(s string) bool { + return seoSlugValidRe.MatchString(s) +} + +// ResolveSeoSlug 统一处理 seoSlug: +// - 手填(provided 非空):校验纯小写字母格式,并查重(冲突报错) +// - 留空:由 title 生成拼音雏形,冲突时追加后缀去重 +// +// exists 由各表提供(查各自表是否已占用该 slug)。 +func ResolveSeoSlug(provided, title string, exists func(slug string) (bool, error)) (string, error) { + if provided != "" { + if !ValidateSeoSlug(provided) { + return "", stderr.ErrParamError + } + used, err := exists(provided) + if err != nil { + return "", err + } + if used { + return "", stderr.ErrParamError + } + return provided, nil + } + return UniqueSeoSlug(GenerateSeoSlug(title), exists) +} + +// UniqueSeoSlug 基于 base 生成同表唯一的 slug;exists 判断某 slug 是否已被占用。 +// 冲突时依次追加纯小写字母后缀 a、b…z、aa、ab…zz、aaa… 直到命中未占用值; +// base 为空则返回空(走稀疏索引,不参与唯一约束)。 +func UniqueSeoSlug(base string, exists func(slug string) (bool, error)) (string, error) { + if base == "" { + return "", nil + } + // n=0 先试 base 原值,之后依次追加 a、b…z、aa… 后缀(双射 26 进制递增)。 + // 表内记录有限且各候选互不相同,必能在有限次内命中未占用值。 + for n := 0; ; n++ { + candidate := base + if n > 0 { + candidate += seoSlugSuffix(n) + } + used, err := exists(candidate) + if err != nil { + return "", err + } + if !used { + return candidate, nil + } + } +} + +// seoSlugSuffix 返回第 n 个纯小写字母后缀(n>=1),按双射 26 进制递增: +// 1->a 2->b … 26->z 27->aa 28->ab … 52->az 53->ba … 702->zz 703->aaa +func seoSlugSuffix(n int) string { + var buf []byte + for n > 0 { + n-- + buf = append([]byte{byte('a' + n%26)}, buf...) + n /= 26 + } + return string(buf) +} diff --git a/web/service/officialWebsiteser/tag.go b/web/service/officialWebsiteser/tag.go new file mode 100644 index 0000000..cde59fe --- /dev/null +++ b/web/service/officialWebsiteser/tag.go @@ -0,0 +1,202 @@ +package officialwebsiteser + +import ( + "time" + + "91porn-server/common/stderr" + officialwebsitemod "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ───────────────────────────────────────── +// Service functions +// ───────────────────────────────────────── + +func ListTag(req *TagListReq) (int64, []officialwebsitemod.Tag, error) { + filter, err := req.Query() + if err != nil { + return 0, nil, err + } + mod := &officialwebsitemod.Tag{} + total, err := mod.Count(filter) + if err != nil { + return 0, nil, err + } + list, err := mod.FindMany(filter, req.FindOptions()) + if err != nil { + return 0, nil, err + } + return total, list, nil +} + +func CreateTag(req *ModifyTagReq) (officialwebsitemod.Tag, error) { + data := req.toMod() + if err := data.Create(); err != nil { + return officialwebsitemod.Tag{}, err + } + return data, nil +} + +func UpdateTag(req *ModifyTagReq) (resp CountResp, err error) { + if req == nil || req.ID == "" { + return + } + id, parseErr := primitive.ObjectIDFromHex(req.ID) + if parseErr != nil || isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.Tag{} + if err = mod.FindOne(officialwebsitemod.M{"_id": id}); err != nil { + return + } + var update officialwebsitemod.M + if update, err = req.toM(); err != nil || update == nil { + return + } + resp.Count, err = mod.Update(officialwebsitemod.M{"_id": id}, update) + return +} + +func DeleteTag(req *DeleteTagReq) (data DeleteTagResp, err error) { + if req == nil { + err = stderr.CodeEmptyData + return + } + var id primitive.ObjectID + if id, err = primitive.ObjectIDFromHex(req.ID); err != nil { + return + } + if isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.Tag{} + data.Count, err = mod.Delete(officialwebsitemod.M{"_id": id}) + return +} + +// ───────────────────────────────────────── +// Mutation (Modify) types +// ───────────────────────────────────────── + +type ModifyTagReq struct { + ID string `json:"id" form:"id"` + Name *string `json:"name"` + Type *string `json:"type"` + IsHot *bool `json:"isHot"` + Sort *int64 `json:"sort"` + IsActive *bool `json:"isActive"` +} + +// ───────────────────────────────────────── +// Delete types +// ───────────────────────────────────────── + +type DeleteTagReq struct { + ID string `json:"id" form:"id"` +} + +type DeleteTagResp struct { + Count int64 `json:"count"` +} + +// ───────────────────────────────────────── +// toMod — ModifyTagReq → model struct +// ───────────────────────────────────────── + +func (req *ModifyTagReq) toMod() officialwebsitemod.Tag { + if req == nil { + return officialwebsitemod.Tag{} + } + return officialwebsitemod.Tag{ + Name: stringOrZero(req.Name), + Type: stringOrZero(req.Type), + IsHot: boolOrZero(req.IsHot), + SortModel: officialwebsitemod.SortModel{ + Sort: int64OrZero(req.Sort), + IsActive: boolOrZero(req.IsActive), + }, + } +} + +// ───────────────────────────────────────── +// toM — ModifyTagReq → officialwebsitemod.M +// ───────────────────────────────────────── + +func (req *ModifyTagReq) toM() (officialwebsitemod.M, error) { + if req == nil { + return nil, stderr.CodeEmptyData + } + set := officialwebsitemod.M{} + if req.Name != nil { + set["name"] = *req.Name + } + if req.Type != nil { + set["type"] = *req.Type + } + if req.IsHot != nil { + set["isHot"] = *req.IsHot + } + if req.Sort != nil { + set["sort"] = *req.Sort + } + if req.IsActive != nil { + set["isActive"] = *req.IsActive + } + if len(set) == 0 { + return nil, nil + } + set["updatedAt"] = time.Now().UTC() + return officialwebsitemod.M{"$set": set}, nil +} + +// ───────────────────────────────────────── +// List types +// ───────────────────────────────────────── + +type TagListReq struct { + PageReq + ID *string `form:"id"` + Name *string `form:"name"` + Type *string `form:"type"` + IsHot *bool `form:"isHot"` + Sort *int64 `form:"sort"` + IsActive *bool `form:"isActive"` + CreatedAt *time.Time `form:"createdAt"` + UpdatedAt *time.Time `form:"updatedAt"` + DeletedAt *time.Time `form:"deletedAt"` +} + +func (r *TagListReq) Query() (officialwebsitemod.M, error) { + filter := officialwebsitemod.M{} + if err := parseObjectIDFilter(filter, "_id", r.ID); err != nil { + return nil, err + } + if r.Name != nil { + filter["name"] = *r.Name + } + if r.Type != nil { + filter["type"] = *r.Type + } + if r.IsHot != nil { + filter["isHot"] = *r.IsHot + } + if r.Sort != nil { + filter["sort"] = *r.Sort + } + if r.IsActive != nil { + filter["isActive"] = *r.IsActive + } + if r.CreatedAt != nil { + filter["createdAt"] = *r.CreatedAt + } + if r.UpdatedAt != nil { + filter["updatedAt"] = *r.UpdatedAt + } + if r.DeletedAt != nil { + filter["deletedAt"] = *r.DeletedAt + } + return filter, nil +} diff --git a/web/service/officialWebsiteser/video.go b/web/service/officialWebsiteser/video.go new file mode 100644 index 0000000..e2709a8 --- /dev/null +++ b/web/service/officialWebsiteser/video.go @@ -0,0 +1,652 @@ +package officialwebsiteser + +import ( + "91porn-server/models/v/tagmod" + "91porn-server/models/v/vidmod" + "math/rand/v2" + "time" + + "91porn-server/common/stderr" + officialwebsitemod "91porn-server/models/v/officialWebsitemod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ───────────────────────────────────────── +// Service functions +// ───────────────────────────────────────── + +func CheckVideoExistByVid(vid []primitive.ObjectID) ([]officialwebsitemod.Video, error) { + filter := bson.M{"_id": bson.M{"$in": vid}} + mod := &officialwebsitemod.Video{} + videos, err := mod.FindMany(filter) + return videos, err +} + +func ListVideo(req *VideoListReq) (int64, []officialwebsitemod.Video, error) { + filter, err := req.Query() + if err != nil { + return 0, nil, err + } + mod := &officialwebsitemod.Video{} + total, err := mod.Count(filter) + if err != nil { + return 0, nil, err + } + list, err := mod.FindMany(filter, req.FindOptions()) + if err != nil { + return 0, nil, err + } + return total, list, nil +} + +// resolveVideoSeoSlug 处理 seoSlug:手填则校验格式并查重(冲突报错);为空则由标题生成雏形并追加后缀去重。 +func resolveVideoSeoSlug(provided, title string, excludeID primitive.ObjectID) (string, error) { + exists := func(slug string) (bool, error) { + filter := officialwebsitemod.M{"seoSlug": slug} + if !excludeID.IsZero() { + filter["_id"] = officialwebsitemod.M{"$ne": excludeID} + } + n, err := (&officialwebsitemod.Video{}).Count(filter) + return n > 0, err + } + if provided != "" { + if !ValidateSeoSlug(provided) { + return "", stderr.ErrParamError + } + used, err := exists(provided) + if err != nil { + return "", err + } + if used { + return "", stderr.ErrParamError + } + return provided, nil + } + return UniqueSeoSlug(GenerateSeoSlug(title), exists) +} + +func CreateVideo(req *ModifyVideoReq) (officialwebsitemod.Video, error) { + data, err := req.toMod() + if err != nil { + return officialwebsitemod.Video{}, err + } + // SEO slug:手填优先(校验唯一),为空则由标题生成雏形并去重 + if data.SeoSlug, err = resolveVideoSeoSlug(data.SeoSlug, data.Title, primitive.NilObjectID); err != nil { + return officialwebsitemod.Video{}, err + } + if err := data.Create(); err != nil { + return officialwebsitemod.Video{}, err + } + return data, nil +} + +func UpdateVideo(req *ModifyVideoReq) (resp CountResp, err error) { + if req == nil || req.ID == "" { + return + } + id, parseErr := primitive.ObjectIDFromHex(req.ID) + if parseErr != nil || isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.Video{} + if err = mod.FindOne(officialwebsitemod.M{"_id": id}); err != nil { + return + } + // 若提交了 seoSlug,先校验/去重(排除自身),回写后由 toM 落库 + if req.SeoSlug != nil { + var slug string + if slug, err = resolveVideoSeoSlug(*req.SeoSlug, mod.Title, id); err != nil { + return + } + *req.SeoSlug = slug + } + var update officialwebsitemod.M + if update, err = req.toM(); err != nil || update == nil { + return + } + resp.Count, err = mod.Update(officialwebsitemod.M{"_id": id}, update) + return +} + +func DeleteVideo(req *DeleteVideoReq) (data DeleteVideoResp, err error) { + if req == nil { + err = stderr.CodeEmptyData + return + } + var id primitive.ObjectID + if id, err = primitive.ObjectIDFromHex(req.ID); err != nil { + return + } + if isNilObjectID(id) { + err = stderr.ErrParamError + return + } + mod := &officialwebsitemod.Video{} + data.Count, err = mod.Delete(officialwebsitemod.M{"_id": id}) + return +} + +func BatchUpdateVideoOwner(req *BatchUpdateVideoOwnerReq) (data BatchUpdateVideoOwnerResp, err error) { + if req == nil { + err = stderr.ErrParamError + return + } + + // Check owner is exist + var ownerID primitive.ObjectID + ownerID, err = primitive.ObjectIDFromHex(req.OwnerID) + if err != nil { + return + } + var heroMod = &officialwebsitemod.Hero{} + err = heroMod.FindOne(officialwebsitemod.M{"_id": ownerID}) + if err != nil { + return + } + if isNilObjectID(heroMod.ID) { + err = stderr.OfficialWebsiteHeroExist + return + } + + var update = bson.M{"$set": bson.M{"heroId": ownerID}} + // Check album is exist, if exist then add into update + if req.AlbumID != "" { + var albumID primitive.ObjectID + albumID, err = primitive.ObjectIDFromHex(req.AlbumID) + if err != nil { + return + } + var album = &officialwebsitemod.Album{} + err = album.FindOne(officialwebsitemod.M{"_id": albumID}) + if err != nil { + return + } + update = bson.M{"$set": bson.M{"heroId": ownerID, "albumId": albumID}} + } + + var videoIDs []primitive.ObjectID + for i := range req.VideoIDs { + videoID, parseErr := primitive.ObjectIDFromHex(req.VideoIDs[i]) + err = parseErr + if err != nil { + return + } + videoIDs = append(videoIDs, videoID) + } + var videoMod = &officialwebsitemod.Video{} + filter := bson.M{"_id": bson.M{"$in": videoIDs}} + data.Count, err = videoMod.UpdateMany(filter, update) + return +} + +// ───────────────────────────────────────── +// Batch (Modify) types +// ───────────────────────────────────────── + +type BatchUpdateVideoOwnerReq struct { + VideoIDs []string `json:"video_ids"` + OwnerID string `json:"owner_id"` + AlbumID string `json:"album_id"` +} + +type BatchUpdateVideoOwnerResp struct { + Count int64 `json:"count"` +} + +// ───────────────────────────────────────── +// Mutation (Modify) types +// ───────────────────────────────────────── + +type ModifyVideoReq struct { + ID string `json:"id" form:"id"` + AlbumID *string `json:"albumId" form:"albumId"` + HeroID *string `json:"heroId" form:"heroId"` + Title *string `json:"title"` + Description *string `json:"description"` + Cover *string `json:"cover"` + Url *string `json:"url"` + SeoSlug *string `json:"seoSlug"` + Tags *[]officialwebsitemod.Tag `json:"tags"` + IsHot *bool `json:"isHot"` + Sort *int64 `json:"sort"` + IsActive *bool `json:"isActive"` + WatchCount *int64 `json:"watchCount"` + LikeCount *int64 `json:"likeCount"` + CommentCount *int64 `json:"commentCount"` + CollectCount *int64 `json:"collectCount"` +} + +// ───────────────────────────────────────── +// Delete types +// ───────────────────────────────────────── + +type DeleteVideoReq struct { + ID string `json:"id" form:"id"` +} + +type DeleteVideoResp struct { + Count int64 `json:"count"` +} + +// ───────────────────────────────────────── +// toMod — ModifyVideoReq → model struct +// ───────────────────────────────────────── + +func (req *ModifyVideoReq) toMod() (officialwebsitemod.Video, error) { + if req == nil { + return officialwebsitemod.Video{}, nil + } + data := officialwebsitemod.Video{ + Title: stringOrZero(req.Title), + Description: stringOrZero(req.Description), + Cover: stringOrZero(req.Cover), + Url: stringOrZero(req.Url), + IsHot: boolOrZero(req.IsHot), + SortModel: officialwebsitemod.SortModel{ + Sort: int64OrZero(req.Sort), + IsActive: boolOrZero(req.IsActive), + }, + WatchCount: int64OrZero(req.WatchCount), + LikeCount: int64OrZero(req.LikeCount), + CommentCount: int64OrZero(req.CommentCount), + CollectCount: int64OrZero(req.CollectCount), + } + if req.AlbumID != nil { + if *req.AlbumID != "" { + albumID, err := primitive.ObjectIDFromHex(*req.AlbumID) + if err != nil || isNilObjectID(albumID) { + return officialwebsitemod.Video{}, stderr.ErrParamError + } + data.AlbumID = albumID + } + } + if req.HeroID != nil { + if *req.HeroID != "" { + heroID, err := primitive.ObjectIDFromHex(*req.HeroID) + if err != nil || isNilObjectID(heroID) { + return officialwebsitemod.Video{}, stderr.ErrParamError + } + data.HeroID = heroID + } + } + if req.Tags != nil { + data.Tags = *req.Tags + } + if req.SeoSlug != nil { + data.SeoSlug = *req.SeoSlug + } + return data, nil +} + +// ───────────────────────────────────────── +// toM — ModifyVideoReq → officialwebsitemod.M +// ───────────────────────────────────────── + +func (req *ModifyVideoReq) toM() (officialwebsitemod.M, error) { + if req == nil { + return nil, stderr.CodeEmptyData + } + set := officialwebsitemod.M{} + if req.AlbumID != nil { + if *req.AlbumID != "" { + albumID, err := primitive.ObjectIDFromHex(*req.AlbumID) + if err != nil || isNilObjectID(albumID) { + return nil, stderr.ErrParamError + } + set["albumId"] = albumID + } + } + if req.HeroID != nil { + if *req.HeroID != "" { + heroID, err := primitive.ObjectIDFromHex(*req.HeroID) + if err != nil || isNilObjectID(heroID) { + return nil, stderr.ErrParamError + } + set["heroId"] = heroID + } + } + if req.Title != nil { + set["title"] = *req.Title + } + if req.Description != nil { + set["description"] = *req.Description + } + if req.Cover != nil { + set["cover"] = *req.Cover + } + if req.Url != nil { + set["url"] = *req.Url + } + if req.Tags != nil { + set["tags"] = *req.Tags + } + if req.SeoSlug != nil && *req.SeoSlug != "" { + set["seoSlug"] = *req.SeoSlug + } + if req.IsHot != nil { + set["isHot"] = *req.IsHot + } + if req.Sort != nil { + set["sort"] = *req.Sort + } + if req.IsActive != nil { + set["isActive"] = *req.IsActive + } + if req.WatchCount != nil { + set["watchCount"] = *req.WatchCount + } + if req.LikeCount != nil { + set["likeCount"] = *req.LikeCount + } + if req.CommentCount != nil { + set["commentCount"] = *req.CommentCount + } + if req.CollectCount != nil { + set["collectCount"] = *req.CollectCount + } + if len(set) == 0 { + return nil, nil + } + set["updatedAt"] = time.Now().UTC() + return officialwebsitemod.M{"$set": set}, nil +} + +func BatchImportVideo(req *BatchImportVideoReq) (resp BatchImportVideoResp, err error) { + if req == nil || len(req.VideoIds) == 0 { + err = stderr.ErrParamError + return + } + objIds := make([]primitive.ObjectID, 0, len(req.VideoIds)) + videoIDSet := make(map[primitive.ObjectID]struct{}, len(req.VideoIds)) + for _, videoId := range req.VideoIds { + var id primitive.ObjectID + id, err = primitive.ObjectIDFromHex(videoId) + if err != nil { + return + } + if _, exists := videoIDSet[id]; exists { + continue + } + videoIDSet[id] = struct{}{} + objIds = append(objIds, id) + } + if len(objIds) == 0 { + return + } + + // 1) Filter out videos that already exist in official website video table by `_id`. + var videoMod = &officialwebsitemod.Video{} + officialVideos, findVideoErr := videoMod.FindMany(officialwebsitemod.M{"_id": bson.M{"$in": objIds}}) + if findVideoErr != nil { + err = findVideoErr + return + } + existedVideoMap := make(map[primitive.ObjectID]struct{}, len(officialVideos)) + for _, v := range officialVideos { + existedVideoMap[v.ID] = struct{}{} + } + + needImportVideoIDs := make([]primitive.ObjectID, 0, len(objIds)) + for _, id := range objIds { + if _, exists := existedVideoMap[id]; exists { + continue + } + needImportVideoIDs = append(needImportVideoIDs, id) + } + if len(needImportVideoIDs) == 0 { + return + } + + mats := []vidmod.Matcher{ + (&vidmod.IDInMatch{IDs: needImportVideoIDs}).New(), + } + list, err := vidmod.FindMany(mats...) + if err != nil { + return + } + if len(list) == 0 { + return + } + + var insertVideos []officialwebsitemod.Video + allTagIDs := make([]primitive.ObjectID, 0) + allTagIDSet := make(map[primitive.ObjectID]struct{}) + for _, video := range list { + insertVideos = append(insertVideos, officialwebsitemod.Video{ + ID: video.ID, + Title: video.Title, + Description: video.Content, + Cover: video.Cover, + Url: video.SourceURL, + IsHot: false, + WatchCount: int64(video.FakePlayCount+video.PlayCount) * rand.Int64N(10), + LikeCount: int64(video.FakeLikeCount+video.LikeCount) * rand.Int64N(10), + CommentCount: int64(video.FakeCommentCount+video.CommentCount) * rand.Int64N(10), + CollectCount: int64(video.CollectCount) * rand.Int64N(10), + SortModel: officialwebsitemod.SortModel{ + IsActive: true, + }, + BaseModel: officialwebsitemod.BaseModel{ + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + }, + }) + for _, tag := range video.Tags { + if _, exists := allTagIDSet[tag]; exists { + continue + } + allTagIDSet[tag] = struct{}{} + allTagIDs = append(allTagIDs, tag) + } + } + + if len(insertVideos) == 0 { + return + } + + // 2) Filter out tags that already exist in official website tag table by `_id`. + var tagMod = &officialwebsitemod.Tag{} + existedTagMap := map[primitive.ObjectID]struct{}{} + existedOfficialTagInfoMap := map[primitive.ObjectID]officialwebsitemod.Tag{} + if len(allTagIDs) > 0 { + officialWebsiteTags, findTagErr := tagMod.FindMany(officialwebsitemod.M{ + "_id": bson.M{"$in": allTagIDs}, + "type": officialwebsitemod.OfficialWebsiteTagTypeVideo, + }) + if findTagErr != nil { + err = findTagErr + return + } + existedTagMap = make(map[primitive.ObjectID]struct{}, len(officialWebsiteTags)) + for _, t := range officialWebsiteTags { + existedTagMap[t.ID] = struct{}{} + existedOfficialTagInfoMap[t.ID] = t + } + } + + needImportTagIDs := make([]primitive.ObjectID, 0, len(allTagIDs)) + for _, tagID := range allTagIDs { + if _, exists := existedTagMap[tagID]; exists { + continue + } + needImportTagIDs = append(needImportTagIDs, tagID) + } + + tagsMap, err := tagmod.FindTagsMapByIDS(needImportTagIDs) + if err != nil { + return + } + + var insertTags []officialwebsitemod.Tag + for _, tagID := range needImportTagIDs { + tag, ok := tagsMap[tagID] + if !ok { + continue + } + insertTags = append(insertTags, officialwebsitemod.Tag{ + ID: tag.ID, + Name: tag.TagName, + Type: officialwebsitemod.OfficialWebsiteTagTypeVideo, + IsHot: len(tag.HotMark) > 0, + SortModel: officialwebsitemod.SortModel{ + IsActive: true, + }, + BaseModel: officialwebsitemod.BaseModel{ + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + }, + }) + } + + // Fill video tags from tag info map to keep video docs self-contained. + for i := range insertVideos { + srcVideo := list[i] + videoTags := make([]officialwebsitemod.Tag, 0, len(srcVideo.Tags)) + for _, tagID := range srcVideo.Tags { + if oldTag, exists := existedOfficialTagInfoMap[tagID]; exists { + videoTags = append(videoTags, officialwebsitemod.Tag{ + ID: oldTag.ID, + Name: oldTag.Name, + Type: officialwebsitemod.OfficialWebsiteTagTypeVideo, + IsHot: oldTag.IsHot, + SortModel: officialwebsitemod.SortModel{ + Sort: oldTag.Sort, + IsActive: true, + }, + }) + continue + } + tag, ok := tagsMap[tagID] + if !ok { + continue + } + videoTags = append(videoTags, officialwebsitemod.Tag{ + ID: tag.ID, + Name: tag.TagName, + Type: officialwebsitemod.OfficialWebsiteTagTypeVideo, + IsHot: len(tag.HotMark) > 0, + SortModel: officialwebsitemod.SortModel{ + IsActive: true, + }, + }) + } + insertVideos[i].Tags = videoTags + } + + // 3) Insert filtered videos and tags into official website tables separately. + var ids []primitive.ObjectID + ids, err = videoMod.InsertMany(insertVideos) + if err != nil { + return + } + + for i := range insertTags { + if createErr := insertTags[i].Create(); createErr != nil { + err = createErr + return + } + } + + resp.Count = int64(len(ids)) + return +} + +// ───────────────────────────────────────── +// Batch Import types +// ───────────────────────────────────────── + +type BatchImportVideoReq struct { + VideoIds []string `json:"videoIds"` +} + +type BatchImportVideoResp struct { + Count int64 `json:"count"` +} + +// ───────────────────────────────────────── +// List types +// ───────────────────────────────────────── + +type VideoListReq struct { + PageReq + ID *string `form:"id"` + IDs []string `form:"ids"` + PrimiteveIDs []primitive.ObjectID `form:"primitiveIds"` + AlbumID *string `form:"albumId"` + HeroID *string `form:"heroID"` + Title *string `form:"title"` + Description *string `form:"description"` + Cover *string `form:"cover"` + Url *string `form:"url"` + Tags *string `form:"tags"` + IsHot *bool `form:"isHot"` + Sort *int64 `form:"sort"` + IsActive *bool `form:"isActive"` + CreatedAt *time.Time `form:"createdAt"` + UpdatedAt *time.Time `form:"updatedAt"` + DeletedAt *time.Time `form:"deletedAt"` +} + +func (r *VideoListReq) Query() (officialwebsitemod.M, error) { + filter := officialwebsitemod.M{} + if err := parseObjectIDFilter(filter, "_id", r.ID); err != nil { + return nil, err + } + if len(r.IDs) > 0 { + var objIDs []primitive.ObjectID + for _, id := range r.IDs { + objID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + objIDs = append(objIDs, objID) + } + filter["_id"] = bson.M{"$in": objIDs} + } + if len(r.PrimiteveIDs) > 0 { + filter["_id"] = bson.M{"$in": r.PrimiteveIDs} + } + if err := parseObjectIDFilter(filter, "albumId", r.AlbumID); err != nil { + return nil, err + } + if err := parseObjectIDFilter(filter, "heroId", r.HeroID); err != nil { + return nil, err + } + if r.Title != nil { + filter["title"] = *r.Title + } + if r.Description != nil { + filter["description"] = *r.Description + } + if r.Cover != nil { + filter["cover"] = *r.Cover + } + if r.Url != nil { + filter["url"] = *r.Url + } + if err := parseJSONFilter(filter, "tags", r.Tags, &[]officialwebsitemod.Tag{}); err != nil { + return nil, err + } + if r.IsHot != nil { + filter["isHot"] = *r.IsHot + } + if r.Sort != nil { + filter["sort"] = *r.Sort + } + if r.IsActive != nil { + filter["isActive"] = *r.IsActive + } + if r.CreatedAt != nil { + filter["createdAt"] = *r.CreatedAt + } + if r.UpdatedAt != nil { + filter["updatedAt"] = *r.UpdatedAt + } + if r.DeletedAt != nil { + filter["deletedAt"] = *r.DeletedAt + } + return filter, nil +} diff --git a/web/service/officialser/official.go b/web/service/officialser/official.go new file mode 100644 index 0000000..07850a2 --- /dev/null +++ b/web/service/officialser/official.go @@ -0,0 +1,45 @@ +package officialser + +import ( + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/officialmod" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// QueryAll 查询列表 +func QueryAll(in *officialmod.QueryAllCond) ([]*officialmod.OfficialConfig, stderr.Code) { + list, err := officialmod.QueryAll(in, in.Options()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + return list, stderr.Success +} + +// UpdateOfficial 编辑官方配置 +func UpdateOfficial(officialID primitive.ObjectID, doc officialmod.OfficiaUpdateDoc) (err error) { + tMod, err := officialmod.FindOneTagByID(officialID) + if err != nil { + log.Error("web service Official UpdateOfficial FindOneTagByID fail error", log.E(err), log.Any("officialID", officialID)) + return + } + if tMod.ID.IsZero() { + log.Error("web service Official UpdateOfficial Official is null", log.E(err), log.Any("officialID", officialID)) + return + } + return officialmod.UpdateOneOfficiaByID(officialID, doc) +} + +// AddOfficial 添加标签 +func AddOfficial(official officialmod.OfficialConfig) (data interface{}, err error) { + return officialmod.InsertOne(&official) +} + +// DelOfficial 删除标签 +func DelOfficial(tag officialmod.WebOfficialDeleteReqInfo) (err error) { + _, err = officialmod.DeleteOfficias(tag.IDs) + if err != nil { + return err + } + return nil +} diff --git a/web/service/operationlogser/operationlog.go b/web/service/operationlogser/operationlog.go new file mode 100644 index 0000000..30b3d95 --- /dev/null +++ b/web/service/operationlogser/operationlog.go @@ -0,0 +1,39 @@ +package operationlogser + +import ( + "91porn-server/common/stderr" + "91porn-server/models/v/operationlogmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func GetOperationLogList(req GetOperationLogListReq) (*GetOperationLogListResp, stderr.Code) { + data := new(GetOperationLogListResp) + filter := bson.M{} + if req.UserID != nil && *req.UserID > 0 { + filter["userID"] = *req.UserID + } + if req.CreatedUser != nil && len(*req.CreatedUser) > 0 { + filter["createdUser"] = *req.CreatedUser + } + if req.Content != nil && len(*req.Content) > 0 { + filter["afterContent"] = *req.Content + } + if req.Start != nil && !req.Start.IsZero() && req.End != nil && !req.End.IsZero() { + filter["createdAt"] = bson.M{"$gte": req.Start, "$lt": req.End} + } + if req.OperationType != nil && *req.OperationType > 0 { + filter["operationType"] = *req.OperationType + } + count := operationlogmod.Count(filter) + data.Total = count + opts := (&options.FindOptions{}). + SetSort(bson.D{{Key: "createdAt", Value: -1}}).SetLimit(int64(req.PageSize)).SetSkip(int64((req.PageNumber - 1) * req.PageSize)) + operationlogList, serr := operationlogmod.FindManyOperationLog(filter, opts) + if serr != nil { + return data, stderr.ErrDbQueryError + } + data.List = operationlogList + return data, stderr.Success +} diff --git a/web/service/operationlogser/type.go b/web/service/operationlogser/type.go new file mode 100644 index 0000000..7dab4ad --- /dev/null +++ b/web/service/operationlogser/type.go @@ -0,0 +1,25 @@ +package operationlogser + +import ( + "time" + + "91porn-server/models/commod" + "91porn-server/models/v/operationlogmod" +) + +// GetOperationLogListReq 获取帖子、用户操作日志列表 请求数据 +type GetOperationLogListReq struct { + UserID *int64 `form:"userID,omitempty" json:"userID,omitempty" bson:"userID,omitempty"` //用户ID + OperationType *int `form:"operationType,omitempty" json:"operationType,omitempty" bson:"operationType,omitempty"` //操作类型 + Content *string `form:"content,omitempty" json:"content,omitempty" bson:"content,omitempty"` //操作内容 + CreatedUser *string `form:"createdUser,omitempty" json:"createdUser,omitempty" bson:"createdUser,omitempty"` //操作人 + Start *time.Time `form:"start,omitempty" json:"start,omitempty" bson:"start,omitempty"` //开始时间 + End *time.Time `form:"end,omitempty" json:"end,omitempty" bson:"end,omitempty"` //结束时间 + commod.Page +} + +// GetOperationLogListResp 获取帖子、用户操作日志列表 回复数据 +type GetOperationLogListResp struct { + Total int64 `json:"total" bson:"total"` + List []operationlogmod.OperationLog `json:"list" bson:"list"` +} diff --git a/web/service/paymentguideser/payment_guide.go b/web/service/paymentguideser/payment_guide.go new file mode 100644 index 0000000..2cd8c04 --- /dev/null +++ b/web/service/paymentguideser/payment_guide.go @@ -0,0 +1,168 @@ +package paymentguideser + +import ( + "context" + "fmt" + "strings" + "time" + + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/v/paymentguidemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type ListReq struct { + Scene string `form:"scene"` + PageNumber int64 `form:"pageNumber,default=1" binding:"min=1"` + PageSize int64 `form:"pageSize,default=20" binding:"min=1,max=100"` +} + +type ListResp struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []paymentguidemod.PaymentGuide `json:"list"` + SceneOptions []SceneOption `json:"sceneOptions"` +} + +type SceneOption struct { + Label string `json:"label"` + Value string `json:"value"` + TotalWatchCount *uint64 `json:"totalWatchCount,omitempty"` +} + +// AddReq keeps the existing flat single-scene request compatible and adds an +// optional batch envelope for applying one configuration to multiple scenes. +type AddReq struct { + paymentguidemod.PaymentGuide + Scenes []string `json:"scenes,omitempty"` + Config *paymentguidemod.PaymentGuide `json:"config,omitempty"` +} + +type BatchAddReq struct { + Scenes []string `json:"scenes"` + Config paymentguidemod.PaymentGuide `json:"config"` +} + +type BatchAddItem struct { + Scene string `json:"scene"` + ID string `json:"id"` +} + +type BatchAddResp struct { + List []BatchAddItem `json:"list"` +} + +func (p AddReq) IsBatch() bool { + return p.Config != nil || p.Scenes != nil +} + +func (p *ListReq) List() (ListResp, error) { + p.Scene = strings.ToUpper(strings.TrimSpace(p.Scene)) + if p.Scene != "" && !paymentguidemod.ConfigurableScene(p.Scene) { + return ListResp{}, fmt.Errorf("unsupported scene: %s", p.Scene) + } + list, total, hasNext, err := paymentguidemod.List( + p.Scene, + (p.PageNumber-1)*p.PageSize, + p.PageSize, + ) + if list == nil { + list = []paymentguidemod.PaymentGuide{} + } + normalizeVideoLimits(list) + return ListResp{ + Total: total, + HasNext: hasNext, + List: list, + SceneOptions: sceneOptions(sysconfdata.GetTotalWatchCount()), + }, err +} + +func sceneOptions(totalWatchCount uint64) []SceneOption { + return []SceneOption{ + {Label: fmt.Sprintf("首页新用户免费%d次试看", totalWatchCount), Value: paymentguidemod.SceneHomeNewUserFreeTrial, TotalWatchCount: &totalWatchCount}, + {Label: "首页老用户", Value: paymentguidemod.SceneHomeOldUser}, + {Label: "视频试看3秒", Value: paymentguidemod.SceneVideoPreviewEnd}, + {Label: "优惠倒计时", Value: paymentguidemod.SceneDiscountCountdown}, + {Label: "视频返回", Value: paymentguidemod.SceneVideoBack}, + {Label: "VIP中心", Value: paymentguidemod.SceneVIPCenter}, + {Label: "VIP内容更新", Value: paymentguidemod.SceneVIPContentUpdate}, + } +} + +func normalizeVideoLimits(list []paymentguidemod.PaymentGuide) { + for i := range list { + if list[i].Scene == paymentguidemod.SceneVIPContentUpdate { + list[i].VideoLimit = list[i].EffectiveVideoLimit() + } + } +} + +func Add(config *paymentguidemod.PaymentGuide) error { + config.Normalize() + if !paymentguidemod.ConfigurableScene(config.Scene) { + return fmt.Errorf("unsupported scene: %s", config.Scene) + } + return paymentguidemod.Insert(config) +} + +func (p BatchAddReq) Configs() ([]paymentguidemod.PaymentGuide, error) { + if len(p.Scenes) == 0 { + return nil, fmt.Errorf("scenes are required") + } + if len(p.Scenes) > paymentguidemod.MaxBatchSceneCount { + return nil, fmt.Errorf("scenes cannot contain more than %d entries", paymentguidemod.MaxBatchSceneCount) + } + seen := make(map[string]struct{}, len(p.Scenes)) + configs := make([]paymentguidemod.PaymentGuide, 0, len(p.Scenes)) + for _, rawScene := range p.Scenes { + scene := strings.ToUpper(strings.TrimSpace(rawScene)) + if !paymentguidemod.ConfigurableScene(scene) { + return nil, fmt.Errorf("unsupported scene: %s", scene) + } + if _, ok := seen[scene]; ok { + return nil, fmt.Errorf("duplicate scene: %s", scene) + } + seen[scene] = struct{}{} + config := p.Config + config.ID = primitive.NilObjectID + config.Scene = scene + config.Segments = append([]string(nil), p.Config.Segments...) + config.VideoIDs = append([]string(nil), p.Config.VideoIDs...) + if config.VideoLimit == 0 { + config.VideoLimit = paymentguidemod.DefaultVIPContentVideoLimit + } + config.CreatedAt = time.Time{} + config.UpdatedAt = time.Time{} + config.Normalize() + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("scene %s: %w", scene, err) + } + configs = append(configs, config) + } + return configs, nil +} + +func BatchAdd(ctx context.Context, configs []paymentguidemod.PaymentGuide) (BatchAddResp, error) { + if err := paymentguidemod.InsertMany(ctx, configs); err != nil { + return BatchAddResp{}, err + } + items := make([]BatchAddItem, 0, len(configs)) + for i := range configs { + items = append(items, BatchAddItem{Scene: configs[i].Scene, ID: configs[i].ID.Hex()}) + } + return BatchAddResp{List: items}, nil +} + +func Edit(config *paymentguidemod.PaymentGuide) error { + config.Normalize() + if !paymentguidemod.ConfigurableScene(config.Scene) { + return fmt.Errorf("unsupported scene: %s", config.Scene) + } + return paymentguidemod.Update(config) +} + +func Delete(id primitive.ObjectID) error { + return paymentguidemod.Delete(id) +} diff --git a/web/service/paymentguideser/payment_guide_test.go b/web/service/paymentguideser/payment_guide_test.go new file mode 100644 index 0000000..54070ac --- /dev/null +++ b/web/service/paymentguideser/payment_guide_test.go @@ -0,0 +1,258 @@ +package paymentguideser + +import ( + "encoding/json" + "strings" + "testing" + "time" + + "91porn-server/models/v/paymentguidemod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestPaymentGuideListResponseUsesLegacyVideoLimitDefault(t *testing.T) { + list := []paymentguidemod.PaymentGuide{ + {Scene: paymentguidemod.SceneVIPContentUpdate}, + {Scene: paymentguidemod.SceneVIPContentUpdate, VideoLimit: 12}, + {Scene: paymentguidemod.SceneVIPCenter}, + } + normalizeVideoLimits(list) + + if list[0].VideoLimit != paymentguidemod.DefaultVIPContentVideoLimit { + t.Fatalf("legacy videoLimit = %d, want %d", list[0].VideoLimit, paymentguidemod.DefaultVIPContentVideoLimit) + } + if list[1].VideoLimit != 12 { + t.Fatalf("configured videoLimit = %d, want 12", list[1].VideoLimit) + } + if list[2].VideoLimit != 0 { + t.Fatalf("unrelated scene videoLimit = %d, want 0", list[2].VideoLimit) + } +} + +func TestSceneOptionsUseConfiguredFreeTrialCountAndHideLegacyScene(t *testing.T) { + options := sceneOptions(5) + if len(options) != paymentguidemod.MaxBatchSceneCount { + t.Fatalf("len(options) = %d, want %d", len(options), paymentguidemod.MaxBatchSceneCount) + } + if options[0].Value != paymentguidemod.SceneHomeNewUserFreeTrial || + options[0].Label != "首页新用户免费5次试看" || + options[0].TotalWatchCount == nil || *options[0].TotalWatchCount != 5 { + t.Fatalf("unexpected free-trial option: %+v", options[0]) + } + for _, option := range options { + if option.Value == paymentguidemod.SceneHomeNewUser { + t.Fatalf("legacy HOME_NEW_USER must not be returned: %+v", options) + } + } +} + +func TestWebConfigurationRejectsLegacyHomeNewUserScene(t *testing.T) { + listReq := ListReq{Scene: paymentguidemod.SceneHomeNewUser, PageNumber: 1, PageSize: 20} + if _, err := listReq.List(); err == nil || !strings.Contains(err.Error(), "unsupported scene") { + t.Fatalf("List() error = %v, want unsupported scene", err) + } + + config := validBatchConfig() + config.Scene = paymentguidemod.SceneHomeNewUser + if err := Add(&config); err == nil || !strings.Contains(err.Error(), "unsupported scene") { + t.Fatalf("Add() error = %v, want unsupported scene", err) + } +} + +func validBatchConfig() paymentguidemod.PaymentGuide { + return paymentguidemod.PaymentGuide{ + Segments: []string{paymentguidemod.SegmentNormal}, + Style: "BOTTOM_SHEET", + Title: "会员内容更新", + VideoIDs: []string{"video-a"}, + Action: paymentguidemod.Action{Type: "NONE"}, + Enable: true, + Sort: 100, + } +} + +func TestBatchAddReqConfigsNormalizesScenesAndDefaultsVideoLimit(t *testing.T) { + req := BatchAddReq{ + Scenes: []string{" home_new_user_free_trial ", "vip_content_update"}, + Config: validBatchConfig(), + } + configs, err := req.Configs() + if err != nil { + t.Fatal(err) + } + if len(configs) != 2 { + t.Fatalf("len(configs) = %d, want 2", len(configs)) + } + if configs[0].Scene != paymentguidemod.SceneHomeNewUserFreeTrial || configs[1].Scene != paymentguidemod.SceneVIPContentUpdate { + t.Fatalf("unexpected scenes: %#v", []string{configs[0].Scene, configs[1].Scene}) + } + for i := range configs { + if configs[i].VideoLimit != paymentguidemod.DefaultVIPContentVideoLimit { + t.Fatalf("configs[%d].VideoLimit = %d, want %d", i, configs[i].VideoLimit, paymentguidemod.DefaultVIPContentVideoLimit) + } + } + + configs[0].Segments[0] = paymentguidemod.SegmentMaxVIP + configs[0].VideoIDs[0] = "changed" + if configs[1].Segments[0] != paymentguidemod.SegmentNormal || configs[1].VideoIDs[0] != "video-a" { + t.Fatal("batch configs must not share slice storage") + } + if req.Config.Segments[0] != paymentguidemod.SegmentNormal || req.Config.VideoIDs[0] != "video-a" { + t.Fatal("building configs must not mutate the request template") + } +} + +func TestBatchAddReqConfigsPreservesExplicitVideoLimit(t *testing.T) { + config := validBatchConfig() + config.VideoLimit = 12 + configs, err := (BatchAddReq{ + Scenes: []string{paymentguidemod.SceneVIPContentUpdate}, + Config: config, + }).Configs() + if err != nil { + t.Fatal(err) + } + if configs[0].VideoLimit != 12 { + t.Fatalf("VideoLimit = %d, want 12", configs[0].VideoLimit) + } +} + +func TestBatchAddReqConfigsSupportsEveryScene(t *testing.T) { + configs, err := (BatchAddReq{ + Scenes: []string{ + paymentguidemod.SceneHomeNewUserFreeTrial, + paymentguidemod.SceneHomeOldUser, + paymentguidemod.SceneVideoPreviewEnd, + paymentguidemod.SceneDiscountCountdown, + paymentguidemod.SceneVideoBack, + paymentguidemod.SceneVIPCenter, + paymentguidemod.SceneVIPContentUpdate, + }, + Config: validBatchConfig(), + }).Configs() + if err != nil { + t.Fatal(err) + } + if len(configs) != paymentguidemod.MaxBatchSceneCount { + t.Fatalf("len(configs) = %d, want %d", len(configs), paymentguidemod.MaxBatchSceneCount) + } +} + +func TestBatchAddReqConfigsDiscardsTemplateIdentityAndTimestamps(t *testing.T) { + config := validBatchConfig() + config.ID = primitive.NewObjectID() + config.Scene = paymentguidemod.SceneVIPCenter + config.CreatedAt = time.Now().Add(-2 * time.Hour) + config.UpdatedAt = time.Now().Add(-time.Hour) + configs, err := (BatchAddReq{ + Scenes: []string{paymentguidemod.SceneHomeNewUserFreeTrial}, + Config: config, + }).Configs() + if err != nil { + t.Fatal(err) + } + if !configs[0].ID.IsZero() { + t.Fatalf("ID = %s, want zero before insertion", configs[0].ID.Hex()) + } + if configs[0].Scene != paymentguidemod.SceneHomeNewUserFreeTrial { + t.Fatalf("Scene = %s, want %s", configs[0].Scene, paymentguidemod.SceneHomeNewUserFreeTrial) + } + if !configs[0].CreatedAt.IsZero() || !configs[0].UpdatedAt.IsZero() { + t.Fatalf("timestamps must be zero before insertion: %+v", configs[0]) + } +} + +func TestBatchAddReqConfigsRejectsInvalidScenes(t *testing.T) { + tests := []struct { + name string + scenes []string + match string + }{ + {name: "empty", scenes: nil, match: "scenes are required"}, + {name: "unsupported", scenes: []string{"UNKNOWN"}, match: "unsupported scene"}, + {name: "duplicate after normalization", scenes: []string{"VIP_CENTER", " vip_center "}, match: "duplicate scene"}, + {name: "too many inputs", scenes: []string{"a", "b", "c", "d", "e", "f", "g", "h"}, match: "more than 7"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := (BatchAddReq{Scenes: tt.scenes, Config: validBatchConfig()}).Configs() + if err == nil || !strings.Contains(err.Error(), tt.match) { + t.Fatalf("Configs() error = %v, want match %q", err, tt.match) + } + }) + } +} + +func TestBatchAddReqConfigsValidatesTemplateForEveryScene(t *testing.T) { + config := validBatchConfig() + config.Title = "" + _, err := (BatchAddReq{ + Scenes: []string{paymentguidemod.SceneVIPCenter}, + Config: config, + }).Configs() + if err == nil || !strings.Contains(err.Error(), "title is required") { + t.Fatalf("Configs() error = %v, want title validation error", err) + } +} + +func TestAddReqSupportsExistingSingleSceneJSON(t *testing.T) { + body := []byte(`{ + "scene":"VIP_CONTENT_UPDATE", + "style":"BOTTOM_SHEET", + "title":"会员内容更新", + "action":{"type":"NONE","value":""} + }`) + var req AddReq + if err := json.Unmarshal(body, &req); err != nil { + t.Fatal(err) + } + if req.IsBatch() { + t.Fatal("existing flat add request must remain single-scene") + } + if req.Scene != paymentguidemod.SceneVIPContentUpdate || req.Title != "会员内容更新" { + t.Fatalf("unexpected single add request: %+v", req.PaymentGuide) + } +} + +func TestAddReqSupportsBatchJSONShape(t *testing.T) { + body := []byte(`{ + "scenes":["HOME_NEW_USER_FREE_TRIAL","VIP_CONTENT_UPDATE"], + "config":{ + "style":"BOTTOM_SHEET", + "title":"会员内容更新", + "videoLimit":20, + "action":{"type":"NONE","value":""}, + "enable":true, + "sort":100 + } + }`) + var req AddReq + if err := json.Unmarshal(body, &req); err != nil { + t.Fatal(err) + } + if !req.IsBatch() || req.Config == nil { + t.Fatalf("unexpected batch add request: %+v", req) + } + configs, err := (BatchAddReq{Scenes: req.Scenes, Config: *req.Config}).Configs() + if err != nil { + t.Fatal(err) + } + if len(configs) != 2 || configs[1].VideoLimit != 20 { + t.Fatalf("unexpected configs: %+v", configs) + } +} + +func TestAddReqTreatsPresentEmptyScenesAsBatch(t *testing.T) { + var req AddReq + if err := json.Unmarshal([]byte(`{"scenes":[]}`), &req); err != nil { + t.Fatal(err) + } + if !req.IsBatch() { + t.Fatal("a present scenes field must use batch validation") + } + _, err := (BatchAddReq{Scenes: req.Scenes, Config: paymentguidemod.PaymentGuide{}}).Configs() + if err == nil || !strings.Contains(err.Error(), "scenes are required") { + t.Fatalf("Configs() error = %v, want scenes validation error", err) + } +} diff --git a/web/service/payvidlgser/payvidlg.go b/web/service/payvidlgser/payvidlg.go new file mode 100644 index 0000000..1ed58cf --- /dev/null +++ b/web/service/payvidlgser/payvidlg.go @@ -0,0 +1,63 @@ +package payvidlgser + +import ( + "time" + + "91porn-server/models/l/payvidlgmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" +) + +// VideoCoinIncomeAggregateTopN +func VideoCoinIncomeAggregateTopN(topN int, start, end time.Time) (ret VideoCoinIncomeAggregateTopNStat, err error) { + list, err := payvidlgmod.VideoCoinIncomeAggregateTopN(topN, start, end) + if err != nil { + return + } + listLen := len(list) + videoIDs := make([]payvidlgmod.ObjectID, listLen) + for i, v := range list { + videoIDs[i] = v.VID + } + videos, err := vidmod.GetVideoListByIDsNoStatus(videoIDs) + if err != nil { + return + } + videoMap := make(map[payvidlgmod.ObjectID]vidmod.VideoModel) + for _, video := range videos { + videoMap[video.ID] = *video + } + listDetail := make([]VideoCoinIncomeAggregateInfo, listLen) + publisherIDs := make([]uint64, 0, listLen) + for i, v := range list { + listDetail[i] = VideoCoinIncomeAggregateInfo{ + ID: v.VID, + Total: v.Total, + Count: v.Count, + CreatedAt: videoMap[v.VID].CreatedAt, + PublisherID: videoMap[v.VID].PublisherID, + Title: videoMap[v.VID].Title, + Coins: videoMap[v.VID].Coins, + } + if videoMap[v.VID].PublisherID != 0 { + publisherIDs = append(publisherIDs, videoMap[v.VID].PublisherID) + } + } + userMap, err := usermod.GetUsersBaseInfoMap(publisherIDs) + if err != nil { + return + } + for _, v := range listDetail { + user, ok := userMap[v.PublisherID] + if ok { + v.PublisherName = user.Name + } + } + gross, err := payvidlgmod.VideoCoinIncomeGross(start, end) + if err != nil { + return + } + ret.Gross = gross + ret.List = listDetail + return +} diff --git a/web/service/payvidlgser/type.go b/web/service/payvidlgser/type.go new file mode 100644 index 0000000..5cf9c03 --- /dev/null +++ b/web/service/payvidlgser/type.go @@ -0,0 +1,23 @@ +package payvidlgser + +import ( + "time" + + "91porn-server/models/l/payvidlgmod" +) + +type VideoCoinIncomeAggregateInfo struct { + ID payvidlgmod.ObjectID `json:"id"` //video id + Total int64 `json:"total" copier:"-"` //视频总金币 + Count int64 `json:"count" copier:"-"` //视频购买总次数 + CreatedAt time.Time `json:"created_at"` //视频入库时间 + PublisherID uint64 `json:"publisher_id"` //视频发布者id + PublisherName string `json:"publisher_name"` //视频发布者名称 + Title string `json:"title"` //视频标题 + Coins int64 `json:"coins"` //视频定价 +} + +type VideoCoinIncomeAggregateTopNStat struct { + List []VideoCoinIncomeAggregateInfo `json:"list"` //总金币排名前N的视频详细信息 + Gross int64 `json:"gross"` //时间段内所有视频金币总收入 +} diff --git a/web/service/prefetch/prefetch.go b/web/service/prefetch/prefetch.go new file mode 100644 index 0000000..9dcb54a --- /dev/null +++ b/web/service/prefetch/prefetch.go @@ -0,0 +1,151 @@ +package prefetch + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "sync" + "time" + + "91porn-server/common/log" + "91porn-server/models/v/sourcemod" + "91porn-server/models/v/vidmod" +) + +var exit = make(chan int) +var wg sync.WaitGroup +var client = &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 20, + MaxIdleConnsPerHost: 20, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: time.Second, + }, + Timeout: 30 * time.Second, +} + +const prefetchSubmitUrl = "https://prefetch.csrrz.com/master/submit" + +func StartSubmitPrefetch() { + wg.Add(1) + go prefetchLoop() +} + +func StopSubmitPrefetch() { + close(exit) + wg.Wait() +} + +func prefetchLoop() { + log.Info("cdn预热提交协程启动....") + ticker := time.NewTicker(time.Hour * 12) + temp := make(chan int) + time.AfterFunc(time.Minute*5, func() { + temp <- 1 + }) + for { + select { + case <-exit: + goto exit + case <-ticker.C: + execute() + case <-temp: + execute() + } + } +exit: + log.Info("cdn预热提交协程退出....") + ticker.Stop() + close(temp) + wg.Done() +} + +func execute() { + videoCdn, imgCdn := sourcemod.GetVideoAndImgCdn() + if len(videoCdn) == 0 || len(imgCdn) == 0 { + log.Error("视频或者图片cdn为空,不执行推送") + return + } + page, pageSize := int64(1), int64(1000) + for { + data, err := vidmod.GetManyPrefetchVideos(page, pageSize) + if err != nil { + log.Error("查询预热视频出错", log.Any("page", page), log.Any("pageSize", pageSize), log.E(err)) + return + } + fmt.Println("page:", page) + page++ + dataLen := len(data) + submitData := &SubmitPrefetch{ + M3u8List: make([]string, dataLen), + VideoCdnDomains: videoCdn, + ImageList: make([]string, 0), + ImgCdnDomains: imgCdn, + } + for i, v := range data { + submitData.ImageList = append(submitData.ImageList, v.Images...) + submitData.M3u8List[i] = v.M3u8Url + } + if len(submitData.M3u8List) == 0 && len(submitData.ImageList) == 0 { + return + } + //提交需要缓存的数据到预热中心节点 + submit(submitData) + //分页查询到最后数据 + if int64(len(data)) < pageSize { + fmt.Println("======end=========") + return + } + } +} + +func submit(sp *SubmitPrefetch) { + data, _ := json.Marshal(sp) + url := prefetchSubmitUrl + req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) + if err != nil { + log.Error("new http post request failed", log.Any("url", url), log.E(err)) + return + } + req.Header.Add("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + log.Error("submit prefetch httpClient do failed", log.Any("url", url), log.E(err)) + return + } + defer resp.Body.Close() + buf, err := io.ReadAll(resp.Body) + if err != nil { + log.Error("submit prefetch read response body failed", log.Any("url", url), log.E(err)) + return + } + var result SubmitPrefetchResp + if err = json.Unmarshal(buf, &result); err != nil { + log.Error("submit prefetch Unmarshal failed", log.Any("url", url), log.E(err)) + return + } + if result.Code != 200 { + log.Error("submit prefetch response failed", log.Any("url", url), log.Any("response's Code", result.Code), log.E(err)) + } +} + +type SubmitPrefetch struct { + M3u8List []string `json:"m3u8List"` + VideoCdnDomains []string `json:"videoCdnDomains"` + ImageList []string `json:"imageList"` + ImgCdnDomains []string `json:"imgCdnDomains"` +} + +type SubmitPrefetchResp struct { + Code int `json:"code"` + Msg string `json:"msg"` +} diff --git a/web/service/productBenefitser/productBenefit.go b/web/service/productBenefitser/productBenefit.go new file mode 100644 index 0000000..11fb891 --- /dev/null +++ b/web/service/productBenefitser/productBenefit.go @@ -0,0 +1,71 @@ +package productBenefitser + +import ( + "time" + + "91porn-server/common/stderr" + "91porn-server/models/v/productbenefitmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// GetProductBenefitList 获取会员卡权益配置 +func GetProductBenefitList(req productbenefitmod.ProductBenefitReq) (code stderr.Code, d []productbenefitmod.ProductBenefit) { + var data []productbenefitmod.ProductBenefit + data, err := productbenefitmod.FindProductBenefit(req.PageNumber, req.PageSize) + if err != nil { + return stderr.ErrDbQueryError, data + } + if len(data) <= 0 { + return stderr.Success, data + } + return stderr.Success, data +} + +// GetAllProductBenefitList 获取会员卡权益配置 +func GetAllProductBenefitList() (code stderr.Code, d []productbenefitmod.ALlProductBenefit) { + var data []productbenefitmod.ALlProductBenefit + data, err := productbenefitmod.FindAllProductBenefit() + if err != nil { + return stderr.ErrDbQueryError, data + } + if len(data) <= 0 { + return stderr.Success, data + } + return stderr.Success, data +} + +// UpdateProductBenefit 更新会员卡权益配置信息 +func UpdateProductBenefit(req *productbenefitmod.ProductBenefitSelector) (code stderr.Code) { + if err := productbenefitmod.UpdateProductBenefit(req.ID, req); err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success +} + +// DeleteProductBenefit 删除会员卡权益配置 +func DeleteProductBenefit(id string) (code stderr.Code) { + if err := productbenefitmod.RemoveProductBenefit(id); err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success +} + +// AddProductBenefit 添加会员卡权益配置 +func AddProductBenefit(req productbenefitmod.ProductBenefit) (code stderr.Code) { + new := &productbenefitmod.ProductBenefit{ + ID: primitive.NewObjectID(), + Img: req.Img, + Privilege: req.Privilege, + PrivilegeName: req.PrivilegeName, + SortCode: req.SortCode, + PrivilegeDesc: req.PrivilegeDesc, + IsCore: req.IsCore, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + if err := productbenefitmod.InsertProductBenefit(new); err != nil { + return stderr.ErrDbInsertError + } + return stderr.Success +} diff --git a/web/service/productser/productHistory.go b/web/service/productser/productHistory.go new file mode 100644 index 0000000..69e9ce3 --- /dev/null +++ b/web/service/productser/productHistory.go @@ -0,0 +1,19 @@ +package productser + +import ( + "91porn-server/common/log" + "91porn-server/models/v/prdcthsomod" +) + +func GetBuyHistoryList(req *prdcthsomod.ProductHistoryQueryReq) (resp *prdcthsomod.ProductHistoryQueryResp, err error) { + total, list, err := prdcthsomod.FindProductHistorys(req.GetCond(), req.GetOpt()) + if err != nil { + log.Error("GetBuyHistoryList prdcthsomod.FindProductHistorys fail", log.Any("req", req), log.E(err)) + return + } + resp = &prdcthsomod.ProductHistoryQueryResp{ + Total: total, + List: list, + } + return +} diff --git a/web/service/proxyser/proxy.go b/web/service/proxyser/proxy.go new file mode 100644 index 0000000..0b52239 --- /dev/null +++ b/web/service/proxyser/proxy.go @@ -0,0 +1,131 @@ +package proxyser + +import ( + "strconv" + + "91porn-server/models/v/proxyincomemod" + "91porn-server/models/v/proxymod" + "91porn-server/models/v/usermod" +) + +var PerformanceConvRat = []int64{100, 25, 15, 10} + +// FindTotalIncome 查询总收益web +func FindTotalIncome(uid uint64) (res map[string]interface{}, err error) { + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return + } + lv1, lv2, lv3, lv4, total, err := proxyincomemod.FindAllIncome(uid) + res = make(map[string]interface{}) + res["uid"] = u.UID + res["promotionCode"] = u.PromCode + res["lv1"] = lv1 + res["lv2"] = lv2 + res["lv3"] = lv3 + res["lv4"] = lv4 + res["total"] = total + res["withdraw"] = 0 + res["withdrawCount"] = 0 + return +} + +func invitationWebResHelp(uid uint64, uids []uint64, lv int, ic chan []proxymod.InvitationWebRes, flagChan chan int) { + invLv, _ := proxymod.GetInveNextDetails(uids) + if len(invLv) <= 0 { + flagChan <- lv - 1 + return + } + data := make([]proxymod.InvitationWebRes, len(invLv)) + for i, v := range invLv { + data[i] = proxymod.InvitationWebRes{ + UID: uid, + IID: v.Invitee, + Lv: lv, + CreatedAt: v.CreatedAt, + } + } + ic <- data + if lv == 4 { + flagChan <- lv + } +} + +// GetInveUsers 获取下级代理 +func GetInveUsers(uid uint64) (i interface{}, err error) { + m := make(map[string]interface{}) + var MaxLv int = 4 + flagChan := make(chan int) + invResChan := make(chan []proxymod.InvitationWebRes, MaxLv) + tmp := []uint64{uid} + for i := 1; i <= MaxLv; i++ { + if len(tmp) <= 0 { + break + } + uids := tmp + go invitationWebResHelp(uid, uids, i, invResChan, flagChan) + uids, total, err := proxymod.GetInveNext(tmp) + if err != nil || len(uids) <= 0 { + break + } + m["totalLv"+strconv.Itoa(i)] = total + tmp = uids + } + rcvSize, ok := <-flagChan + if !ok { + return + } + data := make([]proxymod.InvitationWebRes, 0, rcvSize) + for i := 1; i <= rcvSize; i++ { + res, ok := <-invResChan + if !ok { + break + } + if len(res) > 0 { + data = append(data, res...) + } + } + m["list"] = data + i = m + return +} + +// GetInvrDetails 获取上级代理 +func GetInvrDetails(uid uint64) ([]proxymod.InvitationWebRes, error) { + invs, err := proxymod.GetInvrDetails(uid, 4) + if err != nil { + return []proxymod.InvitationWebRes{}, err + } + invsLen := len(invs) + if invsLen == 0 { + return []proxymod.InvitationWebRes{}, nil + } + data := make([]proxymod.InvitationWebRes, invsLen) + i := 0 + for k, v := range invs { + data[i] = proxymod.InvitationWebRes{ + UID: uid, + IID: v.UID, + Lv: k + 1, + CreatedAt: v.CreatedAt, + } + i++ + } + return data, nil +} + +// GetInvr 获取上级代理信息 +func GetInvr(uid uint64) (ivt uint64, code string, err error) { + ivt, err = proxymod.GetInvrLv1(uid) + if err != nil { + return + } + u, err := usermod.FindUserByUID(ivt) + if err != nil { + return + } + if u != nil { + code = u.PromCode + } + return +} diff --git a/web/service/quicksearchser/quicksearch.go b/web/service/quicksearchser/quicksearch.go new file mode 100755 index 0000000..7555495 --- /dev/null +++ b/web/service/quicksearchser/quicksearch.go @@ -0,0 +1,139 @@ +package quicksearchser + +import ( + "time" + + "91porn-server/models/cache/quicksearchdata" + "91porn-server/models/commod" + "91porn-server/models/v/quicksearchmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + Title *string `form:"title" json:"title"` // 标题 + Enabled *bool `form:"enabled" json:"enabled"` // 状态 + Type *int `form:"type" json:"type"` // 类型 + LinkType *int `form:"linkType" json:"linkType"` // 跳转链接类型 + commod.Page +} + +func (q *WebListReq) filter() bson.M { + var query = bson.M{} + if q.Title != nil && *q.Title != "" { + query["title"] = bson.M{"$regex": q.Title} + } + if q.Enabled != nil { + query["enabled"] = q.Enabled + } + if q.Type != nil { + query["type"] = q.Type + } + if q.LinkType != nil { + query["linkType"] = q.LinkType + } + + return query +} + +type WebListRes struct { + Total int64 `json:"total"` + HasNext bool `json:"hasNext"` + List []quicksearchmod.QuickSearch `json:"list"` +} + +// GetList 获取列表 +func (q *WebListReq) GetList() (res WebListRes, err error) { + res.List, res.Total, res.HasNext, err = quicksearchmod.GetList(q.filter(), int64(q.Skip()), int64(q.Limit()), q.GetSort()) + + return +} + +type WebCreateReq struct { + Enabled bool `json:"enabled" form:"enabled"` // 是否开启 + Title string `json:"title" form:"title"` // 展示标题 + SearchKeyword string `json:"searchKeyword" form:"searchKeyword"` // 搜索关键字 + Link string `json:"link" form:"link"` // 跳转链接 + Type int `json:"type" form:"type"` // 配置类型 1:置顶,2:大家都在搜 + LinkType int `json:"linkType" form:"linkType"` // 跳转链接类型 +} + +// Create 发布数据 +func (p *WebCreateReq) Create() error { + data := quicksearchmod.QuickSearch{ + Enabled: p.Enabled, + Title: p.Title, + SearchKeyword: p.SearchKeyword, + Link: p.Link, + Type: p.Type, + LinkType: p.LinkType, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // 创建数据 + if _, err := quicksearchdata.InsertData(nil, data); err != nil { + return err + } + + return nil +} + +type WebUpdateReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` + Enabled *bool `json:"enabled" bson:"enabled"` // 是否开启 + Title *string `json:"title" bson:"title"` // 展示标题 + SearchKeyword *string `json:"searchKeyword" bson:"searchKeyword"` // 搜索关键字 + Link *string `json:"link" bson:"link"` // 跳转链接 + Type *int `json:"type" bson:"type"` // 配置类型 1:置顶,2:大家都在搜 + LinkType *int `json:"linkType" bson:"linkType"` // 跳转链接类型 +} + +// Update 更新数据 +func (p *WebUpdateReq) Update() error { + _, err := quicksearchmod.GetInfo(p.ID) + if err != nil { + return err + } + + data := make(map[string]interface{}) + if p.Enabled != nil { + data["enabled"] = p.Enabled + } + if p.Title != nil { + data["title"] = p.Title + } + if p.SearchKeyword != nil { + data["searchKeyword"] = p.SearchKeyword + } + if p.Link != nil { + data["link"] = p.Link + } + if p.LinkType != nil { + data["linkType"] = p.LinkType + } + if p.Type != nil { + data["type"] = p.Type + } + data["updatedAt"] = time.Now() + + if _, err = quicksearchdata.UpdateData(nil, p.ID.Hex(), data); err != nil { + return err + } + + return nil +} + +type WebDeleteReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` // ID +} + +// Delete 删除数据 +func (p *WebDeleteReq) Delete() error { + if err := quicksearchdata.DeleteData(nil, p.ID); err != nil { + return err + } + + return nil +} diff --git a/web/service/rechargeser/page.go b/web/service/rechargeser/page.go new file mode 100644 index 0000000..86647d3 --- /dev/null +++ b/web/service/rechargeser/page.go @@ -0,0 +1,125 @@ +package rechargeser + +import ( + "time" + + "91porn-server/common/log" + "91porn-server/models/commod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/rchgordmod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Record struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` //流水id + UID uint64 `json:"uid" bson:"uid"` //用户id + DevID string `json:"devID" bson:"devID"` //设备id + OID string `json:"oid" bson:"oid"` //支付平台订单号 + TraderId string `json:"traderId" bson:"traderId"` //商户ID + UserIP string `json:"userIP" bson:"userIP"` //用户ip + Name string `json:"name" bson:"name"` //用户名称 + Tel string `json:"tel" bson:"tel"` //用户手机号 + PayAct string `json:"payAct" bson:"payAct"` //支付账号 + DevType string `json:"devType" bson:"devType"` //设备系统类型 ios pc android + Amount decimal.Decimal `json:"amount" bson:"amount"` //蝴蝶币数量 + Money decimal.Decimal `json:"money" bson:"money"` //充值金额 订单金额 李秋山确认 + PayMoney decimal.Decimal `json:"payMoney" bson:"payMoney"` //实际到账金额 用户实际支付金额 李秋山确认 + RechargeType string `json:"rechargeType" bson:"rechargeType"` //充值类型 + ProductID primitive.ObjectID `json:"productID" bson:"productID"` //充值金币类型id + VipID primitive.ObjectID `json:"vipID" bson:"vipID"` //是否是vip冲 hi + VipName string `json:"vipName" bson:"vipName"` //vip名称 + Channel string `json:"channel" bson:"channel"` //渠道类型 鲨鱼 金鱼 + Proof string `json:"proof" bson:"proof"` //代充平台 支付凭证 + Status int `json:"status" bson:"status"` //1进行中 2付款失败 3付款成功 4已经退款 + StatusDesc string `json:"statusDesc" bson:"statusDesc"` //状态描述 + LockStatus int `json:"lockStatus" bson:"lockStatus"` //订单操作锁定状态 用户订单更新操作 0-未锁定 可用于编辑订单 1-已锁定 不可编辑 + NotifyTime int64 `json:"notifyTime" bson:"notifyTime"` //第三方回调本地订单的时间 + NotifyStatus int `json:"notifyStatus" bson:"notifyStatus"` //通知第三方上分情况状态 1-成功 2-失败 + Remark string `json:"remark" bson:"remark"` //备注 + ProgressAt time.Time `json:"progressAt" bson:"progressAt"` //第三方下单成功时间 + FailureAt time.Time `json:"failureAt" bson:"failureAt"` //回调失败时间 + SuccessAt time.Time `json:"successAt" bson:"successAt"` //回调成功时间 + PaymentAt time.Time `json:"paymentAt" bson:"paymentAt"` //支付时间 + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间 + UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //刷新时间 + commod.DiscDoc `bson:",inline"` //商区 + ProductType int `json:"productType" bson:"productType"` //支付产品类型 0 站群 1 棋牌 + SourcePage string `json:"sourcePage" bson:"sourcePage"` + SourceRef string `json:"sourceRef" bson:"sourceRef"` + VideoID string `json:"videoId" bson:"videoId"` + ActivityID string `json:"activityId" bson:"activityId"` + ExperimentID string `json:"experimentId" bson:"experimentId"` + ExperimentVariant string `json:"experimentVariant" bson:"experimentVariant"` + SessionID string `json:"sessionId" bson:"sessionId"` +} + +var _100 = decimal.NewFromInt(100) + +func BaiYuan(orders []*rchgordmod.RechargeOrder) []*Record { + records := make([]*Record, len(orders)) + ids := []primitive.ObjectID{} + for i, v := range orders { + if !v.VipID.IsZero() { + ids = append(ids, v.VipID) + } + records[i] = &Record{ + ID: v.ID, + UID: v.UID, + DevID: v.DevID, + OID: v.OID, + TraderId: v.TraderId, + UserIP: v.UserIP, + Name: v.Name, + Tel: v.Tel, + PayAct: v.PayAct, + DevType: v.DevType, + Amount: decimal.NewFromInt(v.Amount).Div(_100).Round(4), + Money: decimal.NewFromInt(v.Money).Div(_100).Round(4), + PayMoney: decimal.NewFromInt(v.PayMoney).Div(_100).Round(4), + RechargeType: v.RechargeType, + ProductID: v.ProductID, + VipID: v.VipID, + Channel: v.Channel, + Proof: v.Proof, + Status: v.Status, + StatusDesc: v.StatusDesc, + LockStatus: v.LockStatus, + NotifyTime: v.NotifyTime, + NotifyStatus: v.NotifyStatus, + Remark: v.Remark, + ProgressAt: v.ProgressAt, + FailureAt: v.FailureAt, + SuccessAt: v.SuccessAt, + PaymentAt: v.PaymentAt, + CreatedAt: v.CreatedAt, + UpdatedAt: v.UpdatedAt, + DiscDoc: v.DiscDoc, + ProductType: v.ProductType, + SourcePage: v.SourcePage, + SourceRef: v.SourceRef, + VideoID: v.VideoID, + ActivityID: v.ActivityID, + ExperimentID: v.ExperimentID, + ExperimentVariant: v.ExperimentVariant, + SessionID: v.SessionID, + } + } + //增加会员卡名称查询 + if len(ids) > 0 { + productMap, err := productmod.ListByIDsMap(ids) + if err != nil { + log.Error("BaiYuan productmod.FindByProductIDs fail", log.E(err)) + return records + } + if productMap != nil { + for i, r := range records { + if !r.VipID.IsZero() && productMap[r.VipID] != nil { + records[i].VipName = productMap[r.VipID].Name + } + } + } + } + return records +} diff --git a/web/service/rechargeser/page_attribution_test.go b/web/service/rechargeser/page_attribution_test.go new file mode 100644 index 0000000..e32821a --- /dev/null +++ b/web/service/rechargeser/page_attribution_test.go @@ -0,0 +1,33 @@ +package rechargeser + +import ( + "testing" + + "91porn-server/models/v/rchgordmod" +) + +func TestBaiYuanCopiesOrderAttribution(t *testing.T) { + order := &rchgordmod.RechargeOrder{ + SourcePage: "VIDEO_BOTTOM_SHEET", + SourceRef: "source-ref", + VideoID: "video-id", + ActivityID: "activity-id", + ExperimentID: "experiment-id", + ExperimentVariant: "A", + SessionID: "session-id", + } + records := BaiYuan([]*rchgordmod.RechargeOrder{order}) + if len(records) != 1 { + t.Fatalf("len(records) = %d", len(records)) + } + record := records[0] + if record.SourcePage != order.SourcePage || + record.SourceRef != order.SourceRef || + record.VideoID != order.VideoID || + record.ActivityID != order.ActivityID || + record.ExperimentID != order.ExperimentID || + record.ExperimentVariant != order.ExperimentVariant || + record.SessionID != order.SessionID { + t.Fatalf("attribution not copied: %#v", record) + } +} diff --git a/web/service/rechargeser/recharge.go b/web/service/rechargeser/recharge.go new file mode 100644 index 0000000..ffcb938 --- /dev/null +++ b/web/service/rechargeser/recharge.go @@ -0,0 +1,217 @@ +package rechargeser + +import ( + "context" + "errors" + "fmt" + "strconv" + "time" + + "91porn-server/common" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/rchgamtmod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// RechargeCallBack 充值成功回调处理逻辑 +func RechargeCallBack(ctx context.Context, oid string, payMoney int64, tradeNo string, code int, paymentAt time.Time) error { + r, err := rchgordmod.FindRechargeOrderByID(tradeNo) + if err != nil { + return err + } + if r.Status == rchgordmod.SUCCESS { + return nil + } + if r.OID != "" && r.OID != oid { + log.Error("WalletSer RechargeCallBack ObjectIDFromHex fail error: 1:") + return errors.New("1") + } + if r.Status != rchgordmod.Processing { + log.Error(fmt.Sprintf("WalletSer RechargeCallBack status fail data:%+v:", r.Status)) + return errors.New("1") + } + g, err := rchgamtmod.GetGoldByID(r.ProductID) + if err != nil { + return err + } + if g == nil { + return errors.New("invalid productID") + } + id, err := primitive.ObjectIDFromHex(tradeNo) + if err != nil { + log.ErrorX(ctx, "ObjectIDFromHex fail", log.Any("tradeNo", tradeNo), log.E(err)) + return err + } + now := time.Now() + set := rchgordmod.EditSelector{ + PayMoney: &payMoney, + OID: &oid, + PaymentAt: &paymentAt, + } + if stderr.Code(code) != stderr.Success { + set.Status = rchgordmod.FAILURE + statusDesc := rchgordmod.Status(rchgordmod.FAILURE).Desc() + set.StatusDesc = &statusDesc + set.FailureAt = &now + return rchgordmod.CallBackModifyWithProcessing(ctx, nil, id, set) + } + if err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + set.SuccessAt = &now + set.Status = rchgordmod.SUCCESS + statusDesc := rchgordmod.Status(rchgordmod.FAILURE).Desc() + set.StatusDesc = &statusDesc + if err = rchgordmod.CallBackModifyWithProcessing(ctx, t, id, set); err != nil { + return err + } + //默认以实际支付金额上分 + amount := payMoney / 10 + //实际支付小于订单金额1块,返回错误 + if amount < (g.Coins - 10) { + log.WarnX(ctx, "invalid;payMoney invalid payMoney value;", log.Any("gold coins", g.Coins), log.Any("amount", amount)) + return errors.New("invalid payMoney value;") + } + //实际支付金额小于订单金额,范围在1块以内,按订单金额上分 + if amount < g.Coins { + amount = g.Coins + } + wallet, err := walletmod.CreditAmount(t, amount, r.UID) + if err != nil { + return err + } + if err = walletmod.CreditConsumption(t, payMoney, r.UID); err != nil { + return err + } + tt := txnmod.TransactionLog{ + TransNo: r.ID, + UID: r.UID, + Amount: r.Amount, + TranType: txnmod.Rchg.Key(), + TranTypeInt: int64(txnmod.Rchg), + ChannelType: r.RechargeType, + Desc: "充值新增-" + strconv.FormatInt(amount, 10), + DiscDoc: r.DiscDoc, + SysType: r.DevType, + ActualAmount: float64(r.Amount), + RealAmount: walletmod.GetRealAmount(wallet), + } + //插入一条支出流水 + return txnmod.InsertTransactionLog(t, &tt) + }); err != nil { + log.ErrorX(ctx, "WalletSer RechargeCallBack Trans fail", log.E(err)) + return err + } + if !r.VipID.IsZero() { + common.Go(func() { _ = buyVIP(r.UID, r.VipID, r.DevType) }) + } + return nil +} + +func checkVipRenew(u *usermod.User, p *productmod.Product) (time.Time, int, int) { + now := time.Now() + var end time.Time + level := p.VipLevel + payVidDiscount := p.PayVidDiscount + d := time.Hour * 24 * time.Duration(p.Duration) + if u.VipExpireDate.After(now) { //renew + end = u.VipExpireDate.Add(d) + if u.VipLevel > p.VipLevel { //当前用户的vip等级比这次购买的大,使用用户的 + level = u.VipLevel + } + if u.PayVidDiscount > 0 && u.PayVidDiscount < p.PayVidDiscount { + payVidDiscount = u.PayVidDiscount + } + } else { + end = now.Add(d) + } + return end, level, payVidDiscount +} + +func debitPlan(w *walletmod.Wallet, amt int64) *walletmod.DebitPlan { + p := walletmod.DebitPlan{} + l1 := w.Amount - amt + if l1 >= 0 { //amount够了 + p.Amount = amt + return &p + } + // l1 < 0 + p.Amount = w.Amount //amount 扣完 + l2 := w.Income + l1 + if l2 >= 0 { + p.Income = -l1 + return &p + } + return nil //余额不足 +} + +func getDiscInfo(user *usermod.User) txnmod.DiscDoc { + discDoc := txnmod.DiscDoc{} + discDoc.IsDirect = user.IsDirect + discDoc.DistrictCode = user.DistrictCode + discDoc.PromSeqe = user.PromSeqe + return discDoc +} + +// BuyProduct 购买产品产生的行为 +func buyVIP(uid uint64, productID primitive.ObjectID, sys string) stderr.Code { + p, err := productmod.FindProduct(productID, sys) + if err != nil || p == nil { + return stderr.ErrParamError + } + u, err := usermod.FindUserByUID(uid) + if err != nil || u == nil { + return stderr.ErrDbQueryError + } + w, err := walletmod.GetWallet(uid) + if err != nil || w == nil { + return stderr.ErrDbQueryError + } + plan := debitPlan(w, p.DiscountedPrice) + if plan == nil { + return stderr.InsufficientBalance + } + vipExpire, vipLevel, payVidDiscount := checkVipRenew(u, p) + sel := usermod.UserSelector{VipExpireDate: &vipExpire, VipLevel: &vipLevel, PayVidDiscount: &payVidDiscount} + if p.DramaDays > 0 { + expire := usermod.RenewDramaExpire(u.DramaExpire, time.Now(), p.DramaDays) + sel.DramaExpire = &expire + } + if err = webg.VideoDB.Trans(func(t *db.MongoTool) error { + var err error + var wallet *walletmod.Wallet + if wallet, err = walletmod.Debit(t, plan, uid); err != nil { //扣钱 + return err + } + history := prdcthsomod.ProductHistory{ //插入商品购买记录 + UID: uid, + ProductID: productID, + Name: p.Name, + Amount: plan.Amount, + Income: plan.Income, + ProductType: prdcthsomod.VIP, + DiscDoc: u.DiscDoc, + SysType: u.SysType, + ProductSnapShot: p, + } + if err = prdcthsomod.InsertProductHistory(t, &history); err != nil { + return err + } + if err = usermod.UpdateVIP(t, uid, u.VipExpireDate, sel); err != nil { + return err + } + txnLog := txnmod.TransactionLog{UID: uid, Amount: -p.DiscountedPrice, TranType: txnmod.PayVIP.Key(), TranTypeInt: int64(txnmod.PayVIP), TransNo: history.ID, Desc: "购买了" + p.Name, DiscDoc: getDiscInfo(u), SysType: u.SysType, ActualAmount: -float64(p.DiscountedPrice), RealAmount: walletmod.GetRealAmount(wallet)} + return txnmod.InsertTransactionLog(t, &txnLog) + }); err != nil { + return stderr.ErrDbTransError + } + return stderr.Success +} diff --git a/web/service/smsser/smsser.go b/web/service/smsser/smsser.go new file mode 100644 index 0000000..b45379f --- /dev/null +++ b/web/service/smsser/smsser.go @@ -0,0 +1,57 @@ +package smsser + +import ( + "fmt" + + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/common/maths" + "91porn-server/common/sms" + "91porn-server/common/stderr" + "91porn-server/web/webg" +) + +// SendSmsCode 发送验证码 +func SendSmsCode(mobile string) stderr.Code { + redisKey := redisconst.MobileCaptchaKey(mobile) + redisCode, err := webg.Redis.Get(redisKey) + if err != nil { + log.Error("SendCaptcha redis get err", log.E(err)) + return stderr.Failure + } + if redisCode != nil { + log.Info("SendCaptcha Resend", log.Any("mobile", mobile)) + return stderr.ErrCaptchaExists + } + code := maths.RandDigits(constant.CaptchaLen) + fmt.Println(code) + if err = webg.Redis.Set(redisKey, code, redisconst.MobileCaptchaExpire); err != nil { + log.Error("SendCaptcha Save Redis error", log.Any("mobile", mobile), log.E(err)) + return stderr.Failure + } + if err = sms.Send(sms.FengNiao, mobile, code); err != nil { + log.Error("SendCaptcha send err", log.Any("mobile", mobile), log.Any("Platform", sms.FengNiao), log.E(err)) + yunErr := sms.Send(sms.YunPian, mobile, code) + if yunErr != nil { + log.Error("SendCaptcha send err", log.Any("mobile", mobile), log.Any("Platform", sms.YunPian), log.E(err)) + return stderr.ErrSmsSendFail + } + } + return stderr.Success +} + +// VerifySmsCode 验证验证码 +func VerifySmsCode(mobile string, code string) stderr.Code { + redisKey := redisconst.MobileCaptchaKey(mobile) + redisCode, err := webg.Redis.Get(redisKey) + if err != nil { + log.Error("VerifySmsCode redis get err", log.E(err)) + return stderr.Failure + } + if redisCode != nil && *redisCode == code { + return stderr.Success + } + log.Info("VerifySmsCode miss", log.Any("mobile", mobile), log.Any("code", code), log.Any("redisCode", redisCode)) + return stderr.ErrCaptcha +} diff --git a/web/service/statser/daily.go b/web/service/statser/daily.go new file mode 100644 index 0000000..136d7b2 --- /dev/null +++ b/web/service/statser/daily.go @@ -0,0 +1,111 @@ +package statser + +import ( + "91porn-server/common/maths" + "fmt" + "math" + "time" +) + +type DailyRecord struct { + Date time.Time `json:"date"` //日期 + //日常 + NewUserCount float64 `json:"newUserCount"` //新增用户数 + SumUserCount float64 `json:"sumUserCount"` //用户数合计 sum = NewUserCount_1 + NewUserCount_2 + ... + NewUserCount_n + BindUserCount float64 `json:"bindUserCount"` //绑定用户数 + SumBindUserCount float64 `json:"sumBindUserCount"` //绑定用户数合计 sum = BindUserCount_1 + BindUserCount_2 + ... + BindUserCount_n + NewBindUserCount float64 `json:"newBindUserCount"` //新增绑定用户数 + VisitUserCount float64 `json:"visitUserCount"` //访问用户数 + SumVisitUserCount float64 `json:"sumVisitUserCount"` //访问用户数合计 sum = VisitUserCount_1 + VisitUserCount_2 + ... + VisitUserCount_n + AveVisitUserCount float64 `json:"aveVisitUserCount"` //平均访问用户数 = SumVisitUserCount / n + IOSRetainUserRate2Day string `json:"iosRetainUserRate2Day"` //IOS次日用户留存率 + ANDRetainUserRate2Day string `json:"andRetainUserRate2Day"` //安卓次日用户留存率 + H5RetainUserRate2Day string `json:"h5RetainUserRate2Day"` //H5次日用户留存率 + RetainUserRate2Day string `json:"retainUserRate2Day"` //次日用户留存率 + RetainUserRate3Day string `json:"retainUserRate3Day"` //3日用户留存率 + RetainUserRate7Day string `json:"retainUserRate7Day"` //7日用户留存率 + RetainUserRate15Day string `json:"retainUserRate15Day"` //15日用户留存率 + RetainUserRate30Day string `json:"retainUserRate30Day"` //30日用户留存率 + //视屏 + NewVideoCount int64 `json:"newVideoCount"` //新增短视屏数 + PlayUserCount float64 `json:"playUserCount"` //播放视屏人数 + PeakViewCount int64 `json:"peakViewCount"` //[未完成]峰值观看数 + PeakViewTime int64 `json:"peakViewTime"` //[未完成]峰值观看时长 + EffecViewCount int64 `json:"effecViewCount"` //[未完成]有效观看数 + AveEffecViewTime int64 `json:"aveEffecViewTime"` //[未完成]平均有效观看时长 + //消费 + PayGold int64 `json:"payAmount"` //消费金币 = 消费付费视屏金额 + 消费vip金额 + PayVipGold int64 `json:"payVipAmount"` //购买VIP金币 + PayVidGold int64 `json:"payVidAmount"` //视频消费金币 + PayVipUserCount float64 `json:"payVipUserCount"` //购买VIP人数 + PayUserCount float64 `json:"payUserCount"` //消费人数 + NewAndPayUserCount float64 `json:"newAndPayUserCount"` //新增并消费人数 + SumPayGold float64 `json:"sumPayAmount"` //消费金币合计 = PayAmount_1 + PayAmount_2 + ... + PayAmount_n + AvePayGold int64 `json:"avePayAmount"` //平均消费金币 = SumPayGold / n + //充值 + MerchantRechargeAmount float64 `json:"merchantRechargeAmount"` //[未完成]商人充值额度 + OnlineRechargeAmount float64 `json:"onlineRechargeAmount"` //在线用户充值金额 + TotalRechargeAmount float64 `json:"totalRechargeAmount"` //总充值金额 = //在线用户充值金额 + 商人充值额度 + RechargeUserCount float64 `json:"rechargeUserCount"` //充值用户数 + NewAndRechargeUserCount float64 `json:"newAndRechargeUserCount"` //新增充值用户数 + ARPU int64 `json:"arpu"` //Average Revenue Per User 每用户平均收入=总收入(总充值)/访问用户数 + ARPPU int64 `json:"arppu"` //Average Revenue Per Paying User 每充值用户平均收益=总收入(总充值)/充值用户数 + SumRechargeAmount float64 `json:"sumRechargeAmount"` //充值金额合计 sum = RechargeUserCount_1 + RechargeUserCount_2 + ... + RechargeUserCount_n + AveRechargeAmount int64 `json:"aveRechargeAmount"` //平均充值金额 = SumRechargeAmount / n + //提现 + GoldWithdrawAmount float64 `json:"goldWithdrawAmount"` //金币提现金额 + ProxyWithdrawAmount float64 `json:"proxyWithdrawAmount"` //全民代理提现金额 +} + +func (this *DailyRecord) BaiYuan() *DailyRecord { + this.MerchantRechargeAmount = maths.ToFloat64_b2(this.MerchantRechargeAmount / 100) + this.OnlineRechargeAmount = maths.ToFloat64_b2(this.OnlineRechargeAmount / 100) + this.TotalRechargeAmount = maths.ToFloat64_b2(this.TotalRechargeAmount / 100) + this.SumRechargeAmount = maths.ToFloat64_b2(this.SumRechargeAmount / 100) + this.GoldWithdrawAmount = maths.ToFloat64_b2(this.GoldWithdrawAmount / 100) + this.ProxyWithdrawAmount = maths.ToFloat64_b2(this.ProxyWithdrawAmount / 100) + + this.NewUserCount = maths.ToFloat64_b2(this.NewUserCount / 100) + this.SumUserCount = maths.ToFloat64_b2(this.SumUserCount / 100) + this.BindUserCount = maths.ToFloat64_b2(this.BindUserCount / 100) + this.SumBindUserCount = maths.ToFloat64_b2(this.SumBindUserCount / 100) + this.NewBindUserCount = maths.ToFloat64_b2(this.NewBindUserCount / 100) + this.VisitUserCount = maths.ToFloat64_b2(this.VisitUserCount / 100) + this.SumVisitUserCount = maths.ToFloat64_b2(this.SumVisitUserCount / 100) + this.AveVisitUserCount = maths.ToFloat64_b2(this.AveVisitUserCount / 100) + this.PlayUserCount = maths.ToFloat64_b2(this.PlayUserCount / 100) + this.PayVipUserCount = maths.ToFloat64_b2(this.PayVipUserCount / 100) + this.PayUserCount = maths.ToFloat64_b2(this.PayUserCount / 100) + this.NewAndPayUserCount = maths.ToFloat64_b2(this.NewAndPayUserCount / 100) + this.RechargeUserCount = maths.ToFloat64_b2(this.RechargeUserCount / 100) + this.NewAndRechargeUserCount = maths.ToFloat64_b2(this.NewAndRechargeUserCount / 100) + return this +} + +type DailyPages struct { + Total int64 `json:"total"` + List []*DailyRecord `json:"list"` +} + +// 四舍五入 +func Rounding(v float64) int64 { + return int64(math.Floor(v + 0.5)) +} + +func Percentage(v float64) string { + return fmt.Sprintf("%.3f%%", v*100) +} + +func Divide(a, b int64) float64 { + if b == 0 { + return 0 + } + return float64(a) / float64(b) +} + +func toInt64(v *int64) int64 { + if v == nil { + return 0 + } + return *v +} diff --git a/web/service/statser/order.go b/web/service/statser/order.go new file mode 100644 index 0000000..1d0de45 --- /dev/null +++ b/web/service/statser/order.go @@ -0,0 +1,301 @@ +package statser + +import ( + "91porn-server/common/maths" + "91porn-server/common/pageopt" + "91porn-server/common/timeutil/timerange" + "91porn-server/models/l/payvidlgmod" + "91porn-server/models/s/statordermod" + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type OrderData struct { + CompleteRate string `json:"completeRate"` //完成率 = 完成订单数 / 总订单数 + OrderCount int64 `json:"orderCount"` //生产订单数 + CompleteOrderCount int64 `json:"completeOrderCount"` //完成订单数 + OrderAmount float64 `json:"orderAmount"` //生产订单金额 + CompleteOrderAmount float64 `json:"completeOrderAmount"` //完成订单的实际支付金额 + PaidOrderCount int64 `json:"paidOrderCount"` //用户已支付金额/三方已出款金额 + PaidAmount float64 `json:"paidAmount"` //用户已支付订单数/三方已出款订单数 +} + +func (o *OrderData) From(items statordermod.Items) { + o.CompleteRate = Percentage(Divide(items.CompleteOrderCount, items.OrderCount)) + o.OrderCount = items.OrderCount + o.CompleteOrderCount = items.CompleteOrderCount + o.OrderAmount = float64(items.OrderAmount) + o.CompleteOrderAmount = float64(items.CompleteOrderAmount) + o.PaidOrderCount = items.PaidOrderCount + o.PaidAmount = float64(items.PaidAmount) +} + +func (o *OrderData) BaiYuan() *OrderData { + o.OrderAmount = maths.ToFloat64_b2(o.OrderAmount / 100) + o.CompleteOrderAmount = maths.ToFloat64_b2(o.CompleteOrderAmount / 100) + o.PaidAmount = maths.ToFloat64_b2(o.PaidAmount / 100) + return o +} + +type OrderRecord struct { + ID primitive.ObjectID `json:"id"` // ID + SumDate time.Time `json:"sumDate"` //日期 + Type statordermod.OrderType `json:"type"` //订单类型 + Channel string `json:"channel"` //渠道 + OrderData + CreatedAt time.Time `json:"createdAt"` //文档创建时间 +} + +type OrderPage struct { + Total int64 `json:"total"` + List []OrderRecord `json:"list"` +} + +type OrderChannelMatch = statordermod.ChannelMatch + +type OrderTypeMatch = statordermod.TypeMatch + +type OrderSumDateGTEAndLTMatch = statordermod.SumDateGTEAndLTMatch + +var Order_Sort_SumDate_n1 = statordermod.Sort_SumDate_n1 + +func OrderPages(skip, limit int64, + channelMatch OrderChannelMatch, + typeMatch OrderTypeMatch, + sumDateGTEAndLTMatch OrderSumDateGTEAndLTMatch, +) (OrderPage, error) { + matList := []pageopt.Matcher{ + channelMatch.New(), + typeMatch.New(), + sumDateGTEAndLTMatch.New(), + } + list, err := statordermod.List(Order_Sort_SumDate_n1, &skip, &limit, matList...) + if err != nil { + return OrderPage{}, err + } + recordList := make([]OrderRecord, 0, len(list)) + for _, v := range list { + record := OrderRecord{ + ID: v.ID, + SumDate: v.SumDate, + Type: v.Type, + Channel: v.Channel, + CreatedAt: v.CreatedAt, + } + record.From(v.Items) + record.BaiYuan() + recordList = append(recordList, record) + } + count, err := statordermod.Count(matList...) + if err != nil { + return OrderPage{}, err + } + return OrderPage{ + Total: count, + List: recordList, + }, nil +} + +type RchgordChannelMatch = rchgordmod.ChannelMatch + +type RchgordCreatedAtGTEAndLTMatch = rchgordmod.CreatedAtGTEAndLTMatch + +func RechargeSucRateMap(sumDate time.Time, channel string) (map[int64]float64, error) { + locDayRange := timerange.LocDayRange(sumDate) + return rchgordmod.SuccessRateMap(channel, locDayRange.Head, locDayRange.Tail, 15) +} + +type DataStruct struct { + SumData string `json:"sumData"` //统计日期 + VIPRecharge int64 `json:"vipRecharge"` //会员卡充值 + CoinRecharge int64 `json:"coinRecharge"` //金币充值 + LoufengTrade int64 `json:"loufengTrade"` //楼凤交易充值 + VipTrande map[string]*VipTrande `json:"vipTrande"` + NewUser int64 `json:"newCount"` //新增用户 +} +type VipTrande struct { + ID primitive.ObjectID `json:"id"` + Name string `json:"name"` + Count int64 `json:"count"` //购买次数 + Amount int64 `json:"amount"` //购买金额 +} + +func getDateString(t time.Time) string { + return t.Format("20060102") +} +func DataStat(start, end time.Time) (res []*DataStruct, err error, vipTitle []string) { + m := make(map[string]*DataStruct) + productMap := make(map[primitive.ObjectID]productmod.ProductWeb) + products, err := productmod.FindMany() + if err != nil { + return + } + vipTitle = make([]string, 0) + for i := range products { + productMap[products[i].ID] = products[i] + vipTitle = append(vipTitle, products[i].Name) + + } + vipTitle = append(vipTitle, "未知") + data, err := prdcthsomod.FindMany(start, end, options.Find()) + if err != nil { + return + } + for _, v := range data { + key := getDateString(v.CreatedAt) + name := productMap[v.ProductID].Name + if name == "" { + name = "未知" + } + if _, ok := m[key]; ok { + if _, ok2 := m[key].VipTrande[name]; ok2 { + m[key].VipTrande[name].Count += 1 + m[key].VipTrande[name].Amount += v.Amount + } else { + m[key].VipTrande[name] = &VipTrande{ + Name: name, + Count: 1, + Amount: v.Income + v.Amount, + } + } + } else { + vipTrandeM := make(map[string]*VipTrande) + m[key] = &DataStruct{ + SumData: key, + VipTrande: vipTrandeM, + } + m[key].VipTrande[name] = &VipTrande{ + Name: name, + Count: 1, + Amount: v.Income + v.Amount, + } + } + } + orders, err := rchgordmod.GetOrderByTime(start, end) + if err != nil { + return + } + for _, v := range orders { + key := getDateString(v.CreatedAt) + if _, ok := m[key]; ok { + } else { + m[key] = &DataStruct{ + SumData: key, + } + } + if v.VipID.IsZero() { + m[key].CoinRecharge += v.PayMoney + } else { + m[key].VIPRecharge += v.PayMoney + } + } + trans, err := txnmod.TransStat(start, end) + if err != nil { + return + } + for _, v := range trans { + key := getDateString(v.CreatedAt) + if _, ok := m[key]; ok { + } else { + m[key] = &DataStruct{ + SumData: key, + } + } + m[key].LoufengTrade += int64(v.ActualAmount) + } + userStat, err := usermod.FindByCreateTime(start, end) + if err != nil { + return + } + for _, v := range userStat { + key := getDateString(v.CreatedAt) + if _, ok := m[key]; ok { + } else { + m[key] = &DataStruct{ + SumData: key, + } + } + m[key].NewUser += 1 + } + res = make([]*DataStruct, 0) + for start.Before(end) { + key := getDateString(start) + if _, ok := m[key]; ok { + } else { + m[key] = &DataStruct{ + SumData: key, + } + } + res = append(res, m[key]) + start = start.Add(time.Hour * 24) + } + return +} + +type PayVidStatInfo struct { + SumData string `json:"sumData"` //统计日期 + ShortVidAmount int64 `json:"shortVidAmount"` //短视频销售额 + ShortVidCount int64 `json:"ShortVidCount"` //短视频销售数量 + LongVidAmount int64 `json:"longVidAmount"` //短视频销售额 + LongVidCount int64 `json:"longVidCount"` //短视频销售数量 +} + +func PayVidStat(start, end time.Time) (res []*PayVidStatInfo, err error) { + m := make(map[string]*PayVidStatInfo) + payVidLogs, err := payvidlgmod.GetPayVidLogsByTime(start, end) + if err != nil { + return + } + vids := make([]primitive.ObjectID, 0) + for i := range payVidLogs { + vids = append(vids, payVidLogs[i].VideoID) + } + longVid, err := vidmod.FindShortVidByIDs(vids) + if err != nil { + return + } + longVidMap := make(map[primitive.ObjectID]int) + for i := range longVid { + longVidMap[longVid[i]] = 1 + } + for _, v := range payVidLogs { + key := getDateString(v.CreatedAt) + if _, ok := m[key]; ok { + } else { + m[key] = &PayVidStatInfo{ + SumData: key, + } + } + _, ok := longVidMap[v.VideoID] + if ok { + m[key].LongVidAmount += v.PayMoney + m[key].LongVidCount += 1 + + } else { + m[key].ShortVidAmount += v.PayMoney + m[key].ShortVidCount += 1 + } + } + + res = make([]*PayVidStatInfo, 0) + for start.Before(end) { + key := getDateString(start) + if _, ok := m[key]; ok { + } else { + m[key] = &PayVidStatInfo{ + SumData: key, + } + } + res = append(res, m[key]) + start = start.Add(time.Hour * 24) + } + return +} diff --git a/web/service/sysconfser/short_drama_card_test.go b/web/service/sysconfser/short_drama_card_test.go new file mode 100644 index 0000000..f557a9d --- /dev/null +++ b/web/service/sysconfser/short_drama_card_test.go @@ -0,0 +1,26 @@ +package sysconfser + +import ( + "testing" + + "91porn-server/models/v/productmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestShortDramaCardOptions(t *testing.T) { + first := primitive.NewObjectID() + second := primitive.NewObjectID() + products := []productmod.Product{ + {ID: first, Name: "短剧月卡"}, + {ID: second, Name: "短剧季卡"}, + } + options := shortDramaCardOptions(products) + if len(options) != 2 { + t.Fatalf("options length = %d, want 2", len(options)) + } + if options[0].Key != "短剧月卡" || options[0].Value != first.Hex() || + options[1].Key != "短剧季卡" || options[1].Value != second.Hex() { + t.Fatalf("options = %#v", options) + } +} diff --git a/web/service/sysconfser/sysconf.go b/web/service/sysconfser/sysconf.go new file mode 100755 index 0000000..9e0df21 --- /dev/null +++ b/web/service/sysconfser/sysconf.go @@ -0,0 +1,180 @@ +package sysconfser + +import ( + "sync" + "time" + + "91porn-server/models/cache/sysconfdata" + "91porn-server/models/v/productmod" + "91porn-server/models/v/sysconfmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WebListReq struct { + GpCode *string `form:"gpCode" json:"gpCode"` +} + +var ensureSysConfInitOnce sync.Once + +type WebListRes struct { + Data map[string][]*sysconfmod.SysConf `json:"data"` +} + +// GetAll 获取所有配置 +func (q *WebListReq) GetAll() (res WebListRes, err error) { + ensureSysConfInitOnce.Do(func() { + sysconfmod.EnsureInitData() + }) + + filter := bson.M{} + if q.GpCode != nil { + filter["gpCode"] = *q.GpCode + } + + sort := bson.D{{Key: "sort_order", Value: -1}} + list, e := sysconfmod.GetAll(filter, sort) + if e != nil { + return res, e + } + if err = fillShortDramaCardOptions(list); err != nil { + return res, err + } + res.Data = groupByField(list) + return +} + +func fillShortDramaCardOptions(items []*sysconfmod.SysConf) error { + var target *sysconfmod.SysConf + for _, item := range items { + if item != nil && item.VCode == string(sysconfmod.VCodeShortDramaCardID) { + target = item + break + } + } + if target == nil { + return nil + } + products, err := productmod.FindActiveDramaCards() + if err != nil { + return err + } + target.SelectValues = shortDramaCardOptions(products) + return nil +} + +func shortDramaCardOptions(products []productmod.Product) []sysconfmod.SelectItem { + options := make([]sysconfmod.SelectItem, 0, len(products)) + for i := range products { + options = append(options, sysconfmod.SelectItem{ + Key: products[i].Name, Value: products[i].ID.Hex(), + }) + } + return options +} + +func groupByField(items []*sysconfmod.SysConf) map[string][]*sysconfmod.SysConf { + grouped := make(map[string][]*sysconfmod.SysConf) + for _, item := range items { + grouped[item.GpCode] = append(grouped[item.GpCode], item) + } + + return grouped +} + +type WebCreateReq struct { + GroupName string `json:"groupName" form:"groupName" binding:"required"` // 分组名 + GpCode string `json:"gpCode" form:"gpCode" binding:"required"` // 分组编码 + VCode string `json:"vCode" form:"vCode" binding:"required"` // 变量名 + Title string `json:"title" form:"title" binding:"required"` // 变量标题 + Tip string `json:"tip" form:"tip"` // 变量描述 + Type string `json:"type" form:"type" binding:"required,oneof=text string img int float bool object text-array string-array select"` // 类型:text,string,img,int,float,bool,object,text-array,string-array,string-select + Value string `json:"value" bson:"value"` // 变量值 + SelectValues []sysconfmod.SelectItem `json:"selectValues" form:"selectValues"` + IsRequired bool `json:"is_required" form:"is_required"` // 是否必填 + SortOrder int `json:"sort_order" form:"sort_order" binding:"required"` // 排序值 +} + +// Create 发布数据 +func (p *WebCreateReq) Create() error { + data := sysconfmod.SysConf{ + GroupName: p.GroupName, + GpCode: p.GpCode, + VCode: p.VCode, + Title: p.Title, + Tip: p.Tip, + Type: p.Type, + Value: p.Value, + IsRequired: p.IsRequired, + SortOrder: p.SortOrder, + SelectValues: p.SelectValues, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // 创建数据 + if _, err := sysconfdata.InsertData(nil, data); err != nil { + return err + } + + return nil +} + +type WebUpdateReq struct { + Configs []ConfigParams `json:"configs" binding:"required"` +} +type ConfigParams struct { + ID primitive.ObjectID `json:"id" binding:"required"` + Value string `json:"value"` // 变量值 +} + +// Update 更新数据 +func (p *WebUpdateReq) Update() error { + for _, pp := range p.Configs { + _, err := sysconfmod.GetInfo(pp.ID) + if err != nil { + return err + } + + data := make(map[string]interface{}) + data["value"] = pp.Value + data["updatedAt"] = time.Now() + + if _, err = sysconfdata.UpdateData(nil, pp.ID.Hex(), data); err != nil { + return err + } + } + + return nil +} + +type WebDeleteReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` +} + +// Delete 删除数据 +func (p *WebDeleteReq) Delete() error { + if err := sysconfdata.DeleteData(nil, p.ID); err != nil { + return err + } + + return nil +} + +type WebUpdateSelectReq struct { + ID primitive.ObjectID `json:"id" binding:"required"` + SelectValues []sysconfmod.SelectItem `json:"selectValues" bson:"selectValues"` +} + +// Delete 删除数据 +func (p *WebUpdateSelectReq) Update() (err error) { + data := make(map[string]interface{}) + data["selectValues"] = p.SelectValues + data["updatedAt"] = time.Now() + _, err = sysconfdata.UpdateData(nil, p.ID.Hex(), data) + if err != nil { + return err + } + return nil +} diff --git a/web/service/tagser/recmd.go b/web/service/tagser/recmd.go new file mode 100644 index 0000000..0fc0b81 --- /dev/null +++ b/web/service/tagser/recmd.go @@ -0,0 +1,52 @@ +package tagser + +import ( + "91porn-server/models/v/recmdtag" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type RecmdTagRecord struct { + ID primitive.ObjectID `json:"id"` //ID + Types recmdtag.RecmdType `json:"types"` //种类 + Name string `json:"name"` //名字 + SortCode int64 `json:"sortCode"` //倒序 + Enable bool `json:"enable"` //使能 true:开 + Remark string `json:"remark"` //备注 +} + +type RecmdTagPage struct { + Total int64 `json:"total"` + List []RecmdTagRecord `json:"list"` +} + +// RecmdTagPages 获取推荐标签页 +func RecmdTagPages(skip, limit int64, typesMatch recmdtag.TypesMatch, nameMatch recmdtag.NameMatch) (RecmdTagPage, error) { + mats := []recmdtag.Matcher{ + typesMatch.New(), + nameMatch.New(), + } + list, err := recmdtag.List(recmdtag.Sort_sortCode_N1, &skip, &limit, mats...) + if err != nil { + return RecmdTagPage{}, nil + } + recordList := make([]RecmdTagRecord, len(list)) + for i, v := range list { + recordList[i] = RecmdTagRecord{ + ID: v.ID, + Types: v.Types, + Name: v.Name, + SortCode: v.SortCode, + Enable: v.Enable, + Remark: v.Remark, + } + } + count, err := recmdtag.Count(mats...) + if err != nil { + return RecmdTagPage{}, nil + } + return RecmdTagPage{ + count, + recordList, + }, nil +} diff --git a/web/service/tagser/tag.go b/web/service/tagser/tag.go new file mode 100644 index 0000000..59a4b21 --- /dev/null +++ b/web/service/tagser/tag.go @@ -0,0 +1,147 @@ +package tagser + +import ( + "91porn-server/common/log" + "91porn-server/models/commod" + "91porn-server/models/v/recmdtag" + "91porn-server/models/v/tagconfmod" + "91porn-server/models/v/tagmod" + + "github.com/jinzhu/copier" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type TagSlice []tagmod.Tag + +func (t TagSlice) ToIDs() []primitive.ObjectID { + ids := []primitive.ObjectID{} + for _, v := range t { + ids = append(ids, v.ID) + } + return ids +} + +// 获取标签列表 +func GetTagList(showWonder int, content string, stdQuery commod.StdQuery) (data interface{}, err error) { + tList, total, err := tagmod.FindTagsAndCount(showWonder, content, stdQuery) + if err != nil { + log.Error("web service tag GetTagList FindTagsAndCount fail error", log.E(err), log.Any("showWonder", showWonder), log.Any("content", content)) + return nil, err + } + idMap, _ := recmdtag.IDMapByTIDs(TagSlice(tList).ToIDs()) + // 获取推送状态 + infoList := make([]tagmod.WebTagListRespInfo, len(tList)) + for i, value := range tList { + rid := idMap[value.ID] //推荐标签ID + // 播放量&播放率 + infoList[i] = tagmod.WebTagListRespInfo{ + ID: value.ID, + TagName: value.TagName, + TagDesc: value.TagDesc, + HotMark: value.HotMark, + CoverImg: value.CoverImg, + SortCode: value.SortCode, + VPlayCount: value.VPlayCount, + TPlayCount: value.TPlayCount, + FakePlayCount: value.FakePlayCount, + CollCount: value.CollCount, + FakeCollCount: value.FakeCollCount, + WonderSortCode: value.WonderSortCode, + IsWonder: value.IsWonder, + CreatedAt: value.CreatedAt, + UpdatedAt: value.UpdatedAt, + VideoCount: value.VideoCount, + IsCommonUsed: value.IsCommonUsed, + UsedSortCode: value.UsedSortCode, + IsRecmd: !idMap[value.ID].IsZero(), + RID: rid, + IsActive: value.IsActive, + } + } + if len(infoList) == 0 { + infoList = []tagmod.WebTagListRespInfo{} + } + resp := make(map[string]interface{}) + resp["list"] = infoList + resp["total"] = total + data = resp + return +} + +// 获取纯净版标签列表 +func GetPureTagList(content string, stdQuery commod.StdQuery) (data interface{}, err error) { + tList, err := tagmod.FindPureTags(content, stdQuery) + if err != nil { + log.Error("web service tag GetPureTagList fail error", log.E(err), log.Any("content", content)) + return nil, err + } + total, err := tagmod.PureTagsCount(content) + if err != nil { + log.Error("web service tag GetPureTagList fail error", log.E(err), log.Any("content", content)) + return nil, err + } + resp := make(map[string]interface{}) + resp["list"] = tList + resp["total"] = total + data = resp + return +} + +// 编辑标签 +func UpdateTag(tagID primitive.ObjectID, doc tagmod.TagUpdateDoc) (err error) { + tMod, err := tagmod.FindOneTagByID(tagID) + if err != nil { + log.Error("web service tag UpdateTag FindOneTagByID fail error", log.E(err), log.Any("tagID", tagID)) + return + } + if doc.FakePlayCount != nil { + playCount := tMod.TPlayCount + *doc.FakePlayCount - tMod.FakePlayCount + doc.TPlayCount = &playCount + } + if doc.FakeCollCount != nil { + collCount := tMod.CollCount + *doc.FakeCollCount - tMod.FakeCollCount + doc.CollCount = &collCount + } + return tagmod.UpdateOneTagByID(tagID, doc) +} + +// 添加标签 +func AddTag(tag tagmod.Tag) (data interface{}, err error) { + return tagmod.InsertOne(&tag) +} + +// 删除标签 +func DelTag(tag tagmod.WebTagDeleteReqInfo) error { + _, err := tagmod.DeleteTags(tag.IDs) + return err +} + +// GetTagconfList 获取标签配置列表 +func GetTagconfList(page commod.Page) (resp TagConfListResponse, err error) { + confs, total, hasNext, err := tagconfmod.List(page) + if err != nil { + return + } + confMap := make(map[primitive.ObjectID]tagconfmod.TagConf) + tagIDs := make([]primitive.ObjectID, len(confs)) + for i, conf := range confs { + tagIDs[i] = conf.TagID + confMap[conf.TagID] = conf + } + tags, err := tagmod.FindTagsByIDS(tagIDs) + if err != nil { + return + } + if err = copier.Copy(&resp.List, &tags); err != nil { + return + } + for i, v := range resp.List { + resp.List[i].ID = confMap[v.TagID].ID + resp.List[i].Sort = *confMap[v.TagID].Sort + resp.List[i].CreatedAt = confMap[v.TagID].CreatedAt + resp.List[i].UpdatedAt = confMap[v.TagID].UpdatedAt + } + resp.Total = total + resp.HasNext = hasNext + return +} diff --git a/web/service/tagser/type.go b/web/service/tagser/type.go new file mode 100644 index 0000000..9d8be13 --- /dev/null +++ b/web/service/tagser/type.go @@ -0,0 +1,32 @@ +package tagser + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type TagConfListResponse struct { + List []TagConfDetail `json:"list"` // 标签配置列表 + HasNext bool `json:"hasNext"` // 是否还有下一页 + Total int64 `json:"total"` // 总数 +} + +type TagConfDetail struct { + ID primitive.ObjectID `json:"id"` + TagID primitive.ObjectID `json:"tagID"` // 标签ID + Sort int `json:"sort"` // 排序 + TagName string `json:"tagName"` // 标签名字 前端使用name + TagDesc string `json:"tagDesc"` // 标签描述 + CoverImg string `json:"coverImg"` // 封面图片 + VPlayCount int64 `json:"vPlayCount"` // 有效播放量 + TPlayCount int64 `json:"tPlayCount"` // 总播放量 = 真播放量+假播放量 夏沫确认 + FakePlayCount int64 `json:"fakePlayCount"` // 播放量(假数据) + PlayRating float64 `json:"playRating"` // 有效播放率 + CollCount int64 `json:"collCount"` // 收藏数 + LikeCount int64 `json:"likeCount"` // 喜欢数: 当对通过标签获取的视频点赞时,计数+1 + FakeCollCount int64 `json:"fakeCollCount"` // 收藏数(假数据) + VideoCount int64 `json:"videoCount"` // 使用此标签的视频数量 + CreatedAt time.Time `json:"createdAt"` // 创建时 + UpdatedAt time.Time `json:"updatedAt"` // 更新时间 +} diff --git a/web/service/taskser/config.go b/web/service/taskser/config.go new file mode 100644 index 0000000..a80fef0 --- /dev/null +++ b/web/service/taskser/config.go @@ -0,0 +1,52 @@ +package taskser + +import ( + "time" + + "91porn-server/common/stderr" + "91porn-server/models/v/taskmod" +) + +func AddConfig(in taskmod.AddConfigCond) stderr.Code { + if err := taskmod.AddConfig(taskmod.TaskConfig{ + Title: in.Title, + Desc: in.Desc, + Type: in.Type, + Detail: in.Detail, + Link: in.Link, + Status: in.Status, + UpdateTime: time.Now(), + CreateTime: time.Now(), + }); err != nil { + return stderr.ErrDbInsertError + } + return stderr.Success +} + +func ModifyConfig(in *taskmod.ModifyConfigCond) stderr.Code { + if err := taskmod.ModifyConfig(in.Cond(), in.Bson()); err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success +} + +func QueryAllConfig(in *taskmod.QueryAllConfigCond) (interface{}, stderr.Code) { + var data map[string]interface{} = map[string]interface{}{ + "list": []interface{}{}, + "count": 0, + } + count, err := taskmod.CountConfig(in.Query()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + if count == 0 { + return data, stderr.Success + } + data["count"] = count + list, err := taskmod.QueryAllConfig(in.Query(), in.Options()) + if err != nil { + return nil, stderr.ErrDbQueryError + } + data["list"] = list + return data, stderr.Success +} diff --git a/web/service/taskser/daily.go b/web/service/taskser/daily.go new file mode 100644 index 0000000..6e35f2a --- /dev/null +++ b/web/service/taskser/daily.go @@ -0,0 +1,114 @@ +package taskser + +import ( + "go.mongodb.org/mongo-driver/mongo/options" + "time" + + "91porn-server/models/commod" + "91porn-server/models/v/dailytaskmod" + + "go.mongodb.org/mongo-driver/bson" +) + +type DailyTaskListReq struct { + commod.Page + Status *bool `form:"status" json:"status" binding:"-"` +} + +func GetDailyTaskList(p DailyTaskListReq) ([]dailytaskmod.DailyTask, bool, error) { + opt := options.Find() + opt.SetLimit(p.Limit64()).SetSkip(p.Skip64()) + opt.SetSort(bson.D{{"sortNum", 1}}) + filter := bson.M{} + if p.Status != nil { + filter["status"] = *p.Status + } + ot, next, err := dailytaskmod.GetTasks(filter, opt) + if err != nil { + return nil, false, err + } + return ot, next, nil +} + +func AddDailyTask(adt dailytaskmod.AddDailyTaskReq) error { + var dt dailytaskmod.DailyTask + i := 0 + if adt.Title != nil { + dt.Title = *adt.Title + i++ + } + if adt.Desc != nil { + dt.Desc = *adt.Desc + i++ + } + if adt.Img != nil { + dt.Img = *adt.Img + i++ + } + if len(adt.Detail) != 0 { + dt.Detail = adt.Detail + i++ + } + if adt.Type != nil { + dt.Type = *adt.Type + i++ + } + if adt.Link != nil { + dt.Link = *adt.Link + i++ + } + if adt.Status != nil { + dt.Status = *adt.Status + i++ + } + if adt.SortNum != nil { + dt.SortNum = *adt.SortNum + i++ + } + if i == 0 { + return nil + } + now := time.Now() + dt.CreatedAt = now + dt.UpdatedAt = now + if err := dailytaskmod.AddDailyTask(nil, dt); err != nil { + return err + } + return nil +} + +func EditDailyTask(edtr dailytaskmod.EditDailyTaskReq) error { + if edtr.ID.IsZero() { + return nil + } + b := make(bson.M) + if edtr.Title != nil { + b["title"] = *edtr.Title + } + if edtr.Desc != nil { + b["desc"] = *edtr.Desc + } + if edtr.Img != nil { + b["img"] = *edtr.Img + } + if len(edtr.Detail) != 0 { + b["detail"] = edtr.Detail + } + if edtr.Type != nil { + b["type"] = *edtr.Type + } + if edtr.Link != nil { + b["link"] = *edtr.Link + } + if edtr.Status != nil { + b["status"] = *edtr.Status + } + if edtr.SortNum != nil { + b["sortNum"] = *edtr.SortNum + } + if len(b) == 0 { + return nil + } + b["updatedAt"] = time.Now() + return dailytaskmod.EditDailyTask(nil, edtr.ID, b) +} diff --git a/web/service/taskser/once.go b/web/service/taskser/once.go new file mode 100644 index 0000000..5db54e4 --- /dev/null +++ b/web/service/taskser/once.go @@ -0,0 +1,116 @@ +package taskser + +import ( + "time" + + "91porn-server/models/commod" + "91porn-server/models/v/oncetaskmod" + + "go.mongodb.org/mongo-driver/bson" +) + +func GetOnceTaskList(p commod.Page) ([]oncetaskmod.OnceTask, bool, error) { + ot, err := oncetaskmod.GetOnceTaskAll(nil) + if err != nil { + return nil, false, err + } + skip := p.Skip() + if uint64(len(ot)) <= skip { + return nil, false, nil + } + ot = ot[skip:] + if uint64(len(ot)) <= p.Limit() { + return ot, false, nil + } + return ot[:p.Limit()], true, nil +} + +func AddOnceTask(aor oncetaskmod.AddOnceReq) error { + var ot oncetaskmod.OnceTask + i := 0 + if aor.Title != nil { + ot.Title = *aor.Title + i++ + } + if aor.Desc != nil { + ot.Desc = *aor.Desc + i++ + } + if aor.Img != nil { + ot.Img = *aor.Img + i++ + } + if len(aor.Prizes) != 0 { + ot.Prizes = aor.Prizes + i++ + } + if aor.Type != nil { + ot.Type = *aor.Type + i++ + } + if aor.FinishCondition != nil { + ot.FinishCondition = *aor.FinishCondition + i++ + } + if aor.Link != nil { + ot.Link = *aor.Link + i++ + } + if aor.Status != nil { + ot.Status = *aor.Status + i++ + } + if aor.SortNum != nil { + ot.SortNum = *aor.SortNum + i++ + } + if i == 0 { + return nil + } + now := time.Now() + ot.CreatedAt = now + ot.UpdatedAt = now + if err := oncetaskmod.AddOnceTask(nil, ot); err != nil { + return err + } + return nil +} + +func EditOnceTask(eor oncetaskmod.EditOnceReq) error { + if eor.ID.IsZero() { + return nil + } + b := make(bson.M) + if eor.Title != nil { + b["title"] = *eor.Title + } + if eor.Desc != nil { + b["desc"] = *eor.Desc + } + if eor.Img != nil { + b["img"] = *eor.Img + } + if len(eor.Prizes) != 0 { + b["prizes"] = eor.Prizes + } + if eor.Type != nil { + b["type"] = *eor.Type + } + if eor.FinishCondition != nil { + b["finishCondition"] = *eor.FinishCondition + } + if eor.Link != nil { + b["link"] = *eor.Link + } + if eor.Status != nil { + b["status"] = *eor.Status + } + if eor.SortNum != nil { + b["sortNum"] = *eor.SortNum + } + if len(b) == 0 { + return nil + } + b["updatedAt"] = time.Now() + return oncetaskmod.UpdateOnceTask(nil, eor.ID, b) +} diff --git a/web/service/taskser/sign.go b/web/service/taskser/sign.go new file mode 100644 index 0000000..a53eabd --- /dev/null +++ b/web/service/taskser/sign.go @@ -0,0 +1,55 @@ +package taskser + +import ( + "91porn-server/common/stderr" + "91porn-server/models/v/signtaskmod" + "time" +) + +func SignAddConfig(in signtaskmod.AddConfigCond) stderr.Code { + if err := signtaskmod.AddConfig(signtaskmod.TaskConfig{ + Title: in.Title, + Desc: in.Desc, + Img: in.Img, + FinishCondition: in.FinishCondition, + Link: in.Link, + Prizes: in.Prizes, + ExtraPrizes: in.ExtraPrizes, + Status: in.Status, + UpdateTime: time.Now(), + CreateTime: time.Now(), + }); err != nil { + return stderr.ErrDbInsertError + } + return stderr.Success +} + +func SignModifyConfig(in *signtaskmod.ModifyConfigCond) stderr.Code { + if err := signtaskmod.ModifyConfig(in.Cond(), in.Bson()); err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success +} + +type SignQueryAllConfigResp struct { + Count int64 `json:"count"` + List []*signtaskmod.TaskConfig `json:"list"` +} + +func SignQueryAllConfig(in *signtaskmod.QueryAllConfigCond) (SignQueryAllConfigResp, stderr.Code) { + var data SignQueryAllConfigResp + count, err := signtaskmod.CountConfig(in.Query()) + if err != nil { + return data, stderr.ErrDbQueryError + } + if count == 0 { + return data, stderr.Success + } + data.Count = count + list, err := signtaskmod.QueryAllConfig(in.Query(), in.Options()) + if err != nil { + return data, stderr.ErrDbQueryError + } + data.List = list + return data, stderr.Success +} diff --git a/web/service/updownloadser/updownload.go b/web/service/updownloadser/updownload.go new file mode 100644 index 0000000..52855ac --- /dev/null +++ b/web/service/updownloadser/updownload.go @@ -0,0 +1,714 @@ +package updownloadser + +import ( + "context" + "errors" + "mime" + "net/http" + "path/filepath" + "runtime/debug" + "strconv" + "strings" + "time" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/elastic" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/timeutil" + v10 "91porn-server/common/v10" + "91porn-server/models" + "91porn-server/models/commod" + "91porn-server/models/l/pullgmod" + "91porn-server/models/v/fsuidmod" + "91porn-server/models/v/idmod" + "91porn-server/models/v/locmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var cities = []string{"上海", "北京", "深圳", "成都", "杭州", "重庆", "广州", "西安", "武汉"} + +// 从server-file 查找用户信息 +func SearchUserFromFS(id string) (respBody vidmod.PublishResport, err error) { + var params = map[string]string{ + "uid": id, + } + code, err := httputil.DefaultClientGetWithResp(&respBody, webg.Conf.URL.SearchUserUrl, nil, params) + log.Info("http method SearchUserFromFS response code ==>", log.Any("statusCode", code), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("search user info wrong ", log.E(err)) + } + return +} + +// SendVidCover2FS 传送图片 +func SendVidCover2FS(id string, ext string, fileData string) (respBody commod.Resp, err error) { + var params = map[string]string{ + "fileData": fileData, + "ext": ext, + } + code, err := httputil.DefaultClientPostJsonWithResp(&respBody, common.BindUrl(webg.Conf.URL.UploadImgUrl, id), nil, params) + log.Info("http method SendVidCover2FS response code ==>", log.Any("statusCode", code), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("file upload wrong ", log.E(err)) + } + return +} + +// SendFile2FS 传送视频文件到FS +func SendFile2FS(fileID string, fileData string, pos int64, total int64) (respBody vidmod.AwsResport, err error) { + var header = map[string]string{ + "Content-Type": "application/json", + } + var params = map[string]interface{}{ + "taskId": fileID, + "fileData": fileData, + "pos": pos, + "totalPos": total, + "type": "sp", + } + c, cancle := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancle() + code, err := httputil.DefaultClientPostJsonWithRespWithCtx(c, &respBody, webg.Conf.URL.UploadUrl, header, params) + log.Info("http method SendFile2FS response code ==>", log.Any("statusCode", code), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("file upload wrong ", log.E(err)) + return + } + return +} + +// SendImageToFS 传送图片 +func SendImageToFS(fileName, fileData string) (respBody vidmod.FsSendSingleResp, err error) { + var params = map[string]interface{}{ + "fileData": fileData, + "fileName": fileName, + } + code, err := httputil.DefaultClientPostJsonWithResp(&respBody, webg.Conf.URL.SendSingleFile, nil, params) + log.Info("http method SendImageToFS response code ==>", log.Any("statusCode", code), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("file upload wrong ", log.E(err)) + } + return +} + +type FileInfo struct { + FileName *string `json:"fileName"` // 文件名称 + FileData *string `json:"fileData"` // 文件存储在那台服务器 + Resize bool `json:"resize"` +} + +type InfoBatch struct { + Batch []*FileInfo `json:"batch"` //Info array +} + +// SendImageToFSBatch 传送图片 +func SendImageToFSBatch(batch InfoBatch) (respBody vidmod.FsSendBatchResp, err error) { + if len(batch.Batch) == 0 { + err = errors.New("no file") + return + } + c, cancle := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancle() + code, err := httputil.DefaultClientPostJsonWithRespWithCtx(c, &respBody, webg.Conf.URL.SendBatchFile, nil, batch) + log.Info("http method SendImageToFSBatch response code ==>", log.Any("statusCode", code), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("SendImageToFSBatch file upload wrong ", log.E(err)) + } + return +} + +// FsIO 获取io.ready +func FsIO(source, mds string) (data []byte, err error) { + _, data, err = httputil.DefaultClientGetBytes(common.BindUrl(getOriginUrl(mds), source), nil) + return data, err +} + +func getOriginUrl(mds string) string { + switch mds { + case constant.MediaSourcePMS: + return webg.Conf.URL.CdnUrl + case constant.MediaSourceSP: + return webg.Conf.URL.OriginUrl + case constant.MediaSourceJH1B: + return webg.Conf.URL.OriginUrl + case constant.MediaSourceLaoSiJi: + return webg.Conf.URL.LaoSiJiOriginUrl + + default: + return webg.Conf.URL.OriginUrl + } +} + +// 获取马甲账号电话号码 +func genMobileAndDevID(uid uint64) (string, string) { + //马甲账号电话号码, 2的字符串拼接uid + orgin := "12222222222" + struid := strconv.FormatUint(uid, 10) + l := 11 - len(struid) + mobile := orgin[:l] + struid + devID := usermod.SystemDevIDPrex + struid + return mobile, devID +} + +// 随机获取生日 +func getBirthDate() string { + var initBirthday = "1993-01-20" + initTime, _ := time.Parse("2006-01-02", initBirthday) + randomTime := common.RandInt(7*24*60*60, 7*365*24*60*60) + return time.Unix(initTime.UTC().Unix()+int64(randomTime), 0).Format("2006-01-02") +} + +// 游客注册 +func register(fsid string) (uid uint64, city string, err error) { + var portrait, summary, name string + uid, err = idmod.GetNextUID() + if err != nil { + return + } + mobile, devID := genMobileAndDevID(uid) + createdAt := time.Now() + if len(webg.Static.Portrait) > 0 { + portrait = webg.Static.Portrait[common.RandInt(0, len(webg.Static.Portrait))] + } + if len(webg.Static.Cities) > 0 { + city = webg.Static.Cities[common.RandInt(0, len(webg.Static.Cities))] + } else { + city = cities[common.RandInt(0, len(cities))] + } + if len(webg.Static.Names) > 0 { + name = webg.Static.Names[common.RandInt(0, len(webg.Static.Names))] + } else { + name = "游客" + strconv.FormatInt(time.Now().Unix(), 10) + } + publisher, err := SearchUserFromFS(fsid) + if err != nil { + return + } + if publisher.Code == http.StatusOK && publisher.Data.UserName != "" { + createdAt = publisher.Data.CreatedAt + portrait = publisher.Data.Portrait + name = publisher.Data.UserName + summary = publisher.Data.Summary + } + u := usermod.User{ + UID: uid, + DevID: devID, + Name: name, + Portrait: portrait, + Gender: "female", + Summary: summary, + Birthday: getBirthDate(), + Region: city, + RegisterIP: "", + WatchCount: 10, //初始化观看次数 默认为10 + PromCode: "", + DevToken: "", + VideoDeduction: 3, //默认视频扣量 30% + VipExpireDate: time.Unix(0, 0), + DevType: "xxxx", + SysType: constant.SysTypeIOS, + Mobile: mobile, + MobileBindAt: &createdAt, + Ver: "3.0.1", + } + err = usermod.InsertUser(&u) + return +} + +// getUIDByFsid 获取文件服发布者id映射的uid +func getUIDByFsid(fsid string) (uint64, string, error) { + uid, err := fsuidmod.FindFsid2UID(fsid) + if err != nil { + return uid, "", err + } + user, err := usermod.FindUserByUID(uid) + if user != nil && err == nil { + return user.UID, user.Region, nil + } + var city string + if uid == 0 { + //生成用户 从fs 获取用户信息 + uid, city, err = register(fsid) + if err != nil { + return 0, "", err + } + //插入映射关系 + _ = fsuidmod.InsertFsid2UID(fsid, uid) + } + return uid, city, nil +} + +// PullFile 同步文件 +func PullFile(pullParam vidmod.PullReq) (code stderr.Code, data map[string]interface{}) { + var pullResp []vidmod.AwsPullResp + var failIds []string + var count int + var failCount int + var lastID string + var page = 1 + awsPullReq := pullParam.AwsPullReq + awsPullReq.Status = "Completed" + rStatus, err := strconv.Atoi(pullParam.RStatus) + if err != nil { + return stderr.Failure, nil + } + coins, err := strconv.ParseInt(pullParam.Coins, 10, 64) + if err != nil { + return stderr.Failure, nil + } + defer func() { + if lastID != "" { + pullLog := pullgmod.PullLog{ + LastID: lastID, + Count: count, + FailCount: failCount, + FailIDs: failIds, + CreatedAt: time.Now(), + VidType: awsPullReq.Type, + NewUpdatedAt: awsPullReq.NewUpdateAt, + } + _ = pullgmod.InsertPullLog(pullLog) + } + if r := recover(); r != nil { + code = stderr.Failure + log.Warn("pull file info occour error", log.Any("Detail", r), log.Any("stack", string(debug.Stack()))) + } + }() + pl, err := pullgmod.FindPullRecord(awsPullReq.Type, awsPullReq.NewUpdateAt) + if err != nil { + return stderr.Failure, nil + } + if pl != nil && !pullParam.Retry { + awsPullReq.ID = pl.LastID + } + for { + awsPullReq.Page = page + param := common.StructToMap(awsPullReq) + resp, err := PullFileInfoFromFS(param) + if err != nil || len(resp.Data) == 0 || resp.Code != http.StatusOK { + break + } + pullResp = resp.Data + city := pullParam.Cityes[common.RandInt(0, len(pullParam.Cityes))] + for _, pr := range pullResp { + if len(pr.Tags) == 0 { + continue + } + tags := make([]primitive.ObjectID, 0, len(pr.Tags)) + for _, t := range pr.Tags { + tid, suc := HandleTag(t, pr.FieldNameFs+"-1.jpg") + if !suc { + continue + } + tags = append(tags, tid) + } + var userId uint64 + randomUid := pullParam.UID[common.RandInt(0, len(pullParam.UID))] + if pr.PublishID != "" { + uid, regcity, err := getUIDByFsid(pr.PublishID) + city = regcity + userId = uid + if err != nil { + userId = randomUid + user, err := usermod.FindUserByUID(userId) + if user != nil && err == nil { + city = user.Region + } + } + } else { + userId = randomUid + user, err := usermod.FindUserByUID(userId) + if user != nil && err == nil { + city = user.Region + } + } + resCode := HandleFileBase(userId, city, tags, awsPullReq.NewUpdateAt, pr, rStatus, coins) + if resCode != stderr.Success { + failIds = append(failIds, pr.ID) + failCount++ + } + lastID = pr.ID + count++ + } + page++ + } + data = make(map[string]interface{}) + data["pullCount"] = count + data["failCount"] = failCount + data["lastID"] = lastID + return stderr.Success, data +} + +func PullSeriesFile(pullParam vidmod.PullReq) (code stderr.Code, data map[string]interface{}) { + var pullResp []vidmod.AwsPullSeriesResp + var failIds []string + var count int + var failCount int + var lastID string + var page = 1 + awsPullReq := pullParam.AwsPullReq + awsPullReq.Type = "SP" + awsPullReq.Status = "Completed" + rStatus, err := strconv.Atoi(pullParam.RStatus) + if err != nil { + return stderr.Failure, nil + } + coins, err := strconv.ParseInt(pullParam.Coins, 10, 64) + if err != nil { + return stderr.Failure, nil + } + defer func() { + if lastID != "" { + pullLog := pullgmod.PullLog{ + LastID: lastID, + Count: count, + FailCount: failCount, + FailIDs: failIds, + CreatedAt: time.Now(), + VidType: pullParam.SyncType, + NewUpdatedAt: awsPullReq.NewUpdateAt, + } + _ = pullgmod.InsertPullLog(pullLog) + } + if r := recover(); r != nil { + code = stderr.Failure + log.Warn("pull file info occour error", log.Any("Detail", r), log.Any("stack", string(debug.Stack()))) + } + }() + pl, err := pullgmod.FindPullRecord(pullParam.SyncType, awsPullReq.NewUpdateAt) + if err != nil { + return stderr.Failure, nil + } + if pl != nil && !pullParam.Retry { + awsPullReq.ID = pl.LastID + } + for { + awsPullReq.Page = page + param := common.StructToMap(awsPullReq) + resp, err := PullSeriesFromFS(param) + if err != nil || len(resp.Data) == 0 || resp.Code != http.StatusOK { + break + } + pullResp = resp.Data + city := pullParam.Cityes[common.RandInt(0, len(pullParam.Cityes))] + for _, pr := range pullResp { + tags := make([]primitive.ObjectID, 0, len(pr.Tags)) + for _, t := range pr.Tags { + tid, suc := HandleTag(t, pr.CoverImg) + if !suc { + continue + } + tags = append(tags, tid) + } + var userId uint64 + randomUid := pullParam.UID[common.RandInt(0, len(pullParam.UID))] + userId = randomUid + user, err := usermod.FindUserByUID(userId) + if user != nil && err == nil { + city = user.Region + } + resCode := HandleSeriesBase(userId, city, tags, awsPullReq.NewUpdateAt, pr, rStatus, coins) + if resCode != stderr.Success { + failIds = append(failIds, pr.ID) + failCount++ + } + lastID = pr.ID + count++ + } + page++ + } + data = make(map[string]interface{}) + data["pullCount"] = count + data["failCount"] = failCount + data["lastID"] = lastID + return stderr.Success, data +} + +// HandleFileBase 处理文件基础信息 +func HandleFileBase(uid uint64, city string, tags []primitive.ObjectID, newUpdatedAt string, req vidmod.AwsPullResp, status int, coins int64) (code stderr.Code) { + defer func() { + if err := recover(); err != nil { + code = stderr.Failure + return + } + }() + if len(tags) == 0 { + return stderr.Failure + } + //过滤调重复的视频 如果重复则跳过此视频 + if vidmod.IsExist(req.CheckSum, req.Size) { + return stderr.Failure + } + l := locmod.Location{ + City: city, + } + if err := locmod.InsertLocationInfo(l); err != nil { + return stderr.ErrDbInsertError + } + lid, err := locmod.GetLocationIDByCity(city) + if err != nil { + return stderr.ErrDbQueryError + } + if vidmod.IsExistBySocID(req.ID) { + return stderr.ErrDbInputExist + } + code = insertBase(uid, tags, lid, newUpdatedAt, req, status, coins) + for _, t := range tags { + if err := tagmod.IncreaseTagVidCount(t, 1); err != nil { + log.Warn("UpdateTag tag video count err %+v\n", log.E(err)) + continue + } + } + return code +} + +// HandleSeriesBase 处理套图基础信息 +func HandleSeriesBase(uid uint64, city string, tags []primitive.ObjectID, newUpdatedAt string, req vidmod.AwsPullSeriesResp, status int, coins int64) (code stderr.Code) { + defer func() { + if err := recover(); err != nil { + code = stderr.Failure + return + } + }() + l := locmod.Location{ + City: city, + } + if err := locmod.InsertLocationInfo(l); err != nil { + return stderr.ErrDbInsertError + } + lid, err := locmod.GetLocationIDByCity(city) + if err != nil { + return stderr.ErrDbQueryError + } + if vidmod.IsExistBySocID(req.ID) { + return stderr.ErrDbInputExist + } + code = insertSeriesBase(uid, tags, lid, newUpdatedAt, req, status, coins) + for _, t := range tags { + err := tagmod.IncreaseTagVidCount(t, 1) + if err != nil { + log.Warn("UpdateTag tag video count err %+v\n", log.E(err)) + continue + } + } + return code +} + +func insertBase(uid uint64, tags []primitive.ObjectID, lid primitive.ObjectID, newUpdatedAt string, req vidmod.AwsPullResp, status int, coins int64) (code stderr.Code) { + oid, _ := primitive.ObjectIDFromHex(req.ID) + flagTime := timeutil.RandReduceTime(time.Now(), 30*24*time.Hour) + v := vidmod.VideoModel{ + ID: oid, + PublisherID: uid, + NewsType: vidmod.SP, + Title: strings.TrimSpace(req.Title), + Tags: tags, + SourceID: req.ID, + Cover: req.FieldNameFs + "-1.jpg", + CoverThumb: req.FieldNameFs + "-2.jpg", + SourceURL: req.FieldNameFs + ".m3u8", + Filename: req.Filename, + PlayTime: req.PlayTime, + SeriesCover: req.CoverImg, + Activity: []primitive.ObjectID{}, + MimeType: mime.TypeByExtension(strings.ToLower(filepath.Ext(req.Filename))), + Resolution: strconv.FormatInt(int64(req.Width), 10) + "*" + strconv.FormatInt(int64(req.Height), 10), + Width: req.Width, + Height: req.Height, + Ratio: req.Ratio, + Via: req.Via, + Rating: 0, + PlayCount: 0, + LikeCount: 0, + FakeLikeCount: common.RandInt(50, 500), + CommentCount: 0, + FakePlayCount: common.RandInt(200, 3000), + ShareCount: common.RandInt(500, 10000), + Status: status, + Location: lid, + FreeTime: vidmod.MinFreeTime, + Coins: coins, + Size: req.Size, + MD5: req.CheckSum, + Actor: strings.Join(req.Actors, ","), + NewUpdatedAt: newUpdatedAt, + CreatedAt: flagTime, + UpdatedAt: flagTime, + } + if _, err := vidmod.InsertBase(v); err != nil { + return stderr.ErrDbInsertError + } + return stderr.Success +} + +func insertSeriesBase(uid uint64, tags []primitive.ObjectID, lid primitive.ObjectID, newUpdatedAt string, req vidmod.AwsPullSeriesResp, status int, coins int64) (code stderr.Code) { + oid, _ := primitive.ObjectIDFromHex(req.ID) + flagTime := timeutil.RandAddTime(time.Now(), 3*24*time.Hour) + v := vidmod.VideoModel{ + ID: oid, + PublisherID: uid, + NewsType: vidmod.COVER, + Title: strings.TrimSpace(req.Title), + Tags: tags, + SourceID: req.ID, + Cover: req.CoverImg, + SeriesCover: req.SeriesCover, + SeriesNum: int(req.Number), + Activity: []primitive.ObjectID{}, + MimeType: req.MimeType, + Via: req.Via, + Rating: 0, + PlayCount: 0, + LikeCount: 0, + FakeLikeCount: common.RandInt(50, 500), + CommentCount: 0, + FakePlayCount: common.RandInt(200, 3000), + ShareCount: common.RandInt(300, 10000), + Status: status, + Location: lid, + FreeTime: vidmod.MinFreeTime, + Coins: coins, + NewUpdatedAt: newUpdatedAt, + CreatedAt: flagTime, + UpdatedAt: flagTime, + } + if _, err := vidmod.InsertBase(v); err != nil { + return stderr.ErrDbInsertError + } + return stderr.Success +} + +// HandleTag 处理标签 +func HandleTag(tagname, img string) (tagID primitive.ObjectID, suc bool) { + tMod := tagmod.Tag{} + tagInfo, err := tagmod.FindOneTagByName(strings.TrimSpace(tagname)) + if err == nil && !tagInfo.ID.IsZero() { + return tagInfo.ID, true + } + tMod.TagName = v10.ExtractPureChar(tagname) + tMod.CoverImg = img + tMod.SortCode = 999 + tMod.IsActive = true + tMod.CreatedAt = time.Now() + tMod.UpdatedAt = time.Now() + id, err := tagmod.InsertOne(&tMod) + if err != nil { + return primitive.NilObjectID, false + } + return id, true +} + +// PullFileInfoFromFS PullFileInfoFromFS +func PullFileInfoFromFS(params map[string]interface{}) (respBody vidmod.Resport, err error) { + code, err := httputil.DefaultClientPostJsonWithResp(&respBody, webg.Conf.URL.PullFileUrl, nil, params) + log.Info("http method PullFileInfoFromFS response code ==>", log.Any("statusCode", code), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("Pull FileInfo From Aws ", log.E(err)) + return + } + return +} + +// PullSeriesFromFS PullSeriesFromFS +func PullSeriesFromFS(params map[string]interface{}) (respBody vidmod.SeriesResport, err error) { + code, err := httputil.DefaultClientPostJsonWithResp(&respBody, webg.Conf.URL.PullSeriesUrl, nil, params) + log.Info("http method PullSeriesFromFS response code ==>", log.Any("statusCode", code), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("Pull Series From Aws ", log.E(err)) + return + } + return +} + +// PullFileInfo 获取文件转码情况及详情 +func PullFileInfo(id string) (respBody vidmod.PuFinfoResp, err error) { + code, err := httputil.DefaultClientPostJsonWithResp(&respBody, common.BindUrl(webg.Conf.URL.PullFileInfo, id), nil, nil) + log.Info("http method PullFileInfo response code ==>", log.Any("statusCode", code), log.Any("respCode", respBody.Code)) + if err != nil { + log.Error("Pull FileInfo From Aws ", log.E(err)) + respBody.Code = stderr.ErrConnectToFs + return + } + switch respBody.Data.Status { + case vidmod.Merging, vidmod.MergeCompleted: + respBody.Code = stderr.FileMerging + case vidmod.MergeError: + respBody.Code = stderr.ErrMergeFile + case vidmod.Converting: + respBody.Code = stderr.FileConverting + case vidmod.ConvertError: + respBody.Code = stderr.ErrConvertFile + case vidmod.UploadLoadingToFs: + respBody.Code = stderr.UploadLoadingToFs + case vidmod.FileUploadError: + respBody.Code = stderr.ErrUploadError + case vidmod.ConvertCompleted, vidmod.Completed: + respBody.Code = stderr.UpLoadFileComplete + } + return +} + +// 新导入的视频数据同步到ES +func SyncNewImportVideoToElastic(newUpdatedAt string, start, end time.Time) { + var page, pageSize int64 = 1, 50 + for { + nVideos, err := vidmod.GetNewImportVideo(newUpdatedAt, start, end, page, pageSize) + if err != nil || len(nVideos) == 0 { + return + } + // 获取标签名字 + var tagsID []primitive.ObjectID + for _, v := range nVideos { + tagsID = append(tagsID, v.Tags...) + } + tags, err := tagmod.FindTagsByIDS(tagsID) + if err != nil { + return + } + var tagsNameMap = make(map[primitive.ObjectID]string) + for _, v := range tags { + tagsNameMap[v.ID] = v.TagName + } + var source = elastic.M{} + for _, v := range nVideos { + var tagsName []string + for _, v1 := range v.Tags { + tagsName = append(tagsName, tagsNameMap[v1]) + } + var tmp = vidmod.ESVideo{ + ID: v.ID, + PublisherID: v.PublisherID, + Title: v.Title, + Tags: v.Tags, + TagsName: tagsName, + Filename: v.Filename, + PlayCount: v.PlayCount, + PurchaseCount: v.PurchaseCount, + LikeCount: v.LikeCount, + CommentCount: v.CommentCount, + ShareCount: v.ShareCount, + FakeLikeCount: v.FakeLikeCount, + FakeShareCount: v.FakeShareCount, + FakePlayCount: v.FakePlayCount, + Status: v.Status, + Location: v.Location, + CreatedAt: v.CreatedAt, + UpdatedAt: v.UpdatedAt, + } + source[v.ID.Hex()] = tmp + } + if err = webg.VideoES.Bulk(models.ESInfoVideoTable, source); err != nil { + continue + } + log.Info("sync video info to elastic successfully running at page", log.Any("page", page)) + page++ + } +} diff --git a/web/service/userser/drama_expire_test.go b/web/service/userser/drama_expire_test.go new file mode 100644 index 0000000..e357aa5 --- /dev/null +++ b/web/service/userser/drama_expire_test.go @@ -0,0 +1,31 @@ +package userser + +import ( + "testing" + "time" + + "91porn-server/models/v/usermod" + "91porn-server/models/v/walletmod" +) + +func TestToUserRecordListIncludesDramaExpire(t *testing.T) { + expire := time.Date(2026, 9, 26, 12, 0, 0, 0, time.UTC) + users := []usermod.User{{UID: 100001, DramaExpire: expire}} + + records := toUserRecordList( + users, + map[uint64]int64{}, + map[uint64]uint64{}, + map[uint64]int64{}, + map[uint64]walletmod.Wallet{}, + map[uint64]int{}, + map[uint64]int{}, + ) + + if len(records) != 1 { + t.Fatalf("record count = %d, want 1", len(records)) + } + if !records[0].DramaExpire.Equal(expire) { + t.Fatalf("dramaExpire = %s, want %s", records[0].DramaExpire, expire) + } +} diff --git a/web/service/userser/faq.go b/web/service/userser/faq.go new file mode 100644 index 0000000..4ec46d0 --- /dev/null +++ b/web/service/userser/faq.go @@ -0,0 +1,19 @@ +package userser + +import ( + "91porn-server/common/log" + "91porn-server/models/commod" + "91porn-server/models/v/faqmod" +) + +// FaqList 常见问题列表 +func FaqList(category string, keywords string, s commod.StdQuery) (data faqmod.FaqListRes, err error) { + result, total, err := faqmod.FaqListForSearch(category, keywords, s) + if err != nil { + log.Error("[METHOD]==>FaqList error", log.E(err)) + return + } + data.List = result + data.Total = total + return +} diff --git a/web/service/userser/login.go b/web/service/userser/login.go new file mode 100644 index 0000000..48c09aa --- /dev/null +++ b/web/service/userser/login.go @@ -0,0 +1,85 @@ +package userser + +import ( + "time" + + "91porn-server/common/pageopt" + "91porn-server/models/l/visitlogmod" + "91porn-server/models/v/usermod" + + "go.mongodb.org/mongo-driver/bson" +) + +type UserInfo struct { + ID *ObjectID `json:"id,omitempty"` + UID *uint64 `json:"uid,omitempty"` //uid + CreatedAt *time.Time `json:"createdAt,omitempty"` //用户创建时间 + HasLocked *bool `json:"hasLocked,omitempty"` //已禁止登陆 + HasBanned *bool `json:"hasBanned,omitempty"` //已禁言 +} + +type LoginInfoRecord struct { + User UserInfo `json:"user,omitempty"` + DevID *string `json:"devID,omitempty"` //设备id + DevType *string `json:"devType,omitempty"` //设备类型 + SysType *string `json:"sysType,omitempty"` //操作系统类型 安卓 IOS + BuildID *string `json:"buildID,omitempty"` //包ID + Ver *string `json:"ver,omitempty"` //APP版本号 + IP *string `json:"ip,omitempty"` //最后登陆IP + LoginTime *time.Time `json:"loginTime,omitempty"` //用户登陆时间 +} + +type LoginInfoPage struct { + Total int64 `json:"total" bson:"total"` + List []LoginInfoRecord `json:"list" bson:"list"` +} + +func LoginInfoPages(skip int64, limit int64, createdAtGTEAndLTMatch pageopt.CreatedAtGTEAndLTMatch, uidMatch pageopt.UIDMatch) (page LoginInfoPage, err error) { + //默认排序条件 + sort := bson.D{{Key: "createdAt", Value: -1}} + //整理匹配条件 + matList := []Matcher{createdAtGTEAndLTMatch.New(), uidMatch.New()} + //分页数据 + logs, err := visitlogmod.List(sort, &skip, &limit, matList...) + if err != nil { + return LoginInfoPage{}, err + } + logsLen := len(logs) + uidList := make([]uint64, logsLen) + for i, v := range logs { + uidList[i] = v.UID + } + uInfoMap, err := usermod.UserMap(uidList) + if err != nil { + return LoginInfoPage{}, err + } + recordList := make([]LoginInfoRecord, logsLen) + for i, log := range logs { + _log := log + record := LoginInfoRecord{} + record.BuildID = &_log.BuildID + record.DevID = &_log.DevID + record.DevType = &_log.DevType + record.SysType = &_log.SysType + record.BuildID = &_log.BuildID + record.Ver = &_log.Ver + record.IP = &_log.IP + record.LoginTime = &_log.CreatedAt + if user, ok := uInfoMap[_log.UID]; ok { + record.User.ID = &user.ID + record.User.UID = &user.UID + record.User.CreatedAt = &user.CreatedAt + record.User.HasLocked = &user.HasLocked + record.User.HasBanned = &user.HasBanned + } + recordList[i] = record + } + count, err := visitlogmod.Count(matList...) + if err != nil { + return LoginInfoPage{}, err + } + return LoginInfoPage{ + List: recordList, + Total: count, + }, nil +} diff --git a/web/service/userser/report.go b/web/service/userser/report.go new file mode 100644 index 0000000..00871fb --- /dev/null +++ b/web/service/userser/report.go @@ -0,0 +1,210 @@ +package userser + +import ( + "time" + + "91porn-server/common/pageopt" + "91porn-server/models/v/reptmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Matcher = pageopt.Matcher + +type UIDMatch = pageopt.UIDMatch + +// M M from Map +type M = bson.M + +// D D from Doc +type D = bson.D + +// A A from Array +type A = []interface{} + +type ObjectID = primitive.ObjectID + +type Tag struct { + ID *ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + Name *string `json:"name,omitempty" bson:"tagName,omitempty"` //标签名 +} + +type Video struct { + Title *string `json:"title,omitempty"` //标题 + PublisherID *uint64 `json:"publisherID,omitempty"` //发布者ID + HasLocked bool `json:"hasLocked"` //发布者是否被封禁 + Cover *string `json:"cover,omitempty"` //封面 + SourceURL *string `json:"sourceURL,omitempty"` //资源地址 + SourceID *string `json:"sourceID,omitempty"` //资源ID + Tags []Tag `json:"tags,omitempty"` //标签 +} + +type Comment struct { + ID *ObjectID `json:"id,omitempty"` //_id + UID *uint64 `json:"uid,omitempty"` //发表评论的 用户ID + Content *string `json:"content,omitempty"` //内容 +} + +type ReportedUser struct { + UID *uint64 `json:"uid,omitempty"` //被举报对用户ID + HasLocked *bool `json:"hasLocked,omitempty"` //已禁止登陆 + HasBanned *bool `json:"hasBanned,omitempty"` //已禁言 +} + +type ReportInfoDoc struct { + ID *ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + UID *uint64 `json:"uid,omitempty" bson:"uid,omitempty"` //举报者ID + ObjType *reptmod.ReportObjType `json:"objType,omitempty" bson:"objType,omitempty"` //举报对象类型 + ObjID *ObjectID `json:"objID,omitempty" bson:"objID,omitempty"` //举报对象ID + Types *string `json:"types,omitempty" bson:"types,omitempty"` //举报分类 + CreatedAt *time.Time `json:"createdAt,omitempty" bson:"createdAt,omitempty"` //举报时间 + Video Video `json:"video,omitempty" bson:"video,omitempty"` //视屏信息 + Comment Comment `json:"comment,omitempty" bson:"comment,omitempty"` //评论信息 + ReportedUser ReportedUser `json:"reportedUser,omitempty" bson:"reportedUser,omitempty"` //被举报的用户 + ReptStatus *bool `json:"reptStatus,omitempty" bson:"reptStatus,omitempty"` //举报状态 +} + +type ReportInfoPage struct { + Total int64 `json:"total" bson:"total"` + List []ReportInfoDoc `json:"list" bson:"list"` +} + +func ReportInfoPages(typesArray []reptmod.ReportObjType, sort D, skip int64, limit int64, uidMatch UIDMatch, typesMatch reptmod.TypesMatch, createdAtMatch reptmod.CreatedAtGTEAndLTMatch) (ReportInfoPage, error) { + inObjTypeMatch := reptmod.InObjTypeMatch{Typs: typesArray} + matList := []Matcher{ + inObjTypeMatch.New(), + uidMatch.New(), + typesMatch.New(), + createdAtMatch.New(), + } + list, err := reptmod.List(sort, skip, limit, matList...) + if err != nil { + return ReportInfoPage{}, err + } + reportList, err := toReportInfoDocList(list) + if err != nil { + return ReportInfoPage{}, err + } + count, err := reptmod.Count(matList...) + if err != nil { + return ReportInfoPage{}, err + } + page := ReportInfoPage{ + Total: count, + List: reportList, + } + return page, nil +} + +func fillUser(doc *ReportInfoDoc, userMap map[uint64]*usermod.BaseInfo) { + if doc.ObjType == nil { + return + } + if *doc.ObjType != reptmod.User { + return + } + ruid := doc.ReportedUser.UID + if ruid == nil { + return + } + user, ok := userMap[*ruid] + if ok { + doc.ReportedUser.HasLocked = &user.HasLocked + doc.ReportedUser.HasBanned = &user.HasBanned + } +} + +func fillVideo(doc *ReportInfoDoc, videoMap map[ObjectID]*vidmod.VideoModel, userMap map[uint64]*usermod.BaseInfo) { + if doc.ObjType == nil { + return + } + if *doc.ObjType != reptmod.Video { + return + } + vid := doc.ObjID + if vid == nil { + return + } + video, ok := videoMap[*vid] + if !ok { + return + } + publishID := video.PublisherID + hasLocked := false + user, ok := userMap[publishID] + if ok { + hasLocked = user.HasLocked + } + doc.Video.Title = &video.Title + doc.Video.Cover = &video.Cover + doc.Video.SourceURL = &video.SourceURL + doc.Video.SourceID = &video.SourceID + doc.Video.PublisherID = &publishID + doc.Video.HasLocked = hasLocked + tagMap, err := tagmod.TagMap(video.Tags) + if err != nil { + return + } + tagList := make([]Tag, len(tagMap)) + i := 0 + for _, tag := range tagMap { + tagList[i] = Tag{ID: &tag.ID, Name: &tag.TagName} + i++ + } + doc.Video.Tags = tagList +} + +func toReportInfoDocList(reportList []reptmod.Report) ([]ReportInfoDoc, error) { + reportListLen := len(reportList) + uidList := make([]uint64, reportListLen) + vidList := make([]ObjectID, 0, reportListLen) + for i, v := range reportList { + uidList[i] = v.UID + switch v.ObjType { + case reptmod.User: + if v.ObjUID != nil { + uidList = append(uidList, *v.ObjUID) + } + case reptmod.Video: + if v.ObjID != nil { + vidList = append(vidList, *v.ObjID) + } + } + } + videoMap, err := vidmod.VideoMap(vidList) + if err != nil { + return nil, err + } + //同时查询发布视频者的信息 + for _, v := range videoMap { + uidList = append(uidList, v.PublisherID) + } + userMap, err := usermod.GetUsersBaseInfoMap(uidList) + if err != nil { + return nil, err + } + docList := make([]ReportInfoDoc, len(reportList)) + for i, v := range reportList { + _v := v + doc := ReportInfoDoc{ + ID: &_v.ID, + ObjType: &_v.ObjType, + UID: &_v.UID, + ReportedUser: ReportedUser{ + UID: _v.ObjUID, + }, + ObjID: _v.ObjID, + Types: &_v.Types, + ReptStatus: &_v.Status, + CreatedAt: &_v.CreatedAt, + } + fillUser(&doc, userMap) + fillVideo(&doc, videoMap, userMap) + docList[i] = doc + } + return docList, nil +} diff --git a/web/service/userser/user.go b/web/service/userser/user.go new file mode 100644 index 0000000..e10c107 --- /dev/null +++ b/web/service/userser/user.go @@ -0,0 +1,1201 @@ +package userser + +import ( + "91porn-server/common/game" + "91porn-server/models/v/aiplazamod" + "91porn-server/models/v/cmtmod" + "91porn-server/models/v/mediamod" + "errors" + "fmt" + "strconv" + "sync" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/pageopt" + "91porn-server/common/stderr" + "91porn-server/common/ysqr" + "91porn-server/models/commod" + "91porn-server/models/l/playlgmod" + "91porn-server/models/v/operationlogmod" + "91porn-server/models/v/proxymod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/videodiscountmod" + "91porn-server/models/v/vidmod" + "91porn-server/models/v/walletmod" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type UserEx struct { + TotalViewTime *int64 `json:"totalViewTime,omitempty"` //总观看时长/秒 + InviterID *uint64 `json:"inviterID,omitempty"` //邀请者ID + InviteCount *int64 `json:"inviteCount,omitempty"` //邀请数 + Amount *int64 `json:"amount,omitempty"` //金币余额 + Income *int64 `json:"income,omitempty"` //作品收益 + VideoCnt *int `json:"videoCnt,omitempty"` //视频数 + UndealVideoCnt *int `json:"undealVideoCnt,omitempty"` //未审核视频数 + Integral int64 `json:"integral"` // 积分 +} + +type UserDoc struct { + ID *primitive.ObjectID `json:"id"` + UID *uint64 `json:"uid,omitempty"` //用户id + DevID *string `json:"devID,omitempty"` //设备id + HasPrivateZone *bool `json:"hasPrivateZone"` //已禁言 + RegisterIP *string `json:"registerIP,omitempty"` //注册ip + DevType *string `json:"devType,omitempty"` //设备类型 + Mobile *string `json:"mobile,omitempty"` //手机号 + Gender *string `json:"gender,omitempty"` //性别 + Name *string `json:"name,omitempty"` //昵称 + Age *int `json:"age,omitempty"` //年龄 + Region *string `json:"region,omitempty"` //地区 + PromotionCode *string `json:"promotionCode,omitempty"` //推广码 + VipLevel *int `json:"vipLevel,omitempty"` //vip等级 + VipExpireDate *time.Time `json:"vipExpireDate,omitempty"` //vip过期时间 + CreatedAt *time.Time `json:"createdAt,omitempty"` //注册时间 + UpdatedAt *time.Time `json:"updatedAt,omitempty"` //更新时间 + HasLocked *bool `json:"hasLocked"` //已禁止登陆 + HasBanned *bool `json:"hasBanned"` //已禁言 + ForbidUpload *bool `json:"forbidUpload"` //是否禁止上传帖子 + Portrait *string `json:"portrait,omitempty"` + Summary *string `json:"summary,omitempty"` + LastActive *time.Time `json:"lastActive,omitempty"` //最后活跃时间 + SuperUser *bool `json:"superUser"` //大v + ActiveValue *int `json:"activeValue"` //活跃度 + OfficialCert *bool `json:"officialCert"` //是否官方认证 + IsMadou *bool `json:"isMadou"` //是否麻豆账号 + TaxLevel *int64 `json:"taxLevel"` //税收等级 + LoginDays *int64 `json:"loginDays"` //登陆天数 + LockReason *string `json:"lockReason"` //禁止登录的原因 + LockImg *string `json:"lockImg"` //禁止登录的原因截图 + LockAt *time.Time `json:"lockAt"` //封禁开始时间 + commod.DiscDoc //商区 + BankActName *string `json:"bankActName" bson:"bankActName"` //银行卡绑定名字 + OriginalSort *int `json:"originalSort" bson:"originalSort"` // 新版原创排序 + VideoDeduction *float64 `json:"videoDeduction"` //视频扣量 + MerchantAccount *string `json:"merchantAccount" bson:"merchantAccount"` //商家账号 + AiUndressCount *int64 `json:"aiUndressCount" bson:"aiUndressCount"` + DownloadCount *int64 `json:"downloadCount" bson:"downloadCount"` + IsAdvertiser *bool `json:"isAdvertiser" bson:"isAdvertiser"` // 是否是打广告用户 + AllGoldVideoFree *bool `json:"allGoldVideoFree" bson:"allGoldVideoFree"` // 是否是全金币视频免费 + GoldVideoFreeExpire *time.Time `json:"goldVideoFreeExpire" bson:"goldVideoFreeExpire,omitempty"` // 金币视频免费日期 + BannedTime *time.Time `json:"bannedTime" bson:"bannedTime"` // 禁言时间 + AiMateBalance float64 `json:"aiMateBalance" bson:"aiMateBalance"` //ai伴侣余额 + AiMateUid string `json:"aiMateUid" bson:"aiMateUid"` // ai伴侣uid,对应第三方服务 + ChatPrice int64 `json:"chatPrice" bson:"chatPrice,omitempty"` // 私信价格 + BroadcastExpire time.Time `json:"broadcastExpire" bson:"broadcastExpire,omitempty"` //直播有效期 + DramaExpire time.Time `json:"dramaExpire" bson:"dramaExpire,omitempty"` //短剧权益有效期 + GoldVideoFreeLimit int64 `json:"goldVideoFreeLimit" bson:"goldVideoFreeLimit"` +} + +type UserRecord struct { + UserDoc + UserEx +} + +type UserPage struct { + Total int64 `json:"total"` + List []UserRecord `json:"list"` +} + +func UserPages(sort D, skip, limit int64, + uidMatch pageopt.UIDMatch, + devIdMatch usermod.DevIDRegexMatch, + vipLevelMatch usermod.VipLevelMatch, + nameMatch usermod.NameRegexMatch, + mobileMatch usermod.MobileMatch, + promotionCodeMatch usermod.PromotionCodeMatch, + hasPromotionCodeMatch usermod.HasPromotionCodeMatch, + lockedMatch usermod.LockedMatch, + bannedMatch usermod.BannedMatch, + forbidUploadMatch usermod.ForbidUploadMatch, + createdAtGTEAndLTMatch usermod.CreatedAtGTEAndLTMatch, + districtCodeMatch usermod.DistrictCodeMatch, + autoFollowMatch usermod.AutoFollowMatch, + registerIPMatch usermod.RegisterIpMatch, +) (page UserPage, err error) { + matchList := []Matcher{ + uidMatch.New(), + devIdMatch.New(), + vipLevelMatch.New(), + nameMatch.New(), + mobileMatch.New(), + promotionCodeMatch.New(), + hasPromotionCodeMatch.New(), + lockedMatch.New(), + bannedMatch.New(), + forbidUploadMatch.New(), + createdAtGTEAndLTMatch.New(), + districtCodeMatch.New(), + autoFollowMatch.New(), + registerIPMatch.New(), + } + //获取user列表 + userList, err := usermod.List(sort, &skip, &limit, matchList...) + count, err := usermod.Count(matchList...) + if err != nil { + return UserPage{}, err + } + var ( + uidList = make([]uint64, len(userList)) + userMap = make(map[uint64]usermod.User, len(userList)) + ) + for i, user := range userList { + uidList[i] = user.UID + userMap[user.UID] = user + if user.TaxLevel == 0 { + userList[i].TaxLevel = constant.DefaultBloggerVideoIncomeTaxLevel // 用户未设置分成,则展示系统默认值 + } + } + var ( + wg = sync.WaitGroup{} + playSecMap = make(map[uint64]int64) + invrLv1Map = make(map[uint64]uint64) + invrCountMap = make(map[uint64]int64) + walletMap = make(map[uint64]walletmod.Wallet) + videoCntMap = make(map[uint64]int) + unDealVideoCntMap = make(map[uint64]int) + totalCount int64 //文档总数 + ) + wg.Add(6) + common.Go(func() { + defer wg.Done() + totalCount = count + }) + common.Go(func() { + defer wg.Done() + m, err := playlgmod.GetUIDPlaySecondMap(uidList) + if err != nil { + log.Error("GetUIDPlaySecondMap err", log.E(err)) + return + } + playSecMap = m + }) + common.Go(func() { + defer wg.Done() + m, err := proxymod.GetInvrLv1Map(uidList) + if err != nil { + log.Error("GetInvrLv1Map err", log.E(err)) + return + } + invrLv1Map = m + }) + common.Go(func() { + defer wg.Done() + m, err := proxymod.GetInviteesCountMap(uidList) + if err != nil { + log.Error("GetInviteesCountMap err", log.E(err)) + return + } + invrCountMap = m + }) + common.Go(func() { + defer wg.Done() + m, err := walletmod.GetWalletMap(uidList) + if err != nil { + log.Error("GetWalletMap err", log.E(err)) + return + } + walletMap = m + }) + common.Go(func() { + defer wg.Done() + videoCntMap, unDealVideoCntMap, err = vidmod.GetVideosByUIDs(uidList) + if err != nil { + log.Error("GetSumDateCountMap err", log.E(err)) + return + } + }) + wg.Wait() + list := toUserRecordList( + userList, + playSecMap, + invrLv1Map, + invrCountMap, + walletMap, + videoCntMap, + unDealVideoCntMap, + ) + return UserPage{ + List: list, + Total: totalCount, + }, nil +} + +func toUserRecordList( + userList []usermod.User, + playSecMap map[uint64]int64, + invrLv1Map map[uint64]uint64, + invrCountMap map[uint64]int64, + walletMap map[uint64]walletmod.Wallet, + videoCntMap map[uint64]int, + unDealVideoCntMap map[uint64]int, +) []UserRecord { + recordList := make([]UserRecord, len(userList)) + for i, user := range userList { + _user := user + //UserDoc + age := _user.Age() + aiUndressFreeTimes := walletMap[_user.UID].AiUndressFreeTimes + downloadCount := walletMap[_user.UID].DownloadCount + aiMateBalance := walletMap[_user.UID].AiMateBalance + userDoc := UserDoc{ + ID: &_user.ID, + UID: &_user.UID, + DevID: &_user.DevID, + HasPrivateZone: &_user.HasPrivateZone, + RegisterIP: &_user.RegisterIP, + DevType: &_user.DevType, + Mobile: &_user.Mobile, + Gender: &_user.Gender, + Name: &_user.Name, + Age: &age, + Region: &_user.Region, + PromotionCode: &_user.PromCode, + VipLevel: &_user.VipLevel, + VipExpireDate: &_user.VipExpireDate, + CreatedAt: &_user.CreatedAt, + UpdatedAt: &_user.UpdatedAt, + HasLocked: &_user.HasLocked, + HasBanned: &_user.HasBanned, + ForbidUpload: &_user.ForbidUpload, + Portrait: &_user.Portrait, + Summary: &_user.Summary, + LastActive: &_user.LastVisitAt, + SuperUser: &_user.SuperUser, + ActiveValue: &_user.ActiveValue, + OfficialCert: &_user.OfficialCert, + TaxLevel: &_user.TaxLevel, + LoginDays: &_user.LoginDays, + DiscDoc: _user.DiscDoc, + LockReason: &_user.LockReason, + LockImg: _user.LockImg, + BankActName: &_user.BankActName, + OriginalSort: &_user.OriginalSort, + VideoDeduction: &_user.VideoDeduction, + MerchantAccount: &_user.MerchantAccount, + AiUndressCount: &aiUndressFreeTimes, + DownloadCount: &downloadCount, + IsAdvertiser: &_user.IsAdvertiser, + BannedTime: &_user.BannedTime, + AllGoldVideoFree: &_user.AllGoldVideoFree, + AiMateBalance: aiMateBalance, + AiMateUid: _user.AiMateUid, + ChatPrice: _user.ChatPrice, + BroadcastExpire: user.BroadcastExpire, + DramaExpire: user.DramaExpire, + } + if !_user.LockAt.IsZero() { + userDoc.LockAt = &_user.LockAt + } + if _user.GoldVideoFreeExpire.After(time.Now()) { + userDoc.GoldVideoFreeExpire = &_user.GoldVideoFreeExpire + userDoc.GoldVideoFreeLimit = _user.GoldVideoFreeLimit + } + if _user.BannedTime.After(time.Now()) { + status := true + userDoc.HasBanned = &status + } + //UserEx + uid := _user.UID + totalViewTime := playSecMap[uid] + inviterId := invrLv1Map[uid] + inviteCount := invrCountMap[uid] + amount := walletMap[uid].Amount + integral := walletMap[uid].Integral + income := walletMap[uid].Income + total := videoCntMap[uid] + unDeal := unDealVideoCntMap[uid] + userEx := UserEx{ + TotalViewTime: &totalViewTime, + InviterID: &inviterId, + InviteCount: &inviteCount, + Amount: &amount, + Income: &income, + VideoCnt: &total, + UndealVideoCnt: &unDeal, + Integral: integral, + } + //Record + recordList[i] = UserRecord{ + UserDoc: userDoc, + UserEx: userEx, + } + } + return recordList +} + +func ModifyUserMobile(uid uint64, mobile string) error { + user, err := usermod.FindUserByUID(uid) + if err != nil { + log.Error(fmt.Sprintf("用户ID:%d;手机号:%s;查询用户信息异常:%v", uid, mobile, err)) + return err + } + // 怀疑是这里引起的 invalid memory address or nil pointer dereference + if user == nil { + log.Warn(fmt.Sprintf("用户ID:%d;手机号:%s;用户不存在", uid, mobile)) + return errors.New(stderr.UserIsNotExists.Msg()) + } + if user.Mobile == mobile { + return nil + } + if user.MobileBindAt == nil && mobile != "" { //首次绑定 + ok, err := usermod.BindMobile(nil, uid, mobile, "") + if err != nil { + return err + } + if !ok { + return errors.New("mobile bind faild") + } + return nil + } + if user.Mobile != "" && mobile == "" { //解绑手机 + ok, err := usermod.UnBindMobile(uid) + if err != nil { + return err + } + if !ok { + return errors.New("mobile unbind faild") + } + return nil + } + ok, err := usermod.ChangeMobile(uid, mobile) //更换手机号 + if err != nil { + return err + } + if !ok { + return errors.New("mobile change faild") + } + return nil +} + +// ModifyUserAmount 修改账号金币 +func ModifyUserAmount(uid uint64, amt int64, manager, reason string) (operationlogmod.OperationLog, error) { + var opLog operationlogmod.OperationLog + if err := webg.VideoDB.Trans(func(t *db.MongoTool) error { + amount, err := walletmod.GetAmount(t, uid) + if err != nil { + return err + } + opLog = operationlogmod.OperationLog{ + ID: primitive.NewObjectID(), + OperationType: constant.User_INCoin, + BeforeContent: strconv.FormatInt(amount, 10), + AfterContent: strconv.FormatInt(amount+amt, 10), + Reason: reason, + UserID: int64(uid), + CreatedUser: manager, + } + amount += amt + if amount < 0 { + amount = 0 + } + return walletmod.SetAmount(t, amount, uid) + }); err != nil { + log.Error(fmt.Sprintf("modifyUserAmount Trans fail error:%+v:", err)) + return opLog, err + } + return opLog, nil +} + +func CreditAmount(uid uint64, amount int64, manager, reason string) error { + var tranType string + var tranTypeInt int64 + var desc string + // var f func(t *db.MongoTool, amt int64, uid uint64) error + if amount > 0 { //上分 + tranType = txnmod.AdminCreaditAmount.Key() + tranTypeInt = int64(txnmod.AdminCreaditAmount) + desc = txnmod.AdminCreaditAmount.Key() + strconv.FormatInt(amount, 10) + } else if amount < 0 { //减分 + tranType = txnmod.AdminDebitAmount.Key() + tranTypeInt = int64(txnmod.AdminDebitAmount) + desc = txnmod.AdminDebitAmount.Key() + strconv.FormatInt(amount, 10) + } else { //不操作 + return nil + } + // f = walletmod.CreditAmount + return webg.VideoDB.Trans(func(t *db.MongoTool) error { + amountOld, err := walletmod.GetAmount(t, uid) + if err != nil { + return err + } + if err = operationlogmod.Insert(operationlogmod.OperationLog{ + ID: primitive.NewObjectID(), + OperationType: constant.User_INCoin, + BeforeContent: strconv.FormatInt(amountOld, 10), + AfterContent: strconv.FormatInt(amountOld+amount, 10), + Reason: reason, + UserID: int64(uid), + CreatedUser: manager, + }); err != nil { + return err + } + wallet, err := walletmod.CreditAmount(t, amount, uid) + if err != nil { + return err + } + return txnmod.InsertTransactionLog(t, &(txnmod.TransactionLog{ + UID: uid, + Amount: amount, + ActualAmount: float64(amount), + Tax: 0, + TaxAmount: 0, + TranType: tranType, + TranTypeInt: tranTypeInt, + Desc: desc, + RealAmount: walletmod.GetRealAmount(wallet), + })) + }) +} + +// FruitCoinChange 果币变更 +func FruitCoinChange(uid uint64, fruitCoin int64, manager, reason string) error { + var ( + tranType string + tranTypeInt int64 + desc string + ) + if fruitCoin > 0 { //上分 + tranType = txnmod.AdminCreditFruitCoin.Key() + tranTypeInt = int64(txnmod.AdminCreditFruitCoin) + desc = txnmod.AdminCreditFruitCoin.Key() + strconv.FormatInt(fruitCoin, 10) + } else if fruitCoin < 0 { //减分 + tranType = txnmod.AdminDebitFruitCoin.Key() + tranTypeInt = int64(txnmod.AdminDebitFruitCoin) + desc = txnmod.AdminDebitFruitCoin.Key() + strconv.FormatInt(fruitCoin, 10) + } else { //不操作 + return nil + } + return webg.VideoDB.Trans(func(t *db.MongoTool) error { + fruitAmountOld, err := walletmod.GetFruitAmount(t, uid) + if err != nil { + return err + } + if err = operationlogmod.Insert(operationlogmod.OperationLog{ + ID: primitive.NewObjectID(), + OperationType: constant.User_INFruitCoin, + BeforeContent: strconv.FormatInt(fruitAmountOld, 10), + AfterContent: strconv.FormatInt(fruitAmountOld+fruitCoin, 10), + Reason: reason, + UserID: int64(uid), + CreatedUser: manager, + }); err != nil { + return err + } + wallet, err := walletmod.CreditFruitCoin(t, uid, fruitCoin) + if err != nil { + return err + } + return txnmod.InsertTransactionLog(t, &(txnmod.TransactionLog{ + UID: uid, + FruitCoin: fruitCoin, + TranType: tranType, + TranTypeInt: tranTypeInt, + Desc: desc, + FruitCoinBalance: wallet.FruitCoin, + })) + }) +} + +func CheckVipRenew(user *usermod.User, vipDay int, vipLevel int) (time.Time, int) { + now := time.Now() + var end time.Time + level := vipLevel + d := time.Hour * 24 * time.Duration(vipDay) + if user.VipExpireDate.After(now) { //renew + end = user.VipExpireDate.Add(d) + if user.VipLevel > level { //当前用户的vip等级比这次购买的大,使用用户的 + level = user.VipLevel + } + } else { + end = now.Add(d) + } + return end, level +} + +// Info () 我的信息 +func Info(uid uint64) (userInfo usermod.UserVipInfoRes, err error) { + var u *usermod.User + var videoDiscountLog videodiscountmod.VideoDiscountLog //视频折扣卡 + u, err = usermod.FindUserByUID(uid) + if err != nil || u == nil { + return + } + //金币视频免费 + videoDiscountLog, err = videodiscountmod.GetByUIDAndWeb(uid) + //信息组装 + if err == nil && !videoDiscountLog.ID.IsZero() { + userInfo.VideoDiscountExpiration = videoDiscountLog.Expiration + userInfo.VideoDiscount = videoDiscountLog.VideoDiscount + } + userInfo.VipExpireDate = u.VipExpireDate + userInfo.GoldVideoFreeExpire = u.GoldVideoFreeExpire + userInfo.PayVidDiscount = u.PayVidDiscount + userInfo.VideoFreeExpiration = u.VideoFreeExpiration + userInfo.GoldVideoCoupon = u.GoldVideoCoupon + return +} + +func CreditGold(uid uint64, amount, income int64, manager, reason string) error { + if amount > 0 && income > 0 { + return errors.New("金额不能为正") + } + if amount == 0 && income == 0 { + return errors.New("金额不能为0") + } + if reason == "" { + return errors.New("描述不能为空") + } + wallet, err := walletmod.GetWallet(uid) + if err != nil { + return err + } + if amount < 0 && wallet.Amount <= 0 { + return errors.New("用户余额不足, 回收失败") + } + if income < 0 && wallet.Income <= 0 { + return errors.New("用户余额不足, 回收失败") + } + return webg.VideoDB.Trans(func(t *db.MongoTool) error { + p := &walletmod.DebitPlan{} + if amount < 0 && wallet.Amount > 0 { + //walletmod.Debit调用的是扣除方法, 所以这里金额要为正才是减少 + p.Amount = -amount + //余额不足时, 扣为0 + if wallet.Amount < p.Amount { + p.Amount = wallet.Amount + } + if err = operationlogmod.Insert(operationlogmod.OperationLog{ + ID: primitive.NewObjectID(), + OperationType: constant.User_ReduceAmount, + BeforeContent: strconv.FormatInt(wallet.Amount, 10), + AfterContent: strconv.FormatInt(wallet.Amount-p.Amount, 10), + Reason: reason, + UserID: int64(uid), + CreatedUser: manager, + }); err != nil { + return err + } + } + if income < 0 && wallet.Income > 0 { + //walletmod.Debit调用的是扣除方法, 所以这里金额要为正才是减少 + p.Income = -income + //余额不足时, 扣为0 + if wallet.Income < p.Income { + p.Income = wallet.Income + } + if err = operationlogmod.Insert(operationlogmod.OperationLog{ + ID: primitive.NewObjectID(), + OperationType: constant.User_ReduceIncome, + BeforeContent: strconv.FormatInt(wallet.Income, 10), + AfterContent: strconv.FormatInt(wallet.Income-p.Income, 10), + Reason: reason, + UserID: int64(uid), + CreatedUser: manager, + }); err != nil { + return err + } + } + if p.Amount > 0 || p.Income > 0 { + //walletmod.Debit调用的是扣除方法, 所以这里金额要为正才是减少 + if _, err = walletmod.Debit(t, p, uid); err != nil { + return err + } + tl := txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + Amount: -p.Amount, + ActualAmount: -float64(p.Amount), + TranType: txnmod.AdminDebitAmount.Key(), + TranTypeInt: int64(txnmod.AdminDebitAmount), + Desc: txnmod.AdminDebitAmount.Key() + "-" + reason, + RealAmount: walletmod.GetRealAmount(wallet), + } + if err = txnmod.InsertTransactionLog(t, &tl); err != nil { + log.Error(" InsertTransactionLog error", log.E(err)) + return err + } + } + return nil + }) +} + +// SetAdvertiser 批量修改用户是否为打广告用户 +func SetAdvertiser(in *usermod.SetAdvertiseCond) error { + doc := usermod.UserSelector{IsAdvertiser: in.Enable} + if _, err := usermod.UpdateMany(in.UId, doc); err != nil { + return err + } + + filter := bson.M{"userID": bson.M{"$in": in.UId}} + cond := bson.M{"$set": bson.M{"isAdvertiser": in.Enable}} + data, err := cmtmod.GetCmtListByCond(filter) + if err != nil { + return err + } + + if len(data) > 0 { + err := cmtmod.UpdateMany(filter, cond) + if err != nil { + return err + } + } + return nil +} + +// GenerateQRCode 生成用户二维码 +func GenerateQRCode(uid uint64) (string, error) { + user, err := usermod.FindUserByUIDForNoCache(uid) + if err != nil { + return "", err + } + content := ysqr.Content{ + UID: uid, + T: ysqr.Login, + LoginClaims: ysqr.LoginClaims{ + DevID: user.DevID, + }, + } + return content.String(webg.Conf.Base.JwtKey), nil +} + +func UpdateLotteryTimes(param *usermod.UpdateLotteryTimesCond, manager string) error { + user, err := usermod.FindUserByUID(param.UID) + if err != nil { + return errors.New("query user by id is err") + } + + if user.ID.IsZero() { + return errors.New("user is null") + } + + if *param.LotteryTimes == 0 { + return errors.New("抽奖次数不能为0") + } + if param.Mark == nil { + return errors.New("描述不能为空") + } + + wallet, err := walletmod.GetWallet(param.UID) + if err != nil { + return err + } + if (*param.LotteryTimes < 0 && wallet.LotteryTimes <= 0) || (*param.LotteryTimes < 0 && wallet.LotteryTimes < -*param.LotteryTimes) { + return errors.New("用户下抽奖次数不足, 回收失败") + } + + tranErr := webg.VideoDB.Trans(func(t *db.MongoTool) error { + p := &walletmod.CreditPlan{} + var tl txnmod.TransactionLog + if *param.LotteryTimes < 0 { + d := -*param.LotteryTimes + p.LotteryTimes = &d + tl = txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: param.UID, + Amount: -*param.LotteryTimes, + ActualAmount: float64(*param.LotteryTimes), + TranType: txnmod.AdminDebitLotteryTimesCount.Key(), + TranTypeInt: int64(txnmod.AdminDebitLotteryTimesCount), + Desc: fmt.Sprintf("官方减去[%v次]-%s", -*param.LotteryTimes, *param.Mark), + RealAmount: walletmod.GetRealAmount(wallet), + } + } + + if *param.LotteryTimes > 0 { + d := -*param.LotteryTimes + p.LotteryTimes = &d + tl = txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: param.UID, + Amount: *param.LotteryTimes, + ActualAmount: float64(*param.LotteryTimes), + TranType: txnmod.AdminAddLotteryTimesCount.Key(), + TranTypeInt: int64(txnmod.AdminAddLotteryTimesCount), + Desc: fmt.Sprintf("官方增加[%v次]-%s", *param.LotteryTimes, *param.Mark), + RealAmount: walletmod.GetRealAmount(wallet), + } + } + w, err := walletmod.DebitLotteryTimes(t, *p.LotteryTimes, param.UID) + if err != nil { + log.Error(fmt.Sprintf("UpdateFreeTimes walletmod.DebitDownloadCounts error%v,manager%s", err, manager)) + return err + } + + if w == nil { + return errors.New("UpdateFreeTimes walletmod.DebitDownloadCounts is null") + } + err = txnmod.InsertTransactionLog(t, &tl) + if err != nil { + log.Error(fmt.Sprintf("UpdateDownloadCounts xnmod.InsertTransactionLog error%v,manager%s", err, manager)) + return err + } + return nil + }) + + if tranErr != nil { + log.Error(fmt.Sprintf("UpdateDownloadCounts Trans error%v,manager%s", tranErr, manager)) + return tranErr + } + return nil +} + +func UpdateFreeTimes(param *usermod.UpdateFreeTimesCond, manager string) error { + user, err := usermod.FindUserByUID(param.UID) + if err != nil { + return errors.New("query user by id is err") + } + + if user.ID.IsZero() { + return errors.New("user is null") + } + + if *param.FreeTimes == 0 { + return errors.New("免费次数不能为0") + } + if param.Mark == nil { + return errors.New("描述不能为空") + } + + wallet, err := walletmod.GetWallet(param.UID) + if err != nil { + return err + } + if (*param.FreeTimes < 0 && wallet.AiUndressFreeTimes <= 0) || (*param.FreeTimes < 0 && wallet.AiUndressFreeTimes < -*param.FreeTimes) { + return errors.New("用户免费次数不足, 回收失败") + } + + tranErr := webg.VideoDB.Trans(func(t *db.MongoTool) error { + p := &walletmod.DebitPlan{} + var tl txnmod.TransactionLog + if *param.FreeTimes < 0 { + p.AiUndressFreeTimes = -*param.FreeTimes + tl = txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: param.UID, + Amount: -*param.FreeTimes, + ActualAmount: float64(*param.FreeTimes), + TranType: txnmod.AdminDebitAiUndressFreeTimes.Key(), + TranTypeInt: int64(txnmod.AdminDebitAiUndressFreeTimes), + Desc: fmt.Sprintf("官方减去AI脱衣免费次数[%v次]-%s", -*param.FreeTimes, *param.Mark), + RealAmount: walletmod.GetRealAmount(wallet), + } + } + + if *param.FreeTimes > 0 { + p.AiUndressFreeTimes = -*param.FreeTimes + tl = txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: param.UID, + Amount: *param.FreeTimes, + ActualAmount: float64(*param.FreeTimes), + TranType: txnmod.AdminAddAiUndressFreeTimes.Key(), + TranTypeInt: int64(txnmod.AdminAddAiUndressFreeTimes), + Desc: fmt.Sprintf("官方增加AI脱衣免费次数[%v次]-%s", *param.FreeTimes, *param.Mark), + RealAmount: walletmod.GetRealAmount(wallet), + } + } + w, err := walletmod.DebitAiFreeTimes(t, p.AiUndressFreeTimes, param.UID) + if err != nil { + log.Error(fmt.Sprintf("UpdateFreeTimes walletmod.DebitAiFreeTimes error%v,manager%s", err, manager)) + return err + } + + if w == nil { + return errors.New("UpdateFreeTimes walletmod.DebitAiFreeTimes is null") + } + err = txnmod.InsertTransactionLog(t, &tl) + if err != nil { + log.Error(fmt.Sprintf("UpdateFreeTimes xnmod.InsertTransactionLog error%v,manager%s", err, manager)) + return err + } + return nil + }) + + if tranErr != nil { + log.Error(fmt.Sprintf("UpdateFreeTimes Trans error%v,manager%s", tranErr, manager)) + return tranErr + } + return nil +} + +func UpdateDownloadCounts(param *usermod.UpdateDownloadCond, manager string) error { + user, err := usermod.FindUserByUID(param.UID) + if err != nil { + return errors.New("query user by id is err") + } + + if user.ID.IsZero() { + return errors.New("user is null") + } + + if *param.Download == 0 { + return errors.New("下载次数不能为0") + } + if param.Mark == nil { + return errors.New("描述不能为空") + } + + wallet, err := walletmod.GetWallet(param.UID) + if err != nil { + return err + } + if (*param.Download < 0 && wallet.DownloadCount <= 0) || (*param.Download < 0 && wallet.DownloadCount < -*param.Download) { + return errors.New("用户下载次数不足, 回收失败") + } + + tranErr := webg.VideoDB.Trans(func(t *db.MongoTool) error { + p := &walletmod.DebitPlan{} + var tl txnmod.TransactionLog + if *param.Download < 0 { + p.DownloadCount = -*param.Download + tl = txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: param.UID, + Amount: -*param.Download, + ActualAmount: float64(*param.Download), + TranType: txnmod.AdminDebitDownloadCount.Key(), + TranTypeInt: int64(txnmod.AdminDebitDownloadCount), + Desc: fmt.Sprintf("官方减去[%v次]-%s", -*param.Download, *param.Mark), + RealAmount: walletmod.GetRealAmount(wallet), + } + } + + if *param.Download > 0 { + p.DownloadCount = -*param.Download + tl = txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: param.UID, + Amount: *param.Download, + ActualAmount: float64(*param.Download), + TranType: txnmod.AdminAddDownloadCount.Key(), + TranTypeInt: int64(txnmod.AdminAddDownloadCount), + Desc: fmt.Sprintf("官方增加[%v次]-%s", *param.Download, *param.Mark), + RealAmount: walletmod.GetRealAmount(wallet), + } + } + w, err := walletmod.DebitDownloadCounts(t, p.DownloadCount, param.UID) + if err != nil { + log.Error(fmt.Sprintf("UpdateFreeTimes walletmod.DebitDownloadCounts error%v,manager%s", err, manager)) + return err + } + + if w == nil { + return errors.New("UpdateFreeTimes walletmod.DebitDownloadCounts is null") + } + err = txnmod.InsertTransactionLog(t, &tl) + if err != nil { + log.Error(fmt.Sprintf("UpdateDownloadCounts xnmod.InsertTransactionLog error%v,manager%s", err, manager)) + return err + } + return nil + }) + + if tranErr != nil { + log.Error(fmt.Sprintf("UpdateDownloadCounts Trans error%v,manager%s", tranErr, manager)) + return tranErr + } + return nil +} + +func DeleteComment(param *usermod.DeleteCommentCond, manager string) error { + user, err := usermod.FindUserByUID(param.UID) + if err != nil { + return errors.New("query user by id is err") + } + + if user.ID.IsZero() { + return errors.New("user is null") + } + + err = cmtmod.DelCommentsByUID(param.UID) + if err != nil { + return err + } + return nil +} + +func QueryGameCode(param *usermod.QueryGameCodeCond) (*game.QueryData, error) { + user, err := usermod.FindUserByUID(param.UID) + if err != nil { + return nil, errors.New("query user by id is err") + } + + if user.ID.IsZero() { + return nil, errors.New("user is null") + } + + gameCode, err := game.QueryTripartiteGameCode(user.UID) + if err != nil { + return nil, err + } + + return gameCode, nil +} + +// 重置uid的ai余额为aiMateBalance +func SetAiMateBalance(uid uint64, aiMateBalance float64, reason string) error { + return webg.VideoDB.Trans(func(t *db.MongoTool) error { + if err := walletmod.SetAiMateBalance(t, uid, aiMateBalance); err != nil { + return err + } + return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{ + TransNo: primitive.NewObjectID(), + UID: uid, + TranType: txnmod.AdminAiMateSet.Key(), + TranTypeInt: int64(txnmod.AdminAiMateSet), + Desc: txnmod.AdminAiMateSet.Key() + "-ai伴侣余额设置为" + strconv.FormatFloat(aiMateBalance, 'f', -1, 64) + "-" + reason, + AiMatePoint: aiMateBalance, + RealAiMatePoint: aiMateBalance, + }) + }) +} + +type UserCommentBanned struct { + UIDList []uint64 `form:"uidList" json:"uidList" binding:"required"` + Enable *bool `form:"enable" json:"enable" binding:"required"` + Reason string `form:"reason" json:"reason"` +} + +func (req UserCommentBanned) Banned() stderr.Code { + if len(req.UIDList) == 0 || req.Enable == nil { + return stderr.Success + } + now := time.Now() + doc := usermod.UserSelector{HasBanned: req.Enable} + if *req.Enable { + bannedTime := now.Add(time.Hour) + doc.BannedTime = &bannedTime + } else { + doc.BannedTime = &now // 服务端那边使用BannedTime去验证是否解禁,所以这里直接设置为当前时间,表示立即解禁 + } + if _, err := usermod.UpdateMany(req.UIDList, doc); err != nil { + return stderr.ErrServerUnavailable + } + + if *req.Enable { + common.Go(func() { + var cids []primitive.ObjectID + var pids []primitive.ObjectID + filter := bson.M{"userID": bson.M{"$in": req.UIDList}} + comments, err := cmtmod.FindList(filter, options.Find().SetLimit(1000).SetSkip(0)) + if err != nil { + return + } + if len(comments) == 0 { + return + } + mData := map[cmtmod.OType][]primitive.ObjectID{} + // 玩家评论过的帖子 重新统计条数 + for _, comment := range comments { + if comment == nil { + continue + } + if _, ok := mData[comment.ObjType]; !ok { + mData[comment.ObjType] = []primitive.ObjectID{comment.ObjID} + } else { + mData[comment.ObjType] = append(mData[comment.ObjType], comment.ObjID) + } + if !comment.CID.IsZero() { + pids = append(pids, comment.CID) + } + cids = append(cids, comment.ID) + } + // 删除评论 + _ = cmtmod.DelCommentsByIDS(cids) + + for oType, oids := range mData { + for _, oid := range oids { + n, err := cmtmod.GetCount(bson.M{"objType": oType, "objID": oid}) + if err != nil { + continue + } + switch oType { + case cmtmod.OTypeVideo: + _ = vidmod.UpdateOne(oid, bson.M{"$set": bson.M{"commentCount": n}}) + case cmtmod.OTypeCartoon: + _, _ = mediamod.UpdateByID(nil, oid, bson.M{"countComment": n}) + case cmtmod.OTypeAiPlaza: + _, _ = aiplazamod.UpdateByID(nil, oid, bson.M{"commentCount": n}) + } + } + } + if len(pids) > 0 { + cmtmod.CmtDecreaseCommentCountByID(pids...) + } + }) + } + + return stderr.Success +} + +type LockUserReq struct { + UID uint64 `form:"uid" json:"uid" binding:"required"` + LockImg *string `form:"lockImg" json:"lockImg"` //禁止登录的原因截图 + Enable *bool `form:"enable" json:"enable" binding:"required"` + Reason *string `form:"reason" json:"reason"` + LockAt *time.Time `form:"lockAt" json:"lockAt"` //封禁开始时间 + DismissVip *bool `form:"dismissVip" json:"dismissVip"` //是否解除vip +} + +var ( + lockedSummary = "" + lockedPortrait = "" + lockBackground = []string{} + lockVipLevel = 0 + lockSuperUser = false + lockOfficialCert = false + lockBirthday = "" + lockRegion = "" +) + +func (req LockUserReq) Lock() stderr.Code { + + now := time.Now() + lockAt := now + if req.LockAt != nil { + lockAt = *req.LockAt + } + doc := usermod.UserSelector{ + HasLocked: req.Enable, + LockReason: req.Reason, + LockImg: req.LockImg, + LockAt: &lockAt, + //Portrait: &lockedPortrait, + //Summary: &lockedSummary, + //Background: &lockBackground, + //Birthday: &lockBirthday, + //SuperUser: &lockSuperUser, + //OfficialCert: &lockOfficialCert, + //Region: &lockRegion, + } + if req.Enable != nil && *req.Enable { + if req.DismissVip != nil && *req.DismissVip { + doc.VipLevel = &lockVipLevel + doc.VipExpireDate = &now + } + if req.LockAt != nil && req.LockAt.IsZero() { + zero := time.Time{} + doc.LockAt = &zero + } + } else { + zero := time.Time{} + doc.LockAt = &zero + } + if _, err := usermod.Update(req.UID, doc); err != nil { + return stderr.ErrDbUpdateError + } + + if req.Enable != nil && *req.Enable { + common.Go(func() { + var cids []primitive.ObjectID + var pids []primitive.ObjectID + filter := bson.M{"userID": bson.M{"$in": req.UID}} + comments, err := cmtmod.FindList(filter, options.Find().SetLimit(1000).SetSkip(0)) + if err != nil { + return + } + + mData := map[cmtmod.OType][]primitive.ObjectID{} + // 玩家评论过的帖子 重新统计条数 + for _, comment := range comments { + if comment == nil { + continue + } + if _, ok := mData[comment.ObjType]; !ok { + mData[comment.ObjType] = []primitive.ObjectID{comment.ObjID} + } else { + mData[comment.ObjType] = append(mData[comment.ObjType], comment.ObjID) + } + if !comment.CID.IsZero() { + pids = append(pids, comment.CID) + } + cids = append(cids, comment.ID) + } + // 删除评论 + _ = cmtmod.DelCommentsByIDS(cids) + + for oType, oids := range mData { + for _, oid := range oids { + n, err := cmtmod.GetCount(bson.M{"objType": oType, "objID": oid}) + if err != nil { + continue + } + switch oType { + case cmtmod.OTypeVideo: + _ = vidmod.UpdateOne(oid, bson.M{"$set": bson.M{"commentCount": n}}) + case cmtmod.OTypeCartoon: + _, _ = mediamod.UpdateByID(nil, oid, bson.M{"countComment": n}) + case cmtmod.OTypeAiPlaza: + _, _ = aiplazamod.UpdateByID(nil, oid, bson.M{"commentCount": n}) + } + } + } + if len(pids) > 0 { + cmtmod.CmtDecreaseCommentCountByID(pids...) + } + }) + } + + return stderr.Success +} + +func CreditIntegral(uid uint64, integral int64, manager, reason string) error { + var tranType string + var tranTypeInt int64 + var desc string + // var f func(t *db.MongoTool, amt int64, uid uint64) error + if integral > 0 { //上分 + tranType = txnmod.AdminCreaditIntegral.Key() + tranTypeInt = int64(txnmod.AdminCreaditIntegral) + desc = txnmod.AdminCreaditIntegral.Key() + strconv.FormatInt(integral, 10) + } else if integral < 0 { //减分 + tranType = txnmod.AdminDebitIntegral.Key() + tranTypeInt = int64(txnmod.AdminDebitIntegral) + desc = txnmod.AdminDebitIntegral.Key() + strconv.FormatInt(integral, 10) + } else { //不操作 + return nil + } + return webg.VideoDB.Trans(func(t *db.MongoTool) error { + integralOld, err := walletmod.GetIntegral(t, uid) + if err != nil { + return err + } + if err = operationlogmod.Insert(operationlogmod.OperationLog{ + ID: primitive.NewObjectID(), + OperationType: constant.User_INIntegral, + BeforeContent: strconv.FormatInt(integralOld, 10), + AfterContent: strconv.FormatInt(integralOld+integral, 10), + Reason: reason, + UserID: int64(uid), + CreatedUser: manager, + }); err != nil { + return err + } + wallet, err := walletmod.CreditIntegral(t, integral, uid) + if err != nil { + return err + } + return txnmod.InsertTransactionLog(t, &(txnmod.TransactionLog{ + UID: uid, + Integral: integral, + ActualAmount: float64(integral), + Tax: 0, + TaxAmount: 0, + TranType: tranType, + TranTypeInt: tranTypeInt, + Desc: desc, + RealIntegral: wallet.RealIntegral(), + })) + }) +} diff --git a/web/service/video_activity_service/type.go b/web/service/video_activity_service/type.go new file mode 100644 index 0000000..5768fa7 --- /dev/null +++ b/web/service/video_activity_service/type.go @@ -0,0 +1,33 @@ +package video_activity_service + +import ( + "time" + + "91porn-server/models/commod" + "91porn-server/models/v/videoactivitymod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// SubmitCond 提交参数 +type SubmitCond struct { + BackgroundImage string `json:"backgroundImage" binding:"required"` // 背景图 + EndTime time.Time `json:"endTime" binding:"required"` // 结束时间 + Desc string `json:"desc"` // 详情描述 + Status videoactivitymod.StatusType `json:"status" binding:"required"` // 状态 +} + +// EditCond 修改参数 +type EditCond struct { + ID primitive.ObjectID `json:"id" binding:"required"` // 文档ID + BackgroundImage *string `json:"backgroundImage"` // 背景图 + EndTime *time.Time `json:"endTime"` // 结束时间 + Desc *string `json:"desc"` // 详情描述 + Status *videoactivitymod.StatusType `json:"status"` // 状态 +} + +// QueryAllCond 查询参数 +type QueryAllCond struct { + Status *int `form:"status"` // 状态 + commod.Page +} diff --git a/web/service/video_activity_service/video_activity.go b/web/service/video_activity_service/video_activity.go new file mode 100644 index 0000000..74dfd63 --- /dev/null +++ b/web/service/video_activity_service/video_activity.go @@ -0,0 +1,84 @@ +package video_activity_service + +import ( + "time" + + "91porn-server/common/stderr" + "91porn-server/models/v/videoactivitymod" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func Submit(cond *SubmitCond) stderr.Code { + if err := videoactivitymod.SubmitDocument(videoactivitymod.VideoActivity{ + BackgroundImage: cond.BackgroundImage, + EndTime: cond.EndTime, + Desc: cond.Desc, + Status: cond.Status, + CreateTime: time.Now(), + }); err != nil { + return stderr.ErrDbInsertError + } + return stderr.Success +} + +func Edit(cond *EditCond) stderr.Code { + filter := bson.M{"_id": cond.ID} + update := bson.M{} + if cond.BackgroundImage != nil { + update["backgroundImage"] = cond.BackgroundImage + } + if cond.EndTime != nil { + update["endTime"] = cond.EndTime + } + if cond.Desc != nil { + update["desc"] = cond.Desc + } + if cond.Status != nil && (*cond.Status == videoactivitymod.Enable || *cond.Status == videoactivitymod.Unused) { + update["status"] = cond.Status + } + if err := videoactivitymod.EditDocument(filter, bson.M{"$set": update}); err != nil { + return stderr.ErrDbUpdateError + } + return stderr.Success +} + +func QueryAll(cond *QueryAllCond) (interface{}, stderr.Code) { + var ( + data = map[string]interface{}{ + "list": []interface{}{}, + "count": 0, + } + filter = bson.M{"status": bson.M{"$in": []videoactivitymod.StatusType{videoactivitymod.Enable, videoactivitymod.Unused}}} + ) + if cond.Status != nil && (videoactivitymod.StatusType(*cond.Status) == videoactivitymod.Enable || videoactivitymod.StatusType(*cond.Status) == videoactivitymod.Unused) { + filter["status"] = cond.Status + } + count, err := videoactivitymod.CountDocument(filter) + if err != nil { + return nil, stderr.ErrDbQueryError + } + if count == 0 { + return data, stderr.Success + } + list, err := videoactivitymod.QueryAllDocument(filter, options.Find().SetLimit(int64(cond.PageSize)).SetSkip(int64((cond.PageNumber-1)*cond.PageSize)).SetSort(bson.M{"createTime": -1})) + if err != nil { + return nil, stderr.ErrDbQueryError + } + data["count"] = count + data["list"] = list + return data, stderr.Success +} + +func Box() (interface{}, stderr.Code) { + filter := bson.M{"status": videoactivitymod.Enable, "endTime": bson.M{"$gte": time.Now()}} + list, err := videoactivitymod.QueryAllDocument(filter, options.Find().SetSort(bson.M{"createTime": -1})) + if err != nil { + return nil, stderr.ErrDbQueryError + } + if len(list) == 0 { + return make([]interface{}, 0), stderr.Success + } + return list, stderr.Success +} diff --git a/web/service/video_gold_coin_ser/video_gold_coin.go b/web/service/video_gold_coin_ser/video_gold_coin.go new file mode 100644 index 0000000..4475117 --- /dev/null +++ b/web/service/video_gold_coin_ser/video_gold_coin.go @@ -0,0 +1,90 @@ +package video_gold_coin_ser + +import ( + "91porn-server/common/stderr" + "91porn-server/models/v/videogoldcoinmod" + "91porn-server/models/v/vidmod" + "errors" + "go.mongodb.org/mongo-driver/bson/primitive" + "strings" + "time" +) + +func QueryAll(in *videogoldcoinmod.QueryAllCond) (*videogoldcoinmod.QueryAllRes, error) { + var out = &videogoldcoinmod.QueryAllRes{} + count, err := videogoldcoinmod.QueryAllCount(in.Filter()) + if err != nil { + return nil, err + } + if count == 0 { + out.List = []*videogoldcoinmod.VideoGoldCoin{} + out.Total = 0 + return out, nil + } + out.Total = count + out.List, err = videogoldcoinmod.QueryAllList(in.Filter(), in.Options()) + if err != nil { + return nil, err + } + return out, nil +} + +func Add(in *videogoldcoinmod.AddCond, manager string) stderr.Code { + videoIdStrs := strings.Split(in.VideoId, ",") + vids := make([]primitive.ObjectID, len(videoIdStrs)) + for i, vidIDStr := range videoIdStrs { + videoID, err := primitive.ObjectIDFromHex(vidIDStr) + if err != nil { + return stderr.ErrParamError + } + vids[i] = videoID + } + list, err := vidmod.GetVideoListByIDs(vids) + if err != nil { + return stderr.ErrNetWorkBusy + } + + now := time.Now() + records := make([]videogoldcoinmod.VideoGoldCoin, len(list)) + for i := range list { + records[i] = videogoldcoinmod.VideoGoldCoin{ + PublisherID: list[i].PublisherID, + VidId: list[i].ID, + VidTitle: list[i].Title, + VidType: list[i].NewsType, + SourceURL: list[i].SourceURL, + Cover: list[i].Cover, + SeriesCover: list[i].SeriesCover, + Coins: list[i].Coins, + Status: 1, + OperateAccount: manager, + CreatedAt: now, + } + } + err = videogoldcoinmod.InsertMany(records) + if err != nil { + return stderr.ErrDbInsertError + } + return stderr.Success +} + +func Edit(in *videogoldcoinmod.EditCond, manager string) error { + status, err := videogoldcoinmod.IsExistByVideoID(in.ID) + if err != nil { + return err + } + if !status { + return errors.New("data is null") + } + + err = videogoldcoinmod.Edit(in.Filter(), in.Update(manager)) + if err != nil { + return err + } + + return nil +} + +func Delete(in *videogoldcoinmod.DeleteCond) error { + return videogoldcoinmod.Remove(in.Filter()) +} diff --git a/web/service/video_media_service/http.go b/web/service/video_media_service/http.go new file mode 100644 index 0000000..b8b126d --- /dev/null +++ b/web/service/video_media_service/http.go @@ -0,0 +1,84 @@ +package video_media_service + +import ( + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/models/v/vidmod" + "fmt" + "net/http" + "strings" +) + +// UploadSkdVideoMedia 上传视频 +func UploadSkdVideoMedia(up vidmod.UploadMediaVideo) error { + var ( + params = (&UploadMediaVideoRequest{ + FsResourceId: up.FsResourceId, + HashId: up.HashId, + Title: HandleTitle(up.Title), + CoverImage: up.CoverImage, + M3u8Src: up.M3u8Src, + FileSize: up.FileSize, + Length: up.Length, + Width: up.Width, + Height: up.Height, + TagsText: up.TagsText, + }).GenerateSkdParams() + out CommonResponse + ) + + code, err := httputil.DefaultClientPostJsonWithResp(&out, params.GetSkdURL(), nil, params) + if err != nil || code != http.StatusOK { + log.Error(fmt.Sprintf("Requst skd upload video media Http.Post fail error:%v;code:%v", err, code)) + return err + } + + c := out.Code + m := out.Msg + if c != 0 || m != "success" { + log.Error(fmt.Sprintf("Requst skd upload video media Http.Post code error:%v;response:%v;msg:%v;params%v", err, code, out.Msg, params)) + return stderr.Failure + } + return nil +} + +// UploadSkdShortVideoMedia 上传短视频 +func UploadSkdShortVideoMedia(up vidmod.UploadMediaVideo) error { + var ( + params = (&UploadMediaVideoRequest{ + FsResourceId: up.FsResourceId, + HashId: up.HashId, + Title: HandleTitle(up.Title), + CoverImage: up.CoverImage, + M3u8Src: up.M3u8Src, + FileSize: up.FileSize, + Length: up.Length, + Width: up.Width, + Height: up.Height, + TagsText: up.TagsText, + }).GenerateSkdParams() + out CommonResponse + ) + + code, err := httputil.DefaultClientPostJsonWithResp(&out, params.GetSkdShortURL(), nil, params) + if err != nil || code != http.StatusOK { + log.Error(fmt.Sprintf("Requst skd upload short video media Http.Post fail error:%v;code:%v", err, code)) + return err + } + c := out.Code + m := out.Msg + if c != 0 || m != "success" { + log.Error(fmt.Sprintf("Requst skd upload short video media Http.Post code error:%v;response:%v;msg:%v;params%v", err, code, out.Msg, params)) + return stderr.Failure + } + return nil +} + +func HandleTitle(title string) (t string) { + // 去除首位空格 + title = strings.TrimSpace(title) + // 去除换行 + title = strings.Replace(title, "\n", "", -1) + return title +} diff --git a/web/service/video_media_service/type.go b/web/service/video_media_service/type.go new file mode 100644 index 0000000..ccd0fd1 --- /dev/null +++ b/web/service/video_media_service/type.go @@ -0,0 +1,189 @@ +package video_media_service + +import ( + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/skd/skdg" + "bytes" + "crypto/md5" + "encoding/hex" + "fmt" + "reflect" + "sort" + "strconv" + "strings" + "time" +) + +const ( + UploadShortVideoMediaRouter = "/services/short_video/create" // 上传短视频API + UploadVideoMediaRouter = "/services/video/create" // 上传长视频API + +) + +type CommonResponse struct { + Code int64 `json:"code"` + Data ResponseData `json:"data"` + Msg string `json:"msg"` +} + +type ResponseData struct { + Data []VideoMediaInfo `json:"data"` + Pagination ResponsePagination `json:"pagination"` +} + +type ResponsePagination struct { + IsLastPage bool `json:"is_last_page"` // 视频是否是最一页 + Page int64 `json:"page" binding:"required" ` // 当前页 + Size int64 `json:"size" binding:"required" ` // 条数 + Count int64 `json:"count" binding:"required" ` // 视频总条数 +} + +// VideoMediaInfo 媒资库数据响应模型 +type VideoMediaInfo struct { + Id int64 `json:"id" bson:"id"` // 视频ID + HashId string `json:"hash_id" bson:"hash_id"` // 视频唯一ID + VLoggerUserId int64 `json:"vlogger_user_id" bson:"vlogger_user_id"` // 视频资源博主ID + Title string `json:"title" bson:"title"` // 视频标题 + Content string `json:"content" bson:"content"` // 帖子详情 + CoverImage string `json:"cover_image" bson:"cover_image"` // 视频资源封面图片 + CoverGif string `json:"cover_gif" bson:"cover_gif"` // 视频资源封面GIF + Pics string `json:"pics" bson:"pics"` // 视频封面 + Gifs string `json:"gifs" bson:"gifs"` // 视频GIF + Src480 string `json:"src_480" bson:"src_480"` // 视频480m3u8文件 + Src720 string `json:"src_720" bson:"src_720"` // 视频720m3u8文件 + Src1080 string `json:"src_1080" bson:"src_1080"` // 视频1080m3u8文件 + FileSize480 int64 `json:"file_size_480" bson:"file_size_480"` // 视频480文件大小 + FileSize720 int64 `json:"file_size_720" bson:"file_size_720"` // 视频720文件大小 + FileSize1080 int64 `json:"file_size1080" bson:"file_size_1080"` // 视频1080文件大小 + Width int64 `json:"width" bson:"width"` // 视频宽度 + Height int64 `json:"height" bson:"height"` // 视频高度 + Length int64 `json:"length" bson:"length"` // 视频长度 + SaleCoin int64 `json:"sale_coin" bson:"sale_coin"` // 视频金币售价 + Used int64 `json:"used" bson:"used"` // 视频是否使用 + TagsText string `json:"tags_text" bson:"tags_text"` // 视频标题文本 +} + +// UploadMediaVideoRequest 上传到媒资库的基础信息请求体 +type UploadMediaVideoRequest struct { + ProId string `json:"pro_id"` // 产品ID + Timestamp string `json:"timestamp"` // unix时间戳10位 + FsResourceId string `json:"fs_resource_id"` // 文件资源id + HashId string `json:"hash_id"` // 视频唯一id + Title string `json:"title"` // 视频标题 + CoverImage string `json:"cover_image"` // 封面图 + M3u8Src string `json:"m3u8_src"` // m3u8地址 + FileSize int `json:"file_size"` // 视频大小 + Length int `json:"length"` // 视频时长 秒 + Width int `json:"width"` // 宽度 + Height int `json:"height"` // 高度 + TagsText string `json:"tags_text"` // 视频标签,多个以逗号分割 + Sign string `json:"sign" binding:"sign"` // 签名 +} + +// UploadMediaVideo 上传到媒资库的基础信息请求体 +type UploadMediaVideo struct { + ProId string `json:"pro_id" sign:"1"` // 产品ID + Timestamp string `json:"timestamp" sign:"1"` // unix时间戳10位 + FsResourceId string `json:"fs_resource_id" sign:"1"` // 文件资源id + HashId string `json:"hash_id" sign:"1"` // 视频唯一id + Title string `json:"title" sign:"1"` // 视频标题 + CoverImage string `json:"cover_image" sign:"1"` // 封面图 + M3u8Src string `json:"m3u8_src" sign:"1"` // m3u8地址 + FileSize int `json:"file_size" sign:"1"` // 视频大小 + Length int `json:"length" sign:"1"` // 视频时长 秒 + Width int `json:"width" sign:"1"` // 宽度 + Height int `json:"height" sign:"1"` // 高度 + TagsText string `json:"tags_text" sign:"1"` // 视频标签,多个以逗号分割 + Sign string `json:"sign" binding:"sign"` // 签名 +} + +// GetSKDProID 获取商户编号 +func (*UploadMediaVideoRequest) GetSKDProID() string { + return skdg.Conf.MediaResourcesCfg.ProId +} + +// ToSkdSign 签名 +func (umv *UploadMediaVideoRequest) ToSkdSign(um UploadMediaVideo) (sign string) { + ss := um.ToStringSlice() + sort.Sort(&ss) + var buf bytes.Buffer + for k, v := range ss { + if k == 0 { + buf.WriteString(v) + } else { + buf.WriteString("&") + buf.WriteString(v) + } + } + buf.WriteString("&secret=") + buf.WriteString(umv.GetSKDAppSecret()) + log.Info(fmt.Sprintf("Upload videoMedia md5 skdsign string:%s", buf.String())) + return strings.ToUpper(md5Sign(buf.String())) +} + +// md5签名 +func md5Sign(buf string) string { + md5Ctx := md5.New() + md5Ctx.Write([]byte(buf)) + cipherStr := md5Ctx.Sum(nil) + newSign := hex.EncodeToString(cipherStr) + return newSign +} + +// ToStringSlice 转化为string切片切片 +func (umv *UploadMediaVideo) ToStringSlice() rchgutil.StringList { + data := make([]string, 0) + t := reflect.TypeOf(*umv) + v := reflect.ValueOf(*umv) + for k := 0; k < v.NumField(); k++ { + jsonTag, _ := t.Field(k).Tag.Lookup("json") + signTag, _ := t.Field(k).Tag.Lookup("sign") + fv := v.Field(k) + if v.Field(k).String() != "" && signTag == "1" { + switch fv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + data = append(data, jsonTag+"="+strconv.FormatInt(v.Field(k).Int(), 10)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + data = append(data, jsonTag+"="+strconv.FormatUint(v.Field(k).Uint(), 10)) + case reflect.String: + data = append(data, jsonTag+"="+v.Field(k).String()) + } + } + } + return data +} + +// GetSKDAppSecret 获取app签名密钥 +func (*UploadMediaVideoRequest) GetSKDAppSecret() string { + return skdg.Conf.MediaResourcesCfg.AppSecret +} + +func (umv *UploadMediaVideoRequest) GenerateSkdParams() UploadMediaVideo { + var upVideoMedia UploadMediaVideo + now := time.Now().Unix() + upVideoMedia.ProId = umv.GetSKDProID() + upVideoMedia.FsResourceId = umv.FsResourceId + upVideoMedia.HashId = umv.HashId + upVideoMedia.Title = umv.Title + upVideoMedia.CoverImage = umv.CoverImage + upVideoMedia.M3u8Src = umv.M3u8Src + upVideoMedia.FileSize = umv.FileSize + upVideoMedia.Length = umv.Length + upVideoMedia.Width = umv.Width + upVideoMedia.Height = umv.Height + upVideoMedia.TagsText = umv.TagsText + upVideoMedia.Timestamp = strconv.FormatInt(now, 10) + upVideoMedia.Sign = umv.ToSkdSign(upVideoMedia) + return upVideoMedia +} + +// GetSkdShortURL 获取短视频上传请求URL +func (umv *UploadMediaVideo) GetSkdShortURL() string { + return skdg.Conf.MediaResourcesCfg.UploadMediaUrl + UploadShortVideoMediaRouter +} + +// GetSkdURL 获取视频上传请求URL +func (umv *UploadMediaVideo) GetSkdURL() string { + return skdg.Conf.MediaResourcesCfg.UploadMediaUrl + UploadVideoMediaRouter +} diff --git a/web/service/vidser/liaoba_top_sort_test.go b/web/service/vidser/liaoba_top_sort_test.go new file mode 100644 index 0000000..4139040 --- /dev/null +++ b/web/service/vidser/liaoba_top_sort_test.go @@ -0,0 +1,55 @@ +package vidser + +import ( + "reflect" + "testing" + + "go.mongodb.org/mongo-driver/bson" +) + +func TestApplyLiaoBaTopSortAllowsDuplicateAndLargeValues(t *testing.T) { + existingVideo := bson.M{"liaoBaTopSort": 1000} + targetUpdate := bson.M{"title": "unchanged"} + sortCode := 1000 + + applyLiaoBaTopSort(targetUpdate, &sortCode) + + if got := targetUpdate["liaoBaTopSort"]; got != sortCode { + t.Fatalf("liaoBaTopSort = %v, want %d", got, sortCode) + } + if got := targetUpdate["liaoBaTop"]; got != true { + t.Fatalf("liaoBaTop = %v, want true", got) + } + if got := targetUpdate["title"]; got != "unchanged" { + t.Fatalf("unrelated field changed: %v", got) + } + if got := existingVideo["liaoBaTopSort"]; got != sortCode { + t.Fatalf("existing video sort changed: %v", got) + } +} + +func TestApplyLiaoBaTopSortZeroUnpinsCurrentVideo(t *testing.T) { + set := bson.M{} + sortCode := 0 + + applyLiaoBaTopSort(set, &sortCode) + + if got := set["liaoBaTopSort"]; got != 0 { + t.Fatalf("liaoBaTopSort = %v, want 0", got) + } + if got := set["liaoBaTop"]; got != false { + t.Fatalf("liaoBaTop = %v, want false", got) + } +} + +func TestLiaoBaTopSortModeListHasStableTieBreakers(t *testing.T) { + want := SortModeList{ + {Field: "liaoBaTopSort", Value: -1}, + {Field: "reviewAt", Value: -1}, + {Field: "_id", Value: -1}, + } + + if got := LiaoBaTopSortModeList(); !reflect.DeepEqual(got, want) { + t.Fatalf("sort = %#v, want %#v", got, want) + } +} diff --git a/web/service/vidser/mediaresouce.go b/web/service/vidser/mediaresouce.go new file mode 100644 index 0000000..01ad349 --- /dev/null +++ b/web/service/vidser/mediaresouce.go @@ -0,0 +1,260 @@ +package vidser + +import ( + "91porn-server/app/service/modulevideoser" + "91porn-server/models/v/modulevidmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidtimeonlinemod" + "context" + "fmt" + "math/rand" + "net/url" + "strconv" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/log" + "91porn-server/common/mdrsdb" + "91porn-server/models/v/vidmod" + + "github.com/shopspring/decimal" +) + +func getMediaPathFromUrl(rawUrl string) string { + url, err := url.Parse(rawUrl) + if err != nil || url.Path == "" { + return "" + } + return url.Path +} + +func getMediaFileInfoFromUrl(rawUrl string) (sourceID, fileName string) { + url, err := url.Parse(rawUrl) + if err != nil { + return + } + path := url.Path + sliX := strings.Split(path, "/") + if len(sliX) > 0 { + fileName = sliX[len(sliX)-1] + } + sliP := strings.Split(fileName, ".") + if len(sliP) > 0 { + sourceID = sliP[0] + } + return +} + +func getIMGPathFromUrl(rawUrl string) string { + //新桶,无需加ims,可直接回源 + if strings.Contains(rawUrl, "cf230705") || strings.Contains(rawUrl, "bf211222") { + return rawUrl + } + if rawUrl == "" { + return "" + } + url, err := url.Parse(rawUrl) + if err != nil { + return "" + } + return common.BindUrl(constant.ImageSourceIMS, url.Path) +} + +func GetMediaList(ctx context.Context, page, size, vloggerUserId int64, title, tagText, hashId string, vMia, videoType int, syncType, terminalCdo string, startTime, endTime time.Time, sortType, maxTotal int) (data []mdrsdb.MediaResource, count int64, err error) { + data, count, err = mdrsdb.FindMediaList(ctx, page, size, vloggerUserId, title, tagText, hashId, vMia, videoType, syncType, terminalCdo, startTime, endTime, sortType, maxTotal) + for i := range data { + data[i].CoverImage = getIMGPathFromUrl(data[i].CoverImage) + data[i].VerticalCover = getIMGPathFromUrl(data[i].VerticalCover) + data[i].Src1080 = getMediaPathFromUrl(data[i].Src1080) + data[i].Src720 = getMediaPathFromUrl(data[i].Src720) + data[i].Src480 = getMediaPathFromUrl(data[i].Src480) + data[i].VerticalCover = getIMGPathFromUrl(data[i].VerticalCover) + tags := strings.Split(data[i].TagsText, ",") + if len(tags) > 5 { + tags = tags[:5] + } + data[i].TagList = tags + } + return +} + +func UseMedias(ctx context.Context, ids []int64, authorID uint64, sectionID primitive.ObjectID, inTags []string, videoType int, manager string, coins *int64, onlineTime time.Time) ([]SensitiveHit, error) { + mbs, err := mdrsdb.FindMediaListWithIDs(ctx, ids, videoType) + if err != nil { + return nil, err + } + terms := LoadEnabledSensitiveTerms() + hits := make([]SensitiveHit, 0) + useIds := make([]int64, 0, len(mbs)) + for i, v := range mbs { + log.Info(fmt.Sprintf("Synchronize media library video information:video_id:%d up_id:%d authorId:%d", v.Id, v.VloggerUserId, authorID)) + mbs[i].CoverImage = getIMGPathFromUrl(mbs[i].CoverImage) + mbs[i].VerticalCover = getIMGPathFromUrl(mbs[i].VerticalCover) + mbs[i].Src1080 = getMediaPathFromUrl(mbs[i].Src1080) + mbs[i].Src720 = getMediaPathFromUrl(mbs[i].Src720) + mbs[i].Src480 = getMediaPathFromUrl(mbs[i].Src480) + mbs[i].VerticalCover = getIMGPathFromUrl(mbs[i].VerticalCover) + var m3u8 string + var m3u8Size int64 + if strings.Contains(mbs[i].Src720, "/") { + m3u8 = mbs[i].Src720 + m3u8Size = mbs[i].FileSize720 + } + if m3u8 == "" { + m3u8 = mbs[i].Src480 + m3u8Size = mbs[i].FileSize480 + } + if m3u8 == "" { + m3u8 = mbs[i].Src1080 + m3u8Size = mbs[i].FileSize1080 + } + tagsList := strings.Split(mbs[i].TagsText, ",") + tags := []string{} + for _, tag := range tagsList { + tag = strings.TrimSpace(tag) + if tag != "" { + tags = append(tags, tag) + } + } + if len(tags) > 5 { + tags = tags[:5] + } + tags = append(tags, inTags...) + if authorID == 0 && v.VloggerUserId != 0 { + authorID = v.VloggerUserId + } else if authorID == 0 { + authorID = 100001 + } + author, _ := usermod.FindUserByUID(authorID) + if author == nil { + authorID = uint64(rand.Int63n(50000) + 100001) + } + var pics []string + if len(v.Pics) > 0 { + pics = strings.Split(v.Pics, ",") + for i, p := range pics { + pics[i] = getIMGPathFromUrl(p) + } + } + // videoType : 0 长视频 1短视频 2图文 + // v.MediaType: 0视频 1 图文 + vt := vidmod.SP + if videoType == 1 { + vt = vidmod.SHORT + } else if videoType == 2 && v.MediaType == 1 { + vt = vidmod.COVER + } + + var ratio float64 = 0 + if v.Height > 0 && v.Width > 0 { + ratio = float64(decimal.NewFromInt(int64(v.Width)).Div(decimal.NewFromInt(int64(v.Height))).Shift(2).IntPart()) / 100 + } + temp := vidmod.WebSubmitReq{ + UID: authorID, + NewsType: vt, + Title: v.Title, + Content: v.Content, + Tags: tags, + PlayTime: v.Length, + Cover: mbs[i].CoverImage, + VerticalCover: mbs[i].VerticalCover, + SeriesCover: pics, + Via: mbs[i].TerminalCdo, + Coins: mbs[i].SaleCoin, + Size: int(m3u8Size), + Resolution: strconv.Itoa(v.Width) + "*" + strconv.Itoa(v.Height), + Ratio: ratio, + Width: v.Width, + Height: v.Height, + MimeType: "", + Actor: "", + SourceURL: m3u8, + MD5: v.HashId, + SourceID: strconv.FormatInt(v.Id, 10), + MDSID: v.HashId, + Account: manager, + RichText: v.RichText, + PreviewStart: v.PreviewStart, + } + if coins != nil { + temp.Coins = *coins + } + if len(tags) > 0 { + if tags[0] != "" { + temp.Status = 1 + } + + } + if m3u8 != "" { + temp.SourceID, temp.Filename = getMediaFileInfoFromUrl(m3u8) + if temp.PlayTime > 60 { + temp.FreeTime = 15 + } else { + temp.FreeTime = int(temp.PlayTime / 4) + } + } + if authorID > 0 { + temp.UID = authorID + } + if !onlineTime.IsZero() { + temp.Status = 0 + } + if temp.NewsType == vidmod.COVER { + temp.Status = 0 + } + if !onlineTime.IsZero() { + temp.Status = vidmod.OnlinePass + } + // 敏感词命中:强制为待审核,且不进入自动上架/专题上架链路 + hit := CheckTextHits(terms, temp.SourceID, temp.Title, temp.Content, temp.RichText) + if hit != nil { + temp.Status = vidmod.WaitingCheck + log.Warn("vidser.UseMedias sensitive hit, forced offline & skip auto-online", + log.Any("sourceId", hit.SourceID), + log.Any("title", hit.Title), + log.Any("detail", hit.Detail)) + hits = append(hits, *hit) + } + code, vid := SubmitBase(temp) + if code != 200 { + log.ErrorX(ctx, "vidser UseMedias is fail", log.Any("code", code), log.Any("vid", vid)) + continue + } + if !onlineTime.IsZero() && hit == nil { + // 加入批量自动上架(命中过的资源不进入此路径) + BatchAuditVidTimeOnlineInfo(vidtimeonlinemod.BatchAuditReq{ + Vids: []primitive.ObjectID{vid}, + SectionID: sectionID, + OnlineTime: onlineTime, + }, int(temp.Coins), manager) + + } else if !sectionID.IsZero() && temp.Status == 1 { + // 加入专题 + sv := modulevidmod.SectionVideo{ + SectionID: sectionID, + VideoID: vid, + VideoReviewedAt: time.Now(), + SortCode: 50, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + _ = modulevideoser.AddVideo(&sv) + } + useIds = append(useIds, v.Id) + } + if len(useIds) > 0 { + if err = mdrsdb.UseMedia(ctx, useIds, videoType); err != nil { + return hits, err + } + } + return hits, nil +} + +func IgnoreMedias(ctx context.Context, ids []int64, videoType int) (err error) { + return mdrsdb.IgnoreMedia(ctx, ids, videoType) +} diff --git a/web/service/vidser/scheduled_module_test.go b/web/service/vidser/scheduled_module_test.go new file mode 100644 index 0000000..0382330 --- /dev/null +++ b/web/service/vidser/scheduled_module_test.go @@ -0,0 +1,27 @@ +package vidser + +import ( + "testing" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestApplyScheduledVideoModuleID(t *testing.T) { + moduleID := primitive.NewObjectID() + update := bson.M{} + + applyScheduledVideoModuleID(update, moduleID) + + if got := update["mId"]; got != moduleID.Hex() { + t.Fatalf("mId = %v, want %s", got, moduleID.Hex()) + } +} + +func TestApplyScheduledVideoModuleIDIgnoresEmptyID(t *testing.T) { + update := bson.M{} + applyScheduledVideoModuleID(update, primitive.NilObjectID) + if _, ok := update["mId"]; ok { + t.Fatal("empty module id must not be written") + } +} diff --git a/web/service/vidser/sensitive_check.go b/web/service/vidser/sensitive_check.go new file mode 100644 index 0000000..6a43dd0 --- /dev/null +++ b/web/service/vidser/sensitive_check.go @@ -0,0 +1,170 @@ +package vidser + +import ( + "errors" + "fmt" + "strings" + + "91porn-server/common/log" + "91porn-server/models/v/sensitivewordmod" + "91porn-server/models/v/vidmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// CheckShelvingFields 直接对一组文本做敏感词命中检测;用于单帖编辑场景, +// 调用方负责把请求字段与 DB 现值合并后传入。 +// 命中规则与 CheckShelvingByIDs 一致:Title / Content / RichText 任一命中即视为违规。 +func CheckShelvingFields(id primitive.ObjectID, title, content, richText string) error { + terms := loadEnabledSensitiveTerms() + if len(terms) == 0 { + return nil + } + titleHits := findSensitiveHits(title, terms) + contentHits := findSensitiveHits(content, terms) + richHits := findSensitiveHits(richText, terms) + if len(titleHits) == 0 && len(contentHits) == 0 && len(richHits) == 0 { + return nil + } + return errors.New(formatHitLine(id, title, titleHits, contentHits, richHits)) +} + +// CheckShelvingByIDs 上架前敏感词命中检测。 +// +// 与 skd/service/contentreviewser VIDEO 任务的扫描规则保持一致: +// 对每条视频的 Title / Content / RichText 三个字段做 substring 命中检测, +// 任一字段命中启用状态的敏感词词条即视为违规,不予上架。 +// +// 返回值: +// - nil —— 全部通过或敏感词库为空 +// - non-nil err —— 至少一条命中,err.Error() 已按视频聚合 +// +// 多条命中时,每条视频独占一行,行内字段按"标题命中 / 内容命中 / 富文本命中"顺序拼接。 +func CheckShelvingByIDs(ids []primitive.ObjectID) error { + if len(ids) == 0 { + return nil + } + terms := loadEnabledSensitiveTerms() + if len(terms) == 0 { + return nil + } + videos, err := vidmod.GetVideoListByIDsNoStatus(ids) + if err != nil { + log.Error("vidser.CheckShelvingByIDs query videos fail", log.Any("ids", ids), log.E(err)) + return errors.New("查询视频信息失败") + } + lines := make([]string, 0) + for _, v := range videos { + titleHits := findSensitiveHits(v.Title, terms) + contentHits := findSensitiveHits(v.Content, terms) + richHits := findSensitiveHits(v.RichText, terms) + if len(titleHits) == 0 && len(contentHits) == 0 && len(richHits) == 0 { + continue + } + lines = append(lines, formatHitLine(v.ID, v.Title, titleHits, contentHits, richHits)) + } + if len(lines) == 0 { + return nil + } + return errors.New(strings.Join(lines, "\n")) +} + +// LoadEnabledSensitiveTerms 启用状态敏感词词条加载(代理 sensitivewordmod.LoadEnabledTerms) +// 保留此处签名以兼容现有 laosijiser / tianyuctrl 等调用方 +func LoadEnabledSensitiveTerms() []string { + return sensitivewordmod.LoadEnabledTerms() +} + +// loadEnabledSensitiveTerms 内部短名别名 +func loadEnabledSensitiveTerms() []string { + return sensitivewordmod.LoadEnabledTerms() +} + +// SensitiveHit 单条命中结果,用于批量同步接口(老司机/天宇媒资库)返回给前端 +type SensitiveHit struct { + SourceID string `json:"sourceId"` // 来源 id(如 laosiji 资源 id / tianyu fileId) + Title string `json:"title"` // 标题 + Detail string `json:"detail"` // "标题命中 XX 内容命中 YY 富文本命中 ZZ" +} + +// CheckTextHits 对一组文本做敏感词命中检测,命中返回 *SensitiveHit,未命中返回 nil。 +// 与 skd 内容审查任务的匹配规则保持一致:任一字段命中即视为命中。 +// 调用方在入库前调用,命中后应将记录的 status 强制为"未上架"语义的值。 +// +// 词条由外部传入,便于批量场景一次加载、多次匹配;传 nil 或空切片视为词库为空,直接返回 nil。 +func CheckTextHits(terms []string, sourceID, title, content, richText string) *SensitiveHit { + if len(terms) == 0 { + return nil + } + titleHits := findSensitiveHits(title, terms) + contentHits := findSensitiveHits(content, terms) + richHits := findSensitiveHits(richText, terms) + if len(titleHits) == 0 && len(contentHits) == 0 && len(richHits) == 0 { + return nil + } + parts := make([]string, 0, 3) + if len(titleHits) > 0 { + parts = append(parts, "标题命中 "+strings.Join(titleHits, "、")) + } + if len(contentHits) > 0 { + parts = append(parts, "内容命中 "+strings.Join(contentHits, "、")) + } + if len(richHits) > 0 { + parts = append(parts, "富文本命中 "+strings.Join(richHits, "、")) + } + return &SensitiveHit{ + SourceID: sourceID, + Title: title, + Detail: strings.Join(parts, " "), + } +} + +// SensitiveForcedOfflineTip 写库后给管理员的统一提示文案 +const SensitiveForcedOfflineTip = "由于部分数据内容命中敏感词,状态强制为下架" + +// findSensitiveHits substring 命中检测的内部别名,代理 sensitivewordmod.MatchHits +func findSensitiveHits(input string, terms []string) []string { + return sensitivewordmod.MatchHits(input, terms) +} + +// shouldCheckSensitiveOnUpdate 判断单帖编辑 UpdateVidInfo 是否需要做敏感词校验 +// +// 1. status 被切到 上架(CheckPass=1) / 上架并免费(Free=3) —— 上架转换 +// 2. 当前已是上架态(1/3) 且本次更新涉及 Title/Content/RichText —— 已上架内容改动 +func shouldCheckSensitiveOnUpdate(vidInfo vidmod.VideoModel, req vidmod.EditReq) bool { + if req.Status != nil && (*req.Status == vidmod.CheckPass || *req.Status == vidmod.Free) { + return true + } + isCurrentlyShelved := vidInfo.Status == vidmod.CheckPass || vidInfo.Status == vidmod.Free + touchesText := req.Title != nil || req.Content != nil || req.RichText != nil + return isCurrentlyShelved && touchesText +} + +// parseObjectIDs 把十六进制 id 字符串数组转 ObjectID,非法 id 静默丢弃 +func parseObjectIDs(ids []string) []primitive.ObjectID { + out := make([]primitive.ObjectID, 0, len(ids)) + for _, s := range ids { + oid, err := primitive.ObjectIDFromHex(s) + if err != nil { + continue + } + out = append(out, oid) + } + return out +} + +// formatHitLine 按需求格式拼接单条命中描述: +// "id:xxxx 对象标题 XXXXXX。 标题命中 XX 内容命中 XX 富文本命中 XX,不予上架成功" +func formatHitLine(id primitive.ObjectID, title string, titleHits, contentHits, richHits []string) string { + parts := make([]string, 0, 3) + if len(titleHits) > 0 { + parts = append(parts, "标题命中 "+strings.Join(titleHits, "、")) + } + if len(contentHits) > 0 { + parts = append(parts, "内容命中 "+strings.Join(contentHits, "、")) + } + if len(richHits) > 0 { + parts = append(parts, "富文本命中 "+strings.Join(richHits, "、")) + } + return fmt.Sprintf("id:%s 对象标题 %s。 %s,不予上架成功", id.Hex(), title, strings.Join(parts, " ")) +} diff --git a/web/service/vidser/tag_search_visibility.go b/web/service/vidser/tag_search_visibility.go new file mode 100644 index 0000000..1fe5b03 --- /dev/null +++ b/web/service/vidser/tag_search_visibility.go @@ -0,0 +1,27 @@ +package vidser + +import ( + "sort" + + "91porn-server/models/v/moduleconfmod" + + "go.mongodb.org/mongo-driver/bson" +) + +var excludedTagSearchModuleIDs = moduleconfmod.ExcludedSearchModuleIDs + +func tagSearchCacheKey(base string, excludedModuleIDs []string) string { + moduleIDs := append([]string(nil), excludedModuleIDs...) + sort.Strings(moduleIDs) + for _, moduleID := range moduleIDs { + base += ":" + moduleID + } + return base +} + +func applyTagSearchVisibilityFilter(filter bson.M, excludedModuleIDs []string) { + if len(excludedModuleIDs) == 0 { + return + } + filter["mId"] = bson.M{"$nin": excludedModuleIDs} +} diff --git a/web/service/vidser/tag_search_visibility_test.go b/web/service/vidser/tag_search_visibility_test.go new file mode 100644 index 0000000..fca3946 --- /dev/null +++ b/web/service/vidser/tag_search_visibility_test.go @@ -0,0 +1,34 @@ +package vidser + +import ( + "reflect" + "testing" + + "go.mongodb.org/mongo-driver/bson" +) + +func TestTagSearchCacheKeyTracksExcludedModules(t *testing.T) { + base := "tag-videos" + first := tagSearchCacheKey(base, []string{"module-b", "module-a"}) + second := tagSearchCacheKey(base, []string{"module-a", "module-b"}) + if first != second { + t.Fatalf("cache keys differ for the same module set: %q != %q", first, second) + } + if first == tagSearchCacheKey(base, []string{"module-a"}) { + t.Fatal("cache key did not change when the excluded module set changed") + } +} + +func TestApplyTagSearchVisibilityFilter(t *testing.T) { + filter := bson.M{"status": 1} + excluded := []string{"module-a", "module-b"} + applyTagSearchVisibilityFilter(filter, excluded) + + want := bson.M{ + "status": 1, + "mId": bson.M{"$nin": excluded}, + } + if !reflect.DeepEqual(filter, want) { + t.Fatalf("filter = %#v, want %#v", filter, want) + } +} diff --git a/web/service/vidser/type.go b/web/service/vidser/type.go new file mode 100644 index 0000000..a24a3a2 --- /dev/null +++ b/web/service/vidser/type.go @@ -0,0 +1,19 @@ +package vidser + +type SortMode struct { + Field string + Value int +} + +type SortModeList []SortMode + +// LiaoBaTopSortModeList returns the deterministic order used by pinned-video +// lists. Duplicate liaoBaTopSort values are allowed, so reviewAt and _id are +// used as stable tie-breakers. +func LiaoBaTopSortModeList() SortModeList { + return SortModeList{ + {Field: "liaoBaTopSort", Value: -1}, + {Field: "reviewAt", Value: -1}, + {Field: "_id", Value: -1}, + } +} diff --git a/web/service/vidser/vid.go b/web/service/vidser/vid.go new file mode 100644 index 0000000..8d707a2 --- /dev/null +++ b/web/service/vidser/vid.go @@ -0,0 +1,1555 @@ +package vidser + +import ( + "91porn-server/app/service/taskser" + "91porn-server/app/service/vidhelpser" + sli "91porn-server/common/slice" + "91porn-server/models/v/dailytaskmod" + "91porn-server/models/v/modulesectionmod" + "91porn-server/models/v/vidtimeonlinemod" + "bytes" + "encoding/json" + "errors" + "fmt" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/bxcodec/faker/v3/support/slice" + "go.mongodb.org/mongo-driver/mongo/options" + + "github.com/gin-gonic/gin" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant" + "91porn-server/common/constant/redisconst" + "91porn-server/common/file" + "91porn-server/common/httputil" + "91porn-server/common/log" + "91porn-server/common/stderr" + "91porn-server/common/timeutil" + "91porn-server/models/commod" + "91porn-server/models/v/imagetopmod" + "91porn-server/models/v/locmod" + "91porn-server/models/v/modulevidmod" + "91porn-server/models/v/operationlogmod" + "91porn-server/models/v/pushmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + "91porn-server/web/service/updownloadser" + "91porn-server/web/vidhelp" + "91porn-server/web/webg" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// SubmitBase 提交基本信息 +func SubmitBase(req vidmod.WebSubmitReq) (code stderr.Code, vid primitive.ObjectID) { + tags := make([]primitive.ObjectID, 0, len(req.Tags)) + for _, t := range req.Tags { + tid, suc := updownloadser.HandleTag(t, req.Cover) + if !suc { + continue + } + tags = append(tags, tid) + } + l := locmod.Location{ + Longitude: req.Location.Longitude, + Latitude: req.Location.Latitude, + Country: req.Location.Country, + CountryCode: req.Location.CountryCode, + Province: req.Location.Province, + ProvinceCode: req.Location.ProvinceCode, + City: req.Location.City, + CityCode: req.Location.CityCode, + Address: req.Location.Address, + Visit: 0, + } + if err := locmod.InsertLocationInfo(l); err != nil { + return stderr.ErrDbInsertError, primitive.NilObjectID + } + lid, err := locmod.GetLocationIDByCity(req.Location.City) + if err != nil { + return stderr.ErrDbQueryError, primitive.NilObjectID + } + series := []string{} + if len(req.SeriesCover) != 0 { + series = req.SeriesCover + } + oid, _ := primitive.ObjectIDFromHex(req.SourceID) + v := vidmod.VideoModel{ + ID: oid, + NewsType: req.NewsType, + PublisherID: req.UID, + Title: req.Title, + Content: req.Content, + MimeType: req.MimeType, + Tags: tags, + SourceID: req.SourceID, + SourceURL: req.SourceURL, + PlayTime: req.PlayTime, + Cover: req.Cover, + CoverThumb: req.CoverThumb, + Activity: []primitive.ObjectID{}, + SeriesCover: series, + SeriesNum: len(series), + Via: req.Via, + Size: req.Size, + Rating: 0, + PlayCount: 0, + Coins: req.Coins, + LikeCount: 0, + CommentCount: 0, + ShareCount: 0, + Status: 0, + Location: lid, + FreeTime: req.FreeTime, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Width: req.Width, + Height: req.Height, + MDSID: req.MDSID, + VerticalCover: req.VerticalCover, + Ratio: req.Ratio, + Resolution: req.Resolution, + SeedLinkUrl: req.SeedLinkUrl, + SeedSize: req.SeedSize, + SeedPlayTime: req.SeedPlayTime, + RichText: req.RichText, + PreviewStart: req.PreviewStart, + } + if len(v.Tags) > 0 && req.Status == 1 { + v.Status = req.Status + v.ReviewAt = time.Now() + v.ReviewAccount = req.Account + } else if req.Status == vidmod.OnlinePass { + v.Status = req.Status + } + if vid, err = vidmod.InsertBase(v); err != nil { + return stderr.ErrDbInsertError, primitive.NilObjectID + } + for _, t := range tags { + if err = tagmod.IncreaseTagVidCount(t, 1); err != nil { + log.Error("SubmitBase error", log.Any("req", req), log.E(err)) + continue + } + } + return stderr.Success, vid +} + +// SendBaseInfo2AWs 上传基本文件信息到aws +func SendBaseInfo2AWs(req vidmod.WebSubmitReq) (code stderr.Code) { + info := vidmod.BaseVid4Aws{ + ID: req.SourceID, + Tags: req.Tags, + CheckSum: req.MD5, + Title: req.Title, + Actors: []string{req.Actor}, + AddedTime: time.Now().Format("2006-01-02"), + PlayTime: uint64(req.PlayTime), + Size: req.Size, + Filename: req.Filename, + Via: "q1", + Type: "sp", + } + params, _ := common.ToJsonM(info) + respBody := commod.Resp{} + if _, err := httputil.DefaultClientPostJsonWithResp(&respBody, webg.Conf.URL.FileInfoUrl, nil, params); err != nil { + log.Error("SendBaseInfo2AWs error", log.Any("req", req), log.E(err)) + return stderr.Failure + } + if respBody.Code != stderr.Success { + log.Error("SendBaseInfo2AWs status error", log.Any("req", req), log.Any("respBody.Code", respBody.Code)) + return stderr.Failure + } + return stderr.Success +} + +// MAP 别名 +type MAP map[string]interface{} + +func ExportVidList(cond MAP, modeList SortModeList) (buff bytes.Buffer, code stderr.Code) { + var err error + var secID primitive.ObjectID + secID, cond, _, err = dealVidListCond(cond) + if code = err.(stderr.Code); code != stderr.Success { + return + } + sort := make(primitive.D, len(modeList)) + for i, v := range modeList { + sort[i] = bson.E{Key: v.Field, Value: v.Value} + } + step := 900 + page := 1 + vModels, total, err := vidmod.GetvideoListByMapCond(cond, sort, uint64(page), uint64(step)) + if err != nil { + code = stderr.ErrDbQueryError + return + } + if total > 30000 { + code = stderr.ErrDbTooMuchRecords + return + } + allVideoInfos, err := vidhelp.EncodeVideoInfo(vModels, secID) + if err != nil { + code = stderr.ErrDbQueryError + return + } + wg := sync.WaitGroup{} + totalPages := int(total)/step + 1 + out := make(chan []*vidmod.WebVideo, totalPages) + for page++; page <= totalPages; page++ { + wg.Add(1) + common.GoParam(page, func(i int) { + defer wg.Done() + vModels, _, err = vidmod.GetvideoListByMapCond(cond, sort, uint64(i), uint64(step)) + if err != nil { + log.Error("vidmod.GetvideoListByMapCond", log.Any("cond", cond), log.Any("sort", sort), + log.Any("page", page), log.Any("step", step), log.E(err)) + return + } + infos, err := vidhelp.EncodeVideoInfo(vModels, secID) + if err != nil { + return + } + out <- infos + }) + } + common.Go(func() { + wg.Wait() + close(out) + }) + for vs := range out { + allVideoInfos = append(allVideoInfos, vs...) + } + excelFile := file.WriteStruct2Xlsx("帖子列表", allVideoInfos) + defer excelFile.Close() + _ = excelFile.DeleteSheet("Sheet1") + _, _ = excelFile.WriteTo(&buff) + return +} + +// GetVidList 获取视频列表 +func GetVidList(cond MAP, modeList SortModeList, page, size uint64, isPush bool) (code stderr.Code, data vidmod.ListResp) { + // 为了不影响查询结果,点赞率与成交率优先处理 + var purchaseRate, likeRate int + if item, ok := cond["purchaseRate"]; ok { + // 防止panic,断言未命中默认为类型默认值 + purchaseRate, _ = item.(int) + delete(cond, "purchaseRate") + } + if item, ok := cond["likeRate"]; ok { + likeRate, _ = item.(int) + delete(cond, "likeRate") + } + secID, cond, videoIDs, err := dealVidListCond(cond) + if code = err.(stderr.Code); code != stderr.Success { + return + } + s := make(primitive.D, len(modeList)) + for i, v := range modeList { + s[i] = bson.E{Key: v.Field, Value: v.Value} + } + //是否强推 + if isPush { + //查询强推视频 + pushList, _, _, err := pushmod.GetPushList(page, size, bson.M{}, bson.D{{Key: "createdAt", Value: -1}}) + if err != nil { + return stderr.ErrDbQueryError, data + } + if len(pushList) <= 0 { + return stderr.Success, data + } + vidIds := make([]primitive.ObjectID, len(pushList)) + for i, p := range pushList { + vidIds[i] = p.VideoID + } + cond["_id"] = bson.M{"$in": vidIds} + } + log.Info("---", log.Any("cond", cond), log.Any("sort", s)) + // 这里会打乱之前排序 + vModels, total, err := vidmod.GetvideoListByMapCond(cond, s, page, size) + if err != nil { + return stderr.ErrDbQueryError, data + } + // 因为查询数据库后数据会打乱,因此此处重新根据专题序号进行排序 + if !secID.IsZero() { + newVideos := make([]*vidmod.VideoModel, 0, len(videoIDs)) + for _, v := range videoIDs { + for _, vv := range vModels { + if v == vv.ID { + newVideos = append(newVideos, vv) + break + } + } + } + vModels = newVideos + } + infos, err := vidhelp.EncodeVideoInfo(vModels, secID) + if err != nil { + return stderr.ErrDbQueryError, data + } + // 当前搜索结果按照成交率排序,正序 + if purchaseRate == 1 { + sort.Slice(infos, func(i, j int) bool { + return infos[i].PurchaseRate < infos[j].PurchaseRate + }) + } + // 倒序 + if purchaseRate == -1 { + sort.Slice(infos, func(i, j int) bool { + return infos[i].PurchaseRate > infos[j].PurchaseRate + }) + } + // 当前搜索结果按照点击率排序,正序 + if likeRate == 1 { + sort.Slice(infos, func(i, j int) bool { + return infos[i].LikeRate < infos[j].LikeRate + }) + } + // 倒序 + if likeRate == -1 { + sort.Slice(infos, func(i, j int) bool { + return infos[i].LikeRate > infos[j].LikeRate + }) + } + return stderr.Success, vidmod.ListResp{VInfos: infos, Total: total} +} + +func dealVidListCond(cond MAP) (primitive.ObjectID, MAP, []primitive.ObjectID, error) { + var secID primitive.ObjectID + var err error + sectionID, ok := cond["sectionID"] + if ok { + secID, err = primitive.ObjectIDFromHex(sectionID.(string)) + if err != nil { + log.Error("primitive.ObjectIDFromHex", log.Any("sectionID", sectionID), log.E(err)) + return secID, cond, nil, stderr.ErrDbQueryError + } + var flag bool + isSorted, ok := cond["isSortedUnderModule"] + if ok { + flag = isSorted.(bool) + } + videoIDs, err := modulevidmod.GetBySectionID(secID, flag) + if err != nil { + return secID, cond, nil, stderr.ErrDbQueryError + } + cond["_id"] = bson.M{"$in": videoIDs} + delete(cond, "sectionID") + delete(cond, "isSortedUnderModule") + return secID, cond, videoIDs, stderr.Success + } + delete(cond, "sectionID") + delete(cond, "isSortedUnderModule") + return secID, cond, nil, stderr.Success +} + +func updateTagVideoCnt(oldTags, newTags []primitive.ObjectID) { + mOld := make(map[primitive.ObjectID]bool) + mNew := make(map[primitive.ObjectID]bool) + for _, v := range newTags { + mNew[v] = true + } + for _, v := range oldTags { + mOld[v] = true + } + for _, v := range newTags { + if !mOld[v] { + _ = tagmod.IncreaseTagVidCount(v, 1) + } + } + for _, v := range oldTags { + if !mNew[v] { + _ = tagmod.IncreaseTagVidCount(v, -1) + } + } +} + +func updatePublisherRewarded(id string, specReward int) { + info, err := vidmod.GetVideoInfo(id) + if err != nil { + return + } + _ = usermod.IncRewarded(info.PublisherID, decimal.NewFromInt(int64(specReward))) +} + +// UpdateVidInfo 更新视频信息 +func UpdateVidInfo(req vidmod.EditReq, manager string) (code stderr.Code, data interface{}) { + vidInfo, err := vidmod.GetVideoInfo(req.ID) + if err != nil { + log.Error("UpdateVidInfo error", log.Any("req", req), log.E(err)) + return stderr.CommentVideoInfoErr, nil + } + + // —— 上架相关时做敏感词校验 —— + // 触发条件: + // 1. status 切换到 上架(CheckPass=1) / 上架并免费(Free=3) + // 2. 当前已上架(1/3) 且本次更新涉及 Title/Content/RichText + // 校验文本: req 提供则用新值, 否则用 DB 现值(即"落库后"的文本快照) + if shouldCheckSensitiveOnUpdate(vidInfo, req) { + title := vidInfo.Title + content := vidInfo.Content + richText := vidInfo.RichText + if req.Title != nil { + title = *req.Title + } + if req.Content != nil { + content = *req.Content + } + if req.RichText != nil { + richText = *req.RichText + } + if err := CheckShelvingFields(vidInfo.ID, title, content, richText); err != nil { + return stderr.ContentSensitiveHit, err.Error() + } + } + + if req.Type == 2 { + if !vidInfo.ID.IsZero() && req.RichText != nil { + if _, err = vidmod.UpdateVideoByMapCond(vidInfo.ID, bson.M{"richText": req.RichText}); err != nil { + return stderr.ErrDbUpdateError, nil + } + return stderr.Success, nil + } + return stderr.ErrParamError, nil + } + + if req.Status != nil && *req.Status != vidInfo.Status { + if err = operationlogmod.Insert(operationlogmod.OperationLog{ + ID: primitive.NewObjectID(), + OperationType: constant.Media_Edit_Status, + BeforeContent: fmt.Sprintf("%v", vidInfo.Status), + AfterContent: fmt.Sprintf("%v", *req.Status), + CreatedUser: manager, + }); err != nil { + return stderr.ErrDbInsertError, nil + } + } + if req.Coins != nil && int64(*req.Coins) != vidInfo.Coins { + if err = operationlogmod.Insert(operationlogmod.OperationLog{ + ID: primitive.NewObjectID(), + OperationType: constant.Media_Edit_Price, + BeforeContent: fmt.Sprintf("%v", vidInfo.Coins), + AfterContent: fmt.Sprintf("%v", *req.Coins), + CreatedUser: manager, + }); err != nil { + return stderr.ErrDbInsertError, nil + } + } + if req.Title != nil && *req.Title != vidInfo.Title { + if err = operationlogmod.Insert(operationlogmod.OperationLog{ + ID: primitive.NewObjectID(), + OperationType: constant.Media_Edit_Content, + BeforeContent: fmt.Sprintf("%v", vidInfo.Title), + AfterContent: fmt.Sprintf("%v", *req.Title), + CreatedUser: manager, + }); err != nil { + return stderr.ErrDbInsertError, nil + } + } + oid, err := primitive.ObjectIDFromHex(req.ID) + if err != nil { + log.Error("UpdateVidInfo error", log.Any("req", req), log.E(err)) + return stderr.ErrParamError, nil + } + if req.FakeLikeCount != nil { + gt, err := vidmod.FakeLikeGteLike(req.ID, *req.FakeLikeCount) + if err != nil { + return stderr.ErrDbQueryError, nil + } + if !gt { + return stderr.ErrParamError, "fakelikecount need gte likecount." + } + } + oldTags, err := vidmod.GetVideoTag(req.ID) + if err != nil { + return stderr.ErrDbQueryError, nil + } + set := bson.M{} + if req.SeriesCover != nil { + sn := len(*req.SeriesCover) + req.SeriesNum = &sn + } + videoInfo, err := vidmod.GetVideoInfo(req.ID) + if err != nil { + return + } + if req.WorksSort != nil { + if *req.WorksSort != 0 && videoInfo.WorksSort != *req.WorksSort { + // 查询并修改重复排序 + err = vidmod.UpdateWorksSort(videoInfo.PublisherID, videoInfo.WorksSort, *req.WorksSort) + if err != nil { + return + } + } + } + if req.Type == 0 { + set, _ = common.ToBsonM(req.EditInfo) + } + newTags := []primitive.ObjectID{} + if len(req.Tags) > 0 { + newTags = vidhelp.AddTags(req.ID, req.Tags) + set["tags"] = newTags + } + if len(req.TagSort) > 0 { + set["tagSort"] = covertTagSort(req) + } + if req.CreatedAt != nil && len(*req.CreatedAt) != 0 { + createdAt := timeutil.StrTimeToTime(*req.CreatedAt) + set["createdAt"] = createdAt + } + if req.Status != nil && *req.Status == vidmod.CheckPass && *req.Status != vidInfo.Status { + set["reviewAt"] = time.Now() + } + if req.PublisherID != nil { + set["publisherID"] = req.PublisherID + } + if req.Content != nil { + set["content"] = req.Content + } + if req.PreviewURL != nil { + set["previewURL"] = *req.PreviewURL + } + if req.UpTag != nil { + set["upTag"] = *req.UpTag + } + if req.NewsType != nil { + set["newsType"] = req.NewsType + } + if req.TimeNodeList != nil { + set["timeNodeList"] = *req.TimeNodeList + } + //if req.LiaoBaTop != nil { + // set["liaoBaTop"] = *req.LiaoBaTop + //} + applyLiaoBaTopSort(set, req.LiaoBaTopSort) + if req.ActivityID != nil { + if *req.ActivityID == "" { + set["activityId"] = primitive.NilObjectID + } else { + activityId, err := primitive.ObjectIDFromHex(*req.ActivityID) + if err != nil { + log.Error(fmt.Sprintf("解析参赛作品活动ID异常:%v", err), log.Any("activityID:", *req.ActivityID)) + return stderr.ErrParamError, nil + } + set["activityId"] = activityId + } + } + if req.HappinessPlazaTop != nil { + set["happinessPlazaTop"] = req.HappinessPlazaTop + id, _ := primitive.ObjectIDFromHex(req.ID) + if *req.HappinessPlazaTop == 1 { + //插入幸福广场置顶 + imgTop := imagetopmod.ImageTopModel{ + ID: primitive.NilObjectID, + NewSort: 0, + NewEndTime: &time.Time{}, + RecommendSort: 0, + RecommendEndTime: &time.Time{}, + UpdatedAt: time.Now(), + } + imgTop.ImageId = id + //插入数据 + if err = imagetopmod.InsertOne(&imgTop); err != nil { + return stderr.ErrDbInsertError, nil + } + } else { + //删除幸福广场置顶 + if err = imagetopmod.DeleteOne(id); err != nil { + return stderr.ErrDbDeleteError, nil + } + } + } + if req.ShareSort != nil { + set["shareSort"] = *req.ShareSort + } + delete(set, "fakeRewarded") + if _, err = vidmod.UpdateVideoByMapCond(oid, set); err != nil { + return stderr.ErrDbUpdateError, nil + } + if req.NewsType != nil { + // 更新 + modulevidmod.UpdateNewsTypeByVideoID(oid, *req.NewsType) + } + updateTagVideoCnt(oldTags, newTags) + if req.FakeRewarded != nil { + updatePublisherRewarded(req.ID, *req.FakeRewarded) + _ = vidmod.IncSpecFakeRewarded(oid, decimal.NewFromInt(int64(*req.FakeRewarded))) + } + format := redisconst.VideoInfoKey() + s := fmt.Sprintf(format, videoInfo.ID.Hex()) + if _, err = webg.Redis.Del(s); err != nil { + return stderr.ErrDbUpdateError, nil + } + return stderr.Success, nil +} + +// applyLiaoBaTopSort only builds the update for the video currently being +// edited. Duplicate sort values are intentional and must not mutate another +// video that already uses the same value. +func applyLiaoBaTopSort(set bson.M, sortCode *int) { + if sortCode == nil { + return + } + set["liaoBaTop"] = *sortCode > 0 + set["liaoBaTopSort"] = *sortCode +} + +func covertTagSort(info vidmod.EditReq) bson.M { + set := make(bson.M, 10) + for _, v := range info.TagSort { + set[v.TagId] = v.SortCode + } + return set +} + +// DeleteManyByTTL 删除视频 被删除的视屏归属为officialUID,未审核或审核未通过的视屏通过TTL删除 +func DeleteManyByTTL(ids ...primitive.ObjectID) (code stderr.Code, data interface{}) { + delSetDoc := (&vidmod.SetDoc{}).SetPublisherID(constant.DeleteUID).SetDeleteAt(time.Now()).SetStatus(vidmod.CheckFailure) + if _, err := vidmod.ModifyMany(ids, *delSetDoc); err != nil { + return stderr.ErrDbUpdateError, nil + } + if err := modulevidmod.DeleteManyByVids(ids); err != nil { + return stderr.ErrDbDeleteError, nil + } + return stderr.Success, nil +} + +func BatchUpdateReco(req vidmod.BatchUpdateRecoRequest) error { + return vidmod.BatchUpdateReco(req.IDs, req.Reco) +} + +// UpdateVids 批量更新视频信息 +func UpdateVids(req vidmod.EditManyReq) (code stderr.Code, data interface{}) { + if len(req.IDs) == 0 { + return + } + oids := make([]primitive.ObjectID, 0, len(req.IDs)) + for _, id := range req.IDs { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Error("UpdateVids error", log.Any("req", req), log.E(err)) + continue + } + oids = append(oids, oid) + } + // 上架(UpStatus=1 上架 / 3 上架并免费)前置敏感词校验 + if req.UpStatus != nil && (*req.UpStatus == vidmod.CheckPass || *req.UpStatus == vidmod.Free) { + if err := CheckShelvingByIDs(oids); err != nil { + return stderr.ContentSensitiveHit, err.Error() + } + } + newTags := vidhelp.AddTags(req.IDs[0], req.Tags) + req.TagIds = append(req.TagIds, newTags...) + req.TagIds = common.Deduplication(req.TagIds) + var ( + count int64 + err error + ) + + if req.Field != "" { // 只更新某个状态 + count, err = vidmod.UpdateVideoBatch(oids, req.Field, req.Status) + if err != nil { + if err.Error() == "overLiaoBaLimit" { + return stderr.LiaoBaTopOverLimit, nil + } + return stderr.ErrDbUpdateError, nil + } + } else if req.OriginPublisherID != nil && req.PublisherID != nil { + filter := bson.M{"publisherID": *req.OriginPublisherID} + updateData := bson.M{ + "$set": bson.M{"publisherID": *req.PublisherID}, + } + // 进行视频转移 + err = vidmod.UpdateMany(filter, updateData) + if err != nil { + log.Error("帖子批量转移失败", + log.Any("OriginPublisherID", *req.OriginPublisherID), + log.Any("PublisherID", *req.PublisherID), + log.E(err), + ) + return stderr.ErrDbUpdateError, nil + } + } else { + // 更新tag或者价格 + var updater = bson.M{ + "updatedAt": time.Now(), + } + if req.Coins != nil { + updater["coins"] = req.Coins + } + if req.PublisherID != nil { + updater["publisherID"] = req.PublisherID + } + if req.UpStatus != nil { + updater["status"] = *req.UpStatus + } + if req.FreeTime != nil { + updater["freeTime"] = *req.FreeTime + } + if req.DownloadAllow != nil { + updater["downloadAllow"] = *req.DownloadAllow + } + // 这里不去判断tag存不存在,因为tag列表是从服务器拉下去的数据,选中上来的tag也是在数据中存在的 + if len(req.TagIds) > 0 { + updater["tags"] = req.TagIds + // 获取相关视频 + videoList, err := vidmod.GetVideoListByIDsNoStatus(oids) + if err != nil { + log.Error(fmt.Sprintf("UpdateVidTags vidmod.GetVideoListByIDsNoStatus err:%v, ids:%v", err, req.IDs)) + return stderr.ErrDbQueryError, nil + } + var tagMap = map[primitive.ObjectID]int{} + for _, v := range videoList { + for _, v := range v.Tags { + tagMap[v] = tagMap[v] - 1 + } + } + for _, tid := range req.TagIds { + tagMap[tid] = len(videoList) + } + // 修改旧tag与新tag的视频数量 + tagmod.IncTagVidCountByMap(tagMap) + } + count, err = vidmod.UpdateManyVideo(oids, updater) + if err != nil { + log.Error(fmt.Sprintf("UpdateVids vidmod.UpdateManyVideo err:%v, ids:%v", err, req.IDs)) + return stderr.ErrDbQueryError, nil + } + } + + return stderr.Success, vidmod.OperateResult{Count: count} +} + +// PassAndEditVids 批量审核更新更新视频信息 +func PassAndEditVids(req vidmod.PassManyReq, manager string) (code stderr.Code, data interface{}) { + // 上架(含直接通过 / 通过并免费)前置敏感词校验 + if req.Pass == vidmod.CheckPass || req.Pass == vidmod.Free { + ids := make([]string, 0, len(req.Infos)) + for _, info := range req.Infos { + ids = append(ids, info.ID) + } + if err := CheckShelvingByIDs(parseObjectIDs(ids)); err != nil { + return stderr.ContentSensitiveHit, err.Error() + } + } + var cnt int64 = 0 + var mergingCnt, convertingCnt, mergeErrorCnt, convertErrorCnt int64 + for _, info := range req.Infos { + oid, err := primitive.ObjectIDFromHex(info.ID) + if err != nil { + log.Error("UpdateVids error", log.Any("req", req), log.E(err)) + continue + } + //非图集才需要同步 + isCover, err := vidmod.IsNewsCover(info.ID) + if err != nil { + return stderr.ErrDbQueryError, nil + } + if req.Pass == 3 || req.Pass == 1 { + if !isCover { + if code := SyncFileFromFs(info.ID); code != stderr.UpLoadFileComplete { + switch code { + case stderr.FileMerging: + mergingCnt++ + case stderr.FileConverting: + convertingCnt++ + case stderr.ErrMergeFile: + mergeErrorCnt++ + case stderr.ErrConvertFile: + convertErrorCnt++ + } + continue + } + } + } + if req.Pass == 2 { + if !isCover { + _ = SyncFileFromFsForNoPassed(info.ID) + } + } + set := make(bson.M) + set["status"] = req.Pass + set["coins"] = info.Coins + if req.Pass == vidmod.Free { + set["status"] = vidmod.CheckPass + set["coins"] = 0 + } + now := time.Now() + set["reason"] = info.Reason + set["freeTime"] = info.FreeTime + set["updatedAt"] = now + set["reviewAt"] = now + set["reviewAccount"] = manager + // 查询视频信息 + videoInfo, err := vidmod.GetVideoInfo(oid.Hex()) + if err != nil { + return stderr.ErrDbQueryError, nil + } + if err = operationlogmod.Insert(operationlogmod.OperationLog{ + ID: primitive.NewObjectID(), + OperationType: constant.Media_Edit_Status, + BeforeContent: fmt.Sprintf("%v", videoInfo.Status), + AfterContent: fmt.Sprintf("%v", req.Pass), + CreatedUser: manager, + }); err != nil { + return stderr.ErrDbInsertError, nil + } + if err = operationlogmod.Insert(operationlogmod.OperationLog{ + ID: primitive.NewObjectID(), + OperationType: constant.Media_Edit_Remark, + BeforeContent: fmt.Sprintf("%v", ""), + AfterContent: fmt.Sprintf("%v", info.Reason), + CreatedUser: manager, + }); err != nil { + return stderr.ErrDbInsertError, nil + } + if set["status"] == vidmod.CheckPass { + tagSort := make(bson.M, len(videoInfo.Tags)) + for _, v := range videoInfo.Tags { + tagSort[v.Hex()] = 0 + } + set["tagSort"] = tagSort + } + _, err = vidmod.UpdateVideoByMapCond(oid, set) + if err != nil { + continue + } + cnt++ + } + or := vidmod.OperateResult{Count: cnt, MergingCnt: mergingCnt, ConvertingCnt: convertingCnt, MerErrCnt: mergeErrorCnt, ConvertErrCnt: convertErrorCnt} + return stderr.Success, or +} + +// PassVid 通过审核 +func PassVid(req vidmod.BatchReq, manager string) (code stderr.Code, data interface{}) { + var mergingCnt, convertingCnt, mergeErrorCnt, convertErrorCnt int64 + if req.Pass != 1 && req.Pass != 2 && req.Pass != 3 { + log.Error("PassVid error", log.Any("req", req)) + return stderr.ErrParamError, nil + } + // 上架(含直接通过 / 通过并免费)前置敏感词校验 + if req.Pass == vidmod.CheckPass || req.Pass == vidmod.Free { + if err := CheckShelvingByIDs(parseObjectIDs(req.IDs)); err != nil { + return stderr.ContentSensitiveHit, err.Error() + } + } + oids := make([]primitive.ObjectID, 0, len(req.IDs)) + for _, id := range req.IDs { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Error("PassVid2 error", log.Any("req", req), log.E(err)) + continue + } + //非图集才需要同步 + info, err := vidmod.GetVideoInfo(id) + if err != nil { + return stderr.ErrDbQueryError, nil + } + if req.Pass == 3 || req.Pass == 1 { + if !info.IsCover() { + if code = SyncFileFromFs(id); code != stderr.UpLoadFileComplete { + switch code { + case stderr.FileMerging: + mergingCnt++ + case stderr.FileConverting: + convertingCnt++ + case stderr.ErrMergeFile: + mergeErrorCnt++ + case stderr.ErrConvertFile: + convertErrorCnt++ + } + log.Info(fmt.Sprintf("check vid pass code[%v]", code)) + continue + } + } + } + if req.Pass == 2 { + if !info.IsCover() { + _ = SyncFileFromFsForNoPassed(id) + } + } + //此行代码 明白意思 再移动顺序,没有明白则,不要随便移动 + oids = append(oids, oid) + + tagSort := make(bson.M, len(info.Tags)) + for _, v := range info.Tags { + tagSort[v.Hex()] = 0 + } + _, err = vidmod.PassVids(oid, req.Pass, req.Reason, manager, tagSort) + if err != nil { + return stderr.ErrDbUpdateError, nil + } + // 完成发布任务 + _ = taskser.CompleteDailyTask(nil, info.PublisherID, dailytaskmod.DailyPublish) + } + if len(oids) > 0 { + err := operationlogmod.Insert(operationlogmod.OperationLog{ + ID: primitive.NewObjectID(), + OperationType: constant.Media_Edit_Status, + //BeforeContent: fmt.Sprintf("%v", videoInfo.Status), + AfterContent: fmt.Sprintf("%v", req.Pass), + CreatedUser: manager, + }) + if err != nil { + return stderr.ErrDbInsertError, nil + } + err = operationlogmod.Insert(operationlogmod.OperationLog{ + ID: primitive.NewObjectID(), + OperationType: constant.Media_Edit_Remark, + BeforeContent: fmt.Sprintf("%v", ""), + AfterContent: fmt.Sprintf("%v", req.Reason), + CreatedUser: manager, + }) + if err != nil { + return stderr.ErrDbInsertError, nil + } + } + or := vidmod.OperateResult{Count: 1, MergingCnt: mergingCnt, ConvertingCnt: convertingCnt, MerErrCnt: mergeErrorCnt, ConvertErrCnt: convertErrorCnt} + return stderr.Success, or +} + +// PushVid 推送视频 +func PushVid(req pushmod.AddReq) (code stderr.Code, data interface{}) { + oid, err := primitive.ObjectIDFromHex(req.VideoID) + if err != nil { + log.Error("PushVid error", log.Any("req", req), log.E(err)) + return stderr.ErrParamError, nil + } + doc := pushmod.PushModel{ + VideoID: oid, + Title: req.Title, + Type: vidmod.SP, + CreatedAt: time.Now(), + } + if err = pushmod.InsertOne(doc); err != nil { + return stderr.ErrDbInsertError, "" + } + return stderr.Success, nil +} + +// PushVidList 获取推送列表 +func PushVidList(req pushmod.ListReq) (code stderr.Code, data interface{}) { + cond := bson.M{} + if len(req.VideoID) != 0 { + oid, err := primitive.ObjectIDFromHex(req.VideoID) + if err != nil { + log.Error("PushVidList error", log.Any("req", req), log.E(err)) + return stderr.ErrParamError, nil + } + cond = bson.M{"videoID": oid} + } + sort := bson.D{{Key: "createdAt", Value: -1}} + infos, _, total, err := pushmod.GetPushList(req.PageNumber, req.PageSize, cond, sort) + if err != nil { + return stderr.ErrDbQueryError, nil + } + return stderr.Success, pushmod.ListResp{VInfos: infos, Total: total} +} + +// DeletePushVid 删除推送视频 +func DeletePushVid(ids []string) (code stderr.Code, data interface{}) { + oids := make([]primitive.ObjectID, 0, len(ids)) + for _, id := range ids { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Error("DeletePushVid error", log.Any("ids", ids), log.E(err)) + continue + } + oids = append(oids, oid) + } + if oids == nil { + return stderr.ErrParamError, nil + } + cnt, err := pushmod.Delete(bson.M{"videoID": bson.M{"$in": oids}}) + if err != nil { + return stderr.ErrDbDeleteError, "" + } + return stderr.Success, pushmod.OperateResult{Count: cnt} +} + +// SyncFileFromFs 从文件服务器同步文件信息未审核通过 +func SyncFileFromFsForNoPassed(vID string) (code stderr.Code) { + v, _ := vidmod.GetVideoInfo(vID) + resp, err := updownloadser.PullFileInfo(v.SourceID) + if err != nil { + return stderr.ErrConnectToFs + } + var resolution, actor, direction, quality string + oid, _ := primitive.ObjectIDFromHex(resp.Data.ID) + sourceID := resp.Data.ID + playTime := resp.Data.PlayTime + freeTime := vidmod.GetFreeTime(resp.Data.PlayTime) + if v.FreeTime != 0 && freeTime != v.FreeTime { + freeTime = v.FreeTime + } + seriesCover := resp.Data.CoverImg + width := resp.Data.Width + height := resp.Data.Height + if width != 0 && height != 0 { + resolution = strconv.FormatInt(int64(resp.Data.Width), 10) + "*" + strconv.FormatInt(int64(resp.Data.Height), 10) + identifi := common.IdentifiVideoInfo(int64(resp.Data.Width), int64(resp.Data.Height)) + direction = string(identifi.Direction) + quality = string(identifi.Quality) + } + md5 := resp.Data.CheckSum + if len(resp.Data.Actors) > 0 { + actor = strings.Join(resp.Data.Actors, ",") + } + size := resp.Data.Size + filename := resp.Data.Filename + via := resp.Data.Via + ratio := resp.Data.Ratio + update := vidmod.WebVideoUpdateDoc{ + ID: oid, + SourceID: &sourceID, + PlayTime: &playTime, + FreeTime: &freeTime, + SeriesCover: &seriesCover, + Resolution: &resolution, + Width: &width, + Height: &height, + MD5: &md5, + Actor: &actor, + Size: &size, + Filename: &filename, + Via: &via, + Ratio: &ratio, + Direction: &direction, + Quality: &quality, + } + if resp.Code == stderr.UpLoadFileComplete { + if v.Cover == "" { + cover := resp.Data.FieldNameFs + "-1.jpg" + update.Cover = &cover + } + if v.CoverThumb == "" { + coverThumb := resp.Data.FieldNameFs + "-2.jpg" + update.CoverThumb = &coverThumb + } + } + _, _ = vidmod.UpdateVideoResolutionPlayTime(v.ID, update) + return resp.Code +} + +// SyncFileFromFs 从文件服务器同步文件信息 +func SyncFileFromFs(vID string) (code stderr.Code) { + v, _ := vidmod.GetVideoInfo(vID) + resp, err := updownloadser.PullFileInfo(v.SourceID) + if err != nil { + return stderr.ErrConnectToFs + } + if resp.Code == stderr.UpLoadFileComplete { + oid, _ := primitive.ObjectIDFromHex(resp.Data.ID) + sourceID := resp.Data.ID + playTime := resp.Data.PlayTime + freeTime := vidmod.GetFreeTime(resp.Data.PlayTime) + if v.FreeTime != 0 && freeTime != v.FreeTime { + freeTime = v.FreeTime + } + seriesCover := resp.Data.CoverImg + resolution := strconv.FormatInt(int64(resp.Data.Width), 10) + "*" + strconv.FormatInt(int64(resp.Data.Height), 10) + width := resp.Data.Width + height := resp.Data.Height + identifi := common.IdentifiVideoInfo(int64(resp.Data.Width), int64(resp.Data.Height)) + direction := string(identifi.Direction) + quality := string(identifi.Quality) + md5 := resp.Data.CheckSum + actor := strings.Join(resp.Data.Actors, ",") + size := resp.Data.Size + filename := resp.Data.Filename + via := resp.Data.Via + ratio := resp.Data.Ratio + update := vidmod.WebVideoUpdateDoc{ + ID: oid, + SourceID: &sourceID, + PlayTime: &playTime, + FreeTime: &freeTime, + VideoCover: &seriesCover, + Resolution: &resolution, + Width: &width, + Height: &height, + MD5: &md5, + Actor: &actor, + Size: &size, + Filename: &filename, + Via: &via, + Ratio: &ratio, + Direction: &direction, + Quality: &quality, + } + if v.Cover == "" { + cover := resp.Data.FieldNameFs + "-1.jpg" + update.Cover = &cover + } + if v.CoverThumb == "" { + coverThumb := resp.Data.FieldNameFs + "-2.jpg" + update.CoverThumb = &coverThumb + } + if v.SeriesCover == nil || len(v.SeriesCover) <= 0 { + update.SeriesCover = &seriesCover + } + _, _ = vidmod.UpdateVideoResolutionPlayTime(v.ID, update) + } + return resp.Code +} + +// SyncBaseInfoFromFs 从文件服务器同步基本信息 +func SyncBaseInfoFromFs(vID string) (code stderr.Code) { + oid, _ := primitive.ObjectIDFromHex(vID) + resp, err := updownloadser.PullFileInfo(vID) + if err != nil { + return stderr.ErrConnectToFs + } + if resp.Code == stderr.UpLoadFileComplete { + update := vidmod.WebSyncServerFileEdit{ + PlayTime: resp.Data.PlayTime, + Resolution: strconv.FormatInt(int64(resp.Data.Width), 10) + "*" + strconv.FormatInt(int64(resp.Data.Height), 10), + MD5: resp.Data.CheckSum, + Size: resp.Data.Size, + Ratio: resp.Data.Ratio, + } + set, _ := common.ToBsonM(update) + if _, err = vidmod.UpdateVideoByMapCond(oid, set); err != nil { + return stderr.Failure + } + } + return resp.Code +} + +// 查看视频转码状态 +func ViewVideoFromFs(id string) (code stderr.Code, data interface{}) { + resp, err := updownloadser.PullFileInfo(id) + if err != nil { + return stderr.ErrConnectToFs, nil + } + back := make(map[string]interface{}, 10) + back["playTime"] = resp.Data.PlayTime + back["size"] = resp.Data.Size + back["width"] = resp.Data.Width + back["height"] = resp.Data.Height + back["status"] = resp.Data.Status + back["ratio"] = resp.Data.Ratio + fileName := resp.Data.Filename + fieldNameFs := resp.Data.FieldNameFs + back["fileName"] = filepath.Join(filepath.Dir(fieldNameFs), fileName) + switch resp.Data.Status { + case vidmod.UploadLoadingToFs: + back["statusDesc"] = "正在上传中" + case vidmod.UploadCompleted: + back["statusDesc"] = "上传完成" + case vidmod.FileUploadError: + back["statusDesc"] = "上传错误" + case vidmod.Merging: + back["statusDesc"] = "正在合并中" + case vidmod.MergeCompleted: + back["statusDesc"] = "合并完成" + case vidmod.MergeError: + back["statusDesc"] = "合并失败" + case vidmod.Converting: + back["statusDesc"] = "正在转码中" + case vidmod.ConvertError: + back["statusDesc"] = "转码失败" + case vidmod.ConvertCompleted, vidmod.Completed: + back["statusDesc"] = "完成状态" + } + return stderr.Success, back +} + +type ( + GetVideoListByTagReq struct { + TagID string `form:"tagID" json:"tagID" binding:"required"` // 标签ID + SortType int `form:"sortType" json:"sortType" binding:"required"` // 排序类型 1:最新上架 2:最多收藏 3:本月最热 4:最多观看 + NewsType string `json:"newsType" form:"newsType" binding:"-"` // 帖子类型: SP、SHORT、COVER + commod.Page + } + GetVideoListByTagRep struct { + List []*vidmod.VideoInfo `json:"list"` // 视频列表 + HasNext bool `json:"hasNext"` // 是否还有下一页 + } +) + +// GetVideoListByTag 通过标签id获取视频列表信息 +func (req *GetVideoListByTagReq) GetVideoListByTag() (rep GetVideoListByTagRep, err error) { + excludedModuleIDs, err := excludedTagSearchModuleIDs() + if err != nil { + return rep, err + } + redisKey := tagSearchCacheKey( + redisconst.GetTagSortVideoKey(req.TagID, req.NewsType, req.SortType, req.Skip(), req.Limit()), + excludedModuleIDs, + ) + s, err := appg.Redis.Get(redisKey) + if err != nil { + log.Error("GetVideoListByTag redis get error", log.E(err), log.Any("req", req)) + } + if s != nil && *s != "" { + err = json.Unmarshal([]byte(*s), &rep) + if err == nil { + return rep, nil + } + log.Error("GetVideoListByTag json unmarshal error", log.E(err), log.Any("val", *s)) + } + // -------- + tid, _ := primitive.ObjectIDFromHex(req.TagID) + filter := bson.M{ + "tags": tid, + "status": 1, //状态,0 未审核 1通过 2审核失败 3视为免费 默认为0 + } + applyTagSearchVisibilityFilter(filter, excludedModuleIDs) + + if req.NewsType != "" { + filter["newsType"] = req.NewsType + } + + // 本项目用到以下3个 + //1: 最新:按最新上架时间 + //2: 收藏:按总收藏量排序 + //4: 观看:播放量最多的数据 + var sortCond bson.D + switch req.SortType { + case 2: + sortCond = bson.D{{Key: "collectCount", Value: -1}, {Key: "reviewAt", Value: -1}} + case 3: + filter["reviewAt"] = bson.M{"$gte": time.Now().AddDate(0, -3, 0)} + sortCond = bson.D{{Key: "likeCount", Value: -1}, {Key: "reviewAt", Value: -1}} + case 4: + sortCond = bson.D{{"playCount", -1}, {Key: "reviewAt", Value: -1}} + case 5: + sortCond = bson.D{{Key: "hot", Value: -1}, {Key: "reviewAt", Value: -1}} + case 1: // 最新 + fallthrough + default: + sortCond = bson.D{{Key: "reviewAt", Value: -1}} + } + + opt := options.Find(). + SetSkip(int64(req.Skip())). + SetLimit(int64(req.Limit())). + SetProjection(bson.M{"chosenDate": 0}). + SetSort(sortCond) + + out, next, err := vidmod.FindList(filter, opt) + if err != nil { + return rep, err + } + + rep.HasNext = next + rep.List = vidhelpser.EncodeVideoInfoNoUID(out) + + go func() { + b, _ := json.Marshal(rep) + _ = appg.Redis.Set(redisKey, string(b), redisconst.GetTagSortVideoExpire()) + }() + + return rep, nil +} + +// BatchAuditVidTimeOnlineInfo 批量新增自动上架视频信息 +func BatchAuditVidTimeOnlineInfo(req vidtimeonlinemod.BatchAuditReq, price int, manager string) (code stderr.Code, data interface{}) { + if len(req.Vids) > 20 { + return stderr.BatchAuditLengthTooLong, nil + } + // 定时上架前置敏感词校验 + if err := CheckShelvingByIDs(req.Vids); err != nil { + return stderr.ContentSensitiveHit, err.Error() + } + //now := time.Now() + //if !req.OnlineTime.After(now) { + // req.OnlineTime = now + //} + // 获取专题 + section, err := modulesectionmod.GetBySectionByID(req.SectionID) + if err != nil { + return stderr.ErrDbQueryError, nil + } + var adds []vidtimeonlinemod.VidTimeOnlineModel + var updateIds []primitive.ObjectID + var reErr stderr.Code + var vIds []primitive.ObjectID + reErr = stderr.Success + for _, id := range req.Vids { + vid, _ := primitive.ObjectIDFromHex(id.Hex()) + //查询视频信息 + vidInfo, err := vidmod.FindOneByFilter(bson.M{"_id": vid}) + if err != nil { + log.Error("AddVidTimeOnlineInfo vidmod.FindOneByFilter error", log.Any("req", req), log.Any("err", err)) + reErr = stderr.ErrDbQueryError + break + } + if vidInfo == nil || vidInfo.ID.IsZero() { + reErr = stderr.VidNotExists + break + } + + // 取消限制长视频 + //if vidInfo.PlayTime > 300 && vidInfo.PublisherID > 150000 { + // return stderr.OnlyOfficialVideo, nil + //} + + // 自动上架支持类型短视频、帖子、长视频 + if vidInfo.NewsType != vidmod.SP && vidInfo.NewsType != vidmod.COVER && vidInfo.NewsType != vidmod.SHORT { + reErr = stderr.VideoTypeDissatisfy + return reErr, nil + } + if len(vidInfo.Tags) <= 0 { + return stderr.VideoTagNoExists, nil + } + //查询上传用户信息 + publisherInfo, err1 := usermod.FindUserByUID(vidInfo.PublisherID) + if err1 != nil { + log.Error("AddVidTimeOnlineInfo usermod.FindUserByUID error", log.Any("req", req), log.Any("err", err), log.Any("publisherID", vidInfo.PublisherID)) + //reErr = stderr.ErrDbQueryError + continue + } + // 查询该帖子是否已经在定时上架列表,如果存在则更新 + videos, err := vidtimeonlinemod.GetByVidIds([]primitive.ObjectID{vidInfo.ID}) + if err != nil { + return stderr.ErrDbQueryError, nil + } + vIds = append(vIds, vidInfo.ID) + if len(videos) > 0 { + // 不需要添加只需要修改上线时间 + updateIds = append(updateIds, vidInfo.ID) + } else { + add := vidtimeonlinemod.VidTimeOnlineModel{ + ID: primitive.NewObjectID(), + PublisherID: vidInfo.PublisherID, + Portrait: publisherInfo.Portrait, + VidType: vidInfo.NewsType, + VidId: vidInfo.ID, + VidTitle: vidInfo.Title, + Cover: vidInfo.Cover, + CreatedAt: time.Now(), + ReviewAccount: manager, + OnlineTime: req.OnlineTime, + SectionId: req.SectionID, + } + if vidInfo.NewsType == vidmod.SP || vidInfo.NewsType == vidmod.SHORT { + add.SourceURL = vidInfo.SourceURL + } else { + add.SeriesCover = vidInfo.SeriesCover + } + adds = append(adds, add) + } + reErr = stderr.Success + } + if reErr != stderr.Success { + return reErr, nil + } + if len(adds) > 0 { + err := vidtimeonlinemod.InsertMany(adds) + if err != nil { + log.Error("AddVidTimeOnlineInfo vidTimeOnlineMod InsertMany error", log.Any("err", err)) + return stderr.ErrDbInsertError, nil + } + } + if len(updateIds) > 0 { + updateData := bson.M{ + "onlineTime": req.OnlineTime, + } + if !section.ID.IsZero() { + updateData["sectionId"] = section.ID + } + err := vidtimeonlinemod.UpdateMany(updateIds, updateData) + if err != nil { + log.Error("AddVidTimeOnlineInfo vidTimeOnlineMod UpdateMany error", log.Any("err", err)) + return stderr.ErrDbInsertError, nil + } + } + if len(vIds) > 0 { + // 批量更新视频信息 + update := bson.M{} + update["updatedAt"] = time.Now() + update["status"] = vidmod.OnlinePass // 定时贴上架标记 + applyScheduledVideoModuleID(update, section.SubModuleID) + if price >= 0 { + update["coins"] = price + } + result, err := vidmod.UpdateManyVideo(vIds, update) + if err != nil { + return stderr.Failure, err + } + if result == 0 { + return stderr.Failure, errors.New("video update failed") + } + } + return stderr.Success, nil +} + +func applyScheduledVideoModuleID(update bson.M, moduleID primitive.ObjectID) { + if !moduleID.IsZero() { + update["mId"] = moduleID.Hex() + } +} + +// GetTimeOnlineVidList 获取定时上线视频列表 +func GetTimeOnlineVidList(req *vidtimeonlinemod.ListReq) (code stderr.Code, data interface{}) { + var err error + cond := bson.M{} + res := gin.H{"vidModels": "", "total": 0} + if req.UID > 0 { + cond["publisherID"] = req.UID + } + if req.VidId != "" { + vid, _ := primitive.ObjectIDFromHex(req.VidId) + cond["vidId"] = vid + } + + if req.Tag != nil { + id, err := tagmod.GetTagIDByName(*req.Tag) + if err != nil { + return stderr.ErrDbQueryError, res + } + cond["tags"] = id + } + + vidModels, total, err := vidtimeonlinemod.GetAllByPage(cond, req.Page) + if err != nil { + return stderr.ErrDbQueryError, res + } + var ( + tags []primitive.ObjectID + resp []vidtimeonlinemod.VidTimeOnlineModelResp + ) + + if len(vidModels) > 0 { + for _, v := range vidModels { + tags = append(tags, v.Tags...) + } + + } else { + return stderr.Success, res + } + mTags := vidhelp.GetTagsByIDs2Map(tags) + + for _, v := range vidModels { + t := vidhelp.GetPartTags(v.Tags, mTags) + resp = append(resp, vidtimeonlinemod.VidTimeOnlineModelResp{ + ID: v.ID, + Tags: t, + VidType: v.VidType, + PublisherID: v.PublisherID, + Portrait: v.Portrait, + VidTitle: v.VidTitle, + VidId: v.VidId, + OnlineSort: v.OnlineSort, + SourceURL: v.SourceURL, + Cover: v.Cover, + SeriesCover: v.SeriesCover, + OnlineTime: v.OnlineTime, + CreatedAt: v.CreatedAt, + ReviewAccount: v.ReviewAccount, + SectionId: v.SectionId, + }) + } + + res["total"] = total + res["vidModels"] = resp + return stderr.Success, res +} + +// DeleteTimeOnlineVid 删除定时上线视频 +func DeleteTimeOnlineVid(ids []primitive.ObjectID) (code stderr.Code) { + list, err := vidtimeonlinemod.GetByVidIds(ids) + if err != nil { + return stderr.ErrDbQueryError + } + vids := []primitive.ObjectID{} + for _, v := range list { + vids = append(vids, v.VidId) + } + err = vidtimeonlinemod.DeleteByIds(ids) + if err != nil { + return stderr.ErrDbDeleteError + } + // 把帖子状态更新为下架 + err = vidmod.UpdateMany(bson.M{"_id": bson.M{"$in": vids}}, bson.M{"$set": bson.M{"status": vidmod.CheckFailure}}) + if err != nil { + return stderr.ErrDbDeleteError + } + return stderr.Success + +} + +// UpdateVidNewsType 批量修改帖子类型 +func UpdateVidNewsType(req vidmod.BatchUpdateVidNewsTypeReq) (code stderr.Code) { + newsTypeList := []string{ + vidmod.SP, + vidmod.SHORT, + vidmod.PIC, + vidmod.COVER, + vidmod.AD_COVER, + vidmod.AD_SP, + vidmod.SEED_LINK, + } + if !slice.Contains(newsTypeList, req.NewsType) { + return stderr.ErrParamError + } + err := vidmod.UpdateVideosByMapCond(req.Ids, bson.M{ + "newsType": req.NewsType, + }) + if err != nil { + log.Error("UpdateVideosByMapCond error", log.Any("req", req), log.E(err)) + } + return stderr.Success +} + +// UpdateVidTags 批量更新视频信息 +func UpdateVidTags(req vidmod.EditVideTagsReq) (code stderr.Code) { + if len(req.Ids) <= 0 || len(req.Tags) <= 0 { + return stderr.ErrParamError + } + + ids := make([]primitive.ObjectID, 0, len(req.Ids)) + for _, id := range req.Ids { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Error("UpdateVids error", log.Any("req", req), log.E(err)) + continue + } + ids = append(ids, oid) + } + + // 获取相关视频 + videoList, err := vidmod.GetVideoListByIDsNoStatus(ids) + if err != nil { + log.Error(fmt.Sprintf("UpdateVidTags vidmod.GetVideoListByIDsNoStatus err:%v, ids:%v", err, req.Ids)) + return + } + var newTags []primitive.ObjectID + var oldTags []primitive.ObjectID + update := bson.M{"updatedAt": time.Now()} + + if req.Tags != nil { + newTags = vidhelp.AddTags(req.Ids[0], req.Tags) + } + + for _, v := range videoList { + oldTags = v.Tags + // 获取旧的tags + if req.Overwrite { + update["tags"] = newTags + } else { + update["tags"] = sli.RemoveRep(append(v.Tags, newTags...)) + } + + // 修改视频tag + _, err = vidmod.UpdateOneByID(v.ID, update) + if err != nil { + log.Error(fmt.Sprintf("UpdateVidTags vidmod.UpdateOneByID err:%v, id:%v, update: %v", err, v.ID, update)) + continue + } + // 修改旧tag与新tag的视频数量 + updateTagVideoCnt(oldTags, newTags) + } + return stderr.Success +} diff --git a/web/service/vipcardexperimentser/service.go b/web/service/vipcardexperimentser/service.go new file mode 100644 index 0000000..418c32e --- /dev/null +++ b/web/service/vipcardexperimentser/service.go @@ -0,0 +1,167 @@ +package vipcardexperimentser + +import ( + "fmt" + "strings" + "time" + + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/productmod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/vipcardexperimentmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type DisableRequest struct { + ExperimentID string `json:"experimentId" binding:"required"` +} + +type StatisticsResponse struct { + Experiment *vipcardexperimentmod.Experiment `json:"experiment"` + Events []vipcardexperimentmod.EventStat `json:"events"` + ProductImpressions []vipcardexperimentmod.ProductImpressionStat `json:"productImpressions"` + Orders []rchgordmod.VIPExperimentOrderStat `json:"orders"` +} + +func Current(now time.Time) (*vipcardexperimentmod.Experiment, error) { + return vipcardexperimentmod.Current(now) +} + +func Publish(experiment *vipcardexperimentmod.Experiment, operator string) error { + if experiment == nil { + return fmt.Errorf("experiment is required") + } + experiment.Normalize() + if err := experiment.Validate(); err != nil { + return err + } + now := time.Now() + if err := validatePublicationWindow(experiment, now); err != nil { + return err + } + productIDs := uniqueProductIDs(experiment.VariantA.ProductIDs, experiment.VariantB.ProductIDs) + products, err := productmod.FindByProductIDs(productIDs) + if err != nil { + return err + } + enabled := make(map[primitive.ObjectID]struct{}, len(products)) + for _, product := range products { + if product.Status { + enabled[product.ID] = struct{}{} + } + } + for _, productID := range productIDs { + if _, ok := enabled[productID]; !ok { + return fmt.Errorf("product is missing or disabled: %s", productID.Hex()) + } + } + return vipcardexperimentmod.Publish(experiment, operator) +} + +func validatePublicationWindow(experiment *vipcardexperimentmod.Experiment, now time.Time) error { + if experiment.StartAt != nil && experiment.StartAt.After(now.Add(time.Minute)) { + return fmt.Errorf("startAt cannot be in the future; publish starts the experiment immediately") + } + if experiment.EndAt != nil && !experiment.EndAt.After(now) { + return fmt.Errorf("endAt must be later than the current time") + } + return nil +} + +func Disable(experimentID, operator string) error { + experimentID = strings.TrimSpace(experimentID) + if experimentID == "" { + return fmt.Errorf("experimentId is required") + } + updated, err := vipcardexperimentmod.Disable(experimentID, operator) + if err != nil { + return err + } + if !updated { + return fmt.Errorf("active experiment not found") + } + return nil +} + +func Statistics(experimentID string) (StatisticsResponse, error) { + response := StatisticsResponse{ + Events: make([]vipcardexperimentmod.EventStat, 0), + ProductImpressions: make([]vipcardexperimentmod.ProductImpressionStat, 0), + Orders: make([]rchgordmod.VIPExperimentOrderStat, 0), + } + experimentID = strings.TrimSpace(experimentID) + if experimentID == "" { + return response, fmt.Errorf("experimentId is required") + } + experiment, err := vipcardexperimentmod.FindByExperimentID(experimentID) + if err != nil { + return response, err + } + if experiment == nil { + return response, fmt.Errorf("experiment not found") + } + response.Experiment = experiment + response.Events, response.ProductImpressions, err = vipcardexperimentmod.EventStatistics(experimentID) + if err != nil { + return response, err + } + response.Orders, err = rchgordmod.VIPExperimentOrderStatistics(experimentID) + if err != nil { + return response, err + } + goldOrders, err := prdcthsomod.VIPExperimentGoldOrderStatistics(experimentID) + if err != nil { + return response, err + } + response.Orders = mergeOrderStatistics(response.Orders, goldOrders) + return response, nil +} + +func mergeOrderStatistics( + cashOrders []rchgordmod.VIPExperimentOrderStat, + goldOrders []prdcthsomod.VIPExperimentGoldOrderStat, +) []rchgordmod.VIPExperimentOrderStat { + result := append(make([]rchgordmod.VIPExperimentOrderStat, 0, len(cashOrders)+len(goldOrders)), cashOrders...) + type orderKey struct { + variant string + productID primitive.ObjectID + } + positions := make(map[orderKey]int, len(result)+len(goldOrders)) + for i := range result { + positions[orderKey{variant: result[i].Variant, productID: result[i].ProductID}] = i + } + for _, goldOrder := range goldOrders { + key := orderKey{variant: goldOrder.Variant, productID: goldOrder.ProductID} + position, exists := positions[key] + if !exists { + position = len(result) + positions[key] = position + result = append(result, rchgordmod.VIPExperimentOrderStat{ + Variant: goldOrder.Variant, + ProductID: goldOrder.ProductID, + }) + } + result[position].GoldPaidOrders += goldOrder.GoldPaidOrders + result[position].GoldPaidAmount += goldOrder.GoldPaidAmount + } + return result +} + +func uniqueProductIDs(groups ...[]primitive.ObjectID) []primitive.ObjectID { + seen := make(map[primitive.ObjectID]struct{}) + result := make([]primitive.ObjectID, 0) + for _, ids := range groups { + for _, id := range ids { + if id.IsZero() { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + result = append(result, id) + } + } + return result +} diff --git a/web/service/vipcardexperimentser/service_test.go b/web/service/vipcardexperimentser/service_test.go new file mode 100644 index 0000000..68fa526 --- /dev/null +++ b/web/service/vipcardexperimentser/service_test.go @@ -0,0 +1,108 @@ +package vipcardexperimentser + +import ( + "strings" + "testing" + "time" + + "91porn-server/models/v/prdcthsomod" + "91porn-server/models/v/rchgordmod" + "91porn-server/models/v/vipcardexperimentmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestValidatePublicationWindow(t *testing.T) { + now := time.Now() + future := now.Add(2 * time.Minute) + if err := validatePublicationWindow( + &vipcardexperimentmod.Experiment{StartAt: &future}, + now, + ); err == nil { + t.Fatal("expected future startAt to fail") + } + + past := now.Add(-time.Minute) + if err := validatePublicationWindow( + &vipcardexperimentmod.Experiment{EndAt: &past}, + now, + ); err == nil { + t.Fatal("expected expired endAt to fail") + } + + end := now.Add(time.Hour) + if err := validatePublicationWindow( + &vipcardexperimentmod.Experiment{EndAt: &end}, + now, + ); err != nil { + t.Fatalf("valid publication window failed: %v", err) + } +} + +func TestMergeOrderStatisticsAddsGoldPurchases(t *testing.T) { + sharedProductID := primitive.NewObjectID() + goldOnlyProductID := primitive.NewObjectID() + result := mergeOrderStatistics( + []rchgordmod.VIPExperimentOrderStat{{ + Variant: vipcardexperimentmod.VariantA, + ProductID: sharedProductID, + CreatedOrders: 2, + PaidOrders: 1, + PaidAmount: 1000, + }}, + []prdcthsomod.VIPExperimentGoldOrderStat{ + { + Variant: vipcardexperimentmod.VariantA, + ProductID: sharedProductID, + GoldPaidOrders: 3, + GoldPaidAmount: 600, + }, + { + Variant: vipcardexperimentmod.VariantB, + ProductID: goldOnlyProductID, + GoldPaidOrders: 1, + GoldPaidAmount: 200, + }, + }, + ) + if len(result) != 2 { + t.Fatalf("merged order count = %d, want 2", len(result)) + } + if result[0].PaidOrders != 1 || result[0].PaidAmount != 1000 || + result[0].GoldPaidOrders != 3 || result[0].GoldPaidAmount != 600 { + t.Fatalf("merged shared product stats = %#v", result[0]) + } + if result[1].Variant != vipcardexperimentmod.VariantB || + result[1].ProductID != goldOnlyProductID || + result[1].GoldPaidOrders != 1 || result[1].GoldPaidAmount != 200 { + t.Fatalf("gold-only product stats = %#v", result[1]) + } +} + +func TestPublishRejectsInvalidUIConfigBeforeDatabaseAccess(t *testing.T) { + productID := primitive.NewObjectID() + experiment := &vipcardexperimentmod.Experiment{ + ExperimentID: "invalid-ui-config", + TrafficA: 50, + TrafficB: 50, + VariantA: vipcardexperimentmod.VariantConfig{ + DefaultProductID: productID, + ProductIDs: []primitive.ObjectID{productID}, + UIConfig: &vipcardexperimentmod.UIConfig{ + BadgeStyles: []vipcardexperimentmod.BadgeStyle{{ + BadgeType: vipcardexperimentmod.BadgeMostPopular, + BackgroundColor: "#FFF", + TextColor: "#FFFFFF", + }}, + }, + }, + VariantB: vipcardexperimentmod.VariantConfig{ + DefaultProductID: productID, + ProductIDs: []primitive.ObjectID{productID}, + }, + } + err := Publish(experiment, "tester") + if err == nil || !strings.Contains(err.Error(), "variantA.uiConfig") { + t.Fatalf("Publish() error = %v, want variantA.uiConfig error", err) + } +} diff --git a/web/service/withdrawser/bankcode.go b/web/service/withdrawser/bankcode.go new file mode 100644 index 0000000..d4963b3 --- /dev/null +++ b/web/service/withdrawser/bankcode.go @@ -0,0 +1,37 @@ +package withdrawser + +import ( + "91porn-server/common/httputil" + "91porn-server/common/log" + "net/http" + _ "strings" +) + +type GetBankInfoMsg struct { + CardType string `json:"cardType" ` //卡类型 + BankCode string `json:"bank"` //银行卡编码 +} + +// GetBankInfo 获取银行卡信息 +func GetBankInfo(bankcard string) string { + //请求地址 + var reqBankInfoUrl = "https://ccdcapi.alipay.com/validateAndCacheCardInfo.json?_input_charset=utf-8&cardNo=" + bankcard + "&cardBinCheck=true" + //返回实体 + var msg GetBankInfoMsg + code, err := httputil.DefaultClientPostJsonWithResp(&msg, reqBankInfoUrl, GetHeader(), nil) + if err != nil { + log.Warn("withdrawser GetBankInfo POSTJsonWithJResp err", log.Any("err", err), log.Any("bankcard", bankcard)) + return "" + } + if code != http.StatusOK { + log.Warn("withdrawser GetBankInfo code != http.StatusOK", log.Any("code", code), log.Any("bankcard", bankcard)) + return "" + } + return msg.BankCode +} + +// GetHeader 获取请求头信息 +func GetHeader() map[string]string { + header := make(map[string]string, 0) + return header +} diff --git a/web/service/withdrawser/refund.go b/web/service/withdrawser/refund.go new file mode 100644 index 0000000..08b14ce --- /dev/null +++ b/web/service/withdrawser/refund.go @@ -0,0 +1,88 @@ +package withdrawser + +import ( + "time" + + "91porn-server/common/log" + "91porn-server/common/maths" + "91porn-server/common/pageopt" + "91porn-server/models/v/wdordmod" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type HumanRefundQuery struct { + ID *primitive.ObjectID `json:"id" form:"id"` + DistrictCode *string `json:"districtCode" form:"districtCode"` + UID *uint64 `json:"uid" form:"uid"` +} + +type HumanRefundRecord struct { + ID primitive.ObjectID `json:"id"` //流水id + UID uint64 `json:"uid"` //用户id + Name string `json:"name"` //用户名 + Money float64 `json:"money"` //税前金额 + PayMoney float64 `json:"payMoney"` //提现金额 + PayType string `json:"payType"` //提现方式,alipay,bankcard + WithdrawType wdordmod.WithdrawTypes `json:"withdrawType"` //提现类型,0:代理提现 1:金币提现 2:商区提现 + ActName string `json:"actName"` //账户持有人 + Act string `json:"act"` //交易账户 + Status int `json:"status"` //1:未审核 2:转账中 3:已拒绝 4:未知错误 5:提现成功 6:提现失败 + StatusDesc string `json:"statusDesc"` //状态说明 + DistrictCode string `json:"districtCode"` //商区码 + CreatedAt time.Time `json:"createdAt"` //提现申请时间 +} + +type humanRefundPage struct { + Total int64 `json:"total"` + List []HumanRefundRecord `json:"list"` +} + +var unknownStatus = wdordmod.UNKNOWN + +func HumanRefundPage(skip, limit int64, query HumanRefundQuery) humanRefundPage { + mats := []pageopt.Matcher{ + (&wdordmod.IDMatch{ID: query.ID}).New(), + (&wdordmod.UIDMatch{UID: query.UID}).New(), + (&wdordmod.DistrictCodeMatch{DistrictCode: query.DistrictCode}).New(), + (&wdordmod.StatusMatch{Status: &unknownStatus}).New(), + } + list, _ := wdordmod.List(wdordmod.Sort_CreatedAt_n1, &skip, &limit, mats...) + records := make([]HumanRefundRecord, len(list)) + for i, v := range list { + records[i] = HumanRefundRecord{ + ID: v.ID, + UID: v.UID, + Name: v.Name, + Money: maths.ToFloat64_b2(float64(v.Money)), + PayMoney: maths.ToFloat64_b2(float64(v.PayMoney)), + PayType: v.PayType, + WithdrawType: v.WithdrawType, + ActName: v.ActName, + Act: v.Act, + Status: v.Status, + StatusDesc: v.StatusDesc, + DistrictCode: v.DistrictCode, + CreatedAt: v.CreatedAt, + } + } + count, _ := wdordmod.Count(mats...) + return humanRefundPage{count, records} +} + +func DealWithUnkownOrder(id string, remark string) error { + w, err := wdordmod.FindWithdrawOrderByIDAndStatus(id, wdordmod.UNKNOWN) //防重入 + if err != nil { + return err + } + now := time.Now() + set := wdordmod.EditSelector{ + Status: wdordmod.FAILURE, + StatusDesc: "[人工退款]" + remark, + CheckedAt: &now, + } + if err = refusedWirhdraw(w, set, "提现被平台拒绝"); err != nil { + log.Error("提现订单人工退款失败", log.E(err)) + } + return err +} diff --git a/web/service/withdrawser/wtihdraw.go b/web/service/withdrawser/wtihdraw.go new file mode 100644 index 0000000..168f100 --- /dev/null +++ b/web/service/withdrawser/wtihdraw.go @@ -0,0 +1,471 @@ +package withdrawser + +import ( + "91porn-server/app/service/messageser" + "91porn-server/common/stderr" + "errors" + "fmt" + "strconv" + "time" + + "91porn-server/app/appg" + "91porn-server/common" + "91porn-server/common/constant/redisconst" + "91porn-server/common/db" + "91porn-server/common/log" + "91porn-server/common/rchgutil" + "91porn-server/common/redis" + "91porn-server/models/v/txnactmod" + "91porn-server/models/v/txnmod" + "91porn-server/models/v/walletmod" + "91porn-server/models/v/wdchannmod" + "91porn-server/models/v/wdordmod" + "91porn-server/models/v/wdtaxmod" + "91porn-server/skd/skdg" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// ExchangeAllow 提现通过 +func ExchangeAllow(id string, usdtRate float64, operator string) (err error) { + // 查询订单信息 + withdrawOrder, ec := wdordmod.FindWithdrawOrderByID(id) + if ec != nil { + return ec + } + now := time.Now() + set := wdordmod.EditSelector{ + CheckedAt: &now, + ProgressAt: &now, + Status: wdordmod.PROCESSING, + StatusDesc: wdordmod.PROCESSINGError, + Operator: &operator, + } + content := "提现审核已通过,预计1个工作日内到账。" + // 特殊处理usdt提现审核 + if usdtRate > 0 { + if withdrawOrder.PayType != wdordmod.TypeUsdt || withdrawOrder.Status != wdordmod.DEFAULT { + return fmt.Errorf("订单异常") + } + // 更新订单状态 + if ec = wdordmod.UpdateOne(nil, bson.M{"_id": withdrawOrder.ID}, bson.M{ + "checkedAt": now, + "progressAt": now, + "successAt": now, + "updatedAt": now, + "receivedAt": now, + "status": wdordmod.SUCCESS, + "statusDesc": wdordmod.SUCCESSStatus, + "usdtRate": usdtRate, + "operator": operator, + }); ec != nil { + return ec + } + go func() { + content = "USDT" + content + // 处理消息 + log.Info(fmt.Sprintf("HandleOfficialMsg pass withdraw start content:%v,uid:%v", content, withdrawOrder.UID)) + code := messageser.HandleOfficialMsg(withdrawOrder.UID, content, 2) + if code != stderr.Success { + log.Error(fmt.Sprintf("HandleOfficialMsg pass withdraw error:%+v:", err), log.Any("uid", withdrawOrder.UID)) + } + }() + return nil + } + //获取银行卡编码 + if withdrawOrder.PayType == wdordmod.TypeBankCard && withdrawOrder.BankCode == "" { + withdrawOrder.BankCode = GetBankInfo(withdrawOrder.Act) + if withdrawOrder.BankCode == "" { + log.Warn("GetBankInfo BankCode == ''", log.Any("uid", withdrawOrder.UID), log.Any("bankAct", withdrawOrder.Act)) + return errors.New("获取银行编码异常,请重试") + } + } + w, err := wdordmod.FindOneAndUpate(id, wdordmod.DEFAULT, set) + if err != nil { + if w.ID.IsZero() || w.ActName == "" { + return err + } + return err + } + var msg rchgutil.ExchgMsg + var tradeNo string + if w.ProductType == 0 { + tradeNo = rchgutil.EChgIDAssemble(w.ID.Hex()) + } + if w.ProductType == 1 { + tradeNo = rchgutil.EChgWLIDAssemble(w.ID.Hex()) + } + if w.PayType == wdordmod.TypeBankCard && w.BankCode == "" { + typeAct := txnactmod.Bank + act, err := txnactmod.FindOne(txnactmod.Query{ + Act: &(w.Act), + AType: &typeAct, + }) + if err != nil { + return err + } + w.BankCode = act.BankCode + } + req := &rchgutil.Exchg{ + TransNo: tradeNo, + UID: strconv.FormatUint(w.UID, 10), + Name: w.Name, + PayMoney: w.PayMoney, + PayType: w.PayType, + ActName: w.ActName, + Act: w.Act, + UserIP: w.UserIP, + DeviceType: w.DeviceType, + DevID: w.DevID, + CheckedAt: now, + ProductType: w.ProductType, + BankCode: w.BankCode, + } + if w.ProductType == 1 { + req.Rate = 3 + } + msg = req.ToPay() + set.OID = &(msg.OID) + if msg.Code != 200 { + if msg.Code == rchgutil.UNKNOWError { + set.Status = wdordmod.UNKNOWN + set.StatusDesc = wdordmod.UNKNOWNError + set.FailureAt = &now + if err = wdordmod.Update(nil, id, set); err != nil { + return err + } + return errors.New(set.StatusDesc) + } + //发生错误,执行退款程序 + set.FailureAt = &now + set.Status = wdordmod.FAILURE + set.StatusDesc = wdordmod.ServerError + "[第三方返回: " + msg.Err + "]" + if err = refusedWirhdraw(w, set, "服务器请求平台失败"); err != nil { + return err + } + return nil + } + common.Go(func() { + if err := CacheProcessingOrder(w.UID); err != nil { + log.Error("CacheProcessingUser", log.Any("order id", id), log.E(err)) + } + }) + return nil +} + +// ExchangeRefuseByHand 手动对特定状态的提现订单进行退款 +func ExchangeRefuseByHand(id string, status int, desc string) error { + w, err := wdordmod.FindWithdrawOrderByIDAndStatus(id, status) //防重入 + if err != nil { + return err + } + //发生错误,执行退款程序 + now := time.Now() + set := wdordmod.EditSelector{ + Status: wdordmod.REFUSE, + StatusDesc: desc, + CheckedAt: &now, + } + if err = refusedWirhdraw(w, set, desc); err != nil { + return err + } + //棋牌usdt兑换拒绝增加usdt可提现额度 + if w.ProductType == 1 && w.PayType == wdordmod.TypeUsdt { + if _, err = walletmod.IncUsdtAmount(w.UID, w.Money); err != nil { + return err + } + } + return nil +} + +// ExchangeRefuse 提现拒绝 +func ExchangeRefuse(id string, statusDesc, operator string) error { + w, err := wdordmod.FindWithdrawOrderByIDAndStatus(id, wdordmod.DEFAULT) //防重入 + if err != nil { + return err + } + //发生错误,执行退款程序 + now := time.Now() + set := wdordmod.EditSelector{ + Status: wdordmod.REFUSE, + StatusDesc: statusDesc, + Operator: &operator, + CheckedAt: &now, + } + if err = refusedWirhdraw(w, set, statusDesc); err != nil { + return err + } + //棋牌usdt兑换拒绝增加usdt可提现额度 + if w.ProductType == 1 && w.PayType == wdordmod.TypeUsdt { + if _, err = walletmod.IncUsdtAmount(w.UID, w.Money); err != nil { + return err + } + } + go func() { + content := "提现审核未通过," + statusDesc + "。" + if w.PayType == wdordmod.TypeUsdt { + content = "USDT" + content + } + if w.PayType == wdordmod.TypeAlipay { + content = "支付宝" + content + } + if w.PayType == wdordmod.TypeBankCard { + content = "银行卡" + content + } + // 处理消息 + log.Info(fmt.Sprintf("HandleOfficialMsg refused withdraw start content:%v,uid:%v", content, w.UID)) + code := messageser.HandleOfficialMsg(w.UID, content, 2) + if code != stderr.Success { + log.Error(fmt.Sprintf("HandleOfficialMsg refused withdraw error:%+v:", err), log.Any("uid", w.UID)) + } + }() + return nil +} + +// GetAllOrders 分页获取订单, +func GetAllOrders(cond primitive.M, opt *options.FindOptions) (total int64, + data []*wdordmod.WithdrawOrder, err error) { + if _, ok := cond["uid"]; !ok { + if b, ok := cond["filterChecked"].(bool); ok && b { + uids, err := GetAllProcessingOrders() + if err != nil { + log.Error("GetAllProcessingUser", log.E(err)) + } + if len(uids) > 0 { + cond["uid"] = bson.M{"$nin": uids} + } + } + } + delete(cond, "filterChecked") + return wdordmod.FindOrders(cond, opt) +} + +// GetAllProcessingOrders 获取过去15分钟内缓存的所有审核过的订单 +func GetAllProcessingOrders() (uids []uint64, err error) { + redisc := getRedis() + if redisc != nil { + now := time.Now() + start := fmt.Sprintf("%d", now.Add(-redisconst.WithdrawOrderDuration).Unix()) + end := fmt.Sprintf("%d", now.Unix()) + uidStrs, err := redisc.ZRangeByScore(redisconst.WithdrawOrderKey, start, end) + if err != nil { + return uids, err + } + uids = make([]uint64, 0, len(uidStrs)) + for _, str := range uidStrs { + uid, err := strconv.ParseUint(str, 0, 64) + if err != nil { + log.Error("strconv.ParseUint", log.Any("str", str), log.E(err)) + continue + } + uids = append(uids, uid) + } + common.Go(func() { + DelCacheProcessingOrders() + }) + } + return +} + +// DelCacheProcessingOrders 删除过期数据(超过配置时间窗口的数据:-inf~前15分钟) +func DelCacheProcessingOrders() { + now := time.Now() + start := "-inf" + end := fmt.Sprintf("(%d", now.Add(-redisconst.WithdrawOrderDuration).Unix()) + removed, err := getRedis().ZRemRangeByScore(redisconst.WithdrawOrderKey, start, end) + if err != nil { + log.Error("redisc.ZRemRangeByScore", log.Any("start", start), log.Any("end", end), log.E(err)) + } + log.Info("redisc.ZRemRangeByScore removed success", log.Any("start", start), log.Any("end", end), + log.Any("number", removed)) +} + +// CacheProcessingOrder 缓存正在审核中的订单 +func CacheProcessingOrder(uid uint64) error { + redisc := getRedis() + if redisc != nil { + _, err := redisc.ZAdd(redisconst.WithdrawOrderKey, redis.Member{Score: float64(time.Now().Unix()), Member: uid}) + return err + } + return errors.New("nil redis client") +} + +func getRedis() *redis.Client { + if appg.Redis != nil { + return appg.Redis + } + if webg.Redis != nil { + return webg.Redis + } + if skdg.Redis != nil { + return skdg.Redis + } + return nil +} + +// ExchgCallBack 提现回调 +func ExchgCallBack(tradeNo string, oid string, money int64, status string, mark string) (err error) { + w, err := wdordmod.FindWithdrawOrderByID(tradeNo) + if err != nil { + return + } + if w.Status == wdordmod.SUCCESS || w.Status == wdordmod.FAILURE { + return + } + if w.Status != wdordmod.PROCESSING && w.Status != wdordmod.UNKNOWN { + err = errors.New("Wtihdraw status error") + log.Error(fmt.Sprintf("Wtihdraw status error ,order:%+v:", w)) + return + } + now := time.Now() + set := wdordmod.EditSelector{ + ReceivedAt: &now, + OID: &oid, + } + switch status { + case "paid": //已经打款 + //修改订单状态 + set.PayMoney = &money + set.SuccessAt = &now + set.Status = wdordmod.SUCCESS + set.StatusDesc = wdordmod.SUCCESSStatus + set.SuccessAt = &now + if err = wdordmod.Update(nil, tradeNo, set); err != nil { + return + } + case "refused": //拒绝 + set.Status = wdordmod.FAILURE + set.StatusDesc = mark + set.FailureAt = &now + if err = refusedWirhdraw(w, set, "提现被平台拒绝"); err != nil { + return + } + } + return +} + +func refusedWirhdraw(w wdordmod.WithdrawOrder, set wdordmod.EditSelector, mark string) error { + if w.ID.IsZero() { + return errors.New("id is invalid") + } + if w.ProductType == 1 { + return errors.New("瓦力游戏已下架") + } + return webg.VideoDB.Trans(func(t *db.MongoTool) error { + switch w.WithdrawType { + case wdordmod.GoldWithdraw: + wallet, err := walletmod.CreditIncome(t, w.Amount, w.UID) + if err != nil { + return err + } + //插入收支流水 + tk := txnmod.TransactionLog{ + TransNo: w.ID, + UID: w.UID, + Amount: w.Amount, + TranType: txnmod.WithdrawRefund.Key(), + TranTypeInt: int64(txnmod.WithdrawRefund), + ChannelType: w.PayType, + Desc: "用户提现失败,退款" + strconv.FormatInt(w.Amount, 10), + DiscDoc: w.DiscDoc, + SysType: w.DeviceType, + ActualAmount: float64(w.Amount), + RealAmount: walletmod.GetRealAmount(wallet), + } + //插入一条支出流水 + if err = txnmod.InsertTransactionLog(t, &tk); err != nil { + return err + } + case wdordmod.AgentWithdraw: + if err := walletmod.CreditMoney(t, w.Money, w.UID); err != nil { + return err + } + case wdordmod.DiscWithdraw: + // err = discWirhdrawRefund(t, w, mark) + // if err != nil { + // log.Error("discWirhdrawRefund failed", log.E(err), log.Any("witOrd", w)) + // return err + // } + default: + return errors.New("invalid type") + } + return wdordmod.Update(t, w.ID.Hex(), set) + }) +} + +// WithdrawType 获取提现方式 +func WithdrawType() (res []*wdchannmod.WithdrawChannelRes, err error) { + wt, err := wdtaxmod.GetWithDrawTariff() + if err != nil { + return + } + wcs, err := wdchannmod.GetWithdrawChannels() + if err != nil { + return + } + res = make([]*wdchannmod.WithdrawChannelRes, len(wcs)) + for i, v := range wcs { + res[i] = &wdchannmod.WithdrawChannelRes{ + WithdrawChannel: *v, + WithdrawTariff: wt, + } + } + return +} + +type WithdrawMethod struct { + ChannelName string //提现渠道名字 + CID string //渠道id + PayType string //支付方式 + MinMoney int //支持最小支付金额 + QpMinMoney int //支持棋牌最小支付金额 + MaxMoney int //支持最大支付金额 + Weight int //权重 + Active bool //是否激活 + CashTax int //代理提现比率 + CoinTax int //金币提现比率 + DiscCashTax int //商区提现比率 +} + +func (w WithdrawMethod) IsValid() bool { + return w.ChannelName != "" && w.CID != "" +} + +// GetWithdrawMethod 获取提现方法 +func GetWithdrawMethod(paymentMethod string) (WithdrawMethod, error) { + active := true + mats := []wdchannmod.Matcher{ + (&wdchannmod.ActiveMatch{Active: &active}).New(), + (&wdchannmod.PayTypeMatch{PayType: &paymentMethod}).New(), + } + wcList, err := wdchannmod.List(nil, nil, nil, mats...) + if err != nil { + log.Error("GetWithdrawChannel failed", log.E(err)) + return WithdrawMethod{}, err + } + wc := wcList.WeightedRandom() + if wc.ID.IsZero() { + return WithdrawMethod{}, nil + } + var method WithdrawMethod + method.ChannelName = wc.ChannelName + method.CID = wc.CID + method.PayType = wc.PayType + method.MinMoney = wc.MinMoney + method.QpMinMoney = wc.QpMinMoney + method.MaxMoney = wc.MaxMoney + method.Weight = wc.Weight + method.Active = wc.Active + wt, err := wdtaxmod.GetWithDrawTariff() + if err != nil { + log.Error("GetWithDrawTariff failed", log.E(err)) + return WithdrawMethod{}, err + } + method.CashTax = wt.CashTax + method.CoinTax = wt.CoinTax + method.DiscCashTax = wt.DiscCashTax + return method, nil +} diff --git a/web/vidhelp/redis.go b/web/vidhelp/redis.go new file mode 100644 index 0000000..6363ccb --- /dev/null +++ b/web/vidhelp/redis.go @@ -0,0 +1,89 @@ +package vidhelp + +import ( + "encoding/json" + "fmt" + + "91porn-server/common/constant/redisconst" + "91porn-server/common/log" + "91porn-server/models/v/vidmod" + "91porn-server/web/webg" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func getVidKey(vids []primitive.ObjectID) []string { + format := redisconst.VideoInfoKey() + keys := make([]string, len(vids)) + for i, v := range vids { + keys[i] = fmt.Sprintf(format, v.Hex()) + } + return keys +} + +func parseVideoInfo(data []interface{}) []*vidmod.VideoModel { + infos := make([]*vidmod.VideoModel, 0, len(data)) + for _, d := range data { + str, ok := d.(string) + if !ok { + continue + } + var v vidmod.VideoModel + if err := json.Unmarshal([]byte(str), &v); err != nil { + continue + } + infos = append(infos, &v) + } + return infos +} + +func getVideoListByIDsFromRedis(ids []primitive.ObjectID) ([]*vidmod.VideoModel, error) { + sInfo := getVidKey(ids) + uInfo, err := webg.Redis.MGet(sInfo...) + if err != nil { + log.Error("getVideoListByIDsFromRedis error", log.Any("ids", ids), log.Any("sInfo", sInfo), log.E(err)) + return nil, err + } + videoInfo := parseVideoInfo(uInfo) + return videoInfo, nil +} + +func setVideoListByIDs2Redis(infos []*vidmod.VideoModel) error { + expire := redisconst.VideoInfoExpire() + format := redisconst.VideoInfoKey() + for _, v := range infos { + d, err := json.Marshal(v) + if err != nil { + continue + } + s := fmt.Sprintf(format, v.ID.Hex()) + _ = webg.Redis.Set(s, string(d), expire) + } + return nil +} + +func incComment(vid primitive.ObjectID) { + infos, err := getVideoListByIDsFromRedis([]primitive.ObjectID{vid}) + if err != nil || len(infos) == 0 { + return + } + if infos[0] == nil { + return + } + infos[0].CommentCount++ + infos[0].FakeCommentCount++ + _ = setVideoListByIDs2Redis(infos) +} + +func decComment(vid primitive.ObjectID) { + infos, err := getVideoListByIDsFromRedis([]primitive.ObjectID{vid}) + if err != nil || len(infos) == 0 { + return + } + if infos[0] == nil { + return + } + infos[0].CommentCount-- + infos[0].FakeCommentCount-- + _ = setVideoListByIDs2Redis(infos) +} diff --git a/web/vidhelp/vid.go b/web/vidhelp/vid.go new file mode 100644 index 0000000..9b89cb6 --- /dev/null +++ b/web/vidhelp/vid.go @@ -0,0 +1,417 @@ +package vidhelp + +import ( + "91porn-server/models/v/moduleconfmod" + "91porn-server/models/v/modulesectionmod" + "errors" + "math" + "reflect" + "strconv" + "strings" + "time" + + "91porn-server/common/crypt" + "91porn-server/common/log" + sli "91porn-server/common/slice" + v10 "91porn-server/common/v10" + "91porn-server/models/l/payvidlgmod" + "91porn-server/models/v/modulevidmod" + "91porn-server/models/v/pushmod" + "91porn-server/models/v/tagmod" + "91porn-server/models/v/usermod" + "91porn-server/models/v/vidmod" + + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// OID 别名 +type OID = primitive.ObjectID + +// Struct2Map 结构体转map +func Struct2Map(obj interface{}) map[string]interface{} { + t := reflect.TypeOf(obj) + v := reflect.ValueOf(obj) + var data = make(map[string]interface{}) + for i := 0; i < t.NumField(); i++ { + key, ok := t.Field(i).Tag.Lookup("json") + if !ok { + log.Error("Struct2Map error", log.Any("obj", obj)) + continue + } + data[key] = v.Field(i).Interface() + } + data["updatedAt"] = time.Now() + return data +} + +// AddTags 没有的tag就添加 +func AddTags(id string, tagNames []string) []primitive.ObjectID { + back := []primitive.ObjectID{} + cover, err := vidmod.GetVideoCover(id) + if err != nil { + return back + } + back = make([]primitive.ObjectID, 0, len(tagNames)) + for _, n := range tagNames { + tMod := tagmod.Tag{ + TagName: v10.ExtractPureChar(n), + CoverImg: cover, + Description: "", + SortCode: 0, + VPlayCount: 0, + TPlayCount: 0, + FakePlayCount: 0, + FakeCollCount: 0, + IsActive: true, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + data, err := tagmod.FindOneTagByName(v10.ExtractPureChar(n)) + if err != nil { + log.Error("AddTags FindOneTagByName error", log.Any("id", id), log.Any("tagNames", tagNames), log.E(err)) + continue + } + if data.ID.IsZero() { + oid, err := tagmod.InsertOne(&tMod) + if err != nil { + log.Error("AddTags InsertOne error", log.Any("id", id), log.Any("tagNames", tagNames), log.E(err)) + } else { + back = append(back, oid) + } + continue + } + back = append(back, data.ID) + } + return back +} + +// GetTagsByIDs2Map 通过tagid列表获取tag详情列表 +func GetTagsByIDs2Map(ids []OID) map[primitive.ObjectID]vidmod.TagInfo { + m := make(map[primitive.ObjectID]vidmod.TagInfo) + tags, err := tagmod.FindTagsByIDS(ids) + if err != nil { + log.Error("GetTagsByIDs2Map error", log.Any("ids", ids), log.E(err)) + return m + } + for _, info := range tags { + b := vidmod.TagInfo{ + ID: info.ID, + Name: info.TagName, + CoverImg: info.CoverImg, + Description: info.Description, + } + m[info.ID] = b + } + return m +} + +// GetPartTags 从内存获取tagInfo +func GetPartTags(ids []OID, mTags map[primitive.ObjectID]vidmod.TagInfo) []vidmod.TagInfo { + infos := make([]vidmod.TagInfo, len(ids)) + for i, v := range ids { + infos[i] = mTags[v] + } + return infos +} + +// Transfer2Info 将视频数据库模式转为返回模式 +func transfer2Info(v *vidmod.VideoModel, uInfo usermod.BaseInfo, ti []vidmod.TagInfo, pushing bool) *vidmod.WebVideo { + isUser := false + if v.PublisherID > 115000 { + isUser = true + } + chosen := false + if v.Chosen { + chosen = v.Chosen + } + freeArea := false + if v.FreeArea { + freeArea = v.FreeArea + } + coins := v.Coins + if v.Status == 0 && coins == vidmod.PretendAccInitCoins { + coins = vidmod.PretendAccDefaultCoins + } + newsType := vidmod.SP + if len(v.NewsType) != 0 { + newsType = v.NewsType + } + tags := make([]string, len(ti)) + for i, tag := range ti { + tags[i] = tag.Name + } + return &vidmod.WebVideo{ + ID: v.ID, + NewsType: newsType, + Publisher: uInfo, + PubliserID: uInfo.UID, + PublisherName: uInfo.Name, + Title: v.Title, + Content: v.Content, + Tags: ti, + Cover: v.Cover, + CoverThumb: v.CoverThumb, + SeriesCover: v.SeriesCover, + IsUser: isUser, + PlayCount: v.PlayCount, + Status: v.Status, + PlayTime: v.PlayTime, + LikeCount: v.LikeCount, + Resolution: v.Resolution, + CommentCount: v.CommentCount, + ShareCount: v.ShareCount, + FakeLikeCount: v.FakeLikeCount, + FakeCommentCount: v.FakeCommentCount, + FakeShareCount: v.FakeShareCount, + FakePlayCount: v.FakePlayCount, + Coins: coins, + CreatedAt: v.CreatedAt, + UpdatedAt: v.UpdatedAt, + FreeTime: v.FreeTime, + SourceID: v.SourceID, + SourceURL: v.SourceURL, + LinkUrl: v.LinkUrl, + PreviewURL: v.PreviewURL, + SortCode: v.SortCode, + FileName: v.Filename, + Chosen: chosen, + FreeArea: freeArea, + Pushing: pushing, + Size: v.Size, + Reason: v.Reason, + IsTopping: v.IsTopping, + IsRecommend: v.IsRecommend, + IsChoosen: v.IsChoosen, + RecoWeight: v.RecoWeight, + ReviewAccount: v.ReviewAccount, + Rewarded: v.Rewarded, + FakeRewarded: v.FakeRewarded, + TagSort: coverTagSort(v.TagSort, ti), + LiaoBaTop: v.LiaoBaTop, + LiaoBaTopSort: v.LiaoBaTopSort, + WorksSort: v.WorksSort, + PageViewCount: v.PageViewCount, + TagsExport: strings.Join(tags, "|"), + ActivityID: v.ActivityID, + HappinessPlazaTop: v.HappinessPlazaTop, + ShareSort: v.ShareSort, + TimeNodeList: v.TimeNodeList, + DownloadAllow: v.DownloadAllow, + ShowType: v.ShowType, + } +} + +func coverTagSort(id interface{}, ti []vidmod.TagInfo) []vidmod.TagSort { + tagSort := []vidmod.TagSort{} + tagInfo := make(map[string]int) + type keyValue struct { + Key string + Value int + } + xx := []keyValue{} + if err := crypt.Obj2Obj(id, &xx); err != nil { + return tagSort + } + if len(ti) > 0 { + ids := make([]string, len(xx)) + for i, v := range xx { + ids[i] = v.Key + tagInfo[v.Key] = v.Value + } + tagSort = make([]vidmod.TagSort, len(ti)) + for i, v := range ti { + var sortCode int + if sli.Contains(ids, v.ID.Hex()) { + sortCode = tagInfo[v.ID.Hex()] + } + tagSort[i] = vidmod.TagSort{ + TagId: v.ID.Hex(), + TagName: v.Name, + SortCode: sortCode, + } + } + } + return tagSort +} + +// GetUserInfo2Map 获取用户基本信息到map +func GetUserInfo2Map(uids []uint64) map[uint64]*usermod.BaseInfo { + userInfo, err := usermod.GetUsersBaseInfo(uids) + if err != nil { + return nil + } + m := make(map[uint64]*usermod.BaseInfo) + for _, i := range userInfo { + m[i.UID] = i + } + return m +} + +// IsPushing 是否推送 +func IsPushing(id primitive.ObjectID) bool { + push, _ := pushmod.IsExist(id) + return push +} + +// EncodeVideoInfo 封装返回的视频列表信息 +func EncodeVideoInfo(vInfos []*vidmod.VideoModel, secID primitive.ObjectID) (back []*vidmod.WebVideo, err error) { + vInfosLen := len(vInfos) + if vInfosLen == 0 { + return + } + uids := make([]uint64, vInfosLen) + var tags []primitive.ObjectID + videoIDs := make([]primitive.ObjectID, vInfosLen) + for i, v := range vInfos { + uids[i] = v.PublisherID + tags = append(tags, v.Tags...) + videoIDs[i] = v.ID + } + videoSalesStat, err := payvidlgmod.VideoCoinIncomeAggregateByVideoIDs(videoIDs) + if err != nil { + log.Error("VideoCoinIncomeAggregateByVideoIDs", log.E(err)) + return + } + videoSalesMap := map[primitive.ObjectID]payvidlgmod.VideoCoinIncome{} + for _, stat := range videoSalesStat { + videoSalesMap[stat.VID] = stat + } + videos, err := modulevidmod.GetBySectionIDAndVids(secID, videoIDs) + if err != nil { + log.Error("GetBySectionIDAndVids", log.E(err)) + return + } + videoMap := map[primitive.ObjectID]modulevidmod.SectionVideo{} + videoSectionMap := map[primitive.ObjectID]modulesectionmod.Section{} + moduleMap := map[primitive.ObjectID]moduleconfmod.ModuleConf{} + + var secId []primitive.ObjectID + var moduleId []primitive.ObjectID + for _, v := range videos { + videoMap[v.VideoID] = v + secId = append(secId, v.SectionID) + } + + // 获取专题信息 + if len(secId) > 0 { + sections, err := modulesectionmod.GetBySectionByIDs(secId) + if err != nil { + log.Error("moduleSection GetBySectionByIDs err", log.E(err)) + return nil, errors.New("moduleSection GetBySectionByIDs err") + } + if len(sections) > 0 { + for _, v := range videos { + for _, s := range sections { + if v.SectionID == s.ID { + videoSectionMap[v.VideoID] = s + moduleId = append(moduleId, s.SubModuleID) + } + } + } + } + // 获取模块信息 + if len(moduleId) > 0 { + moduleConfs, err := moduleconfmod.FindByIDs(moduleId) + if err != nil { + log.Error("moduleConf FindByIDs err", log.E(err)) + return nil, errors.New("moduleConf FindByIDs err") + } + + if len(moduleConfs) > 0 { + for _, s := range sections { + for _, m := range moduleConfs { + if s.SubModuleID == m.ID { + moduleMap[s.ID] = m + } + + } + } + } + } + } + mUser, _ := usermod.GetUsersBaseInfoMap(uids) + mTags := GetTagsByIDs2Map(tags) + back = make([]*vidmod.WebVideo, len(vInfos)) + for i, v := range vInfos { + var uInfo usermod.BaseInfo + if mUser[v.PublisherID] != nil { + uInfo = *mUser[v.PublisherID] + } + info := transfer2Info(v, uInfo, GetPartTags(v.Tags, mTags), IsPushing(v.ID)) + info.TotalSellAmount = videoSalesMap[v.ID].Total + info.TotalSellCount = videoSalesMap[v.ID].Count + info.TotalSellDays = CalculateVideoSellDays(v.ReviewAt) + info.PurchaseRate = CalculatePurchaseRate(videoSalesMap[v.ID].Count, int64(v.PlayCount)) + info.HitRate = CalculateClickRate(int64(info.PlayCount), info.PageViewCount) + info.LikeRate = CalculateLikeRate(int64(info.LikeCount), int64(info.PlayCount)) + info.SectionID = secID + info.SortCodeUnderSection = videoMap[v.ID].SortCode + info.ReviewAt = v.ReviewAt + info.SeedSize = v.SeedSize + info.SeedLinkUrl = v.SeedLinkUrl + info.SeedPlayTime = v.SeedPlayTime + info.RichText = v.RichText + info.PreviewStart = v.PreviewStart + info.SectionName = videoSectionMap[v.ID].SectionName + + back[i] = info + if !videoSectionMap[v.ID].ID.IsZero() { + info.ModuleName = moduleMap[videoSectionMap[v.ID].ID].ModuleName + } + } + return +} + +// IncCommentCount 评论统计次数加加 +func IncCommentCount(id primitive.ObjectID) error { + if err := vidmod.IncCommentCount(id); err != nil { + return err + } + incComment(id) + return nil +} + +// DecCommentCount 评论统计次数减少 +func DecCommentCount(id primitive.ObjectID) error { + if err := vidmod.DecCommentCount(id, -1); err != nil { + return err + } + decComment(id) + return nil +} + +// CalculateVideoSellDays 计算审核通过后天数 +func CalculateVideoSellDays(reviewdAt time.Time) float64 { + if reviewdAt.IsZero() { + return 0 + } + return math.Round(time.Since(reviewdAt).Hours() / 24) +} + +// CalculatePurchaseRate 计算视频购买率 +func CalculatePurchaseRate(sellCount, playCount int64) float64 { + if playCount <= 0 { + return 0.00 + } + value, _ := strconv.ParseFloat(decimal.NewFromInt(sellCount).Div(decimal.NewFromInt(playCount)).Mul(decimal.NewFromInt(100)).StringFixed(2), 64) + return value +} + +// CalculateClickRate 计算视频点击率 +func CalculateClickRate(pageViewCount, playCount int64) float64 { + if playCount <= 0 { + return 0.00 + } + value, _ := strconv.ParseFloat(decimal.NewFromInt(pageViewCount).Div(decimal.NewFromInt(playCount)).Mul(decimal.NewFromInt(100)).StringFixed(2), 64) + return value +} + +// CalculateLikeRate 计算视频点赞率 +func CalculateLikeRate(likeCount, playCount int64) float64 { + if playCount <= 0 { + return 0.00 + } + value, _ := strconv.ParseFloat(decimal.NewFromInt(likeCount).Div(decimal.NewFromInt(playCount)).Mul(decimal.NewFromInt(100)).StringFixed(2), 64) + return value +} diff --git a/web/webg/ai_callback.go b/web/webg/ai_callback.go new file mode 100644 index 0000000..8c092ae --- /dev/null +++ b/web/webg/ai_callback.go @@ -0,0 +1,14 @@ +package webg + +import ( + "errors" + "strings" +) + +// AICallbackURL returns the externally reachable Web callback URL for AI jobs. +func AICallbackURL(path string) (string, error) { + if Conf == nil || strings.TrimSpace(Conf.URL.AiImageToVideoCallbackUrl) == "" { + return "", errors.New("ai image-to-video callback url is not configured") + } + return strings.TrimRight(strings.TrimSpace(Conf.URL.AiImageToVideoCallbackUrl), "/") + "/" + strings.TrimLeft(path, "/"), nil +} diff --git a/web/webg/elastic.go b/web/webg/elastic.go new file mode 100644 index 0000000..b636d77 --- /dev/null +++ b/web/webg/elastic.go @@ -0,0 +1,24 @@ +package webg + +import ( + "os" + + "91porn-server/common/elastic" + "91porn-server/common/log" +) + +func InitElastic() { + opts := elastic.Options{ + Address: []string{Conf.Elastic.VideoUrl}, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 30, + UserName: Conf.Elastic.UserName, + PassWord: Conf.Elastic.PassWord, + } + c, err := elastic.InitElastic(opts) + if err != nil { + log.Error("startUp VideoES InitElastic error", log.E(err)) + os.Exit(1) + } + VideoES = c +} diff --git a/web/webg/redis.go b/web/webg/redis.go new file mode 100644 index 0000000..655eadb --- /dev/null +++ b/web/webg/redis.go @@ -0,0 +1,28 @@ +package webg + +import ( + "os" + + "91porn-server/common/log" + "91porn-server/common/redis" +) + +func InitRedis() { + r, err := redis.WithURL(Conf.Redis.URL) + if err != nil { + log.Error("startUp rUrl redis error", log.E(err)) + os.Exit(1) + } + Redis = r +} + +func RedisClose() error { + if Redis != nil { + if err := Redis.Close(); err != nil { + log.Error("Redis Redis Close Error", log.E(err)) + return err + } + log.Info("Redis Redis Close OK") + } + return nil +} diff --git a/web/webg/webg.go b/web/webg/webg.go new file mode 100644 index 0000000..9a661ac --- /dev/null +++ b/web/webg/webg.go @@ -0,0 +1,155 @@ +package webg + +import ( + "91porn-server/common/constant" + "91porn-server/common/db" + "91porn-server/common/elastic" + "91porn-server/common/httputil" + "91porn-server/common/laosiji_app" + "91porn-server/common/redis" + "91porn-server/common/tg" +) + +type StaticSource struct { + Portrait []string `json:"portrait"` + Cities []string `json:"cities"` + Names []string `json:"names"` + SysUser []uint64 `json:"sysUser"` +} + +type GlobalConfig struct { + Base struct { + Port int64 `json:"port"` + PProf bool `json:"pprof"` + Env string `json:"env"` + JwtKey string `json:"jwtKey"` + WebJwtKey string `json:"webJwtKey"` + // 媒体分片(TS)鉴权签名密钥对象(含 keyVersion/key);未配置时回退内置默认(版本 default + constant.MediaSourceAuthKey) + TsAuth constant.TsAuthKeyConfig `json:"tsAuth"` + } `json:"base"` + Log struct { + Level string `json:"level"` + DisableStacktrace bool `json:"disableStacktrace"` + } `json:"log"` + Mongo struct { + VideoDbUrl string `json:"videoDbUrl"` //主数据库 存放强业务相关的数据 + StatDbUrl string `json:"statDbUrl"` //统计数据库 存放统计 日志 渠道相关的数据 + LogDbUrl string `json:"logDbUrl"` //日志数据库 + } `json:"mongo"` + DaiChong struct { + AppID string `json:"appId"` + AppSecret string `json:"appSecret"` + Domain string `json:"domain"` + } `json:"daichong"` + Welfare struct { + AppID string `json:"appId"` + AppSecret string `json:"appSecret"` + } `json:"welfare"` + Redis struct { + URL string `json:"url"` + } `json:"redis"` + Im struct { + AppID string `json:"appId"` + AppName string `json:"appName"` + AppSecret string `json:"appSecret"` + } `json:"im"` + ImV2 struct { + BaseURL string `json:"baseUrl"` + DynamicConfigDomain string `json:"dynamicConfigDomain"` + SocketURL string `json:"socketUrl"` + MerchantCode string `json:"merchantCode"` + TenantCode string `json:"tenantCode"` + AppKey string `json:"appKey"` + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + } `json:"imv2"` + AdCenter struct { + ApiDomain string `json:"apiDomain"` // api地址 + MerchantCode string `json:"merchantCode"` // 商户 code + AppCode string `json:"appCode"` // 应用 code + AesKey string `json:"aesKey"` // 数据解密KEY + } `json:"adCenter"` + Game struct { + URL string `json:"url"` + MercId int `json:"mercId"` + } `json:"game"` + URL struct { + ImCenterUrl string `json:"imCenterUrl"` + OriginUrl string `json:"originUrl"` + JH1BOriginUrl string `json:"jH1BOriginUrl"` + LaoSiJiOriginUrl string `json:"laoSiJiOriginUrl"` + LaoSiJiTSCdn string `json:"laoSiJiTSCdn"` + CdnUrl string `json:"cdnUrl"` + SearchUserUrl string `json:"searchUserUrl"` + UploadImgUrl string `json:"uploadImgUrl"` + UploadUrl string `json:"uploadUrl"` + SendSingleFile string `json:"sendSingleFile"` + SendBatchFile string `json:"sendBatchFile"` + PullFileUrl string `json:"pullFileUrl"` + PullFileInfo string `json:"pullFileInfo"` + FileInfoUrl string `json:"fileInfoUrl"` + PullSeriesUrl string `json:"pullSeriesUrl"` + LocalWebUrl string `json:"localWebUrl"` + LocalAppUrl string `json:"localAppUrl"` + HttpProxy string `json:"httpProxy"` + TransactionUrl string `json:"transactionUrl"` //支付下单地址 + AiServer string `json:"aiServer"` //ai订单请求地址 + AiUndressServer string `json:"aiUndressServer"` // AI图片脱衣服务地址 + AiImageToVideoCallbackUrl string `json:"aiImageToVideoCallbackUrl"` // AI任务回调地址 + } `json:"url"` + LSJ struct { + AppID string `json:"appId"` + APIKey string `json:"apiKey"` + APIUrl string `json:"apiUrl"` + ImageYuan string `json:"imageYuan"` + } `json:"lsj"` + Switch struct { + CdnOriginBaseOnLocal bool `json:"cdnOriginBaseOnLocal"` //是否开启cdn url,为文件服务源站 + } `json:"switch"` + Elastic struct { + VideoUrl string `json:"videoUrl"` + UserName string `json:"userName"` + PassWord string `json:"passWord"` + } `json:"elastic"` + EnableIPWhite struct { + IsEnable bool `json:"isEnable"` + } `json:"enableIPWhite"` + MediaResourceDBCfg struct { + BaseUrl string `json:"baseUrl"` //域名 + Merchat string `json:"merchat"` //商户号 + Secret string `json:"secret"` //商户密钥 + } `json:"mediaResourceDBCfg"` //媒体资源库配置 + GodCommentAward int64 `json:"godCommentAward"` //神评奖励 + Kafka struct { + Url string `yaml:"url"` + Group string `yaml:"group"` + } `yaml:"kafka"` + Customer struct { + Secret string `json:"secret"` + } `json:"customer"` + LaoSiJiAiMate laosiji_app.Config `json:"laoSiJiAiMate"` + SMS struct { + FengNiao struct { + Url string `json:"url"` + ChinaMercId string `json:"chinaMercId"` + ChinaSecret string `json:"chinaSecret"` + ChinaTmplId string `json:"chinaTmplId"` + InterMercId string `json:"interMercId"` + InterSecret string `json:"interSecret"` + InterTmplId string `json:"interTmplId"` + } `json:"fengNiao"` + } `json:"sms"` +} + +var ( + Conf *GlobalConfig + Redis *redis.Client + VideoDB *db.MongoDB + LogDB *db.MongoDB + StatDB *db.MongoDB + Bot *tg.Bot + ProxyCfg *httputil.ProxyCfg + ImAuth string //im token + VideoES *elastic.Client + Static *StaticSource +) diff --git a/亚模块红点AI女友接口.md b/亚模块红点AI女友接口.md new file mode 100644 index 0000000..ee52bb7 --- /dev/null +++ b/亚模块红点AI女友接口.md @@ -0,0 +1,156 @@ +# 亚模块、更新红点、AI 女友接口 + +本文只列 App/H5 需要对接的服务端字段。接口沿用现有登录鉴权和统一响应结构,业务数据位于 `data`。 + +## 1. 亚模块列表 + +```http +GET /api/app/modules/list +``` + +后端只返回当前已启用且处于上下架时间范围内的亚模块。亚模块对象新增: + +```json +{ + "id": "66a000000000000000000001", + "moduleName": "示例模块", + "onlineAt": "2026-07-27T08:00:00Z", + "offlineAt": "2026-08-27T08:00:00Z", + "excludeLatest": true, + "excludeRecommend": true, + "searchOnlyWhenInactive": true +} +``` + +- `onlineAt`、`offlineAt` 为空表示不限制。 +- App 无需自行判断时间,也不要缓存已经下架的亚模块。 +- 亚模块失效后,普通列表、模块/专题入口、分享页和直接视频详情均不可访问;搜索结果仍可访问。 + +## 2. 首页及亚模块更新红点 + +```http +GET /api/app/content/update-markers +``` + +返回: + +```json +{ + "code": 200, + "data": { + "homeLatestAt": "2026-07-27T08:10:00Z", + "todayLatestAt": "2026-07-27T08:10:00Z", + "modules": [ + { + "moduleId": "66a000000000000000000001", + "latestAt": "2026-07-27T08:05:00Z" + } + ] + } +} +``` + +- `homeLatestAt`:主页“最新”内容最近审核通过时间。 +- `todayLatestAt`:“今日更新”最近审核通过时间;“今日”按 UTC+8 自然日计算。 +- `modules[].latestAt`:该亚模块视频最近审核通过时间;动漫、漫画等媒体取最近新增或子集更新时间。 +- 没有内容时对应时间为 `null`。 +- 客户端保存各入口的本地 `lastReadAt`。当 `latestAt > lastReadAt` 时显示红点,用户点击入口后更新本地已读时间。 + +## 3. 搜索专属视频访问 + +视频关键字搜索和片库搜索的每个视频对象新增: + +```json +{ + "id": "66b000000000000000000001", + "searchAccessToken": "短时效凭证" +} +``` + +从搜索结果进入视频详情时原样传回: + +```http +GET /api/app/vid/info?videoID=66b000000000000000000001&searchAccessToken=短时效凭证 +``` + +- 普通视频详情可不传 `searchAccessToken`。 +- 凭证与当前用户、视频绑定,有效期 10 分钟。 +- 仅搜索可见的视频未传、传错或凭证过期时,接口按内容不存在处理。 + +## 4. AI 女友入口 + +App 从现有启动配置判断入口是否展示: + +```http +GET /api/app/ping/v +``` + +使用字段 `aiGirlFriend`: + +```json +{ + "aiGirlFriend": true +} +``` + +`aiGirlFriend=true` 时展示入口。点击入口后调用: + +```http +POST /api/app/aimatev2/url +``` + +无需请求体。成功响应: + +```json +{ + "code": 200, + "data": { + "url": "https://example.com/ai" + } +} +``` + +前端打开 `data.url`。该接口会先处理上一次未结算余额,再把用户主钱包的可用金币上分到 AI 女友;换算比例为 `10金币=1元`,前端不参与金额换算。 + +用户关闭 AI 女友页面或返回 App 后,调用现有钱包接口刷新余额: + +```http +GET /api/app/mine/wallet +``` + +钱包接口会先尝试把 AI 女友剩余余额下分回主钱包,然后返回最新钱包数据;响应结构不变。历史别名 `GET /api/app/mine/qianbao` 同步支持,但新代码统一使用 `/mine/wallet`。 + +旧 `/api/app/aimate/*` 接口仅为旧客户端保留,新版 App/H5 不再调用 `login`、`getBalance`、`currencys` 或 `exchange`,也不再直接使用 `ping/v` 返回的 `aiMateH5`。 + +## 5. 管理后台配置 + +管理后台沿用现有接口: + +```http +POST /api/web/admin/module/conf/add +POST /api/web/admin/module/conf/edit +``` + +新增请求字段: + +```json +{ + "onlineAt": "2026-07-27T08:00:00Z", + "offlineAt": "2026-08-27T08:00:00Z", + "excludeLatest": true, + "excludeRecommend": true, + "searchOnlyWhenInactive": true +} +``` + +编辑时清空时间限制: + +```json +{ + "id": "66a000000000000000000001", + "clearOnlineAt": true, + "clearOfflineAt": true +} +``` + +`offlineAt` 必须晚于 `onlineAt`,否则保存失败。 diff --git a/付费引导Ping下发接口文档.md b/付费引导Ping下发接口文档.md new file mode 100644 index 0000000..14c84c0 --- /dev/null +++ b/付费引导Ping下发接口文档.md @@ -0,0 +1,526 @@ +# 91porn 付费引导 Ping 下发接口文档 + +> 对接对象:App/H5 前端 +> +> 文档状态:已按测试确认的最终规则更新,后端接口与 Web 配置能力已同步实现 +> +> 目标:普通付费引导弹窗随 Ping 一次性下发,弹窗触发时不再临时请求接口;会员内容上新仍使用独立接口实时判断。 + +## 1. 调整概要 + +付费引导共有 7 个场景编码: + +| 场景 | 说明 | 新版前端取值方式 | +|---|---|---| +| `HOME_NEW_USER_FREE_TRIAL` | 首页新用户免费 X 次试看;X 取系统配置 `totalWatchCount` | Ping 下发 | +| `HOME_OLD_USER` | 首页老用户引导;与新用户免费试看场景共用首页开关 | Ping 下发 | +| `VIDEO_PREVIEW_END` | 视频试看 3 秒引导 | Ping 下发 | +| `DISCOUNT_COUNTDOWN` | 优惠倒计时弹窗 | Ping 下发 | +| `VIDEO_BACK` | 退出视频时引导 | Ping 下发 | +| `VIP_CENTER` | 会员中心引导 | Ping 下发 | +| `VIP_CONTENT_UPDATE` | 会员内容上新引导 | 独立接口实时查询 | + +新版前端处理规则: + +1. App/H5 启动时读取 Ping 中的 `paymentGuide` 并缓存。 +2. 前 6 个普通场景在触发时直接读取 Ping 缓存,不再调用付费引导查询接口。 +3. `VIP_CONTENT_UPDATE` 必须实时查询最新视频、`contentVersion` 及展示状态,继续调用独立接口。 +4. `show` 字段保留。普通场景以 Ping 中的 `show` 为准,上新场景以独立接口的 `show` 为准。 +5. 首页新用户免费试看、视频试看 3 秒、App 活跃 3 分钟等触发时机仍由前端控制。 +6. `DISCOUNT_COUNTDOWN` 的倒计时由前端在每次 App 冷启动时重新创建,固定为 2 小时;后端不计算或下发倒计时起止时间,也不使用 `durationSeconds` 作为倒计时。 +7. 除 `DISCOUNT_COUNTDOWN` 外,普通弹窗实际展示后,前端调用展示回执接口,并在本地立即将该场景的 `show` 设为 `false`,防止同一次启动重复展示。 + +## 2. 枚举 + +### 2.1 场景 `scene` + +```text +HOME_NEW_USER_FREE_TRIAL +HOME_OLD_USER +VIDEO_PREVIEW_END +DISCOUNT_COUNTDOWN +VIDEO_BACK +VIP_CENTER +VIP_CONTENT_UPDATE +``` + +### 2.2 用户分层 `segment` + +```text +NEW_NEVER_PAID +OLD_NEVER_PAID +PAID_UPGRADE +MAX_VIP +NORMAL +UNREGISTERED +``` + +未付费用户的新老分层只按注册时间判断:注册不足 24 小时为 `NEW_NEVER_PAID`,达到 24 小时后为 `OLD_NEVER_PAID`。免费观看次数不会改变全局用户分层;次数用完时仅将 `HOME_NEW_USER_FREE_TRIAL.show` 置为 `false`。 + +## 3. Ping 全量接口 + +### 3.1 App + +```http +GET /api/app/ping/domain +``` + +### 3.2 H5 + +```http +GET /api/app/ping/domain/h5 +``` + +两个接口的 `data` 均新增 `paymentGuide`,其他字段保持不变。 + +### 3.3 响应示例 + +```json +{ + "code": 200, + "msg": "success", + "data": { + "paymentGuide": { + "enabled": true, + "segment": "OLD_NEVER_PAID", + "scenes": { + "HOME_NEW_USER_FREE_TRIAL": { + "enabled": false, + "show": false, + "totalWatchCount": 3 + }, + "HOME_OLD_USER": { + "enabled": true, + "show": true, + "configId": "6889f2014a4fcb5012340001", + "style": "BOTTOM_SHEET", + "title": "开通会员", + "description": "开通会员后可观看完整影片", + "cover": "20260804/payment-guide/home-old-user.png", + "productId": "6889f2014a4fcb5012349999", + "durationSeconds": 0, + "action": { + "type": "VIP_PRODUCT", + "value": "6889f2014a4fcb5012349999" + } + }, + "VIDEO_PREVIEW_END": { + "enabled": true, + "show": true, + "configId": "6889f2014a4fcb5012340002", + "style": "CENTER_POPUP", + "title": "试看 3 秒", + "description": "开通会员观看完整内容", + "cover": "20260804/payment-guide/preview-end.png", + "productId": "6889f2014a4fcb5012349999", + "durationSeconds": 0, + "action": { + "type": "VIP_PRODUCT", + "value": "6889f2014a4fcb5012349999" + } + }, + "DISCOUNT_COUNTDOWN": { + "enabled": true, + "show": true, + "configId": "6889f2014a4fcb5012340006", + "style": "CENTER_POPUP", + "title": "现在下单获得优惠福利", + "description": "距优惠结束", + "cover": "20260804/payment-guide/discount-countdown.png", + "productId": "6889f2014a4fcb5012349999", + "durationSeconds": 0, + "action": { + "type": "VIP_PRODUCT", + "value": "6889f2014a4fcb5012349999" + } + }, + "VIDEO_BACK": { + "enabled": true, + "show": false, + "configId": "6889f2014a4fcb5012340004", + "style": "CENTER_POPUP", + "title": "退出视频引导", + "description": "开通会员观看更多内容", + "cover": "20260804/payment-guide/video-back.png", + "productId": "6889f2014a4fcb5012349999", + "durationSeconds": 0, + "action": { + "type": "VIP_PRODUCT", + "value": "6889f2014a4fcb5012349999" + } + }, + "VIP_CENTER": { + "enabled": true, + "show": true, + "configId": "6889f2014a4fcb5012340003", + "style": "CENTER_POPUP", + "title": "限时会员福利", + "description": "立即开通", + "cover": "20260804/payment-guide/vip-center.png", + "productId": "6889f2014a4fcb5012349999", + "durationSeconds": 7200, + "action": { + "type": "VIP_PRODUCT", + "value": "6889f2014a4fcb5012349999" + } + }, + "VIP_CONTENT_UPDATE": { + "enabled": true + } + } + } + } +} +``` + +### 3.4 `paymentGuide` 字段 + +| 字段 | 类型 | 说明 | +|---|---|---| +| `enabled` | Boolean | 付费引导全局开关;为 `false` 时所有场景均不展示 | +| `segment` | String | 当前用户的付费引导分层 | +| `scenes` | Object | 按场景编码索引的引导配置 | + +### 3.5 普通场景字段 + +| 字段 | 类型 | 必定返回 | 说明 | +|---|---|---|---| +| `enabled` | Boolean | 是 | 当前场景是否有匹配用户分层、在有效期内且已启用的配置 | +| `show` | Boolean | 是 | 当前用户此次是否允许展示;已展示过对应配置时为 `false` | +| `configId` | String | `enabled=true` 时 | 配置 ID,展示回执时原样传回 | +| `style` | String | `enabled=true` 时 | 弹窗样式编码 | +| `title` | String | `enabled=true` 时 | 标题 | +| `description` | String | `enabled=true` 时 | 描述,可为空字符串 | +| `cover` | String | `enabled=true` 时 | 封面图,可为空字符串;相对路径按现有静态资源域名规则拼接 | +| `productId` | String | `enabled=true` 时 | 会员商品 ID,可为空字符串 | +| `durationSeconds` | Number | `enabled=true` 时 | 配置中的兼容时长字段,可为 `0`;不用于 `DISCOUNT_COUNTDOWN` 的前端 2 小时倒计时 | +| `action.type` | String | `enabled=true` 时 | 操作类型:`VIP_PRODUCT`/`INTERNAL`/`EXTERNAL`/`NONE` | +| `action.value` | String | `enabled=true` 时 | 会员商品 ID、内链地址、外链地址或空字符串 | + +只要普通场景存在有效配置,以上卡片字段都会完整返回,与 `show` 是 `true` 还是 `false` 无关。无有效配置、全局开关关闭或首页共用开关关闭时,对应场景只返回 `enabled=false`、`show=false`。 + +`enabled` 与 `show` 的区别: + +| `enabled` | `show` | 含义 | +|---|---|---| +| `false` | `false` | 无有效配置,或全局开关已关闭 | +| `true` | `false` | 配置有效,但当前用户已展示过或不满足该场景的实时展示条件;仍返回完整卡片字段 | +| `true` | `true` | 配置有效且当前用户可展示 | + +`DISCOUNT_COUNTDOWN` 不使用后端历史展示记录进行拦截:只要配置有效且相关开关开启,Ping 中始终返回 `enabled=true`、`show=true`。同一次启动只弹一次由前端本地控制;下次 App 冷启动重新开始本地 2 小时倒计时。 + +`HOME_NEW_USER_FREE_TRIAL` 和 `HOME_OLD_USER` 共用同一个首页引导开关。新用户免费试看场景额外返回 `totalWatchCount`;当新用户剩余免费观看次数为 0 时,该场景保持 `enabled=true` 并返回完整卡片配置,但 `show=false`。`HOME_NEW_USER` 仅保留后端兼容识别,不再通过 Ping 或 Web 配置列表返回。 + +`VIP_CONTENT_UPDATE` 在 Ping 中只返回 `enabled`;前端不得使用 Ping 推断该场景的 `show`,必须调用第 5 节的独立接口。 + +## 4. Ping 增量刷新 + +```http +GET /api/app/ping/domain/refresh?keys=paymentGuide +``` + +适用时机: + +- 用户登录成功。 +- 用户退出登录。 +- 会员购买成功或会员状态变化。 +- 前端需要主动更新 Ping 缓存时。 + +不应在准备弹出普通弹窗时调用此接口。 + +响应示例: + +```json +{ + "code": 200, + "msg": "success", + "data": { + "paymentGuide": { + "enabled": true, + "segment": "PAID_UPGRADE", + "scenes": { + "HOME_NEW_USER_FREE_TRIAL": { + "enabled": false, + "show": false, + "totalWatchCount": 3 + }, + "HOME_OLD_USER": { + "enabled": false, + "show": false + }, + "VIDEO_PREVIEW_END": { + "enabled": false, + "show": false + }, + "DISCOUNT_COUNTDOWN": { + "enabled": false, + "show": false + }, + "VIDEO_BACK": { + "enabled": true, + "show": true, + "configId": "6889f2014a4fcb5012340004", + "style": "CENTER_POPUP", + "title": "升级会员", + "description": "升级后可观看更多内容", + "cover": "20260804/payment-guide/upgrade.png", + "productId": "6889f2014a4fcb5012349999", + "durationSeconds": 0, + "action": { + "type": "VIP_PRODUCT", + "value": "6889f2014a4fcb5012349999" + } + }, + "VIP_CENTER": { + "enabled": false, + "show": false + }, + "VIP_CONTENT_UPDATE": { + "enabled": true + } + } + } + } +} +``` + +## 5. 会员内容上新引导 + +### 5.1 获取引导 + +```http +GET /api/app/payment/guide?scene=VIP_CONTENT_UPDATE +Authorization: {App用户token} +``` + +调用前建议先判断: + +```text +paymentGuide.enabled == true +&& paymentGuide.scenes.VIP_CONTENT_UPDATE.enabled == true +``` + +满足后再调用独立接口。最终是否展示必须以该接口返回的 `show` 为准。 + +### 5.2 `show=true` 响应 + +```json +{ + "code": 200, + "msg": "success", + "data": { + "show": true, + "configId": "6889f2014a4fcb5012340005", + "contentVersion": "4a7cbdb742c44a47", + "segment": "OLD_NEVER_PAID", + "style": "BOTTOM_SHEET", + "title": "查看完整影片", + "description": "开通会员后可观看", + "cover": "20260804/payment-guide/content-update.png", + "videos": [ + { + "id": "videoId", + "title": "视频标题", + "cover": "20260804/video/cover.jpg", + "coverThumb": "20260804/video/cover-thumb.jpg", + "playTime": 1384, + "playCount": 12000 + } + ], + "productId": "6889f2014a4fcb5012349999", + "durationSeconds": 7200, + "action": { + "type": "VIP_PRODUCT", + "value": "6889f2014a4fcb5012349999" + } + } +} +``` + +### 5.3 `show=false` 响应 + +```json +{ + "code": 200, + "msg": "success", + "data": { + "show": false, + "segment": "OLD_NEVER_PAID" + } +} +``` + +`show=false` 时不展示弹窗。常见原因: + +- 付费引导全局开关已关闭。 +- 无匹配当前场景、用户分层和有效期的配置。 +- 当前 `contentVersion` 已展示过。 +- 当前没有符合条件的最新会员视频。 + +视频数量由 Web 后台对应配置的 `videoLimit` 控制;未配置或为 `0` 时默认 4 条,最大 20 条。 + +## 6. 展示回执 + +```http +POST /api/app/payment/guide/impression +Authorization: {App用户token} +Content-Type: application/json +``` + +### 6.1 普通场景(不含优惠倒计时) + +```json +{ + "configId": "6889f2014a4fcb5012340002", + "scene": "VIDEO_PREVIEW_END", + "contentVersion": "", + "videoId": "videoId", + "requestId": "6f2e26f7-6e86-40eb-9276-6d45f79eae9f" +} +``` + +### 6.2 会员内容上新场景 + +```json +{ + "configId": "6889f2014a4fcb5012340005", + "scene": "VIP_CONTENT_UPDATE", + "contentVersion": "4a7cbdb742c44a47", + "videoId": "videoId", + "requestId": "e14ae540-a5d2-4a6a-8740-c3e9877545f1" +} +``` + +### 6.3 请求字段 + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `configId` | String | 是 | Ping 或上新引导接口返回的配置 ID | +| `scene` | String | 是 | 实际展示的场景 | +| `contentVersion` | String | 上新场景必填 | 普通场景传空字符串;`VIP_CONTENT_UPDATE` 必须原样传回 | +| `videoId` | String | 否 | 相关视频 ID,无视频上下文时传空字符串或省略 | +| `requestId` | String | 是 | 本次回执的 UUID;重试必须复用同一个 UUID | + +成功响应: + +```json +{ + "code": 200, + "msg": "success", + "data": "" +} +``` + +前端注意: + +- 只有弹窗真正展示成功后才上报,接口返回 `show=true` 但未展示时不上报。 +- 上报发起后立即在本地将对应场景的 `show` 设为 `false`,避免同一次启动重复展示。 +- 网络失败可使用相同 `requestId` 有界重试,不要为同一次展示生成多个 `requestId`。 +- `DISCOUNT_COUNTDOWN` 无需调用展示回执;其同一次启动去重和每次冷启动重置均由前端本地处理。兼容版本即使上报该场景,也不会影响后续 Ping 的 `show`。 + +## 7. Web 后台配置接口 + +现有付费引导配置接口结构保持不变: + +```http +GET /api/web/admin/payment-guide/list?scene={scene}&pageNumber=1&pageSize=20 +POST /api/web/admin/payment-guide/add +POST /api/web/admin/payment-guide/edit +DELETE /api/web/admin/payment-guide/delete?id={id} +``` + +列表响应新增 `sceneOptions`,后台下拉应直接使用该字段,不再硬编码场景名称。示例: + +```json +{ + "sceneOptions": [ + { + "label": "首页新用户免费3次试看", + "value": "HOME_NEW_USER_FREE_TRIAL", + "totalWatchCount": 3 + } + ] +} +``` + +`HOME_NEW_USER` 不会出现在 `sceneOptions` 或默认配置列表中,也不能再新增、编辑为该旧场景。 + +全局开关新增系统配置: + +```text +vCode: paymentGuideEnabled +type: bool +``` + +首页新用户免费试看、老用户共用开关新增系统配置: + +```text +vCode: paymentGuideHomeEnabled +type: bool +``` + +全局开关继续复用现有系统配置查询和更新接口,不新增 Web API。 + +首页引导只提供一个开关,同时控制 `HOME_NEW_USER_FREE_TRIAL` 和 `HOME_OLD_USER`;两个场景的用户分层、素材和弹窗内容仍可分别配置。后台不得再展示旧 `HOME_NEW_USER` 或两个独立的首页开关。 + +`DISCOUNT_COUNTDOWN` 按普通付费引导场景配置卡片内容、用户分层、启用状态和有效期。倒计时固定 2 小时并由前端每次 App 冷启动重新开始,后端配置中的 `durationSeconds` 不参与该倒计时。 + +## 8. 兼容性 + +- `GET /api/app/payment/guide?scene={scene}` 支持当前 7 个可配置场景;旧 `HOME_NEW_USER` 仍可被后端识别但固定不展示。 +- 新版前端只在 `VIP_CONTENT_UPDATE` 场景调用该接口。 +- 原接口的 `show` 字段保留,不做破坏性删除。 +- Ping 新增字段对旧版本属于向后兼容的增量字段。 +- 旧的 `paymentStatusPopupConfig` 在旧客户端完成淘汰前继续保留,新客户端不得同时展示旧弹窗和新付费引导弹窗。 + +## 9. 前端接入流程 + +### 9.1 启动与缓存 + +1. 调用全量 Ping。 +2. 使用成功响应的 `paymentGuide` 覆盖本地缓存。 +3. Ping 网络失败时可使用最近一次成功缓存;首次安装且无缓存时默认不展示,不得自行假定 `show=true`。 + +### 9.2 普通场景(不含优惠倒计时) + +1. 前端达到场景触发条件。 +2. 检查 `paymentGuide.enabled`。 +3. 检查 `paymentGuide.scenes[scene].enabled` 和 `show`。 +4. 两者均为 `true` 时展示 Ping 中的弹窗配置。 +5. 展示成功后上报回执,并在本地将该场景的 `show` 设为 `false`。 + +### 9.3 会员内容上新 + +1. 前端达到上新弹窗检查时机。 +2. 检查 Ping 的全局开关及 `VIP_CONTENT_UPDATE.enabled`。 +3. 启用时调用上新引导接口。 +4. 只有接口返回 `show=true` 时才展示。 +5. 展示成功后携带原始 `contentVersion` 上报回执。 + +### 9.4 优惠倒计时 + +1. 读取 `paymentGuide.scenes.DISCOUNT_COUNTDOWN`。 +2. `enabled=true` 且 `show=true` 时展示倒计时弹窗。 +3. 每次 App 冷启动在前端本地重新创建固定 2 小时倒计时;不读取 `durationSeconds`、绝对结束时间或旧配置的 `lastDiscountTime`。 +4. 同一次启动只展示一次由前端本地状态控制;无需调用展示回执,下次冷启动可再次展示。 + +### 9.5 用户状态变化 + +登录、退出或会员状态变化后,调用 Ping 增量刷新并替换本地 `paymentGuide` 缓存。 + +## 10. 联调检查项 + +- 全局开关关闭时,所有普通场景不展示,上新场景不调用独立接口。 +- 场景无有效配置时,返回 `enabled=false` 和 `show=false`。 +- 配置有效但已展示时,返回 `enabled=true`、`show=false` 和完整卡片字段。 +- 注册不足 24 小时且剩余免费观看次数为 0 时,仍返回 `segment=NEW_NEVER_PAID`;仅 `HOME_NEW_USER_FREE_TRIAL.show=false`,不得匹配仅配置给 `OLD_NEVER_PAID` 的其他场景。 +- 用户分层改变后,增量刷新返回新分层的配置。 +- `HOME_NEW_USER_FREE_TRIAL` 和 `HOME_OLD_USER` 由同一个首页开关控制;旧 `HOME_NEW_USER` 不再返回。 +- `VIDEO_PREVIEW_END` 的业务含义为“视频试看 3 秒”,不再使用“视频试看结束”文案。 +- `DISCOUNT_COUNTDOWN` 无有效配置时不展示;有有效配置且开关开启时 Ping 始终返回完整卡片及 `show=true`。 +- `DISCOUNT_COUNTDOWN` 每次 App 冷启动由前端重新创建固定 2 小时倒计时,后端不计算倒计时,也不依据展示回执抑制下一次启动。 +- 普通场景触发时不发起付费引导查询请求。 +- 上新场景仅在 Ping 对应开关开启时调用独立接口。 +- 上新场景的 `show=false` 、视频空列表和无新 `contentVersion` 均不展示。 +- 除优惠倒计时外,展示回执成功后后续 Ping 返回 `show=false`;优惠倒计时的同一次启动去重由前端本地控制。 +- 新客户端不同时使用旧 `paymentStatusPopupConfig` 和新 `paymentGuide`。 diff --git a/会员内容上新弹窗接口.md b/会员内容上新弹窗接口.md new file mode 100644 index 0000000..aa6badc --- /dev/null +++ b/会员内容上新弹窗接口.md @@ -0,0 +1,258 @@ +# 会员内容上新弹窗接口 + +## 一、业务流程 + +1. App 使用达到产品规定时长后,回到首页时请求“获取会员内容上新弹窗”接口。 +2. 后端判断当前用户是否需要展示弹窗: + - 存在尚未展示过的最新 VIP 内容版本时,返回 `show=true`。 + - 当前内容版本已经展示过,或没有可用配置时,返回 `show=false`。 +3. `show=true` 时,前端直接使用响应中的 `videos` 渲染最新内容,数量由后台配置。 +4. 弹窗实际展示成功后,前端调用“上报弹窗已展示”接口。 +5. 上报成功后,同一用户再次请求相同内容版本时返回 `show=false`。 +6. 后续有新的 VIP 内容上架、内容版本变化后,该用户可以再次收到弹窗。 + +## 二、获取会员内容上新弹窗 + +### 请求 + +```http +GET /api/app/payment/guide?scene=VIP_CONTENT_UPDATE +Authorization: <用户登录Token> +``` + +### Query 参数 + +| 参数 | 类型 | 是否必传 | 说明 | +| --- | --- | --- | --- | +| `scene` | String | 是 | 固定传 `VIP_CONTENT_UPDATE` | + +### `show=true` 返回示例 + +```json +{ + "code": 200, + "msg": "success", + "data": { + "show": true, + "configId": "6889f6d34a4fcb5012345678", + "contentVersion": "4a7cbdb742c44a47", + "segment": "OLD_NEVER_PAID", + "style": "BOTTOM_SHEET", + "title": "VIP会员特享内容更新上架啦", + "description": "精彩内容抢先看", + "cover": "", + "videos": [ + { + "id": "6889f1014a4fcb5012340001", + "title": "视频标题1", + "cover": "https://example.com/video-1.jpg", + "coverThumb": "https://example.com/video-1-thumb.jpg", + "playTime": 1384, + "playCount": 12000 + }, + { + "id": "6889f1024a4fcb5012340002", + "title": "视频标题2", + "cover": "https://example.com/video-2.jpg", + "coverThumb": "https://example.com/video-2-thumb.jpg", + "playTime": 1062, + "playCount": 9850 + }, + { + "id": "6889f1034a4fcb5012340003", + "title": "视频标题3", + "cover": "https://example.com/video-3.jpg", + "coverThumb": "https://example.com/video-3-thumb.jpg", + "playTime": 926, + "playCount": 8160 + }, + { + "id": "6889f1044a4fcb5012340004", + "title": "视频标题4", + "cover": "https://example.com/video-4.jpg", + "coverThumb": "https://example.com/video-4-thumb.jpg", + "playTime": 745, + "playCount": 6300 + } + ], + "productId": "6889f2014a4fcb5012349999", + "durationSeconds": 0, + "action": { + "type": "VIP_PRODUCT", + "value": "6889f2014a4fcb5012349999" + } + } +} +``` + +### `show=false` 返回示例 + +```json +{ + "code": 200, + "msg": "success", + "data": { + "show": false, + "segment": "OLD_NEVER_PAID" + } +} +``` + +### 响应字段 + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `show` | Boolean | 是否展示弹窗;为 `false` 时前端不展示 | +| `configId` | String | 当前弹窗配置 ID;`show=true` 时返回 | +| `contentVersion` | String | 当前最新内容版本;展示回执时原样传回 | +| `segment` | String | 当前用户所属分层 | +| `style` | String | 弹窗样式 | +| `title` | String | 弹窗标题 | +| `description` | String | 弹窗描述 | +| `cover` | String | 弹窗整体封面,没有配置时为空字符串 | +| `videos` | Object[] | 前端渲染使用的最新 VIP 内容列表,数量由后台配置 | +| `productId` | String | 会员商品 ID | +| `durationSeconds` | Integer | 配置的持续时间,单位为秒 | +| `action` | Object | 弹窗按钮操作配置 | + +### `videos` 字段 + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | String | 视频 ID | +| `title` | String | 视频标题 | +| `cover` | String | 视频封面 | +| `coverThumb` | String | 视频缩略图;优先用于列表展示,为空时使用 `cover` | +| `playTime` | Integer | 视频时长,单位为秒 | +| `playCount` | Integer | 视频展示播放量 | + +### 内容筛选和排序 + +- 仅返回审核通过的 VIP 视频内容。 +- 排除免费专区内容。 +- 排除金币付费内容。 +- 排除不可推荐内容。 +- 排除业务配置中不参与该弹窗的模块。 +- 按审核通过时间倒序排列。 +- 返回数量由付费引导配置的 `videoLimit` 控制;未配置或传 `0` 时默认 4 条,最大 20 条。 +- 前端不需要再次排序;如果可用内容不足配置数量,按实际数量返回。 + +## 三、上报弹窗已展示 + +### 请求 + +```http +POST /api/app/payment/guide/impression +Authorization: <用户登录Token> +Content-Type: application/json +``` + +### 请求体 + +```json +{ + "configId": "6889f6d34a4fcb5012345678", + "scene": "VIP_CONTENT_UPDATE", + "contentVersion": "4a7cbdb742c44a47", + "requestId": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +### 请求参数 + +| 参数 | 类型 | 是否必传 | 说明 | +| --- | --- | --- | --- | +| `configId` | String | 是 | 获取接口返回的 `configId` | +| `scene` | String | 是 | 固定传 `VIP_CONTENT_UPDATE` | +| `contentVersion` | String | 是 | 获取接口返回的 `contentVersion`,原样传回 | +| `requestId` | String | 是 | 客户端每次展示生成的 UUID,用于请求幂等 | +| `videoId` | String | 否 | 用户点击或当前展示的视频 ID | + +### 成功返回 + +```json +{ + "code": 200, + "msg": "success", + "data": "" +} +``` + +### 调用时机 + +- 只有弹窗已经实际展示成功时才调用。 +- 仅获取数据但未展示弹窗时,不调用展示回执。 +- 回执请求失败时可以使用相同的 `requestId` 重试。 + +## 四、前端处理规则 + +- `show=false`:不展示弹窗,不调用展示回执。 +- `show=true`:使用 `videos` 渲染内容卡片。 +- 视频封面优先使用 `coverThumb`,为空时回退到 `cover`。 +- `playTime` 单位为秒,由前端格式化为 `mm:ss` 或 `hh:mm:ss`。 +- `playCount` 由前端按现有规则格式化,例如 `12000` 显示为 `1.2W`。 +- 用户点击视频时,使用对应的 `id` 跳转视频详情。 +- 用户点击开通会员时,按 `action.type` 和 `action.value` 执行跳转。 +- 弹窗展示成功后,将 `configId`、`scene`、`contentVersion` 和新生成的 `requestId` 上报。 + +## 五、重复请求行为 + +| 场景 | 返回结果 | +| --- | --- | +| 同一内容版本尚未上报展示 | 重复请求仍返回 `show=true` 和相同内容 | +| 同一内容版本已经上报展示 | 返回 `show=false` | +| 后续上架新 VIP 内容,内容版本发生变化 | 返回 `show=true` 和新的 `contentVersion` | +| 没有有效弹窗配置 | 返回 `show=false` | +| 没有符合条件的 VIP 内容 | 返回 `show=false` | + +## 六、后台返回数量和批量场景配置 + +付费引导新增、编辑接口的配置对象支持: + +```json +{ + "scene": "VIP_CONTENT_UPDATE", + "videoLimit": 20 +} +``` + +`videoLimit` 仅用于 `VIP_CONTENT_UPDATE` 场景;不传或传 `0` 时默认 4 条, +合法范围为 1~20。修改数量会改变 `contentVersion`,已经看过旧内容版本的 +用户会按新版本再次满足展示条件。 + +后台需要将同一份配置一次应用到多个场景时,调用: + +```http +POST /api/web/admin/payment-guide/add +Content-Type: application/json +``` + +```json +{ + "scenes": [ + "HOME_NEW_USER", + "VIP_CONTENT_UPDATE" + ], + "config": { + "segments": [], + "style": "BOTTOM_SHEET", + "title": "VIP会员内容更新上架啦", + "description": "精彩内容抢先看", + "cover": "", + "videoIds": [], + "videoLimit": 4, + "productId": "6889f2014a4fcb5012349999", + "durationSeconds": 0, + "action": { + "type": "VIP_PRODUCT", + "value": "6889f2014a4fcb5012349999" + }, + "enable": true, + "sort": 100 + } +} +``` + +`scenes` 支持当前全部合法场景,单次最多 6 个,不允许重复。后台会为每个场景 +生成独立的配置 ID,整批成功或整批回滚。该接口只批量复制配置,不会改变 +各场景原有的 App 内容下发逻辑。 diff --git a/短视频推荐接口.md b/短视频推荐接口.md new file mode 100644 index 0000000..6127551 --- /dev/null +++ b/短视频推荐接口.md @@ -0,0 +1,650 @@ +# 短视频推荐接口 + +本文仅提供前端联调需要的接口变更。公共请求头、登录态、签名、加密及错误处理均沿用现有项目。 + +## 一、App/H5接口 + +### 1. 短视频推荐列表 + +修改现有接口: + +```http +GET /api/app/recommend/vid/list?pageNumber=1&pageSize=20 +``` + +请求: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `pageNumber` | int | 沿用现有传参 | +| `pageSize` | int | 固定传20 | + +响应: + +```json +{ + "vInfos": [], + "totalPages": 100, + "hasNext": true, + "queueVersion": "20260724" +} +``` + +新增字段: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `hasNext` | bool | 是否可以继续加载,以该字段为准 | +| `queueVersion` | string | 推荐队列版本,仅用于日志排查 | + +前端不上传已看视频ID或offset,不再使用 `totalPages` 判断结束。 + +短视频分享时,现有接口补传视频ID: + +```http +POST /api/app/share/output +``` + +```json +{ + "content": "分享链接", + "videoID": "短视频ID" +} +``` + +`videoID` 用于累计真实分享推荐分;旧客户端不传仍可正常生成分享内容,但不会累计本次分享分。 + +### 2. 免费观看次数 + +修改现有接口: + +```http +GET /api/app/vid/user/count?vid={videoId} +``` + +响应增加: + +```json +{ + "isCan": true, + "watchCount": 2, + "totalWatchCount": 3 +} +``` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `watchCount` | uint64 | 当前剩余免费观看次数 | +| `totalWatchCount` | uint64 | 免费观看总次数,本次新增 | + +传或不传 `vid` 时都返回 `totalWatchCount`。 + +### 3. 模块配置 + +修改现有接口: + +```http +GET /api/app/modules/list +``` + +排序项增加: + +```json +{ + "val": 2, + "name": "热门推荐", + "refreshMode": "RANDOM_TOP_N", + "randomCandidateN": 30 +} +``` + +前端只根据 `refreshMode` 判断随机刷新,禁止匹配 `name` 或硬编码 `val`。 + +枚举: + +```text +DEFAULT +RANDOM_TOP_N +``` + +兼容说明:后台历史数据没有 `refreshMode` 时,后端会把稳定排序值 +`val=2` 归一为 `RANDOM_TOP_N`;标题即使配置错误也不会影响刷新功能。 + +### 4. 亚模块热门随机刷新 + +新增接口: + +```http +GET /api/app/vid/module/{subModuleId}/refresh +``` + +请求: + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `moduleSort` | int | 是 | 当前排序项的 `val` | +| `pageSize` | int | 否 | 最大30 | +| `refreshToken` | string | 是 | 每次刷新生成UUID | + +响应: + +```json +{ + "allVideoInfo": [], + "candidateCount": 30, + "hasNext": true, + "refreshMode": "RANDOM_TOP_N", + "refreshToken": "uuid" +} +``` + +候选池未变化时,同一个 `refreshToken` 重试会得到相同顺序;用户主动再次下拉时 +必须生成新的 `refreshToken`。`candidateCount` 是本次实际进入候选池的视频数, +可能小于30。`hasNext` 表示候选池中是否还有本次未返回的视频,即 +`candidateCount > allVideoInfo.length`。 + +### 5. 亚模块受限视频 + +搜索结果中的视频增加: + +```json +{ + "searchAccessTicket": "short-lived-ticket" +} +``` + +从搜索进入详情时携带: + +```http +GET /api/app/vid/info?id={videoId}&searchAccessTicket={ticket} +``` + +从搜索进入播放时继续携带 `searchAccessTicket`。非搜索来源不传。 + +### 6. 首页更新红点 + +新增接口: + +```http +GET /api/app/content/update-markers +``` + +响应: + +```json +{ + "homeLatestAt": "2026-07-24T08:00:00Z", + "todayLatestAt": "2026-07-24T08:00:00Z", + "modules": [ + { + "moduleId": "moduleId", + "latestAt": "2026-07-24T08:00:00Z" + } + ] +} +``` + +前端在本地保存最后查看时间并判断红点。 + +### 7. AI女友V2 + +入口展示继续读取: + +```http +GET /api/app/ping/v +``` + +当 `data.aiGirlFriend=true` 时展示入口。点击后调用: + +```http +POST /api/app/aimatev2/url +``` + +无需请求体,成功返回: + +```json +{ + "url": "https://example.com/ai" +} +``` + +前端打开 `data.url`。用户关闭页面或返回 App 后,调用: + +```http +GET /api/app/mine/wallet +``` + +后端会在获取 URL 时自动上分,并在查询钱包时先尝试下分;钱包响应结构不变。新版不再调用 `/api/app/aimate/*`,也不再直接使用 `aiMateH5`。 + +### 8. 获取付费引导 + +新增接口: + +```http +GET /api/app/payment/guide?scene={scene}&videoId={videoId} +``` + +场景: + +```text +HOME_NEW_USER +HOME_OLD_USER +VIDEO_PREVIEW_END +VIDEO_BACK +VIP_CENTER +VIP_CONTENT_UPDATE +``` + +响应: + +```json +{ + "show": true, + "configId": "configId", + "segment": "OLD_NEVER_PAID", + "style": "BOTTOM_SHEET", + "title": "查看完整影片", + "description": "开通会员后可观看", + "cover": "https://example.com/cover.jpg", + "videoIds": ["videoId"], + "productId": "productId", + "durationSeconds": 7200, + "action": { + "type": "VIP_PRODUCT", + "value": "productId" + } +} +``` + +`show=false` 时不弹。播放5分钟、试看3秒和App活跃3分钟均由前端计时。 + +用户分层枚举: + +```text +NEW_NEVER_PAID +OLD_NEVER_PAID +PAID_UPGRADE +MAX_VIP +NORMAL +UNREGISTERED +``` + +未付费用户注册不足 24 小时按 `NEW_NEVER_PAID` 处理,达到 24 小时后按 `OLD_NEVER_PAID` 处理。免费观看次数不会改变该分层;次数用完时只关闭 `HOME_NEW_USER_FREE_TRIAL` 的展示。过期会员按 `OLD_NEVER_PAID` 处理;退款后会员权益已回收时同样按未付费处理。 + +### 9. 付费引导展示回执 + +新增接口: + +```http +POST /api/app/payment/guide/impression +``` + +请求: + +```json +{ + "configId": "configId", + "scene": "VIP_CONTENT_UPDATE", + "videoId": "videoId", + "requestId": "uuid" +} +``` + +只有弹窗实际展示成功后调用。 + +回执成功后,同一用户、同一 `configId`、同一 `scene` 不再返回该引导。 + +### 10. 视频免费试看角标 + +所有视频列表对象增加: + +```json +{ + "showFreeTrialBadge": true, + "freeTrialRemaining": 2, + "canUseFreeTrial": true +} +``` + +前端直接根据 `showFreeTrialBadge` 展示角标,不自行组合判断条件。 + +### 11. 评论区Banner + +新增接口: + +```http +GET /api/app/banner/list?scene=COMMENT_TOP +``` + +响应: + +```json +{ + "list": [ + { + "id": "bannerId", + "imageUrl": "https://example.com/banner.gif", + "mediaType": "GIF", + "linkType": "INTERNAL", + "linkValue": "video://detail?id=videoId", + "sort": 100 + } + ] +} +``` + +枚举: + +```text +mediaType: IMAGE | GIF +linkType: INTERNAL | EXTERNAL | NONE +``` + +`COMMENT_TOP` 支持配置多条图片或 GIF,按 `sort` 从大到小返回,前端按返回顺序轮播。 +未启用或不在有效时间内的配置不会返回;没有有效配置时返回空数组。 + +### 12. VIP套餐及A/B + +修改现有接口: + +```http +GET /api/app/vip/product +``` + +现有套餐字段保持不变,响应增加实验信息;每个套餐对象增加 `badgeType`、`badgeText`: + +```json +{ + "experimentId": "vip-card-202607-v1", + "variant": "A", + "defaultProductId": "productId", + "list": [ + { + "showType": 2, + "position": "vip", + "list": [ + { + "productID": "productId", + "productName": "全尊新人卡", + "badgeType": "MOST_POPULAR", + "badgeText": "最受欢迎" + } + ] + } + ], + "integralList": [], + "daichong": {}, + "isNewUser": true +} +``` + +角标枚举: + +```text +MOST_POPULAR 最受欢迎 +NEW_USER_OFFER 新人特惠 +空值或未知值 不展示 +``` + +前端只按套餐对象的 `badgeType` 判断是否展示角标,显示内容使用 `badgeText`;不得通过套餐名称、`sort`、数组位置或默认套餐判断。基础配置与 A/B 配置的合并由后端完成,前端不处理配置优先级。前端下单时原样回传 `experimentId` 和 `variant`。实际价格以该接口和下单结果为准。 + +### 13. VIP卡片统计事件 + +新增接口: + +```http +POST /api/app/analytics/events +``` + +请求: + +```json +{ + "events": [ + { + "eventId": "uuid", + "eventName": "VIP_PRODUCT_IMPRESSION", + "sessionId": "uuid", + "occurredAt": "2026-07-24T08:00:00Z", + "experimentId": "vip-card-202607-v1", + "variant": "A", + "productId": "productId" + } + ] +} +``` + +事件: + +```text +VIP_CARD_PAGE_VIEW +VIP_PRODUCT_IMPRESSION +VIP_CARD_CLOSE_WITHOUT_PURCHASE +``` + +曝光必须在UI实际展示后上报。 + +### 14. 创建订单 + +修改现有接口: + +```http +POST /api/app/mine/topay +``` + +新增非必填字段: + +```json +{ + "sourcePage": "VIDEO_BOTTOM_SHEET", + "sourceRef": "videoId", + "videoId": "videoId", + "activityId": "activityId", + "experimentId": "vip-card-202607-v1", + "experimentVariant": "A", + "sessionId": "uuid" +} +``` + +来源枚举: + +```text +HOME_USER_SEGMENT +VIDEO_BOTTOM_BANNER +VIDEO_BOTTOM_SHEET +VIP_CENTER +H5_ACTIVITY +UNKNOWN +``` + +### 15. 动漫漫画最新列表 + +修改现有接口: + +```http +GET /api/app/media/list?pageNumber=1&pageSize=20 +``` + +媒体对象增加: + +```json +{ + "updateTime": "2026-07-24T08:00:00Z", + "latestPublishedAt": "2026-07-24T08:00:00Z" +} +``` + +前端按后端返回顺序展示,不自行排序。 + +## 二、管理后台接口 + +### 1. 亚模块配置 + +修改现有: + +```text +POST /api/web/admin/module/conf/add +POST /api/web/admin/module/conf/edit +``` + +增加: + +```json +{ + "onlineAt": "2026-07-24T00:00:00Z", + "offlineAt": "2026-08-24T00:00:00Z", + "excludeLatest": true, + "excludeRecommend": false, + "searchOnlyWhenInactive": true, + "haiJiaoStyle": { + "sortRules": [ + { + "val": 2, + "name": "热门推荐", + "refreshMode": "RANDOM_TOP_N", + "randomCandidateN": 30 + } + ] + } +} +``` + +### 2. 付费引导配置 + +```text +GET /api/web/admin/payment-guide/list +POST /api/web/admin/payment-guide/add +POST /api/web/admin/payment-guide/edit +DELETE /api/web/admin/payment-guide/delete?id={id} +``` + +新增/编辑使用完整配置对象,编辑时必须带 `id`: + +```json +{ + "id": "编辑时必传,新增不传", + "scene": "VIDEO_PREVIEW_END", + "segments": ["OLD_NEVER_PAID"], + "style": "BOTTOM_SHEET", + "title": "查看完整影片", + "description": "开通会员后可观看", + "cover": "https://example.com/cover.jpg", + "videoIds": ["videoId"], + "productId": "productId", + "durationSeconds": 7200, + "action": { + "type": "VIP_PRODUCT", + "value": "productId" + }, + "enable": true, + "sort": 100, + "startAt": "2026-07-24T00:00:00Z", + "endAt": "2026-08-24T00:00:00Z" +} +``` + +`startAt/endAt` 可不传;编辑时不传表示清空时间限制。 +`segments` 传空数组表示所有用户分层。已展示过的配置如果要重新触达用户,应新增 +配置生成新的 `configId`,不要直接复用原配置。 + +### 3. 评论Banner配置 + +```text +GET /api/web/admin/banner/list?scene=COMMENT_TOP +POST /api/web/admin/banner/add +POST /api/web/admin/banner/edit +DELETE /api/web/admin/banner/delete?id={id} +``` + +新增/编辑使用完整配置对象,编辑时必须带 `id`: + +```json +{ + "id": "编辑时必传,新增不传", + "scene": "COMMENT_TOP", + "imageUrl": "https://example.com/banner.gif", + "mediaType": "GIF", + "linkType": "INTERNAL", + "linkValue": "video://detail?id=videoId", + "sort": 100, + "enable": true, + "startAt": "2026-07-24T00:00:00Z", + "endAt": "2026-08-24T00:00:00Z" +} +``` + +`COMMENT_TOP` 支持新增多条配置。每条配置独立设置图片/GIF、内外链、排序、启用状态及 +上下架时间;前端按 App 接口返回顺序轮播。 + +### 4. VIP卡片实验 + +```text +GET /api/web/admin/vip-card-experiment/current +POST /api/web/admin/vip-card-experiment/publish +POST /api/web/admin/vip-card-experiment/disable +GET /api/web/admin/vip-card-experiment/statistics?experimentId={id} +``` + +发布请求中的每个变体增加套餐角标配置: + +```json +{ + "variantA": { + "defaultProductId": "productId", + "productIds": { + "1": "第1张卡片的productId", + "2": "第2张卡片的productId" + }, + "productBadges": [ + { + "productId": "productId", + "badgeType": "MOST_POPULAR", + "badgeText": "最受欢迎" + } + ] + } +} +``` + +`productIds` 的 key 是从 `1` 开始且连续的卡片顺序,value 是商品ID;发布和查询接口均使用该结构。 +同一变体内排序号和商品ID都不能重复。App/H5 套餐接口保持旧版本的独立 `position` 分组和外层顺序: +会员卡、预售卡仍分别返回;同一分组内严格按该数字顺序排列,不支持跨 `position` 穿插排序。 +同一变体最多配置一个 `MOST_POPULAR`;`productId` 必须属于该变体的 `productIds`。 + +### 5. 免费试看角标开关 + +复用现有系统配置接口,配置项: + +```text +freeTrialBadgeEnabled = true | false +``` + +### 6. 动漫漫画更新 + +复用现有接口: + +```text +POST /api/web/admin/media/update +POST /api/web/admin/media_content/create +POST /api/web/admin/media_content/update +``` + +列表响应增加: + +```json +{ + "contentUpdateTime": "2026-07-24T08:00:00Z", + "latestPublishedAt": "2026-07-24T08:00:00Z" +} +``` + +### 7. 视频置顶排序 + +复用现有视频编辑接口。`sort` 放开0~99限制,并允许多个视频填写相同值。前端不需要交换或修改其他视频的排序号。 + +## 三、无需新增后端接口 + +- 金币支付半屏:复用现有支付接口。 +- 播放页Banner放大:纯UI。 +- 删除支付温馨提示:纯UI。 +- iOS搜索回车:纯客户端事件。